{"record":{"id":"ce2addd41663fe44","repo":"router-for-me/CLIProxyAPI","slug":"wsrelay-stream-closed","errorCode":null,"errorMessage":"wsrelay: stream closed","messagePattern":"wsrelay: stream closed","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/wsrelay/http.go","lineNumber":145,"sourceCode":"\t\tsend := func(ev StreamEvent) bool {\n\t\t\tif ctx == nil {\n\t\t\t\tout <- ev\n\t\t\t\treturn true\n\t\t\t}\n\t\t\tselect {\n\t\t\tcase <-ctx.Done():\n\t\t\t\treturn false\n\t\t\tcase out <- ev:\n\t\t\t\treturn true\n\t\t\t}\n\t\t}\n\t\tfor {\n\t\t\tselect {\n\t\t\tcase <-ctx.Done():\n\t\t\t\treturn\n\t\t\tcase msg, ok := <-respCh:\n\t\t\t\tif !ok {\n\t\t\t\t\t_ = send(StreamEvent{Err: errors.New(\"wsrelay: stream closed\")})\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tswitch msg.Type {\n\t\t\t\tcase MessageTypeStreamStart:\n\t\t\t\t\tresp := decodeResponse(msg.Payload)\n\t\t\t\t\tif okSend := send(StreamEvent{Type: MessageTypeStreamStart, Status: resp.Status, Headers: resp.Headers}); !okSend {\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\t\t\t\tcase MessageTypeStreamChunk:\n\t\t\t\t\tchunk := decodeChunk(msg.Payload)\n\t\t\t\t\tif okSend := send(StreamEvent{Type: MessageTypeStreamChunk, Payload: chunk}); !okSend {\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\t\t\t\tcase MessageTypeStreamEnd:\n\t\t\t\t\t_ = send(StreamEvent{Type: MessageTypeStreamEnd})\n\t\t\t\t\treturn\n\t\t\t\tcase MessageTypeError:\n\t\t\t\t\t_ = send(StreamEvent{Type: MessageTypeError, Err: decodeError(msg.Payload)})","sourceCodeStart":127,"sourceCodeEnd":163,"githubUrl":"https://github.com/router-for-me/CLIProxyAPI/blob/78f0c4079e3e6273d65d03b5549cffc898703264/internal/wsrelay/http.go#L127-L163","documentation":"While streaming a relayed response, internal/wsrelay/http.go iterates respCh and forwards StreamStart/StreamChunk events. If respCh closes before the stream completes (no terminal event), it emits a StreamEvent with `Err: wsrelay: stream closed`, telling the consumer the SSE/stream body was truncated by the relay websocket shutting down rather than ending normally. Unlike error 41 this occurs in the streaming path, so partial data may already have been consumed.","triggerScenarios":"Streaming relay request where the websocket closes mid-body: relay peer restart, session deadline reached during a long stream, network drop, or upstream aborting the stream and the peer closing instead of sending MessageTypeError.","commonSituations":"Long completions relayed over wsrelay that exceed session deadlines; mobile/unstable clients; relay behind aggressive LB idle timeouts that do not honor in-flight chunks; Codex executor streams that die mid-generation.","solutions":["Detect StreamEvent.Err == \"wsrelay: stream closed\" in your consumer and retry the request, since output is truncated.","Persist partial output if idempotency matters, then re-issue with the accumulated conversation so the model can resume.","Increase wsrelay session deadlines (internal/wsrelay/session.go) to exceed your longest expected stream duration.","Ensure the relay peer sends MessageTypeError on upstream failure instead of closing, so clients can distinguish errors from truncation."],"exampleFix":null,"handlingStrategy":"retry","validationCode":null,"typeGuard":"func isStreamClosedEvent(ev StreamEvent) bool {\n    return ev.Err != nil && strings.Contains(ev.Err.Error(), \"wsrelay: stream closed\")\n}","tryCatchPattern":"for ev := range stream {\n    if isStreamClosedEvent(ev) {\n        // output truncated: retry with accumulated transcript so far\n        savePartial(ev)\n        retryWithTranscript()\n        return\n    }\n    consume(ev)\n}","preventionTips":["Persist partial streamed output as it arrives so a closed stream is recoverable, not lost.","Keep relay session deadlines larger than maximum stream duration.","Ensure relay peers emit MessageTypeError instead of closing on upstream failure."],"tags":["websocket","wsrelay","streaming","truncation"],"backgroundTag":null,"analyzedSha":"78f0c4079e3e6273d65d03b5549cffc898703264","analyzedAt":"2026-08-15T12:26:37.444Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}