@@ -4,7 +4,7 @@ import { useRateLimitStore } from '../store/rateLimitStore';
44import { getCsrfTokenFromCookie , CSRF_HEADER_NAME } from '../utils/csrf' ;
55import { toast } from 'sonner' ;
66import { announce } from '@/lib/utils/announce' ;
7- import { parseApiError } from '../utils/apiError' ;
7+ import { parseApiError , isTimeoutError } from '../utils/apiError' ;
88import { getAppRouter } from '../navigation/appRouter' ;
99
1010// Deduplication: avoid showing multiple toasts for simultaneous errors
@@ -64,6 +64,14 @@ function notifyError(message: string, key?: string) {
6464// Use cookie-based auth (HttpOnly cookie set by the server). Do not read tokens from localStorage.
6565const apiBaseURL = process . env . NEXT_PUBLIC_API_URL || 'http://localhost:3001' ;
6666
67+ // Default timeout for all requests unless a longer one is needed (e.g. payment submission).
68+ export const DEFAULT_TIMEOUT_MS = 15000 ;
69+ // Payment submission endpoints (charge creation, settlement processing) can take longer
70+ // than standard reads because they may wait for provider confirmation.
71+ export const PAYMENT_TIMEOUT_MS = 30000 ;
72+ // URLs matching these paths get the extended payment timeout.
73+ const PAYMENT_TIMEOUT_PATHS : ReadonlyArray < string > = [ '/payments' , '/settlements' ] ;
74+
6775if ( ! process . env . NEXT_PUBLIC_API_URL && typeof window !== 'undefined' ) {
6876 console . warn (
6977 '[API Client] NEXT_PUBLIC_API_URL is not set. Defaulting to http://localhost:3001. ' +
@@ -73,7 +81,7 @@ if (!process.env.NEXT_PUBLIC_API_URL && typeof window !== 'undefined') {
7381
7482export const apiClient = axios . create ( {
7583 baseURL : apiBaseURL ,
76- timeout : 15000 , // 15 seconds for normal requests
84+ timeout : DEFAULT_TIMEOUT_MS ,
7785 headers : {
7886 'Content-Type' : 'application/json' ,
7987 } ,
@@ -92,6 +100,15 @@ const refreshClient = axios.create({
92100// Attach CSRF token to all state-changing requests (double-submit cookie pattern)
93101const STATE_METHODS = [ 'post' , 'put' , 'patch' , 'delete' ] as const ;
94102
103+ // Resolve the timeout for a request. Payment submission endpoints get the extended
104+ // timeout; everything else uses the default. Note that Axios merges the instance
105+ // default into every request config, so the URL match is the authoritative signal
106+ // here and cannot be distinguished from a caller-provided value at this point.
107+ function resolveRequestTimeout ( url : string | undefined ) : number {
108+ const safeUrl = url || '' ;
109+ return PAYMENT_TIMEOUT_PATHS . some ( ( path ) => safeUrl . includes ( path ) ) ? PAYMENT_TIMEOUT_MS : DEFAULT_TIMEOUT_MS ;
110+ }
111+
95112apiClient . interceptors . request . use ( ( config ) => {
96113 const method = ( config . method || '' ) . toLowerCase ( ) ;
97114 if ( STATE_METHODS . includes ( method as ( typeof STATE_METHODS ) [ number ] ) ) {
@@ -101,13 +118,7 @@ apiClient.interceptors.request.use((config) => {
101118 }
102119 }
103120
104- // Use 30 second timeout for payment submission endpoints, 15 seconds for others
105- const url = config . url || '' ;
106- if ( url . includes ( '/payments' ) || url . includes ( '/settlements' ) ) {
107- config . timeout = 30000 ;
108- } else {
109- config . timeout = 15000 ;
110- }
121+ config . timeout = resolveRequestTimeout ( config . url ) ;
111122
112123 return config ;
113124} ) ;
@@ -196,6 +207,11 @@ apiClient.interceptors.response.use(
196207 const limit = rawLimit ? parseInt ( String ( rawLimit ) , 10 ) : undefined ;
197208 useRateLimitStore . getState ( ) . setRateLimited ( seconds , endpoint , limit ) ;
198209 notifyError ( `Too many attempts. Please try again in ${ seconds } seconds.` , `429_${ endpoint } ` ) ;
210+ } else if ( isTimeoutError ( error ) ) {
211+ // Axios aborts timed-out requests and surfaces an ECONNABORTED error.
212+ // Avoid the generic network message so the user knows the request was
213+ // given ample time and can retry.
214+ notifyError ( 'The request timed out. Please try again.' , `timeout_${ originalRequest ?. url || 'unknown' } ` ) ;
199215 } else if ( ! error . response ) {
200216 notifyError ( 'Network error. Please check your connection.' , 'network_error' ) ;
201217 } else if ( error . response ?. status >= 500 ) {
0 commit comments