Skip to content

feat: add CORS middleware for browser-based clients - #121

Merged
koinsaari merged 1 commit into
mainfrom
feat/cors-middleware
Aug 1, 2026
Merged

feat: add CORS middleware for browser-based clients#121
koinsaari merged 1 commit into
mainfrom
feat/cors-middleware

Conversation

@koinsaari

Copy link
Copy Markdown
Contributor

Summary

  • New internal/middleware.CORS: allows a single configured origin to call the API cross-origin, no-op when CORS_ALLOWED_ORIGIN is unset.
  • Wired into cmd/api's /v1 mux, applied before the OpenAPI validator so preflight OPTIONS requests are handled directly.
  • Non-matching/missing origins are forwarded unmodified — the browser, not the server, withholds the response.

🤖 Generated with Claude Code

@claude

claude Bot commented Aug 1, 2026

Copy link
Copy Markdown

Claude finished @koinsaari's task in 2m 5s —— View job


Review complete ✅

  • Read REVIEW.md and CLAUDE.md for review instructions/context
  • Examine diff and changed files
  • Check against core invariants and review checklist
  • Post final review
  • Set commit status (see note below)

No blocking issues. This is a clean, well-scoped middleware addition.

What it does: internal/middleware.CORS (internal/middleware/cors.go:10) allowlists a single exact origin from CORS_ALLOWED_ORIGIN, no-ops when unset, and is wired in cmd/api/main.go:141 outside the OpenAPI request validator so preflight OPTIONS requests short-circuit before hitting spec/auth validation — reasonable, since preflights carry no auth and don't need schema checks.

Invariant check (per REVIEW.md):

  • Pure data layer / no accessibility scoring — n/a, this touches only transport-level CORS headers.
  • AuditFlags determinism — untouched.
  • Validation-before-a11y ordering — untouched; no write path affected.
  • No HTTP framework introduced — confirmed, stdlib net/http only.
  • SQL/JSONB symmetry — n/a, no persistence code in this diff.

Security: Origin is matched with exact string equality against a server-side configured value before being reflected into Access-Control-Allow-Origin — no wildcard, no reflection of arbitrary client-supplied origins, so no CORS bypass or header-injection risk. Access-Control-Allow-Methods (GET, POST, PATCH, DELETE) matches the methods actually defined in api/openapi.yaml.

Tests: internal/middleware/cors_test.go covers no-origin-configured, no-Origin-header, matching origin, non-matching origin, and preflight-matching-origin — good branch coverage for a small pure function.

🟡 Nits (non-blocking):

  1. Vary: Origin (internal/middleware/cors.go:23) is only set on the matching-origin branch. Since the response does differ based on the Origin header (headers present vs. absent), a shared/proxy cache could store a non-matching-origin response and later serve it to a legitimate matching-origin request, silently dropping the CORS headers. Consider setting Vary: Origin unconditionally, before the match check.
  2. No test exercises a preflight (OPTIONS) request from a non-matching origin — that path currently falls through to next.ServeHTTP untested. Worth a quick case alongside TestCORS_PreflightMatchingOrigin_ShortCircuits.

No blocking issues — approving from a review-feedback standpoint (implementation not requested).

Note: I was unable to run go build/go test locally or set the HEAD commit status (403 Resource not accessible by integration) in this environment — verdict above is based on static review of the diff only. CI should confirm the build/tests pass.

@koinsaari
koinsaari force-pushed the feat/cors-middleware branch from 4ef1a9e to f706b04 Compare August 1, 2026 14:48
@claude

claude Bot commented Aug 1, 2026

Copy link
Copy Markdown

Claude finished @koinsaari's task in 1m 50s —— View job


Review complete ✅

  • Read REVIEW.md and CLAUDE.md for review instructions/context
  • Examine diff and changed files
  • Check against core invariants and review checklist
  • Post final review
  • Set commit status (see note below)

No blocking issues. This is a clean, well-scoped middleware addition.

What it does: internal/middleware.CORS (internal/middleware/cors.go:10) allowlists a single exact origin from CORS_ALLOWED_ORIGIN, no-ops when unset, and is wired in cmd/api/main.go:141 outside the OpenAPI request validator so preflight OPTIONS requests short-circuit before hitting spec/auth validation — reasonable, since preflights carry no auth and don't need schema checks.

Invariant check (per REVIEW.md):

  • Pure data layer / no accessibility scoring — n/a, this touches only transport-level CORS headers.
  • AuditFlags determinism — untouched.
  • Validation-before-a11y ordering — untouched; no write path affected.
  • No HTTP framework introduced — confirmed, stdlib net/http only.
  • SQL/JSONB symmetry — n/a, no persistence code in this diff.

Security: Origin is matched with exact string equality against a server-side configured value before being reflected into Access-Control-Allow-Origin — no wildcard, no reflection of arbitrary client-supplied origins, so no CORS bypass or header-injection risk. Access-Control-Allow-Methods (GET, POST, PATCH, DELETE) matches the methods defined in api/openapi.yaml. No Access-Control-Allow-Credentials header, consistent with X-API-Key (not cookie) auth.

Re-check of the previous review's nits: Both have already been addressed in this exact commit (f706b04):

  1. Vary: Origin (internal/middleware/cors.go:16) is now set unconditionally, before the origin match check — no longer risks a shared cache serving a stale non-matching response to a legitimate origin.
  2. internal/middleware/cors_test.go:95 (TestCORS_PreflightNonMatchingOrigin_PassesThrough) now covers a preflight request from a non-matching origin.

Tests: internal/middleware/cors_test.go covers no-origin-configured, no-Origin-header, matching origin, non-matching origin, preflight-matching (short-circuits with 204 + method/header allowlist), and preflight-non-matching (passes through) — full branch coverage for this handler.

No blocking issues — no new nits to raise.

Notes:

  • I was unable to run go build/go test locally in this environment (tool permission denied) — verdict above is based on static review of the actual committed code. CI should confirm the build/tests pass.
  • I was unable to set the HEAD commit status (403 Resource not accessible by integration) — same limitation as the prior review pass. Verdict: no Important findings, so this would be state=success.

Restricts cross-origin access to a single configured origin via
CORS_ALLOWED_ORIGIN. No-op when unset.
@koinsaari
koinsaari force-pushed the feat/cors-middleware branch from f706b04 to fb34147 Compare August 1, 2026 14:55
@koinsaari
koinsaari merged commit 75a8a57 into main Aug 1, 2026
@koinsaari
koinsaari deleted the feat/cors-middleware branch August 1, 2026 14:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant