记录一次修复 Spring Framework Bug 的经历

Spring RestTemplate拦截器修改请求体导致的诡异问题

最近在工作中发现了Spring的一个”特性”(也许可以叫Bug?),反正我已经给Spring提了PR,等着看能不能合进去 已经合入啦!

问题背景

最近在调用第三方API时,遇到了一个有意思的场景。整个调用流程大概是这样的:

  1. 先调用 /login 接口,发送username和password,对方服务返回一个JWT。
  2. 之后的每个请求接口都是标准格式,需要把JWT和请求参数放到一个JSON中,类似这样:
{
    "token": "JWT-TOKENxxxxxx",
    "data": {
        "key1": "value1",
        "key2": "value2"
    }
}
  1. 发送请求,然后拿到响应报文。

解决方案

为了避免在每个接口都重复封装token,我想到了用 org.springframework.http.client.ClientHttpRequestInterceptor 来拦截请求,统一修改请求体。

代码大概长这样:

this.restTemplate = new RestTemplateBuilder()
        .requestFactory(() -> new ReactorNettyClientRequestFactory())
        .interceptors((request, body, execution) -> {
            byte[] newBody = addToken(body); // 调用登陆获取token,修改入参body,添加token
            return execution.execute(request, newBody);
        })
        .build();

诡异的问题

修改完成后,进入测试阶段,奇怪的事情就发生了:token能正确获取,body也修改成功了,但对方的接口一直报400,Invalid JSON。更奇葩的是,我把newBody整个复制出来,用独立的Main代码发送请求,居然一次就成功了。

深入源码

不服气的我只能往源码里找原因。从RestTemplate一路Debug到org.springframework.http.client.InterceptingClientHttpRequest.InterceptingRequestExecution#execute,发现了这么一段代码:

@Override
public ClientHttpResponse execute(HttpRequest request, byte[] body) throws IOException {
    if (this.iterator.hasNext()) { //这里是在执行interceptor链,我的登陆和修改body接口就在这里执行
        ClientHttpRequestInterceptor nextInterceptor = this.iterator.next();
        return nextInterceptor.intercept(request, body, this);
    }
    else { // 上面的interceptor链执行完后,下面就是真实执行发送请求逻辑
        HttpMethod method = request.getMethod();
        ClientHttpRequest delegate = requestFactory.createRequest(request.getURI(), method);
        request.getHeaders().forEach((key, value) -> delegate.getHeaders().addAll(key, value)); 
        if (body.length > 0) {
            if (delegate instanceof StreamingHttpOutputMessage streamingOutputMessage) {
                streamingOutputMessage.setBody(new StreamingHttpOutputMessage.Body() {
                    @Override
                    public void writeTo(OutputStream outputStream) throws IOException {
                        StreamUtils.copy(body, outputStream);
                    }

                    @Override
                    public boolean repeatable() {
                        return true;
                    }
                });
            }
            else {
                StreamUtils.copy(body, delegate.getBody());
            }
        }
        return delegate.execute();
    }
}

在Debug到request.getHeaders().forEach这里时,我突然发现request里的Content-Length居然和body.length(被修改后的请求体)不一样。

问题根源

继续往上追溯,在org.springframework.http.client.AbstractBufferingClientHttpRequest中找到了这段代码:

@Override
protected ClientHttpResponse executeInternal(HttpHeaders headers) throws IOException {
    byte[] bytes = this.bufferedOutput.toByteArrayUnsafe();
    if (headers.getContentLength() < 0) {
        headers.setContentLength(bytes.length);
    }
    ClientHttpResponse result = executeInternal(headers, bytes);
    this.bufferedOutput.reset();
    return result;
}

原来Content-Length在执行拦截器之前就已经被设置了。但我们在拦截器里修改了body,导致对方接收到的JSON格式总是不对,因为Content-Length和实际的请求体长度不匹配。

解决问题

这时候为了先解决问题,就先在interceptor中重新赋值了Content-Length

this.restTemplate = new RestTemplateBuilder()
        .requestFactory(() -> new ReactorNettyClientRequestFactory())
        .interceptors((request, body, execution) -> {
            byte[] newBody = addToken(body); // 调用登陆获取token,修改入参body,添加token
            request.getHeaders().setContentLength(body.length); // 重新设置Content-Length
            return execution.execute(request, newBody);
        })
        .build();

测试后,问题解决了。

反思和改进

问题虽然解决了,但我心里总觉得有点不对劲。虽然是我手动在拦截器里修改了请求体,但按理说,Spring 应该也有责任确保 Content-Length 的正确性吧?毕竟,Spring 的文档里也没明确说这一块到底该谁来负责,感觉有点模棱两可。

再说了,我们平时用 RestTemplate 的时候,谁会去手动设置 Content-Length 啊?不都是框架自动处理的吗?所以,我觉得这个地方 Spring 也应该承担一部分责任。

想了想,周末抽空给 Spring 提了个 PR,主要就是想让框架在拦截器修改请求体后自动更新 Content-Length。有兴趣的同学可以去看看,链接在这里:Update Content-Length when body changed by Interceptor

说真的,虽然不是第一次给开源项目提 PR,但每次做完还是觉得挺有成就感的。记录一下,也算是给自己的一个小小鼓励吧。


记录一次修复 Spring Framework Bug 的经历
https://coding.gs/2024/09/01/fixing-bug-in-spring-framework/
作者
K
发布于
2024年9月1日
许可协议