- From: Valentin Gosu <valentin.gosu@gmail.com>
- Date: Tue, 10 Mar 2026 10:11:32 +0100
- To: Michael Toomim <toomim@gmail.com>
- Cc: Patrick Meenan <patmeenan@gmail.com>, HTTP Working Group <ietf-http-wg@w3.org>, Braid <braid-http@googlegroups.com>
- Message-ID: <CACQYfiJzPQKuVeQxA5ZZTbpPDQh=CLA+vUwwwhXZXN4tzfrvcA@mail.gmail.com>
Hi Michael,
Thank you for exploring the edges of the HTTP cache implementations.
On Sun, 8 Mar 2026 at 04:56, Michael Toomim <toomim@gmail.com> wrote:
> Thanks, Patrick! I'm learning so much from all this!
>
> I am seeing how most of the experimental conditions in our tests have
> ambiguous cache directives -- so yes, it makes sense that the browsers
> are behaving according to their heuristics.
>
> After adding experimental conditions for all the ideas you suggested,
> and refining our mental model of what was happening, we were able to
> identify existing issue reports out there for Chrome's 20-second delay:
>
> https://stackoverflow.com/questions/27513994/chrome-stalls-when-making-multiple-requests-to-same-resource
> and for Firefox's total hang when streaming:
> https://bugzilla.mozilla.org/show_bug.cgi?id=1544313
>
> Both issues can be avoided with the right parameters. For Chrome,
> setting a client-side {cache: 'no-store'} in fetch() eliminates the
> 20-second delay (Yay!) and for Firefox, it just needs a Content-Type to
> be specified on the response. (Otherwise, Firefox's content-sniffer
> tries to consume the first 512 bytes of the stream to guess what the
> content-type is, and stalls everything until it gets those bytes.)
>
You've correctly identified the issue. We might fix that one soon by
disabling the content sniffer for fetch requests.
Regarding the other issue, with opening a second tab in Firefox, that's
caused by readers of HTTP cache waiting for writers to finish writing into
the cache entry.
Since the first fetch doesn't actually complete (stream is left open), the
second one waits forever for the cache entry to be ready (or until the TCP
connection/HTTP response eventually times out).
I filed https://bugzilla.mozilla.org/show_bug.cgi?id=2022146 so we can
track this.
Thanks!
> Now we're just seeing one remaining issue -- for some reason, chrome is
> not obeying the {cache: 'no-store'} on a javascript fetch() in the
> particular case that a browser tab is closed, and then "unclosed" with
> cmd-shift-t / ctrl-shift-t. The same issue happens when you quit
> chrome, and then restore all closed tabs. When restoring the closed
> tab, the inner fetch() uses the cache -- totally disregarding our cache:
> 'no-store' directive.
>
> But now that I know how to use the netlogger (thanks!), I was able to
> capture the URL_REQUEST events for both conditions (normal pageload, and
> restoring-closed-tab), and computed a diff between them:
>
> https://www.diffchecker.com/sqrqUjcM/
>
> The "reopened tab" is on the right, in green. You can see on line 35
> that it has replaced the "DISABLE_CACHE" flag with a
> "SKIP_CACHE_VALIDATION" flag... which actually does the opposite of what
> we want!
>
> Digging into the Chromium source, we see that in frame_fetch_context.cc
> (
> https://chromium.googlesource.com/chromium/src/+/c7969a55cf2c0369dbcc3f409ee2cc5e84139d31/third_party/blink/renderer/core/loader/frame_fetch_context.cc)
>
> it uses a special WebFrameLoadType for restored tabs, equivalent to
> kBackForward, and this is interpreted within DetermineCacheMode() as
> "mojom::FetchCacheMode::kForceCache", which then becomes
> LOAD_SKIP_CACHE_VALIDATION when it reaches the network layer. This must
> be why our netlog prints out SKIP_CACHE_VALIDATION for the unclosed tab
> case.
>
> The issue seems to be that this frame-level cache SKIP_CACHE_VALIDATION
> policy propagates to all subresource requests (including our fetch())
> until the document's load() event fires -- even though our fetch
> explicitly says to skip the cache with {cache: 'no-store'}. It seems
> that this explicit "no-store" should take precedence over the default
> frame cache policy, I would think.
>
> I just wrote this up as a Chromium issue:
> https://issues.chromium.org/issues/490673934
>
> Thanks for the education!
>
> Michael
>
> On 3/7/26 3:48 AM, Patrick Meenan wrote:
> > It's unlikely that this is an issue with any of the browsers. The
> > response headers to both requests have no cache-control directives and
> > both requests have the same cache key (without Vary) so if the request
> > can be cached and/or reused is largely up to heuristics <https://
> > datatracker.ietf.org/doc/html/rfc7234#section-4.2.2> and what browsers
> > decide to do when two in-flight requests match the same cache entry and
> > how they partition the caches.
> >
> > Document response headers:
> >
> > t=422 [st=6] HTTP_TRANSACTION_READ_RESPONSE_HEADERS
> > --> HTTP/1.1 200 OK
> > Date: Sat, 07 Mar 2026 04:11:47 GMT
> > Connection: keep-alive
> > Keep-Alive: timeout=5
> > Content-Length: 477
> >
> > fetch() response headers:
> >
> > t= 430 [st= 1] HTTP_TRANSACTION_READ_RESPONSE_HEADERS
> > --> HTTP/1.1 200 OK
> > Date: Sat, 07 Mar 2026 04:11:47 GMT
> > Connection: keep-alive
> > Keep-Alive: timeout=5
> > Transfer-Encoding: chunked
> >
> > For deterministic behavior, you need to be explicit about the cache
> > treatment of the responses. What you use largely depends on what you
> > want the behavior to be.
> >
> > "Vary: Subscribe" will add the value of the Subscribe header to the
> > cache key but it needs to be on both responses because the browser will
> > lookup based on the URL and then check if any existing vary conditions
> > match.
> >
> > Adding cache: "no-store" to the fetch request bypasses the cache
> > entirely for that request (which is probably what you want to do to
> > avoid the cache lookup time anyway if you never want to cache the
> > response) but it doesn't change the cache behavior of the document
> request.
> >
> > A "cache-control: no-store" response header prevents the document
> > response from being written to the cache (including for back/forward
> > navigations and other cases where the browser might use stale
> > responses). max-age=0 might be more appropriate or even a long cache
> > lifetime if the document itself is static.
> >
> > On Fri, Mar 6, 2026 at 11:17 PM Michael Toomim <toomim@gmail.com
> > <mailto:toomim@gmail.com>> wrote:
> >
> > __
> >
> > Thanks, Patrick! This is very helpful. We just updated the
> > experiment and have learned more about what's happening. You've
> > helped me understand how the caching is *supposed* to work, and I'm
> > starting to see the shape of the browser's behaviors.
> >
> > Yes, the requests have the same URL. The server differentiates the
> > second request because it adds a `Subscribe: true` header asking its
> > response to be streamed. The stream response headers are then sent
> > immediately, along with an initial chunk of stream data.
> >
> > The 20-second delay isn't for that second request, though. It's for
> > opening a new tab to the same URL (when devtools are open in the
> > first tab), and the delay depends specifically on the headers
> > specified on the 2nd request. If that 2nd (streaming) request does
> > *not* say Cache-Control: no-store, then the loading of a new tab to
> > that URL spins for 20 seconds. So perhaps it's waiting for the
> > streaming response to ... finish?
> >
> > I recorded a netlog, and put it here: https://braid.org/files/bugs/
> > chrome-20second-netlog.json <https://braid.org/files/bugs/
> > chrome-20second-netlog.json>
> >
> > We added two new conditions to the experiment: a "Vary: Subscribe"
> > response header (to teach the cache how to distinguish the two
> > requests) and the client-side fetch(url, {cache: 'no-store'})
> > parameter. Either of these are enough to fix the 20-second delay,
> > presumably by making it clear to Chrome that it can't try to re-use
> > the streaming response (which hasn't finished) for the new tab it's
> > opening.
> >
> > I think we probably have enough to write these up as bug reports for
> > Chrome and FF, now!
> >
> > Thanks, and cheers!
> >
> > Michael
> >
> > On 3/5/26 5:10 PM, Patrick Meenan wrote:
> >> Most of what you are describing is outside the scope of HTTP-
> >> proper (except for the actual caching of the responses) and is in
> >> the browser's fetch spec implementation before it hits HTTP.
> >>
> >> When you leave the response open as a stream, do you send
> >> an initial response with headers and just never finish it or do
> >> you leave the request hanging until there is data to send?
> >>
> >> How are the response headers varied? Specifically, what are the
> >> headers used for the HTML response? The reason I ask is that the
> >> client has to make its decision to fetch or not based 100% on any
> >> headers on the cached entry and has no idea what the second set of
> >> headers will be.
> >>
> >> Do the requests have the same URLs? How does the server
> >> differentiate between "first" and "second"? How does it handle any
> >> prefetch requests the browser might be sending?
> >>
> >> A Chrome netlog will help show what Chrome is doing (I'm happy to
> >> look if you collect one): https://www.chromium.org/for-testers/
> >> providing-network-details/ <https://www.chromium.org/for-testers/
> >> providing-network-details/>
> >>
> >> The 20-second delay could well be the request de-duplication
> >> timeout where Chrome will hold additional requests for identical
> >> URLs to make sure they aren't for the same static resource
> >> (instead of wasting bandwidth downloading the same thing twice).
> >> It should be able to make that determination as soon as the
> >> headers are complete though and not have to wait for the response
> >> to end.
> >>
> >> You have some level of control over this on the javascript side if
> >> you know what you are fetching shouldn't go through the cache:
> >> https://developer.mozilla.org/en-US/docs/Web/API/RequestInit#cache
> >> <https://developer.mozilla.org/en-US/docs/Web/API/RequestInit#cache
> >
> >> i.e. fetch('/foo', cache: 'no-store') will skip checking or
> >> writing to the cache at request time.
> >>
> >>
> >>
> >> On Thu, Mar 5, 2026 at 6:23 PM Michael Toomim <toomim@gmail.com
> >> <mailto:toomim@gmail.com>> wrote:
> >>
> >> Hello HTTPers!
> >>
> >> We've been experimenting with using fetch() to open a
> >> streaming HTTP response from the same URL that a page loads
> >> from. Specifically, the page loads at /foo, then its
> >> JavaScript does a fetch() back to /foo with the response left
> >> open as a stream. We've discovered some strange browser quirks
> >> that we'd appreciate the WGs input on.
> >>
> >> We wrote up a detailed test case with reproduction steps,
> >> results matrices, and videos here:
> >>
> >> https://braid.org/meeting-108/cache-quirks <https://
> >> braid.org/meeting-108/cache-quirks>
> >>
> >>
> >> The test server varies Cache-Control headers (no-cache, no-
> >> store, none) across the initial page load and the streaming
> >> fetch, creating 5 experimental conditions. We tested on Chrome
> >> 145, Safari 26.3.1, and Firefox 148.0. Here's a summary of
> >> what we found:
> >>
> >> *Quirk 1: Cached page content returned instead of stream
> >> data.* When a tab is closed and restored (e.g. Shift-Cmd-T),
> >> the second fetch() returns the cached HTML of the page itself
> >> rather than the streamed response from the server. This
> >> affects Chrome, Safari, and Firefox on most endpoints.
> >> Setting Cache-Control: no-store on the initial page load fixes
> >> it in Chrome and Safari, but not Firefox.
> >>
> >> *Quirk 2: 20-second delay before anything renders.* In Chrome,
> >> opening a second tab to the same URL causes the page itself to
> >> hang for precisely 20 seconds before loading. (Not just the
> >> inner fetch. Nothing renders at all.) Safari handles this
> >> fine. Setting no-store on the streaming fetch (or both
> >> requests) resolves it for Chrome, but Firefox recognizes no
> >> stream data at all in this scenario.
> >>
> >> The exact 20-second duration suggests this may be Chrome's
> >> cache lock timing out — perhaps Chrome is blocking a second
> >> request to the same URL while waiting for the first
> >> (streaming) response to become cacheable, and gives up after
> >> 20 seconds. (In HTTP/2 setups, a similar 20-second delay can
> >> occur from the SETTINGS frame acknowledgment timeout, but our
> >> test case is plain HTTP/1.1, so cache locking seems the more
> >> likely culprit here.)
> >>
> >> *Quirk 3: Streaming fetch hangs indefinitely.* In Firefox
> >> 148.0, the streaming fetch() now hangs forever on all
> >> endpoints, regardless of Cache-Control settings. This appears
> >> to be a regression — previously only some no-
> >> store configurations triggered the hang, and it required
> >> opening a second tab. Now it occurs even on first load. Chrome
> >> and Safari are unaffected.
> >>
> >> We'd appreciate the WG's guidance on what the expected
> >> behavior should be in these cases. In particular:
> >>
> >> * Should the cache be allowed to serve the initial page's
> >> response body for a subsequent fetch() to the same URL
> >> when the requests carry different headers?
> >> * Is Chrome's 20-second blocking behavior a cache lock, and
> >> does the spec allow for this behavior? Should a streaming
> >> response that hasn't completed block other requests to the
> >> same URL?
> >> * Is there something in the spec that would explain or allow
> >> for Firefox's hanging behavior with streaming responses?
> >>
> >>
> >> Thanks for any insight!
> >>
> >> Michael
> >>
>
>
>
Received on Tuesday, 10 March 2026 09:11:51 UTC