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 that

View on GitHub (pinned to 01a25a7d17)

Solutions

  1. Check stream.IsClosed() (or context cancellation) before each Send and exit the send loop on true
  2. Compare the returned error against grpcshim.ErrStreamClosed with errors.Is and treat it as normal termination, not a failure
  3. 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

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


AI-assisted analysis of vitessio/vitess@01a25a7d17 (2026-09-01). Data as JSON: /api/errors/57fb70f10a64eef5. Report an issue: GitHub.