grpc/grpc-go · error

failed to unmarshal, message is %T, want proto.Message

Error message

failed to unmarshal, message is %T, want proto.Message

What it means

The default gRPC proto codec's Unmarshal was given a value that is not a protobuf message. Internally, messageV2Of() only accepts values implementing protoadapt.MessageV1 (old github.com/golang/protobuf generated types) or protoadapt.MessageV2 (google.golang.org/protobuf generated types); anything else returns nil and Unmarshal rejects it. The %T in the message shows the concrete Go type that was passed, which is the fastest clue to what went wrong.

Solutions

  1. Inspect the %T in the message and replace that argument with a pointer to a code-generated proto message (e.g. *mypb.FooRequest).
  2. If you wrote the struct by hand, define a .proto file and regenerate with protoc-gen-go so the type implements proto.Message.
  3. Ensure you pass a pointer (&resp), not a value, to RecvMsg.
  4. If mixing old/new protobuf, regenerate all .pb.go with the current google.golang.org/protobuf and remove stale github.com/golang/protobuf types from the call site.
  5. If you genuinely need a non-proto wire format, register a custom encoding.Codec and do not route those types through the default proto codec.

Example fix

// before
var req MyPlainStruct
_ = stream.RecvMsg(&req)

// after
var req *mypb.MyMessage // generated by protoc-gen-go
_ = stream.RecvMsg(req)
Defensive patterns

Strategy: type-guard

Validate before calling

// Before RecvMsg/SendMsg, ensure the value is a proto.Message.
import "google.golang.org/protobuf/proto"

func mustProto(v any) proto.Message {
    if m, ok := v.(proto.Message); ok {
        return m
    }
    panic(fmt.Sprintf("not a proto.Message: %T", v))
}

var req *mypb.Foo = &mypb.Foo{}
_ = mustProto(req)

Type guard

import "google.golang.org/protobuf/proto"

func isProtoMessage(v any) bool {
    _, ok := v.(proto.Message)
    return ok
}

// usage:
if !isProtoMessage(req) {
    return fmt.Errorf("req must be a proto.Message, got %T", req)
}

Try / catch

if err := stream.RecvMsg(resp); err != nil {
    if strings.Contains(err.Error(), "want proto.Message") {
        // programming error: wrong message type; do not retry.
        return fmt.Errorf("codec type mismatch for %T: %w", resp, err)
    }
    return err
}

Prevention

When it happens

Trigger: Calling client.RecvMsg(resp) / server-side handler srv.RecvMsg(req) where resp/req is a plain Go struct (not a generated proto message), a nil pointer, a map/slice, or a value type instead of a pointer to a generated message. Also occurs with a custom codec registration that routes the wrong type into the proto codec, or when a message generated by an incompatible/old protoc-gen-go is mixed with the modern runtime.

Common situations: Passing a hand-written struct to SendMsg/RecvMsg instead of a .proto-generated type; passing a value (not a pointer) so the interface is satisfied differently; accidentally importing the deprecated github.com/golang/protobuf with an incompatible generated code version; upgrading grpc-go / protobuf major versions without regenerating .pb.go files; using a generic any-shaped placeholder during local testing.

Related errors


AI-assisted analysis of grpc/grpc-go@0c51461d27 (2026-08-11). Data as JSON: /api/errors/1c4a38032605ace9. Report an issue: GitHub.

Appendix: source

Thrown at encoding/proto/proto.go:88

		}
		data = append(data, mem.SliceBuffer(buf))
	} else {
		pool := mem.DefaultBufferPool()
		buf := pool.Get(size)
		if _, err := marshalOptions.MarshalAppend((*buf)[:0], vv); err != nil {
			pool.Put(buf)
			return nil, err
		}
		data = append(data, mem.NewBuffer(buf, pool))
	}

	return data, nil
}

func (c *codecV2) Unmarshal(data mem.BufferSlice, v any) (err error) {
	vv := messageV2Of(v)
	if vv == nil {
		return fmt.Errorf("failed to unmarshal, message is %T, want proto.Message", v)
	}

	buf := data.MaterializeToBuffer(mem.DefaultBufferPool())
	defer buf.Free()
	// TODO: Upgrade proto.Unmarshal to support mem.BufferSlice. Right now, it's not
	//  really possible without a major overhaul of the proto package, but the
	//  vtprotobuf library may be able to support this.
	return proto.Unmarshal(buf.ReadOnlyData(), vv)
}

func messageV2Of(v any) proto.Message {
	switch v := v.(type) {
	case protoadapt.MessageV1:
		return protoadapt.MessageV2Of(v)
	case protoadapt.MessageV2:
		return v
	}

View on GitHub (pinned to 0c51461d27)