Skip to content

fix(errors): preserve fastify statusCode in global error handler#3899

Merged
capJavert merged 4 commits into
mainfrom
fix/error-handler-preserve-status
May 22, 2026
Merged

fix(errors): preserve fastify statusCode in global error handler#3899
capJavert merged 4 commits into
mainfrom
fix/error-handler-preserve-status

Conversation

@capJavert
Copy link
Copy Markdown
Contributor

@capJavert capJavert commented May 21, 2026

What

Makes the global setErrorHandler in src/index.ts preserve fastify's statusCode for client-side errors instead of unconditionally returning 500.

Why

Investigating /public/v1/search/{posts,tags} 5xx during routine monitoring, every 500 I dug into turned out to be a non-500 in disguise. Two patterns observed:

1. Rate-limit hits reported as 500

fastify-rate-limit throws a FastifyError with statusCode: 429. The current handler ignores it and writes 500. Log line on each occurrence:

User rate limit exceeded. Please slow down.

Trace response_status_code matches log timestamp 1:1, but reports 500.

2. Schema validation errors reported as 500

Requests with malformed querystrings (e.g. q[query]=... instead of q=..., or missing required q) raise an FST_ERR_VALIDATION with statusCode: 400:

querystring must have required property 'q'

Same handler, same 500.

Net: /public/v1 5xx metrics are polluted with non-server errors, and legitimate 429s (which we want to track for abuse) are invisible.

Change

app.setErrorHandler((err: FastifyError, req, res) => {
  const statusCode =
    typeof err.statusCode === 'number' && err.statusCode >= 400 && err.statusCode < 600
      ? err.statusCode
      : 500;

  if (statusCode >= 500) {
    req.log.error({ err }, err.message);
  } else {
    req.log.warn({ err }, err.message);
  }

  res.code(statusCode).send({
    statusCode,
    error: statusCode === 500 ? 'Internal Server Error' : err.name || 'Error',
    message: statusCode === 500 ? undefined : err.message,
  });
});
  • Preserves any 4xx/5xx statusCode set by fastify or plugins.
  • Downgrades the log line from ERROR to WARN for 4xx so client mistakes don't trip noise alerts.
  • Keeps the bare { statusCode: 500, error: 'Internal Server Error' } body for true 5xx (no message leak).

Tests

Two new regression tests on /public/v1/search/posts:

  1. Missing required q parameter → 400 (was 500).
  2. Unknown time enum value → 400 (was 500).

Both fail on main and pass on this branch.

Risk

  • Body shape change for non-500 errors: previously every error was { statusCode: 500, error: 'Internal Server Error' }. Now 4xx errors include the original err.message. This is a small response-shape change but matches what fastify would emit by default and what the public API's own auth and rate-limit handlers already emit.
  • No change for 500s: same body, same status code, same log level.

The global setErrorHandler in src/index.ts was unconditionally coercing
every uncaught error to 500. That hid two real signals on /public/v1
during the daily.dev hackathon:

- Rate-limit hits (fastify-rate-limit throws a FastifyError with
  statusCode 429) → reported as 500
- Schema validation errors (FST_ERR_VALIDATION with statusCode 400) →
  reported as 500

Now we preserve any 4xx/5xx statusCode set by fastify or plugins,
default to 500 otherwise. Also downgrades the log line from ERROR to
WARN for 4xx so client mistakes don't trip noise alerts.

Adds two regression tests under /public/v1/search/posts (missing required
q parameter and invalid time enum).
@pulumi
Copy link
Copy Markdown

pulumi Bot commented May 21, 2026

🍹 The Update (preview) for dailydotdev/api/prod (at ec9913e) was successful.

✨ Neo Explanation

Routine deployment of a bug fix that corrects HTTP error response codes (500 → 400 for validation errors), with standard image rollout across all workloads and migration jobs. ✅ Low Risk

