grpc/grpc-go · error
extproc: response message does not implement proto.Message
Error message
extproc: response message does not implement proto.Message
What it means
Returned by clientStream.RecvMsg (ext_proc.go:1117) when the destination message m passed to RecvMsg does not implement proto.Message. The ext_proc filter unmarshals response bodies as protobuf, so it requires the caller's receive buffer to be a generated proto message; a plain Go struct or *any is rejected.
Solutions
- Pass a pointer to a protobuf-generated message type to RecvMsg (the same type the server stream sends).
- If you wrote the message by hand, regenerate it from a .proto so it embeds proto.Message/ProtoReflect.
- Avoid generic any-typed wrappers around the stream; preserve the concrete proto type at the call site.
- Disable response_body_mode (set to NONE) if your RPCs are not protobuf-encoded.
Example fix
// before: non-proto receive buffer
var out struct{ Field string }
stream.RecvMsg(&out) // triggers: does not implement proto.Message
// after: use the generated proto type
var out mypb.MyResponse
stream.RecvMsg(&out) Defensive patterns
Strategy: type-guard
Validate before calling
// Validate the receive buffer implements proto.Message before calling RecvMsg.
func ensureProtoMessage(m any) error {
if _, ok := m.(proto.Message); !ok {
return fmt.Errorf("RecvMsg target %T does not implement proto.Message", m)
}
return nil
} Type guard
// Type guard for the RecvMsg destination.
func isProtoMessage(m any) bool {
_, ok := m.(proto.Message)
return ok
} Try / catch
if err := stream.RecvMsg(out); err != nil {
if strings.Contains(err.Error(), "does not implement proto.Message") {
// programming error: regenerate the message from .proto and pass a *T pointer
}
} Prevention
- Always generate messages from .proto and pass &mypb.MyResponse{} to RecvMsg.
- Avoid generic stream wrappers that erase the concrete proto type.
- Disable response_body_mode (NONE) for non-protobuf streaming services.
- Add a compile-time check: var _ proto.Message = (*mypb.MyResponse)(nil).
When it happens
Trigger: Triggered when the application calls RecvMsg on a stream routed through the ext_proc filter with a message that is not a proto.Message — e.g. a manually defined struct, a *[]byte, or a non-proto wrapper — while response body processing mode is GRPC and the message is taken from mutatedRespBuffer.
Common situations: Using a hand-rolled message type instead of a .pb.go generated struct, passing &someValue where someValue is not a proto, mixing the ext_proc filter with a non-protobuf streaming service, or generic helpers that accept any losing the proto type.
Related errors
- extproc: failed to unmarshal override
- external processor returned invalid body mutation in body…
- external processor returned unexpected status
- external processor returned unexpected status
- external processor sent response body after response…
AI-assisted analysis of grpc/grpc-go@0c51461d27 (2026-08-11).
Data as JSON: /api/errors/a2b9debf446d9c89.
Report an issue: GitHub.
Appendix: source
Thrown at internal/xds/httpfilter/extproc/ext_proc.go:1117
func (cs *clientStream) RecvMsg(m any) error {
// Initiate response header processing because external processor requires the
// events to be sent in the correct order, i.e. response header before
// response message. And if Header() has not already been called, send the
// response headers to external processor server first.
if err := cs.initiateResponseHeaderProcessing(); err != nil {
return err
}
// If all the responses from external processor server have been drained or if
// the external processor is bypassed, or if the response body mode is skip,
// then receive directly from the dataplane stream.
if cs.responseDrained.Load() || (cs.procStreamBypass.HasFired() && !cs.respForwardingStarted) || cs.config.processingModes.responseBodyMode == modeSkip {
return cs.recvFromDataplane(m)
}
msg, ok := m.(proto.Message)
if !ok {
return fmt.Errorf("extproc: response message does not implement proto.Message")
}
// Start the background receiving loop on the first RecvMsg call to capture
// the type of message to be received.
if !cs.respForwardingStarted {
cs.respForwardingStarted = true
go cs.responseForwardingToProcServerLoop(msg.ProtoReflect().Type())
}
// Pull response messages from mutatedRespBuffer.
select {
case streamedResp, ok := <-cs.mutatedRespBuffer.Get():
cs.mutatedRespBuffer.Load()
if !ok {
// Closed channel implies that all messages from the external processor
// have been received. Start receiving directly from dataplane stream.
cs.responseDrained.Store(true)
return cs.recvFromDataplane(m)View on GitHub (pinned to 0c51461d27)