Skip to content

Commit 390a1de

Browse files
committed
fix: Parse headers from both Header objects and plain objects
1 parent f929b4e commit 390a1de

File tree

2 files changed

+147
-3
lines changed

2 files changed

+147
-3
lines changed

src/utils/fetch-interceptor.js

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,22 @@ function extractRequestDetails( input, init = {} ) {
146146
} else if ( input instanceof Request ) {
147147
url = input.url;
148148
method = input.method;
149-
headers = input.headers;
149+
// Merge Request headers with init.headers (init takes precedence, like native fetch)
150+
if ( init.headers ) {
151+
const requestHeaders = serializeHeaders( input.headers );
152+
const initHeaders = serializeHeaders( init.headers );
153+
// Merge with case-insensitive key matching (lowercase all keys)
154+
const merged = {};
155+
Object.entries( requestHeaders ).forEach( ( [ key, value ] ) => {
156+
merged[ key.toLowerCase() ] = value;
157+
} );
158+
Object.entries( initHeaders ).forEach( ( [ key, value ] ) => {
159+
merged[ key.toLowerCase() ] = value;
160+
} );
161+
headers = merged;
162+
} else {
163+
headers = input.headers;
164+
}
150165
}
151166

152167
return {
@@ -272,18 +287,27 @@ async function serializeBody( source ) {
272287
}
273288

274289
/**
275-
* Serializes Headers object to a plain object.
290+
* Serializes Headers object or plain object to a plain object.
276291
*
277-
* @param {Headers} headers The Headers object to serialize.
292+
* @param {Headers|Object} headers The Headers object or plain object to serialize.
278293
*
279294
* @return {Object} Plain object representation of headers.
280295
*/
281296
function serializeHeaders( headers ) {
282297
const result = {};
298+
299+
// Handle Headers object (has forEach method)
283300
if ( headers && typeof headers.forEach === 'function' ) {
284301
headers.forEach( ( value, key ) => {
285302
result[ key ] = value;
286303
} );
287304
}
305+
// Handle plain object (from init.headers)
306+
else if ( headers && typeof headers === 'object' ) {
307+
Object.entries( headers ).forEach( ( [ key, value ] ) => {
308+
result[ key ] = value;
309+
} );
310+
}
311+
288312
return result;
289313
}

src/utils/fetch-interceptor.test.js

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,126 @@ describe( 'initializeFetchInterceptor', () => {
7676
expect( window.fetch ).toBe( currentFetch );
7777
} );
7878

79+
describe( 'request header capture', () => {
80+
it( 'should capture headers from plain object with string URL', async () => {
81+
initializeFetchInterceptor();
82+
83+
await window.fetch( 'https://example.com/api', {
84+
method: 'POST',
85+
headers: {
86+
'Content-Type': 'application/json',
87+
Authorization: 'Bearer test-token',
88+
'X-Custom-Header': 'custom-value',
89+
},
90+
} );
91+
92+
await waitForAsyncLogging();
93+
94+
expect( bridge.onNetworkRequest ).toHaveBeenCalledWith(
95+
expect.objectContaining( {
96+
requestHeaders: expect.objectContaining( {
97+
'Content-Type': 'application/json',
98+
Authorization: 'Bearer test-token',
99+
'X-Custom-Header': 'custom-value',
100+
} ),
101+
} )
102+
);
103+
} );
104+
105+
it( 'should capture headers from Request object', async () => {
106+
initializeFetchInterceptor();
107+
108+
const request = new Request( 'https://example.com/api', {
109+
method: 'GET',
110+
headers: {
111+
Accept: 'application/json',
112+
'X-Request-ID': '12345',
113+
},
114+
} );
115+
116+
await window.fetch( request );
117+
118+
await waitForAsyncLogging();
119+
120+
expect( bridge.onNetworkRequest ).toHaveBeenCalledWith(
121+
expect.objectContaining( {
122+
requestHeaders: expect.objectContaining( {
123+
accept: 'application/json',
124+
'x-request-id': '12345',
125+
} ),
126+
} )
127+
);
128+
} );
129+
130+
it( 'should merge Request headers with init override', async () => {
131+
initializeFetchInterceptor();
132+
133+
const request = new Request( 'https://example.com/api', {
134+
headers: {
135+
'Content-Type': 'application/json',
136+
'X-Original': 'original-value',
137+
},
138+
} );
139+
140+
await window.fetch( request, {
141+
headers: {
142+
'Content-Type': 'application/xml', // Override
143+
'X-Additional': 'additional-value', // Additional
144+
},
145+
} );
146+
147+
await waitForAsyncLogging();
148+
149+
expect( bridge.onNetworkRequest ).toHaveBeenCalledWith(
150+
expect.objectContaining( {
151+
requestHeaders: expect.objectContaining( {
152+
'content-type': 'application/xml', // Overridden (lowercase)
153+
'x-original': 'original-value', // Preserved (lowercase)
154+
'x-additional': 'additional-value', // Added (lowercase)
155+
} ),
156+
} )
157+
);
158+
} );
159+
160+
it( 'should handle empty headers', async () => {
161+
initializeFetchInterceptor();
162+
163+
await window.fetch( 'https://example.com/api' );
164+
165+
await waitForAsyncLogging();
166+
167+
expect( bridge.onNetworkRequest ).toHaveBeenCalledWith(
168+
expect.objectContaining( {
169+
requestHeaders: {},
170+
} )
171+
);
172+
} );
173+
174+
it( 'should handle Headers instance', async () => {
175+
initializeFetchInterceptor();
176+
177+
const headers = new Headers();
178+
headers.append( 'Authorization', 'Bearer token123' );
179+
headers.append( 'Content-Type', 'application/json' );
180+
181+
await window.fetch( 'https://example.com/api', {
182+
method: 'POST',
183+
headers,
184+
} );
185+
186+
await waitForAsyncLogging();
187+
188+
expect( bridge.onNetworkRequest ).toHaveBeenCalledWith(
189+
expect.objectContaining( {
190+
requestHeaders: expect.objectContaining( {
191+
authorization: 'Bearer token123',
192+
'content-type': 'application/json',
193+
} ),
194+
} )
195+
);
196+
} );
197+
} );
198+
79199
describe( 'request body serialization', () => {
80200
it( 'should serialize FormData with files correctly', async () => {
81201
initializeFetchInterceptor();

0 commit comments

Comments
 (0)