{"record":{"id":"57fb70f10a64eef5","repo":"vitessio/vitess","slug":"errstreamclosed","errorCode":"ErrStreamClosed","errorMessage":"stream closed for sending","messagePattern":"stream closed for sending","errorType":"error_code","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"go/vt/vtctl/internal/grpcshim/bidi_stream.go","lineNumber":31,"sourceCode":"See the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\npackage grpcshim\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"io\"\n\t\"sync\"\n\n\t\"google.golang.org/grpc\"\n\t\"google.golang.org/grpc/metadata\"\n)\n\n// ErrStreamClosed is the error types embedding BidiStream should return when\n// their Send method is called and IsClosed returns true.\nvar ErrStreamClosed = errors.New(\"stream closed for sending\")\n\n// BidiStream is a shim struct implementing both the grpc.ClientStream and\n// grpc.ServerStream interfaces. It can be embedded into other types that need\n// all of those methods to satisfy the compiler, but are only interested in the\n// parameterized Send/Recv methods typically called by gRPC streaming servers\n// and clients. For example, in the localvtctldclient:\n//\n//\ttype backupStreamAdapter struct {\n//\t\t*grpcshim.BidiStream\n//\t\tch chan *vtctldatapb.BackupResponse\n//\t}\n//\n//\tfunc (stream *backupStreamAdapter) Recv() (*vtctldatapb.BackupResponse, error) {\n//\t\tselect {\n//\t\tcase <-stream.Context().Done():\n//\t\t\treturn nil, stream.Context().Err()\n//\t\tcase <-stream.Closed():\n//\t\t\t// Stream has been closed for future sends. If there are messages that","sourceCodeStart":13,"sourceCodeEnd":49,"githubUrl":"https://github.com/vitessio/vitess/blob/01a25a7d176f94613b8d59d799f438380a8760e4/go/vt/vtctl/internal/grpcshim/bidi_stream.go#L13-L49","documentation":"ErrStreamClosed is the sentinel error that bidi stream shims in grpcshim must return when their Send method is invoked after the stream has been closed (IsClosed returns true). It signals to callers that the peer or local side already finished the stream, so no further messages can be sent.","triggerScenarios":"Calling Send on a BidiStream-embedding stream after Close was called or the gRPC context ended; e.g. a vtctld streaming RPC handler or localvtctldclient continuing to send events after cancellation.","commonSituations":"Race between client cancellation (context done) and server Send; watchers/event streams where the consumer stops reading and closes while the producer still has buffered messages.","solutions":["Check stream.IsClosed() (or context cancellation) before each Send and exit the send loop on true","Compare the returned error against grpcshim.ErrStreamClosed with errors.Is and treat it as normal termination, not a failure","Ensure the producer goroutine observes ctx.Done() so it stops sending promptly when the peer closes"],"exampleFix":"// before\nif err := stream.Send(msg); err != nil {\n  return err\n}\n// after\nif err := stream.Send(msg); err != nil {\n  if errors.Is(err, grpcshim.ErrStreamClosed) {\n    return nil // peer closed; normal end of stream\n  }\n  return err\n}","handlingStrategy":"try-catch","validationCode":"if ctx.Err() != nil || stream.IsClosed() {\n    return nil\n}","typeGuard":"func isStreamClosed(err error) bool { return errors.Is(err, grpcshim.ErrStreamClosed) }","tryCatchPattern":"if err := stream.Send(msg); err != nil {\n    if errors.Is(err, grpcshim.ErrStreamClosed) {\n        return nil\n    }\n    return err\n}","preventionTips":["Select on ctx.Done() in send loops so producers stop as soon as the peer closes","Always treat ErrStreamClosed as normal stream termination via errors.Is","Check IsClosed() before each Send rather than caching the result"],"tags":["grpc","streaming","bidi-stream","lifecycle"],"backgroundTag":"stream-closed","analyzedSha":"01a25a7d176f94613b8d59d799f438380a8760e4","analyzedAt":"2026-09-01T17:28:30.605Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T10:18:20.063Z"}