Skip to content

FR: request trailersΒ #44

Description

@passcod

πŸ€– The fetch spec's trailers proposal (whatwg/fetch#1940) adds trailers: Promise<Headers> to RequestInit β€” request trailers, sent after the body. Recording what the stack underneath us can actually do, ahead of that landing, because the answer differs per protocol and one of the three is silent about it.

Verified by reading the pinned dependencies (reqwest 0.13.4, hyper 1.8.1, h3 0.0.8), not by running anything.

reqwest can carry them, but not the way we build bodies today

reqwest::Body::wrap<B: http_body::Body> (src/async_impl/body.rs:138) accepts any http-body, and its poll_frame passes frames through untouched (f.map_data(Into::into)), so a trailers frame survives into reqwest.

What we use is reqwest::Body::wrap_stream (src/fetch.rs:113), fed by StreamBodyReceiver::into_stream β€” a stream of Bytes. There is no frame in that type, so trailers are unrepresentable on our current path regardless of protocol.

HTTP/1.1: yes, under two conditions

hyper's client will write them. The gate in write_trailers is T::is_server() && !self.state.allow_trailer_fields, which does not apply to a client. But the encoder has to be chunked and carry the allowed trailer fields, which hyper takes from the request's own Trailer header (proto/h1/role.rs:1390):

let allowed_trailer_fields: Vec<HeaderValue> =
    headers.get_all(header::TRAILER).iter().cloned().collect();
if !allowed_trailer_fields.is_empty() {
    return enc.into_chunked_with_trailing_fields(allowed_trailer_fields);
}

Without it, Kind::Chunked(None) logs "attempted to encode trailers, but the trailer header is not set" and drops them. With it, each name is filtered twice more: it must appear in Trailer, and it must pass is_valid_trailer_field. Anything else is dropped with a debug log.

So HTTP/1.1 needs: no Content-Length (chunked), a Trailer: header naming the fields, and only those names reach the wire.

HTTP/2: yes, natively

proto/h2/mod.rs:168 forwards a trailers frame to send_trailers. Note it also warns that a TE header set to anything other than trailers is illegal in HTTP/2, which is worth knowing if we ever set TE ourselves.

HTTP/3: silently dropped

reqwest's h3 request-body pump, src/async_impl/h3_client/pool.rs:229:

Some(Ok(frame)) => {
    if let Ok(b) = frame.into_data() {
        if let Err(e) = send.send_data(Bytes::copy_from_slice(&b)).await { ... }
    }
}

A trailers frame fails into_data(), so the if let does nothing and the loop moves on; send.finish() then ends the stream. The h3 crate has RequestStream::send_trailers, so this is reqwest not wiring it up rather than a protocol limitation.

This is the part that matters most for us: HTTP/3 is not a mode the caller selects, it is a mode we upgrade into via Alt-Svc. So the same upgrade path that #23 broke would silently change request semantics β€” a request whose trailers arrived over TCP yesterday would send none today, with nothing observable at the call site.

What implementing it needs

  1. A frame-carrying body: StreamBodySender moves Bytes today, and would need to move Frame<Bytes> (or a Bytes-plus-final-HeaderMap pair), passed to reqwest with Body::wrap rather than wrap_stream.
  2. A one-shot equivalent for non-streaming bodies (string, Buffer, URLSearchParams), which currently become reqwest::Body::from β€” a body that yields one data frame and then a trailers frame.
  3. Derive the Trailer header from the resolved Headers' names for HTTP/1.1. hyper requires it, callers will not know to set it, and the failure mode without it is a silent drop.
  4. Await the promise at the end of the body, which is what the spec describes and what a frame-based body models directly.
  5. Decide what HTTP/3 does. Silently dropping is not an option; the choices are to reject the request when trailers are requested and the connection is h3, or to suppress the upgrade for requests carrying trailers. The second keeps the request working at the cost of a slower path, and is invisible to the caller β€” which cuts both ways.

Point 5 wants deciding before any of the rest is built, since it shapes whether trailers are a body concern or an agent-level one.

Testing it

The conformance harness (#25) already has a trailers dimension for response trailers, and the rows that can emit them (node-h1, node-h2, haproxy-h1, haproxy-h2). Request trailers need a row that can report what it received, which the Node origin can do β€” it already reports GOAWAY counts for the h2 GOAWAY dimension, so the shape exists. That would give per-protocol evidence rather than the source reading above, including confirming the HTTP/3 drop.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions