Skip to content

Commit b93e027

Browse files
committed
Add error handling
1 parent ee6f4a0 commit b93e027

File tree

7 files changed

+410
-186
lines changed

7 files changed

+410
-186
lines changed

src/api/coderApi.ts

Lines changed: 90 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import {
33
type AxiosInstance,
44
type AxiosHeaders,
55
type AxiosResponseTransformer,
6+
isAxiosError,
67
} from "axios";
78
import { Api } from "coder/site/src/api/api";
89
import {
@@ -30,6 +31,12 @@ import {
3031
HttpClientLogLevel,
3132
} from "../logging/types";
3233
import { sizeOf } from "../logging/utils";
34+
import {
35+
parseOAuthError,
36+
requiresReAuthentication,
37+
isNetworkError,
38+
} from "../oauth/errors";
39+
import { type OAuthSessionManager } from "../oauth/sessionManager";
3340
import { type UnidirectionalStream } from "../websocket/eventStreamConnection";
3441
import {
3542
OneWayWebSocket,
@@ -58,14 +65,15 @@ export class CoderApi extends Api {
5865
baseUrl: string,
5966
token: string | undefined,
6067
output: Logger,
68+
oauthSessionManager?: OAuthSessionManager,
6169
): CoderApi {
6270
const client = new CoderApi(output);
6371
client.setHost(baseUrl);
6472
if (token) {
6573
client.setSessionToken(token);
6674
}
6775

68-
setupInterceptors(client, baseUrl, output);
76+
setupInterceptors(client, baseUrl, output, oauthSessionManager);
6977
return client;
7078
}
7179

@@ -302,6 +310,7 @@ function setupInterceptors(
302310
client: CoderApi,
303311
baseUrl: string,
304312
output: Logger,
313+
oauthSessionManager?: OAuthSessionManager,
305314
): void {
306315
addLoggingInterceptors(client.getAxiosInstance(), output);
307316

@@ -334,6 +343,11 @@ function setupInterceptors(
334343
throw await CertificateError.maybeWrap(err, baseUrl, output);
335344
},
336345
);
346+
347+
// OAuth token refresh interceptors
348+
if (oauthSessionManager) {
349+
addOAuthInterceptors(client, output, oauthSessionManager);
350+
}
337351
}
338352

339353
function addLoggingInterceptors(client: AxiosInstance, logger: Logger) {
@@ -363,7 +377,7 @@ function addLoggingInterceptors(client: AxiosInstance, logger: Logger) {
363377
},
364378
(error: unknown) => {
365379
logError(logger, error, getLogLevel());
366-
return Promise.reject(error);
380+
throw error;
367381
},
368382
);
369383

@@ -374,7 +388,80 @@ function addLoggingInterceptors(client: AxiosInstance, logger: Logger) {
374388
},
375389
(error: unknown) => {
376390
logError(logger, error, getLogLevel());
377-
return Promise.reject(error);
391+
throw error;
392+
},
393+
);
394+
}
395+
396+
/**
397+
* Add OAuth token refresh interceptors.
398+
* Success interceptor: proactively refreshes token when approaching expiry.
399+
* Error interceptor: reactively refreshes token on 401/403 responses.
400+
*/
401+
function addOAuthInterceptors(
402+
client: CoderApi,
403+
logger: Logger,
404+
oauthSessionManager: OAuthSessionManager,
405+
) {
406+
client.getAxiosInstance().interceptors.response.use(
407+
// Success response interceptor: proactive token refresh
408+
(response) => {
409+
if (oauthSessionManager.shouldRefreshToken()) {
410+
logger.debug(
411+
"Token approaching expiry, triggering proactive refresh in background",
412+
);
413+
414+
// Fire-and-forget: don't await, don't block response
415+
oauthSessionManager.refreshToken().catch((error) => {
416+
logger.warn("Background token refresh failed:", error);
417+
});
418+
}
419+
420+
return response;
421+
},
422+
// Error response interceptor: reactive token refresh on 401/403
423+
async (error: unknown) => {
424+
if (!isAxiosError(error)) {
425+
throw error;
426+
}
427+
428+
const status = error.response?.status;
429+
if (status !== 401 && status !== 403) {
430+
throw error;
431+
}
432+
433+
if (!oauthSessionManager.isLoggedInWithOAuth()) {
434+
throw error;
435+
}
436+
437+
logger.info(`Received ${status} response, attempting token refresh`);
438+
439+
try {
440+
const newTokens = await oauthSessionManager.refreshToken();
441+
client.setSessionToken(newTokens.access_token);
442+
443+
logger.info("Token refresh successful, updated session token");
444+
} catch (refreshError) {
445+
logger.error("Token refresh failed:", refreshError);
446+
447+
const oauthError = parseOAuthError(refreshError);
448+
if (oauthError && requiresReAuthentication(oauthError)) {
449+
logger.error(
450+
`OAuth error requires re-authentication: ${oauthError.errorCode}`,
451+
);
452+
453+
oauthSessionManager
454+
.showReAuthenticationModal(oauthError)
455+
.catch((err) => {
456+
logger.error("Failed to show re-auth modal:", err);
457+
});
458+
} else if (isNetworkError(refreshError)) {
459+
logger.warn(
460+
"Token refresh failed due to network error, will retry later",
461+
);
462+
}
463+
}
464+
throw error;
378465
},
379466
);
380467
}

