fix: honor stale-if-error on connection errors during revalidation#5513
Open
jeswr wants to merge 2 commits into
Open
fix: honor stale-if-error on connection errors during revalidation#5513jeswr wants to merge 2 commits into
jeswr wants to merge 2 commits into
Conversation
stale-if-error was only honored when the origin responded with a 500-504 status code; if the origin was unreachable (e.g. ECONNREFUSED), CacheRevalidationHandler.onResponseError failed the request outright even when a stale entry within the stale-if-error window existed. RFC 5861 section 4 defines an error as "any situation that would result in a 500, 502, 503 or 504 HTTP response status code being returned" - for a client-side cache, failing to reach the origin is exactly the situation a gateway would surface as 502/504. Repro (undici main, Node 22): store a response with 'Cache-Control: max-age=1, stale-if-error=60', stop the origin, wait past freshness, request again. - Before: throws ECONNREFUSED - After: stale cached response served (and the error still propagates once outside the stale-if-error window) The interceptor already computes the within-threshold decision (withinStaleIfErrorThreshold) and passes it to the handler; this change uses it on the error path too, when no response has started, invoking the callback with success so the stale entry is served. The mnot cache-tests conformance check 'stale-sie-close' now answers yes in all environments (previously no); no required tests regressed. Fixes nodejs#5507 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #5513 +/- ##
=======================================
Coverage 93.44% 93.44%
=======================================
Files 110 110
Lines 37328 37338 +10
=======================================
+ Hits 34881 34891 +10
Misses 2447 2447 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
7 tasks
Addresses review feedback by trimming the stale-if-error comments to terse one-liners in line with the surrounding interceptor style; comment-only, no logic change. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2fff5f1 to
52c2709
Compare
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.
Fixes #5507
What
stale-if-error(response and request directive) was only honored when the origin responded with a 500-504 status code. If the origin was unreachable (connection refused, reset, etc.),CacheRevalidationHandler.onResponseErrorfailed the request outright even when a stale entry within thestale-if-errorwindow existed.RFC 5861 §4 defines an error as "any situation that would result in a 500, 502, 503 or 504 HTTP response status code being returned" — for a client-side cache, failing to reach the origin is exactly the situation a gateway would surface as 502/504, and resilience across origin outages is the extension's headline use case (nginx
proxy_cache_use_stale error, Varnish and Fastly all treat connect errors this way).Fix
The interceptor already computes the within-threshold decision (
withinStaleIfErrorThresholdinlib/interceptor/cache.js) and passes it to the revalidation handler asallowErrorStatusCodes. This change also uses it on the error path: inonResponseError, when the response hasn't started yet (the callback is still pending) and we're within the threshold, invoke the callback with success so the stale entry is served. Errors after a response has started, or outside the window, propagate exactly as before.Repro / evidence
Store a response with
Cache-Control: max-age=1, stale-if-error=60; stop the origin; wait past freshness; request again (undici main @ cb4c2f1, Node 22):ECONNREFUSED(the equivalent scenario with the origin returning 503 already served stale, making the inconsistency surprising)stale-if-errorwindow the connection error is propagated againAdded tests for both the response directive and the request directive variants in
test/interceptors/cache.js("stale-if-error (response) on connection error", "stale-if-error on connection error"); both fail on main and pass with this change.Cache test suite (
test/interceptors/cache*.js,test/cache-interceptor/*): 123 pass / 0 fail (baseline 121 pass + the 2 new tests). mnot cache-tests conformance runner (node test/cache-interceptor/cache-tests.mjs): thestale-sie-closecheck ("Does HTTP cache serve stale stored response when server sendsCache-Control: stale-if-errorand subsequently closes the connection?") now answers yes in all environments (previously no) — summary went from 220 passed / 58 failed-optional to 222 passed / 56 failed-optional, with 0 required failures and no regressions. No skip-list entries relate to this path (stale-close-must-revalidate/stale-close-no-cachedepend on plain-close behavior withoutstale-if-error, which is unchanged).Notes
jeswr:fix/cache-must-revalidate-max-stale,jeswr:fix/cache-if-modified-since-value.🤖 Generated with Claude Code