Skip to content

Fetch Posture #1946

Description

@jasnell

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions