fix: bundle lambda deps + {proxy+} routing (fixes 502/CORS outage)#279
Merged
Conversation
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>
- 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 7, 2026
Reconcile the shared-dispatcher routing refactor with main's CORS fixes (#277/#279), S3 report uploads, and static-export frontend. Resolution: - Handlers keep the @branch/lambda-http dispatch() architecture. Re-applied main's functional deltas on top: projects PUT uses ProjectValidationUtils .buildUpdateValues (+ JSON guard, reject empty); reports gains POST /reports/generate, GET /reports/upload-url (S3 presign), and POST /reports (record uploaded file), and saveReportRecord now passes title. - api_gateway.tf keeps the branch ANY-on-root + {proxy+} model; ported main's aws_api_gateway_gateway_response.cors so APIGW 4XX/5XX errors carry CORS headers. Dropped main's duplicated proxy resources left by the auto-merge. - next.config.ts takes main's static-export config (dev rewrites obsolete; apiFetch targets service ports / NEXT_PUBLIC_API_BASE_URL directly). - Lambda build scripts keep cjs + --external:@aws-sdk/*; reports retains main's pdfmake font-copy step. - Regenerated lambda package-lock.json files. Verified: all 6 lambdas + frontend typecheck clean; reports unit 44/44, projects unit + dashboard unit pass; terraform fmt + validate succeed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.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.
What was broken
Every lambda returned 502
InternalServerErrorExceptionon every request — including theOPTIONSpreflight — which the browser reported as an opaque "CORS error" on all routes, in both prod and preview.Root cause
The
packagescript wastsc && zip dist.tsconly transpiles; it does not bundle dependencies, so the deploy zip had the compiled handler but nonode_modules. At cold startrequire('kysely')/require('pg')/ etc. threwRuntime.ImportModuleErrorand the function crashed at init — before the handler'sOPTIONSshort-circuit could run. A 502 preflight fails the browser CORS check regardless of the CORS headers added in #277, so the fix there couldn't help.Diagnosis was against live prod:
curlshowedHTTP/2 502+x-amzn-errortype: InternalServerErrorExceptiononOPTIONSandGETfor every service resource.Fixes (both in one PR)
Bundling
packagenow bundles with esbuild into a single self-containedhandler.js(deps inlined). Verified it loads and runs with nonode_modulespresent.dist/fonts/Robotoandreport-serviceresolvesFONT_DIRfrom__dirname(falls back tonode_modulesin local dev). Verified PDF generation works from the bundle with only the shipped fonts.jestfromdependencies→devDependencies(was wrongly a runtime dep); addedesbuildas a devDependency; lockfiles regenerated sonpm cistays in sync.Routing
/auth,/donors, …), so sub-paths like/auth/loginand/projects/{id}matched no method → 403, never reaching the lambda. Added a{proxy+}greedy child per service with anANYmethod (coversOPTIONStoo), in prod + preview, and bumped the deployment redeploy trigger./servicemount prefix fromrawPathso the existing route table (written for bare paths like/login,/{id}) matches. No-op in local dev where paths are already bare.OPTIONSshort-circuit to the reports and users handlers (the other four already had it) so preflight returns 200.Verification
OPTIONS=200on every service (was 404 for reports/users).POST /auth/login(empty body) →400(reaches the login handler), not 404 — proves prefix strip end-to-end.terraform validatepasses for bothinfrastructure/awsandinfrastructure/preview.🤖 Generated with Claude Code