From 2dbdd340ec32a56ca521e9433a904e0efaa8c029 Mon Sep 17 00:00:00 2001 From: Yannick Utard Date: Thu, 22 Oct 2026 15:53:11 +0200 Subject: [PATCH] fix(http): stop the runtime's fetch idle timeout from killing long queries Bun's fetch aborts a response that stays silent for 300s (360s in practice on Bun 1.4.3), independently of the request's AbortSignal. A long-running /query sends its headers and then nothing until the statement finishes, so any query over ~6 minutes failed with "Request failed (network error): POST .../query (The operation timed out.)", whatever --connect-timeout and --read-timeout said, while the statement kept running server-side. Pass timeout: false to Bun's fetch and disable undici's headersTimeout and bodyTimeout, so the CLI's own connect and read timeouts are the only limits and --read-timeout 0 is unlimited for streams, as documented. Co-authored-by: Cursor --- CHANGELOG.md | 2 ++ cli/src/lib/http.test.ts | 24 ++++++++++++++++++++++++ cli/src/lib/http.ts | 15 +++++++++++++-- 3 files changed, 39 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 86c7b3b..c681b62 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +- Stop queries that run longer than 6 minutes from failing with `Request failed (network error): … (The operation timed out.)`. `--read-timeout` is now the only limit on query streams, and `0` means no limit. + - Keep the TypeScript manifest compiler out of CLI downloads by installing it with the cached single-file preview runtime. - Complete stdin-based credential configuration before exiting on Windows. diff --git a/cli/src/lib/http.test.ts b/cli/src/lib/http.test.ts index 34601e7..7f4a092 100644 --- a/cli/src/lib/http.test.ts +++ b/cli/src/lib/http.test.ts @@ -362,6 +362,30 @@ describe('shared dispatcher', () => { }); }); +describe('native fetch timeout', () => { + test.each([false, true])( + 'is disabled so long-running streams outlive 300s of silence (ignoreSslErrors=%p)', + ignoreSslErrors => { + setCliContext({ + debug: false, + json: false, + agent: false, + ignoreSslErrors, + }); + const init = buildFetchInit( + { + method: 'POST', + url: 'https://api.altertable.test/query', + authHeader: 'Authorization: Basic test', + }, + {}, + AbortSignal.timeout(1_000) + ) as { timeout?: unknown }; + expect(init.timeout).toBe(false); + } + ); +}); + describe('ignore ssl errors', () => { const request = { method: 'GET', diff --git a/cli/src/lib/http.ts b/cli/src/lib/http.ts index 35549d2..3d22c7f 100644 --- a/cli/src/lib/http.ts +++ b/cli/src/lib/http.ts @@ -59,6 +59,8 @@ type MockHttpEntry = { type UndiciAgentOptions = { keepAliveTimeout: number; keepAliveMaxTimeout: number; + headersTimeout: number; + bodyTimeout: number; connect?: { rejectUnauthorized: boolean }; }; @@ -86,6 +88,8 @@ export function getSharedDispatcher(): unknown { sharedDispatcher = createDispatcher({ keepAliveTimeout: 30_000, keepAliveMaxTimeout: 60_000, + headersTimeout: 0, + bodyTimeout: 0, }); return sharedDispatcher; @@ -104,6 +108,8 @@ function getInsecureDispatcher(): unknown { insecureDispatcher = createDispatcher({ keepAliveTimeout: 30_000, keepAliveMaxTimeout: 60_000, + headersTimeout: 0, + bodyTimeout: 0, connect: { rejectUnauthorized: false }, }); @@ -130,8 +136,12 @@ function warnInsecureTlsOnce(): void { } /** - * Bun's fetch reads `tls`; Node/undici reads `dispatcher`. Set both so the - * escape hatch works under either runtime. + * Bun's fetch reads `tls` and `timeout`; Node/undici reads `dispatcher`. Set + * both so the escape hatch works under either runtime. + * + * Both runtimes otherwise abort a response that stays silent for 300s, which + * kills long-running queries whatever `--read-timeout` says. The connect and + * read timeouts are enforced through `signal` instead. */ export function buildFetchInit( options: HttpSendOptions, @@ -148,6 +158,7 @@ export function buildFetchInit( headers, body: options.body, signal, + timeout: false, dispatcher: ignoreSslErrors ? getInsecureDispatcher() : getSharedDispatcher(),