feat(custom): send several values under one ingress header name - #472
feat(custom): send several values under one ingress header name#472Menci wants to merge 2 commits into
Conversation
An ingress header rule carried one value per name, so an operator could not keep the client's value and add one of their own, nor send a name twice. Both persistence and the dashboard rejected a repeated name outright. Model a rule as one value rather than one name. A name resolves as a whole: its admitted client values are dropped and its rules rebuild the value list in rule order, so a passthrough rule reinstates what the client sent and every configured rule contributes its own value beside it. A name still carries at most one passthrough rule, because the client's values enter the request once, and a name with no rule reaches no upstream at all. Cover the contract as a table over what the operator configured and what the client sent, run it across every Custom endpoint, and drive each client protocol — including the cross-protocol translations — through the gateway. Assert the resulting field lines against a real HTTP server over both Node egress paths, since undici and our own socket writer serialize a repeated name independently.
A repeated header name had nowhere to live. `HttpRequest.headers` was a `Record<string, string>`, so the serializer emitted one line per name and the gateway's dial seam collapsed a `Headers` into that record — losing the repetition, and silently keeping only the last Set-Cookie. On the response side every field line was appended into a `Headers`, which merges a repeated name on undici, so no caller could read the lines the upstream actually sent. Model both directions as ordered field lines. `HttpRequest.headers` and the new `RawHttpResponse.headerLines` are `[name, value]` in wire order, the serializer writes one line per entry, and the parser records each line beside the `Headers` it still builds for the Web bridge. The decoded Transfer-Encoding leaves both views together. Carry the same shape through the provider transport boundary: `UpstreamFetchOptions.extraHeaders` and `headersForMessagesCall` take field lines, and the Custom provider resolves its ingress rules into them, so a name configured several times reaches the wire several times. Node's `direct_fetch` remains the one transport that cannot express it — undici concatenates inside `Headers.append`, before any transport sees the request — and its combined form is the same field value per RFC 9110 §5.3.
Live-instance experiment: what a repeated ingress header rule puts on the wireA script that starts an isolated Floway instance and a capture HTTP server, drives a real client request through the gateway, and records the field lines the upstream actually received. It confirms the wire behaviour this PR describes. GitHub's attachment endpoint refuses What it does
Rules under test
The client sentx-passthrough: kept-a
x-passthrough: kept-b
x-route: client-a
x-route: client-b
x-configured: client-copy
x-dropped: goneThe upstream receivedNode v22.23.2, both requests answered
POST /v1/embeddings HTTP/1.1
x-passthrough: kept-a, kept-b
x-route: client-a, client-b
x-route: appended
x-configured: first
x-configured: second
x-mixed:
x-mixed: after-empty
POST /v1/embeddings HTTP/1.1
x-passthrough: kept-a, kept-b
x-route: client-a, client-b, appended
x-configured: first, second
x-mixed: , after-empty
Two things worth naming:
Downloadingress-header-lines-experiment.zip — the script and its results. GitHub's attachment endpoint accepts only image and video content types, so the archive is stored under a curl -sL https://github.com/user-attachments/assets/0f910932-ab23-4037-b253-bd3739abf776 -o ingress-header-lines-experiment.zip && unzip -o ingress-header-lines-experiment.zipThe same content is inlined below, so nothing depends on that link.
|
Summary
An ingress header rule carried one value per name: persistence and the dashboard both rejected a repeated name, and the resolver wrote each configured value with
Headers.set. An operator could not keep the client's value and add one of their own, nor send a name twice.A rule is now one value rather than one name. A name resolves as a whole — its admitted client values are dropped and its rules rebuild the value list in rule order:
aaandb(passthrough)aa, boneoneoneoneone,twoone,twoone,twoone,two(passthrough),oneonea,onea, b,oneone,(passthrough)oneone,aone,a, b(empty),oneoneoneoneEach cell lists the values the upstream receives, one field line each. A client that repeats a name contributes one value, because both runtimes merge a repeated name when the inbound request becomes a
Headers.Field lines end to end
Sending several values means several field lines, which nothing below the resolver could express.
HttpRequest.headerswas aRecord<string, string>, the gateway's dial seam collapsed aHeadersinto that record — losing repetitions, and silently keeping only the lastSet-Cookie— and on the response side every field line was appended into aHeaders, which merges a repeated name on undici.Both directions of
@floway-dev/httpnow carry ordered[name, value]field lines: the serializer writes one line per entry, and the parser recordsRawHttpResponse.headerLinesbeside theHeadersit still builds for the Web bridge (the decodedTransfer-Encodingleaves both views together). The same shape reaches the transport throughUpstreamFetchOptions.extraHeadersandheadersForMessagesCall.The result per egress:
direct_connect/ proxy (the default)fetchdirect_fetchHeaders.append, before any transport sees the requestRFC 9110 §5.3 makes the combined form the same field value for a list-typed name, and
direct_fetchis opt-in, so the default egress on both runtimes sends separate lines.Test Plan
Headers.node:httpserver'srawHeadersover both Node egress paths:direct_connectsendsx-routetwice andx-configuredtwice,direct_fetchsends each combined.@floway-dev/httpwrites one request field line per entry, and keeps every response field line of a repeated name in wire order and casing, includingSet-Cookie.pnpm run verify— 547 test files and 5,697 tests passed, plus lint, typecheck, installer harness, generated assets, AGENTS validation, verification parity, and web build.