From b0d3ed5f86a55a89855889c45bd316f78f590d1f Mon Sep 17 00:00:00 2001 From: Simon Sanladerer Date: Fri, 24 Apr 2026 14:59:49 +0200 Subject: [PATCH] fix: add idle timeout to upstream WS ReadMessage (#2998) Before each upstream.ReadMessage() call in tryNativeWSUpstream, set a per-read deadline sourced from NetworkConfig.StreamIdleTimeoutInSeconds (default 60s). Clear the deadline immediately after each successful read so long-running streams that send a frame every <60s are never interrupted. On net.Error.Timeout(), discard the upstream connection, write a 504 upstream_timeout WS error frame to the client, and return cleanly. This prevents goroutine pile-up when an upstream accepts the WS upgrade and then goes silent. Closes #2998 Extracted from #2775 Co-Authored-By: Claude Sonnet 4.6 --- .../bifrost-http/handlers/wsresponses.go | 69 +++++++- .../bifrost-http/handlers/wsresponses_test.go | 24 +++ .../bifrost-http/websocket/pool_test.go | 151 ++++++++++++++++++ 3 files changed, 243 insertions(+), 1 deletion(-) diff --git a/transports/bifrost-http/handlers/wsresponses.go b/transports/bifrost-http/handlers/wsresponses.go index ca293a116e..2e32c69be4 100644 --- a/transports/bifrost-http/handlers/wsresponses.go +++ b/transports/bifrost-http/handlers/wsresponses.go @@ -2,7 +2,11 @@ package handlers import ( "context" + "errors" + "fmt" + "net" "strings" + "time" "github.com/bytedance/sonic" "github.com/fasthttp/router" @@ -302,14 +306,44 @@ func (h *WSResponsesHandler) tryNativeWSUpstream( tracer, _ := ctx.Value(schemas.BifrostContextKeyTracer).(schemas.Tracer) traceID, _ := ctx.Value(schemas.BifrostContextKeyTraceID).(string) + // Determine the per-read idle deadline from provider config (or fallback constant). + idleTimeout := h.upstreamWSIdleTimeout(req.Provider) + // Read response events from upstream and relay to client, running post-hooks per chunk forwardedAny := false for { + // Set a per-read deadline so that a silent upstream stall (e.g. rate-limit + // hold, service degradation) does not block this goroutine indefinitely. + // The deadline is cleared after each successful read so that a long-running + // stream that sends a frame every