fix(daemon): strip hop-by-hop headers when bridging provider responses

Copying the upstream Transfer-Encoding: chunked onto our response while
Node also frames the streamed body as chunked yields a duplicated
'Transfer-Encoding: chunked, chunked' header. Strict HTTP/1.x clients
such as Go's net/http reject this with 'too many transfer encodings',
breaking streaming clients like meat against the daemon.

Drop all hop-by-hop headers (connection, keep-alive, proxy-*, te,
trailer, transfer-encoding, upgrade) so Node derives correct framing
(Content-Length for buffered bodies, chunked for streams) for this
connection.
This commit is contained in:
redshift
2026-08-05 13:45:11 +01:00
parent 86ac688b7b
commit 970206df5b
+23 -2
View File
@@ -21,6 +21,24 @@ import { decodeCashuTokenAmount } from "../wallet";
import { getClientsFromStore } from "../../utils/clients";
import { getUsageSummary } from "./usage-summary";
// Hop-by-hop headers describe the *upstream* connection, not this one, and must
// never be copied onto our response. In particular, copying the upstream's
// `Transfer-Encoding: chunked` while Node also frames the streamed body as
// chunked produces a duplicated `Transfer-Encoding: chunked, chunked` header,
// which strict HTTP/1.x clients (e.g. Go's net/http) reject with
// "too many transfer encodings". Node derives Content-Length for buffered
// bodies and Transfer-Encoding: chunked for streams itself.
const HOP_BY_HOP_HEADERS = new Set([
"connection",
"keep-alive",
"proxy-authenticate",
"proxy-authorization",
"te",
"trailer",
"transfer-encoding",
"upgrade",
]);
type ClientMode = "xcashu" | "lazyrefund" | "apikeys";
type WalletStatusOutput = {
@@ -1512,10 +1530,13 @@ export function createDaemonRequestHandler(deps: {
...(deps.routstrPubkey ? { routstrPubkey: deps.routstrPubkey } : {}),
});
// Bridge the Web `Response` to the Node `ServerResponse` with no
// transforms: status + headers + pipe(body → res).
// Bridge the Web `Response` to the Node `ServerResponse`: status +
// headers + pipe(body → res). Hop-by-hop headers are dropped (see
// HOP_BY_HOP_HEADERS) so Node computes correct framing for THIS
// connection.
res.statusCode = response.status;
response.headers.forEach((value, key) => {
if (HOP_BY_HOP_HEADERS.has(key.toLowerCase())) return;
res.setHeader(key, value);
});