Properly merge different forms of headers in UrlSource (#126)

This commit is contained in:
Vanilagy
2025-09-19 18:04:02 +02:00
parent cfc2c2d864
commit e28c34cb7a
2 changed files with 53 additions and 20 deletions
+50 -17
View File
@@ -519,26 +519,59 @@ export const SECOND_TO_MICROSECOND_FACTOR = 1e6 * (1 + Number.EPSILON);
*/ */
export type SetRequired<T, K extends keyof T> = T & Required<Pick<T, K>>; export type SetRequired<T, K extends keyof T> = T & Required<Pick<T, K>>;
export const mergeObjectsDeeply = <T extends object, S extends object>(a: T, b: S): T & S => { /**
const result = { ...a } as T & S; * Merges two RequestInit objects with special handling for headers.
* Headers are merged case-insensitively, but original casing is preserved.
* init2 headers take precedence and will override case-insensitive matches from init1.
*/
export const mergeRequestInit = (init1: RequestInit, init2: RequestInit): RequestInit => {
const merged: RequestInit = { ...init1, ...init2 };
for (const key in b) { // Special handling for headers
if ( if (init1.headers || init2.headers) {
typeof a[key as unknown as keyof T] === 'object' const headers1 = init1.headers ? normalizeHeaders(init1.headers) : {};
&& a[key as unknown as keyof T] !== null const headers2 = init2.headers ? normalizeHeaders(init2.headers) : {};
&& typeof b[key] === 'object'
&& b[key] !== null const mergedHeaders = { ...headers1 };
) {
result[key] = mergeObjectsDeeply( // For each header in headers2, check if a case-insensitive match exists in mergedHeaders
a[key as unknown as keyof T] as object, Object.entries(headers2).forEach(([key2, value2]) => {
b[key], const existingKey = Object.keys(mergedHeaders).find(
) as (T & S)[Extract<keyof S, string>]; key1 => key1.toLowerCase() === key2.toLowerCase(),
} else { );
result[key] = b[key] as (T & S)[Extract<keyof S, string>];
} if (existingKey) {
delete mergedHeaders[existingKey];
}
mergedHeaders[key2] = value2;
});
merged.headers = mergedHeaders;
} }
return result; return merged;
};
/** Normalizes HeadersInit to a Record<string, string> format. */
const normalizeHeaders = (headers: HeadersInit): Record<string, string> => {
if (headers instanceof Headers) {
const result: Record<string, string> = {};
headers.forEach((value, key) => {
result[key] = value;
});
return result;
}
if (Array.isArray(headers)) {
const result: Record<string, string> = {};
headers.forEach(([key, value]) => {
result[key] = value;
});
return result;
}
return headers;
}; };
export const retriedFetch = async ( export const retriedFetch = async (
+3 -3
View File
@@ -12,7 +12,7 @@ import {
binarySearchLessOrEqual, binarySearchLessOrEqual,
closedIntervalsOverlap, closedIntervalsOverlap,
MaybePromise, MaybePromise,
mergeObjectsDeeply, mergeRequestInit,
promiseWithResolvers, promiseWithResolvers,
retriedFetch, retriedFetch,
toDataView, toDataView,
@@ -326,7 +326,7 @@ export class UrlSource extends Source {
const abortController = new AbortController(); const abortController = new AbortController();
const response = await retriedFetch( const response = await retriedFetch(
this._url, this._url,
mergeObjectsDeeply(this._options.requestInit ?? {}, { mergeRequestInit(this._options.requestInit ?? {}, {
headers: { headers: {
// We could also send a non-range request to request the same bytes (all of them), but doing it like // We could also send a non-range request to request the same bytes (all of them), but doing it like
// this is an easy way to check if the server supports range requests in the first place // this is an easy way to check if the server supports range requests in the first place
@@ -393,7 +393,7 @@ export class UrlSource extends Source {
abortController = new AbortController(); abortController = new AbortController();
response = await retriedFetch( response = await retriedFetch(
this._url, this._url,
mergeObjectsDeeply(this._options.requestInit ?? {}, { mergeRequestInit(this._options.requestInit ?? {}, {
headers: { headers: {
Range: `bytes=${worker.currentPos}-`, Range: `bytes=${worker.currentPos}-`,
}, },