在 Kratos 中设置自定义 HTTP 响应格式

在用 Kratos 写后端服务的时候,你可能已经注意到:默认情况下,Kratos 会直接把你的返回值转成 JSON 发出去,错误的话就用它自带的 errors 包处理。这其实挺方便的,但现实项目里往往没那么简单。

比如,前端同学可能会说:"能不能所有接口都返回一样的结构?这样我写拦截器和错误提示就不用猜了。"或者老系统对接时要求:"必须带 success 字段,不然我们解析不了。"

这时候,我们就得自己动手,定制一套统一的响应格式了。

可能是我打开的方式不对,网上没有现成的代码可以复制,自己摸索了一些时间,给后来人省点力气,文章末尾有代码,应该能复制调试使用,希望你不用再像我一样翻半天源码、试各种写法,最后发现原来就差一个 http.ResponseEncoder 配置。

想要这样的返回

成功的响应:

json 复制代码
{
  "code": 200,
  "message": "OK",
  "success": true,
  "data": { /* 真正的业务数据 */ }
}

出错的时候也别乱来,统一成这样:

json 复制代码
{
  "code": 400,
  "message": "参数错误",
  "success": false,
  "data": null
}

这样前端一看就知道:哦,successfalse,那肯定出问题了;message 能直接拿来 toast 提示;data 有东西就是正常数据,没东西就跳过。大家都省心!

Kratos 怎么让我们自定义

Kratos 很贴心,早就留了两个"钩子"给我们:

  • ResponseEncoder:专门处理正常返回的数据。
  • ErrorEncoder:专门处理出错的情况。

这两个东西本质上就是两个函数,我们可以告诉 Kratos:"以后成功了按这个格式包,失败了按那个格式包",它就会乖乖照做。

而且它们是在创建 HTTP Server 的时候通过配置项传进去的,非常干净,不污染业务逻辑。

具体操作

定义万能包装结构体

在我们的 server 包里,建个结构体,用来装所有返回内容:

go 复制代码
type httpUnifiedResponse struct {
	Code    int32  `json:"code"`
	Message string `json:"message"`
	Success bool   `json:"success"`
	Data    any    `json:"data"` // any 就是 interface{},啥都能塞
}

成功时的函数(ResponseEncoder)

go 复制代码
func encodeHTTPResponse(w nethttp.ResponseWriter, r *nethttp.Request, v any) error {
	// 如果我们想支持重定向(比如登录跳转),可以加个判断
	if rd, ok := v.(interface{ Redirect() (string, int) }); ok {
		url, code := rd.Redirect()
		nethttp.Redirect(w, r, url, code)
		return nil
	}

	// 正常情况:包一层再返回
	w.Header().Set("Content-Type", "application/json; charset=utf-8")
	return json.NewEncoder(w).Encode(httpUnifiedResponse{
		Code:    200,
		Message: "OK",
		Success: true,
		Data:    v, // 把我们 handler 返回的东西塞进 data
	})
}

这里的 code: 200 是业务意义上的"成功码",不是 HTTP 状态码(虽然碰巧一样)。HTTP 状态码框架默认还是 200,没问题。

出错时的函数(ErrorEncoder)

go 复制代码
func encodeHTTPError(w nethttp.ResponseWriter, r *nethttp.Request, err error) {
	// 把 error 转成 Kratos 标准错误
	se := errors.FromError(err)
	if se == nil {
		// 万一不是标准错误,兜个底
		se = errors.New(errors.UnknownCode, errors.UnknownReason, "")
	}

	w.Header().Set("Content-Type", "application/json; charset=utf-8")
	w.WriteHeader(int(se.Code)) // 这里才真正设置 HTTP 状态码(比如 400、500)

	_ = json.NewEncoder(w).Encode(httpUnifiedResponse{
		Code:    se.Code,    // 用错误里的 code
		Message: se.Message, // 错误信息
		Success: false,
		Data:    nil,
	})
}

