tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

commit 273a954c9a4a1a283fb336f140ec86500df6a0cd
parent 802c6f88c0d70f12baa719be3000bd72827db6e9
Author: acreskeyMoz <acreskey@mozilla.com>
Date:   Mon, 24 Nov 2025 14:01:02 +0000

Bug 1958245 - Firefox not caching 303 redirects r=necko-reviewers,valentin

Per updated RFCs, we will cache resources from 303 redirects for performance reasons, provided there are explicit cache directives.

https://www.rfc-editor.org/rfc/rfc9110.html#name-303-see-other

Differential Revision: https://phabricator.services.mozilla.com/D272071

Diffstat:
Mnetwerk/protocol/http/nsHttpChannel.cpp | 17++++++++++++++++-
Mnetwerk/protocol/http/nsHttpResponseHead.cpp | 13+++++++------
Anetwerk/test/unit/test_303_redirect_caching.js | 180+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mnetwerk/test/unit/xpcshell.toml | 2++
4 files changed, 205 insertions(+), 7 deletions(-)

diff --git a/netwerk/protocol/http/nsHttpChannel.cpp b/netwerk/protocol/http/nsHttpChannel.cpp @@ -3225,12 +3225,27 @@ nsresult nsHttpChannel::ContinueProcessResponse3(nsresult rv) { break; case 301: case 302: + case 303: case 307: case 308: - case 303: #if 0 case 305: // disabled as a security measure (see bug 187996). #endif + // RFC 9110: 303 responses are cacheable, but only with explicit + // freshness information (max-age or Expires). Without these directives, + // 303 should not be cached. + if (httpStatus == 303) { + uint32_t freshnessLifetime = 0; + bool hasFreshness = + (NS_SUCCEEDED( + mResponseHead->ComputeFreshnessLifetime(&freshnessLifetime)) && + freshnessLifetime > 0) || + mResponseHead->HasHeader(nsHttp::Expires); + if (mResponseHead->NoStore() || mResponseHead->NoCache() || + !hasFreshness) { + CloseCacheEntry(false); + } + } // don't store the response body for redirects MaybeInvalidateCacheEntryForSubsequentGet(); PushRedirectAsyncFunc(&nsHttpChannel::ContinueProcessResponse4); diff --git a/netwerk/protocol/http/nsHttpResponseHead.cpp b/netwerk/protocol/http/nsHttpResponseHead.cpp @@ -614,12 +614,13 @@ nsresult nsHttpResponseHead::ComputeFreshnessLifetime(uint32_t* result) { return NS_OK; } - // From RFC 7234 Section 4.2.2, heuristics can only be used on responses - // without explicit freshness whose status codes are defined as cacheable - // by default, and those responses without explicit freshness that have been - // marked as explicitly cacheable. + // From RFC 9111 Section 4.2.2 + // (https://www.rfc-editor.org/rfc/rfc9111.html#name-calculating-heuristic-fresh), + // heuristics can only be used on responses without explicit freshness whose + // status codes are defined as cacheable by default, and those responses + // without explicit freshness that have been marked as explicitly cacheable. // Note that |MustValidate| handled most of non-cacheable status codes. - if ((mStatus == 302 || mStatus == 304 || mStatus == 307) && + if ((mStatus == 302 || mStatus == 303 || mStatus == 304 || mStatus == 307) && !mCacheControlPublic && !mCacheControlPrivate) { LOG(( "nsHttpResponseHead::ComputeFreshnessLifetime [this = %p] " @@ -666,6 +667,7 @@ bool nsHttpResponseHead::MustValidate() { case 300: case 301: case 302: + case 303: case 304: case 307: case 308: @@ -673,7 +675,6 @@ bool nsHttpResponseHead::MustValidate() { case 410: break; // Uncacheable redirects - case 303: case 305: // Other known errors case 401: diff --git a/netwerk/test/unit/test_303_redirect_caching.js b/netwerk/test/unit/test_303_redirect_caching.js @@ -0,0 +1,180 @@ +// ABOUTME: Tests HTTP 303 redirect caching behavior with various cache headers +// ABOUTME: per RFC 9110 which allows caching 303 with explicit freshness info +"use strict"; + +const { HttpServer } = ChromeUtils.importESModule( + "resource://testing-common/httpd.sys.mjs" +); + +let httpserver = null; + +function make_channel(url) { + return NetUtil.newChannel({ uri: url, loadUsingSystemPrincipal: true }); +} + +const responseBody = "response body"; + +function contentHandler(metadata, response) { + response.setHeader("Content-Type", "text/plain"); + response.bodyOutputStream.write(responseBody, responseBody.length); +} + +function setup_test_server() { + httpserver = new HttpServer(); + httpserver.registerPathHandler("/content", contentHandler); + httpserver.start(-1); +} + +registerCleanupFunction(async () => { + if (httpserver) { + await new Promise(resolve => httpserver.stop(resolve)); + } +}); + +function get_url(path) { + return `http://localhost:${httpserver.identity.primaryPort}${path}`; +} + +// Test 1: 303 with Cache-Control: max-age should be cached +add_task(async function test_303_with_max_age() { + setup_test_server(); + + let requestCount = 0; + httpserver.registerPathHandler("/redirect1", (metadata, response) => { + requestCount++; + response.setStatusLine(metadata.httpVersion, 303, "See Other"); + response.setHeader("Location", get_url("/content"), false); + response.setHeader("Cache-Control", "max-age=3600", false); + }); + + // First request + let chan = make_channel(get_url("/redirect1")); + let buffer = await new Promise(resolve => { + chan.asyncOpen(new ChannelListener((request, buf) => resolve(buf), null)); + }); + Assert.equal(buffer, responseBody); + Assert.equal(requestCount, 1, "First request should hit server"); + + // Second request - should use cached redirect + chan = make_channel(get_url("/redirect1")); + buffer = await new Promise(resolve => { + chan.asyncOpen(new ChannelListener((request, buf) => resolve(buf), null)); + }); + Assert.equal(buffer, responseBody); + Assert.equal(requestCount, 1, "Second request should use cached redirect"); +}); + +// Test 2: 303 with Expires header should be cached +add_task(async function test_303_with_expires() { + let requestCount = 0; + httpserver.registerPathHandler("/redirect2", (metadata, response) => { + requestCount++; + response.setStatusLine(metadata.httpVersion, 303, "See Other"); + response.setHeader("Location", get_url("/content"), false); + // Set Expires to 1 hour in the future + let expires = new Date(); + expires.setHours(expires.getHours() + 1); + response.setHeader("Expires", expires.toUTCString(), false); + }); + + // First request + let chan = make_channel(get_url("/redirect2")); + let buffer = await new Promise(resolve => { + chan.asyncOpen(new ChannelListener((request, buf) => resolve(buf), null)); + }); + Assert.equal(buffer, responseBody); + Assert.equal(requestCount, 1, "First request should hit server"); + + // Second request - should use cached redirect + chan = make_channel(get_url("/redirect2")); + buffer = await new Promise(resolve => { + chan.asyncOpen(new ChannelListener((request, buf) => resolve(buf), null)); + }); + Assert.equal(buffer, responseBody); + Assert.equal(requestCount, 1, "Second request should use cached redirect"); +}); + +// Test 3: 303 without cache headers should NOT be cached +add_task(async function test_303_without_cache_headers() { + let requestCount = 0; + httpserver.registerPathHandler("/redirect3", (metadata, response) => { + requestCount++; + response.setStatusLine(metadata.httpVersion, 303, "See Other"); + response.setHeader("Location", get_url("/content"), false); + // No Cache-Control or Expires headers + }); + + // First request + let chan = make_channel(get_url("/redirect3")); + let buffer = await new Promise(resolve => { + chan.asyncOpen(new ChannelListener((request, buf) => resolve(buf), null)); + }); + Assert.equal(buffer, responseBody); + Assert.equal(requestCount, 1, "First request should hit server"); + + // Second request - should hit server again (not cached) + chan = make_channel(get_url("/redirect3")); + buffer = await new Promise(resolve => { + chan.asyncOpen(new ChannelListener((request, buf) => resolve(buf), null)); + }); + Assert.equal(buffer, responseBody); + Assert.equal( + requestCount, + 2, + "Second request should hit server (not cached)" + ); +}); + +// Test 4: 303 with Cache-Control: no-store should NOT be cached +add_task(async function test_303_with_no_store() { + let requestCount = 0; + httpserver.registerPathHandler("/redirect4", (metadata, response) => { + requestCount++; + response.setStatusLine(metadata.httpVersion, 303, "See Other"); + response.setHeader("Location", get_url("/content"), false); + response.setHeader("Cache-Control", "no-store", false); + }); + + // First request + let chan = make_channel(get_url("/redirect4")); + let buffer = await new Promise(resolve => { + chan.asyncOpen(new ChannelListener((request, buf) => resolve(buf), null)); + }); + Assert.equal(buffer, responseBody); + Assert.equal(requestCount, 1, "First request should hit server"); + + // Second request - should hit server again due to no-store + chan = make_channel(get_url("/redirect4")); + buffer = await new Promise(resolve => { + chan.asyncOpen(new ChannelListener((request, buf) => resolve(buf), null)); + }); + Assert.equal(buffer, responseBody); + Assert.equal(requestCount, 2, "Second request should hit server (no-store)"); +}); + +// Test 5: 303 with Cache-Control: no-cache should require revalidation +add_task(async function test_303_with_no_cache() { + let requestCount = 0; + httpserver.registerPathHandler("/redirect5", (metadata, response) => { + requestCount++; + response.setStatusLine(metadata.httpVersion, 303, "See Other"); + response.setHeader("Location", get_url("/content"), false); + response.setHeader("Cache-Control", "max-age=3600, no-cache", false); + }); + + // First request + let chan = make_channel(get_url("/redirect5")); + let buffer = await new Promise(resolve => { + chan.asyncOpen(new ChannelListener((request, buf) => resolve(buf), null)); + }); + Assert.equal(buffer, responseBody); + Assert.equal(requestCount, 1, "First request should hit server"); + + // Second request - should revalidate due to no-cache + chan = make_channel(get_url("/redirect5")); + buffer = await new Promise(resolve => { + chan.asyncOpen(new ChannelListener((request, buf) => resolve(buf), null)); + }); + Assert.equal(buffer, responseBody); + Assert.equal(requestCount, 2, "Second request should revalidate (no-cache)"); +}); diff --git a/netwerk/test/unit/xpcshell.toml b/netwerk/test/unit/xpcshell.toml @@ -46,6 +46,8 @@ prefs = [ ["test_1073747.js"] +["test_303_redirect_caching.js"] + ["test_304_headers.js"] ["test_304_responses.js"]