@@ -15,7 +15,7 @@ import type {
1515 SessionResponse ,
1616} from './types' ;
1717import type { Protocol } from './utils' ;
18- import { localNetworkFetchOptions , resolveBaseUrl } from './utils' ;
18+ import { fetchWithTimeout , resolveBaseUrl } from './utils' ;
1919
2020export class EcaRemoteApi {
2121 private baseUrl : string ;
@@ -37,12 +37,17 @@ export class EcaRemoteApi {
3737 return h ;
3838 }
3939
40+ /** Default timeout for REST requests (ms). */
41+ private static readonly REQUEST_TIMEOUT_MS = 15_000 ;
42+
4043 /**
4144 * Generic fetch-check-parse helper.
4245 * - Adds auth headers automatically.
4346 * - Throws an `Error` when the response status is not OK,
4447 * unless `allowStatus` includes that specific code.
4548 * - Returns `undefined` for 204 No Content or void endpoints.
49+ * - Enforces a per-request timeout via AbortController to prevent
50+ * hung connections during page refresh or network instability.
4651 */
4752 private async request < T = void > (
4853 path : string ,
@@ -52,24 +57,33 @@ export class EcaRemoteApi {
5257 auth ?: boolean ;
5358 /** HTTP status codes that should NOT throw (e.g. 409 for idempotent ops). */
5459 allowStatus ?: number [ ] ;
60+ /** Override the default request timeout (ms). */
61+ timeoutMs ?: number ;
5562 } = { } ,
5663 ) : Promise < T > {
57- const { method = 'GET' , body, auth = true , allowStatus = [ ] } = options ;
64+ const { method = 'GET' , body, auth = true , allowStatus = [ ] , timeoutMs } = options ;
5865 const hasBody = body !== undefined ;
5966
6067 const url = `${ this . baseUrl } ${ path } ` ;
61- const res = await fetch ( url , {
62- ...localNetworkFetchOptions ( url ) ,
63- method,
64- headers : auth ? this . headers ( hasBody ) : ( hasBody ? { 'Content-Type' : 'application/json' } : undefined ) ,
65- ...( hasBody ? { body : JSON . stringify ( body ) } : { } ) ,
66- } ) ;
68+ const res = await fetchWithTimeout (
69+ url ,
70+ {
71+ method,
72+ headers : auth ? this . headers ( hasBody ) : ( hasBody ? { 'Content-Type' : 'application/json' } : undefined ) ,
73+ ...( hasBody ? { body : JSON . stringify ( body ) } : { } ) ,
74+ } ,
75+ timeoutMs ?? EcaRemoteApi . REQUEST_TIMEOUT_MS ,
76+ ) ;
6777
6878 if ( ! res . ok && ! allowStatus . includes ( res . status ) ) {
6979 // Try to extract a structured error message from the body
7080 const errBody = await res . json ( ) . catch ( ( ) => null ) ;
7181 const message = errBody ?. error ?. message || `${ method } ${ path } failed: ${ res . status } ` ;
72- throw new Error ( message ) ;
82+ const error = new Error ( message ) ;
83+ // Attach the HTTP status code so callers can distinguish 404 from
84+ // transient errors (500, timeout) without fragile string matching.
85+ ( error as any ) . status = res . status ;
86+ throw error ;
7387 }
7488
7589 // Return parsed JSON for responses that have a body
0 commit comments