taler-typescript-core

Wallet core logic and WebUIs for various components
Log | Files | Refs | Submodules | README | LICENSE

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:
Apackages/taler-util/src/http-impl.node.test.ts | 76++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mpackages/taler-util/src/http-impl.node.ts | 17+++++++++++++++--
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;