kratos 的proto http 插件——protoc-gen-go-http,对body以及query参数只能选择其一支持,不论其Method为何种。query、vars支持同时存在。

源码可查:go-kratos/kratos/cmd/protoc-gen-go-http/template.go

Google API 设计指南上对方法的设计有点不一样。

GET POST PUT DELETE
query (/hello?x=s) 支持( 不与body同时存在,后面同 ) 支持 支持 支持
vars (/hello/{name}) 支持( 不与body同时存在,后面同 ) 支持 支持 支持
body 支持( 不与query同时存在,后面同 ) 支持 支持 支持

若当前工具不满足当下需求,可以选择以下两种方式进行解决。

解决方式

开发者自定义

对有需要query以及body的接口,不使用annotations注释对应接口,也就是不让工具生成胶水代码,由开发复制类似的胶水代码,在代码中自行写入HTTP router。

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
func _Greeter_PostHello0_HTTP_Handler(srv GreeterHTTPServer) func(ctx http.Context) error {
return func(ctx http.Context) error {
var in HelloRequest

// 在此处加入自定义的解析代码,其他代码修改极少
#########################################################
if err := ctx.BindQuery(&in); err != nil {
return err
}
if err := ctx.BindVars(&in); err != nil {
return err
}
##############################################################

http.SetOperation(ctx, "/helloworld.Greeter/PostHello")
h := ctx.Middleware(func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.PostHello(ctx, req.(*HelloRequest))
})
out, err := h(ctx, &in)
if err != nil {
return err
}
reply := out.(*HelloReply)
return ctx.Result(200, reply)
}
}

修改工具

protoc-gen-go-http的template中,对数据参数进行过判断,也就是我们需要把这里的判断删除,修改这个工具。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 让其不要是if else 判断,而是if, if 并行判断。
var in {{.Request}}
{{- if .HasBody}}
if err := ctx.Bind(&in{{.Body}}); err != nil {
return err
}

{{- if not (eq .Body "")}}
if err := ctx.BindQuery(&in); err != nil {
return err
}
{{- end}}
{{- else}}
if err := ctx.BindQuery(&in{{.Body}}); err != nil {
return err
}
{{- end}}
{{- if .HasVars}}
if err := ctx.BindVars(&in); err != nil {
return err
}
{{- end}}

由于这个工具是独立于Kratos存在的,所以,即使上游出现变动,也并不影响我们使用Kratos

Header中解析数据

1
2
3
4
5
6
7
// client发上来的header保存在此处
transport.FromClientContext(ctx)

transport.FromServerContext(ctx)

和"google.golang.org/grpc/metadata"中的函数同:
FromIncomingContext、FromOutgoingContext

请求中的参数解析

从test文件的示例可以看出,github.com/go-kratos/kratos/encoding/form/form_test.go,解析URL中的嵌套数据的方法:

嵌套参数

1
2
3
4
5
6
7
message HelloRequest {
string name = 1;
message test {
string one = 1;
}
test ones = 2;
}

URL Query:

1
http://localhost:8000/helloworld?ones.one=1234

数组参数

1
2
3
Simples: []string{"3344", "5566"}

simples=3344&simples=5566

FieldMask参数

1
2
3
Field:     &fieldmaskpb.FieldMask{Paths: []string{"1", "2"}}

field=1,2

FieldMask借助库:

https://github.com/mennanov/fmutils

https://github.com/mennanov/fieldmask-utils

不过当前Kratos对query中的fieldmask会进行大写转换,导致其字段无法进行有效的Filter。我提了一个issue

fieldmask_utils是对字段名进行匹配,而不是tag名,而Kratos则会将Me ——> _me,而其字段为Me,那库则无法将其正常过滤,正常GRPC协议不会出现此转换,此转换应该是Kratos HTTP decode request时发生的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import fieldmask_utils "github.com/mennanov/fieldmask-utils"

// 可以对fieldmask做一下中间拦截,也可以直接返回
func naming(s string) string {
if s == "foo" {
return "Foo"
}
return s
}

func main () {
var request UpdateUserRequest
userDst := &testproto.User{} // a struct to copy to
mask, _ := fieldmask_utils.MaskFromPaths(request.FieldMask.Paths, naming)
fieldmask_utils.StructToStruct(mask, request.User, userDst)
// Only the fields mentioned in the field mask will be copied to userDst, other fields are left intact
}

FieldMask用在response、request参数限制返回,以及指定参数的更新上。

Netflix API 设计实践: 使用FieldMask

request中指定paths,response根据paths mask参数,返回需要的字段。

Netflix API 设计实践(二): 使用FieldMask进行数据变更

1
2
3
4
5
6
7
8
9
10
11
12
13
14
message UpdateProductionRequest {
ProductionUpdateOperation update = 1;
google.protobuf.FieldMask update_mask = 2;
}

{
"update": {
"format": "test"
}
"update_mask": [
"format",
"schedule.planned_launch_date",
]
}

更新操作就会执行更新formatschedule.planned_launch_date两个字段,由于后者没有传值,变相的也就是将此字段置空。