This is a routine application deployment rolling out commit ee2d5dc6 (fixing Fastify's error handler to return proper 4xx status codes for schema validation errors instead of always returning 500). All deployments and cron jobs are updated solely to reflect the new image tag and version label.

The two migration Jobs (DB and Clickhouse) are being cycled as expected — the old commit-suffixed Jobs are deleted and new ones for the current commit are created. This is the standard migration pattern for this stack.

Resource Changes

    Name                                                       Type                           Operation
~   vpc-native-update-tag-materialized-views-cron              kubernetes:batch/v1:CronJob    update
~   vpc-native-calculate-top-readers-cron                      kubernetes:batch/v1:CronJob    update
~   vpc-native-clean-stale-user-transactions-cron              kubernetes:batch/v1:CronJob    update
~   vpc-native-update-source-public-threshold-cron             kubernetes:batch/v1:CronJob    update
~   vpc-native-user-profile-analytics-clickhouse-cron          kubernetes:batch/v1:CronJob    update
~   vpc-native-clean-gifted-plus-cron                          kubernetes:batch/v1:CronJob    update
~   vpc-native-daily-digest-cron                               kubernetes:batch/v1:CronJob    update
~   vpc-native-update-highlighted-views-cron                   kubernetes:batch/v1:CronJob    update
~   vpc-native-update-trending-cron                            kubernetes:batch/v1:CronJob    update
~   vpc-native-private-deployment                              kubernetes:apps/v1:Deployment  update
~   vpc-native-clean-zombie-opportunities-cron                 kubernetes:batch/v1:CronJob    update
~   vpc-native-update-current-streak-cron                      kubernetes:batch/v1:CronJob    update
~   vpc-native-sync-subscription-with-cio-cron                 kubernetes:batch/v1:CronJob    update
~   vpc-native-user-posts-analytics-refresh-cron               kubernetes:batch/v1:CronJob    update
~   vpc-native-channel-digests-cron                            kubernetes:batch/v1:CronJob    update
~   vpc-native-post-analytics-history-day-clickhouse-cron      kubernetes:batch/v1:CronJob    update
~   vpc-native-expire-super-agent-trial-cron                   kubernetes:batch/v1:CronJob    update
~   vpc-native-channel-highlights-cron                         kubernetes:batch/v1:CronJob    update
~   vpc-native-clean-zombie-images-cron                        kubernetes:batch/v1:CronJob    update
~   vpc-native-clean-zombie-users-cron                         kubernetes:batch/v1:CronJob    update
~   vpc-native-worker-job-deployment                           kubernetes:apps/v1:Deployment  update
~   vpc-native-update-tags-str-cron                            kubernetes:batch/v1:CronJob    update
~   vpc-native-ws-deployment                                   kubernetes:apps/v1:Deployment  update
-   vpc-native-api-clickhouse-migration-1c932a10               kubernetes:batch/v1:Job        delete
-   vpc-native-api-db-migration-1c932a10                       kubernetes:batch/v1:Job        delete
~   vpc-native-update-achievement-rarity-cron                  kubernetes:batch/v1:CronJob    update
~   vpc-native-clean-channel-highlights-cron                   kubernetes:batch/v1:CronJob    update
~   vpc-native-bg-deployment                                   kubernetes:apps/v1:Deployment  update
~   vpc-native-clean-zombie-user-companies-cron                kubernetes:batch/v1:CronJob    update
~   vpc-native-clean-expired-better-auth-sessions-cron         kubernetes:batch/v1:CronJob    update
~   vpc-native-temporal-deployment                             kubernetes:apps/v1:Deployment  update
~   vpc-native-clean-old-notifications-cron                    kubernetes:batch/v1:CronJob    update
+   vpc-native-api-db-migration-ee2d5dc6                       kubernetes:batch/v1:Job        create
~   vpc-native-squad-posts-analytics-refresh-cron              kubernetes:batch/v1:CronJob    update
~   vpc-native-rotate-daily-quests-cron                        kubernetes:batch/v1:CronJob    update
~   vpc-native-hourly-notification-cron                        kubernetes:batch/v1:CronJob    update
~   vpc-native-materialize-monthly-best-post-archives-cron     kubernetes:batch/v1:CronJob    update
+   vpc-native-api-clickhouse-migration-ee2d5dc6               kubernetes:batch/v1:Job        create
~   vpc-native-user-profile-analytics-history-clickhouse-cron  kubernetes:batch/v1:CronJob    update
~   vpc-native-post-analytics-clickhouse-cron                  kubernetes:batch/v1:CronJob    update
~   vpc-native-personalized-digest-deployment                  kubernetes:apps/v1:Deployment  update
... and 12 other changes

capJavert and others added 3 commits May 22, 2026 11:55
Global error handler now preserves fastify statusCode, so validation
errors surface as 400 instead of being clobbered to 500.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@capJavert capJavert merged commit 97c615f into main May 22, 2026
9 checks passed
@capJavert capJavert deleted the fix/error-handler-preserve-status branch May 22, 2026 10:42
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