From c327a9d7bece4a15fd18c114f68bfbe8eb023bfa Mon Sep 17 00:00:00 2001 From: Jeremy Sfez Date: Tue, 26 May 2026 14:26:28 +0200 Subject: [PATCH 1/2] feat(api): add requests headers --- packages/api-client/src/fetch.test.ts | 28 +++++++++++++++++++++++++++ packages/api-client/src/fetch.ts | 17 +++++++++++----- 2 files changed, 40 insertions(+), 5 deletions(-) diff --git a/packages/api-client/src/fetch.test.ts b/packages/api-client/src/fetch.test.ts index 90fe6e85..dfa47be5 100644 --- a/packages/api-client/src/fetch.test.ts +++ b/packages/api-client/src/fetch.test.ts @@ -72,6 +72,34 @@ describe("apiFetch", () => { expect(fetchMock).toHaveBeenCalledTimes(2); }); + it("adds stable request id and increments retry attempt headers", async () => { + const requestIds: string[] = []; + const retryAttempts: string[] = []; + const fetchMock = vi.fn(async (input: RequestInfo | URL) => { + const request = input instanceof Request ? input : new Request(input); + requestIds.push(request.headers.get("x-argos-request-id") ?? ""); + retryAttempts.push(request.headers.get("x-argos-retry-attempt") ?? ""); + + return new Response("{}", { + status: requestIds.length === 1 ? 500 : 200, + }); + }); + + const response = await apiFetch( + new Request("https://api.argos-ci.test/builds"), + { + fetch: fetchMock as unknown as typeof fetch, + minTimeout: 0, + }, + ); + + expect(response.status).toBe(200); + expect(requestIds).toHaveLength(2); + expect(requestIds[0]).toBeTruthy(); + expect(requestIds[0]).toBe(requestIds[1]); + expect(retryAttempts).toEqual(["0", "1"]); + }); + it("aborts the request after the configured timeout", async () => { const fetchMock = vi.fn( async (input: RequestInfo | URL) => diff --git a/packages/api-client/src/fetch.ts b/packages/api-client/src/fetch.ts index de5a3572..7f747225 100644 --- a/packages/api-client/src/fetch.ts +++ b/packages/api-client/src/fetch.ts @@ -20,13 +20,19 @@ async function createRequestFactory(request: Request, timeout: number) { // Snapshot the body once so retries do not clone/tee the original Request. const body = request.body ? await request.arrayBuffer() : undefined; const headers = new Headers(request.headers); + const requestId = + headers.get("x-argos-request-id") ?? globalThis.crypto.randomUUID(); - return () => - new Request(request.url, { + return (retryAttempt: number) => { + 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, + headers: requestHeaders, integrity: request.integrity, keepalive: request.keepalive, method: request.method, @@ -36,6 +42,7 @@ async function createRequestFactory(request: Request, timeout: number) { referrerPolicy: request.referrerPolicy, signal: AbortSignal.any([request.signal, AbortSignal.timeout(timeout)]), }); + }; } export async function apiFetch(input: Request, options: APIFetchOptions = {}) { @@ -48,8 +55,8 @@ export async function apiFetch(input: Request, options: APIFetchOptions = {}) { ); return pRetry( - async () => { - const response = await fetchImpl(createRequest()); + async (attemptNumber) => { + const response = await fetchImpl(createRequest(attemptNumber - 1)); if (response.status >= 500) { throw new APIError(`Internal Server Error (${response.status})`); } From ab29dbab60028e5db5bb914bdd3dd776c949128e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Greg=20Berg=C3=A9?= Date: Tue, 26 May 2026 20:58:00 +0200 Subject: [PATCH 2/2] chore: better support of header override --- packages/api-client/src/fetch.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/api-client/src/fetch.ts b/packages/api-client/src/fetch.ts index 7f747225..f0cfceb1 100644 --- a/packages/api-client/src/fetch.ts +++ b/packages/api-client/src/fetch.ts @@ -20,8 +20,8 @@ async function createRequestFactory(request: Request, timeout: number) { // Snapshot the body once so retries do not clone/tee the original Request. const body = request.body ? await request.arrayBuffer() : undefined; const headers = new Headers(request.headers); - const requestId = - headers.get("x-argos-request-id") ?? globalThis.crypto.randomUUID(); + const existingRequestId = headers.get("x-argos-request-id")?.trim(); + const requestId = existingRequestId || globalThis.crypto.randomUUID(); return (retryAttempt: number) => { const requestHeaders = new Headers(headers);