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

  1. Pass a pointer to a protobuf-generated message type to RecvMsg (the same type the server stream sends).
  2. If you wrote the message by hand, regenerate it from a .proto so it embeds proto.Message/ProtoReflect.
  3. Avoid generic any-typed wrappers around the stream; preserve the concrete proto type at the call site.
  4. 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

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


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)