SMOODEV-2513: Add connect timeout to TS fetch SDK (default-off) - #92
SMOODEV-2513: Add connect timeout to TS fetch SDK (default-off)#92brentrager wants to merge 1 commit into
Conversation
…i dispatcher) Bounds only the connection-establishment phase so a black-holed connect (a SYN to a dead pod IP still lingering in a ClusterIP's iptables) fails fast and retry can land on a live endpoint, instead of stalling until the whole-request timeout. Node only, via a lazily-loaded undici Agent dispatcher; ignored in browser/worker builds. Default-off: unset preserves the previous behavior exactly. Parity with Rust with_connect_timeout (#88). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
🦋 Changeset detectedLatest commit: 22ba327 The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
Consolidates PRs #88 (Rust), #89 (Go), #90 (Python), #91 (.NET) and #92 (TypeScript), open since 2026-07-10, onto current main. The work was sound; it had gone stale against traceparent injection and the redaction fix, both of which touch the same single-request site. The gap is real: without a connect timeout, a SYN to a dead pod IP still lingering in a ClusterIP's iptables burns the WHOLE-request budget before retry can try a live endpoint. Measured locally, an unbounded connect to a black hole fails after 10.5s (undici's own default); bounded at 500ms it fails in ~1s. Two changes from the original PRs: - undici is an OPTIONAL PEER dependency, not a runtime dependency. #92 added it to `dependencies`, which would have put a full HTTP stack in the tree of every consumer -- including browser bundles -- for an opt-in Node-only feature. It follows the @opentelemetry/api pattern this package already uses: devDependency for our own tests, optional peer for consumers, lazy import at the call site. - Requesting a connect timeout without undici installed now THROWS with an actionable message rather than silently continuing unbounded. A timeout that quietly isn't applied is the exact failure this feature exists to prevent. Per Spec C, the five regression tests read their knobs (black-hole URL, connect timeout, whole timeout, elapsed ceiling) from the committed spec/connect-timeout-corpus.json instead of each hard-coding them, so the thresholds cannot drift apart per language. The TS suite carries a positive control so a corpus that failed to load reads red rather than green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0152bbE1veqfG1SVJdyLCBxC
…103) Consolidates PRs #88 (Rust), #89 (Go), #90 (Python), #91 (.NET) and #92 (TypeScript), open since 2026-07-10, onto current main. The work was sound; it had gone stale against traceparent injection and the redaction fix, both of which touch the same single-request site. The gap is real: without a connect timeout, a SYN to a dead pod IP still lingering in a ClusterIP's iptables burns the WHOLE-request budget before retry can try a live endpoint. Measured locally, an unbounded connect to a black hole fails after 10.5s (undici's own default); bounded at 500ms it fails in ~1s. Two changes from the original PRs: - undici is an OPTIONAL PEER dependency, not a runtime dependency. #92 added it to `dependencies`, which would have put a full HTTP stack in the tree of every consumer -- including browser bundles -- for an opt-in Node-only feature. It follows the @opentelemetry/api pattern this package already uses: devDependency for our own tests, optional peer for consumers, lazy import at the call site. - Requesting a connect timeout without undici installed now THROWS with an actionable message rather than silently continuing unbounded. A timeout that quietly isn't applied is the exact failure this feature exists to prevent. Per Spec C, the five regression tests read their knobs (black-hole URL, connect timeout, whole timeout, elapsed ceiling) from the committed spec/connect-timeout-corpus.json instead of each hard-coding them, so the thresholds cannot drift apart per language. The TS suite carries a positive control so a corpus that failed to load reads red rather than green. Claude-Session: https://claude.ai/code/session_0152bbE1veqfG1SVJdyLCBxC Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
|
Landed via #103, which consolidates all five ports onto current main. The work here was sound — it had gone stale against traceparent injection (#95) and the credential-redaction fix (#98), both of which touch the same single-request site. Two changes from this PR in the consolidated version:
Per Spec C the five regression tests now read their knobs (black-hole URL, connect timeout, whole timeout, elapsed ceiling) from Closing in favour of #103. |
What
Adds an optional connect timeout to the TypeScript SmooAI fetch SDK — parity with the Rust PR #88 (
with_connect_timeout, SMOODEV-2513).RequestOptions.connectTimeoutMsoption (same options surface as the existing whole-requesttimeout: { timeoutMs }).FetchBuilder.withConnectTimeout(ms), mirroringwithTimeout(...)and the Rust builder.Why
api-prime's ~16s stalls (SMOODEV-2498 / SMOODEV-2481) were fresh SYNs to dead pod IPs still lingering in a ClusterIP's iptables. The whole-request
timeoutonly fires after the entire request budget elapses, so a black-holed connect stalls for the full window before retry can even try a live endpoint. A connect timeout bounds only the connection-establishment phase: a dead connect fails in ~the configured window and the configured retry lands on a live pod. Slow-but-alive handlers are unaffected.How
Node's global
fetchis undici under the hood. A connect timeout requires adispatcher— an undiciAgent({ connect: { timeout: ms } })— passed on the fetch init. WhenconnectTimeoutMsis set we lazily build (and cache, keyed by ms) such an Agent and attach it asdispatcher.^6to match Node 22's bundled undici handler interface (npm undici@8'sAgentis rejected by Node 22's built-in fetch withUND_ERR_INVALID_ARG/invalid onRequestStart method— a real cross-version footgun). It's imported via a guardedawait import('undici'), so it never loads unless a connect timeout is actually requested, and it stays an external dynamic import in both the node and browser builds (not bundled).Default-off (behavior-identical)
When
connectTimeoutMsis unset, no dispatcher is attached — the fetch call is byte-identical to today. The undici import is never reached. In browser/worker environments the option is ignored (no connect-timeout knob there;getConnectTimeoutDispatcherreturnsundefinedbefore importing undici).Test
src/fetch.connect-timeout.spec.tsmirrors the Rustconnect_timeout_tests.rs: a connect to the non-routable black-holehttp://10.255.255.1:80/withconnectTimeoutMs: 500and a 10x-larger whole-requesttimeoutof 5000ms fails in ~1s (well under 3s), and the error is not the mollitia whole-requestTimeoutError. Plus a builder-path test and an unset-option (default-behavior) test. All 51 TS tests pass (48 existing + 3 new); typecheck, lint, format, and both builds are green.🤖 Generated with Claude Code