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

cookii · 2024-9-1 22:18:38 · 49 次点击
# Spring RestTemplate 拦截器修改请求体导致的诡异问题

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

## 问题背景

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

1. 先调用 `/login` 接口,发送 username 和 password ,对方服务返回一个 JWT 。
2. 之后的每个请求接口都是标准格式,需要把 JWT 和请求参数放到一个 JSON 中,类似这样:

```json
{
    "token": "JWT-TOKENxxxxxx",
    "data": {
        "key1": "value1",
        "key2": "value2"
    }
}
```

3. 发送请求,然后拿到响应报文。

## 解决方案

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

代码大概长这样:
```java
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`,发现了这么一段代码:

```java
@Oferride
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() {
                    @Oferride
                    public void writeTo(OutputStream outputStream) throws IOException {
                        StreamUtils.copy(body, outputStream);
                    }

                    @Oferride
                    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`中找到了这段代码:

```java
@Oferride
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`

```java
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();
```
测试后,问题解决了。


## 反思和改进

问题虽然解决了,但我琢磨了一下,虽然是我在拦截器中修改了 body ,但这个地方 Spring 应该还是有责任把错误的`Content-Length`修正的。
第一,Spring 的文档中没有明确写这里应该由谁来负责,是个灰色地带。
第二,我们用`RestTemplate`谁会自己设置`Content-Length`啊,不都是框架设置的吗,所以这里不也应该由框架来负责嘛。

思考完,周末找了个时间给 Spring 提了个 PR ,有兴趣的同学可以到这里看看。[Update Content-Length when body changed by Interceptor]( https://github.com/spring-projects/spring-framework/pull/33459)

有一说一,虽然不是第一次提 PR ,但是还是感觉挺爽的,记录一下。

写的挺乱的,技术一般,大佬轻喷。
举报· 49 次点击
登录 注册 站外分享
快来抢沙发
0 条回复  
返回顶部