Summary
A response's set-cookie header must surface as a string array on IncomingMessage.headers, even for a single cookie. Perry collapses duplicate response headers into one value, so res.headers['set-cookie'] returns only the last cookie string.
Repro
import http from "node:http";
const server = http.createServer((_req, res) => {
res.setHeader("Set-Cookie", ["a=1", "b=2"]);
res.end("ok");
});
server.listen(0, () => {
const port = (server.address() as any).port;
http.get({ port }, (res: any) => {
console.log("set-cookie", JSON.stringify(res.headers["set-cookie"]));
res.on("data", () => {});
res.on("end", () => server.close(() => console.log("closed")));
});
});
setTimeout(() => {}, 1000);
# node v26
set-cookie ["a=1","b=2"]
closed
# perry
set-cookie "b=2"
closed
Root cause
IncomingMessageHandle.headers (crates/perry-ext-http/src/lib.rs) is a HashMap<String, String>, built by inserting each (k, v) pair from the reqwest response — duplicates clobber. Node's rule (per RFC 7230 §3.2.2 / the _http_incoming.js matchKnownFields logic):
set-cookie → always accumulated into an array;
- a small set of single-value headers (
content-length, content-type, host, authorization, …) → first value wins;
- everything else → duplicates joined with
, .
Fix sketch
Change IncomingMessageHandle to preserve header multiplicity (e.g. Vec<(String, String)> or a small multimap) and apply Node's combination rules in the res.headers getter (js_http_response_headers). headersDistinct / rawHeaders already round-trip the raw pairs, so the underlying data is available; this is about the combined-headers view. Touches the pub struct and its build sites across perry-ext-http (client) and the perry-ext-http-server server-side IncomingMessage, so both paths should be updated together.
Found while fixing client transport errors (#5078); deferred there to keep that PR focused.
Summary
A response's
set-cookieheader must surface as a string array onIncomingMessage.headers, even for a single cookie. Perry collapses duplicate response headers into one value, sores.headers['set-cookie']returns only the last cookie string.Repro
Root cause
IncomingMessageHandle.headers(crates/perry-ext-http/src/lib.rs) is aHashMap<String, String>, built by inserting each(k, v)pair from the reqwest response — duplicates clobber. Node's rule (per RFC 7230 §3.2.2 / the_http_incoming.jsmatchKnownFieldslogic):set-cookie→ always accumulated into an array;content-length,content-type,host,authorization, …) → first value wins;,.Fix sketch
Change
IncomingMessageHandleto preserve header multiplicity (e.g.Vec<(String, String)>or a small multimap) and apply Node's combination rules in theres.headersgetter (js_http_response_headers).headersDistinct/rawHeadersalready round-trip the raw pairs, so the underlying data is available; this is about the combined-headersview. Touches thepubstruct and its build sites acrossperry-ext-http(client) and theperry-ext-http-serverserver-sideIncomingMessage, so both paths should be updated together.Found while fixing client transport errors (#5078); deferred there to keep that PR focused.