背景
我准备写一个接口,允许前端通过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¶m=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没有这个限制。