vitessio/vitess · warning
ErrStreamClosed
ErrStreamClosed
Error message
stream closed for sending
What it means
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.
Source
Thrown at go/vt/vtctl/internal/grpcshim/bidi_stream.go:31
See the License for the specific language governing permissions and
limitations under the License.
*/
package grpcshim
import (
"context"
"errors"
"io"
"sync"
"google.golang.org/grpc"
"google.golang.org/grpc/metadata"
)
// ErrStreamClosed is the error types embedding BidiStream should return when
// their Send method is called and IsClosed returns true.
var ErrStreamClosed = errors.New("stream closed for sending")
// BidiStream is a shim struct implementing both the grpc.ClientStream and
// grpc.ServerStream interfaces. It can be embedded into other types that need
// all of those methods to satisfy the compiler, but are only interested in the
// parameterized Send/Recv methods typically called by gRPC streaming servers
// and clients. For example, in the localvtctldclient:
//
// type backupStreamAdapter struct {
// *grpcshim.BidiStream
// ch chan *vtctldatapb.BackupResponse
// }
//
// func (stream *backupStreamAdapter) Recv() (*vtctldatapb.BackupResponse, error) {
// select {
// case <-stream.Context().Done():
// return nil, stream.Context().Err()
// case <-stream.Closed():
// // Stream has been closed for future sends. If there are messages thatView on GitHub (pinned to 01a25a7d17)
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
Example fix
// before
if err := stream.Send(msg); err != nil {
return err
}
// after
if err := stream.Send(msg); err != nil {
if errors.Is(err, grpcshim.ErrStreamClosed) {
return nil // peer closed; normal end of stream
}
return err
} Defensive patterns
Strategy: try-catch
Validate before calling
if ctx.Err() != nil || stream.IsClosed() {
return nil
} Type guard
func isStreamClosed(err error) bool { return errors.Is(err, grpcshim.ErrStreamClosed) } Try / catch
if err := stream.Send(msg); err != nil {
if errors.Is(err, grpcshim.ErrStreamClosed) {
return nil
}
return err
} Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- remote error: %v
- stream send error: %v
- executor is closed
- failed to load static auth plugin. Plugin configured but grp
- gRPCVtctldClient in a SHUTDOWN state
AI-assisted analysis of vitessio/vitess@01a25a7d17 (2026-09-01).
Data as JSON: /api/errors/57fb70f10a64eef5.
Report an issue: GitHub.