Skip to content

fix(forwarder): increase shell foreground timeout and improve error reporting - #227

Open
quangdang46 wants to merge 6 commits into
leookun:mainfrom
quangdang46:fix/foreground-timeout-error-message
Open

fix(forwarder): increase shell foreground timeout and improve error reporting#227
quangdang46 wants to merge 6 commits into
leookun:mainfrom
quangdang46:fix/foreground-timeout-error-message

Conversation

@quangdang46

Copy link
Copy Markdown

Summary

Two fixes for issues causing Cursor to display "An unexpected error occurred on our servers":

1. Shell foreground timeout: 30s → 120s

The default shellForegroundTimeoutDuration was 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 no block_until_ms is specified in the exec args.

File: internal/backend/forwarder/shell_recovery.go:37

2. Improved failStream error message

When a stream fails, failStream would 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 cause
After: 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-2258

Testing

  • go vet ./internal/backend/forwarder/... — clean
  • Verified the forked repo builds successfully on darwin/arm64

Related

Users experiencing the issue would see repeated forwarder synthetic shell recovery log entries with reason=foreground_deadline_exceeded followed by provider pass retries up to 15+ times before the stream ultimately fails with "unknown error".

…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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant