|
| 1 | +--- |
| 2 | +layout: post |
| 3 | +title: "When Safari’s ‘Ignore Cache’ Ignored Cache-Control" |
| 4 | +date: 2026-08-17 16:11:40 |
| 5 | +categories: Web Development |
| 6 | +main: "" |
| 7 | +meta: "Safari Technology Preview 250 fixed a subtle Web Inspector bug that overwrote page-authored Cache-Control request headers when the resource cache was disabled." |
| 8 | +--- |
| 9 | + |
| 10 | +As you know, I’m a little bit obsessed with caching. I’ve written about |
| 11 | +[`Cache-Control`](/2019/03/cache-control-for-civilians/), [how much cache |
| 12 | +coverage sites leave on the table](/2024/08/cache-grab-how-much-are-you-leaving-on-the-table/), |
| 13 | +[better reuse with |
| 14 | +`No-Vary-Search`](/2026/05/better-browser-caching-with-no-vary-search/), and even |
| 15 | +[why we have a `Cache-Control` _request_ |
| 16 | +header](/2025/03/why-do-we-have-a-cache-control-request-header/). That is why |
| 17 | +this otherwise rather vague line in the [Safari Technology Preview 250 release |
| 18 | +notes](https://webkit.org/blog/18191/release-notes-for-safari-technology-preview-250/) |
| 19 | +caught my eye: |
| 20 | + |
| 21 | +> Fixed disabling the resource cache overwriting a `Cache-Control` header |
| 22 | +> already set by the page. |
| 23 | +
|
| 24 | +At first glance, that almost sounds like the feature working as intended: of |
| 25 | +course disabling cache has to override caching somehow. But disabling WebKit’s |
| 26 | +local resource cache and replacing a header that the page deliberately added to |
| 27 | +its request are two very different things. |
| 28 | + |
| 29 | +The release note piqued my interest, so I dug into [the underlying WebKit |
| 30 | +change](https://github.com/WebKit/WebKit/commit/96437189e7eecba977fe0a452b7e35fecc6ea776). |
| 31 | +The bug is small, specific, and absolutely fascinating. |
| 32 | + |
| 33 | +{% include promo.html %} |
| 34 | + |
| 35 | +## Web Inspector Was Changing the Outgoing Request |
| 36 | + |
| 37 | +Safari Web Inspector’s [<cite>Ignore |
| 38 | +Cache</cite>](https://webkit.org/web-inspector/network-tab/#controlling-resource-caching) |
| 39 | +toggle tells WebKit not to use cached resources for future requests while Web |
| 40 | +Inspector is open. That is a useful development convenience, particularly when |
| 41 | +you need to know that you’re looking at the latest copy of a resource. |
| 42 | + |
| 43 | +Before this fix, WebKit did two things when that toggle was active: |
| 44 | + |
| 45 | +* it set an internal policy that bypassed its resource cache; and |
| 46 | +* it set the outgoing request’s `Cache-Control` and `Pragma` headers to |
| 47 | + `no-cache`. |
| 48 | + |
| 49 | +The first behaviour is exactly what we asked for. The second is where things got |
| 50 | +interesting: WebKit _set_ those headers even if the page had already supplied |
| 51 | +its own. The debugging setup didn’t just change whether Safari could reuse |
| 52 | +a local response; it changed the request that the application sent. |
| 53 | + |
| 54 | +## This Is a `Cache-Control` Request Header |
| 55 | + |
| 56 | +We tend to think of `Cache-Control` as a response header: |
| 57 | + |
| 58 | +```http |
| 59 | +HTTP/2 200 |
| 60 | +Cache-Control: max-age=31536000, immutable |
| 61 | +``` |
| 62 | + |
| 63 | +That is the origin telling browsers and other caches how they may store and |
| 64 | +reuse its response. But, as [we’ve looked at |
| 65 | +before](/2025/03/why-do-we-have-a-cache-control-request-header/), |
| 66 | +`Cache-Control` can also appear on a _request_, where the client tells caches |
| 67 | +what kind of stored response it is prepared to accept. |
| 68 | + |
| 69 | +A page can add one itself: |
| 70 | + |
| 71 | +```js |
| 72 | +fetch('/data.json', { |
| 73 | + headers: { |
| 74 | + 'Cache-Control': 'max-age=12345' |
| 75 | + } |
| 76 | +}); |
| 77 | +``` |
| 78 | + |
| 79 | +That is almost the exact case in WebKit’s new regression test. With caching |
| 80 | +enabled, the server receives `max-age=12345`. With caching disabled and no |
| 81 | +page-authored header, it receives WebKit’s `no-cache`. Crucially, with caching |
| 82 | +disabled _and_ the page setting `max-age=12345`, the server should still receive |
| 83 | +`max-age=12345`. |
| 84 | + |
| 85 | +Before the fix, it received `no-cache` instead. The value that the page asked |
| 86 | +for never reached the network. |
| 87 | + |
| 88 | +## Cache Bypass and Request Semantics Are Now Separate |
| 89 | + |
| 90 | +The fix is pleasingly small. When Web Inspector has disabled caching, WebKit now |
| 91 | +adds its `no-cache` headers only if the request doesn’t already contain them. It |
| 92 | +still applies its internal `ReloadIgnoringCacheData` policy, so the local |
| 93 | +resource cache remains bypassed either way. |
| 94 | + |
| 95 | +In other words: |
| 96 | + |
| 97 | +```text |
| 98 | +Page-authored request header: preserved |
| 99 | +Web Inspector cache bypass: still applied |
| 100 | +``` |
| 101 | + |
| 102 | +This is the distinction that the release note hides. Web Inspector can ignore |
| 103 | +its local cache without rewriting the application’s request to achieve it. |
| 104 | + |
| 105 | +This also wasn’t a general Safari caching failure. The affected condition was |
| 106 | +much narrower: Web Inspector had disabled the resource cache, and the request |
| 107 | +already contained a page-authored `Cache-Control` header. The release note |
| 108 | +doesn’t establish which shipping Safari versions contained the old behaviour, |
| 109 | +only that Safari Technology Preview 250 contains the fix. |
| 110 | + |
| 111 | +## How This Could Trip You Up |
| 112 | + |
| 113 | +Changing `max-age=12345` to `no-cache` is not cosmetic. A shared cache, CDN, or |
| 114 | +origin that receives the request may respond differently because `no-cache` |
| 115 | +requires a stored response to be successfully validated before reuse. The tool |
| 116 | +that was meant to help inspect the request had changed the request’s caching |
| 117 | +semantics before it reached the network. |
| 118 | + |
| 119 | +That could lead to some wonderfully confusing investigations. Server logs might |
| 120 | +show a `no-cache` header that the application never sent. An endpoint might |
| 121 | +behave differently only while Web Inspector is open and <cite>Ignore |
| 122 | +Cache</cite> is active. Or a supposed warm-cache test might make every request |
| 123 | +look as though the user had explicitly demanded revalidation. |
| 124 | + |
| 125 | +None of those observations would be false, exactly; they would be true only |
| 126 | +under the debugging conditions that produced them. |
| 127 | + |
| 128 | +For caching work, I keep the conditions separate: |
| 129 | + |
| 130 | +* **Cold cache:** clear the relevant data once, then load with caching enabled. |
| 131 | +* **Warm cache:** load or revisit normally with caching enabled. |
| 132 | +* **Cache disabled:** treat this as a separate diagnostic mode, not a warm-cache |
| 133 | + test. |
| 134 | + |
| 135 | +It is also worth recording the browser version, whether Web Inspector was open, |
| 136 | +and whether <cite>Ignore Cache</cite> was active. If the request headers |
| 137 | +themselves are in question, compare what Web Inspector shows with CDN/origin |
| 138 | +logs or another capture outside that setup. |
| 139 | + |
| 140 | +This is a tiny browser bug with a tiny fix, not a particularly harmful caching |
| 141 | +failure. But it is a lovely bit of trivia about how developer tools work: before |
| 142 | +the fix included in Safari Technology Preview 250, asking Web Inspector to |
| 143 | +ignore its own cache could also make it ignore what the page had already said |
| 144 | +about caching. |
| 145 | + |
| 146 | +That is exactly the kind of distinction that keeps caching interesting long |
| 147 | +after any sensible person would have moved on. |
0 commit comments