33 type AxiosInstance ,
44 type AxiosHeaders ,
55 type AxiosResponseTransformer ,
6+ isAxiosError ,
67} from "axios" ;
78import { Api } from "coder/site/src/api/api" ;
89import {
@@ -31,6 +32,12 @@ import {
3132 HttpClientLogLevel ,
3233} from "../logging/types" ;
3334import { sizeOf } from "../logging/utils" ;
35+ import {
36+ parseOAuthError ,
37+ requiresReAuthentication ,
38+ isNetworkError ,
39+ } from "../oauth/errors" ;
40+ import { type OAuthSessionManager } from "../oauth/sessionManager" ;
3441import { type UnidirectionalStream } from "../websocket/eventStreamConnection" ;
3542import {
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
364378function 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}
0 commit comments