Re: [whatwg/fetch] Add support for compression dictionary transport (PR #1854)

annevk left a comment (whatwg/fetch#1854)

<details>
<summary>AI feedback</summary>

Reviewed at 4d3401a. A local Bikeshed build shows no new warnings from this PR, and all new autolinks resolve. The two link errors in that build come from the branch being 11 commits behind main and go away on rebase. The destination additions (destination type list, table, *subresource request*, `RequestDestination`) are consistent. Neither CSP (whose effective directive defaults to `connect-src`) nor Fetch Metadata (which defers to Fetch's destinations) needs changes.

**Blockers**

1. The [`no-cors` check](https://github.com/whatwg/fetch/blob/4d3401afef60066ee6ebd64230cfc05510b07246/fetch.bs#L6706-L6707) in *HTTP-network compression-dictionary fetch* falls back for every `no-cors` request. That includes same-origin `<script src>`, `<img>`, and `<link rel=stylesheet>` loads, which is the RFC's own [version-upgrade example](https://www.rfc-editor.org/rfc/rfc9842#section-1.1.1). It contradicts:
   - [RFC 9842 §9.3.2](https://www.rfc-editor.org/rfc/rfc9842#section-9.3.2), which allows same-origin responses;
   - the [same-origin no-cors WPT](https://github.com/web-platform-tests/wpt/blob/78fe5369583fb0bc10b35a9b977bd41e49dbcfa0/fetch/compression-dictionary/dictionary-fetch-no-cors.tentative.https.html#L41-L50);
   - Chromium (see [this thread](https://github.com/whatwg/fetch/pull/1854#discussion_r3924343670)).

   It also makes the later [opaque → network error step](https://github.com/whatwg/fetch/blob/4d3401afef60066ee6ebd64230cfc05510b07246/fetch.bs#L6743-L6744) unreachable, because only `no-cors` requests get opaque tainting. Consider falling back when *response tainting* is "`opaque`" instead; main fetch sets it for each hop before this runs, and it stays opaque. Then either drop the later step or turn it into a deliberate check for dcb/dcz responses that weren't negotiated.

2. [Registration](https://github.com/whatwg/fetch/blob/4d3401afef60066ee6ebd64230cfc05510b07246/fetch.bs#L6623-L6679) sits at the end of *HTTP-network-or-cache fetch*. *HTTP fetch* runs that before its [CORS check](https://github.com/whatwg/fetch/blob/4d3401afef60066ee6ebd64230cfc05510b07246/fetch.bs#L5892-L5894) and before [redirect handling](https://github.com/whatwg/fetch/blob/4d3401afef60066ee6ebd64230cfc05510b07246/fetch.bs#L5918). Two consequences:
   - A "`cors`"-tainted response that then fails the CORS check still becomes a dictionary the page could never read. §9.3.2 forbids this; it opens a compression oracle on the dictionary's contents.
   - 3xx responses get registered, which #1739 says must not happen.

   Consider moving registration into *HTTP fetch*, after the CORS check, and only when the internal response's status is not a redirect status. It's also worth deciding whether non-ok statuses should register. WPT tests for both cases would help.

3. The [new request headers](https://github.com/whatwg/fetch/blob/4d3401afef60066ee6ebd64230cfc05510b07246/fetch.bs#L6726-L6733) are added to *httpRequest*, which *HTTP-network-or-cache fetch* then [stores in the HTTP cache](https://github.com/whatwg/fetch/blob/4d3401afef60066ee6ebd64230cfc05510b07246/fetch.bs#L6506-L6509). So the stored request carries `Available-Dictionary` and `Accept-Encoding: …, dcb, dcz`.
   - Cache lookups happen before this layer adds those headers, so `Vary: accept-encoding, available-dictionary` ([§6.2](https://www.rfc-editor.org/rfc/rfc9842#section-6.2)) never matches.
   - Because `Vary: Accept-Encoding` is so common, any response fetched while a dictionary matched can't be reused from the cache, whether or not it was compressed with that dictionary.
   - The [WPT](https://github.com/web-platform-tests/wpt/blob/78fe5369583fb0bc10b35a9b977bd41e49dbcfa0/fetch/compression-dictionary/dictionary-registration.tentative.https.html#L62-L94) says the browser cache must not vary on these headers.

   Consider adding the headers to a copy that is only used for the network request, or stating that the stored request excludes them.

4. [Decoding](https://github.com/whatwg/fetch/blob/4d3401afef60066ee6ebd64230cfc05510b07246/fetch.bs#L6737-L6764) is underspecified and collides with *HTTP-network fetch*:
   - **Which dictionary:** the transform never says which dictionary's bytes it decodes with. It only has a hash, re-parsed from the request's header list. It could use *bestMatch* directly.
   - **Stream handling:** "transforming … with an algorithm" and "error the transformed stream" could use the TransformStream "set up" + "piped through" pattern ([example](https://github.com/whatwg/fetch/blob/4d3401afef60066ee6ebd64230cfc05510b07246/fetch.bs#L5361-L5374)), and say what the stream errors with. The [WPT](https://github.com/web-platform-tests/wpt/blob/78fe5369583fb0bc10b35a9b977bd41e49dbcfa0/fetch/compression-dictionary/dictionary-decompression.tentative.https.html#L57-L79) expects a `TypeError` on read (see also [this thread](https://github.com/whatwg/fetch/pull/1854#discussion_r3340334004)).
   - **Double decoding:** *HTTP-network fetch* still runs [*handle content codings*](https://github.com/whatwg/fetch/blob/4d3401afef60066ee6ebd64230cfc05510b07246/fetch.bs#L1650-L1658) on every chunk. Once a user agent supports dcb/dcz, it's ambiguous whether that layer tries, and fails, to decode them without a dictionary. Adding `[[!RFC9842]]` to its ["Follow the relevant requirements" list](https://github.com/whatwg/fetch/blob/4d3401afef60066ee6ebd64230cfc05510b07246/fetch.bs#L6839-L6840) makes that reading more likely.
   - **Deleting `Content-Encoding`:** the [delete step](https://github.com/whatwg/fetch/blob/4d3401afef60066ee6ebd64230cfc05510b07246/fetch.bs#L6763-L6764) races with *HTTP-network fetch*'s in-parallel loop, which [re-reads `Content-Encoding` for every chunk](https://github.com/whatwg/fetch/blob/4d3401afef60066ee6ebd64230cfc05510b07246/fetch.bs#L7075-L7076).
     - Body info's content encoding flips to "" mid-stream; Resource Timing [expects "dcb"](https://github.com/web-platform-tests/wpt/blob/78fe5369583fb0bc10b35a9b977bd41e49dbcfa0/resource-timing/content-encoding.https.html#L28).
     - The decoded size stays equal to the encoded size.
     - Every other coding keeps the header, and the difference is visible through `response.headers`.

   Consider recording the selected dictionary (e.g., on fetch params) and decoding dcb/dcz inside *handle content codings*. Failures would then terminate the controller, and body info would stay correct, as for other codings. `Content-Encoding` could then stay, and `[[!RFC9842]]` could come out of *HTTP-network fetch*.

5. [`Use-As-Dictionary` member validation](https://github.com/whatwg/fetch/blob/4d3401afef60066ee6ebd64230cfc05510b07246/fetch.bs#L6635-L6657) is incomplete:
   - `match-dest=()` reaches "If *matchDestList* is empty, then return" and isn't registered. But [§2.1.2](https://www.rfc-editor.org/rfc/rfc9842#section-2.1.2) says "An empty list for 'match-dest' MUST match all destinations" (see [this comment](https://github.com/whatwg/fetch/pull/1854#discussion_r3378112675)).
   - Member types aren't checked:
     - `match` must be a String; a Token or Integer would go straight into URL pattern creation.
     - `match-dest` must be an Inner List of Strings. With `match-dest="script"`, `[0]` is a string that the next step iterates over.
     - `id` must be a String before its length means anything.
     - `type` must be a Token.
   - The only defined type is `raw`, which both dcb and dcz require ([§4](https://www.rfc-editor.org/rfc/rfc9842#section-4), [§5](https://www.rfc-editor.org/rfc/rfc9842#section-5)). Comparing against the token `raw` would be clearer than an implementation-defined set.
   - The step removes items from a list while iterating over it. It also links *request*'s *destination* where *destination type* is meant.

   A possible rewrite: "If *dictionaryValue*["`match-dest`"] exists: if its [0] is not an inner list, return; if it is non-empty and none of its items' [0] is a string that is a destination type the user agent supports, return."

6. [Expiry](https://github.com/whatwg/fetch/blob/4d3401afef60066ee6ebd64230cfc05510b07246/fetch.bs#L6672-L6675) is based on Fetch's *stale response*, which ignores `no-store`, `no-cache`, and `must-revalidate`. Under the current text, all three of these WPT cases would register and use the dictionary, but the tests expect otherwise:
   - [no-store](https://github.com/web-platform-tests/wpt/blob/78fe5369583fb0bc10b35a9b977bd41e49dbcfa0/fetch/compression-dictionary/dictionary-registration.tentative.https.html#L245-L254) (`max-age=3600, no-store`)
   - [no-cache](https://github.com/web-platform-tests/wpt/blob/78fe5369583fb0bc10b35a9b977bd41e49dbcfa0/fetch/compression-dictionary/dictionary-registration.tentative.https.html#L232-L243) (`max-age=3600, no-cache`)
   - [must-revalidate](https://github.com/web-platform-tests/wpt/blob/78fe5369583fb0bc10b35a9b977bd41e49dbcfa0/fetch/compression-dictionary/dictionary-registration.tentative.https.html#L218-L230) (`max-age=0, must-revalidate, stale-while-revalidate=3600`)

   The spec needs to say how each directive is handled.

7. The secure-context checks ([registration](https://github.com/whatwg/fetch/blob/4d3401afef60066ee6ebd64230cfc05510b07246/fetch.bs#L6624-L6627), [use](https://github.com/whatwg/fetch/blob/4d3401afef60066ee6ebd64230cfc05510b07246/fetch.bs#L6712-L6713)) look at the request's *client*. For navigations, that is the initiator's environment, not the new document's:
   - An https page navigating top-level to an http:// URL passes both checks, so it could register and advertise dictionaries over cleartext. [§8](https://www.rfc-editor.org/rfc/rfc9842#section-8) says the mechanism "MUST only be used in secure contexts (HTTPS)".
   - Conversely, an http page navigating to an https:// URL is refused.
   - *client* can also be null (CORS-preflight requests have none), and that case isn't handled.

   Consider checking that the request's *current URL* is a potentially trustworthy URL. If an environment check is also wanted, use the *reserved client* when it's non-null (as *determine the network partition key* does), and handle null.

8. The compression-dictionary cache is never defined. ["The unique compression-dictionary cache associated with *key*"](https://github.com/whatwg/fetch/blob/4d3401afef60066ee6ebd64230cfc05510b07246/fetch.bs#L3358) cites RFC 9842, which defines no such cache (see [this comment](https://github.com/whatwg/fetch/pull/1854#discussion_r3288832006)). ["Store *response* … with its associated *pattern*, *dictionaryValue*, and *expirationTime*"](https://github.com/whatwg/fetch/blob/4d3401afef60066ee6ebd64230cfc05510b07246/fetch.bs#L6677-L6678) leaves several things undefined:
   - **What an entry holds:** it needs the SHA-256 hash for `Available-Dictionary`, and the fetch time for the [§2.2.3](https://www.rfc-editor.org/rfc/rfc9842#section-2.2.3) tie-break.
   - **Replacement:** every cache hit re-registers and adds a duplicate. The WPT ["New dictionary registration overrides the existing one"](https://github.com/web-platform-tests/wpt/blob/78fe5369583fb0bc10b35a9b977bd41e49dbcfa0/fetch/compression-dictionary/dictionary-registration.tentative.https.html#L32-L60) depends on this behavior.
   - **Timing:** the step runs while the body is still streaming. The hash needs the full body, and an errored or cancelled body must not register.
   - **Matching:** ["finding the best matching dictionary"](https://github.com/whatwg/fetch/blob/4d3401afef60066ee6ebd64230cfc05510b07246/fetch.bs#L6721-L6722) links to RFC §2.2 as a whole, which isn't an algorithm.

   Consider:
   - defining a compression dictionary struct and a per-partition cache, exported so [Clear-Site-Data #94](https://github.com/w3c/webappsec-clear-site-data/pull/94) can link to it;
   - registering only after the body is fully read (e.g., clone the response and fully read it);
   - writing the [§2.2.2](https://www.rfc-editor.org/rfc/rfc9842#section-2.2.2) and §2.2.3 matching in Fetch, over the stored entries.

9. Registration skips the cookie-blocking check that the [use path](https://github.com/whatwg/fetch/blob/4d3401afef60066ee6ebd64230cfc05510b07246/fetch.bs#L6709-L6710) has. [§10](https://www.rfc-editor.org/rfc/rfc9842#section-10) says clients MUST treat dictionaries like cookies, and [the rationale given here](https://github.com/whatwg/fetch/pull/1854#discussion_r2631491172) applies just as much to storing them. Consider applying the same check before registering.

**Suggestions**

- Consider factoring registration into its own algorithm, for example "process a `Use-As-Dictionary` header given *request* and *response*". *HTTP-network-or-cache fetch* now has eight early `return response` exits (see [this comment](https://github.com/whatwg/fetch/pull/1854#discussion_r3288933011)).
  - Likewise, the [*fallback* variable](https://github.com/whatwg/fetch/blob/4d3401afef60066ee6ebd64230cfc05510b07246/fetch.bs#L6698-L6704) only wraps one call.
  - A helper that returns the best-matching dictionary or null, covering the mode, cookie, secure-context and partition checks, would allow a single "If *bestMatch* is null, then return the result of running *HTTP-network fetch* …".
- Consider using URL Pattern's exported definitions instead of the [local anchors](https://github.com/whatwg/fetch/blob/4d3401afef60066ee6ebd64230cfc05510b07246/fetch.bs#L77-L79):
  - [*build a URL pattern from an HTTP structured field value*](https://urlpattern.spec.whatwg.org/#build-a-url-pattern-from-an-http-structured-field-value) already serializes the base URL and calls *create* with an empty map. It asserts that its input is a string, hence the type check in 5.
  - *has regexp groups* is exported `for="URL pattern"`.
  - ["*pattern* is failure"](https://github.com/whatwg/fetch/blob/4d3401afef60066ee6ebd64230cfc05510b07246/fetch.bs#L6669-L6670) can't happen: *create* throws a `TypeError` and never returns failure.
  - `<a>build a URL pattern from an HTTP structured field value</a>` and `<a for="URL pattern">has regexp groups</a>` both build with current Bikeshed data.
- Consider making ["Add the `Available-Dictionary` and `Dictionary-ID` (if applicable) headers"](https://github.com/whatwg/fetch/blob/4d3401afef60066ee6ebd64230cfc05510b07246/fetch.bs#L6726-L6727) precise with *set a structured field value* (see [this comment](https://github.com/whatwg/fetch/pull/1854#discussion_r3288862879)): the hash as a Byte Sequence, and the ID as a String when it's non-empty.
  - Then verify against *bestMatch*'s hash instead of [re-parsing the header](https://github.com/whatwg/fetch/blob/4d3401afef60066ee6ebd64230cfc05510b07246/fetch.bs#L6746-L6752).
  - These two headers aren't forbidden request-headers, so authors can set them ([earlier question](https://github.com/whatwg/fetch/pull/1854#discussion_r2538218786)). Appending would send two values, which [§2.2](https://www.rfc-editor.org/rfc/rfc9842#section-2.2) forbids; the later item parse then fails, so a dcb response becomes a network error. Setting would silently overwrite the author's value. Falling back when either header is present is another option.
- Consider falling back when the request has a `Range` header. Such requests already get [`Accept-Encoding: identity`](https://github.com/whatwg/fetch/blob/4d3401afef60066ee6ebd64230cfc05510b07246/fetch.bs#L6288-L6291) to avoid content-coded partial responses. This layer would append dcb/dcz anyway, giving "identity, dcb, dcz", and a 206 slice of a dcb stream can't be decoded.
- Consider requiring exactly one coding, a byte-case-insensitive match for `dcb` or `dcz`, in the [`Content-Encoding` check](https://github.com/whatwg/fetch/blob/4d3401afef60066ee6ebd64230cfc05510b07246/fetch.bs#L6737-L6741):
  - *extract header list values* can also return failure; *HTTP-network fetch* checks for "null or failure".
  - "Does not contain `dcb` or `dcz`" can be read two ways.
  - Content codings are case-insensitive.
  - `Content-Encoding: dcb, gzip` would currently be "decoded", and then the whole header deleted.
- Consider limiting dictionary use to the modes where it makes sense. WebSocket and WebTransport handshakes have a client and use modes "`websocket`"/"`webtransport`", so they would also get `Available-Dictionary` and dcb/dcz.
- Consider Fetch's convention for header names, `` `<a http-header><code>Use-As-Dictionary</code></a>` ``, with `type:http-header` [anchors](https://github.com/whatwg/fetch/blob/4d3401afef60066ee6ebd64230cfc05510b07246/fetch.bs#L19-L23). Currently `<a><code>Use-As-Dictionary</code></a>` is a dfn link without backticks, passed where a header name (a byte sequence) is expected. The convention builds.
- Consider tuple indexing throughout, e.g. *dictionaryValue*["`match`"][0]. HTML's [*obtain an opener policy*](https://html.spec.whatwg.org/multipage/browsers.html#obtain-coop) does this, including an "is a token" check. The [*bare item* anchor](https://github.com/whatwg/fetch/blob/4d3401afef60066ee6ebd64230cfc05510b07246/fetch.bs#L31) could then go: it links to RFC 9651's "Parsing a Bare Item" algorithm rather than a definition, and the text currently mixes `["match-dest"][0]` with "its bare item".
- Consider adding the [requested note](https://github.com/whatwg/fetch/pull/1854#issuecomment-4519482126) near the [destination list](https://github.com/whatwg/fetch/blob/4d3401afef60066ee6ebd64230cfc05510b07246/fetch.bs#L1837): the hyphen in "`compression-dictionary`" is a mistake kept for compatibility, and new destinations shouldn't repeat it.
- Consider adding WPT tests for:
  - `Use-As-Dictionary` on a redirect response;
  - `Use-As-Dictionary` on a response that fails the CORS check;
  - explicit `match-dest=()`;
  - `Range` requests.

  The existing tests are also still `.tentative`.
- Consider refreshing the commit message:
  - It links the draft and says "RFC pending publication"; RFC 9842 was published in September 2025.
  - It still mentions a "`compression-dictionary`" initiator, which was removed.
  - It doesn't link whatwg/html#11620.
  - "Fix #1739, #1839" would auto-close #1839, which this PR no longer addresses now that no-cors support is gone. #1739 isn't met yet either (see 2).

**Nits**

- Lines over 100 columns: 3350, 6632, 6650, 6652–6653, 6665–6666, 6712, 6747, 6752.
- The [new `<h3>`](https://github.com/whatwg/fetch/blob/4d3401afef60066ee6ebd64230cfc05510b07246/fetch.bs#L6686-L6687) and [`<h3 id=http-network-fetch>`](https://github.com/whatwg/fetch/blob/4d3401afef60066ee6ebd64230cfc05510b07246/fetch.bs#L6769-L6770) each have one blank line before them; the spec uses two.
- Capitalize "Append" and "Delete" at the start of a step ([6729](https://github.com/whatwg/fetch/blob/4d3401afef60066ee6ebd64230cfc05510b07246/fetch.bs#L6729), [6732](https://github.com/whatwg/fetch/blob/4d3401afef60066ee6ebd64230cfc05510b07246/fetch.bs#L6732), [6763](https://github.com/whatwg/fetch/blob/4d3401afef60066ee6ebd64230cfc05510b07246/fetch.bs#L6763)).
- ["Configured to block cookies"](https://github.com/whatwg/fetch/blob/4d3401afef60066ee6ebd64230cfc05510b07246/fetch.bs#L6709): Fetch already says "configured to disable cookies" ([3522](https://github.com/whatwg/fetch/blob/4d3401afef60066ee6ebd64230cfc05510b07246/fetch.bs#L3522), [3560](https://github.com/whatwg/fetch/blob/4d3401afef60066ee6ebd64230cfc05510b07246/fetch.bs#L3560)).
- [`<!-- This is defined in [[!RFC9842]] -->`](https://github.com/whatwg/fetch/blob/4d3401afef60066ee6ebd64230cfc05510b07246/fetch.bs#L6628) repeats what the link already says.
- ["For each"](https://github.com/whatwg/fetch/blob/4d3401afef60066ee6ebd64230cfc05510b07246/fetch.bs#L6652) isn't linked. Infra's conditional remove would avoid mutating the list while iterating over it (moot if the rewrite in 5 lands).
- [Double blank line](https://github.com/whatwg/fetch/blob/4d3401afef60066ee6ebd64230cfc05510b07246/fetch.bs#L80-L81) in the anchors block (moot once the URL Pattern anchors go).
- The new layer changes the request after the ["not further modified" comment and the WebDriver BiDi *before request sent* steps](https://github.com/whatwg/fetch/blob/4d3401afef60066ee6ebd64230cfc05510b07246/fetch.bs#L6362-L6364), so BiDi never sees these headers. Revalidation headers already have the same issue.
- PR checklist: the "Firefox" link points to WebKit's standards-positions #160. The MDN item links to the guide rather than an MDN issue, and `Request.destination` gains a new value.

</details>


-- 
Reply to this email directly or view it on GitHub:
https://github.com/whatwg/fetch/pull/1854#issuecomment-6625171361
You are receiving this because you are subscribed to this thread.

Message ID: <whatwg/fetch/pull/1854/c6625171361@github.com>

Received on Wednesday, 4 November 2026 07:26:05 UTC