@@ -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.8.0
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.8.0
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 */
12311249function 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