From 1f581267e2ec211efb093ad81240f79ea255a492 Mon Sep 17 00:00:00 2001 From: Jesse Thibault Date: Fri, 27 Mar 2026 14:21:02 -0700 Subject: [PATCH] Fix 401 retry logic and wire forceRefresh through auth token provider Two bugs fixed: 1. Fetch.ts: The 401 retry loop used do {} while(false) with continue, which in JavaScript exits the loop immediately (continue in do-while jumps to the condition check, which is false, so the loop ends). This meant no REST client ever retried on 401 despite the code clearly intending to. Fixed by replacing with a proper while(true) loop that breaks after one retry. Also changed headers.append() to headers.set() to avoid duplicate Authorization headers on retry. 2. Client.ts: The default authTokenProvider created by getClient() ignored the forceRefresh parameter defined by the IAuthorizationTokenProvider interface. The parameter is now forwarded to SDK.getAccessToken(), enabling token refresh when the host supports it. Background: ADO services reject access tokens within ~5 minutes of expiry. The IAuthorizationTokenProvider interface defines forceRefresh for exactly this scenario, but it was never wired through. Combined with the broken retry loop, extensions had no way to recover from near-expiry token rejections. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Common/Client.ts | 4 ++-- src/Common/Fetch.ts | 16 +++++++++------- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/src/Common/Client.ts b/src/Common/Client.ts index 0f51a60d..a69114b8 100644 --- a/src/Common/Client.ts +++ b/src/Common/Client.ts @@ -27,8 +27,8 @@ export function getClient(clientClass: RestClientFactory, clientOptions?: if (!options.authTokenProvider) { options.authTokenProvider = { - getAuthorizationHeader: (): Promise => { - return getAccessToken().then(token => token ? ("Bearer " + token) : ""); + getAuthorizationHeader: (forceRefresh?: boolean): Promise => { + return (getAccessToken as (forceRefresh?: boolean) => Promise)(forceRefresh).then(token => token ? ("Bearer " + token) : ""); } }; } diff --git a/src/Common/Fetch.ts b/src/Common/Fetch.ts index ee219ded..c2ccdc80 100644 --- a/src/Common/Fetch.ts +++ b/src/Common/Fetch.ts @@ -174,17 +174,17 @@ export async function issueRequest(requestUrl: string, options?: RequestInit, vs options.credentials = "same-origin"; let refreshToken = false; + let retried = false; - do { + while (true) { // Ensure we have an authorization token available from the auth manager. if (vssRequestOptions && vssRequestOptions.authTokenProvider) { const authHeader = await vssRequestOptions.authTokenProvider.getAuthorizationHeader(refreshToken); if (authHeader) { - headers.append("Authorization", authHeader); - refreshToken = true; + headers.set("Authorization", authHeader); } - headers.append("X-TFS-FedAuthRedirect", "Suppress"); + headers.set("X-TFS-FedAuthRedirect", "Suppress"); } // Execute the http request defined by the caller. @@ -198,13 +198,15 @@ export async function issueRequest(requestUrl: string, options?: RequestInit, vs throw error; } - // If we recieved a 401 and have a token manager, we will refresh our token and try again. - if (response.status === 401 && !refreshToken && vssRequestOptions && vssRequestOptions.authTokenProvider) { + // If we received a 401 and have a token manager, refresh the token and retry once. + if (response.status === 401 && !retried && vssRequestOptions && vssRequestOptions.authTokenProvider) { + retried = true; refreshToken = true; continue; } - } while (false); + break; + } // Parse error details from requests that returned non 200-299 status codes. if (!response.ok) {