diff --git a/src/misc.ts b/src/misc.ts index 1c842f2..54b17b3 100644 --- a/src/misc.ts +++ b/src/misc.ts @@ -519,26 +519,59 @@ export const SECOND_TO_MICROSECOND_FACTOR = 1e6 * (1 + Number.EPSILON); */ export type SetRequired = T & Required>; -export const mergeObjectsDeeply = (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) { - if ( - typeof a[key as unknown as keyof T] === 'object' - && a[key as unknown as keyof T] !== null - && typeof b[key] === 'object' - && b[key] !== null - ) { - result[key] = mergeObjectsDeeply( - a[key as unknown as keyof T] as object, - b[key], - ) as (T & S)[Extract]; - } else { - result[key] = b[key] as (T & S)[Extract]; - } + // Special handling for headers + if (init1.headers || init2.headers) { + const headers1 = init1.headers ? normalizeHeaders(init1.headers) : {}; + const headers2 = init2.headers ? normalizeHeaders(init2.headers) : {}; + + const mergedHeaders = { ...headers1 }; + + // For each header in headers2, check if a case-insensitive match exists in mergedHeaders + Object.entries(headers2).forEach(([key2, value2]) => { + const existingKey = Object.keys(mergedHeaders).find( + key1 => key1.toLowerCase() === key2.toLowerCase(), + ); + + if (existingKey) { + delete mergedHeaders[existingKey]; + } + + mergedHeaders[key2] = value2; + }); + + merged.headers = mergedHeaders; } - return result; + return merged; +}; + +/** Normalizes HeadersInit to a Record format. */ +const normalizeHeaders = (headers: HeadersInit): Record => { + if (headers instanceof Headers) { + const result: Record = {}; + headers.forEach((value, key) => { + result[key] = value; + }); + return result; + } + + if (Array.isArray(headers)) { + const result: Record = {}; + headers.forEach(([key, value]) => { + result[key] = value; + }); + return result; + } + + return headers; }; export const retriedFetch = async ( diff --git a/src/source.ts b/src/source.ts index 6e548ce..6f65adb 100644 --- a/src/source.ts +++ b/src/source.ts @@ -12,7 +12,7 @@ import { binarySearchLessOrEqual, closedIntervalsOverlap, MaybePromise, - mergeObjectsDeeply, + mergeRequestInit, promiseWithResolvers, retriedFetch, toDataView, @@ -326,7 +326,7 @@ export class UrlSource extends Source { const abortController = new AbortController(); const response = await retriedFetch( this._url, - mergeObjectsDeeply(this._options.requestInit ?? {}, { + mergeRequestInit(this._options.requestInit ?? {}, { headers: { // 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 @@ -393,7 +393,7 @@ export class UrlSource extends Source { abortController = new AbortController(); response = await retriedFetch( this._url, - mergeObjectsDeeply(this._options.requestInit ?? {}, { + mergeRequestInit(this._options.requestInit ?? {}, { headers: { Range: `bytes=${worker.currentPos}-`, },