fix(ai-llm-proxy): prevent mid-flight request cancellation from wasting LLM cost - #535
Merged
Merged
Conversation
✅ Snyk checks have passed. No issues have been found so far.
💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse. |
The proxy never listened for the client connection closing, so an AbortSignal.timeout firing client-side, a closed tab, or a dropped connection left the outbound LLM fetch running for up to its own 60s timeout — burning LLM cost for a response nobody could receive, then attempting to write to an already-dead response. Wire an AbortController into handleRequest that aborts on req's 'close' event and is combined with the existing upstream timeout via AbortSignal.any(), so either condition stops the outbound fetch. Guard sendJson and the final response write against a disconnected client, and attach a no-op res error listener so a late write-after- close can never crash the process with an unhandled 'error' event. Add unit tests proving the outbound fetch signal aborts on client disconnect and that no write/crash happens afterwards. Signed-off-by: Lukas Hirt <info@hirt.cz>
Every ai-llm-proxy caller set its own AbortSignal.timeout with wildly inconsistent values (30s or 60s), while the proxy's own upstream fetch allows up to 60s plus unaccounted latency for OIDC discovery/userinfo validation. Callers using the 30s value could abort mid-flight while the proxy was still legitimately working, cancelling a request that would otherwise have succeeded. Raise and unify every client-side timeout that calls the ai-llm-proxy to 90s — comfortably above the proxy's 60s upstream timeout plus margin — across web-app-ai-data-insights-sidebar, web-app-ai-doc-summary, web-app-ai-folder-brief-sidebar, web-app-ai-folder-readme-generator, web-app-ai-image-alt-text-sidebar (main call only; the separate 10s vision-capability probe is intentionally untouched), web-app-ai-multi- doc-synthesizer, web-app-ai-quick-draft-creator, web-app-ai-sensitive- data-scanner, web-app-ai-smart-collections-nav, web-app-ai-smart-file- tagger-qa, web-app-chat-with-file, and web-app-version-changelog. (web-app-ai-smart-collections-nav's useRecentFiles.ts REQUEST_TIMEOUT_MS is a WebDAV REPORT/getFileContents timeout, not an LLM proxy call, and is left untouched.) Also add TimeoutError-specific error handling to the two composables that were missing it entirely: web-app-ai-sensitive-data-scanner's useLlm/useScanner and web-app-ai-quick-draft-creator's useLLM (which required wrapping the fetch call itself in try/catch, since a timeout abort rejects before there's a Response to check .ok on). Signed-off-by: Lukas Hirt <info@hirt.cz>
…ceiling The proxy's upstream LLM timeout will become configurable via LLM_TIMEOUT_MS in a separate PR (#530), with a default of 60s but no fixed upper bound. A hardcoded 90s client-side timeout could again fire before a larger admin-configured proxy timeout, recreating the mid-flight cancellation issue this branch fixes. Raise every client composable's AbortSignal.timeout from 90_000 to 300_000 (5 minutes) so it acts purely as a generous outer bound against a hung network or dead proxy process, rather than an attempt to closely track the proxy's own (separately configurable) timeout. Signed-off-by: Lukas Hirt <info@hirt.cz>
LukasHirt
force-pushed
the
fix-ai-llm-proxy-mid-flight-cancellation
branch
from
July 27, 2026 12:47
7fa5e3c to
3fe12ae
Compare
dj4oC
approved these changes
Jul 29, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Proactive follow-up (no tracked issue) addressing user-reported "AI proxy requests cancelled mid-flight" behavior. Prior research identified two independent root causes across the client → proxy → upstream LLM request chain, both fixed here.
Root cause 1 — client-side timeouts fire before the proxy's own timeout, and are inconsistent
Every
web-app-ai-*(and a couple non-AI) composable calling theai-llm-proxyset its ownAbortSignal.timeout(...), with inconsistent values of either 30s or 60s. The proxy itself allows up to 60s for its own upstream LLM fetch, plus unaccounted latency before that for OIDC discovery/userinfo validation. Any composable using the 30s value could abort a request the proxy was still legitimately working on.Fix: raised and unified every client-side
AbortSignal.timeout(...)call that hits the ai-llm-proxy to90_000(90s) — comfortably above the proxy's 60s upstream timeout — with a one-line comment at each call site. Left untouched: the 10s vision-capability probe inweb-app-ai-image-alt-text-sidebar, andweb-app-ai-smart-collections-nav'suseRecentFiles.tsREQUEST_TIMEOUT_MS(a WebDAV REPORT/getFileContents timeout, not an LLM call).Also added missing
TimeoutError-specific error handling (matching the message style already used by sibling packages) to the two composables that lacked it entirely:web-app-ai-sensitive-data-scanner(useLlm.ts/useScanner.ts)web-app-ai-quick-draft-creator(useLLM.ts) — this one needed the outboundfetchwrapped in its own try/catch, since a timeout abort rejects the fetch itself before there's aResponseto check.okon.Root cause 2 — the proxy never notices the client disconnected
In
packages/ai-llm-proxy/src/index.ts'shandleRequest, once the outbound fetch to the LLM endpoint started, there was no listener for the client request's'close'event. If the client's own timeout fired, the tab closed, or the connection dropped, the proxy kept awaiting the full upstream LLM response (up to 60s) — burning LLM cost for a response nobody would receive — then tried to write to an already-dead response.Fix:
AbortControllerintohandleRequest, aborted on the request's'close'event.AbortSignal.any([...]), so either condition stops the outbound fetch.sendJsonand the final response write against a disconnected client (res.writableEnded/res.destroyedchecks).res.on('error', () => {})listener so a write-after-close can never crash the process via an unhandled'error'event.Verification
Per touched package, ran (or the equivalent for
ai-llm-proxy, which has nocheck:types/lintscript, sotsc/scopedeslintwere used instead):check:types— all passlint— 0 errors (only pre-existing warnings unrelated to these changes)test:unit— all pass, including 5 new tests inpackages/ai-llm-proxy/tests/unit/proxy.spec.ts(28/28 total in that file)Packages covered:
ai-llm-proxy,web-app-ai-data-insights-sidebar,web-app-ai-doc-summary,web-app-ai-folder-brief-sidebar,web-app-ai-folder-readme-generator,web-app-ai-image-alt-text-sidebar,web-app-ai-multi-doc-synthesizer,web-app-ai-quick-draft-creator,web-app-ai-sensitive-data-scanner,web-app-ai-smart-collections-nav,web-app-ai-smart-file-tagger-qa,web-app-chat-with-file,web-app-version-changelog.Test plan
ai-llm-proxyunit tests cover the new disconnect-abort behavior and write-after-close safetyFollow-up: client timeout raised from 90s to a 5-minute safety net
The
90_000(90s) client-side timeout above was chosen to comfortably exceed the proxy's hardcoded 60s upstream timeout. However, a separate, still-open PR (#530,feat/ai-llm-proxy-configurable-timeout) makes that upstream timeout configurable viaLLM_TIMEOUT_MS(default unchanged at 60s, but with no fixed upper bound). If an admin setsLLM_TIMEOUT_MSabove 90s to accommodate a slower self-hosted model, the client's 90s timeout would again fire before the proxy's configured timeout — reintroducing the exact mid-flight cancellation bug this PR fixes, just at a different threshold.Fix: raised every client composable's
AbortSignal.timeout(...)from90_000to300_000(5 minutes), and reworded the accompanying comments accordingly. The client-side timeout is no longer meant to closely track the proxy's own upstream timeout — it's now a generous, fixed outer bound whose only job is to prevent the browser tab from hanging indefinitely if the network or the proxy process itself never responds at all (e.g. a killed container, a dropped TCP connection with no FIN, or a proxy that hangs before even starting its upstream fetch). The proxy's own (separately configurable) timeout remains the authoritative, real cutoff for how long an LLM call is allowed to run.Left untouched: the proxy's own
AbortSignal.timeout(60_000)inpackages/ai-llm-proxy/src/index.ts(governed by #530, out of scope here), the 10s vision-capability probe inweb-app-ai-image-alt-text-sidebar, andweb-app-ai-smart-collections-nav'suseRecentFiles.tsWebDAV timeout.Verified
check:types,lint, andtest:unitpass for all 12 touchedweb-app-*packages after the change.