fix: add CORS middleware for cross-origin browser requests #257
No reviewers
Labels
No labels
domain:backend
domain:devops
domain:frontend
status:approved
status:in-progress
status:needs-fix
status:qa
type:bug
type:devops
type:feature
No milestone
No project
No assignees
1 participant
Due date
No due date set.
Dependencies
No dependencies set.
Reference
ldraney/pal-e-api!257
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "256-cors-middleware"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
The API had zero CORS headers, causing every cross-origin fetch from
pal-e-production.tail5b443a.ts.netto be blocked by the browser's same-origin policy. This addsCORSMiddlewareto FastAPI before router includes, with env-var-driven origins.Changes
src/pal_e_docs/config.py-- addedcors_originssetting (env varPALDOCS_CORS_ORIGINS, comma-separated string, defaults tohttps://pal-e-production.tail5b443a.ts.net,http://localhost:5173)src/pal_e_docs/main.py-- addedCORSMiddlewarewithallow_credentials=True, all standard methods, wildcard headers, parsed fromsettings.cors_originstests/test_cors.py-- 7 tests covering allowed/disallowed origins, preflight OPTIONS, credentials header, and no-origin passthroughDesign notes
allow_credentials=Trueis set because the frontend attaches a Keycloak Bearer token viaAuthorizationheader (confirmed insrc/lib/api-client.ts). The frontend does NOT usecredentials: 'include'on fetch, butallow_credentialsis required for the browser to expose response headers when anAuthorizationheader is present.PALDOCS_prefix convention (notPAL_E_DOCS_as suggested in the issue), consistent with all other settings inconfig.py.Originheader is present in the request.Test Plan
ruff formatandruff checkcleancurl -D- -H "Origin: https://pal-e-production.tail5b443a.ts.net" https://pal-e-docs.tail5b443a.ts.net/healthzreturnsaccess-control-allow-originheaderReview Checklist
ruff formatandruff checkpassPALDOCS_prefix conventionRelated Notes
forgejo_admin/pal-e-platform#278(hostname swap will needPALDOCS_CORS_ORIGINSupdate)Related
Closes #256
QA Review -- PR #257
Acceptance Criteria Check (from #256)
CORSMiddlewareadded tomain.pybefore router includes -- line 52, beforeinclude_routercalls at line 60+allow_originsconfigurable via env var --PALDOCS_CORS_ORIGINS(comma-separated), follows existingPALDOCS_prefix conventionhttps://pal-e-production.tail5b443a.ts.netandhttp://localhost:5173allow_methodsincludes GET, POST, PATCH, PUT, DELETE, OPTIONSallow_headers="*"allow_credentials=True-- confirmed frontend uses Bearer token via Authorization header (notcredentials: 'include')tests/test_cors.pywith 7 tests covering: allowed origins, disallowed origins, preflight OPTIONS, all methods, credentials header, no-origin passthroughruff formatandruff checkcleanCode Quality
FastAPI()instantiation, before router includes)strip()andif o.strip()strtype with comma-split at usage site -- simple and appropriate for a flat env varconftest.pyfixtures properly and cover both positive and negative casesclientfixture (duplicatesconftest.py) -- minor but acceptable since it keeps the test self-containedNits
None.
VERDICT: APPROVED
PR #257 Review
DOMAIN REVIEW
Tech stack: Python / FastAPI / Pydantic Settings / CORSMiddleware
config.py --
cors_originsfield added as a comma-separated string with sensible defaults (https://pal-e-production.tail5b443a.ts.net,http://localhost:5173). The env var resolves toPALDOCS_CORS_ORIGINSvia the existingenv_prefix = "PALDOCS_"convention. This is correct -- the PR body explains the deviation from the issue's suggestedPAL_E_DOCS_CORS_ORIGINSname, and the existing config confirmsPALDOCS_is the established prefix. Good call.main.py -- CORSMiddleware is added BEFORE all
app.include_router()calls, which is the correct FastAPI pattern (middleware must wrap routes to intercept preflight OPTIONS before routing). The origin parsing ([o.strip() for o in settings.cors_origins.split(",") if o.strip()]) correctly handles whitespace and empty segments.CORS configuration specifics:
allow_credentials=True-- PR body documents the justification (Keycloak Bearer token via Authorization header). Acceptable.allow_methods=["GET", "POST", "PATCH", "PUT", "DELETE", "OPTIONS"]-- Covers all standard REST methods. Correct.allow_headers=["*"]-- Wildcard headers is standard for APIs that accept Authorization headers. Acceptable.*in origins -- good, explicit allowlist only.OWASP CORS check: Origins are allowlisted (not wildcarded), credentials are scoped to known origins, no reflected-origin pattern. Clean.
test_cors.py -- 7 tests covering:
Test coverage is thorough. Both happy path and negative cases are covered. The disallowed-origin test correctly asserts header absence, not just value mismatch.
BLOCKERS
None.
NITS
Test isolation: The test file imports
appdirectly frommain.py, which means it uses the module-levelsettingssingleton with defaultcors_origins. This works because the defaults match what the tests assert against, but if someone changes the default in config.py, all 7 tests break silently. Consider a comment in the test file noting this coupling, or usingmonkeypatchon the settings object. Non-blocking -- the current approach is simple and correct for the current state.Preflight max-age: No
max_ageparameter is set on the middleware, so browsers will re-send preflight requests on every cross-origin call. Settingmax_age=600(10 minutes) would reduce preflight traffic. Minor optimization, not a correctness issue.SOP COMPLIANCE
256-cors-middlewarefollows{issue-number}-{kebab-case-purpose}PROCESS OBSERVATIONS
VERDICT: APPROVED