fix: answer CORS preflight in preview + prod API Gateway#277
Merged
Conversation
The frontend sends Content-Type: application/json on every request, so even GETs are non-simple and trigger a browser preflight. The preview frontend (shared CloudFront) and preview API are cross-origin; the API resources only defined GET/POST, so preflight OPTIONS hit no method and API Gateway returned a 403 with no CORS headers -> opaque 'CORS error' in the browser. - Add OPTIONS to every preview API resource, routed to the proxy lambda (which already returns 200 + Access-Control-Allow-Origin: * for OPTIONS). - Add CORS headers to DEFAULT_4XX/5XX gateway responses so real errors (lambda 502s, unmatched routes) surface with their status instead of as CORS errors. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Same missing-OPTIONS issue as the preview API: the frontend sends Content-Type: application/json on every request, so cross-origin GETs preflight, and the prod API resources defined only GET/POST -> preflight 403 with no CORS headers. - Append OPTIONS to every prod resource, routed to the proxy lambda. - CORS headers on DEFAULT_4XX/5XX gateway responses. - Add a redeploy trigger to the deployment (it had none) so the new OPTIONS methods actually reach the prod stage. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Auto-formatted .tf files with terraform fmt - Updated README.md with terraform-docs Co-authored-by: nourshoreibah <nourshoreibah@users.noreply.github.com>
Contributor
Terraform Plan 📖
|
nourshoreibah
added a commit
that referenced
this pull request
Jul 1, 2026
) * fix: bundle lambda deps + add {proxy+} routing (fixes 502/CORS outage) Every lambda returned 502 InternalServerErrorException on every request (including OPTIONS preflight), which the browser surfaced as an opaque "CORS error" on all routes in both prod and preview. Root cause: the `package` script was `tsc && zip dist` — tsc only transpiles, it does not bundle dependencies, so the deploy zip contained the compiled handler but no node_modules. At cold start `require('kysely')` / `require('pg')` / etc. failed with Runtime.ImportModuleError, so the function crashed at init — before the handler's OPTIONS short-circuit ran. A 502 preflight fails the browser's CORS check regardless of the CORS headers added in #277. Fixes, in one PR: Bundling - package script now bundles with esbuild into a single self-contained handler.js (deps inlined). Verified the bundle loads and runs with no node_modules present. - reports keeps its Roboto TTFs: esbuild can't inline the font files pdfmake reads at render time, so the package step copies them to dist/fonts/Roboto and report-service resolves FONT_DIR from __dirname (falls back to node_modules in local dev). Verified PDF generation works from the bundle with only the shipped fonts. - moved `jest` from dependencies to devDependencies (it was wrongly a runtime dep) and added `esbuild` as a devDependency; lockfiles regenerated so `npm ci` stays in sync. Routing - API Gateway only had single-level resources (/auth, /donors, ...), so sub-paths like /auth/login and /projects/{id} matched no method and returned 403 — never reaching the lambda. Added a {proxy+} greedy child per service with an ANY method (covers OPTIONS preflight too), in both prod and preview, and bumped the deployment redeploy trigger. - handlers strip their own /service mount prefix from rawPath so the existing route table (written for bare paths like /login, /{id}) matches; no-op for local dev where paths are already bare. - added the missing OPTIONS short-circuit to the reports and users handlers (the other four already had it) so preflight returns 200. Verified: all 6 bundles build; OPTIONS=200 on every service; POST /auth/login reaches the login handler (400, not 404); auth unit tests pass; terraform validate passes for both workspaces. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore: auto-format terraform and update documentation - Auto-formatted .tf files with terraform fmt - Updated README.md with terraform-docs Co-authored-by: nourshoreibah <nourshoreibah@users.noreply.github.com> * chore: regenerate lambda READMEs --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Bug
Cross-origin API calls fail with a CORS error (e.g.
GET .../prod/expendituresfrom the CloudFront frontend). Hit in a preview environment; the same latent bug exists in prod.Cause
apps/frontend/src/lib/api.tssendsContent-Type: application/jsonon every request → even GETs are non-simple → the browser fires a preflightOPTIONS. Frontend (CloudFront) and API (*.execute-api) are cross-origin, but the API resources only definedGET/POST— noOPTIONS. API Gateway answers the preflight403 Missing Authentication Tokenwith noAccess-Control-Allow-Origin, so the browser blocks it. The lambda's ownAccess-Control-Allow-Origin: *never applies because the preflight never reaches it.Fix — both
infrastructure/preview/api_gateway.tfandinfrastructure/aws/api_gateway.tfOPTIONSto every resource, routed to the proxy lambda — each handler short-circuitsOPTIONSwith200+Access-Control-Allow-Origin: *.DEFAULT_4XX/DEFAULT_5XXgateway responses so lambda 502s / unmatched routes surface with their real status instead of as CORS errors.triggers, so a method-set change wouldn't redeploy the stage — addedtriggers = { redeploy = sha1(jsonencode(local.lambda_methods)) }(preview already had one).Both modules
terraform validateclean.Apply / rollout
terraform-apply(redeploys theprodstage with the OPTIONS methods).test-environmentlabel.