/** * @license * Copyright 2026 Steven Roussey * SPDX-License-Identifier: Apache-2.0 */ import { AbortSignalJobError, JobError } from "@workglow/job-queue"; /** * Machine-readable error codes for {@link FetchUrlJob} / {@link FetchUrlTask}. * Persisted as `error_code` on queued jobs when a fetch fails. */ export declare const FetchUrlErrorCode: { readonly INVALID_URL: "FETCH_INVALID_URL"; readonly PRIVATE_DENIED: "FETCH_PRIVATE_DENIED"; readonly SCOPE_DENIED: "FETCH_SCOPE_DENIED"; readonly DNS_FAILED: "FETCH_DNS_FAILED"; readonly TOO_MANY_REDIRECTS: "FETCH_TOO_MANY_REDIRECTS"; readonly REDIRECT_MISSING_LOCATION: "FETCH_REDIRECT_MISSING_LOCATION"; readonly HTTP_CLIENT_ERROR: "FETCH_HTTP_CLIENT_ERROR"; readonly HTTP_RATE_LIMITED: "FETCH_HTTP_RATE_LIMITED"; readonly HTTP_SERVER_ERROR: "FETCH_HTTP_SERVER_ERROR"; readonly RESPONSE_PARSE_ERROR: "FETCH_RESPONSE_PARSE_ERROR"; readonly INVALID_RESPONSE_TYPE: "FETCH_INVALID_RESPONSE_TYPE"; readonly NETWORK_ERROR: "FETCH_NETWORK_ERROR"; readonly NO_RESPONSE_BODY: "FETCH_NO_RESPONSE_BODY"; readonly CONFIGURATION: "FETCH_CONFIGURATION"; readonly CONTENT_LENGTH_MISMATCH: "FETCH_CONTENT_LENGTH_MISMATCH"; /** * The request failed after body bytes had already been delivered to the * consumer. Deliberately absent from {@link FETCH_URL_RETRYABLE_ERROR_CODES}: * a retry re-issues from byte 0 while the consumer's stream subscription * survives the attempt, so the partial body and the retry's full body would * concatenate into a corrupt result the job then reports as success. Distinct * from {@link FetchUrlErrorCode.NETWORK_ERROR}, which is the same wire failure * before the first byte reached anyone and stays retryable. */ readonly BODY_TRUNCATED: "FETCH_BODY_TRUNCATED"; /** * A 307/308 redirect crossed to a different origin while the request carried * a body. 307/308 preserve method and body by definition, so there is no * downgrade that both withholds the body and still performs the write the * caller asked for. Deliberately absent from * {@link FETCH_URL_RETRYABLE_ERROR_CODES}: a retry re-issues the same hop and * meets the same refusal. */ readonly REDIRECT_BODY_NOT_REPLAYED: "FETCH_REDIRECT_BODY_NOT_REPLAYED"; }; export type FetchUrlErrorCodeValue = (typeof FetchUrlErrorCode)[keyof typeof FetchUrlErrorCode]; /** Error codes that should be retried by the job queue. */ export declare const FETCH_URL_RETRYABLE_ERROR_CODES: ReadonlySet; export declare function isFetchUrlErrorCode(value: string | undefined | null): value is FetchUrlErrorCodeValue; export declare function isFetchUrlRetryableErrorCode(code: string | undefined | null): code is FetchUrlErrorCodeValue; export interface FetchUrlJobErrorDetails { readonly url?: string; readonly httpStatus?: number; readonly httpStatusText?: string; readonly httpErrorMessage?: string; } export type FetchUrlJobErrorInstance = JobError & { code: FetchUrlErrorCodeValue; url?: string; httpStatus?: number; httpStatusText?: string; httpErrorMessage?: string; retryDate?: Date; }; /** * Create a {@link JobError} for a fetch failure with a stable `code` for persistence. */ export declare function createFetchUrlJobError(code: FetchUrlErrorCodeValue, message: string, options?: FetchUrlJobErrorDetails & { retryDate?: Date; }): FetchUrlJobErrorInstance; /** * Reconstruct a fetch error from persisted queue fields (`error`, `error_code`). */ export declare function fetchUrlJobErrorFromPersisted(message: string, errorCode: string | undefined): JobError | undefined; /** * Adapter for {@link registerErrorCodeReconstructor}. Reconstructs a * `FetchUrlJobError`-shaped error from a persisted `FETCH_*` code. * * Unknown future codes that still start with `FETCH_` fall back to a generic * `PermanentJobError` so a forward-compat worker can persist a new code and * an older client can still surface a typed error (with a warning logged so * the version skew is visible). */ export declare function buildFetchUrlError(errorCode: string, message: string): JobError; export declare function httpStatusToFetchUrlErrorCode(status: number): FetchUrlErrorCodeValue; export declare function createFetchUrlHttpError(url: string, status: number, statusText: string, retryDate?: Date, body?: string): FetchUrlJobErrorInstance; /** Reads `{message}` from a JSON error body, if that field is a non-empty string. */ export declare function jsonMessageFromHttpBody(body: string | undefined): string | undefined; /** * True when `error` (or a nested `cause`) is a dropped connection / DNS / * timeout rather than a completed body we failed to decode. `response.text()` * and `response.json()` throw these when the peer closes mid-body; they must * not be classified as {@link FetchUrlErrorCode.RESPONSE_PARSE_ERROR}. * * Abort is excluded: a cancelled fetch is not a transient network blip. * * A `SyntaxError` is excluded from the MESSAGE heuristic — never from the * `code` / `cause` checks — because a decode failure's message embeds * server-controlled bytes: V8 quotes a snippet of the body into it * (`Unexpected token 'G', "Gateway timeout..." is not valid JSON`), so a * response could otherwise choose its own error code and keep the queue * retrying a URL that can never decode. A body that reached `JSON.parse` at all * arrived complete — the stream errors before the parser runs when the peer * drops mid-body — so a `SyntaxError`'s message is never network evidence. * Discriminated by `name` rather than `instanceof` because this classifier is * reachable from worker-hosted job code, where realms differ. */ export declare function isFetchUrlNetworkCause(error: unknown, depth?: number): boolean; export declare function wrapFetchUrlNetworkError(url: string, cause: unknown): FetchUrlJobErrorInstance; export declare function isFetchUrlJobError(error: unknown): error is FetchUrlJobErrorInstance; /** @internal Used by fetch helpers when the run was aborted. */ export declare function createFetchUrlAbortedError(): AbortSignalJobError;