{"record":{"id":"0300addc6bbcc6f4","repo":"cloudflare/cloudflared","slug":"unable-to-wait-for-both-streams-while-proxying","errorCode":null,"errorMessage":"unable to wait for both streams while proxying","messagePattern":"unable to wait for both streams while proxying","errorType":"error_code","errorClass":null,"httpStatus":null,"severity":"error","filePath":"stream/stream.go","lineNumber":103,"sourceCode":"\n// Pipe copies copy data to & from provided io.ReadWriters.\nfunc Pipe(tunnelConn, originConn io.ReadWriter, log *zerolog.Logger) {\n\t_ = PipeBidirectional(NopCloseWriterAdapter(tunnelConn), NopCloseWriterAdapter(originConn), 0, log)\n}\n\n// PipeBidirectional copies data to two unidirectional streams. It is a special case of Pipe where it receives a concept that allows for Read and Write side to be closed independently.\n// The main difference is that when piping data from a reader to a writer, if EOF is read, then this implementation propagates the EOF signal to the destination/writer by closing the write side of the\n// Bidirectional Stream.\n// Finally, depending on once EOF is ready from one of the provided streams, the other direction of streaming data will have a configured time period to also finish, otherwise,\n// the method will return immediately  with a timeout error. It is however, the responsibility of the caller to close the associated streams in both ends in order to free all the resources/go-routines.\nfunc PipeBidirectional(downstream, upstream Stream, maxWaitForSecondStream time.Duration, log *zerolog.Logger) error {\n\tstatus := newBiStreamStatus()\n\n\tgo unidirectionalStream(downstream, upstream, \"upstream->downstream\", status, log)\n\tgo unidirectionalStream(upstream, downstream, \"downstream->upstream\", status, log)\n\n\tif err := status.wait(maxWaitForSecondStream); err != nil {\n\t\treturn errors.Wrap(err, \"unable to wait for both streams while proxying\")\n\t}\n\n\treturn nil\n}\n\nfunc unidirectionalStream(dst WriterCloser, src Reader, dir string, status *bidirectionalStreamStatus, log *zerolog.Logger) {\n\tdefer func() {\n\t\t// The bidirectional streaming spawns 2 goroutines to stream each direction.\n\t\t// If any ends, the callstack returns, meaning the Tunnel request/stream (depending on http2 vs quic) will\n\t\t// close. In such case, if the other direction did not stop (due to application level stopping, e.g., if a\n\t\t// server/origin listens forever until closure), it may read/write from the underlying ReadWriter (backed by\n\t\t// the Edge<->cloudflared transport) in an unexpected state.\n\t\t// Because of this, we set this recover() logic.\n\t\tif err := recover(); err != nil {\n\t\t\tif status.isAnyDone() {\n\t\t\t\t// We handle such unexpected errors only when we detect that one side of the streaming is done.\n\t\t\t\tlog.Debug().Msgf(\"recovered from panic in stream.Pipe for %s, error %s, %s\", dir, err, debug.Stack())\n\t\t\t} else {","sourceCodeStart":85,"sourceCodeEnd":121,"githubUrl":"https://github.com/cloudflare/cloudflared/blob/2253eeeb25a44a713a4b60b8ba1e1b3f377d1a0f/stream/stream.go#L85-L121","documentation":"PipeBidirectional pumps data between two connections with one goroutine per direction and waits for both to finish. This error is returned when the second stream does not complete within maxWaitForSecondStream after the first one ended, i.e. proxying stalled with one direction still open.","triggerScenarios":"unidirectionalStream in one direction returns (usually on read error or EOF) while the other direction blocks longer than maxWaitForSecondStream — e.g. the remote keeps the connection open without sending data.","commonSituations":"Half-closed connections where the origin never closes its write side; dead peers not answering FINs; long-lived idle streams (e.g. SSH, websocket) exceeding the wait budget; network partitions.","solutions":["Increase maxWaitForSecondStream if legitimate long-lived half-open streams are expected","Verify both peers properly close their sockets when done (check origin server keep-alive/close behavior)","Check for hung reads caused by a dead peer and add application-level timeouts/keepalives","Inspect the wrapped error to identify which direction stalled"],"exampleFix":"// before\nif err := status.wait(maxWaitForSecondStream); err != nil {\n    return errors.Wrap(err, \"unable to wait for both streams while proxying\")\n}\n// after\nconst maxWaitForSecondStream = 5 * time.Minute // was too short for idle SSH sessions\nif err := status.wait(maxWaitForSecondStream); err != nil {\n    return errors.Wrap(err, \"unable to wait for both streams while proxying\")\n}","handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"if err := PipeBidirectional(down, up, log); err != nil {\n    var wrapped *errors.Wrap // cloudflared errors package\n    if strings.Contains(err.Error(), \"unable to wait for both streams\") {\n        log.Warn().Err(err).Msg(\"stream stalled; closing connection\")\n    }\n    _ = wrapped\n}","preventionTips":["Ensure peers close sockets promptly at end of session","Enable TCP keepalives to detect dead peers","Size maxWaitForSecondStream to your workload (long idle streams need longer waits)"],"tags":["network","proxy","streams"],"backgroundTag":"request-timeout","analyzedSha":"2253eeeb25a44a713a4b60b8ba1e1b3f377d1a0f","analyzedAt":"2026-09-06T04:14:33.757Z","contentChangedAt":"2026-09-06T04:14:33.757Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}