解决kratos在http下报错unsupported message type

背景

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

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

protobuf 复制代码
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=后接数组

url 复制代码
localhost:8000/items/product?order=[{"field":"name", "desc":true}]

访问报错,提示:

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

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

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

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

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

拼接后的url为:

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

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

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

css 复制代码
order[]=后接参数
order[0]=后接参数

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

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

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

于是我又构造这样的url:

url 复制代码
localhost:8000/items/product?order[0].field=name&order[0].desc=true

请求还是报错:

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

这次报的错不一样了。

解决方案

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

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

go 复制代码
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方法代码如下:

go 复制代码
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没有这个限制。

相关推荐
却尘17 小时前
一篇小白也能看懂的 Go 字符串拼接 & Builder & cap 全家桶
后端·go
ん贤17 小时前
一次批量删除引发的死锁,最终我选择不加锁
数据库·安全·go·死锁
mtngt111 天前
AI DDD重构实践
go
Grassto3 天前
12 go.sum 是如何保证依赖安全的?校验机制源码解析
安全·golang·go·哈希算法·go module
Grassto4 天前
11 Go Module 缓存机制详解
开发语言·缓存·golang·go·go module
程序设计实验室5 天前
2025年的最后一天,分享我使用go语言开发的电子书转换工具网站
go
我的golang之路果然有问题5 天前
使用 Hugo + GitHub Pages + PaperMod 主题 + Obsidian 搭建开发博客
golang·go·github·博客·个人开发·个人博客·hugo
啊汉7 天前
古文观芷App搜索方案深度解析:打造极致性能的古文搜索引擎
go·软件随想
asaotomo8 天前
一款 AI 驱动的新一代安全运维代理 —— DeepSentry(深哨)
运维·人工智能·安全·ai·go
码界奇点8 天前
基于Gin与GORM的若依后台管理系统设计与实现
论文阅读·go·毕业设计·gin·源代码管理