- From: James M Snell <notifications@github.com>
- Date: Wed, 12 Aug 2026 11:31:44 -0700
- To: whatwg/fetch <fetch@noreply.github.com>
- Cc: Subscribed <subscribed@noreply.github.com>
- Message-ID: <whatwg/fetch/issues/1946@github.com>
jasnell created an issue (whatwg/fetch#1946)
### What is the issue with the Fetch Standard?
## Problem
Fetch's design reflects the security and implementation constraints of web browsers. This model produces constraints throughout the spec:
* Forbidden request-headers prevent scripts from spoofing `Origin`, `Cookie`, `Host`, `Referer`, and other headers the browser must control.
* Forbidden response-header names prevent scripts from reading `Set-Cookie` to protect the `HttpOnly` cookie boundary.
* CORS mediates cross-origin access, including preflight, response filtering (opaque/CORS filtered responses), and response tainting.
* Request modes (`no-cors`, `cors`, `same-origin`, `navigate`) gate behavior based on the browser's origin model.
* Headers guards (`request`, `request-no-cors`, `response`, `immutable`) enforce header restrictions at the API level.
* Service workers, CSP, COEP/CORP, mixed content, referrer policy, and deferred fetch/fetchLater are browser-specific mechanisms threaded through the core algorithms.
Non-browser JavaScript runtimes have adopted the Fetch API (`fetch()`, `Request`, `Response`, `Headers`) as a standard HTTP client and server interface. These runtimes operate in a different trust model:
* The sandboxing constraints differ from browsers. Some runtimes (e.g. Workers) do sandbox untrusted code, but under a different security model with different requirements. Other runtimes do not sandbox at all.
* There is no single global user. Browsers act on behalf of one user whose credentials (cookies, client certs) are attached to outgoing requests automatically. Server-side runtimes typically handle requests from many users and manage credentials per-request.
* There is no browser cookie jar. Cookie management is the application's responsibility.
* There is no same-origin policy. The application controls all outbound requests.
* There are no service workers intercepting fetches.
* There is no document, navigable, or browsing context.
These other runtimes have independently diverged from the fetch spec by relaxing forbidden headers, ignoring CORS, and stripping navigation-related properties. The divergence is largely undocumented and inconsistent between runtimes. Code that moves between runtimes encounters subtle incompatibilities, and none of these runtimes can claim Fetch spec conformance despite implementing substantial parts of the spec correctly.
## Proposal: Posture
A **posture** is a property of the environment in which `fetch()` is invoked.
It is effectively a conformance profile.
The fetch posture can be one of:
- `'sandboxed'`: The current (default) posture. The environment enforces the browser's multi-origin sandboxing model. All existing spec behavior applies unchanged.
- `'unrestricted'`: The environment does not enforce multi-origin sandboxing. Browser-specific restrictions are relaxed as described below.
(we can bikeshed the names..)
The posture is simply a constant internal property of the runtime. It is **not** a new API or configurable option.
The default posture is `'sandboxed'`. This means:
* Existing spec text does not need to be updated to say "if sandboxed, do X." The existing text *is* the sandboxed behavior by default.
* Only the `'unrestricted'` deviations need explicit annotation, using language like:
> If the **fetch posture** is `'unrestricted'`, skip this step.
* Browser implementations remain fully compliant (or as compliant as they already are) **without any changes**.
Bottom line: The `'sandboxed'` posture is the spec as it exists today.
The spec **only needs** new conditional text at the specific points where `'unrestricted'` behavior diverges. If you don't support the `'unrestricted'` posture, you ignore those branches.
## What changes in `'unrestricted'` posture
### No forbidden Request-headers
In `'unrestricted'` posture, no request-headers are forbidden. The `forbidden request-header` algorithm returns false for all headers.
Spec change: In the definition of "forbidden request-header" (2.2.2), add a short-circuit:
> A header (*name*, *value*) is a **forbidden request-header** if the following conditions
> are true:
>
> 1. If the **fetch posture** is `'unrestricted'`, return false.
> 2. [existing conditions]
**API impact:**
- `Headers.validate()` step 3 becomes a no-op (guard `'request'` never blocks).
- The `'request-no-cors'` guard becomes equivalent to `'request'` (but see 4.3).
- `Cookie`, `Cookie2`, `Origin`, `Host`, `Referer`, `Set-Cookie` and all others can be freely set on outgoing requests.
- The method-override smuggling check (`X-HTTP-Method`, etc.) also becomes a no-op.
### No forbidden Response-headers
In `'unrestricted'` posture, no response-heades are forbidden. `Set-Cookie` and `Set-Cookie2` are visible on response headers.
**Spec changes:**
- In "forbidden response-header name" (2.2.2): gate the definition on sandboxed posture.
- In "basic filtered response" (2.2.6): do not strip forbidden response-header names in
unrestricted posture (or, more precisely, the set is empty, so there is nothing to strip).
- `Headers.validate()` step 4 becomes a no-op (guard `'response'` never blocks).
### CORS is bypassed
In `'unrestricted'` posture, the CORS protocol is not enforced. All requests are treated as if they are same-origin for the purposes of response tainting and filtering.
**Spec changes:** In **main fetch** (4.1), the response tainting decision tree changes:
- Response tainting is always `'basic'` in unrestricted posture.
- The response is always a **basic filtered response** (never CORS-filtered or opaque).
- CORS preflight is never triggered.
- The CORS check algorithm is never invoked.
- `Access-Control-*` response headers are not processed for filtering purposes (though they remain visible as ordinary headers on the response).
**API impact:**
- `Response.type` is always `'default'` or `'error'` (never `'cors'`, `'opaque'`, or `'opaqueredirect'`).
- Redirected responses are fully visible (not opaque-redirect filtered).
- The `redirect: "manual"` option returns a full, non-opaque redirect response with its actual status (3xx), headers (including `Location`), and body visible. In sandboxed posture, this produces an opaque-redirect filtered response per "Atomic HTTP redirect handling," a security measure to prevent scripts from observing redirect targets. This concern does not apply in unrestricted posture. The `Response.type` for a manually-intercepted redirect is `'default'`.
### Request modes are simplified
In `'unrestricted'` posture:
- `RequestInit.mode` is silently ignored. The internal request mode is effectively always `'cors'` in the sense that no CORS-safelisted restrictions are applied, but the CORS *enforcement* is bypassed.
- `RequestInit.credentials` is silently ignored. Credentials mode has no meaning without a cookie jar and CORS enforcement.
**Browser-specific `Request` properties** (`mode`, `credentials`, `destination`, `referrer`, `referrerPolicy`, `keepalive`, `isReloadNavigation`, `isHistoryNavigation`) follow a **presence-optional, value-undefined** rule:
- Implementations MAY include these properties on the `Request` prototype, or MAY omit them.
- If a property IS present, its getter MUST return `undefined`.
- If a property is NOT present, accessing it returns `undefined` by normal JS semantics.
This avoids choosing between "absent" (breaks code that accesses without checking) and "present-but-fixed" (returns misleading values like `'cors'` for `mode`). Either way, `request.mode === "cors"` evaluates to `false`, and `request.mode === undefined` or `request.mode == null` evaluates to `true`. Code that needs to feature-detect can use `'mode' in request`, and code that doesn't check gets `undefined` rather than a misleading value.
The spec would express this as:
> The **mode** getter steps are: if the **fetch posture** is `'unrestricted'`, return undefined.
> Otherwise, [existing steps].
**Headers guard simplification:**
- The `'request-no-cors'` guard is never used (since `no-cors` mode is not meaningful).
- Headers guards reduce to: `'none'`, `'request'`, `'response'`, `'immutable'`.
- The `'request'` guard imposes no restrictions (since forbidden request-headers are empty).
### No forbidden methods
In `'unrestricted'` posture, there are no forbidden methods. The `Request` constructor does not throw for `CONNECT`, `TRACE`, or `TRACK`.
### Referrer and referrer policy are not applicable
In `'unrestricted'` posture:
- `RequestInit.referrer` and `RequestInit.referrerPolicy` are silently ignored.
- The "determine request's referrer" step in main fetch is skipped.
- No `Referer` header is automatically appended (the application may set it manually via the
now-unrestricted headers).
### Service Worker Interception is not applicable
In `'unrestricted'` posture, the service worker interception block in HTTP fetch is skipped. The request's `service-workers mode` is effectively always `'none'`.
### Browser Security Policies are not applicable
In `'unrestricted'` posture, the following main fetch steps are skipped:
- "report Content Security Policy violations for request"
- "should request be blocked by Content Security Policy"
- "should fetching request be blocked as mixed content"
- "Upgrade a mixed content request to a potentially trustworthy URL"
- HSTS upgrade
- Cross-Origin Resource Policy (CORP) check
- "nosniff" MIME type blocking
- Cross-Origin-Embedder-Policy (COEP) checks
### Port Blocking is implementation-defined
In `'unrestricted'` posture, port blocking ("should be blocked due to a bad port") is implementation-defined. Runtimes MAY choose to block bad ports or not.
### `RequestInit.cache` default is implementation-defined
In `'unrestricted'` posture, the default value for `RequestInit.cache` is implementation-defined rather than `'default'`.
The browser's `'default'` cache mode implies interaction with the browser's HTTP cache, which non-browser runtimes typically do not have. Runtimes like Deno default to `'no-store'` or `'no-cache'`, while Cloudflare Workers has its own caching semantics. Rather than mandating a single default, the spec acknowledges that the appropriate default depends on the runtime's caching architecture. All valid `RequestInit.cache` values MUST be accepted without throwing; the runtime's actual caching behavior for each value is implementation-defined, but the corresponding `Cache-Control` request headers SHOULD be sent.
### Subresource Integrity is implementation-defined
In `'unrestricted'` posture, support for `RequestInit.integrity` is implementation-defined.
- Implementations MUST accept `undefined` and `''` (empty string) without throwing.
- If a non-empty integrity string is provided and the implementation does not support that integrity algorithm, it MUST throw (not silently ignore).
- Implementations MAY support any subset of integrity algorithms (including none beyond the empty string).
SRI is useful on the server (verifying downloaded artifacts, for instance) but is not universally needed. Making it implementation-defined allows runtimes to adopt it incrementally without being non-conformant.
### Keepalive body size limits are implementation-defined
In `'unrestricted'` posture, the keepalive body size limits enforced per fetch group in HTTP-network-or-cache fetch (4.6) are implementation-defined.
### fetchLater / Deferred Fetch are not available
`fetchLater()`, `FetchLaterResult`, `DeferredRequestInit`, and the entire deferred fetch infrastructure (4.12) are not applicable in `'unrestricted'` posture.
### Navigation-related Request properties are presence-optional, value-undefined
The following request properties are not meaningful in `'unrestricted'` posture and follow the **presence-optional, value-undefined** rule described in 4.4:
* `destination` (no document destinations)
* `referrer`, `referrerPolicy` (see 4.6)
* `isReloadNavigation`, `isHistoryNavigation`
* `keepalive` (tied to tab lifetime)
* `mode`, `credentials` (see 4.4)
Implementations SHOULD include these on the `Request` prototype but MAY omit them. If present, their getters MUST return `undefined`.
### `file:` URL Support is implementation-defined
In `'unrestricted'` posture, `fetch()` of `file:` URLs is implementation-defined. Runtimes MAY
support it. The spec does not define the behavior but acknowledges it as a valid extension point.
### Relative URL resolution is mplementation-Defined
In `'unrestricted'` posture, relative URL resolution in `fetch()` is implementation-defined. Runtimes have varying base URL semantics (CWD, module URL, explicit base).
## What does not change
Everything else that's not mentioned.
## Spec integration pattern
Since `'sandboxed'` is the default posture, existing spec text does not need to be wrapped in "if sandboxed" conditionals. Changes are only needed at the points where unrestricted behavior diverges, expressed as:
> If the **fetch posture** is `'unrestricted'`, skip this step.
or:
> If the **fetch posture** is `'unrestricted'`, [alternative behavior].
This keeps edits localized. The spec does not need a "if sandboxed, do X" conditional around every browser-specific step; those steps apply by default.
### Conformance statement
The spec's conformance section should include:
> A conformant implementation operates in one of two **fetch posture**:
>
> - **Sandboxed** (default): The implementation enforces the multi-origin sandboxing model.
> All normative requirements apply as written. This is the conformance mode for web browsers.
> Any algorithm step or requirement that does not reference the fetch posture assumes sandboxed.
> - **Unrestricted**: The implementation does not enforce the multi-origin sandboxing model.
> Requirements that are explicitly conditioned on the unrestricted posture are modified or
> skipped as specified. All other requirements apply as written.
>
> An implementation MUST NOT mix postures within a single environment (e.g., a runtime cannot
> enforce CORS for some requests and not others based on runtime conditions, unless it is
> implementing the sandboxed posture and using the spec's existing conditional mechanisms like
> request mode).
### WPT implications
Web Platform Tests would need a mechanism to test both modes. For unrestricted-posture tests:
- Tests that verify forbidden headers are rejected should have unrestricted-posture counterparts that verify they are accepted.
- Tests for CORS enforcement should have unrestricted-posture counterparts that verify CORS is not enforced.
- Tests for opaque/filtered responses should have counterparts verifying basic responses.
## Where is this coming from
I'm the one opening this issue but the proposal here is coming from the ECMA TC-55 / WinterTC, representing multiple Web compatible runtimes includes Node.js, Deno, Cloudflare Workers, etc.
--
Reply to this email directly or view it on GitHub:
https://github.com/whatwg/fetch/issues/1946
You are receiving this because you are subscribed to this thread.
Message ID: <whatwg/fetch/issues/1946@github.com>
Received on Wednesday, 12 August 2026 18:31:49 UTC