【grpc】12.grpc-gw自定义选项
# 消息序列化
# 1. Custom serializer 自定义序列化器
有时候可能希望在 MessagePack 而不是 JSON 中序列化请求/响应消息
var m your.MsgPackMarshaler mux := runtime.NewServeMux( runtime.WithMarshalerOption("application/x-msgpack", m), )
1
2
3
4默认配置参考
runtime v2
runtime.WithMarshalerOption( runtime.MIMEWildcard, &runtime.JSONPb{ MarshalOptions: protojson.MarshalOptions{ EmitUnpopulated: true, UseEnumNumbers: true, }, }, ),
1
2
3
4
5
6
7
8
9runtime.JSONPb
// JSONPb is a Marshaler which marshals/unmarshals into/from JSON // with the "google.golang.org/protobuf/encoding/protojson" marshaler. // It supports the full functionality of protobuf unlike JSONBuiltin. // // The NewDecoder method returns a DecoderWrapper, so the underlying // *json.Decoder methods can be used. type JSONPb struct { protojson.MarshalOptions protojson.UnmarshalOptions }
1
2
3
4
5
6
7
8
9
10protojson.MarshalOptions
// Multiline specifies whether the marshaler should format the output in // indented-form with every textual element on a new line. // If Indent is an empty string, then an arbitrary indent is chosen. // 指定了marshaler是否应该以缩进的形式来格式化输出,每个文本元素都在一个新的行上。如果缩进是一个空字符串,那么将选择一个任意的缩进。 Multiline bool // Indent specifies the set of indentation characters to use in a multiline // formatted output such that every entry is preceded by Indent and // terminated by a newline. If non-empty, then Multiline is treated as true. // Indent can only be composed of space or tab characters. //指定了在多行格式化输出中使用的缩进字符集,这样每个条目都以缩进为前导,以换行为结束。如果非空,则多行被视为真。缩进只能由空格或制表符组成。 Indent string // AllowPartial allows messages that have missing required fields to marshal // without returning an error. If AllowPartial is false (the default), // Marshal will return error if there are any missing required fields. // 允许在不返回错误的情况下对缺少必填字段的邮件进行处理。如果AllowPartial为false(默认),如果有任何缺失的必填字段,Marshal将返回错误。 AllowPartial bool // UseProtoNames uses proto field name instead of lowerCamelCase name in JSON // field names. // 在JSON字段名中使用proto字段名而不是lowerCamelCase名称。 UseProtoNames bool // UseEnumNumbers emits enum values as numbers. // 将枚举值作为数字返回 UseEnumNumbers bool // EmitUnpopulated specifies whether to emit unpopulated fields. It does not // emit unpopulated oneof fields or unpopulated extension fields. // The JSON value emitted for unpopulated fields are as follows: // 指定是否返回未填充的字段。它不返回未填充的oneof字段或未填充的扩展字段。未填充字段的JSON值如下: // ╔═══════╤════════════════════════════╗ // ║ JSON │ Protobuf field ║ // ╠═══════╪════════════════════════════╣ // ║ false │ proto3 boolean fields ║ // ║ 0 │ proto3 numeric fields ║ // ║ "" │ proto3 string/bytes fields ║ // ║ null │ proto2 scalar fields ║ // ║ null │ message fields ║ // ║ [] │ list fields ║ // ║ {} │ map fields ║ // ╚═══════╧════════════════════════════╝ EmitUnpopulated bool // Resolver is used for looking up types when expanding google.protobuf.Any // messages. If nil, this defaults to using protoregistry.GlobalTypes. Resolver interface { protoregistry.ExtensionTypeResolver protoregistry.MessageTypeResolver }
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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51protojson.UnmarshalOptions
// If AllowPartial is set, input for messages that will result in missing // required fields will not return an error. // 如果设置了AllowPartial,对信息的输入将导致缺少必要的字段,将不会返回错误。 AllowPartial bool // If DiscardUnknown is set, unknown fields are ignored. // 如果设置了DiscardUnknown,未知字段将被忽略。 DiscardUnknown bool // Resolver is used for looking up types when unmarshaling // google.protobuf.Any messages or extension fields. // If nil, this defaults to using protoregistry.GlobalTypes. Resolver interface { protoregistry.MessageTypeResolver protoregistry.ExtensionTypeResolver }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 2. 在 JSON 中使用原型名称
protocol buffer 编译器会生成默认使用的camelCase JSON标签。如果你想使用proto文件中使用的确切大小写,设置
UseProtoNames: true
:mux := runtime.NewServeMux( runtime.WithMarshalerOption(runtime.MIMEWildcard, &runtime.JSONPb{ MarshalOptions: protojson.MarshalOptions{ UseProtoNames: true, }, UnmarshalOptions: protojson.UnmarshalOptions{ DiscardUnknown: true, }, }), )
1
2
3
4
5
6
7
8
9
10
# 3. 根据Content-Type 自定义反序列化器
mux := runtime.NewServeMux(
runtime.WithMarshalerOption("application/json+strict", &runtime.JSONPb{
UnmarshalOptions: &protojson.UnmarshalOptions{
DiscardUnknown: false, // explicit "false", &protojson.UnmarshalOptions{} would have the same effect
},
}),
)
2
3
4
5
6
7
# http请求头和gRPC client metadata映射
如果你不喜欢默认的映射规则,或者你想传输全部的请求头,grpc-gw支持自定义映射规则
https://pkg.go.dev/github.com/grpc-ecosystem/grpc-gateway/runtime#DefaultHeaderMatcher
// DefaultHeaderMatcher is used to pass http request headers to/from gRPC context. This adds permanent HTTP header // keys (as specified by the IANA) to gRPC context with grpcgateway- prefix. HTTP headers that start with // 'Grpc-Metadata-' are mapped to gRPC metadata after removing prefix 'Grpc-Metadata-'. func DefaultHeaderMatcher(key string) (string, bool) { key = textproto.CanonicalMIMEHeaderKey(key) if isPermanentHTTPHeader(key) { return MetadataPrefix + key, true } else if strings.HasPrefix(key, MetadataHeaderPrefix) { return key[len(MetadataHeaderPrefix):], true } return "", false }
1
2
3
4
5
6
7
8
9
10
11
12
自定义
HeaderMatcherFunc
使用
WithIncomingHeaderMatcher
注册映射函数func CustomMatcher(key string) (string, bool) { switch key { case "X-Custom-Header1": return key, true case "X-Custom-Header2": return "custom-header2", true default: return key, false } } mux := runtime.NewServeMux( runtime.WithIncomingHeaderMatcher(CustomMatcher), )
1
2
3
4
5
6
7
8
9
10
11
12
13
14要保留默认映射规则和您自己的规则, 可以这么写
func CustomMatcher(key string) (string, bool) { switch key { case "X-User-Id": return key, true default: return runtime.DefaultHeaderMatcher(key) } }
1
2
3
4
5
6
7
8
# gRPC server metadata到http响应头 映射
grpc-gw默认:https://pkg.go.dev/github.com/grpc-ecosystem/grpc-gateway/runtime#WithOutgoingHeaderMatcher
// WithOutgoingHeaderMatcher returns a ServeMuxOption representing a headerMatcher for outgoing response from gateway. // // This matcher will be called with each header in response header metadata. If matcher returns true, that header will be // passed to http response returned from gateway. To transform the header before passing to response, // matcher should return modified header. func WithOutgoingHeaderMatcher(fn HeaderMatcherFunc) ServeMuxOption { return func(mux *ServeMux) { mux.outgoingHeaderMatcher = fn } }
1
2
3
4
5
6
7
8
9
10自定义
if appendCustomHeader { grpc.SendHeader(ctx, metadata.New(map[string]string{ "x-custom-header1": "value", })) }
1
2
3
4
5
# 改变响应消息或设置响应标头
# 1. 设置 HTTP headers
demo
func myFilter(ctx context.Context, w http.ResponseWriter, resp proto.Message) error { t, ok := resp.(*externalpb.Tokenizer) if ok { w.Header().Set("X-My-Tracking-Token", t.Token) t.Token = "" } return nil } mux := runtime.NewServeMux( runtime.WithForwardResponseOption(myFilter), )
1
2
3
4
5
6
7
8
9
10
11
# 2. 控制 HTTP 响应状态代码
demo
_ = grpc.SetHeader(ctx, metadata.Pairs("x-http-code", "401")) ... func httpResponseModifier(ctx context.Context, w http.ResponseWriter, p proto.Message) error { md, ok := runtime.ServerMetadataFromContext(ctx) if !ok { return nil } // set http status code if vals := md.HeaderMD.Get("x-http-code"); len(vals) > 0 { code, err := strconv.Atoi(vals[0]) if err != nil { return err } // delete the headers to not expose any grpc-metadata in http response delete(md.HeaderMD, "x-http-code") delete(w.Header(), "Grpc-Metadata-X-Http-Code") w.WriteHeader(code) } return nil } ... gwMux := runtime.NewServeMux( runtime.WithForwardResponseOption(httpResponseModifier), )
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
27
# 3. 修改默认响应body和error
grpc-gw 出错时默认返回格式
{ "code":5, "message":"Not Found", "details":[] }
1
2
3
4
5
# 3.1 方案一
将返回的内容定义为每条消息中回复的一部分
message HelloWorldWrapper { int code = 1; HelloWorldResponse data = 2; string error = 3; } message HelloWorldResponse { string key = 1; }
1
2
3
4
5
6
7
8
9缺点是大量的proto api需要冗余定义
# 3.2 方案二
核心函数
runtime.WithForwardResponseOption
只会在正常响应的时候调用runtime.WithErrorHandler
该函数只要请求就会调用,且在正常请求runtime.WithForwardResponseOption
之后调用
代码示例
custom res
type StandardResp struct { Code int `json:"code"` Data interface{} `json:"data"` Error string `json:"error"` } const ( proxyFlag = "__success__" ) func HttpSuccessHandler(ctx context.Context, w http.ResponseWriter, p proto.Message) error { fmt.Print("111111111111111") resp := StandardResp{ Code: 0, Data: p, Error: "", } bs, _ := json.Marshal(&resp) return errors.New(proxyFlag + string(bs)) } func HttpErrorHandler(ctx context.Context, mux *runtime.ServeMux, m runtime.Marshaler, w http.ResponseWriter, r *http.Request, err error) { fmt.Print("00000000000000") w.Header().Set("Content-Type", "application/json") // success proxy raw := err.Error() if strings.HasPrefix(raw, proxyFlag) { raw = raw[len(proxyFlag):] w.Write([]byte(raw)) return } // normal error s, ok := status.FromError(err) if !ok { s = status.New(codes.Unknown, err.Error()) } resp := StandardResp{ Code: 1, Data: nil, Error: s.Message(), } bs, _ := json.Marshal(&resp) w.Write(bs) }
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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47mux
// gateway gwMux := runtime.NewServeMux( runtime.WithForwardResponseOption(HttpSuccessHandler), runtime.WithErrorHandler(HttpErrorHandler), )
1
2
3
4
5
# 自定义错误返回
# 1. 定义error response 格式
demo
type StandardResp struct { Code int `json:"code"` Data interface{} `json:"data"` Error string `json:"error"` } const ( proxyFlag = "__success__" ) func HttpSuccessHandler(ctx context.Context, w http.ResponseWriter, p proto.Message) error { fmt.Print("111111111111111") resp := StandardResp{ Code: 0, Data: p, Error: "", } bs, _ := json.Marshal(&resp) return errors.New(proxyFlag + string(bs)) } func HttpErrorHandler(ctx context.Context, mux *runtime.ServeMux, m runtime.Marshaler, w http.ResponseWriter, r *http.Request, err error) { fmt.Print("00000000000000") w.Header().Set("Content-Type", "application/json") // success proxy raw := err.Error() if strings.HasPrefix(raw, proxyFlag) { raw = raw[len(proxyFlag):] w.Write([]byte(raw)) return } // normal error s, ok := status.FromError(err) if !ok { s = status.New(codes.Unknown, err.Error()) } resp := StandardResp{ Code: 1, Data: nil, Error: s.Message(), } bs, _ := json.Marshal(&resp) w.Write(bs) } ... // gateway gwMux := runtime.NewServeMux( runtime.WithForwardResponseOption(HttpSuccessHandler), runtime.WithErrorHandler(HttpErrorHandler), )
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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
# 2. 自定义路由错误处理
在
*runtime.ServeMux
由于路由问题而无法为请求提供服务时定义错误行为,请使用runtime.WithRoutingErrorHandler
通过这个错误处理程序,将配置所有的HTTP路由错误。默认行为是将HTTP错误代码映射到gRPC错误,HTTP 状态及其到 gRPC 状态的映射:
- HTTP
404 Not Found
-> gRPC5 NOT_FOUND
- HTTP
405 Method Not Allowed
-> gRPC12 UNIMPLEMENTED
- HTTP
400 Bad Request
-> gRPC3 INVALID_ARGUMENT
- HTTP
demo
func handleRoutingError(ctx context.Context, mux *ServeMux, marshaler Marshaler, w http.ResponseWriter, r *http.Request, httpStatus int) { if httpStatus != http.StatusMethodNotAllowed { runtime.DefaultRoutingErrorHandler(ctx, mux, marshaler, writer, request, httpStatus) return } // Use HTTPStatusError to customize the DefaultHTTPErrorHandler status code err := &HTTPStatusError{ HTTPStatus: httpStatus Err: status.Error(codes.Unimplemented, http.StatusText(httpStatus)) } runtime.DefaultHTTPErrorHandler(ctx, mux, marshaler, w , r, err) } .... mux := runtime.NewServeMux( runtime.WithRoutingErrorHandler(handleRoutingError), )
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 获取 HTTP Path pattern
proto
syntax = "proto3"; option go_package = "github.com/grpc-ecosystem/grpc-gateway/v2/examples/internal/proto/examplepb"; package grpc.gateway.examples.internal.proto.examplepb; import "google/api/annotations.proto"; service LoginService { rpc Login (LoginRequest) returns (LoginReply) { option (google.api.http) = { post: "/v1/example/login" body: "*" }; } } message LoginRequest {} message LoginReply {}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18main.go
mux := runtime.NewServeMux( runtime.WithMetadata(func(ctx context.Context, r *http.Request) metadata.MD { md := make(map[string]string) if method, ok := runtime.RPCMethod(ctx); ok { md["method"] = method // /grpc.gateway.examples.internal.proto.examplepb.LoginService/Login } if pattern, ok := runtime.HTTPPathPattern(ctx); ok { md["pattern"] = pattern // /v1/example/login } return metadata.New(md) }), )
1
2
3
4
5
6
7
8
9
10
11
12