import { HttpError } from "../errors"; import { isArtifactsError } from "./artifacts"; const HTTP_BAD_GATEWAY = 502; const REFERENCE = /reference\s*=\s*([a-z0-9]+)/i; const RETRY_DELAY_MS = 250; const REMOTE_PREFIX = /^ArtifactsError:\s*/; const CODE_PATTERNS: Array<[RegExp, ArtifactsErrorCode]> = [ [/being forked|fork is still in progress/i, "FORK_IN_PROGRESS"], [/being imported|import is still in progress/i, "IMPORT_IN_PROGRESS"], [/being created/i, "CREATE_IN_PROGRESS"], [/already exists/i, "ALREADY_EXISTS"], [/requires? auth/i, "REMOTE_AUTH_REQUIRED"], [/memory limit/i, "MEMORY_LIMIT"], [/not found|does not exist/i, "NOT_FOUND"], ]; /** * Remote bindings in local dev (and some RPC paths) deliver ArtifactsErrors as plain Errors whose message * starts with "ArtifactsError:", losing `name` and `code`; rebuild a typed error so callers can branch on it. */ function revive(error: unknown): ArtifactsError | null { if (!(error instanceof Error) || !REMOTE_PREFIX.test(error.message)) return null; const message = error.message.replace(REMOTE_PREFIX, ""); const code = CODE_PATTERNS.find(([pattern]) => pattern.test(message))?.[1] ?? "INTERNAL_ERROR"; return Object.assign(new Error(message), { name: "ArtifactsError" as const, code, numericCode: 0 }); } /** An Artifacts failure without a typed error code, e.g. the opaque "internal error; reference = …". */ export class ArtifactsUnavailableError extends HttpError { readonly operation: string; readonly reference: string | null; constructor(operation: string, target: string, cause: unknown) { const detail = cause instanceof Error ? cause.message : String(cause); const reference = REFERENCE.exec(detail)?.[1] ?? null; super( HTTP_BAD_GATEWAY, `Cloudflare Artifacts failed to ${operation} for ${target}${reference ? ` (reference ${reference})` : ""}. ` + "This is usually transient; try again shortly.", ); this.name = "ArtifactsUnavailableError"; this.operation = operation; this.reference = reference; } } /** * Runs an Artifacts binding call, keeping typed ArtifactsErrors intact (callers branch on their codes) * but turning untyped failures into a logged, user-readable error that names the operation and repo. * Idempotent reads are retried once, since untyped failures are usually transient. */ export async function artifactsCall( operation: string, target: string, call: () => Promise, options: { idempotent?: boolean } = {}, ): Promise { try { return await call(); } catch (first) { if (!options.idempotent || typedFailure(first)) return rethrow(operation, target, first); await new Promise((resolve) => setTimeout(resolve, RETRY_DELAY_MS)); try { return await call(); } catch (second) { return rethrow(operation, target, second); } } } function typedFailure(error: unknown): boolean { if (isArtifactsError(error) || error instanceof ArtifactsUnavailableError) return true; const revived = revive(error); return revived !== null && revived.code !== "INTERNAL_ERROR"; } function rethrow(operation: string, target: string, error: unknown): never { if (isArtifactsError(error) || error instanceof ArtifactsUnavailableError) throw error; const revived = revive(error); if (revived && revived.code !== "INTERNAL_ERROR") throw revived; const wrapped = new ArtifactsUnavailableError(operation, target, error); console.error(`Artifacts ${operation} failed`, { target, reference: wrapped.reference, error }); throw wrapped; }