← back to Kickbacks Ai Upstream

src/util/http.ts

33 lines

// Shared fetch wrapper that applies a request timeout (audit 2A-01 / EXT-04 /
// 2J-09). None of the extension's HTTP clients passed an AbortSignal, so a
// black-holed backend connection (no FIN, slow drip) would hang forever:
// activation awaits portfolio + killswitch serially, the auth refresh
// single-flight parks every later caller behind the one stuck promise, and
// stuck keep-alive sockets accumulate against the 30s/60s/90s timers.
//
// Each client takes an injectable `f: Fetch` (tests pass a mock); only the
// DEFAULT is wrapped, so production gets a timeout while test injection is
// unchanged. A caller that supplies its own `signal` keeps it.

type Fetch = typeof fetch;

export function timeoutFetch(ms: number): Fetch {
  return ((input: any, init?: any) => {
    const existing = init && init.signal;
    let signal = existing;
    if (!signal) {
      try {
        // Node 18+/modern Electron expose AbortSignal.timeout; guard so an
        // older host degrades to the prior (no-timeout) behaviour rather than
        // throwing during activation.
        signal = (AbortSignal as any).timeout
          ? (AbortSignal as any).timeout(ms)
          : undefined;
      } catch {
        signal = undefined;
      }
    }
    return fetch(input, { ...(init || {}), signal });
  }) as Fetch;
}