Skip to content

Add crossOriginStorage option on RequestInit #1954

Open

Description

@tomayac

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:

  1. Without crossOriginStorage, nothing changes.
  2. Otherwise, look the hash up in COS first. A disclosable hit is served with no network request.
  3. 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

  1. Content-Type on a hit. An entry stores bytes only, so a hit has no type, and instantiateStreaming() rejects anything but application/wasm: code that works on a cold cache fails on a warm one. A plain fetch() 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-declared contentType, 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, since integrity pins the bytes; please confirm.
  2. Other response fields on a hit. Which status, statusText, Content-Length, type, and url should a hit report, and should it get a Resource Timing entry?
  3. Hook point. Below the service worker, so respondWith() wins, and probably below the HTTP cache: in HTTP fetch, after the service worker declines?
  4. Streaming and SRI. With integrity set, main fetch buffers the whole body today, so instantiateStreaming(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?
  5. Without integrity. We suggest a TypeError at Request construction.
  6. Permissions Policy. When cross-origin-storage is disallowed, we suggest behaving as if the member were absent, matching Add crossoriginstorage attribute to <link> and <script> html#12770.
  7. Probe budget. fetch() lookups have to count against the same probe budget as requestFileHandle(), or a loop of fetches sidesteps it. We suggest Fetch invokes COS's shared algorithm.
  8. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    addition/proposalNew features or enhancementsneeds implementer interestMoving the issue forward requires implementers to express interest

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions