fix(generator-cli): raise HTTP timeouts for hosted FAI analysis calls - #17613
fix(generator-cli): raise HTTP timeouts for hosted FAI analysis calls#17613willkendall01 wants to merge 1 commit into
Conversation
AutoVersionStep calls the hosted FAI service at `/sdks/analyze-commit-diff` for remote generation. FAI chunks a multi-MB SDK diff and fans it out over dozens of sequential LLM calls, so one request legitimately runs for minutes — longer than undici's default 300s headers timeout on Node's global fetch. Routes that call through a `faiFetch` helper that raises undici's header and body timeouts to 960s, just above the FAI load balancer's own idle timeout, so the server's timeout wins the race and the caller gets an HTTP status it can log rather than an opaque client-side abort. undici only honours a per-request `dispatcher` when called through its own `fetch` export — Node's global fetch silently ignores the option — so the helper installs a process-wide dispatcher once, mirroring what the CLI already does at startup in `packages/cli/cli/src/cli.ts`. An `HTTP_PROXY` dispatcher, if the process installed one, is left alone so the proxy is not dropped. Co-Authored-By: Claude <noreply@anthropic.com>
There was a problem hiding this comment.
AI Review Summary
Small, well-tested change that installs a process-wide undici dispatcher with long timeouts before FAI calls. Main concerns: the global dispatcher affects every fetch in the process (not just FAI), and the proxy guard only checks HTTP_PROXY while a rejected install promise would be cached forever.
- 🟡 1 warning(s)
- 🔵 2 suggestion(s)
To request another review, comment /ai-review on this pull request.
| if (process.env.HTTP_PROXY != null) { | ||
| return; | ||
| } |
There was a problem hiding this comment.
🟡 warning
Proxy detection only checks HTTP_PROXY. FAI is HTTPS, so a process configured via HTTPS_PROXY (or lowercase https_proxy/http_proxy, which undici's EnvHttpProxyAgent and most tooling honour) would have its ProxyAgent silently replaced. Widen the guard:
| if (process.env.HTTP_PROXY != null) { | |
| return; | |
| } | |
| const proxyEnvVars = ["HTTP_PROXY", "http_proxy", "HTTPS_PROXY", "https_proxy", "ALL_PROXY", "all_proxy"]; | |
| if (proxyEnvVars.some((name) => process.env[name] != null)) { | |
| return; | |
| } |
| dispatcherInstallation ??= installLongRunningDispatcher(); | ||
| await dispatcherInstallation; |
There was a problem hiding this comment.
🔵 suggestion
If installLongRunningDispatcher ever rejects (it currently can't, but the try/catch only wraps the import+construction — a future edit could change that), the rejected promise is memoized and every subsequent faiFetch call throws before the request is even attempted. Cheap insurance:
| dispatcherInstallation ??= installLongRunningDispatcher(); | |
| await dispatcherInstallation; | |
| dispatcherInstallation ??= installLongRunningDispatcher().catch(() => undefined); | |
| await dispatcherInstallation; |
| setGlobalDispatcher( | ||
| new Agent({ | ||
| headersTimeout: LONG_RUNNING_TIMEOUT_MS, | ||
| bodyTimeout: LONG_RUNNING_TIMEOUT_MS | ||
| }) | ||
| ); |
There was a problem hiding this comment.
🔵 suggestion
setGlobalDispatcher with a fresh Agent replaces whatever dispatcher is currently installed, including any non-proxy customization (connect timeouts, custom CA, keep-alive tuning) another part of the process may have set. Worth noting the blast radius in the doc comment, or gating on getGlobalDispatcher() still being the stock default, since this changes timeouts for all fetches in the process, not just FAI.
There was a problem hiding this comment.
Devin Review found 1 potential issue.
1 flag not posted on this PR by your GitHub settings — view it in Devin Review. (Configure)
| setGlobalDispatcher( | ||
| new Agent({ | ||
| headersTimeout: LONG_RUNNING_TIMEOUT_MS, | ||
| bodyTimeout: LONG_RUNNING_TIMEOUT_MS | ||
| }) | ||
| ); |
There was a problem hiding this comment.
🟡 CLI transport settings are overwritten
When the main CLI invokes faiFetch, it replaces the existing dispatcher and restores undici's default connection timeout. Slow connections can now fail.
Prompt for agents
Avoid replacing the process-wide dispatcher inside packages/generator-cli/src/utils/faiFetch.ts. The main Fern CLI installs an Agent with an effectively unlimited connect timeout before PostGenerationPipeline runs, but installLongRunningDispatcher replaces it with an Agent that only customizes header and body timeouts. Use an isolated undici fetch/dispatcher for the hosted FAI request, or preserve the existing dispatcher's transport behavior while extending these timeouts. Also cover execution after the main CLI dispatcher has already been installed.
Was this helpful? React with 👍 or 👎 to provide feedback.
Description
Linear ticket: Refs
AutoVersionStepcalls the hosted FAI service atPOST /sdks/analyze-commit-diffwhen no local BAMLaiconfig is supplied (remote generation via fiddle). FAI chunks a multi-MB SDK diff and fans it out over 30-40 sequential LLM calls plus a changelog rollup, so a single request legitimately runs 2-5 minutes.Node's global
fetchgives up after undici's default 300s headers timeout, which is shorter than the FAI load balancer's idle timeout. On the slowest diffs the client would abort before the server did — and an aborted request is indistinguishable from a broken one, which is exactly the signalAutoVersionStepuses to choose between trusting the analysis and falling back to a blind PATCH bump.Companion to fern-api/fern-platform#14376, which raises that load balancer's idle timeout from 60s to 900s. Without this change the client's 300s cap simply becomes the next binding constraint.
Changes Made
packages/generator-cli/src/utils/faiFetch.ts: raises undici's header and body timeouts to 960s — deliberately just above the load balancer's 900s, so the server's timeout wins the race and the caller receives a loggable HTTP status instead of an opaque client abort.AutoVersionStep.analyzeViaFaiServicecallsfaiFetchinstead of globalfetch.undici(already in the catalog at 6.27.0, and already bundled by the CLI build).Why a process-wide dispatcher rather than a per-request one
undici only honours a per-request
dispatcherwhen called through its ownfetchexport. Verified empirically on Node 24.17.0:So the helper installs a global dispatcher once per process, which is the same conclusion
packages/cli/cli/src/cli.ts:167-172already reached. Two consequences worth reviewing:HTTP_PROXYis set, so it cannot clobber aProxyAgentthe process installed.AutoVersionStepruns inside the fern CLI process it was already covered, sincecli.tsinstalls an effectively-unlimited dispatcher at startup. This change only affects the standalonegenerator-clibinary — which is the path fiddle-coordinator runs, and the one that was actually failing.Testing
src/__test__/faiFetch.test.ts: pass-through tofetch, timeouts exceed 900s, dispatcher installed at most once across many requests, and theHTTP_PROXYcase.generator-clisuite: 480 passed, 1 skipped. The existingvi.stubGlobal("fetch", ...)tests for the FAI path keep working untouched, because the helper calls the ambientfetchbinding.tsc --project tsconfig.build.jsonclean,biome format+biome lint --error-on-warningsclean,knipclean.Context on the impact
In one week of production logs,
AutoVersionSteplogged 50FAI analysis failedfallbacks across 107 version decisions (~47%), every one of themstatus 504from the load balancer. 50 of the 55 PATCH bumps in that window were timeouts rather than analyzed decisions. Every failure was on a diff >= 1.2MB (median 2.8MB); nothing below that threshold ever timed out.Generated with Claude Code