9352f10d0d
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
98 lines
3.8 KiB
TypeScript
98 lines
3.8 KiB
TypeScript
export interface GiteaWebhookIngressRetry {
|
|
enabled: boolean;
|
|
maxBodyBytes: number;
|
|
maxRetries: number;
|
|
tryIntervalMs: number;
|
|
dialTimeoutMs: number;
|
|
writeTimeoutMs: number;
|
|
retryStatusCodes: number[];
|
|
}
|
|
|
|
export interface GiteaWebhookCaddyRoute {
|
|
publicPath: string;
|
|
remotePort: number;
|
|
}
|
|
|
|
export function validateGiteaWebhookTiming(
|
|
responseBudgetMs: number,
|
|
ingressRetry: Pick<GiteaWebhookIngressRetry, "maxRetries" | "tryIntervalMs" | "dialTimeoutMs" | "writeTimeoutMs">,
|
|
): void {
|
|
if (responseBudgetMs >= 9000) {
|
|
throw new Error("responseBudgetMs must stay below 9000 to leave margin under GitHub's 10 second webhook deadline");
|
|
}
|
|
if (ingressRetry.maxRetries !== 1) {
|
|
throw new Error("ingressRetry.maxRetries must be 1 so the fsync acceptance path has one bounded replay");
|
|
}
|
|
const responseHeaderTimeoutMs = deriveGiteaWebhookResponseHeaderTimeoutMs(responseBudgetMs, ingressRetry);
|
|
const hardBoundMs = (ingressRetry.dialTimeoutMs + ingressRetry.writeTimeoutMs + responseHeaderTimeoutMs) * (ingressRetry.maxRetries + 1)
|
|
+ ingressRetry.tryIntervalMs * ingressRetry.maxRetries;
|
|
if (hardBoundMs > responseBudgetMs) {
|
|
throw new Error("Caddy retry attempts and intervals must fit inside responseBudgetMs");
|
|
}
|
|
}
|
|
|
|
export function deriveGiteaWebhookAttemptBudgetMs(
|
|
responseBudgetMs: number,
|
|
ingressRetry: Pick<GiteaWebhookIngressRetry, "maxRetries" | "tryIntervalMs">,
|
|
): number {
|
|
const attempts = ingressRetry.maxRetries + 1;
|
|
const intervalBudgetMs = ingressRetry.maxRetries * ingressRetry.tryIntervalMs;
|
|
const attemptBudgetMs = Math.floor((responseBudgetMs - intervalBudgetMs) / attempts);
|
|
if (!Number.isFinite(attemptBudgetMs) || attemptBudgetMs < 1) {
|
|
throw new Error("responseBudgetMs must leave a positive response-header timeout for every Caddy attempt");
|
|
}
|
|
return attemptBudgetMs;
|
|
}
|
|
|
|
export function deriveGiteaWebhookResponseHeaderTimeoutMs(
|
|
responseBudgetMs: number,
|
|
ingressRetry: Pick<GiteaWebhookIngressRetry, "maxRetries" | "tryIntervalMs" | "dialTimeoutMs" | "writeTimeoutMs">,
|
|
): number {
|
|
const attemptBudgetMs = deriveGiteaWebhookAttemptBudgetMs(responseBudgetMs, ingressRetry);
|
|
const responseHeaderTimeoutMs = attemptBudgetMs - ingressRetry.dialTimeoutMs - ingressRetry.writeTimeoutMs;
|
|
if (!Number.isFinite(responseHeaderTimeoutMs) || responseHeaderTimeoutMs < 1) {
|
|
throw new Error("dialTimeoutMs and writeTimeoutMs must leave a positive response-header timeout in every Caddy attempt");
|
|
}
|
|
return responseHeaderTimeoutMs;
|
|
}
|
|
|
|
export function renderGiteaWebhookCaddyHandle(
|
|
route: GiteaWebhookCaddyRoute,
|
|
retry: GiteaWebhookIngressRetry,
|
|
responseBudgetMs: number,
|
|
legacyResponseHeaderTimeoutSeconds: number,
|
|
): string {
|
|
if (!retry.enabled) {
|
|
return ` handle ${route.publicPath}* {
|
|
reverse_proxy 127.0.0.1:${route.remotePort} {
|
|
transport http {
|
|
response_header_timeout ${legacyResponseHeaderTimeoutSeconds}s
|
|
}
|
|
}
|
|
}`;
|
|
}
|
|
validateGiteaWebhookTiming(responseBudgetMs, retry);
|
|
const responseHeaderTimeoutMs = deriveGiteaWebhookResponseHeaderTimeoutMs(responseBudgetMs, retry);
|
|
const replayBufferBytes = retry.maxBodyBytes + 1;
|
|
const retryStatuses = retry.retryStatusCodes.join(", ");
|
|
return ` handle ${route.publicPath}* {
|
|
request_body {
|
|
max_size ${retry.maxBodyBytes}
|
|
}
|
|
reverse_proxy 127.0.0.1:${route.remotePort} {
|
|
request_buffers ${replayBufferBytes}
|
|
lb_retries ${retry.maxRetries}
|
|
lb_try_duration ${responseBudgetMs}ms
|
|
lb_try_interval ${retry.tryIntervalMs}ms
|
|
lb_retry_match {
|
|
expression \`method('POST') && ({rp.is_transport_error} || {rp.status_code} in [${retryStatuses}])\`
|
|
}
|
|
transport http {
|
|
dial_timeout ${retry.dialTimeoutMs}ms
|
|
write_timeout ${retry.writeTimeoutMs}ms
|
|
response_header_timeout ${responseHeaderTimeoutMs}ms
|
|
}
|
|
}
|
|
}`;
|
|
}
|