commit fdbf27485b14b3e81f9cb5ffd3fa2d9b2776709a
parent ba17dce2c2a39b4f4d15f28a07ceb1f92191711c
Author: Valentin Gosu <valentin.gosu@gmail.com>
Date: Tue, 7 Oct 2025 09:52:27 +0000
Bug 1937766 - Make sure pragma:no-cache is respected even when a cache-control header is present r=necko-reviewers,kershaw,emilio
Even though the pragma: no-cache header is deprecated according to
https://httpwg.org/specs/rfc9111.html#field.pragma this header is often
present on the web, sometimes conflicting with the cache-control header.
Normally cache-control would take precedence when there's a conflict,
but since other browsers treat pragma: no-cache as a definite signal to not
cache, a different behaviour risks webcompat issues.
We can probably make an exception for cache-control: immutable. Since this is
a signal that the resource is unlikely to change, it's probably safe to treat
resources as cacheable in this case.
Differential Revision: https://phabricator.services.mozilla.com/D266972
Diffstat:
4 files changed, 74 insertions(+), 8 deletions(-)
diff --git a/netwerk/protocol/http/nsHttpResponseHead.h b/netwerk/protocol/http/nsHttpResponseHead.h
@@ -198,9 +198,16 @@ class nsHttpResponseHead {
}
bool NoCache_locked() const MOZ_REQUIRES(mRecursiveMutex) {
- // We ignore Pragma: no-cache if Cache-Control is set.
MOZ_ASSERT_IF(mCacheControlNoCache, mHasCacheControl);
- return mHasCacheControl ? mCacheControlNoCache : mPragmaNoCache;
+ // Normally we would ignore Pragma: no-cache if Cache-Control is set.
+ // But since all other browsers treat the existence of Pragma: no-cache
+ // as a signal to not-cache even when it conflicts with Cache-Control
+ // it is safer just to do the same. Previous behaviour where Pragma
+ // was ignored when Cache-Control was present resulted in several
+ // web-compat issues (Bug 1937766)
+ // However, the presence of cacheControl immutable indicates that
+ // pragma: no-cache is incorrectly added to the response.
+ return (mPragmaNoCache && !mCacheControlImmutable) || mCacheControlNoCache;
}
private:
diff --git a/netwerk/test/gtest/TestHttpResponseHead.cpp b/netwerk/test/gtest/TestHttpResponseHead.cpp
@@ -67,7 +67,8 @@ TEST(TestHttpResponseHead, bug1649807)
Unused << head.ParseStatusLine("HTTP/1.1 200 OK"_ns);
Unused << head.ParseHeaderLine("content-type: text/plain"_ns);
Unused << head.ParseHeaderLine("etag: Just testing"_ns);
- Unused << head.ParseHeaderLine("cache-control: age=99999"_ns);
+ Unused << head.ParseHeaderLine(
+ "cache-control: public, max-age=31536000, immutable"_ns);
Unused << head.ParseHeaderLine("accept-ranges: bytes"_ns);
Unused << head.ParseHeaderLine("content-length: 1408"_ns);
Unused << head.ParseHeaderLine("connection: close"_ns);
@@ -76,7 +77,27 @@ TEST(TestHttpResponseHead, bug1649807)
Unused << head.ParseHeaderLine("date: Tue, 12 May 2020 09:24:23 GMT"_ns);
ASSERT_FALSE(head.NoCache())
- << "Cache-Control wins over Pragma: no-cache";
+ << "Cache-Control: immutable wins over Pragma: no-cache";
+ AssertRoundTrips(head);
+}
+
+TEST(TestHttpResponseHead, bug1937766)
+{
+ nsHttpResponseHead head;
+
+ Unused << head.ParseStatusLine("HTTP/1.1 200 OK"_ns);
+ Unused << head.ParseHeaderLine("content-type: text/plain"_ns);
+ Unused << head.ParseHeaderLine("etag: Just testing"_ns);
+ Unused << head.ParseHeaderLine("cache-control: age=99999"_ns);
+ Unused << head.ParseHeaderLine("accept-ranges: bytes"_ns);
+ Unused << head.ParseHeaderLine("content-length: 1408"_ns);
+ Unused << head.ParseHeaderLine("connection: close"_ns);
+ Unused << head.ParseHeaderLine("server: httpd.js"_ns);
+ Unused << head.ParseHeaderLine("pragma: no-cache"_ns);
+ Unused << head.ParseHeaderLine("date: Tue, 12 May 2020 09:24:23 GMT"_ns);
+
+ ASSERT_TRUE(head.NoCache())
+ << "Pragma: no-cache wins over Cache-Control";
AssertRoundTrips(head);
}
diff --git a/testing/web-platform/tests/fetch/http-cache/pragma-no-cache-with-cache-control.html b/testing/web-platform/tests/fetch/http-cache/pragma-no-cache-with-cache-control.html
@@ -5,11 +5,16 @@
<script src="/resources/testharnessreport.js"></script>
<body>
<script>
-promise_test(async t => {
+promise_test(async _t => {
// According to https://www.rfc-editor.org/rfc/rfc9111.html#name-pragma
// the pragma header is deprecated.
// When there's a mismatch between pragma and Cache-Control then the latter
- // should be respected, and the resource should be cached.
+ // should be respected, and the resource should be cached, but this could
+ // lead to web-compat issues when one browser caches and others don't.
+ // It's safer to avoid caching when Pragma: no-cache is present, even though
+ // this further ossifies its use.
+ // See https://bugzilla.mozilla.org/show_bug.cgi?id=1937766 and
+ // https://issues.chromium.org/issues/447171250 for discussion.
const url = 'resources/cached_pragma_rand.py'
// First fetch to populate the cache
@@ -22,7 +27,26 @@ promise_test(async t => {
assert_true(response2.ok, 'Second fetch should succeed');
const text2 = await response2.text();
- assert_equals(text1, text2, 'Responses should be identical, indicating caching');
-}, 'Response with Cache-Control: max-age=2592000, public and Pragma: no-cache should be cached');
+ assert_not_equals(text1, text2, 'Responses should be different, indicating no cache use');
+}, 'Response with Cache-Control: max-age=2592000, public and Pragma: no-cache should not be cached');
+
+promise_test(async _t => {
+ // Cache-Control: immutable should be cached even when Pragma: no-cache is present
+ // because immutable is a strong directive indicating the resource will never change.
+ const url = 'resources/cached_pragma_immutable_rand.py'
+
+ // First fetch to populate the cache
+ const response1 = await fetch(url, { cache: 'default' });
+ assert_true(response1.ok, 'First fetch should succeed');
+ const text1 = await response1.text();
+
+ // Second fetch should be served from cache
+ const response2 = await fetch(url, { cache: 'default' });
+ assert_true(response2.ok, 'Second fetch should succeed');
+ const text2 = await response2.text();
+
+ assert_equals(text1, text2, 'Responses should be identical, indicating cache use');
+}, 'Response with Cache-Control: max-age=2592000, immutable and Pragma: no-cache should be cached');
+
</script>
</body>
diff --git a/testing/web-platform/tests/fetch/http-cache/resources/cached_pragma_immutable_rand.py b/testing/web-platform/tests/fetch/http-cache/resources/cached_pragma_immutable_rand.py
@@ -0,0 +1,14 @@
+def main(request, response):
+ # Disable non-standard XSS protection
+ response.headers.set(b"X-XSS-Protection", b"0")
+ response.headers.set(b"Content-Type", b"text/html")
+
+ # Set caching headers with immutable directive
+ # Cache-Control: immutable indicates the resource will never change,
+ # and should be cached even when Pragma: no-cache is present.
+ response.headers.set(b"Cache-Control", b"max-age=2592000, immutable")
+ response.headers.set(b"Pragma", b"no-cache")
+
+ # Include a timestamp to verify caching behavior
+ import time
+ response.content = f"Timestamp: {time.time()}".encode('utf-8')