Skip to content

Commit 9f04022

Browse files
committed
Add error handling
1 parent 5555ac8 commit 9f04022

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 {
@@ -31,6 +32,12 @@ import {
3132
HttpClientLogLevel,
3233
} from "../logging/types";
3334
import { sizeOf } from "../logging/utils";
35+
import {
36+
parseOAuthError,
37+
requiresReAuthentication,
38+
isNetworkError,
39+
} from "../oauth/errors";
40+
import { type OAuthSessionManager } from "../oauth/sessionManager";
3441
import { type UnidirectionalStream } from "../websocket/eventStreamConnection";
3542
import {
3643
OneWayWebSocket,
@@ -59,14 +66,15 @@ export class CoderApi extends Api {
5966
baseUrl: string,
6067
token: string | undefined,
6168
output: Logger,
69+
oauthSessionManager?: OAuthSessionManager,
6270
): CoderApi {
6371
const client = new CoderApi(output);
6472
client.setHost(baseUrl);
6573
if (token) {
6674
client.setSessionToken(token);
6775
}
6876

69-
setupInterceptors(client, baseUrl, output);
77+
setupInterceptors(client, baseUrl, output, oauthSessionManager);
7078
return client;
7179
}
7280

@@ -327,6 +335,7 @@ function setupInterceptors(
327335
client: CoderApi,
328336
baseUrl: string,
329337
output: Logger,
338+
oauthSessionManager?: OAuthSessionManager,
330339
): void {
331340
addLoggingInterceptors(client.getAxiosInstance(), output);
332341

@@ -359,6 +368,11 @@ function setupInterceptors(
359368
throw await CertificateError.maybeWrap(err, baseUrl, output);
360369
},
361370
);
371+
372+
// OAuth token refresh interceptors
373+
if (oauthSessionManager) {
374+
addOAuthInterceptors(client, output, oauthSessionManager);
375+
}
362376
}
363377

364378
function addLoggingInterceptors(client: AxiosInstance, logger: Logger) {
@@ -388,7 +402,7 @@ function addLoggingInterceptors(client: AxiosInstance, logger: Logger) {
388402
},
389403
(error: unknown) => {
390404
logError(logger, error, getLogLevel());
391-
return Promise.reject(error);
405+
throw error;
392406
},
393407
);
394408

@@ -399,7 +413,80 @@ function addLoggingInterceptors(client: AxiosInstance, logger: Logger) {
399413
},
400414
(error: unknown) => {
401415
logError(logger, error, getLogLevel());
402-
return Promise.reject(error);
416+
throw error;
417+
},
418+
);
419+
}
420+
421+
/**
422+
* Add OAuth token refresh interceptors.
423+
* Success interceptor: proactively refreshes token when approaching expiry.
424+
* Error interceptor: reactively refreshes token on 401/403 responses.
425+
*/
426+
function addOAuthInterceptors(
427+
client: CoderApi,
428+
logger: Logger,
429+
oauthSessionManager: OAuthSessionManager,
430+
) {
431+
client.getAxiosInstance().interceptors.response.use(
432+
// Success response interceptor: proactive token refresh
433+
(response) => {
434+
if (oauthSessionManager.shouldRefreshToken()) {
435+
logger.debug(
436+
"Token approaching expiry, triggering proactive refresh in background",
437+
);
438+
439+
// Fire-and-forget: don't await, don't block response
440+
oauthSessionManager.refreshToken().catch((error) => {
441+
logger.warn("Background token refresh failed:", error);
442+
});
443+
}
444+
445+
return response;
446+
},
447+
// Error response interceptor: reactive token refresh on 401/403
448+
async (error: unknown) => {
449+
if (!isAxiosError(error)) {
450+
throw error;
451+
}
452+
453+
const status = error.response?.status;
454+
if (status !== 401 && status !== 403) {
455+
throw error;
456+
}
457+
458+
if (!oauthSessionManager.isLoggedInWithOAuth()) {
459+
throw error;
460+
}
461+
462+
logger.info(`Received ${status} response, attempting token refresh`);
463+
464+
try {
465+
const newTokens = await oauthSessionManager.refreshToken();
466+
client.setSessionToken(newTokens.access_token);
467+
468+
logger.info("Token refresh successful, updated session token");
469+
} catch (refreshError) {
470+
logger.error("Token refresh failed:", refreshError);
471+
472+
const oauthError = parseOAuthError(refreshError);
473+
if (oauthError && requiresReAuthentication(oauthError)) {
474+
logger.error(
475+
`OAuth error requires re-authentication: ${oauthError.errorCode}`,
476+
);
477+
478+
oauthSessionManager
479+
.showReAuthenticationModal(oauthError)
480+
.catch((err) => {
481+
logger.error("Failed to show re-auth modal:", err);
482+
});
483+
} else if (isNetworkError(refreshError)) {
484+
logger.warn(
485+
"Token refresh failed due to network error, will retry later",
486+
);
487+
}
488+
}
489+
throw error;
403490
},
404491
);
405492
}

src/commands.ts

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

363-
// Check if using OAuth
364-
const isOAuthLoggedIn =
365-
await this.oauthSessionManager.isLoggedInWithOAuth();
366-
if (isOAuthLoggedIn) {
367-
this.logger.info("Logging out via OAuth");
368-
try {
369-
await this.oauthSessionManager.logout();
370-
} catch (error) {
371-
this.logger.warn(
372-
"OAuth logout failed, continuing with cleanup:",
373-
error,
374-
);
375-
}
376-
}
363+
// Fire and forget
364+
this.oauthSessionManager.logout().catch((error) => {
365+
this.logger.warn("OAuth logout failed, continuing with cleanup:", error);
366+
});
377367

378368
// Clear from the REST client. An empty url will indicate to other parts of
379369
// the code that we are logged out.
@@ -545,19 +535,6 @@ export class Commands {
545535
},
546536
);
547537
}
548-
// Check if app has a URL to open
549-
if (app.url) {
550-
return vscode.window.withProgress(
551-
{
552-
location: vscode.ProgressLocation.Notification,
553-
title: `Opening ${app.name || "application"} in browser...`,
554-
cancellable: false,
555-
},
556-
async () => {
557-
await vscode.env.openExternal(vscode.Uri.parse(app.url!));
558-
},
559-
);
560-
}
561538

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

src/extension.ts

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

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

8494
const myWorkspacesProvider = new WorkspaceProvider(
@@ -124,14 +134,6 @@ export async function activate(ctx: vscode.ExtensionContext): Promise<void> {
124134
ctx.subscriptions,
125135
);
126136

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

0 commit comments

Comments
 (0)