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
- Call SetSendCompress before any SendHeader or Send call—ideally at the very start of the handler.
- Decide the compression algorithm based on request metadata (available before body read), not request body.
- If using interceptors, set compression in a stream interceptor before the handler runs.
- 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
- Always call SetSendCompress at the start of the handler, before any Send or SendHeader.
- Decide compression based on request metadata, not request body.
- Use a stream interceptor to set compression early and consistently.
- Review handler logic to ensure no implicit header-sending occurs before SetSendCompress.
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
- gRPC requires a ResponseWriter supporting http.Flusher
- invalid gRPC request content-type
- invalid gRPC request method
- buffer size is not an exponent of two
- cannot have a leading slash
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)