From 3cbc60740ab0eb7c07f13ee868cc1943df2b690e Mon Sep 17 00:00:00 2001 From: James M Snell Date: Sat, 11 Jul 2026 08:02:46 -0700 Subject: [PATCH 1/2] Add .trailers to Response/Request Adds `Promise` `trailers` properties to `Request` and `Response` When sending... `trailers` resolves to an empty, immutable `Headers` since we're not dealing with sending trailers in this change. When receiving... `trailers` resolves to an immutable, populated/filtered `Headers` resolved after the body is completed. This means the trailers promise will not resolve if the body is not consumed. --- fetch.bs | 300 ++++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 288 insertions(+), 12 deletions(-) diff --git a/fetch.bs b/fetch.bs index fa6deae20..6dca829f3 100755 --- a/fetch.bs +++ b/fetch.bs @@ -1651,6 +1651,74 @@ these steps: +

Trailer state

+ +

A trailer state is a struct used to represent +HTTP trailer fields received after a response body +([[HTTP]], Section 6.5). +It has: + +

+ +

A trailer state is shared by reference between a +response and its clones. + +

+

To complete a trailer state given a trailer state +trailerState: + +

    +
  1. If trailerState's state is not + "pending", then return. + +

  2. Set trailerState's state to + "complete". + +

  3. For each observer of + trailerState's observers: + run observer. + +

  4. Empty trailerState's + observers. +

+
+ +
+

To error a trailer state given a trailer state +trailerState and a value reason: + +

    +
  1. If trailerState's state is not + "pending", then return. + +

  2. Set trailerState's state to + "errored". + +

  3. Set trailerState's error to + reason. + +

  4. For each observer of + trailerState's observers: + run observer. + +

  5. Empty trailerState's + observers. +

+
+ +

Requests

This section documents how requests work in detail. To get started, see @@ -2579,6 +2647,18 @@ message as HTTP/2 does not support them.

The source and length concepts of a network's response's body are always null. +

A response has an associated +trailer state +(a trailer state). Unless stated otherwise it is a new trailer state. + +

A response's +trailer header list +is its trailer state's header list. + +

The trailer state is shared by reference between a +response and its clones (see clone), ensuring trailer fields +received after cloning are visible to all copies. +

A response has an associated cache state (the empty string, "local", or "validated"). Unless stated otherwise, it is the empty @@ -2692,17 +2772,23 @@ of defining the concrete types of filtered responses.)

A basic filtered response is a filtered response whose -type is "basic" and +type is "basic", header list excludes any headers in internal response's header list whose name is a +forbidden response-header name, and +trailer header list excludes any +headers in +internal response's +trailer header list whose +name is a forbidden response-header name.

A CORS filtered response is a filtered response whose -type is "cors" and +type is "cors", header list excludes any headers in internal response's @@ -2710,6 +2796,14 @@ of defining the concrete types of filtered responses.) name is not a CORS-safelisted response-header name, given internal response's +CORS-exposed header-name list, and +trailer header list excludes any +headers in +internal response's +trailer header list whose +name is not a +CORS-safelisted response-header name, given +internal response's CORS-exposed header-name list.

