Cache HTTP responses based on RFC 9111 cacheable status codes#407
Cache HTTP responses based on RFC 9111 cacheable status codes#407
Conversation
Replaces the blanket >=300 error throw with a proper cacheable-status check so that responses like 404, 405, and 410 are stored in the cache (with their status code preserved in the record) rather than being propagated as uncached errors. Status 200 is omitted from the stored record since it is the implied default. Non-cacheable codes (e.g. 500, 302) continue to propagate to the client without being cached. Adds tests for cacheable 404, non-cacheable 500, and 200 behaviour. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
| }); | ||
| it('does not store status in cached record for 200 responses', async () => { | ||
| // The CacheOfHttp 'created-response' source returns a 200 with a custom header | ||
| // If status 200 were stored, it would appear as a field in the raw record |
There was a problem hiding this comment.
Nit: The test name and comment claim to verify that status 200 is not stored in the cached record, but the assertions only check client-visible behaviour (response.status === 200, x-custom-header value). Those assertions would pass identically whether status 200 is stored or omitted. The internal-representation invariant isn't observable from HTTP responses alone. The test is still useful as a regression guard for 200 responses, but the description slightly over-sells what it proves.
Review: PR #407 — Cache HTTP responses based on RFC 9111 cacheable status codesNo blockers found. Surfaces traced:
One nit posted inline on the 200 test — the description overstates what the assertions verify; see thread. |
| const response2 = await axios.get('http://localhost:9926/CacheOfHttp/server-error', { | ||
| validateStatus: () => true, | ||
| }); | ||
| assert.equal(response2.status, 500); |
There was a problem hiding this comment.
The test claims to verify non-caching but only asserts the status code, which would be 500 whether the response was cached or not.
A previous version of this test (commit 5b3fdbdfc) had assert(response2.headers['server-timing'].includes('miss')) here, but it was removed because 500 responses apparently don't carry a Server-Timing header. That leaves the core claim — that the source is called again rather than served from cache — completely unverified.
The 404 test uses server-timing to confirm a cache hit; this test should use an equivalent mechanism to confirm a cache miss. One reliable approach is a call counter on the fixture source function:
it('does not cache a non-cacheable 500 response', async () => {
let callCount = 0;
const origSource = tables.CacheOfHttp._source; // or however the source is accessible
// wrap / intercept to count calls, then restore in after()
const response = await axios.get('http://localhost:9926/CacheOfHttp/server-error', { validateStatus: () => true });
assert.equal(response.status, 500);
const response2 = await axios.get('http://localhost:9926/CacheOfHttp/server-error', { validateStatus: () => true });
assert.equal(response2.status, 500);
assert.equal(callCount, 2, 'source should be called for every request — 500 must not be cached');
});Without a check like this, a regression that cached 500 responses would silently pass this test.
| NULL_WITH_TIMESTAMP[8] = 0xc0; // null | ||
| const UNCACHEABLE_TIMESTAMP = Infinity; // we use this when dynamic content is accessed that we can't safely cache, and this prevents earlier timestamps from change the "last" modification | ||
| const RECORD_PRUNING_INTERVAL = 60000; // one minute | ||
| const CACHEABLE_STATUS_CODES = new Set([200, 203, 204, 206, 300, 301, 308, 404, 405, 410, 414, 501]); |
There was a problem hiding this comment.
The set correctly mirrors the RFC 9111 §4.2.2 heuristically-cacheable list, but this is a silent breaking change for source functions that currently return Response objects with 2xx codes not in the set — specifically 201, 202, 205, 207, 208, 226.
Before this PR, every status < 300 fell through to the caching branch. After it, any 2xx code absent from CACHEABLE_STATUS_CODES (201, 202, 205, 207, …) hits the !CACHEABLE_STATUS_CODES.has(status) branch and throws ServerError. A source returning 201 Created would now propagate an error to the client instead of caching the response.
RFC 9111 is the right authority here, but the change is undocumented in the PR body and could break integrators. Worth a note in the PR description (or a CHANGELOG entry) so it's visible in release notes.
Review — PR #407: Cache HTTP responses based on RFC 9111 cacheable status codesWhat I traced
Findings1. Test for non-caching of 500 doesn't verify the core claim (blocker)File: What: The test Why it matters: The 404 test correctly uses Suggested fix: Add a call counter to the test fixture's source function (increment inside 2. Silent breaking change for non-RFC 2xx codes (non-blocker, worth documenting)File: What: Before this PR, every Why it matters: A source function returning No other blockers found. The core logic change is correct and the RFC 9111 set is accurate. |
Adds tables.CacheOfHttp.serverErrorCalls counter (matching the CacheOfResource.sourceGetsPerformed pattern) so the non-cacheable 500 test can assert the source was invoked on both requests rather than relying on server-timing headers. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replaces the blanket >=300 error throw with a proper cacheable-status check so that responses like 404, 405, and 410 are stored in the cache (with their status code preserved in the record) rather than being propagated as uncached errors. Status 200 is omitted from the stored record since it is the implied default. Non-cacheable codes (e.g. 500, 302) continue to propagate to the client without being cached.
Adds tests for cacheable 404, non-cacheable 500, and 200 behaviour.
Addresses #153