Repository navigation
Add crossOriginStorage option on RequestInit #1954
Description
What problem are you trying to solve?
Cross-Origin Storage (COS) (spec draft) lets large resources, such as AI models, Wasm modules, JavaScript libraries, and web fonts, be stored once and reused across origins, keyed by their cryptographic hash. Two sites that use the same 3 GB model download it once between them.
Markup, module graphs, and stylesheets can opt a resource into COS alongside its integrity hash: see whatwg/html#12770, whatwg/html#12771, and w3c/csswg-drafts#14056. Script-driven fetches, which is how most large models and Wasm modules load, have no such opt-in.
What solutions exist today?
The imperative navigator.crossOriginStorage.requestFileHandle(). Using it for a Wasm module means about 30 lines: a lookup, a fallback fetch(), a tee() so compilation and storage overlap, a background pipeTo() into COS, and a cancel() on failure (full example).
How would you solve it?
A crossOriginStorage member on RequestInit, used with integrity. crossOriginStorage only makes sense for "GET", and the Request constructor should likely throw a TypeError for any other method.
const { instance } = await WebAssembly.instantiateStreaming(
fetch('https://cdn.example/module.wasm', {
integrity: 'sha256-abc123...',
crossOriginStorage: { origins: '*', contentType: 'application/wasm' },
}),
imports,
);dictionary CrossOriginStorageRequestOptions {
(DOMString or sequence<DOMString>) origins;
DOMString contentType;
};
partial dictionary RequestInit {
(DOMString or sequence<DOMString> or CrossOriginStorageRequestOptions) crossOriginStorage;
};| Value | Meaning |
|---|---|
| absent | No COS participation (today's behavior) |
"" |
Same-site only |
"*" |
Any origin, subject to the Public Hash List |
["https://a.example", …] |
The listed origins only |
{ origins, contentType } |
As origins, plus a declared Content-Type (open question 1) |
"*" and the array form are shorthands for { origins }, and "" for {}. Same-site is spelled "": fetch() has no create: true, so the member's presence is the opt-in and its value is the scope. The array form matches getFileHandle()'s origins exactly; the other integrations use their host's list syntax, since attribute values are text, import attribute values are currently restricted to strings by TC39, and CSS has no arrays.
A list is capped at an implementation-defined length, so it can't approximate "*", and it is bounded by a Cross-Origin-Storage-Allow-Origin header on the fetched resource's response. Whoever supplies the bytes sends the header; unlisted origins are dropped and the fetch still succeeds. "" and "*" need no header.
// `https://acme-cdn.example` must respond with
// `Cross-Origin-Storage-Allow-Origin: https://acme-inc.example, https://acme-cdn.example`.
await fetch('https://acme-cdn.example/acme-inc-corporate.wasm', {
integrity: 'sha256-def456...',
crossOriginStorage: ['https://acme-inc.example', 'https://acme-cdn.example'],
});Processing model, shared with the other integrations:
- Without
crossOriginStorage, nothing changes. - Otherwise, look the hash up in COS first. A disclosable hit is served with no network request.
- Anything else falls through to the network. If the bytes match
integrity, store them in COS with the declared scope.
A failed lookup covers absence, the Public Hash List, GREASE'ing, and the probe budget alike; all look like a plain cache miss by design.
Feature detection: an engine reads only the RequestInit members it supports, so a getter detects the member, as for duplex:
let supported = false;
new Request('data:,', { get crossOriginStorage() { supported = true; } });Browsers without COS, and server runtimes like Node.js, Deno, and Bun, ignore the unknown member and fetch as usual.
Anything else?
Open questions
Content-Typeon a hit. An entry stores bytes only, so a hit has no type, andinstantiateStreaming()rejects anything butapplication/wasm: code that works on a cold cache fails on a warm one. A plainfetch()has no destination to derive a type from, and storing a type would add unverifiable metadata that writers can disagree on. We now favor a caller-declaredcontentType, applied on hits and misses alike (revised from an earlier preference for storing a computed type). Overriding the server's type on a miss seems fine, sinceintegritypins the bytes; please confirm.- Other response fields on a hit. Which
status,statusText,Content-Length,type, andurlshould a hit report, and should it get a Resource Timing entry? - Hook point. Below the service worker, so
respondWith()wins, and probably below the HTTP cache: in HTTP fetch, after the service worker declines? - Streaming and SRI. With
integrityset, main fetch buffers the whole body today, soinstantiateStreaming(fetch(url, { integrity }))doesn't overlap download and compilation. Is streaming SRI on the table, or can at least a COS hit, verified at write time, stream? - Without
integrity. We suggest aTypeErroratRequestconstruction. - Permissions Policy. When
cross-origin-storageis disallowed, we suggest behaving as if the member were absent, matching Addcrossoriginstorageattribute to<link>and<script>html#12770. - Probe budget.
fetch()lookups have to count against the same probe budget asrequestFileHandle(), or a loop of fetches sidesteps it. We suggest Fetch invokes COS's shared algorithm. - Cancelled body on a miss. We suggest abandoning the COS write.
Complementing requestFileHandle()
This adds to the imperative API (WICG/cross-origin-storage#74), which stays necessary for three cases a fetch can't express: bytes that arrive from Background Fetch, Range-assembled shards, or local files; lookups with no URL, such as probing whether a better model variant is already cached; and handles that are transferred or written through.
Activity
Link the filed Fetch Standard issue
Keep the Fetch issue text as filed
Add the WHATWG Fetch issue
[cross-origin-storage] Add tests for the fetch() integration
Drop the Fetch issue draft