Skip to content

Strip te, trailer and proxy-connection hop-by-hop headers in Falcon::Middleware::Proxy. - #370

Open
samuel-williams-shopify wants to merge 6 commits into
mainfrom
proxy-hop-by-hop-headers
Open

samuel-williams-shopify wants to merge 6 commits into
mainfrom
proxy-hop-by-hop-headers

Conversation

@samuel-williams-shopify

@samuel-williams-shopify samuel-williams-shopify commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor

Falcon::Middleware::Proxy now strips te, trailer and proxy-connection from requests before forwarding them upstream, while preserving te: trailers, and removes the stale host header when it rewrites the request authority.

Problem

HOP_HEADERS listed connection, keep-alive, public, proxy-authenticate, proxy-authorization, transfer-encoding and upgrade, but omitted three connection-specific fields that intermediaries must remove:

  • te describes transfer codings the immediate peer will accept. Forwarding it to an HTTP/2 upstream (the default for falcon virtual, since Environment::Application#protocol is HTTP/2) produces a malformed request per RFC 9113 §8.2.2 unless the value is exactly trailers. async-http tolerates this today, but a stricter upstream would reset the stream.
  • trailer is meaningless once transfer-encoding has been stripped and the body is de-chunked for the upstream connection (RFC 9112 §7.1.3).
  • proxy-connection is named in RFC 9110 §7.6.1 and RFC 9112 Appendix C.2.2 as a field intermediaries must remove; it is widely emitted by clients.

Separately, HTTP/2 ingress may carry a host field alongside :authority. prepare_request rewrote request.authority but left host untouched, so an HTTP/2 upstream would see :authority and host disagree (RFC 9113 §8.3.1), and an HTTP/1.1 upstream could receive two host lines (one written from request.authority, one forwarded).

Solution

Add the three field names to HOP_HEADERS (keeping the list alphabetical) and correct the comment's RFC attribution: RFC 9110 §7.6.1 for the connection-specific fields, RFC 9112 §7.1.3 for trailer, the legacy RFC 2616 §13.5.1 list for the proxy-authentication fields, and RFC 2068 §14.35 for public (a response-only relic, left in place).

prepare_headers captures te before any extraction, strips everything in HOP_HEADERS, then re-adds te: trailers if the client advertised trailers. RFC 9113 §8.2.2 permits exactly this value over HTTP/2, and gRPC's PROTOCOL-HTTP2 makes it mandatory ("used to detect incompatible proxies"); Envoy normalises to te: trailers and nginx grpc_pass injects it, and curl reverted an unconditional strip for the same reason (curl/curl#17122). This is truthful because the proxy forwards request trailers end-to-end: the ingress Headers object is shared with the upstream request, and the async-http client calls trailer! on it. The value is captured before extracting fields named by connection, because a compliant HTTP/1.1 client sends te: trailers with connection: te; reading it afterwards would drop exactly the well-formed case.

One caveat: over HTTP/1.1, RFC 9110 §10.1.4 says a sender of te MUST also list it in connection. The proxy cannot know the upstream protocol in prepare_headers, and the async-http client writes its own connection header, so the forwarded te: trailers is not accompanied by connection: te. Envoy and nginx accept the same trade-off, and in practice it only matters for HTTP/1.1 upstreams.

prepare_request now deletes host after rewriting the authority. HTTP/1.1 ingress already strips host (protocol-http1 removes it when reading the request line), so this only affects HTTP/2 ingress, and both upstream protocols derive their own authority from request.authority.

Tests in test/falcon/middleware/proxy.rb assert that trailer, proxy-connection and upgrade are removed while accept survives; that te: gzip is removed, te: trailers is kept, te: gzip, trailers is forwarded as trailers, and te: trailers survives when te is also listed in connection; that fields named by a mixed-case Connection: TE, Keep-Alive are extracted case-insensitively; and that host is removed when the authority is rewritten. The existing connection: x-custom-hop test passed on main and pins that behaviour rather than covering a new path.

Not addressed here

  • The response direction is unsanitised: call returns the upstream response verbatim, so an HTTP/1.1 upstream's connection, keep-alive or proxy-connection reach an HTTP/2 downstream as malformed headers (RFC 9113 §8.2.2). Protocol::Rack::Response::HOP_HEADERS is the sibling list for that direction. This is worth a follow-up PR.
  • expect was deliberately not added: it is end-to-end, and the upstream must see it to send 100 Continue.
  • http2-settings was deliberately not added: it must be listed in connection (RFC 7540 §3.2.1), so it is already removed by the connection-driven extraction when sent correctly.

This branch has not been deployed

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant