grpc/grpc-go · error

transport: set send compressor called after headers sent or

Error message

transport: set send compressor called after headers sent or stream done

What it means

Returned by ServerStream.SetSendCompress (server_stream.go:113-114) when the caller tries to change the outbound compressor after response headers have already been sent or the stream is in the terminal streamDone state. The send compressor name travels in the initial gRPC response headers (grpc-encoding), so it can only be set before the first WriteHeader/Send. This guards against a race where mid-stream clients would never see a new grpc-encoding value.

Source

Thrown at internal/transport/server_stream.go:114

// SendCompress returns the send compressor name.
func (s *ServerStream) SendCompress() string {
	return s.sendCompress
}

// ContentSubtype returns the content-subtype for a request. For example, a
// content-subtype of "proto" will result in a content-type of
// "application/grpc+proto". This will always be lowercase.  See
// https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-HTTP2.md#requests for
// more details.
func (s *ServerStream) ContentSubtype() string {
	return s.contentSubtype
}

// SetSendCompress sets the compression algorithm to the stream.
func (s *ServerStream) SetSendCompress(name string) error {
	if s.isHeaderSent() || s.getState() == streamDone {
		return errors.New("transport: set send compressor called after headers sent or stream done")
	}

	s.sendCompress = name
	return nil
}

// SetContext sets the context of the stream. This will be deleted once the
// stats handler callouts all move to gRPC layer.
func (s *ServerStream) SetContext(ctx context.Context) {
	s.ctx = ctx
}

// ClientAdvertisedCompressors returns the compressor names advertised by the
// client via grpc-accept-encoding header.
func (s *ServerStream) ClientAdvertisedCompressors() []string {
	values := strings.Split(s.clientAdvertisedCompressors, ",")
	for i, v := range values {
		values[i] = strings.TrimSpace(v)

View on GitHub (pinned to 03255a9237)

Solutions

  1. Move the SetSendCompress call to the very start of the handler, before any Send/SendHeader and before returning the first message.
  2. If setting compression in an interceptor, use a unary/stream server interceptor that runs before the handler sends data, and set it on the stream immediately.
  3. Guard the call: check the returned error and treat it as non-fatal (log) when the stream has already progressed, or restructure so the decision is known up front.

Example fix

// before
func (s *server) Chat(stream pb.Chat_ServerStream) error {
    if err := stream.Send(&pb.Msg{...}); err != nil { return err }
    // too late: headers already sent
    _ = stream.SetSendCompress("gzip")
}
// after
func (s *server) Chat(stream pb.Chat_ServerStream) error {
    if err := stream.SetSendCompress("gzip"); err != nil { return err }
    if err := stream.Send(&pb.Msg{...}); err != nil { return err }
}
Defensive patterns

Strategy: try-catch

Try / catch

// Always set send compression at the very start of the handler; treat a late
// SetSendCompress error as non-fatal if your interceptor may run late.
func handler(stream pb.Svc_ServerStream) error {
    if err := stream.SetSendCompress("gzip"); err != nil {
        // headers already sent -> decide whether to abort or ignore
        return err
    }
    // ... handle ...
}

Prevention

When it happens

Trigger: A server handler or interceptor calls stream.SetSendCompress(name) after stream.SendHeader()/stream.SetHeader()+SendHeader, after the first stream.Send() (which implicitly sends headers), or after the RPC has finished (streamDone). Any of these flip isHeaderSent() or the state to streamDone.

Common situations: Calling SetSendCompress inside a response interceptor that runs after the handler already replied; calling it after a streaming Send; setting compression based on request data that is only known after the first message round-trip; ordering bug in a custom compressor setup.

Related errors


AI-assisted analysis of grpc/grpc-go@03255a9237 (2026-08-07). Data as JSON: /api/errors/ba12975df4bea2c6. Report an issue: GitHub.