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
- Move the SetSendCompress call to the very start of the handler, before any Send/SendHeader and before returning the first message.
- 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.
- 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
- Call SetSendCompress before any Send, SendHeader, or returning the first message.
- Prefer registering compressors via encoding.RegisterCompressor at startup and using WithDefaultCallOptions(UseCompressor(...)) over per-stream mutation.
- If configuring in an interceptor, ensure it runs before the handler emits data.
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
- invalid gRPC request method %q
- invalid gRPC request content-type %q
- grpc: invalid gzip compression level: %d
- received %d-bytes data exceeding the limit %d bytes
- lrs: failed to receive first LoadStatsResponse: %v
AI-assisted analysis of grpc/grpc-go@03255a9237 (2026-08-07).
Data as JSON: /api/errors/ba12975df4bea2c6.
Report an issue: GitHub.