UNPKG

@argos-ci/api-client

Version:
152 lines (151 loc) 5.34 kB
import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.mjs"; import createFetchClient from "openapi-fetch"; import createDebug from "debug"; import pRetry from "p-retry"; const debug = createDebug("@argos-ci/api-client"); //#endregion //#region src/fetch.ts const DEFAULT_TIMEOUT = 3e4; var APIError = class extends Error { /** * HTTP status code of the response that triggered the error, when available. */ status; /** * Raw error payload returned by the API (or the infrastructure in front of * it). Useful when the body does not match the expected error shape. */ data; constructor(message, options) { super(message, options?.cause != null ? { cause: options.cause } : void 0); this.name = "APIError"; this.status = options?.status; this.data = options?.data; } }; async function createRequestFactory(request, timeout) { const body = request.body ? await request.arrayBuffer() : void 0; const headers = new Headers(request.headers); const requestId = headers.get("x-argos-request-id")?.trim() || globalThis.crypto.randomUUID(); return (retryAttempt) => { const requestHeaders = new Headers(headers); requestHeaders.set("x-argos-request-id", requestId); requestHeaders.set("x-argos-retry-attempt", String(retryAttempt)); return new Request(request.url, { body, cache: request.cache, credentials: request.credentials, headers: requestHeaders, integrity: request.integrity, keepalive: request.keepalive, method: request.method, mode: request.mode, redirect: request.redirect, referrer: request.referrer, referrerPolicy: request.referrerPolicy, signal: AbortSignal.any([request.signal, AbortSignal.timeout(timeout)]) }); }; } async function apiFetch(input, options = {}) { input.signal.throwIfAborted(); const fetchImpl = options.fetch ?? fetch; const createRequest = await createRequestFactory(input, options.timeout ?? DEFAULT_TIMEOUT); return pRetry(async (attemptNumber) => { const response = await fetchImpl(createRequest(attemptNumber - 1)); if (response.status >= 500) throw new APIError(`Internal Server Error (${response.status})`); return response; }, { minTimeout: options.minTimeout, retries: options.retries ?? 3, shouldRetry: () => !input.signal.aborted, onFailedAttempt: (context) => { debug("API request failed", context.error.message); if (context.retriesLeft > 0) debug(`Retrying API request... (${context.retriesLeft} left)`); } }); } //#endregion //#region src/schema.ts var schema_exports = /* @__PURE__ */ __exportAll({}); //#endregion //#region src/index.ts /** * Create Argos API client. */ function createClient(options) { const { baseUrl, authToken } = options || {}; return createFetchClient({ baseUrl: baseUrl || "https://api.argos-ci.com/v2/", headers: { Authorization: authToken ? `Bearer ${authToken}` : void 0 }, fetch: apiFetch }); } /** * Maximum length of a raw error body included in the formatted message. * Keeps things readable when the body is an HTML error page or a stack trace. */ const MAX_RAW_BODY_LENGTH = 500; /** * Check whether the value matches the expected API error shape: * `{ error: string, details?: [{ message }] }`. */ function isStructuredAPIError(error) { return typeof error === "object" && error !== null && "error" in error && typeof error.error === "string" && error.error.length > 0; } /** * Format the HTTP status of a response, e.g. `HTTP 413 Payload Too Large`. */ function formatStatus(response) { if (!response) return null; return response.statusText ? `HTTP ${response.status} ${response.statusText}` : `HTTP ${response.status}`; } /** * Format a raw (non-structured) error body so it can be shown to the user. * Returns `null` when there is nothing meaningful to display. */ function formatRawBody(error) { if (error == null) return null; let text; if (typeof error === "string") text = error; else try { text = JSON.stringify(error); } catch { text = String(error); } const trimmed = text.trim(); if (!trimmed || trimmed === "{}") return null; return trimmed.length > MAX_RAW_BODY_LENGTH ? `${trimmed.slice(0, MAX_RAW_BODY_LENGTH)}…` : trimmed; } /** * Format an API error into a human-readable message. * * When the body matches the expected `{ error, details }` shape, it is used * directly. Otherwise — empty body, HTML error page, plain text, etc., which * happens when the response comes from infrastructure in front of the API * (proxy, load balancer, CDN, rate limiter) rather than from the API itself — * we fall back to the HTTP status and the raw body so the failure stays * debuggable instead of surfacing an empty message. */ function formatAPIError(error, response) { debug("API error", { error, status: response?.status }); if (isStructuredAPIError(error)) { const detailMessage = error.details?.map((detail) => detail.message).join(", "); return detailMessage ? `${error.error}: ${detailMessage}` : error.error; } return [formatStatus(response), formatRawBody(error)].filter(Boolean).join(": ") || "Unknown API error"; } /** * Handle API errors. */ function throwAPIError(error, response) { throw new APIError(formatAPIError(error, response), { status: response?.status, data: error }); } //#endregion export { APIError, schema_exports as ArgosAPISchema, createClient, formatAPIError, throwAPIError };