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
- Inspect the %T in the message and replace that argument with a pointer to a code-generated proto message (e.g. *mypb.FooRequest).
- If you wrote the struct by hand, define a .proto file and regenerate with protoc-gen-go so the type implements proto.Message.
- Ensure you pass a pointer (&resp), not a value, to RecvMsg.
- 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.
- 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
- Always pass pointers to protoc-gen-go-generated types to SendMsg/RecvMsg.
- Keep a single protobuf runtime (google.golang.org/protobuf) and regenerate all .pb.go on upgrades.
- In tests, assert req implements proto.Message before the first RPC.
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
- extauthz: failed to unmarshal config
- extauthz: failed to unmarshal override config
- rls_csp: error parsing config
- bad resolver state
- cannot register a nil Codec
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)