diff --git a/src/interceptors/ClientRequest/utils/recordRawHeaders.test.ts b/src/interceptors/ClientRequest/utils/recordRawHeaders.test.ts index db401b32..17c3c843 100644 --- a/src/interceptors/ClientRequest/utils/recordRawHeaders.test.ts +++ b/src/interceptors/ClientRequest/utils/recordRawHeaders.test.ts @@ -256,3 +256,42 @@ it('isolates headers between different headers instances', async () => { expect(firstClone.get('Content-Type')).toBe('application/json') expect(secondClone.get('Content-Type')).toBeNull() }) + +/** + * Node.js 24+ may pass additional internal arguments to Headers.prototype.set + * and Headers.prototype.append. This test ensures we only record the first + * two arguments (name, value) and ignore any additional internal arguments. + * @see https://github.com/mswjs/interceptors/issues/762 + */ +it('ignores extra internal arguments passed to .set() and .append()', () => { + recordRawFetchHeaders() + const headers = new Headers() + + // Simulate Node.js 24+ behavior where internal calls may pass extra arguments + // by calling the prototype methods directly with additional arguments. + ; (Headers.prototype.set as Function).call( + headers, + 'X-Set-Header', + 'set-value', + true // extra internal argument + ) + ; (Headers.prototype.append as Function).call( + headers, + 'X-Append-Header', + 'append-value', + true // extra internal argument + ) + + const rawHeaders = getRawFetchHeaders(headers) + + // Verify that raw headers only contain [name, value] tuples + expect(rawHeaders).toEqual([ + ['X-Set-Header', 'set-value'], + ['X-Append-Header', 'append-value'], + ]) + + // Ensure no extra elements in each tuple + for (const tuple of rawHeaders) { + expect(tuple).toHaveLength(2) + } +}) diff --git a/src/interceptors/ClientRequest/utils/recordRawHeaders.ts b/src/interceptors/ClientRequest/utils/recordRawHeaders.ts index cfa1c9d8..146b59c8 100644 --- a/src/interceptors/ClientRequest/utils/recordRawHeaders.ts +++ b/src/interceptors/ClientRequest/utils/recordRawHeaders.ts @@ -113,9 +113,15 @@ export function recordRawFetchHeaders() { headersInit instanceof Headers && Reflect.has(headersInit, kRawHeaders) ) { + // Ensure each header tuple has exactly 2 elements (name, value). + // Node.js 24+ may have stored tuples with extra internal arguments. + const rawHeadersFromInit = Reflect.get(headersInit, kRawHeaders) as RawHeaders + const sanitizedHeaders = rawHeadersFromInit.map( + (tuple): HeaderTuple => [tuple[0], tuple[1]] + ) const headers = Reflect.construct( target, - [Reflect.get(headersInit, kRawHeaders)], + [sanitizedHeaders], newTarget ) ensureRawHeadersSymbol(headers, [ @@ -124,7 +130,7 @@ export function recordRawFetchHeaders() { * This prevents multiple Headers instances from pointing * at the same internal "rawHeaders" array. */ - ...Reflect.get(headersInit, kRawHeaders), + ...sanitizedHeaders, ]) return headers } @@ -149,14 +155,20 @@ export function recordRawFetchHeaders() { Headers.prototype.set = new Proxy(Headers.prototype.set, { apply(target, thisArg, args: HeaderTuple) { - recordRawHeader(thisArg, args, 'set') + // Use only the first two arguments (name, value) to record raw headers. + // Node.js 24+ may pass additional internal arguments that should not + // be included in the raw headers array. + recordRawHeader(thisArg, [args[0], args[1]], 'set') return Reflect.apply(target, thisArg, args) }, }) Headers.prototype.append = new Proxy(Headers.prototype.append, { apply(target, thisArg, args: HeaderTuple) { - recordRawHeader(thisArg, args, 'append') + // Use only the first two arguments (name, value) to record raw headers. + // Node.js 24+ may pass additional internal arguments that should not + // be included in the raw headers array. + recordRawHeader(thisArg, [args[0], args[1]], 'append') return Reflect.apply(target, thisArg, args) }, })