likes
comments
collection
share

解决kratos在http下报错unsupported message type

作者站长头像
站长
· 阅读数 30

背景

我准备写一个接口,允许前端通过api查询数据。为了省事,我准备让用户可以设置一些查询条件,例如要查询的字段、一些过滤条件、排序等。

因此,我设计了一个ListItems接口,定义如下:

syntax = "proto3";

package api.items;

import "google/api/annotations.proto";

option go_package = "kratos_demo/api/items;items";
option java_multiple_files = true;
option java_package = "api.items";

service Items {
    rpc ListItems (ListItemsRequest) returns (ListItemsReply) {
        option (google.api.http) = {
            get: "/items/{collection}",
        };
    }
}

message Filter {
    string field = 1;
    string op = 2;
    string value = 3;
};

message OrderBy {
    string field = 1;
    bool desc = 2;
};

message ListItemsRequest {
    string collection = 1;
    string fields = 2;
    repeated Filter filters = 3;
    repeated OrderBy order =4;
    string group = 5;
    int32 page = 6;
    int32 page_size = 7;
}

message ListItemsReply {}

可以看到,repeated OrderBy order =4;中,order为repeated,即数组。

然后,我就开始构造url进行调用,尝试了很多种方法都失败了,这里我把失败的尝试写下来,希望大家避免踩坑。

失败案例

失败方案一:order=后接数组

localhost:8000/items/product?order=[{"field":"name", "desc":true}]

访问报错,提示:

{
    "code": 400,
    "reason": "CODEC",
    "message": "parsing list "order": unsupported message type: "api.items.OrderBy"",
    "metadata": {}
}

难道是因为order参数没有编码?

失败方案二:order=后接encodeURIComponent()后的字符串

于是我将参数调用encodeURIComponent后重新请求:

> encodeURIComponent('[{"field":"name", "desc":true}]')
> '%5B%7B%22field%22%3A%22name%22%2C%20%22desc%22%3Atrue%7D%5D'

拼接后的url为:

localhost:8000/items/product?order=%5B%7B%22field%22%3A%22name%22%2C%20%22desc%22%3Atrue%7D%5D

调用后还是报相同的错误。

然后,又试了一些其他方法,同样的失败还有:

order[]=后接参数
order[0]=后接参数

失败方案三:order[0].field="name"&order[0].desc="true"

还是不甘心,于是就问了一下文心一言:

解决kratos在http下报错unsupported message type

不得不说,文心一言还是很厉害的。(为啥不问chatgpt?你懂的!)

于是我又构造这样的url:

localhost:8000/items/product?order[0].field=name&order[0].desc=true

请求还是报错:

{
    "code": 400,
    "reason": "CODEC",
    "message": "invalid path: "order[0]" is not a message",
    "metadata": {}
}

这次报的错不一样了。

解决方案

我于是深入到代码去看问题到底出在了哪里。

在kratos->encoding->form->proto_decode.go 157行有这样一段代码:

func parseField(fd protoreflect.FieldDescriptor, value string) (protoreflect.Value, error) {
    switch fd.Kind() {
    case protoreflect.BoolKind:
        v, err := strconv.ParseBool(value)
        if err != nil {
            return protoreflect.Value{}, err
        }
        return protoreflect.ValueOfBool(v), nil
    case protoreflect.EnumKind:
        enum, err := protoregistry.GlobalTypes.FindEnumByName(fd.Enum().FullName())
        switch {
        case errors.Is(err, protoregistry.NotFound):
            return protoreflect.Value{}, fmt.Errorf("enum %q is not registered", fd.Enum().FullName())
        case err != nil:
            return protoreflect.Value{}, fmt.Errorf("failed to look up enum: %w", err)
        }
        v := enum.Descriptor().Values().ByName(protoreflect.Name(value))
        if v == nil {
            i, err := strconv.ParseInt(value, 10, 32) //nolint:gomnd
            if err != nil {
                return protoreflect.Value{}, fmt.Errorf("%q is not a valid value", value)
            }
            v = enum.Descriptor().Values().ByNumber(protoreflect.EnumNumber(i))
            if v == nil {
                return protoreflect.Value{}, fmt.Errorf("%q is not a valid value", value)
            }
        }
        return protoreflect.ValueOfEnum(v.Number()), nil
    case protoreflect.Int32Kind, protoreflect.Sint32Kind, protoreflect.Sfixed32Kind:
        v, err := strconv.ParseInt(value, 10, 32) //nolint:gomnd
        if err != nil {
            return protoreflect.Value{}, err
        }
        return protoreflect.ValueOfInt32(int32(v)), nil
    case protoreflect.Int64Kind, protoreflect.Sint64Kind, protoreflect.Sfixed64Kind:
        v, err := strconv.ParseInt(value, 10, 64) //nolint:gomnd
        if err != nil {
            return protoreflect.Value{}, err
        }
        return protoreflect.ValueOfInt64(v), nil
    case protoreflect.Uint32Kind, protoreflect.Fixed32Kind:
        v, err := strconv.ParseUint(value, 10, 32) //nolint:gomnd
        if err != nil {
            return protoreflect.Value{}, err
        }
        return protoreflect.ValueOfUint32(uint32(v)), nil
    case protoreflect.Uint64Kind, protoreflect.Fixed64Kind:
        v, err := strconv.ParseUint(value, 10, 64) //nolint:gomnd
        if err != nil {
            return protoreflect.Value{}, err
        }
        return protoreflect.ValueOfUint64(v), nil
    case protoreflect.FloatKind:
        v, err := strconv.ParseFloat(value, 32) //nolint:gomnd
        if err != nil {
            return protoreflect.Value{}, err
        }
        return protoreflect.ValueOfFloat32(float32(v)), nil
    case protoreflect.DoubleKind:
        v, err := strconv.ParseFloat(value, 64) //nolint:gomnd
        if err != nil {
            return protoreflect.Value{}, err
        }
        return protoreflect.ValueOfFloat64(v), nil
    case protoreflect.StringKind:
        return protoreflect.ValueOfString(value), nil
    case protoreflect.BytesKind:
        v, err := base64.StdEncoding.DecodeString(value)
        if err != nil {
            return protoreflect.Value{}, err
        }
        return protoreflect.ValueOfBytes(v), nil
    case protoreflect.MessageKind, protoreflect.GroupKind:
        return parseMessage(fd.Message(), value)
    default:
        panic(fmt.Sprintf("unknown field kind: %v", fd.Kind()))
    }
}

