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
Raised in clientStream.RecvMsg when the message received from the dataplane stream does not implement proto.Message. The interceptor must marshal response bodies to forward them to the ext-proc server, so a non-proto message cannot be processed. This is an internal invariant of the gRPC stream plumbing.
Source
Thrown at internal/xds/httpfilter/extproc/ext_proc.go:957
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 03255a9237)
Solutions
- Ensure the channel used with the extproc interceptor uses the proto codec (the default for gRPC).
- In tests, have the fake dataplane stream return concrete proto.Message types from RecvMsg.
- Do not attach the extproc interceptor to a channel carrying non-proto payloads.
Example fix
// before (test fake): returns a plain struct
func (f *fakeStream) RecvMsg(m interface{}) error { *(m.(*plainStruct)) = plainStruct{}; return nil }
// after: return a proto.Message
func (f *fakeStream) RecvMsg(m interface{}) error { *(m.(*pb.SomeResponse)) = pb.SomeResponse{}; return nil } Defensive patterns
Strategy: type-guard
Validate before calling
// Guard before calling RecvMsg in test fakes / custom codecs.
func recvSafe(s grpc.ClientStream, m interface{}) error {
if _, ok := m.(proto.Message); !ok {
return fmt.Errorf("RecvMsg target must be proto.Message, got %T", m)
}
return s.RecvMsg(m)
} Type guard
func isProtoMessage(m interface{}) bool { _, ok := m.(proto.Message); return ok } Prevention
- Use the default proto codec on channels carrying the extproc interceptor.
- In tests, return concrete proto.Message values from fake RecvMsg.
When it happens
Trigger: clientStream.RecvMsg(m) is called and m (returned/typed by the underlying gRPC stream) is not a proto.Message. Realistically only reachable if the dataplane ClientStream is replaced with a fake whose RecvMsg produces a non-proto value, or a generic-codec stream that emits non-proto types.
Common situations: Tests substituting a mock ClientStream that returns a plain struct. Use of a custom codec (not proto) on the same channel that has the extproc interceptor attached.
Related errors
- extproc: incorrect config type provided (%T): %v
- extproc: error parsing override %v: unknown type %T, want *a
- extproc: failed to unmarshal override %v: %v
- extproc: incorrect override config type provided (%T): %v
- error parsing custom audit logger config: %v
AI-assisted analysis of grpc/grpc-go@03255a9237 (2026-08-07).
Data as JSON: /api/errors/a2b9debf446d9c89.
Report an issue: GitHub.