feat(backups): configure backup webhook timeout
Some checks failed
Release Workflow / determine-release-type (push) Has been cancelled
Release Workflow / checks (push) Has been cancelled
Release Workflow / e2e-tests (push) Has been cancelled
Release Workflow / build-images (push) Has been cancelled
Release Workflow / publish-release (push) Has been cancelled
Release Workflow / request-docs-version-update (push) Has been cancelled

This commit is contained in:
Nicolas Meienberger 2026-05-04 07:52:21 +02:00
parent 38f5a669ae
commit 8772d4796e
No known key found for this signature in database
10 changed files with 19 additions and 5 deletions

View file

@ -45,6 +45,7 @@ describe("parseConfig", () => {
ENABLE_DEV_PANEL: "true",
SERVER_IP: "0.0.0.0",
SERVER_IDLE_TIMEOUT: "120",
WEBHOOK_TIMEOUT: "90",
PORT: "8080",
APP_VERSION: "1.2.3",
MIGRATIONS_PATH: "/tmp/migrations",
@ -57,6 +58,7 @@ describe("parseConfig", () => {
environment: "development",
serverIp: "0.0.0.0",
serverIdleTimeout: 120,
webhookTimeout: 90,
resticHostname: "configured-restic-host",
port: 8080,
migrationsPath: "/tmp/migrations",

View file

@ -32,6 +32,7 @@ const envSchema = z
NODE_ENV: z.enum(["development", "production", "test"]).default("production"),
SERVER_IP: z.string().default("localhost"),
SERVER_IDLE_TIMEOUT: z.coerce.number().int().default(60),
WEBHOOK_TIMEOUT: z.coerce.number().int().default(60),
RESTIC_HOSTNAME: z.string().optional(),
PORT: z.coerce.number().int().default(4096),
MIGRATIONS_PATH: z.string().optional(),
@ -122,6 +123,7 @@ const envSchema = z
environment: s.NODE_ENV,
serverIp: s.SERVER_IP,
serverIdleTimeout: s.SERVER_IDLE_TIMEOUT,
webhookTimeout: s.WEBHOOK_TIMEOUT,
resticHostname: s.RESTIC_HOSTNAME || getResticHostname(),
port: s.PORT,
migrationsPath: s.MIGRATIONS_PATH,

View file

@ -146,6 +146,7 @@ test("close emits a synthetic backup.cancelled for a queued backup", () => {
},
webhooks: { pre: null, post: null },
webhookAllowedOrigins: [],
webhookTimeoutMs: 60_000,
}),
);

View file

@ -64,6 +64,7 @@ const createBackupRunPayload = async ({
},
webhooks: schedule.backupWebhooks ?? { pre: null, post: null },
webhookAllowedOrigins: config.webhookAllowedOrigins,
webhookTimeoutMs: config.webhookTimeout * 1000,
};
};
@ -82,6 +83,7 @@ const executeBackupWithoutAgent = async (
options: payload.options,
webhooks: payload.webhooks,
webhookAllowedOrigins: payload.webhookAllowedOrigins,
webhookTimeoutMs: payload.webhookTimeoutMs,
signal,
onProgress,
formatError: toErrorDetails,

View file

@ -48,6 +48,7 @@ test("emits backup.failed when a backup command hits a restic error", async () =
},
webhooks: { pre: null, post: null },
webhookAllowedOrigins: [],
webhookTimeoutMs: 60_000,
}),
);

View file

@ -54,6 +54,7 @@ const createRunPayload = (overrides: Partial<BackupRunPayload> = {}) =>
},
webhooks: { pre: null, post: null },
webhookAllowedOrigins: ["http://localhost:8080"],
webhookTimeoutMs: 60_000,
...overrides,
});
@ -337,6 +338,7 @@ test("waits for running-job registration before returning to the processor loop"
},
webhooks: { pre: null, post: null },
webhookAllowedOrigins: [],
webhookTimeoutMs: 60_000,
});
const cancelPayload = fromPartial<BackupCancelPayload>({
jobId: "job-1",

View file

@ -67,6 +67,7 @@ export const handleBackupRunCommand = (context: ControllerCommandContext, payloa
options: payload.options,
webhooks: payload.webhooks,
webhookAllowedOrigins: payload.webhookAllowedOrigins,
webhookTimeoutMs: payload.webhookTimeoutMs,
signal: abortController.signal,
onProgress: (progress) => {
void Runtime.runPromise(

View file

@ -42,6 +42,7 @@ const backupRunSchema = z.object({
runtime: backupRuntimeSchema,
webhooks: backupWebhooksSchema,
webhookAllowedOrigins: z.array(z.string()),
webhookTimeoutMs: z.number(),
}),
});

View file

@ -45,6 +45,7 @@ const runWithHooks = <TResult>(
options: {},
webhooks: { pre: null, post: null },
webhookAllowedOrigins: ["http://localhost:8080"],
webhookTimeoutMs: 60_000,
signal: defaultSignal(),
...options,
}),

View file

@ -3,7 +3,6 @@ import { z } from "zod";
import type { CompressionMode, RepositoryConfig, ResticBackupProgressDto } from "../restic/index.js";
import { toErrorDetails, toMessage } from "../utils/index.js";
const DEFAULT_BACKUP_WEBHOOK_TIMEOUT_MS = 10_000;
const MAX_BACKUP_WEBHOOK_BODY_BYTES = 64 * 1024;
const MAX_BACKUP_WEBHOOK_HEADERS = 32;
const MAX_BACKUP_WEBHOOK_HEADER_BYTES = 8 * 1024;
@ -82,6 +81,7 @@ type BackupLifecycleOptions<TResult> = {
options: BackupOptions;
webhooks: BackupWebhooks;
webhookAllowedOrigins: readonly string[];
webhookTimeoutMs: number;
signal: AbortSignal;
onProgress?: (progress: ResticBackupProgressDto) => void;
formatError?: (error: unknown) => string;
@ -190,7 +190,7 @@ const runBackupWebhook = (
formatError: (error: unknown) => string;
allowedOrigins: readonly string[];
signal?: AbortSignal;
timeoutMs?: number;
timeoutMs: number;
},
) =>
Effect.suspend(() => {
@ -198,8 +198,7 @@ const runBackupWebhook = (
return Effect.succeed(null);
}
const timeoutMs = options.timeoutMs ?? DEFAULT_BACKUP_WEBHOOK_TIMEOUT_MS;
const controller = createAbortController(timeoutMs, options.signal);
const controller = createAbortController(options.timeoutMs, options.signal);
return Effect.tryPromise({
try: async () => {
@ -256,6 +255,7 @@ export const runBackupLifecycle = <TResult>({
options,
webhooks,
webhookAllowedOrigins,
webhookTimeoutMs,
signal,
onProgress,
formatError = toErrorDetails,
@ -268,6 +268,7 @@ export const runBackupLifecycle = <TResult>({
{
formatError,
allowedOrigins: webhookAllowedOrigins,
timeoutMs: webhookTimeoutMs,
signal,
},
);
@ -312,7 +313,7 @@ export const runBackupLifecycle = <TResult>({
status: backupResult.hookStatus,
error: backupResult.hookError,
},
{ formatError, allowedOrigins: webhookAllowedOrigins },
{ formatError, allowedOrigins: webhookAllowedOrigins, timeoutMs: webhookTimeoutMs },
);
if (signal.aborted) {