-
-
Notifications
You must be signed in to change notification settings - Fork 4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[Question] 请问如何设置 proto3 默认值,零值字段被忽略,导致 http api 接口字段缺失 #1952
Comments
不会啊,kratos里面默认是发射proto协议的零值,配置如下 |
因为自定义了 ResponseEncoder,kratos的encoding json会选择使用默认的encoding/json,情况类似:#1539
代码大致如下: func ResponseEncoder(w http.ResponseWriter, r *http.Request, v interface{}) error {
codec, _ := kHttp.CodecForRequest(r, "Accept")
rsp := &Response{
Code: http.StatusOK,
Message: http.StatusText(http.StatusOK),
//Data: v,
}
if m, ok := v.(proto.Message); ok {
any, err := anypb.New(m)
if err != nil {
return err
}
rsp.Data = any
}
data, err := MarshalOptions.Marshal(rsp)
if err != nil {
return err
}
w.Header().Set("Content-Type", ContentType(codec.Name()))
_, err = w.Write(data)
if err != nil {
return err
}
return nil
} 但这种方案不够优雅,同时 data 中会出现 同时思考一个问题,在项目 Response 结构已经确定,无法修改的情况下,kratos 写死结构的做法确实不够灵活,即使这是使用 google 推荐的方式,但在对接遗留的老项目、或者考虑接口兼容性,这样的情况会让大部分项目放弃使用 kratos,这种情况在 kratos 的其它模块设计中也同样存在。 |
这个是 func(w nethttp.ResponseWriter, r *nethttp.Request, v interface{}) error {
codec, _ := http.CodecForRequest(r, "Accept")
data, err := codec.Marshal(v)
if err != nil {
return err
}
w.WriteHeader(nethttp.StatusOK)
var reply *ResponseBody
if err == nil {
reply = &ResponseBody{
Code: 0,
Msg: "",
}
} else {
reply = &ResponseBody{
Code: 500,
Msg: "",
}
var target *errors.Error
if errors.As(err, target) {
reply.Code = int32(target.Code)
reply.Msg = target.Message
}
}
replyData, err := codec.Marshal(reply)
if err != nil {
return err
}
var newData = make([]byte, 0, len(replyData)+len(data)+8)
newData = append(newData, replyData[:len(replyData)-1]...)
newData = append(newData, []byte(`,"data":`)...)
newData = append(newData, data...)
newData = append(newData, '}')
w.Header().Set("Content-Type", "application/json")
_, err = w.Write(newData)
if err != nil {
return err
}
return nil
}), |
kratos并没有写死结构,是你自己的 proto 声明,proto生成出来的struct结构体本来就带省略的tag,你自己包装结构体没有使用protojson,不是本就如此吗。我觉得这是没有正确使用 proto 带来的副作用,因为你的外部结构体是强加在 proto 上的,就要承受相应的副作用问题。可以考虑拼接字符串,或者在网关上加通用的结构,通过网关去做结构上的兼容 |
感谢回复,这也是一种解决方案 |
拼接byte数组的方式已经理解,https://github.com/go-kratos/gateway 可以添加通用结构吗? 请问有比较通用的处理方式吗? |
感谢回复,最终采用了字符串拼接的方式。 |
这样子会不会好一点的呢? 只不过要多进行一次序列化 |
还是有零值问题呀 |
这个利用到pbjson库,但是前提要设置
|
20230329 网上的方法都试遍了 还是使用这个字节拼接的方式 靠谱 |
20230329 I have tried all the methods on the Internet, but I still use this byte splicing method, which is reliable |
type httpResponse struct {
Code int `json:"code"`
Message string `json:"message"`
Data interface{} `json:"data"`
}
// ApiErrorEncoder 错误响应封装
func ApiErrorEncoder() http.EncodeErrorFunc {
return func(w stdhttp.ResponseWriter, r *stdhttp.Request, err error) {
if err == nil {
return
}
se := &httpResponse{}
gs, ok := status.FromError(err)
if !ok {
se = &httpResponse{Code: stdhttp.StatusInternalServerError}
}
se = &httpResponse{
Code: httpstatus.FromGRPCCode(gs.Code()),
Message: gs.Message(),
Data: nil,
}
codec, _ := http.CodecForRequest(r, "Accept")
body, err := codec.Marshal(se)
if err != nil {
w.WriteHeader(stdhttp.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/"+codec.Name())
w.WriteHeader(se.Code)
_, _ = w.Write(body)
}
}
// ApiResponseEncoder 请求响应封装
func ApiResponseEncoder() http.EncodeResponseFunc {
return func(w stdhttp.ResponseWriter, r *stdhttp.Request, v interface{}) error {
if v == nil {
return nil
}
resp := &httpResponse{
Code: stdhttp.StatusOK,
Message: stdhttp.StatusText(stdhttp.StatusOK),
}
codec := encoding.GetCodec("json")
respData, err := codec.Marshal(resp)
if err != nil {
return err
}
data, err := codec.Marshal(v)
if err != nil {
return err
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(stdhttp.StatusOK)
_, err = w.Write(bytes.Replace(respData, []byte("null"), data, 1))
if err != nil {
return err
}
return nil
}
}```
看了一下上面还是觉得字符拼接或者替换还是更靠谱些,改成替换优雅点 |
我这里报异常: {
"code": 500,
"reason": "",
"message": "failed to marshal, message is *server.httpResponse, want proto.Message",
"metadata": {}
}
|
中间件定义看我这个就行了,嘎嘎好使。 不过有一说一,能从 "code message data" 格式中迁移出来的就尽量迁移出来吧,搞那些玩意儿干啥。每个后端的 "code message data" 的定义都不一样,连累前端挨个写 axios 中间件。都用上 kratos 了,这些老观念该抛弃就抛弃了吧。 package server
import (
v1 "code_msg_data/api/helloworld/v1"
"code_msg_data/internal/conf"
"code_msg_data/internal/service"
sj "encoding/json"
nt "net/http"
"strings"
"github.com/go-kratos/kratos/v2/encoding"
"github.com/go-kratos/kratos/v2/encoding/json"
"github.com/go-kratos/kratos/v2/errors"
"github.com/go-kratos/kratos/v2/log"
"github.com/go-kratos/kratos/v2/middleware/recovery"
"github.com/go-kratos/kratos/v2/transport/http"
)
// 最终效果
// $ curl http://localhost:8000/helloworld/kvii
// {"code":0,"message":"success","data":{"message":"Hello kvii"}}
// $ curl http://localhost:8000/helloworld/err
// {"code":404,"message":"user not found"}
// NewHTTPServer new an HTTP server.
func NewHTTPServer(c *conf.Server, greeter *service.GreeterService, logger log.Logger) *http.Server {
var opts = []http.ServerOption{
http.Middleware(
recovery.Recovery(),
),
http.ErrorEncoder(DefaultErrorEncoder), // <- 关键代码
http.ResponseEncoder(DefaultResponseEncoder), // <- 关键代码
}
if c.Http.Network != "" {
opts = append(opts, http.Network(c.Http.Network))
}
if c.Http.Addr != "" {
opts = append(opts, http.Address(c.Http.Addr))
}
if c.Http.Timeout != nil {
opts = append(opts, http.Timeout(c.Http.Timeout.AsDuration()))
}
srv := http.NewServer(opts...)
v1.RegisterGreeterHTTPServer(srv, greeter)
return srv
}
// DefaultResponseEncoder copy from http.DefaultResponseEncoder
func DefaultResponseEncoder(w http.ResponseWriter, r *http.Request, v interface{}) error {
if v == nil {
return nil
}
if rd, ok := v.(http.Redirector); ok {
url, code := rd.Redirect()
nt.Redirect(w, r, url, code)
return nil
}
codec := encoding.GetCodec(json.Name) // ignore Accept Header
data, err := codec.Marshal(v)
if err != nil {
return err
}
bs, _ := sj.Marshal(NewResponse(data))
w.Header().Set("Content-Type", ContentType(codec.Name()))
_, err = w.Write(bs)
if err != nil {
return err
}
return nil
}
// DefaultErrorEncoder copy from http.DefaultErrorEncoder.
func DefaultErrorEncoder(w http.ResponseWriter, r *http.Request, err error) {
se := FromError(errors.FromError(err)) // change error to BaseResponse
codec := encoding.GetCodec(json.Name) // ignore Accept header
body, err := codec.Marshal(se)
if err != nil {
w.WriteHeader(nt.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", ContentType(codec.Name()))
// w.WriteHeader(int(se.Code)) // ignore http status code
_, _ = w.Write(body)
}
const (
baseContentType = "application"
)
// ContentType returns the content-type with base prefix.
func ContentType(subtype string) string {
return strings.Join([]string{baseContentType, subtype}, "/")
}
func NewResponse(data []byte) BaseResponse {
return BaseResponse{
Code: 0,
Message: "success",
Data: sj.RawMessage(data),
}
}
func FromError(e *errors.Error) *BaseResponse {
if e == nil {
return nil
}
return &BaseResponse{
Code: e.Code,
Message: e.Message,
}
}
type BaseResponse struct {
Code int32 `json:"code"`
Message string `json:"message"`
Data sj.RawMessage `json:"data,omitempty"`
} |
自定义MarshalJSON package cloudapiv3
import (
"encoding/json"
"google.golang.org/protobuf/encoding/protojson"
"google.golang.org/protobuf/proto"
)
// Error 实现了
type Error struct {
Code string `json:"Code"`
Message string `json:"Message"`
}
type Response struct {
RequestId string `json:"RequestId"`
Error *Error `json:"Error,omitempty"`
Data any `json:"Data,omitempty"`
}
type CloudAPIV3Response struct {
Response *Response `json:"Response"`
}
var (
// MarshalOptions is a configurable JSON format marshaller.
MarshalOptions = protojson.MarshalOptions{
EmitUnpopulated: true,
}
)
// MarshalJSON 序列化
func (resp *Response) MarshalJSON() ([]byte, error) {
var dataRaw json.RawMessage
var err error
switch m := resp.Data.(type) {
case proto.Message:
dataRaw, err = MarshalOptions.Marshal(m)
default:
dataRaw, err = json.Marshal(m)
}
if err != nil {
return nil, err
}
return json.Marshal(&struct {
RequestId string `json:"RequestId"`
Error *Error `json:"Error,omitempty"`
Data json.RawMessage `json:"Data,omitempty"`
}{
RequestId: resp.RequestId,
Error: resp.Error,
Data: dataRaw,
})
} |
主要是在响应阶段,kratos字段如果是默认值,则整体不返回。 如果是状态码不返回,建议使用楼上的方案,制定统一状态码,重写 ResponseEncoder func encoderResponse() http.ServerOption {
return http.ResponseEncoder(func(writer nethttp.ResponseWriter, request *nethttp.Request, i interface{}) error {
reply := &zresp.HttpResponse{
Code: 200,
Msg: "success",
Data: i,
TraceId: request.Header.Get("zoro-traceId"),
}
codec := encoding.GetCodec("json")
data, err := codec.Marshal(reply)
if err != nil {
return err
}
writer.Header().Set("Content-Type", "application/json")
_, _ = writer.Write(data)
return nil
})
} 但是这样的方案并不能解决 data 内部的空值字段不返回的问题,如果要解决内部,建议 将 proto 生成的 pb 文件进行全文搜索修改,删除 omitempty 信息即可。不过就是每次生成都要修改一遍。所以可以将 protoc 做一个插件,会更加便捷。 |
@Qsr9504 对于一些需要区分零值和空值的类型,为什么不试试 Well-Known Type 里的 XxValue 呢(文档链接)?在工程的 "third_party/google/protobuf/wrappers.proto" 中已经默认附带了这些 proto 文件,直接引用就好了。就像这样。自己写个工程测一下就知道怎么用了。 import "google/protobuf/wrappers.proto"; // <- 这样导包
service Greeter {
rpc Xx (Aa) returns (Bb) {
option (google.api.http) = {
post: "/xx",
body: "*"
};
}
}
message Aa {
google.protobuf.StringValue aaa = 1; // <- 这样使用
}
message Bb {
google.protobuf.StringValue bbb = 1;
} |
请问如何设置 proto3 默认值,零值字段被忽略,导致 api 接口字段缺失。
比如用户余额接口,余额为0时(balance float64),接口余额字段缺失,这在对外提供的 http api 接口情况下,是无法接受。
请问在 kratos 中有解决方案吗?
The text was updated successfully, but these errors were encountered: