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 proto codec's Unmarshal received a destination value that is not a proto.MessageV1 or proto.MessageV2. The receiving side of an RPC must hand the codec a generated protobuf message pointer; passing a non-proto pointer causes messageV2Of to return nil and Unmarshal to fail before decoding.

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 03255a9237)

Solutions

  1. Use the generated protobuf message pointer types (e.g. *pb.GetResponse) as the reply argument for every client call and handler return.
  2. Regenerate stubs with protoc / buf after .proto changes and rebuild the whole module.
  3. For non-proto payloads, register a custom codec and request it via content-type / ForceCodec so the proto codec is bypassed entirely.

Example fix

// before
var resp struct{ Msg string }
client.Invoke(ctx, "/svc/Do", req, &resp)

// after
var resp pb.Pong
client.Invoke(ctx, "/svc/Do", req, &resp)
Defensive patterns

Strategy: type-guard

Validate before calling

func isProtoPtr(v any) bool {
    // best-effort: check the concrete type implements protoadapt.MessageV1/V2
    switch v.(type) {
    case protoadapt.MessageV1, protoadapt.MessageV2:
        return true
    }
    return false
}

Type guard

func isUnmarshalableProto(v any) bool {
    switch v.(type) {
    case protoadapt.MessageV1, protoadapt.MessageV2:
        return true
    }
    return false
}

Try / catch

if err := proto.Unmarshal(buf, m); err != nil {
    return fmt.Errorf("unmarshal: %w", err)
}

Prevention

When it happens

Trigger: Calling a generated client method with a reply argument that is a plain *struct, *map, or *string instead of the generated *pb.SomeReply; service handler returning into the wrong type; nil reply pointer.

Common situations: Declaring local request/response structs that mirror the proto but are not the generated types; upgrading proto definitions and forgetting to regenerate stubs; passing &someConcreteStruct where the generated *pb.X was expected.

Related errors


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