src/commands.ts

Lines changed: 4 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -482,20 +482,10 @@ export class Commands {
482482
}
483483
this.logger.info("Logging out");
484484

485-
// Check if using OAuth
486-
const isOAuthLoggedIn =
487-
await this.oauthSessionManager.isLoggedInWithOAuth();
488-
if (isOAuthLoggedIn) {
489-
this.logger.info("Logging out via OAuth");
490-
try {
491-
await this.oauthSessionManager.logout();
492-
} catch (error) {
493-
this.logger.warn(
494-
"OAuth logout failed, continuing with cleanup:",
495-
error,
496-
);
497-
}
498-
}
485+
// Fire and forget
486+
this.oauthSessionManager.logout().catch((error) => {
487+
this.logger.warn("OAuth logout failed, continuing with cleanup:", error);
488+
});
499489

500490
// Clear from the REST client. An empty url will indicate to other parts of
501491
// the code that we are logged out.
@@ -667,19 +657,6 @@ export class Commands {
667657
},
668658
);
669659
}
670-
// Check if app has a URL to open
671-
if (app.url) {
672-
return vscode.window.withProgress(
673-
{
674-
location: vscode.ProgressLocation.Notification,
675-
title: `Opening ${app.name || "application"} in browser...`,
676-
cancellable: false,
677-
},
678-
async () => {
679-
await vscode.env.openExternal(vscode.Uri.parse(app.url!));
680-
},
681-
);
682-
}
683660

684661
// If no URL or command, show information about the app status
685662
vscode.window.showInformationMessage(`${app.name}`, {

src/extension.ts

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -70,14 +70,24 @@ export async function activate(ctx: vscode.ExtensionContext): Promise<void> {
7070
// Try to clear this flag ASAP
7171
const isFirstConnect = await mementoManager.getAndClearFirstConnect();
7272

73+
const url = mementoManager.getUrl();
74+
75+
// Create OAuth session manager before the main client
76+
const oauthSessionManager = await OAuthSessionManager.create(
77+
url || "",
78+
serviceContainer,
79+
ctx,
80+
);
81+
ctx.subscriptions.push(oauthSessionManager);
82+
7383
// This client tracks the current login and will be used through the life of
7484
// the plugin to poll workspaces for the current login, as well as being used
7585
// in commands that operate on the current login.
76-
const url = mementoManager.getUrl();
7786
const client = CoderApi.create(
7887
url || "",
7988
await secretsManager.getSessionToken(),
8089
output,
90+
oauthSessionManager,
8191
);
8292

8393
const myWorkspacesProvider = new WorkspaceProvider(
@@ -123,14 +133,6 @@ export async function activate(ctx: vscode.ExtensionContext): Promise<void> {
123133
ctx.subscriptions,
124134
);
125135

126-
const oauthSessionManager = await OAuthSessionManager.create(
127-
url || "",
128-
secretsManager,
129-
output,
130-
ctx,
131-
);
132-
ctx.subscriptions.push(oauthSessionManager);
133-
134136
// Listen for session token changes and sync state across all components
135137
ctx.subscriptions.push(
136138
secretsManager.onDidChangeSessionToken(async (token) => {
@@ -409,6 +411,7 @@ export async function activate(ctx: vscode.ExtensionContext): Promise<void> {
409411
isFirstConnect,
410412
);
411413
if (details) {
414+
// TODO if the URL is different then we need to update the OAuth session!!! (Centralize this logic)
412415
ctx.subscriptions.push(details);
413416
// Authenticate the plugin client which is used in the sidebar to display
414417
// workspaces belonging to this deployment.

0 commit comments

Comments
 (0)