Golang HTTP server中的error propagation
Preface
错误处理是程序设计的重要环节,理想的错误处理应为开发人员提供详细的context,并记录日志;它排查问题的重要信息源。并能为用户提供恰到好处的信息: 适当抽象,隐藏程序细节。最好能提供解决建议,或者reference id
, 协助开发人员缩小排查范围。
特别的, 如何在HTTP server中建立正确的error处理机制, 并能自动生成符合HTTP语义的response呢?
本文提出一个简单解决方案: 提供error的stack trace, 并能将error转换为HTTP error.
Error propagation
软件模块可能嵌套较多层级,一个error由low-level模块产生,一级级的传递到上层被处理,再反馈给用户,称为error propagation, 像环环相扣的链条. 即error以逆调用链方向传递(称为Wrap, 每层包裹一些context, 再传递出去),并记录如代码位置, 函数名等关键信息,这样的error stack能还原出错时代码执行路径,对理解问题至关重要.
想想如果没有调用栈, 仅凭一条error message, 且往往是内部库函数产生的error,被很多代码调用, 根本无法理解程序执行路径.
Java有完善的Error handling, 能记录error stack:
另一个问题: 在HTTP Server场景,为遵循HTTP标准,还需要考虑出error与HTTP response的转换: 涉及到定制HTTP status code, response header 即response body.
Error handling in Golang
Golang自诞生来,因其极简的设计,内部对error propagation并无支持. (直到Go 1.13加入初步支持,远非完美, 前身是golang.org/x/xerrors
) 但有成熟的三方库可采用,如github.com/pkg/errors
和github.com/juju/errors
, 但error propagation思路大同小异.
Error propagation的套路便是由错误源头开始,在向上层传递错误时,层层”包裹”, 附加context. 而在错误处理层, 又需要解开包裹,取出错误链条的第一个Error, 即错误源头,从而区分处理不同error。
一个函数调用链以源错误为开端,层层返回给调用层,同时附加调用栈和额外message,这个简单的模型足以处理一般系统的错误。
因此error库至少需要两个函数Wrap
和Cause
: Wrap
用来包装error: 添加context形成新error;Cause
, 也成为Unwrap则用于取的被包裹的原始错误。
接下来我们来看看如何用github.com/juju/errors
来实现error propagation并且转换为HTTP response.
juju/errors
juju/errors, github收获千星,提供了简单的error Unwrap逻辑,并内置了一些标准HTTP错误类型。
因为我们想做一些额外定制: 区分是否Server端错误,附加error时HTTP response header等,仅用到juju/errors
的Wrap和Cause函数, 并没有使用其定义的错误类型,而选择我们自己定义能对应到HTTP标准错误的类型。
在juju/errors
中
新建error, 替代标准errors函数,新建的error会记录call stack.
1 | New(message string) error |
Wrap error:
1 | Trace(other error) error |
Trace仅Wrap, Annotate会增加额外message.
1 | func TestJuju(t *testing.T) { |
输出
1 | errors_propagation/errors/juju_test.go:11: root cause |
可见Error stack打印出带有函数行数的调用链。
打印Cause:
1 | fmt.Printf("Cause is %+v\n", errors.Cause(err)) |
输出root cause:
1 | errors_propagation/errors/juju_test.go:11: root cause |
可见用errors.New创建的error,带有context信息, 而非juju/errors
创建的信息则没有, 可以用errors.Trace
进行wrap.
测试代码可以在这里找到.
error转换为HTTP error response
一般情况是: HTTP server的handler layer接到请求,转入business logic; 层层调用之后,某处报错,错误再返回到handler层,这时需要将错误转换为符合HTTP语义的response. 如: 权限不足返回403
, 数据未找到返回404
, 程序崩溃返回500
.
有了Error propagation为基础,我们可以提取root cause, 我们需要定义一系列标准HTTP error, 能从error中提取HTTP status code, response header及error message.
以Status code为例,我们自定义错误需要实现HTTPStatus() int
interface, 错误处理模块根据interface转换来判断返回的错误是否能提取status code, 同理还有是否是Service Failure等其他功能.
例如404错误,我们定义:
1 | type BadRequest struct { |
处理错误时, 利用type conversion来判断是否是我们定义的标准HTTP错误, 既能提取status code.
1 | if hs, ok := errors.Cause(err).(HasHTTPStatus); ok { |
同样,我们可以实现HTTP response header等其他功能.
Put it together
Error转换为HTTP response机制需要在所有HTTP handler共享, HTTP handler仅需将返回的error传入转换代码,转换模块负责写HTTP status, header, body等.
一个好办法是function adapter, 将标准的http.Handler添加error返回值,adapt过程中处理error
1 | func handleExample(w http.ResponseWriter, r *http.Request) error { |
声明adaper, 调用自定义的WriteError
函数转换error并写HTTP response:
1 | type WebHandlerFunc func(http.ResponseWriter, *http.Request) error |
Example: 模拟read user info的各种可能错误
这里带给大家的例子是: 一个GET /users?userID=xxx
endpoint, 不同的userID不同的HTTP response, 来模拟真实HTTP server遇到的情况, 404 user not found, 403 forbidden, 500 server error, 正常返回等,可以通过这个精简的例子来看error propgation, 即error是如何转为标准HTTP response的.
例子代码在[^1]
运行demo
1 | go build . && ./error_propagation |
试返回404的user
1 | curl -v http://localhost:8090/users\?userID\=notfountindb |
返回
1 | < HTTP/1.1 404 Not Found |
同时程序打印error stack:
1 | { |
代码分析
产生错误时,new我们定义的标准HTTP错误NewBadRequest
, 同时Wrap: 加入错误产生的行数.
1 | func checkUserID(userID string) error { |
HTTP handler中发现错误, Wrap并返回:
1 | func handleUser(w http.ResponseWriter, r *http.Request) error { |
, 错误处理层AddErrorHandler
负责写HTTP response, 核心是调用转换函数, 写Status Code, response header及标准化的error body: 包含error code
和error message
1 | func WriteError(w http.ResponseWriter, err error) { |
总结
我们分析error处理的一般思路: error propagation; 及如何将模块error propagate到用户: 转化为HTTP error response. 并结合demo给出了处理框架.
这套处理思路来自现在任职公司Nearmap, API组(这是一家好公司!); 是经过实际验证,靠谱的解决方案. 同时也解决了之前自己的疑问: 如何在HTTP server中妥善处理error, 在此表示感谢.
[^1]: Demo code