@@ -8,6 +8,13 @@ export const API_BASE_URL = RAW_URL.endsWith('/')
88 : RAW_URL ;
99
1010export const BACKEND_URL = new URL ( API_BASE_URL ) . origin ;
11+ const WRITE_METHODS = new Set ( [ 'post' , 'put' , 'delete' , 'patch' ] ) ;
12+ const CSRF_COOKIE_NAME = 'XSRF-TOKEN' ;
13+
14+ type RetriableAxiosConfig = InternalAxiosRequestConfig & {
15+ _csrfRetryAttempted ?: boolean ;
16+ _csrfRefreshAttempted ?: boolean ;
17+ } ;
1118
1219export const getCookie = ( name : string ) : string | null => {
1320 if ( typeof document === 'undefined' ) return null ;
@@ -23,6 +30,32 @@ export const getCookie = (name: string): string | null => {
2330 return null ;
2431} ;
2532
33+ let csrfRefreshPromise : Promise < void > | null = null ;
34+
35+ const shouldAttachCsrfToken = ( method ?: string ) => WRITE_METHODS . has ( method ?. toLowerCase ( ) || '' ) ;
36+
37+ const setCsrfHeader = ( config : InternalAxiosRequestConfig , token : string ) => {
38+ if ( typeof config . headers ?. set === 'function' ) {
39+ config . headers . set ( 'X-XSRF-TOKEN' , token ) ;
40+ } else {
41+ ( config . headers as any ) [ 'X-XSRF-TOKEN' ] = token ;
42+ }
43+ } ;
44+
45+ const refreshCsrfToken = async ( ) => {
46+ if ( typeof window === 'undefined' ) return ;
47+
48+ if ( ! csrfRefreshPromise ) {
49+ csrfRefreshPromise = api . get ( `/status?t=${ Date . now ( ) } ` , {
50+ headers : { 'Cache-Control' : 'no-cache' }
51+ } ) . then ( ( ) => undefined ) . finally ( ( ) => {
52+ csrfRefreshPromise = null ;
53+ } ) ;
54+ }
55+
56+ await csrfRefreshPromise ;
57+ } ;
58+
2659export const api = axios . create ( {
2760 baseURL : API_BASE_URL ,
2861 withCredentials : true ,
@@ -33,19 +66,22 @@ export const api = axios.create({
3366} ) ;
3467
3568api . interceptors . request . use (
36- ( config : InternalAxiosRequestConfig ) => {
69+ async ( config : InternalAxiosRequestConfig ) => {
3770 if ( ! config . headers ) {
3871 config . headers = { } as any ;
3972 }
4073
41- if ( [ 'post' , 'put' , 'delete' , 'patch' ] . includes ( config . method ?. toLowerCase ( ) || '' ) ) {
42- const token = getCookie ( 'XSRF-TOKEN' ) ;
74+ const retryableConfig = config as RetriableAxiosConfig ;
75+ if ( shouldAttachCsrfToken ( config . method ) ) {
76+ let token = getCookie ( CSRF_COOKIE_NAME ) ;
77+ if ( ! token && ! retryableConfig . _csrfRefreshAttempted ) {
78+ retryableConfig . _csrfRefreshAttempted = true ;
79+ await refreshCsrfToken ( ) ;
80+ token = getCookie ( CSRF_COOKIE_NAME ) ;
81+ }
82+
4383 if ( token ) {
44- if ( typeof config . headers . set === 'function' ) {
45- config . headers . set ( 'X-XSRF-TOKEN' , token ) ;
46- } else {
47- ( config . headers as any ) [ 'X-XSRF-TOKEN' ] = token ;
48- }
84+ setCsrfHeader ( config , token ) ;
4985 }
5086 }
5187
@@ -54,6 +90,32 @@ api.interceptors.request.use(
5490 error => Promise . reject ( error )
5591) ;
5692
93+ api . interceptors . response . use (
94+ response => response ,
95+ async ( error ) => {
96+ const config = error . config as RetriableAxiosConfig | undefined ;
97+ if ( ! config || ! shouldAttachCsrfToken ( config . method ) || config . _csrfRetryAttempted || error . response ?. status !== 403 ) {
98+ return Promise . reject ( error ) ;
99+ }
100+
101+ config . _csrfRetryAttempted = true ;
102+
103+ try {
104+ await refreshCsrfToken ( ) ;
105+ const token = getCookie ( CSRF_COOKIE_NAME ) ;
106+ if ( token ) {
107+ if ( ! config . headers ) {
108+ config . headers = { } as any ;
109+ }
110+ setCsrfHeader ( config , token ) ;
111+ }
112+ return await api . request ( config ) ;
113+ } catch {
114+ return Promise . reject ( error ) ;
115+ }
116+ }
117+ ) ;
118+
57119export const extractApiErrorMessage = ( error : unknown , fallback : string ) : string => {
58120 if ( typeof error === 'string' && error . trim ( ) ) {
59121 return error ;
0 commit comments