{"record":{"id":"ba12975df4bea2c6","repo":"grpc/grpc-go","slug":"transport-set-send-compressor-called-after-header","errorCode":null,"errorMessage":"transport: set send compressor called after headers sent or stream done","messagePattern":"transport: set send compressor called after headers sent or stream done","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/transport/server_stream.go","lineNumber":114,"sourceCode":"\n// SendCompress returns the send compressor name.\nfunc (s *ServerStream) SendCompress() string {\n\treturn s.sendCompress\n}\n\n// ContentSubtype returns the content-subtype for a request. For example, a\n// content-subtype of \"proto\" will result in a content-type of\n// \"application/grpc+proto\". This will always be lowercase.  See\n// https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-HTTP2.md#requests for\n// more details.\nfunc (s *ServerStream) ContentSubtype() string {\n\treturn s.contentSubtype\n}\n\n// SetSendCompress sets the compression algorithm to the stream.\nfunc (s *ServerStream) SetSendCompress(name string) error {\n\tif s.isHeaderSent() || s.getState() == streamDone {\n\t\treturn errors.New(\"transport: set send compressor called after headers sent or stream done\")\n\t}\n\n\ts.sendCompress = name\n\treturn nil\n}\n\n// SetContext sets the context of the stream. This will be deleted once the\n// stats handler callouts all move to gRPC layer.\nfunc (s *ServerStream) SetContext(ctx context.Context) {\n\ts.ctx = ctx\n}\n\n// ClientAdvertisedCompressors returns the compressor names advertised by the\n// client via grpc-accept-encoding header.\nfunc (s *ServerStream) ClientAdvertisedCompressors() []string {\n\tvalues := strings.Split(s.clientAdvertisedCompressors, \",\")\n\tfor i, v := range values {\n\t\tvalues[i] = strings.TrimSpace(v)","sourceCodeStart":96,"sourceCodeEnd":132,"githubUrl":"https://github.com/grpc/grpc-go/blob/03255a9237b6eb32710f6bc4f2de9a675b99fe36/internal/transport/server_stream.go#L96-L132","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\nfunc (s *server) Chat(stream pb.Chat_ServerStream) error {\n    if err := stream.Send(&pb.Msg{...}); err != nil { return err }\n    // too late: headers already sent\n    _ = stream.SetSendCompress(\"gzip\")\n}\n// after\nfunc (s *server) Chat(stream pb.Chat_ServerStream) error {\n    if err := stream.SetSendCompress(\"gzip\"); err != nil { return err }\n    if err := stream.Send(&pb.Msg{...}); err != nil { return err }\n}","handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"// Always set send compression at the very start of the handler; treat a late\n// SetSendCompress error as non-fatal if your interceptor may run late.\nfunc handler(stream pb.Svc_ServerStream) error {\n    if err := stream.SetSendCompress(\"gzip\"); err != nil {\n        // headers already sent -> decide whether to abort or ignore\n        return err\n    }\n    // ... handle ...\n}","preventionTips":["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."],"tags":["transport","compression","server-stream","grpc","streaming"],"analyzedSha":"03255a9237b6eb32710f6bc4f2de9a675b99fe36","analyzedAt":"2026-08-07T00:29:34.215Z","schemaVersion":2},"datasetVersion":"2026-08-07T03:17:09.362Z"}