grpc/grpc-go · warning

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 when called after headers have already been sent to the client or the stream is done. The compression algorithm for the response is advertised in the 'grpc-encoding' header, which is sent with the initial response headers. Once headers are flushed (via SendHeader, Write, or WriteStatus), changing the compressor would be invisible to the client and corrupt the stream.

Solutions

  1. Call SetSendCompress before any SendHeader or Send call—ideally at the very start of the handler.
  2. Decide the compression algorithm based on request metadata (available before body read), not request body.
  3. If using interceptors, set compression in a stream interceptor before the handler runs.
  4. Restructure handler logic: read all input first (without sending), choose compression, then send.

Example fix

// before: SetSendCompress after Send
func (s *server) Stream(stream MyService_StreamServer) error {
    stream.Send(&Response{...}) // headers sent here
    stream.SetSendCompress("gzip") // error!
}
// after: set compression before any Send
func (s *server) Stream(stream MyService_StreamServer) error {
    stream.SetSendCompress("gzip") // before headers
    stream.Send(&Response{...})
}
Defensive patterns

Strategy: validation

Validate before calling

// Call SetSendCompress before any headers are sent.
// Validate stream state before setting compression.
func safeSetSendCompress(stream grpc.ServerStream, name string) error {
    // Must be called before SendHeader or any Send
    return stream.SetSendCompress(name)
}
// Usage: call at the very start of the handler, before any Send.

Try / catch

err := stream.SetSendCompress("gzip")
if err != nil && strings.Contains(err.Error(), "after headers sent") {
    // too late; compression already negotiated in headers
    log.Print("SetSendCompress called too late; set before first Send")
}

Prevention

When it happens

Trigger: Calling stream.SetSendCompress(name) after calling stream.SendHeader(), stream.Send() (which implicitly sends headers), or after the stream has completed (streamDone). The guard at server_stream.go:113 checks isHeaderSent() and getState()==streamDone.

Common situations: Server interceptor or handler calling SetSendCompress late in the request lifecycle; logic that decides compression based on the request body (read after headers are sent); ordering bug where SetSendCompress is called after the first SendMsg; middleware that sets compression conditionally but too late.

Related errors


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

Appendix: 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 0c51461d27)