taler-typescript-core

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

commit e79647797cfc80c02cdc8283c4a2ec6308e2ca03
parent dbcf19b6c15df3a5574dd67079422e9b33db0086
Author: Florian Dold <dold@taler.net>
Date:   Sat, 22 Aug 2026 12:32:41 +0200

util: send DELETE bodies with a content type

Based on finding and suggested fix by Iván Ávalos.

Diffstat:
Mpackages/taler-util/src/http-common.test.ts | 64++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mpackages/taler-util/src/http-common.ts | 16+++++++++++++---
2 files changed, 77 insertions(+), 3 deletions(-)

diff --git a/packages/taler-util/src/http-common.test.ts b/packages/taler-util/src/http-common.test.ts @@ -20,6 +20,8 @@ import { TalerError } from "./errors.js"; import { Headers, HeadersImpl, + HttpLib, + HttpRawRequestOptions, HttpResponse, readTalerErrorResponse, readUnexpectedResponseDetails, @@ -75,3 +77,65 @@ test("a well-formed error body is returned unchanged", async (t) => { assert.strictEqual(detail.code, 1234); assert.strictEqual(detail.hint, "nope"); }); + +function makeRecordingHttpLib(): { + http: HttpLib; + requests: HttpRawRequestOptions[]; +} { + const requests: HttpRawRequestOptions[] = []; + const http = new HttpLib( + { + async fetch(_url, opt) { + requests.push(opt); + return { + status: 204, + headers: new HeadersImpl(), + async bytes() { + return new Uint8Array(); + }, + }; + }, + }, + { enableThrottling: false }, + ); + return { http, requests }; +} + +test("DELETE encodes a supplied JSON body", async () => { + const { http, requests } = makeRecordingHttpLib(); + + await http.fetch("https://sync.example/backups/account/blocks/nonce", { + method: "DELETE", + body: { delete_sig: "sig" }, + }); + + assert.strictEqual(requests.length, 1); + assert.strictEqual(requests[0].method, "DELETE"); + assert.strictEqual(requests[0].headers["Content-Type"], "application/json"); + assert.strictEqual( + new TextDecoder().decode(requests[0].body), + JSON.stringify({ delete_sig: "sig" }), + ); +}); + +test("bodyless DELETE does not gain a body or content type", async () => { + const { http, requests } = makeRecordingHttpLib(); + + await http.fetch("https://example.com/resource", { method: "DELETE" }); + + assert.strictEqual(requests[0].body, undefined); + assert.strictEqual(requests[0].headers["Content-Type"], undefined); +}); + +test("DELETE preserves a caller-supplied content type", async () => { + const { http, requests } = makeRecordingHttpLib(); + + await http.fetch("https://example.com/resource", { + method: "DELETE", + headers: { "Content-Type": "application/custom" }, + body: "payload", + }); + + assert.strictEqual(requests[0].headers["Content-Type"], "application/custom"); + assert.strictEqual(new TextDecoder().decode(requests[0].body), "payload"); +}); diff --git a/packages/taler-util/src/http-common.ts b/packages/taler-util/src/http-common.ts @@ -577,10 +577,18 @@ export function encodeBody(body: unknown): Uint8Array { throw new TypeError("unsupported request body type"); } -export function getDefaultHeaders(method: string): Record<string, string> { +export function getDefaultHeaders( + method: string, + hasBody = false, +): Record<string, string> { const headers: Record<string, string> = {}; - if (method === "POST" || method === "PUT" || method === "PATCH") { + if ( + method === "POST" || + method === "PUT" || + method === "PATCH" || + (method === "DELETE" && hasBody) + ) { // Default to JSON if we have a body headers["Content-Type"] = "application/json"; } @@ -724,7 +732,7 @@ export class HttpLib implements HttpRequestLibrary { timeoutMs = opt.timeout.d_ms; } - const headers = getDefaultHeaders(requestMethod); + const headers = getDefaultHeaders(requestMethod, opt?.body != null); if (opt?.headers != null) { for (const [key, value] of Object.entries(opt.headers)) { if (value === undefined) continue; @@ -739,6 +747,8 @@ export class HttpLib implements HttpRequestLibrary { requestMethod === "PUT" ) { body = encodeBody(opt?.body); + } else if (requestMethod === "DELETE" && opt?.body != null) { + body = encodeBody(opt.body); } if (opt?.body instanceof URLSearchParams) {