即,如果是message的类型,就会调用parseMessage方法。parseMessage方法代码如下:

func parseMessage(md protoreflect.MessageDescriptor, value string) (protoreflect.Value, error) {
    var msg proto.Message
    switch md.FullName() {
    case "google.protobuf.Timestamp":
        if value == nullStr {
            break
        }
        t, err := time.Parse(time.RFC3339Nano, value)
        if err != nil {
            return protoreflect.Value{}, err
        }
        msg = timestamppb.New(t)
    case "google.protobuf.Duration":
        if value == nullStr {
            break
        }
        d, err := time.ParseDuration(value)
        if err != nil {
            return protoreflect.Value{}, err
        }
        msg = durationpb.New(d)
    case "google.protobuf.DoubleValue":
        v, err := strconv.ParseFloat(value, 64) //nolint:gomnd
        if err != nil {
            return protoreflect.Value{}, err
        }
        msg = wrapperspb.Double(v)
    case "google.protobuf.FloatValue":
        v, err := strconv.ParseFloat(value, 32) //nolint:gomnd
        if err != nil {
            return protoreflect.Value{}, err
        }
        msg = wrapperspb.Float(float32(v))
    case "google.protobuf.Int64Value":
        v, err := strconv.ParseInt(value, 10, 64) //nolint:gomnd
        if err != nil {
            return protoreflect.Value{}, err
        }
        msg = wrapperspb.Int64(v)
    case "google.protobuf.Int32Value":
        v, err := strconv.ParseInt(value, 10, 32) //nolint:gomnd
        if err != nil {
            return protoreflect.Value{}, err
        }
        msg = wrapperspb.Int32(int32(v))
    case "google.protobuf.UInt64Value":
        v, err := strconv.ParseUint(value, 10, 64) //nolint:gomnd
        if err != nil {
            return protoreflect.Value{}, err
        }
        msg = wrapperspb.UInt64(v)
    case "google.protobuf.UInt32Value":
        v, err := strconv.ParseUint(value, 10, 32) //nolint:gomnd
        if err != nil {
            return protoreflect.Value{}, err
        }
        msg = wrapperspb.UInt32(uint32(v))
    case "google.protobuf.BoolValue":
        v, err := strconv.ParseBool(value)
        if err != nil {
            return protoreflect.Value{}, err
        }
        msg = wrapperspb.Bool(v)
    case "google.protobuf.StringValue":
        msg = wrapperspb.String(value)
    case "google.protobuf.BytesValue":
        v, err := base64.StdEncoding.DecodeString(value)
        if err != nil {
            if v, err = base64.URLEncoding.DecodeString(value); err != nil {
                return protoreflect.Value{}, err
            }
        }
        msg = wrapperspb.Bytes(v)
    case "google.protobuf.FieldMask":
        fm := &fieldmaskpb.FieldMask{}
        for _, fv := range strings.Split(value, ",") {
            fm.Paths = append(fm.Paths, jsonSnakeCase(fv))
        }
        msg = fm
    case "google.protobuf.Value":
        fm, err := structpb.NewValue(value)
        if err != nil {
            return protoreflect.Value{}, err
        }
        msg = fm
    case "google.protobuf.Struct":
        var v structpb.Struct
        if err := protojson.Unmarshal([]byte(value), &v); err != nil {
            return protoreflect.Value{}, err
        }
        msg = &v
    default:
        return protoreflect.Value{}, fmt.Errorf("unsupported message type: %q", string(md.FullName()))
    }
    return protoreflect.ValueOfMessage(msg.ProtoReflect()), nil
}

可以看到,parseMessage并不能处理message类型的,会直接进到default里面,会直接报错。

因为我的.proto定义的order参数为message类型(repeated OrderBy order),因此无法解析,一直报错。

结论:url中不能有repeated message类型

在kratos github issue里,有这样的一个回答:查看原文

Note that fields which are mapped to URL query parameters must have a primitive type or a repeated primitive type or a non-repeated message type. In the case of a repeated type, the parameter can be repeated in the URL as ...?param=A&param=B. In the case of a message type, each field of the message is mapped to a separate parameter, such as ...?foo.a=A&foo.b=B&foo.c=C.

翻译过来就是,url query参数必须为基本类型或者基本类型的数组或者非数组的message类型。

因此,要想解决文本中提到的问题,需要把order变为非数组,或者变成字符串的数组,或者干脆最好的办法,把这个请求变成一个post的吧,因为post没有这个限制。