共计 3127 个字符,预计需要花费 8 分钟才能阅读完成。
Spring RestTemplate 拦截器修改请求体导致的诡异问题
最近在工作中发现了 Spring 的一个 ” 特性 ”(也许可以叫 Bug?),反正我已经给 Spring 提了 PR,等着看能不能合进去。
问题背景
最近在调用第三方 API 时,遇到了一个有意思的场景。整个调用流程大概是这样的:
- 先调用
/login
接口,发送 username 和 password,对方服务返回一个 JWT。 - 之后的每个请求接口都是标准格式,需要把 JWT 和请求参数放到一个 JSON 中,类似这样:
{
"token": "JWT-TOKENxxxxxx",
"data": {
"key1": "value1",
"key2": "value2"
}
}
- 发送请求,然后拿到响应报文。
解决方案
为了避免在每个接口都重复封装 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();
测试后,问题解决了。
反思和改进
问题虽然解决了,但我琢磨了一下,虽然是我在拦截器中修改了 body,但这个地方 Spring 应该还是有责任把错误的 Content-Length
修正的。
第一,Spring 的文档中没有明确写这里应该由谁来负责,是个灰色地带。
第二,我们用 RestTemplate
谁会自己设置 Content-Length
啊,不都是框架设置的吗,所以这里不也应该由框架来负责嘛。
思考完,周末找了个时间给 Spring 提了个 PR,有兴趣的同学可以到这里看看。Update Content-Length when body changed by Interceptor
有一说一,虽然不是第一次提 PR,但是还是感觉挺爽的,记录一下。
写的挺乱的,技术一般,大佬轻喷。