问题

最近在为项目接入公司内部的一个服务的时候发现,虽然调用某个接口“成功”了,但是实际并没有生效。我看了一下 SDK 的代码 (这个 SDK 封装了 HTTP 的使用, 对接了多个不同服务) 之后发现,它实际封装 HTTP 调用的伪代码如下:

1
2
3
4
5
6
7
8
9
public String call(...) {
// ...
try {
var response = request().execute();
return response.bodyString();
} catch (IOException e) {
throw new Error("...", e);
}
}

这里的 execute 方法的文档写了, 这个方法在传输成功的情况下, 是不会抛出异常的 (i.e. HTTP Status Code 是 5XX, 4XX, 但是不会抛异常). 然而, SDK 提供的接口直接忽略了 response 的内容, 返回类型为 void, 也就是说使用方无法通过异常捕捉, 或者返回内容, 来判断是否调用成功.

解释

看到这个 HTTP 的封装之后, 我的第一猜测是实现的同事没有发现这个方法在状态码非 200 的情况会不抛异常. 所以我就问了他一下, 结果他告诉我这么写是故意的. 这样设计的理由如下:

他们团队觉得很多情况下 4XX 其实类似于 Java 中的 CheckedException, 也就是说是实际可以预见的错误. 对于这种情况, 更应该使用条件判断来处理这种情况. 所以他们规定, 他们提供的接口应该在所有代码可以正常处理的情况下, 返回 200 的状态码, 同时在 Response Body 中提供更详细的状态.

最初的猜测: 屏蔽传输细节?

本来我以为他们这么做,是为了屏蔽传输层的细节,也就是说这里不再依赖 HTTP 的状态码,而是让业务实现方自行通过 payload 来保证错误处理。但是我的同事给出了上边的解释,并不是基于这个原因.

Sum Type?

在看了他给出的解释之后, 我感觉这样的设计, 似乎在某种意义上来说, 是为了模拟 Sum Type,例如 Rust 中的 Result

1
2
3
4
enum Result<T, E> {
Ok(T),
Err(E)
}

使用方不在使用异常来区分这样的情况, 而是使用分支判断 (或者某些语言中使用模式匹配) 来分别处理.

问题?

像 Golang,Rust 之类的语言, 都希望开发者使用返回值来做错误处理。 我觉得这样的优势是通过在编译器来显式地提醒开发者某些调用可能出现的错误情况。 但是在上面说的这种 RPC 场景, 我们是没有办法保证的, 这是因为 SDK 对接的服务的实现方并不一定意识到了这个假设,例如我调用的这个服务,直接丢弃了 response

另一方面,最近接入的很多外部的服务,有一部分也是通过 response body 中的字段来提供更详细的错误信息,但是这个时候状态码往往也不是 200。

我个人感觉,让状态码不是 200,似乎能更好的保证使用方会某种程度上做好错误处理。