An opaque filtered response is a @@ -2719,6 +2813,7 @@ of defining the concrete types of filtered responses.) status is 0, status message is the empty byte sequence, header list is « », +trailer header list is « », body is null, and body info is a new response body info. @@ -2729,6 +2824,7 @@ is a filtered response whose status is 0, status message is the empty byte sequence, header list is « », +trailer header list is « », body is null, and body info is a new response body info. @@ -2781,7 +2877,10 @@ console.log((await fetch("/surprise-me", { redirect: "manual" })).type); // "opa internal response.

  • Let newResponse be a copy of response, except for its - body. + body and trailer state. + +

  • Set newResponse's trailer state to + response's trailer state.

  • If response's body is non-null, then set newResponse's body to the result of cloning @@ -5310,6 +5409,9 @@ steps:

  • Set fetchParams's request's done flag. +

  • Complete a trailer state given response's + trailer state. +

  • If fetchParams's process response end-of-body is non-null, then run fetchParams's process response end-of-body given response. @@ -5360,13 +5462,34 @@ steps: flushAlgorithm set to processResponseEndOfBody. -

  • Set internalResponse's body's stream to the - result of internalResponse's body's stream - piped through transformStream. +

  • Let readable be transformStream's + readable. + +

  • Let pipePromise be the result of + piping internalResponse's + body's stream to + transformStream's writable. + +

  • Set internalResponse's body's + stream to readable. + +

  • +

    Upon rejection of pipePromise with + reason: + +

      +
    1. Error a trailer state given response's + trailer state and reason. +

    -

    This {{TransformStream}} is needed for the purpose of receiving a notification when - the stream reaches its end, and is otherwise an identity transform stream. +

    The {{TransformStream}} receives a notification when the body + stream reaches its end (via the flush algorithm) or when it errors (via the + pipe promise rejection). On successful completion, + processResponseEndOfBody completes + the trailer state. On error, the trailer state is + errored, which rejects any pending + {{Response/trailers}} promises.

  • If fetchParams's process response consume body is @@ -6760,6 +6883,21 @@ optional boolean forceNewConnection (default false), run these steps:

  • Break. + +

  • +

    If the HTTP response includes trailer fields + ([[HTTP]], Section 6.5), + then, after the response body has been fully transmitted, set + response's trailer header list to a + header list containing each received trailer field as a + header. + +

    Trailer fields are received after the response body. The HTTP + layer is responsible for omitting trailer fields prohibited by HTTP semantics + (e.g., fields used in transfer codings, content framing, or routing). The + trailer header list is populated before + completing the trailer state + in the fetch response handover steps.

    The exact layering between Fetch and HTTP still needs to be sorted through and @@ -8616,6 +8754,7 @@ interface Request { readonly attribute boolean isHistoryNavigation; readonly attribute AbortSignal signal; readonly attribute RequestDuplex duplex; + readonly attribute Promise<Headers> trailers; [NewObject] Request clone(); }; @@ -8663,6 +8802,9 @@ used or observed from JavaScript.

    A {{Request}} object has an associated signal (null or an {{AbortSignal}} object), initially null. +

    A {{Request}} object has an associated +trailers promise (null or a {{Promise}}), initially null. +

    A {{Request}} object's body is its request's body. @@ -8815,6 +8957,10 @@ object), initially null. See issue #1254 for defining "full". +

    request . trailers +

    Returns a promise that resolves to an empty immutable {{Headers}} object. Client-side + requests do not support sending trailer fields. +

    request . clone()

    Returns a clone of request. @@ -9287,6 +9433,32 @@ set; otherwise false.

    The duplex getter steps are to return "half". +

    +

    The trailers getter steps are: + +

      +
    1. +

      If this's trailers promise is null, then: + +

        +
      1. Let promise be a new promise. + +

      2. Resolve promise with a new {{Headers}} object + whose header list is « » and whose guard is + "immutable". + +

      3. Set this's trailers promise to promise. +

      + +
    2. Return this's trailers promise. +

    +
    + +

    Client-side requests do not support sending trailer fields. The +{{Request/trailers}} attribute always returns a promise that resolves to empty +immutable {{Headers}}. This does not foreclose future extension to support sending +trailer fields. +


    @@ -9331,6 +9503,7 @@ interface Response { readonly attribute boolean ok; readonly attribute ByteString statusText; [SameObject] readonly attribute Headers headers; + readonly attribute Promise<Headers> trailers; [NewObject] Response clone(); }; @@ -9352,6 +9525,9 @@ enum ResponseType { "basic", "cors", "default", "error", "opaque", "opaqueredire

    A {{Response}} object also has an associated headers (null or a {{Headers}} object), initially null. +

    A {{Response}} object has an associated +trailers promise (null or a {{Promise}}), initially null. +

    A {{Response}} object's body is its response's body. @@ -9394,8 +9570,20 @@ enum ResponseType { "basic", "cors", "default", "error", "opaque", "opaqueredire

    response . headers

    Returns response's headers as {{Headers}}. +

    response . trailers +
    +

    Returns a promise that resolves to {{Headers}} containing the response's trailer fields. + +

    For responses obtained from {{WindowOrWorkerGlobalScope/fetch()}}, the promise resolves after the + response body has been fully received. For synthetically constructed responses (via the + {{Response/Response()}} constructor, {{Response/error()}}, {{Response/redirect()}}, or + {{Response/json()}}), the promise resolves immediately to empty immutable {{Headers}}. + +

    If the response body stream errors, the promise is rejected. +

    response . clone() -

    Returns a clone of response. +

    Returns a clone of response. The clone shares the same + trailer state, so both will receive the same trailer fields.


    @@ -9486,12 +9674,25 @@ constructor steps are:
  • Perform initialize a response given this, init, and bodyWithType. + +

  • Complete a trailer state given this's response's + trailer state. -

    The static error() method steps are to return the -result of creating a {{Response}} object, given a new network error, -"immutable", and the current realm. +

    +

    The static error() method steps are: + +

      +
    1. Let responseObject be the result of creating a {{Response}} + object, given a new network error, "immutable", and the current realm. + +

    2. Complete a trailer state given responseObject's + response's trailer state. + +

    3. Return responseObject. +

    +

    The static @@ -9518,6 +9719,9 @@ are:

  • Append (`Location`, value) to responseObject's response's header list. +

  • Complete a trailer state given responseObject's + response's trailer state. +

  • Return responseObject. @@ -9540,6 +9744,9 @@ are:

  • Perform initialize a response given responseObject, init, and (body, "application/json"). +

  • Complete a trailer state given responseObject's + response's trailer state. +

  • Return responseObject. @@ -9574,6 +9781,72 @@ otherwise false.

    The headers getter steps are to return this's headers. +

    +

    The trailers getter steps are: + +

      +
    1. If this's trailers promise is null, then: + +

        +
      1. Let response be this's response. + +

      2. Let trailerState be response's + trailer state. + +

      3. Let promise be a new promise. + +

      4. +

        If trailerState's state is + "complete", then: + +

          +
        1. Let headers be a new {{Headers}} object whose + header list is response's + trailer header list and whose guard is + "immutable". + +

        2. Resolve promise with headers. +

        + +
      5. +

        Otherwise, if trailerState's state is + "errored", then reject promise with + trailerState's error. + +

      6. +

        Otherwise (trailerState's state is + "pending"): + +

          +
        1. +

          Append the following steps to trailerState's + observers: + +

            +
          1. If trailerState's state is + "complete", then: + +

              +
            1. Let headers be a new {{Headers}} object whose + header list is response's + trailer header list and whose guard is + "immutable". + +

            2. Resolve promise with headers. +

            + +
          2. Otherwise, reject promise with + trailerState's error. +

          +
        + +
      7. Set this's trailers promise to promise. +

      + +
    2. Return this's trailers promise. +

    +
    +
    @@ -9741,6 +10014,9 @@ with a promise, request, responseObject, and an
  • If response's body is non-null and is readable, then error response's body with error. + +

  • Error a trailer state given response's + trailer state and error. From b0c272ed76e0bbededc68b57ee1a627c0506a372 Mon Sep 17 00:00:00 2001 From: James M Snell Date: Sat, 11 Jul 2026 10:50:55 -0700 Subject: [PATCH 2/2] Support sending trailers with new option Adds a new constructor option to both `Request` and `Response` that allows sending trailing header fields: ```js async function trailersReady() { // ... return { "X-Checksum": '...' }; } const response = new Response(readable, { headers: { "Trailer": "X-Checksum" }, trailers: trailersReady(), }); ``` It is defined such that support is entirely optional. A conformant implementation can simply ignore the option entirely, meaning all existing implementations are ok out of the box. An implementation that chooses to at least pay some attention to the option but not actually send the headers should at least mark the promise as handled to avoid unhandled rejection warnings, but that would be the minimum support requirement. --- fetch.bs | 277 ++++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 252 insertions(+), 25 deletions(-) diff --git a/fetch.bs b/fetch.bs index 6dca829f3..c5eb40d98 100755 --- a/fetch.bs +++ b/fetch.bs @@ -1654,8 +1654,9 @@ these steps:

    Trailer state

    A trailer state is a struct used to represent -HTTP trailer fields received after a response body -([[HTTP]], Section 6.5). +HTTP trailer fields +([[HTTP]], Section 6.5) +associated with a request or response. It has:

      @@ -1673,7 +1674,8 @@ It has:

    A trailer state is shared by reference between a -response and its clones. +request or response and its +clones.

    To complete a trailer state given a trailer state @@ -1718,6 +1720,92 @@ It has:

    +
    +

    To process a trailers init given a trailer state +trailerState, a {{Promise}} trailersPromise, a +headers guard guard, and a header list +headerList: + +

      +
    1. +

      Upon fulfillment of trailersPromise with + init: + +

        +
      1. Let trailerHeaders be a new {{Headers}} object whose + guard is guard. + +

      2. Fill trailerHeaders with init. + +

      3. Let declaredNames be the result of + getting, decoding, and splitting + `Trailer` from headerList. + +

      4. If declaredNames is null, then set declaredNames + to « ». + +

      5. Let lowercasedDeclaredNames be a new list. + +

      6. For each name of declaredNames, + append name, byte-lowercased, to + lowercasedDeclaredNames. + +

      7. Let namesToRemove be a new list. + +

      8. +

        For each header of trailerHeaders's + header list: + +

          +
        1. If lowercasedDeclaredNames does not contain + header's name, then + append header's name to + namesToRemove. +

        + +
      9. For each name of namesToRemove, + delete name from + trailerHeaders's header list. + +

      10. Set trailerState's header list + to trailerHeaders's header list. + +

      11. Complete a trailer state given trailerState. +

      + +
    2. +

      Upon rejection of trailersPromise with + reason: + +

        +
      1. Error a trailer state given trailerState and + reason. +

      +
    + +

    The guard applied to trailerHeaders +ensures that forbidden request-header names (for requests) or +forbidden response-header names (for responses) are excluded via the +standard {{Headers}} filtering before the `Trailer` declaration +filter is applied. Trailer field names present in the resolved {{HeadersInit}} +but not pre-declared in the `Trailer` header are silently dropped. +

    + +
    +

    To ignore a trailers init given a {{Promise}} +trailersPromise: + +

      +
    1. Mark as handled trailersPromise. +

    + +

    This algorithm is provided for use by specifications that extend +Fetch and need to discard a user-provided {{RequestInit/trailers}} or +{{ResponseInit/trailers}} promise without triggering unhandled rejection +warnings. Within this specification, process a trailers init handles +promise rejection directly. +

    +

    Requests

    @@ -2373,6 +2461,17 @@ Unless stated otherwise, it is unset.

    A request has an associated done flag. Unless stated otherwise, it is unset. +

    A request has an associated +trailer state +(a trailer state). Unless stated otherwise it is a new trailer state +whose state is "complete". + +

    A request's trailer state defaults to +"complete" with an empty header list, +representing a request with no trailer fields to send. When the +{{RequestInit/trailers}} option is provided, the trailer state +starts as "pending" and is completed when the promise resolves. +

    A request has an associated timing allow failed flag. Unless stated otherwise, it is unset. @@ -6884,6 +6983,29 @@ optional boolean forceNewConnection (default false), run these steps:

  • Break. +

  • +

    If request's trailer state's + state is "pending", the + implementation MAY, after the request body has been fully transmitted, + observe request's trailer state and send + trailer fields from its header list once it + is completed. Implementations that + do not support sending trailer fields MAY ignore the trailer state + entirely. + +

    Sending trailer fields is entirely optional. + Implementations that do not support sending trailer fields are fully + compliant with this specification. The {{RequestInit/trailers}} option + exists to provide a standardized interface for implementations that do + support sending trailer fields. Promise rejection is handled by the + process a trailers init algorithm (which is called during + request construction), so implementations that ignore the trailer state + do not cause unhandled promise rejections. + Whether or not to actually forward trailer fields is an implementation + decision, consistent with how HTTP intermediaries may forward, discard, + or store trailer fields + ([[HTTP]], Section 6.5). +

  • If the HTTP response includes trailer fields ([[HTTP]], Section 6.5), @@ -8775,6 +8897,7 @@ dictionary RequestInit { AbortSignal? signal; RequestDuplex duplex; RequestPriority priority; + Promise<HeadersInit> trailers; any window; // can only be set to null }; @@ -8884,6 +9007,12 @@ object), initially null.

    {{RequestInit/priority}}
    A string to set request's priority. + +
    {{RequestInit/trailers}} +
    A promise that resolves to {{HeadersInit}} providing trailer fields to send after the + request body. Trailer field names must be pre-declared in the `Trailer` header; + undeclared trailer fields are silently dropped. Implementations are not required to send + trailer fields and may ignore this option entirely.
    request . method @@ -8958,8 +9087,21 @@ object), initially null. defining "full".
    request . trailers -

    Returns a promise that resolves to an empty immutable {{Headers}} object. Client-side - requests do not support sending trailer fields. +

    +

    Returns a promise that resolves to immutable {{Headers}} containing the request's + trailer fields. + +

    If the {{RequestInit/trailers}} option was provided, the promise resolves when + the provided promise fulfills, with trailer fields filtered by the `Trailer` + header declaration. If no {{RequestInit/trailers}} option was provided, the + promise resolves immediately to empty immutable {{Headers}}. + +

    The resolution timing of this promise reflects the timing of the + user-provided {{RequestInit/trailers}} promise, not the timing of body + transmission. This may indirectly reveal when payload processing completes if + the caller ties the promise to body consumption (e.g., computing a digest over + the body stream), but this timing is controlled by the caller, not the + implementation.

    request . clone()

    Returns a clone of request. @@ -9364,6 +9506,21 @@ constructor steps are:

  • Set this's request's body to finalBody. + +

  • +

    If init["{{RequestInit/trailers}}"] exists, then: + +

      +
    1. Let trailerState be a new trailer state. + +

    2. Set this's request's trailer state + to trailerState. + +

    3. Process a trailers init given trailerState, + init["{{RequestInit/trailers}}"], + this's headers's guard, and + this's request's header list. +

    @@ -9437,27 +9594,72 @@ set; otherwise false.

    The trailers getter steps are:

      -
    1. -

      If this's trailers promise is null, then: +

    2. If this's trailers promise is null, then: -

        -
      1. Let promise be a new promise. +

          +
        1. Let trailerState be this's request's + trailer state. -

        2. Resolve promise with a new {{Headers}} object - whose header list is « » and whose guard is - "immutable". +

        3. Let promise be a new promise. -

        4. Set this's trailers promise to promise. -

        +
      2. +

        If trailerState's state is + "complete", then: + +

          +
        1. Let headers be a new {{Headers}} object whose + header list is trailerState's + header list and whose guard is + "immutable". + +

        2. Resolve promise with headers. +

        + +
      3. +

        Otherwise, if trailerState's state is + "errored", then reject promise with + trailerState's error. + +

      4. +

        Otherwise (trailerState's state is + "pending"): + +

          +
        1. +

          Append the following steps to trailerState's + observers: + +

            +
          1. If trailerState's state is + "complete", then: + +

              +
            1. Let headers be a new {{Headers}} object whose + header list is trailerState's + header list and whose guard is + "immutable". + +

            2. Resolve promise with headers. +

            + +
          2. Otherwise, reject promise with + trailerState's error. +

          +
        + +
      5. Set this's trailers promise to promise. +

    3. Return this's trailers promise.

    -

    Client-side requests do not support sending trailer fields. The -{{Request/trailers}} attribute always returns a promise that resolves to empty -immutable {{Headers}}. This does not foreclose future extension to support sending -trailer fields. +

    When no {{RequestInit/trailers}} option is provided, the +trailer state is immediately "complete" with an +empty header list, so the {{Request/trailers}} promise +resolves to empty immutable {{Headers}}. When a {{RequestInit/trailers}} option is +provided, the promise resolves to the filtered trailer fields once the provided +promise fulfills.


    @@ -9513,6 +9715,7 @@ dictionary ResponseInit { unsigned short status = 200; ByteString statusText = ""; HeadersInit headers; + Promise<HeadersInit> trailers; }; enum ResponseType { "basic", "cors", "default", "error", "opaque", "opaqueredirect" }; @@ -9575,9 +9778,15 @@ enum ResponseType { "basic", "cors", "default", "error", "opaque", "opaqueredire

    Returns a promise that resolves to {{Headers}} containing the response's trailer fields.

    For responses obtained from {{WindowOrWorkerGlobalScope/fetch()}}, the promise resolves after the - response body has been fully received. For synthetically constructed responses (via the - {{Response/Response()}} constructor, {{Response/error()}}, {{Response/redirect()}}, or - {{Response/json()}}), the promise resolves immediately to empty immutable {{Headers}}. + response body has been fully received. For synthetically constructed responses (via + {{Response/error()}}, {{Response/redirect()}}, or + {{Response/json()}} and the {{Response/Response()}} constructor without a + {{ResponseInit/trailers}} option), the promise resolves immediately to empty immutable + {{Headers}}. + +

    If the {{ResponseInit/trailers}} option was provided (to the {{Response/Response()}} + constructor or {{Response/json()}}), the promise resolves when the provided promise fulfills, + with trailer fields filtered by the `Trailer` header declaration.

    If the response body stream errors, the promise is rejected. @@ -9675,8 +9884,17 @@ constructor steps are:

  • Perform initialize a response given this, init, and bodyWithType. -

  • Complete a trailer state given this's response's - trailer state. +

  • +

    If init["{{ResponseInit/trailers}}"] exists, then + process a trailers init given this's response's + trailer state, + init["{{ResponseInit/trailers}}"], + this's headers's guard, and + this's response's header list. + +

  • +

    Otherwise, complete a trailer state given this's + response's trailer state. @@ -9744,8 +9962,17 @@ are:

  • Perform initialize a response given responseObject, init, and (body, "application/json"). -

  • Complete a trailer state given responseObject's - response's trailer state. +

  • +

    If init["{{ResponseInit/trailers}}"] exists, then + process a trailers init given responseObject's + response's trailer state, + init["{{ResponseInit/trailers}}"], + responseObject's headers's guard, and + responseObject's response's header list. + +

  • +

    Otherwise, complete a trailer state given responseObject's + response's trailer state.

  • Return responseObject.