Skip to content

Commit c7fb528

Browse files
betegoncodex
andauthored
fix(cli): preserve a 401 on the final request attempt (#1370)
When two transient errors are followed by a 401, the CLI refreshes its OAuth token even though no request attempts remain, then throws `Exhausted all retry attempts`. Return the original 401 and response body instead so callers retain the authentication error. Extracts the 401 fix from #1367. Earlier 401 recovery and the retry budget stay unchanged. One regression test reuses the existing API/OAuth fixture to cover `503 → 503 → 401` and verify that no unused token refresh occurs. Co-authored-by: GPT-6 <noreply@openai.com>
1 parent 9358775 commit c7fb528

2 files changed

Lines changed: 17 additions & 1 deletion

File tree

‎packages/cli/src/lib/sentry-client.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -315,13 +315,14 @@ type AttemptResult =
315315
/**
316316
* Decide what to do with a successful HTTP response.
317317
* Returns 'done' for final responses, 'retry' for retryable errors and 401s.
318+
* Only refresh credentials when another attempt can use them.
318319
*/
319320
async function handleResponse(
320321
response: Response,
321322
headers: Headers,
322323
isLastAttempt: boolean
323324
): Promise<AttemptResult> {
324-
if (response.status === 401) {
325+
if (response.status === 401 && !isLastAttempt) {
325326
const refreshed = await handleUnauthorized(headers);
326327
return refreshed ? { action: "retry" } : { action: "done", response };
327328
}

‎packages/cli/test/lib/api-client.test.ts‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,21 @@ describe("401 retry behavior", () => {
225225
expect(oauthRequests).toHaveLength(1);
226226
});
227227

228+
test("preserves a final-attempt 401 without refreshing the token", async () => {
229+
const requests: RequestLog[] = [];
230+
const body = { detail: "Unauthorized" };
231+
globalThis.fetch = createMockFetch(requests, (_req, requestCount) =>
232+
Response.json(body, { status: requestCount < 3 ? 503 : 401 })
233+
);
234+
235+
const result = await rawApiRequest("/test-endpoint/");
236+
237+
expect(result.status).toBe(401);
238+
expect(result.body).toEqual(body);
239+
expect(requests).toHaveLength(3);
240+
expect(requests.filter((r) => r.url.includes("/oauth/token/"))).toEqual([]);
241+
});
242+
228243
test("does not retry for manual API tokens (no refresh token)", async () => {
229244
// Manual API tokens have no expiry and no refresh token
230245
await setAuthToken("manual-api-token"); // No expiry, no refresh token

0 commit comments

Comments
 (0)