这样一来,哪怕我们代码里 return nil, errors.BadRequest("xxx", "yyy"),前端收到的也不是一堆乱七八糟的文本,而是一个规规矩矩的 JSON!

把两个函数配置进 Server

回到我们创建 HTTP Server 的地方(比如 NewHTTPServer 函数),加上这两行配置:

go 复制代码
var opts = []http.ServerOption{
	http.PathPrefix("/api"),
	http.Middleware(
		recovery.Recovery(),
		logging.Server(logger),
	),
	http.ResponseEncoder(encodeHTTPResponse), // ← 成功走这里
	http.ErrorEncoder(encodeHTTPError),       // ← 失败走这里
	// ... 其他配置照常
}

搞定!从此以后,这个 Server 下的所有接口,都会自动套上我们定义的格式。

实际例子感受下

假设我们的 handler 是这样的:

go 复制代码
return &accessv1.LoginResponse{Token: "abc123"}, nil

前端收到的就是:

json 复制代码
{
  "code": 200,
  "message": "OK",
  "success": true,
  "data": {
    "token": "abc123"
  }
}

如果中间报错了,比如:

go 复制代码
return nil, errors.BadRequest("USER_NOT_FOUND", "用户不存在")

那前端看到的是:

json 复制代码
{
  "code": 400,
  "message": "用户不存在",
  "success": false,
  "data": null
}

而且浏览器 Network 面板里状态码也是 400,完全符合规范,前端调试起来贼舒服。

完整代码

带注释,放心抄

目录结构

text 复制代码
server/
├── grpc.go
├── http.go
├── http_encoder.go
└── server.go

1 directory, 4 files
go 复制代码
package server

import (
	"encoding/json"
	nethttp "net/http"

	"github.com/go-kratos/kratos/v2/errors"
)

type httpUnifiedResponse struct {
	Code    int32  `json:"code"`
	Message string `json:"message"`
	Success bool   `json:"success"`
	Data    any    `json:"data"`
}

func encodeHTTPResponse(w nethttp.ResponseWriter, r *nethttp.Request, v any) error {
	if rd, ok := v.(interface{ Redirect() (string, int) }); ok {
		url, code := rd.Redirect()
		nethttp.Redirect(w, r, url, code)
		return nil
	}

	w.Header().Set("Content-Type", "application/json; charset=utf-8")
	return json.NewEncoder(w).Encode(httpUnifiedResponse{
		Code:    200,
		Message: "OK",
		Success: true,
		Data:    v,
	})
}

func encodeHTTPError(w nethttp.ResponseWriter, r *nethttp.Request, err error) {
	se := errors.FromError(err)
	if se == nil {
		se = errors.New(errors.UnknownCode, errors.UnknownReason, "")
	}

	w.Header().Set("Content-Type", "application/json; charset=utf-8")
	w.WriteHeader(int(se.Code))
	_ = json.NewEncoder(w).Encode(httpUnifiedResponse{
		Code:    se.Code,
		Message: se.Message,
		Success: false,
		Data:    nil,
	})
}

200 效果:

错误效果:

相关推荐
Cache技术分享2 小时前
280. Java Stream API - Debugging Streams:如何调试 Java 流处理过程?
前端·后端
辜月十2 小时前
Conda配置文件.condarc
后端
真是他2 小时前
C# UDP 基本使用
后端
今天没有盐2 小时前
Python字符串操作全解析:从基础定义到高级格式化
后端·scala·编程语言
IT 行者2 小时前
Spring Framework 6.x 异常国际化完全指南:让错误信息“说“多国语言
java·后端·spring·异常处理·problemdetail·国际化i18n
Victor3563 小时前
Hibernate(18)Hibernate的延迟加载是什么?
后端
Victor3563 小时前
Hibernate(17)什么是Hibernate的悲观锁?
后端
一只叫煤球的猫3 小时前
并行不等于更快:CompletableFuture 让你更慢的 5 个姿势
java·后端·性能优化
Codebee3 小时前
深入揭秘Ooder框架信息架构中的钩子机制:从原理到企业级实践
后端