Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions src/interceptors/ClientRequest/utils/recordRawHeaders.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
})
20 changes: 16 additions & 4 deletions src/interceptors/ClientRequest/utils/recordRawHeaders.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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, [
Expand All @@ -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
}
Expand All @@ -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')
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you have a reference to Node.js implementation of those additional arguments to Headers? What do they affect?

By not recording them, are we risking recreating a Headers instance that's not a 1-1 representation of the one recorded?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The 3rd boolean argument is passed internally during Headers operations — it's not part of the public Headers API spec which only defines set(name, value) and append(name, value) with 2 arguments.

Regarding 1-1 representation: No risk here. The boolean is purely internal (likely for validation skipping or provenance tracking). It's never exposed through any public Headers API (entries(), get(), forEach(), etc.), so it cannot affect actual header name/value data. The recorded raw headers will still be a 1-1 representation of the actual header data.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What are your thoughts on doing args.slice(0, 2)? I suppose we can benchmark which would be faster here.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Happy to change it to args.slice(0, 2) if you prefer — it's arguably more readable and explicitly conveys "take first 2 elements."

Performance-wise, [args[0], args[1]] is marginally faster (direct property access vs method call), but the difference is negligible in this context.

Let me know your preference and I'll update the PR.

return Reflect.apply(target, thisArg, args)
},
})
Expand Down