commit b13a53c32576ebffdd9fa21496d2d175bdc3b1d9
parent 5651a7a0219817936487a473959570e86a9c2d42
Author: Florian Dold <dold@taler.net>
Date: Mon, 10 Aug 2026 01:30:33 +0200
wallet: set Content-Length for Node requests
Diffstat:
2 files changed, 91 insertions(+), 2 deletions(-)
diff --git a/packages/taler-util/src/http-impl.node.test.ts b/packages/taler-util/src/http-impl.node.test.ts
@@ -0,0 +1,76 @@
+/*
+ This file is part of GNU Taler
+ (C) 2026 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+
+ SPDX-License-Identifier: AGPL3.0-or-later
+*/
+
+import assert from "node:assert";
+import { createServer } from "node:http";
+import type { IncomingHttpHeaders } from "node:http";
+import type { AddressInfo } from "node:net";
+import { test } from "node:test";
+import { createPlatformHttpLib } from "./http.js";
+
+test("node HTTP requests with a buffered body use Content-Length", async (t) => {
+ let received: Promise<{ headers: IncomingHttpHeaders; bodyLength: number }>;
+ let resolveReceived: (value: {
+ headers: IncomingHttpHeaders;
+ bodyLength: number;
+ }) => void;
+ received = new Promise((resolve) => {
+ resolveReceived = resolve;
+ });
+
+ const server = createServer((req, res) => {
+ const chunks: Uint8Array[] = [];
+ req.on("data", (chunk) => chunks.push(chunk));
+ req.on("end", () => {
+ resolveReceived({
+ headers: req.headers,
+ bodyLength: chunks.reduce(
+ (length, chunk) => length + chunk.byteLength,
+ 0,
+ ),
+ });
+ res.end();
+ });
+ });
+ await new Promise<void>((resolve, reject) => {
+ server.once("error", reject);
+ server.listen(0, "127.0.0.1", resolve);
+ });
+ t.after(
+ () =>
+ new Promise<void>((resolve, reject) =>
+ server.close((err) => (err ? reject(err) : resolve())),
+ ),
+ );
+
+ const port = (server.address() as AddressInfo).port;
+ const http = createPlatformHttpLib({ enableThrottling: false });
+ await http.fetch(`http://127.0.0.1:${port}/`, {
+ method: "POST",
+ body: { hello: "world" },
+ compress: "deflate",
+ });
+
+ const request = await received;
+ assert.strictEqual(request.headers["transfer-encoding"], undefined);
+ assert.strictEqual(
+ request.headers["content-length"],
+ String(request.bodyLength),
+ );
+ assert.strictEqual(request.headers["content-encoding"], "deflate");
+});
diff --git a/packages/taler-util/src/http-impl.node.ts b/packages/taler-util/src/http-impl.node.ts
@@ -93,6 +93,19 @@ export const rawLib: HttpRawLib = {
fetch(url: string, opt: HttpRawRequestOptions): Promise<HttpRawResponse> {
const parsedUrl = new URL(url);
+ // The shared HTTP layer has already completely encoded (and possibly
+ // compressed) the body. Tell Node its final length so that it does not
+ // select chunked transfer encoding when req.write() is called below.
+ // Keep an explicitly supplied Content-Length, including one with unusual
+ // casing, for callers that need to control the header themselves.
+ const headers = { ...opt.headers };
+ const hasContentLength = Object.keys(headers).some(
+ (name) => name.toLowerCase() === "content-length",
+ );
+ if (opt.body !== undefined && !hasContentLength) {
+ headers["Content-Length"] = String(opt.body.byteLength);
+ }
+
let path = parsedUrl.pathname;
if (parsedUrl.search != null) {
path += parsedUrl.search;
@@ -104,7 +117,7 @@ export const rawLib: HttpRawLib = {
host: parsedUrl.hostname,
method: opt.method,
path,
- headers: opt.headers,
+ headers,
timeout: opt.timeoutMs,
followRedirects: opt.redirect !== "manual",
};
@@ -112,7 +125,7 @@ export const rawLib: HttpRawLib = {
const chunks: Uint8Array[] = [];
if (SHOW_CURL_HTTP_REQUEST) {
- printAsCurl(parsedUrl.href, opt);
+ printAsCurl(parsedUrl.href, { ...opt, headers });
}
let timeoutHandle: NodeJS.Timeout | undefined = undefined;