Compare commits
8
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eba639b59c | ||
|
|
da36283ad1 | ||
|
|
c050368782 | ||
|
|
82b50f0fd7 | ||
|
|
a0149d5894 | ||
|
|
8579fcbfc2 | ||
|
|
b6f3aba23e | ||
|
|
31e218295d |
No files matched your search
@@ -1,3 +1,5 @@
|
|||||||
|
## [1.76.1](https://github.com/homarr-labs/homarr/compare/v1.76.0...v1.76.1) (2026-08-26)
|
||||||
|
|
||||||
## [1.76.0](https://github.com/homarr-labs/homarr/compare/v1.75.0...v1.76.0) (2026-08-21)
|
## [1.76.0](https://github.com/homarr-labs/homarr/compare/v1.75.0...v1.76.0) (2026-08-21)
|
||||||
|
|
||||||
## [1.75.0](https://github.com/homarr-labs/homarr/compare/v1.74.0...v1.75.0) (2026-08-14)
|
## [1.75.0](https://github.com/homarr-labs/homarr/compare/v1.74.0...v1.75.0) (2026-08-14)
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "homarr",
|
"name": "homarr",
|
||||||
"version": "1.76.0",
|
"version": "1.76.1",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "cross-env CI=true turbo build",
|
"build": "cross-env CI=true turbo build",
|
||||||
|
|||||||
@@ -5,4 +5,5 @@ export {
|
|||||||
createCustomCheckServerIdentity,
|
createCustomCheckServerIdentity,
|
||||||
createHttpsAgentAsync,
|
createHttpsAgentAsync,
|
||||||
fetchWithTrustedCertificatesAsync,
|
fetchWithTrustedCertificatesAsync,
|
||||||
|
mergeHeadersWithUserAgent,
|
||||||
} from "./request";
|
} from "./request";
|
||||||
@@ -18,6 +18,14 @@ import packageJson from "../../../../../package.json";
|
|||||||
|
|
||||||
export const getDefaultUserAgent = () => `Homarr/${packageJson.version} (+https://homarr.dev)`;
|
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 = (
|
export const createCustomCheckServerIdentity = (
|
||||||
trustedHostnames: TrustedCertificateHostname[],
|
trustedHostnames: TrustedCertificateHostname[],
|
||||||
): typeof checkServerIdentity => {
|
): typeof checkServerIdentity => {
|
||||||
@@ -77,30 +85,18 @@ export const fetchWithTrustedCertificatesAsync = async (
|
|||||||
undefined,
|
undefined,
|
||||||
options?.bodyTimeout !== undefined ? { bodyTimeout: options.bodyTimeout } : 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) {
|
if (options?.timeout) {
|
||||||
const { bodyTimeout: _bodyTimeout, dispatcher: _dispatcher, ...fetchOptions } = options;
|
const { bodyTimeout: _bodyTimeout, dispatcher: _dispatcher, ...fetchOptions } = options;
|
||||||
return await withTimeoutAsync(
|
return await withTimeoutAsync(
|
||||||
async (signal) =>
|
async (signal) => fetch(url, { ...buildFetchOptions(fetchOptions), signal, dispatcher: agent }),
|
||||||
fetch(url, {
|
|
||||||
...fetchOptions,
|
|
||||||
headers: {
|
|
||||||
"User-Agent": getDefaultUserAgent(),
|
|
||||||
...fetchOptions.headers,
|
|
||||||
},
|
|
||||||
signal,
|
|
||||||
dispatcher: agent,
|
|
||||||
}),
|
|
||||||
options.timeout,
|
options.timeout,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { bodyTimeout: _bodyTimeout, dispatcher: _dispatcher, ...fetchOptions } = options ?? {};
|
const { bodyTimeout: _bodyTimeout, dispatcher: _dispatcher, ...fetchOptions } = options ?? {};
|
||||||
return fetch(url, {
|
return fetch(url, { ...buildFetchOptions(fetchOptions), dispatcher: agent });
|
||||||
...fetchOptions,
|
|
||||||
headers: {
|
|
||||||
"User-Agent": getDefaultUserAgent(),
|
|
||||||
...fetchOptions.headers,
|
|
||||||
},
|
|
||||||
dispatcher: agent,
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
@@ -0,0 +1,120 @@
|
|||||||
|
// @vitest-environment node
|
||||||
|
|
||||||
|
import { createServer } from "node:http";
|
||||||
|
import type { Server } from "node:http";
|
||||||
|
import type { AddressInfo } from "node:net";
|
||||||
|
import type { RequestInit } from "undici";
|
||||||
|
|
||||||
|
import { describe, expect, test, vi } from "vitest";
|
||||||
|
|
||||||
|
import { fetchWithTrustedCertificatesAsync, mergeHeadersWithUserAgent } from "@homarr/core/infrastructure/http";
|
||||||
|
|
||||||
|
vi.mock("@homarr/core/infrastructure/certificates", () => ({
|
||||||
|
getAllTrustedCertificatesAsync: vi.fn(async () => []),
|
||||||
|
getTrustedCertificateHostnamesAsync: vi.fn(async () => []),
|
||||||
|
}));
|
||||||
|
|
||||||
|
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 keep existing headers when given a tuple array", () => {
|
||||||
|
// Arrange
|
||||||
|
const headers: [string, string][] = [
|
||||||
|
["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/");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const startCaptureServerAsync = async (): Promise<{
|
||||||
|
server: Server;
|
||||||
|
getReceivedHeaders: () => Record<string, string | string[] | undefined>;
|
||||||
|
port: number;
|
||||||
|
}> => {
|
||||||
|
let receivedHeaders: Record<string, string | string[] | undefined> = {};
|
||||||
|
const server = createServer((req, res) => {
|
||||||
|
receivedHeaders = req.headers;
|
||||||
|
res.writeHead(200, { "Content-Type": "application/json" });
|
||||||
|
res.end("{}");
|
||||||
|
});
|
||||||
|
await new Promise<void>((resolve) => server.listen(0, resolve));
|
||||||
|
const port = (server.address() as AddressInfo).port;
|
||||||
|
return {
|
||||||
|
server,
|
||||||
|
port,
|
||||||
|
getReceivedHeaders: () => receivedHeaders,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
describe("fetchWithTrustedCertificatesAsync", () => {
|
||||||
|
test("should deliver a Headers instance with all headers to the remote server", async () => {
|
||||||
|
// Arrange - the @immich/sdk hands over a Headers instance after merging its defaults
|
||||||
|
const { server, port, getReceivedHeaders } = await startCaptureServerAsync();
|
||||||
|
const headers = new Headers({ "x-api-key": "immich-api-key", Accept: "application/json" });
|
||||||
|
|
||||||
|
// Act
|
||||||
|
try {
|
||||||
|
const response = await fetchWithTrustedCertificatesAsync(`http://127.0.0.1:${port}/api/server/statistics`, {
|
||||||
|
headers: headers as unknown as RequestInit["headers"],
|
||||||
|
});
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(response.status).toBe(200);
|
||||||
|
expect(getReceivedHeaders()["x-api-key"]).toBe("immich-api-key");
|
||||||
|
expect(getReceivedHeaders()["user-agent"]).toContain("Homarr/");
|
||||||
|
} finally {
|
||||||
|
server.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
// @vitest-environment node
|
||||||
|
|
||||||
|
import { createServer } from "node:http";
|
||||||
|
import type { AddressInfo } from "node:net";
|
||||||
|
|
||||||
|
import { getServerStatistics, init } from "@immich/sdk";
|
||||||
|
import { describe, expect, test, vi } from "vitest";
|
||||||
|
|
||||||
|
import { fetchWithTrustedCertificatesAsync } from "@homarr/core/infrastructure/http";
|
||||||
|
|
||||||
|
vi.mock("@homarr/core/infrastructure/certificates", () => ({
|
||||||
|
getAllTrustedCertificatesAsync: vi.fn(async () => []),
|
||||||
|
getTrustedCertificateHostnamesAsync: vi.fn(async () => []),
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe("real @immich/sdk end-to-end through fetchWithTrustedCertificatesAsync", () => {
|
||||||
|
test("x-api-key set via init() reaches the server", async () => {
|
||||||
|
let receivedXApiKey: string | string[] | undefined;
|
||||||
|
const server = createServer((req, res) => {
|
||||||
|
receivedXApiKey = req.headers["x-api-key"];
|
||||||
|
res.writeHead(200, { "Content-Type": "application/json" });
|
||||||
|
res.end(JSON.stringify({ usage: 1000, photos: 5, videos: 2 }));
|
||||||
|
});
|
||||||
|
await new Promise<void>((resolve) => server.listen(0, resolve));
|
||||||
|
const port = (server.address() as AddressInfo).port;
|
||||||
|
|
||||||
|
init({ baseUrl: `http://127.0.0.1:${port}/api`, apiKey: "sdk-configured-key" });
|
||||||
|
try {
|
||||||
|
const stats = await getServerStatistics({
|
||||||
|
fetch: fetchWithTrustedCertificatesAsync as unknown as typeof fetch,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(receivedXApiKey).toBe("sdk-configured-key");
|
||||||
|
expect(stats).toMatchObject({ usage: 1000, photos: 5, videos: 2 });
|
||||||
|
} finally {
|
||||||
|
server.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in new issue
Block a user