> A cache MUST NOT use an incomplete response to answer requests unless the response has been made complete, or the request is partial and specifies a range wholly within the incomplete response.
This behavior as described
1. client requests 1-200; [cached]
2. client requests 1-400; [cache responds with 206 with 1-200]
The cache is able to extend it's cache (which would result in a 403 non-successful). But otherwise MUST NOT use an incomplete cache, to answer a request for a complete answer. Is there room for the cache to pretend it's a server, and behave as if it was just the server? The server is allowed to return 206 however it wants, but unless I missed something, why is the cache allowed to yolo out a 206?
edit: additionally section 3.2 seems to make this even more explicit, this behavior is invalid.
> Caches are required to update a stored response's header fields from another (typically newer) response in several situations; for example.
The ambiguity here is unfortunate because they say required here, but don't use the keyword MUST.
> A message is considered "complete" when all of the octets indicated by its framing are available.
So in your scenario, the first response is complete, and so the caching behavior does not conflict with the spec.
I do think the cache is allowed to retain, and respond for the 200 bytes. I don't think it's free to ignore the header updates, nor do I think it's free to return half the requested bytes in lieu of extending the existing cache.
That's irrelevant. Otherwise, requests for 400 bytes against a resource that is actually only 200 bytes long would never be considered complete and would be disallowed to be cached.
Isn't that a fantastic use for a 2nd Chrome Profile or even just downloading a Chromium build[1] and using that, showing the behavior in a bleeding edge build?
Even just having a web-accessible endpoint that reproduced the issue would have made the process a lot smoother I think. Apparently in response to OP's request for an easier test case, OP asked for GCP cloud credits(?) to host their server with?. You probably used more bandwidth & CPU loading the new Chromium issue tracker page then you would have just setting up a simple vps to reproduce the issue
(1) I'm not setting up your server to repo the issue. I have no idea what all that code is going to do. Is it going to try to pown my machine?
(2) No, I'm not going to use your server as a repo. I have no idea you aren't updating it every 5 minutes with a new version.
There's a reason developers ask for an MCVE (Minimal complete verifiable example)
https://www.google.com/search?q=MCVE
It's not unreasonable to ask for one. Sorry if that sucks for you because it's difficult for you pair down your code but that's where we are. Try it on the other side and you'll start to get it.
server.go:
package main
import (
"net/http"
)
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.Write([]byte(`
<script>
(async () => {
await fetch("/test", { headers: { range: "bytes=0-4" } }).then(resp => console.log('bytes=0-4', resp.status));
await fetch("/test", { headers: { range: "bytes=0-10" } }).then(resp => console.log('bytes=0-10', resp.status));
})();
</script>
`))
})
http.HandleFunc("/test", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
switch r.Header.Get("Range") {
case "bytes=0-4":
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
w.Header().Set("Content-Range", "bytes 0-4/1000000")
w.Header().Set("Last-Modified", "Mon, 03 Mar 2025 00:00:00 GMT")
w.Header().Set("Etag", "1234567890")
w.WriteHeader(http.StatusPartialContent)
w.Write([]byte("01234"))
case "bytes=0-10":
w.WriteHeader(http.StatusForbidden)
w.Write([]byte("Forbidden"))
default:
w.WriteHeader(http.StatusBadRequest)
w.Write([]byte("Bad Request"))
}
})
http.ListenAndServe(":8080", nil)
}
`go run server.go` and open up http://localhost:8080 in the browser. For Chrome, in the console one should see bytes=0-4 206
bytes=0-10 206
but if we use "disable cache" this becomes bytes=0-4 206
GET http://localhost:8080/test 403 (Forbidden)
bytes=0-10 403
In both Safari and Firefox the second request is 403, cache or not.Now, is this surprising? Yes. Does this violate any spec? I'll take Chromium dev's word [1] and say likely not. Should it be "fixed"? Hard to say, but I agree that "fixing" it could break existing things.
HTTP partial content responses need to be evaluated (like any other response) according to their metadata: servers are not required to send you exactly the ranges you request, so you need to pay attention to Content-Range and process accordingly (potentially issuing more requests).
The latest response from the Chromium team (https://issues.chromium.org/issues/390229583#comment20) seems to take a different approach from your comment, and says that you should think of it as a streaming response where the connection failed partway through, which feels reasonable to me, except for the fact that `await`ing the response doesn't seem to trigger any errors: https://issues.chromium.org/issues/390229583#comment21
Yes, the mismatch between the response headers and the content is a problem. Unfortunately, IME browsers often do "fix ups" of headers that make them less than reliable, this might be one of them -- it's effectively rewriting the response but failing to update all of the metadata.
The bug summary says "Chrome returns wrong status code while using range header with caches." That's indeed not a bug. I think the most concerning thing here is that the Content-Range header is obviously incorrect, so Chrome should either be updating it or producing a clear error to alert you -- which it looks like the Chrome dev acknowledges when they say "it is probably a bug that there is no AbortError exception on the read".
I might try to add some tests for this to https://cache-tests.fyi/#partial
As in it should bubble the error up to the user.
Honestly, this genre of "big tech company refused to fix my very obscure edge case and that confirms all my priors about them" post is getting a little tiresome. There are like three of them coming through the front page every day.
It's a cache consistency bug at its root. The value was there, and now it's not. The reporter says "the browser is responsible for cache coherency" (call this the "MESI camp"). The Chrome folks say "the app is responsible for cache coherency" (the "unsnooped incoherent" gang). Neither is wrong. And the problem remains obscure regardless.
I'm not sure Chrome's current caching behavior is helpful because the second response does not indicate which part of the data is returned. So, the application has no choice but to discard the data.
But thank you for your comments. This helped me to crystalize why I think this is a bug.
When we handle this in the hardware world it's via algorithms that know about the mutability of the cached data and operate on top of primitives like "flush" and "invalidate" that can restore the inconsistent memory system to a known state. HTTP didn't spec that stuff, but the closest analog is "fetch it again", which is exactly what the suggested workaround is in the bug.
Ahh, let's just wait for the startup to fix it then.
I'm interested in what kind of application depends on this behavior - if an application gets partial data from the server, especially one that doesn't match the content-length header, that should always be an error to me.
ETA: I’m wrong here—turns out the range request contract explicitly allows this: “a server might want to send only a subset of the data requested for reasons of its own” <https://www.rfc-editor.org/rfc/rfc9110.html#name-206-partial...>.
Consider a read() call in Linux if you ask to read 16kb and the cache has 4kb page ready, it may give you that.
You'll need another call to get the rest, and if there is a bad disk sector, that first read() may bot notice that
> A server that supports range requests (Section 14) will usually attempt to satisfy all of the requested ranges, since sending less data will likely result in another client request for the remainder. However, a server might want to send only a subset of the data requested for reasons of its own, such as temporary unavailability, cache efficiency, load balancing, etc. Since a 206 response is self-descriptive, the client can still understand a response that only partially satisfies its range request.
[1] https://www.rfc-editor.org/rfc/rfc9110.html#name-206-partial...
> A Content-Length header field present in a 206 response indicates the number of octets in the content of this message, which is usually not the complete length of the selected representation.
While in the article (and in the mailing group discussion) it seems that Chrome is responding with a `content-length` of 1943504 while the body of the response only contains 138721 octets. Unless there's some even more obscure part of the spec, that definitely seems like a bug as it makes detecting the need to re-request more annoying.
Prototypical “bad” technical debt, perhaps.