@argos-ci/api-client
Version:
152 lines (151 loc) • 5.34 kB
JavaScript
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 };