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

  1. Ensure the channel used with the extproc interceptor uses the proto codec (the default for gRPC).
  2. In tests, have the fake dataplane stream return concrete proto.Message types from RecvMsg.
  3. 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

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


AI-assisted analysis of grpc/grpc-go@03255a9237 (2026-08-07). Data as JSON: /api/errors/a2b9debf446d9c89. Report an issue: GitHub.