Where
src/oauth/xai.ts line 97 and the catch branch on line 114, on dev:
function isAbortError(error:unknown):boolean{return error instanceof DOMException&&error.name==="AbortError";}
...
}catch(error){if(isAbortError(error)&&signal?.aborted)throw error;last=error; ... await sleep(...);continue;}
Problem
The early-exit guard depends on the rejection being a DOMException named AbortError. That holds for a bare controller.abort(), but when a caller aborts with a reason, fetch (undici, and Bun follows the same spec) rejects with that reason object as-is:
controller.abort(new Error("user cancel"))
-> rejection: ctor Error, name "Error", instanceof DOMException false
Measured on Node 24.17. In that case isAbortError returns false, the guard is skipped, and the loop sleeps and retries up to two more times on a signal that is already aborted. AbortSignal.timeout() expiry (name === "TimeoutError") is likewise not treated as terminal, so a 30s token timeout can stretch to ~90s across three attempts.
Fix
Check the signal, not the error's class:
} catch (error) {
if (signal?.aborted) throw error; // any reason
if ((error as { name?: string })?.name === "TimeoutError") throw error;
...
}
Repro
Pass signal from a controller, call controller.abort(new Error("x")) before the first fetch resolves, inject deps.sleep that records calls: sleep is invoked twice instead of zero times.
Found while porting this module into ima2-gen (devlog 260909_grok_native_oauth/001_opencodex_port_spec.md D3).
Where
src/oauth/xai.tsline 97 and the catch branch on line 114, ondev:Problem
The early-exit guard depends on the rejection being a
DOMExceptionnamedAbortError. That holds for a barecontroller.abort(), but when a caller aborts with a reason,fetch(undici, and Bun follows the same spec) rejects with that reason object as-is:Measured on Node 24.17. In that case
isAbortErrorreturns false, the guard is skipped, and the loop sleeps and retries up to two more times on a signal that is already aborted.AbortSignal.timeout()expiry (name === "TimeoutError") is likewise not treated as terminal, so a 30s token timeout can stretch to ~90s across three attempts.Fix
Check the signal, not the error's class:
Repro
Pass
signalfrom a controller, callcontroller.abort(new Error("x"))before the first fetch resolves, injectdeps.sleepthat records calls: sleep is invoked twice instead of zero times.Found while porting this module into ima2-gen (devlog
260909_grok_native_oauth/001_opencodex_port_spec.mdD3).