fix(public-api): include statusCode in rate-limit errorResponseBuilder#3906
Open
capJavert wants to merge 4 commits into
Open
fix(public-api): include statusCode in rate-limit errorResponseBuilder#3906capJavert wants to merge 4 commits into
capJavert wants to merge 4 commits into
Conversation
@fastify/rate-limit's errorResponseBuilder returns a plain object that is then thrown as the error. Without an explicit statusCode field, the global setErrorHandler (which keys off err.statusCode) treats it as an unknown error and returns 500 instead of 429. Adds 'statusCode: 429' to both IP and per-user errorResponseBuilders. Now exceeding either limit returns a proper 429 with retryAfter and the rate_limit_exceeded error code, matching what the docs promise and what consumers expect. Regression test exercises the per-user limit (61 requests > 60/min cap) and asserts the 429 status + body shape.
|
🍹 The Update (preview) for dailydotdev/api/prod (at 69748ae) was successful. ✨ Neo ExplanationRoutine deployment of a bug fix that corrects how rate-limit errors are surfaced to callers; all resource changes are image tag updates and migration job rotations. ✅ Low RiskThis is a standard application deployment rolling out a new container image (commit Resource Changes Name Type Operation
~ vpc-native-temporal-deployment kubernetes:apps/v1:Deployment update
~ vpc-native-clean-expired-better-auth-sessions-cron kubernetes:batch/v1:CronJob update
~ vpc-native-calculate-top-readers-cron kubernetes:batch/v1:CronJob update
~ vpc-native-user-profile-updated-sync-cron kubernetes:batch/v1:CronJob update
~ vpc-native-check-analytics-report-cron kubernetes:batch/v1:CronJob update
~ vpc-native-personalized-digest-deployment kubernetes:apps/v1:Deployment update
~ vpc-native-expire-super-agent-trial-cron kubernetes:batch/v1:CronJob update
~ vpc-native-ws-deployment kubernetes:apps/v1:Deployment update
~ vpc-native-user-profile-analytics-clickhouse-cron kubernetes:batch/v1:CronJob update
~ vpc-native-worker-job-deployment kubernetes:apps/v1:Deployment update
~ vpc-native-private-deployment kubernetes:apps/v1:Deployment update
~ vpc-native-clean-old-notifications-cron kubernetes:batch/v1:CronJob update
~ vpc-native-update-achievement-rarity-cron kubernetes:batch/v1:CronJob update
~ vpc-native-clean-zombie-user-companies-cron kubernetes:batch/v1:CronJob update
~ vpc-native-rotate-weekly-quests-cron kubernetes:batch/v1:CronJob update
+ vpc-native-api-clickhouse-migration-2b6a4ce6 kubernetes:batch/v1:Job create
~ vpc-native-update-views-cron kubernetes:batch/v1:CronJob update
~ vpc-native-bg-deployment kubernetes:apps/v1:Deployment update
~ vpc-native-daily-digest-cron kubernetes:batch/v1:CronJob update
~ vpc-native-hourly-notification-cron kubernetes:batch/v1:CronJob update
~ vpc-native-rotate-daily-quests-cron kubernetes:batch/v1:CronJob update
~ vpc-native-generate-search-invites-cron kubernetes:batch/v1:CronJob update
- vpc-native-api-db-migration-edf1a05e kubernetes:batch/v1:Job delete
~ vpc-native-materialize-monthly-best-post-archives-cron kubernetes:batch/v1:CronJob update
~ vpc-native-materialize-yearly-best-post-archives-cron kubernetes:batch/v1:CronJob update
~ vpc-native-clean-zombie-images-cron kubernetes:batch/v1:CronJob update
~ vpc-native-validate-active-users-cron kubernetes:batch/v1:CronJob update
~ vpc-native-post-analytics-history-day-clickhouse-cron kubernetes:batch/v1:CronJob update
~ vpc-native-user-posts-analytics-refresh-cron kubernetes:batch/v1:CronJob update
~ vpc-native-clean-channel-highlights-cron kubernetes:batch/v1:CronJob update
~ vpc-native-squad-posts-analytics-refresh-cron kubernetes:batch/v1:CronJob update
~ vpc-native-clean-stale-user-transactions-cron kubernetes:batch/v1:CronJob update
~ vpc-native-clean-zombie-users-cron kubernetes:batch/v1:CronJob update
~ vpc-native-user-profile-analytics-history-clickhouse-cron kubernetes:batch/v1:CronJob update
~ vpc-native-clean-gifted-plus-cron kubernetes:batch/v1:CronJob update
~ vpc-native-channel-digests-cron kubernetes:batch/v1:CronJob update
+ vpc-native-api-db-migration-2b6a4ce6 kubernetes:batch/v1:Job create
~ vpc-native-update-tags-str-cron kubernetes:batch/v1:CronJob update
~ vpc-native-update-tag-materialized-views-cron kubernetes:batch/v1:CronJob update
~ vpc-native-channel-highlights-cron kubernetes:batch/v1:CronJob update
~ vpc-native-clean-zombie-opportunities-cron kubernetes:batch/v1:CronJob update
... and 12 other changes |
The global setErrorHandler rewrites the response body using 'err.name || "Error"', which drops the original 'rate_limit_exceeded' code from @fastify/rate-limit's errorResponseBuilder. Asserting on it fails. Fixing that round-trip is out of scope here — we just verify the 429 status and message string for now.
…dler @fastify/rate-limit's errorResponseBuilder returns a plain object that is thrown verbatim, including an 'error' field naming the failure code (e.g. 'rate_limit_exceeded'). The previous handler discarded it and substituted 'Error' (because the thrown plain object has no .name). Prefer the object's own 'error' field when present, fall back to err.name, then 'Error'. Restores the documented public API response shape for 429s.
…hack @fastify/rate-limit's default errorResponseBuilder returns 'new Error()' with statusCode set (see index.js#L31). Our overrides were returning plain objects which lost both the Error prototype (no .name) and any non-statusCode metadata. Switch both IP and per-user builders to return a real Error with .name = 'rate_limit_exceeded' and .statusCode = 429. The global setErrorHandler then works as-is: err.statusCode preserves 429, err.name preserves the public 'rate_limit_exceeded' code in the body. Reverts the err.error workaround added in the previous commit — no longer needed once builders return proper Errors.
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
Adds
statusCode: 429to botherrorResponseBuildercallbacks insrc/routes/public/index.ts(IP rate limiter and per-user rate limiter).Why
Follow-up to #3899. That PR made the global
setErrorHandlerpreserveerr.statusCode, but rate-limit hits on/public/v1/*are still surfacing as 500 in production.Root cause:
@fastify/rate-limitdoesn't throw aFastifyError. It callserrorResponseBuilder()and throws the returned object verbatim. The current builder returns:No
statusCodefield. The thrown object hastype: 'Object'(a plain object, not anErrorinstance) anderr.statusCode === undefined, so the global handler falls through to the default 500 branch.Verified in observability: every
User rate limit exceeded. Please slow down.log line lines up with aresponse_status_code=500trace at the same millisecond.Fix
Same change applied to the IP rate limiter.
Tests
Regression test in
__tests__/routes/public/rateLimit.tsthat fires 61 requests through the per-user limiter (cap is 60/min) and asserts:429(not 500){ statusCode: 429, error: 'rate_limit_exceeded', message: ... }Fails on
main, passes on this branch.Impact
Retry-Afterinstead of an opaque 500./public/v15xx metrics stop being polluted with rate-limit hits, so we can actually alert on real server errors.