kratos响应劫持

在我们的日常开发中,HTTP响应通常包含如下几个字段,但是kratos并没有去帮助我们封装这个响应体,而是靠我们手动构造

{
  success: true,
  code: 200,
  msg: "success",
  data: []
}

常规响应

kratos没有统一的响应格式,都是我们程序员自定义的,如果说要更换相应格式,那么我们应该对其默认的响应结构体编码进行修改

默认使用的DefaultResponseEncoder

// DefaultResponseEncoder encodes the object to the HTTP response.
func DefaultResponseEncoder(w http.ResponseWriter, r *http.Request, v interface{}) error {
        if v == nil {
                return nil
        }
        if rd, ok := v.(Redirector); ok {
                url, code := rd.Redirect()
                http.Redirect(w, r, url, code)
                return nil
        }
        codec, _ := CodecForRequest(r, "Accept")
        data, err := codec.Marshal(v)
        if err != nil {
                return err
        }
        w.Header().Set("Content-Type", httputil.ContentType(codec.Name()))
        _, err = w.Write(data)
        if err != nil {
                return err
        }
        return nil
}

自定义ResponseEncoder

// 自定义响应格式
func ResponseEncoder(w http.ResponseWriter, r *http.Request, v interface{}) error {
    if v == nil {
        return nil
    }
    if rd, ok := v.(kratos_http.Redirector); ok {
        url, code := rd.Redirect()
        http.Redirect(w, r, url, code)
        return nil
    }
    codec, _ := kratos_http.CodecForRequest(r, "Accept")
    resp := &httpResponse{
        Success: true,
        Code:    200,
        Msg:     "success",
        Data:    v,
    }
    data, err := codec.Marshal(resp)
    if err != nil {
        return err
    }
    w.Header().Set("Content-Type", "application/"+codec.Name())
    _, err = w.Write(data)
    return err
}

internal/server/http.go下添加配置

// 自定义响应体
opts = append(opts, http.ResponseEncoder(ResponseEncoder))
srv := http.NewServer(opts...)

错误响应

// 自定义错误格式
func ErrorEncoder(w http.ResponseWriter, r *http.Request, err error) {
	resp := new(httpResponse)
	// 如果能从kratos error解析出来, 则使用对应的code和message作为错误响应
	if gs, ok := status.FromError(err); ok {
		resp = &httpResponse{
			Success: false,
			Code:    kratos_status.FromGRPCCode(gs.Code()),
			Msg:     gs.Message(),
			Data:    nil,
		}
	} else {
		resp = &httpResponse{
			Success: false,
			Code:    http.StatusInternalServerError,
			Msg:     err.Error(),
			Data:    nil,
		}
	}
	codec, _ := kratos_http.CodecForRequest(r, "Accept")
	body, err := codec.Marshal(resp)
	if err != nil {
		w.WriteHeader(http.StatusInternalServerError)
		return
	}
	w.Header().Set("Content-Type", "application/"+codec.Name())
	w.WriteHeader(resp.Code) // 响应状态码
	_, _ = w.Write(body)
}

internal/server/http.go下添加配置

// 自定义错误响应
opts = append(opts, http.ErrorEncoder(ErrorEncoder))

首先先解析错误,看看是否符合grpc格式,如果符合将原本在grpc中的Code和Message写入自定义的结构体当中,如果不符合则返回默认的状态码500,并且写入响应体body当中

自定义错误状态码

https://go-kratos.dev/zh-cn/docs/component/errors/

安装kratos错误处理相关的proto包

go install github.com/go-kratos/kratos/cmd/protoc-gen-go-errors/v2@latest

api/shortvid-service/v1下新建errors.proto

syntax = "proto3";

option go_package = "shortvid-backend/api/shortvid-service/v1;v1";

import "errors/errors.proto";

enum ErrorReason {
    // 设置缺省错误码
    option (errors.default_code) = 500;
    
    // 用户相关
    USER_NOT_FOUND = 0 [(errors.code) = 404];
}

然后生成对应的客户端代码

Makefile中写入

.PHONY: errors
# errors
errors:
        protoc --proto_path=. \
         --proto_path=./third_party \
         --go_out=paths=source_relative:. \
         --go-errors_out=paths=source_relative:. \
         $(API_PROTO_FILES)

然后执行

make errors

在service层使用自定义的枚举错误常量

if userModel == nil {
    return nil, v1.ErrorUserNotFound("user not found")
}

完整代码

server/encoder.go
package server

import (
    "net/http"

    kratos_http "github.com/go-kratos/kratos/v2/transport/http"
    kratos_status "github.com/go-kratos/kratos/v2/transport/http/status"
    "google.golang.org/grpc/status"
)

type httpResponse struct {
    Success bool   `json:"success"`
    Code    int    `json:"code"`
    Msg     string `json:"msg"`
    Data    any    `json:"data,omitempty"`
}

// 自定义响应格式
func ResponseEncoder(w http.ResponseWriter, r *http.Request, v interface{}) error {
    if v == nil {
        return nil
    }
    if rd, ok := v.(kratos_http.Redirector); ok {
        url, code := rd.Redirect()
        http.Redirect(w, r, url, code)
        return nil
    }
    codec, _ := kratos_http.CodecForRequest(r, "Accept")
    resp := &httpResponse{
        Success: true,
        Code:    200,
        Msg:     "success",
        Data:    v,
    }
    data, err := codec.Marshal(resp)
    if err != nil {
        return err
    }
    w.Header().Set("Content-Type", "application/"+codec.Name())
    _, err = w.Write(data)
    return err
}

// 自定义错误格式
func ErrorEncoder(w http.ResponseWriter, r *http.Request, err error) {
    resp := new(httpResponse)
    if gs, ok := status.FromError(err); ok {
        resp = &httpResponse{
            Success: false,
            Code:    kratos_status.FromGRPCCode(gs.Code()),
            Msg:     gs.Message(),
            Data:    nil,
        }
    } else {
        resp = &httpResponse{
            Success: false,
            Code:    http.StatusInternalServerError,
            Msg:     err.Error(),
            Data:    nil,
        }
    }
    codec, _ := kratos_http.CodecForRequest(r, "Accept")
    body, err := codec.Marshal(resp)
    if err != nil {
        w.WriteHeader(http.StatusInternalServerError)
        return
    }
    w.Header().Set("Content-Type", "application/"+codec.Name())
    w.WriteHeader(resp.Code) // 响应状态码
    _, _ = w.Write(body)
}
server/http.go
package server

import (
    v1 "shortvid-backend/api/shortvid-service/v1"
    "shortvid-backend/app/shortvid-service/internal/conf"
    "shortvid-backend/app/shortvid-service/internal/service"

    "github.com/go-kratos/kratos/v2/log"
    "github.com/go-kratos/kratos/v2/middleware/recovery"
    "github.com/go-kratos/kratos/v2/transport/http"
)

// NewHTTPServer new an HTTP server.
func NewHTTPServer(c *conf.Server, users *service.UsersService, logger log.Logger) *http.Server {
    var opts = []http.ServerOption{
        http.Middleware(
            recovery.Recovery(),
        ),
    }
    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()))
    }

    // 自定义响应格式
    opts = append(opts, http.ResponseEncoder(ResponseEncoder))
    opts = append(opts, http.ErrorEncoder(ErrorEncoder))

    srv := http.NewServer(opts...)
    v1.RegisterUsersServiceHTTPServer(srv, users)
    return srv
}