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:
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) {