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