fix: return accurate error messages for API client errors - #141
fix: return accurate error messages for API client errors#141kawacukennedy wants to merge 2 commits into
Conversation
The global error handler always sent { error: 'Internal Server Error' },
even for 4xx statuses. A validation failure on /geocode (missing query),
a rejected contact form body, or any thrown 400 was reported to the client
as a server fault, which is misleading and hides the real cause.
- Client errors (4xx) now surface the error message; 5xx stays generic to
avoid leaking internals.
- Extract app construction into buildApp() in app.ts so the server can be
exercised via inject() without binding a port.
- Add route-level tests covering healthcheck, 404s, validation errors,
the local ZIP-code short circuit, upstream failures, and the 4xx/5xx
error-handler split.
- Run the API test suite in CI before deploying.
anthony-maio
left a comment
There was a problem hiding this comment.
Thanks for the review on #143 — happy to return the favor. I verified this locally: fetched the branch, bun install, bun test → 13 pass / 0 fail on bun 1.4.0.
The server.ts → app.ts extraction is faithful — routes, schemas, CORS, hooks, and telemetry all moved verbatim. The only behavior change is the intended one in the error handler.
I specifically checked the leak concern: all four service clients (Nominatim, Zammad, Turnstile, Github) throw plain Errors without a statusCode, so upstream failures still surface as generic 500s. Only Fastify validation errors and explicitly-set 4xx statuses get their message passed through. Good.
One non-blocking suggestion: deflock-api-deploy.yml only triggers on push to master, so these tests gate deploys but never run on PRs. Consider adding a pull_request trigger with the same api/** path filter (or a separate CI workflow) so failures show up during review instead of at deploy time.
The inject() pattern here is a nice template for covering the new routes in #138 too.
Previously the test job only ran on push to master, so failures surfaced only at deploy time. Run the API test suite on pull_request (path-filtered to api/**) so regressions show up during review. The deploy job now depends on test and is gated to push-to-master only, so fork PRs (which lack deploy secrets) run tests without attempting to deploy unreviewed code. Addresses review feedback on FoggedLens#141.
|
Good suggestion — you're right that the test job only gating on Changes to
Also reflected your note about #138 — the Thanks again for the detailed review — the leak-check on the four service clients validating that upstream failures stay generic 500s is exactly the kind of confirmation that's valuable. |
📋 Description
The API's global error handler in
server.tsreturned{ error: 'Internal Server Error' }for every error, regardless of status. That means a400from request validation (e.g./geocodecalled without the requiredqueryparam) or a thrown client error was reported to the caller as a server fault — misleading, and it hides the real cause from the frontend and from anyone debugging against the API.This PR:
4xxresponses surface the actual error message, while5xxresponses stay generic (Internal Server Error) to avoid leaking internals.api/app.ts(buildApp()), the idiomatic Fastify pattern, so the server can be exercised viaserver.inject()without binding a port.server.tsis now a thin entrypoint and behaves identically (verified live).api/app.test.ts).🎯 Type of Change
🧪 Testing
Route-level tests using
app.inject():/healthcheck(HEAD) → 200/geocodewithoutquery→ 400 with a useful message (was "Internal Server Error")/geocode?query=90210→ 200 from the local ZIP dataset, no Nominatim call (stubbed fetch asserts it's never hit)/geocode/multi?query=10001→ 200 arrayInternal Server ErrorAlso verified against a live server (
bun server.ts): healthcheck, ZIP geocode, 400 validation message, and 404 body all respond correctly.✅ Checklist
🔍 Additional Notes
Behavior is otherwise unchanged — all routes, schemas, CORS, caching headers, telemetry, and the healthcheck route are identical to before, just relocated into
buildApp(). This also makes the API easier to test for future contributors (e.g. new routes in #138 can be covered with the sameinject()pattern).