Skip to content
Merged
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
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,6 @@
"dependencies": {
"@openally/result": "2.0.0",
"lru-cache": "11.2.6",
"undici": "7.24.4"
"undici": "8.7.0"
}
}
29 changes: 27 additions & 2 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -42,10 +63,14 @@ export function getEncodingCharset(
export function createHeaders(
options: Partial<Pick<RequestOptions, "headers" | "authorization">>
): 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;
Expand Down
11 changes: 11 additions & 0 deletions test/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down
Loading