diff --git a/package.json b/package.json index 5f14b24..70ad135 100644 --- a/package.json +++ b/package.json @@ -46,6 +46,6 @@ "dependencies": { "@openally/result": "2.0.0", "lru-cache": "11.2.6", - "undici": "7.24.4" + "undici": "8.7.0" } } diff --git a/src/utils.ts b/src/utils.ts index b465a76..83865e4 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -34,6 +34,27 @@ export function getEncodingCharset( return charset in kCharsetConversionTable ? kCharsetConversionTable[charset] : "utf-8"; } +/** + * @description Set a header case-insensitively, removing any existing entry that + * differs only by casing. HTTP header names are case-insensitive and HTTP/2 forbids + * singular headers (e.g. `user-agent`) from carrying multiple values, so we must not + * end up with both `user-agent` and `User-Agent` in the same object. + */ +function setHeader( + headers: IncomingHttpHeaders, + name: string, + value: IncomingHttpHeaders[string] +): void { + const lowerName = name.toLowerCase(); + for (const key of Object.keys(headers)) { + if (key.toLowerCase() === lowerName) { + delete headers[key]; + } + } + + headers[name] = value; +} + /** * @description Create a default plain Object headers that will contains a Set of default values like: * - User-agent @@ -42,10 +63,14 @@ export function getEncodingCharset( export function createHeaders( options: Partial> ): IncomingHttpHeaders { - const headers = Object.assign({ ...DEFAULT_HEADER }, options.headers ?? {}); + const headers: IncomingHttpHeaders = { ...DEFAULT_HEADER }; + + for (const [name, value] of Object.entries(options.headers ?? {})) { + setHeader(headers, name, value); + } if (options.authorization) { - headers.Authorization = createAuthorizationHeader(options.authorization); + setHeader(headers, "Authorization", createAuthorizationHeader(options.authorization)); } return headers; diff --git a/test/utils.test.ts b/test/utils.test.ts index 56c0c99..5f788d4 100644 --- a/test/utils.test.ts +++ b/test/utils.test.ts @@ -67,6 +67,17 @@ describe("createHeaders", () => { assert.deepStrictEqual(result, { "user-agent": "myUserAgent" }); }); + it("should overwrite the default 'user-agent' header regardless of casing (no duplicate)", () => { + // A caller-provided header must replace the default one even when the casing + // differs, otherwise both 'user-agent' and 'User-Agent' end up in the object + // and HTTP/2 rejects the request with ERR_HTTP2_HEADER_SINGLE_VALUE. + const result = Utils.createHeaders({ + headers: { "User-Agent": "myUserAgent" } + }); + + assert.deepStrictEqual(result, { "User-Agent": "myUserAgent" }); + }); + it("should add authorization header (and override original property)", () => { const result = Utils.createHeaders({ headers: {