@@ -395,9 +395,15 @@ export class HttpClient {
395395 timeoutSignal : AbortSignal ,
396396 callerSignal : AbortSignal | undefined ,
397397 requestId : string ,
398+ effectiveSignal : AbortSignal = timeoutSignal ,
398399 ) : void {
399400 if ( isAbortError ( err ) || isTimeoutError ( err ) ) {
400- if ( timeoutSignal . aborted && ( callerSignal == null || ! callerSignal . aborted ) ) {
401+ const timeoutWon =
402+ timeoutSignal . aborted &&
403+ ( callerSignal == null ||
404+ ! callerSignal . aborted ||
405+ effectiveSignal . reason === timeoutSignal . reason ) ;
406+ if ( timeoutWon ) {
401407 throw new RequestTimeoutError ( this . requestTimeoutMs , requestId ) ;
402408 }
403409 throw err ;
@@ -451,7 +457,7 @@ export class HttpClient {
451457 // the caller hadn't already aborted, surface a clear RequestTimeoutError.
452458 // A timeout/abort during the fetch itself: classify it (RequestTimeoutError
453459 // when our deadline fired; otherwise rethrow the caller's abort unmodified).
454- this . rethrowIfAbort ( err , timeoutSignal , options . signal , requestId ) ;
460+ this . rethrowIfAbort ( err , timeoutSignal , options . signal , requestId , effectiveSignal ) ;
455461 // If a RequestTimeoutError already propagated from somewhere (e.g. from a
456462 // nested call or from a test-injected fetchImpl), pass it through unchanged
457463 // rather than re-wrapping it as a TransportError.
@@ -500,8 +506,14 @@ export class HttpClient {
500506 return { body : ( await response . json ( ) ) as T , requestId, status : response . status } ;
501507 } catch ( err ) {
502508 // A timeout/abort can fire mid-body-read (headers received, stream stalls).
503- this . rethrowIfAbort ( err , timeoutSignal , options . signal , requestId ) ;
504- throw err ;
509+ this . rethrowIfAbort ( err , timeoutSignal , options . signal , requestId , effectiveSignal ) ;
510+ // Otherwise the successful response body was not valid JSON — a
511+ // misconfigured endpoint, a proxy / captive-portal / login page that
512+ // returns HTML with a 200 status, or an empty body. Surface a typed
513+ // error carrying the requestId instead of letting the raw SyntaxError
514+ // escape to index.ts, where it would print a bare `{"error":"..."}`
515+ // and break the --output json envelope contract.
516+ throw malformedResponseError ( response , requestId , err ) ;
505517 }
506518 }
507519
@@ -511,14 +523,8 @@ export class HttpClient {
511523 } catch ( err ) {
512524 // safeReadJson rethrows aborts/timeouts (it swallows only non-abort parse
513525 // errors), so a timeout fired mid-body-read on a non-OK response lands here.
514- this . rethrowIfAbort ( err , timeoutSignal , options . signal , requestId ) ;
515- // Otherwise the successful response body was not valid JSON — a
516- // misconfigured endpoint, a proxy / captive-portal / login page that
517- // returns HTML with a 200 status, or an empty body. Surface a typed
518- // error carrying the requestId instead of letting the raw SyntaxError
519- // escape to index.ts, where it would print a bare `{"error":"..."}`
520- // and break the --output json envelope contract.
521- throw malformedResponseError ( response , requestId , err ) ;
526+ this . rethrowIfAbort ( err , timeoutSignal , options . signal , requestId , effectiveSignal ) ;
527+ throw err ;
522528 }
523529
524530 // Edge proxies / load balancers return 408/502/504 without our error
0 commit comments