fix(forwarder): increase shell foreground timeout and improve error reporting - #227
Open
quangdang46 wants to merge 6 commits into
Open
fix(forwarder): increase shell foreground timeout and improve error reporting#227quangdang46 wants to merge 6 commits into
quangdang46 wants to merge 6 commits into
Conversation
…d improve error reporting - Increase shell foreground timeout default from 30s to 120s to reduce premature stream failures when running long-running terminal commands inside Cursor agent turns (foreground_deadline_exceeded). - Improve failStream error message: include the actual error cause text instead of always showing "unknown error" to help users and upstream Cursor identify the real failure reason. Fixes issues where Cursor displays "An unexpected error occurred on our servers" due to premature tool execution timeouts and opaque error messages sent back through RunSSE.
…ent timeout on long-poll BidiAppend The proxy's upstream HTTP client (proxy :18080 → backend :18090) had ResponseHeaderTimeout=60s, causing "net/http: timeout awaiting response headers" when Backend BidiAppend handler takes longer than 60s to produce the first response header (e.g. waiting for slow model provider start). This manifests as "An unexpected error occurred on our servers" with "Network disconnected" in Cursor, and RetriableError in the agent loop. Removing ResponseHeaderTimeout allows the backend to take as long as it needs before writing the first header byte. The actual request is still bounded by the caller's context/deadline.
…onnection drops The proxy upstream client (proxy :18080 → backend :18090) used ForceAttemptHTTP2: true, sharing a single HTTP/2 connection for all concurrent BidiService streams from Cursor (multitask, multiple tabs). HTTP/2 multiplexing errors in Go's h2 transport can silently drop streams when multiple concurrent Agent requests are forwarded simultaneously, causing Cursor to receive 'Network disconnected' / RetriableError on otherwise healthy connections. Switching to HTTP/1.1 ensures each request gets an independent TCP connection, avoiding HTTP/2 stream-level failures entirely. Also disable HTTP/2 on the proxy-facing Transport for consistency.
When multiple Cursor tabs or /multitask send concurrent BidiAppend requests for the same conversation, each request must acquire a conversation file lock. The lock was using a 30-second acquire timeout, causing subsequent requests to block silently for up to 30s. During this silent wait, Cursor's client-side SSE reader timeout fires first, producing 'Network disconnected' / RetriableError even though BYOK is still alive — it's just waiting for the lock. Reducing to 5s means the lock either acquires quickly (<10ms for the common uncontended case) or fails fast with a clear error, letting Cursor retry immediately instead of hanging until its client timeout. Also reduces staleConversationLockRemoveTimeout from 30s to 5s for consistency with the faster failure path.
The OpenAI adapter's HTTP client used NewHTTPClient(0) which means no timeout on the HTTP request to the model provider. When 9Router or any upstream provider stalls on a request, the provider call hangs indefinitely — Cursor sees 'Network disconnected' from its client-side timeout, but BYOK's goroutine remains blocked forever without logging any error. Adding a 5-minute timeout ensures the HTTP call either succeeds quickly (<30s typical) or fails with a clear timeout error that gets propagated through the forwarder and logged.
The OpenAI adapter used NewHTTPClient(0) which disables timeout. When the upstream model provider stalls, the HTTP call hangs indefinitely. Cursor's client-side timeout fires first producing 'Network disconnected', but BYOK's goroutine stays blocked silently. 90s is chosen because it's slightly longer than Cursor's typical client-side timeout (60s) but short enough to fail fast and let the forwarder retry with a clear error.
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
Two fixes for issues causing Cursor to display "An unexpected error occurred on our servers":
1. Shell foreground timeout: 30s → 120s
The default
shellForegroundTimeoutDurationwas 30 seconds, which caused premature stream failures when running long-running terminal commands during Cursor agent turns. This timeout is used as the default when noblock_until_msis specified in the exec args.File:
internal/backend/forwarder/shell_recovery.go:372. Improved failStream error message
When a stream fails,
failStreamwould always report "unknown error" as the terminal message regardless of the actual error cause. This meant Cursor could only display a generic error message to the user.Before:
"unknown error"regardless of actual causeAfter: Includes the actual
terminalCode(e.g.,"foreground_deadline_exceeded","provider_error") and appends the underlying error text when available.File:
internal/backend/forwarder/service.go:2245-2258Testing
go vet ./internal/backend/forwarder/...— cleanRelated
Users experiencing the issue would see repeated
forwarder synthetic shell recoverylog entries withreason=foreground_deadline_exceededfollowed by provider pass retries up to 15+ times before the stream ultimately fails with "unknown error".