Skip to content

Commit 09023da

Browse files
committed
Fix doubleKnock request.clone() errors
This refactors the doubleKnock function and related signing functions to get the request body once at the top level instead of cloning it multiple times during iterations and redirects. Changes: - Add optional `body` parameter to SignRequestOptions and DoubleKnockOptions - Update signRequestDraft and signRequestRfc9421 to accept pre-read body - Modify doubleKnock to read body once and pass through all operations - Update redirect handling to reuse the same body buffer
1 parent d5b05e2 commit 09023da

1 file changed

Lines changed: 40 additions & 24 deletions

File tree

fedify/sig/http.ts

Lines changed: 40 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,12 @@ export interface SignRequestOptions {
5858
*/
5959
currentTime?: Temporal.Instant;
6060

61+
/**
62+
* The request body as ArrayBuffer. If provided, avoids cloning the request body.
63+
* @since 1.7.6
64+
*/
65+
body?: ArrayBuffer | null;
66+
6167
/**
6268
* The OpenTelemetry tracer provider. If omitted, the global tracer provider
6369
* is used.
@@ -102,6 +108,7 @@ export async function signRequest(
102108
keyId,
103109
span,
104110
options.currentTime,
111+
options.body,
105112
);
106113
} else {
107114
// Default to draft-cavage
@@ -111,6 +118,7 @@ export async function signRequest(
111118
keyId,
112119
span,
113120
options.currentTime,
121+
options.body,
114122
);
115123
}
116124

@@ -142,15 +150,17 @@ async function signRequestDraft(
142150
keyId: URL,
143151
span: Span,
144152
currentTime?: Temporal.Instant,
153+
bodyBuffer?: ArrayBuffer | null,
145154
): Promise<Request> {
146155
if (privateKey.algorithm.name !== "RSASSA-PKCS1-v1_5") {
147156
throw new TypeError("Unsupported algorithm: " + privateKey.algorithm.name);
148157
}
149158
const url = new URL(request.url);
150-
const body: ArrayBuffer | null =
151-
request.method !== "GET" && request.method !== "HEAD"
152-
? await request.clone().arrayBuffer()
153-
: null;
159+
const body: ArrayBuffer | null = bodyBuffer !== undefined
160+
? bodyBuffer
161+
: request.method !== "GET" && request.method !== "HEAD"
162+
? await request.clone().arrayBuffer()
163+
: null;
154164
const headers = new Headers(request.headers);
155165
if (!headers.has("Host")) {
156166
headers.set("Host", url.host);
@@ -382,16 +392,18 @@ async function signRequestRfc9421(
382392
keyId: URL,
383393
span: Span,
384394
currentTime?: Temporal.Instant,
395+
bodyBuffer?: ArrayBuffer | null,
385396
): Promise<Request> {
386397
if (privateKey.algorithm.name !== "RSASSA-PKCS1-v1_5") {
387398
throw new TypeError("Unsupported algorithm: " + privateKey.algorithm.name);
388399
}
389400

390401
const url = new URL(request.url);
391-
const body: ArrayBuffer | null =
392-
request.method !== "GET" && request.method !== "HEAD"
393-
? await request.clone().arrayBuffer()
394-
: null;
402+
const body: ArrayBuffer | null = bodyBuffer !== undefined
403+
? bodyBuffer
404+
: request.method !== "GET" && request.method !== "HEAD"
405+
? await request.clone().arrayBuffer()
406+
: null;
395407

396408
const headers = new Headers(request.headers);
397409
if (!headers.has("Host")) {
@@ -1214,6 +1226,12 @@ export interface DoubleKnockOptions {
12141226
*/
12151227
log?: (request: Request) => void;
12161228

1229+
/**
1230+
* The request body as ArrayBuffer. If provided, avoids cloning the request body.
1231+
* @since 1.7.6
1232+
*/
1233+
body?: ArrayBuffer | null;
1234+
12171235
/**
12181236
* The OpenTelemetry tracer provider. If omitted, the global tracer provider
12191237
* is used.
@@ -1225,13 +1243,13 @@ export interface DoubleKnockOptions {
12251243
* Helper function to create a new Request for redirect handling.
12261244
* @param request The original request.
12271245
* @param location The redirect location.
1228-
* @param body The request body as ArrayBuffer or undefined.
1246+
* @param body The request body as ArrayBuffer or null.
12291247
* @returns A new Request object for the redirect.
12301248
*/
12311249
function createRedirectRequest(
12321250
request: Request,
12331251
location: string,
1234-
body: ArrayBuffer | undefined,
1252+
body: ArrayBuffer | null,
12351253
): Request {
12361254
return new Request(location, {
12371255
method: request.method,
@@ -1269,11 +1287,19 @@ export async function doubleKnock(
12691287
const firstTrySpec: HttpMessageSignaturesSpec = specDeterminer == null
12701288
? "rfc9421"
12711289
: await specDeterminer.determineSpec(origin);
1290+
1291+
// Get the request body once at the top level to avoid multiple clones
1292+
const body = options.body !== undefined
1293+
? options.body
1294+
: request.method !== "GET" && request.method !== "HEAD"
1295+
? await request.clone().arrayBuffer()
1296+
: null;
1297+
12721298
let signedRequest = await signRequest(
12731299
request,
12741300
identity.privateKey,
12751301
identity.keyId,
1276-
{ spec: firstTrySpec, tracerProvider },
1302+
{ spec: firstTrySpec, tracerProvider, body },
12771303
);
12781304
log?.(signedRequest);
12791305
let response = await fetch(signedRequest, {
@@ -1288,13 +1314,10 @@ export async function doubleKnock(
12881314
response.headers.has("Location")
12891315
) {
12901316
const location = response.headers.get("Location")!;
1291-
const body = request.method !== "GET" && request.method !== "HEAD"
1292-
? await request.clone().arrayBuffer()
1293-
: undefined;
12941317
return doubleKnock(
12951318
createRedirectRequest(request, location, body),
12961319
identity,
1297-
options,
1320+
{ ...options, body },
12981321
);
12991322
} else if (
13001323
// FIXME: Temporary hotfix for Mastodon RFC 9421 implementation bug (as of 2025-06-19).
@@ -1323,7 +1346,7 @@ export async function doubleKnock(
13231346
request,
13241347
identity.privateKey,
13251348
identity.keyId,
1326-
{ spec, tracerProvider },
1349+
{ spec, tracerProvider, body },
13271350
);
13281351
log?.(signedRequest);
13291352
response = await fetch(signedRequest, {
@@ -1338,17 +1361,10 @@ export async function doubleKnock(
13381361
response.headers.has("Location")
13391362
) {
13401363
const location = response.headers.get("Location")!;
1341-
// IMPORTANT: Use arrayBuffer() instead of .body to prevent "TypeError: unusable"
1342-
// When using .body (ReadableStream), subsequent clone() calls in signRequest functions
1343-
// will fail because the stream has already been consumed. Using arrayBuffer() ensures
1344-
// the body can be safely cloned for HTTP signature generation.
1345-
const body = request.method !== "GET" && request.method !== "HEAD"
1346-
? await request.clone().arrayBuffer()
1347-
: undefined;
13481364
return doubleKnock(
13491365
createRedirectRequest(request, location, body),
13501366
identity,
1351-
options,
1367+
{ ...options, body },
13521368
);
13531369
} else if (response.status !== 400 && response.status !== 401) {
13541370
await specDeterminer?.rememberSpec(origin, spec);

0 commit comments

Comments
 (0)