fix(http): preserve SDK headers when injecting User-Agent
The v1.76.0 User-Agent injection object-spread `fetchOptions.headers`, which silently drops all headers when the value is a Headers instance. The Immich SDK passes a Headers object with the merged x-api-key header, so requests lost their API key and returned 401 (issue #6672). Build a real Headers instance instead, keeping case-insensitive user-agent handling.
This commit is contained in:
1 parent
aca3b5d861
commit
31e218295d
3 files changed
+65
-18
No files matched your search
@@ -5,4 +5,5 @@ export {
|
||||
createCustomCheckServerIdentity,
|
||||
createHttpsAgentAsync,
|
||||
fetchWithTrustedCertificatesAsync,
|
||||
mergeHeadersWithUserAgent,
|
||||
} from "./request";
|
||||
@@ -18,6 +18,14 @@ import packageJson from "../../../../../package.json";
|
||||
|
||||
export const getDefaultUserAgent = () => `Homarr/${packageJson.version} (+https://homarr.dev)`;
|
||||
|
||||
export const mergeHeadersWithUserAgent = (headers: unknown): Headers => {
|
||||
const mergedHeaders = new Headers(headers as HeadersInit);
|
||||
if (!mergedHeaders.has("user-agent")) {
|
||||
mergedHeaders.set("user-agent", getDefaultUserAgent());
|
||||
}
|
||||
return mergedHeaders;
|
||||
};
|
||||
|
||||
export const createCustomCheckServerIdentity = (
|
||||
trustedHostnames: TrustedCertificateHostname[],
|
||||
): typeof checkServerIdentity => {
|
||||
@@ -77,30 +85,18 @@ export const fetchWithTrustedCertificatesAsync = async (
|
||||
undefined,
|
||||
options?.bodyTimeout !== undefined ? { bodyTimeout: options.bodyTimeout } : undefined,
|
||||
));
|
||||
const buildFetchOptions = (fetchOptions: Omit<RequestInit, "dispatcher" | "bodyTimeout" | "timeout">) => ({
|
||||
...fetchOptions,
|
||||
headers: mergeHeadersWithUserAgent(fetchOptions.headers) as unknown as RequestInit["headers"],
|
||||
});
|
||||
if (options?.timeout) {
|
||||
const { bodyTimeout: _bodyTimeout, dispatcher: _dispatcher, ...fetchOptions } = options;
|
||||
return await withTimeoutAsync(
|
||||
async (signal) =>
|
||||
fetch(url, {
|
||||
...fetchOptions,
|
||||
headers: {
|
||||
"User-Agent": getDefaultUserAgent(),
|
||||
...fetchOptions.headers,
|
||||
},
|
||||
signal,
|
||||
dispatcher: agent,
|
||||
}),
|
||||
async (signal) => fetch(url, { ...buildFetchOptions(fetchOptions), signal, dispatcher: agent }),
|
||||
options.timeout,
|
||||
);
|
||||
}
|
||||
|
||||
const { bodyTimeout: _bodyTimeout, dispatcher: _dispatcher, ...fetchOptions } = options ?? {};
|
||||
return fetch(url, {
|
||||
...fetchOptions,
|
||||
headers: {
|
||||
"User-Agent": getDefaultUserAgent(),
|
||||
...fetchOptions.headers,
|
||||
},
|
||||
dispatcher: agent,
|
||||
});
|
||||
return fetch(url, { ...buildFetchOptions(fetchOptions), dispatcher: agent });
|
||||
};
|
||||
@@ -0,0 +1,50 @@
|
||||
import { describe, expect, test } from "vitest";
|
||||
|
||||
import { mergeHeadersWithUserAgent } from "@homarr/core/infrastructure/http";
|
||||
|
||||
describe("mergeHeadersWithUserAgent", () => {
|
||||
test("should keep existing headers when given a Headers instance", () => {
|
||||
// Arrange
|
||||
const headers = new Headers({ "x-api-key": "some-api-key", Accept: "application/json" });
|
||||
|
||||
// Act
|
||||
const merged = mergeHeadersWithUserAgent(headers);
|
||||
|
||||
// Assert
|
||||
expect(merged.get("x-api-key")).toBe("some-api-key");
|
||||
expect(merged.get("accept")).toBe("application/json");
|
||||
expect(merged.get("user-agent")).toContain("Homarr/");
|
||||
});
|
||||
|
||||
test("should keep existing headers when given a record", () => {
|
||||
// Arrange
|
||||
const headers = { "x-api-key": "some-api-key", Accept: "application/json" };
|
||||
|
||||
// Act
|
||||
const merged = mergeHeadersWithUserAgent(headers);
|
||||
|
||||
// Assert
|
||||
expect(merged.get("x-api-key")).toBe("some-api-key");
|
||||
expect(merged.get("accept")).toBe("application/json");
|
||||
expect(merged.get("user-agent")).toContain("Homarr/");
|
||||
});
|
||||
|
||||
test("should not override an existing user-agent in any casing", () => {
|
||||
// Arrange
|
||||
const headers = new Headers({ "USER-AGENT": "custom-agent" });
|
||||
|
||||
// Act
|
||||
const merged = mergeHeadersWithUserAgent(headers);
|
||||
|
||||
// Assert
|
||||
expect(merged.get("user-agent")).toBe("custom-agent");
|
||||
});
|
||||
|
||||
test("should fall back to the default user agent when no headers are provided", () => {
|
||||
// Act
|
||||
const merged = mergeHeadersWithUserAgent(undefined);
|
||||
|
||||
// Assert
|
||||
expect(merged.get("user-agent")).toContain("Homarr/");
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user