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=xxxendpoint, 不同的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