HttpMessageNotReadableException: Required request body is missing

背景

最近将基于 Spring Boot 1.4 的项目迁移到 Spring Boot 2.2,迁移后 application/x-www-form-urlencoded 类型的 POST 请求获取 body 失败,表现为抛出异常

org.springframework.http.converter.HttpMessageNotReadableException: Required request body is missing: public void com.example.controller.FooController.bar(java.lang.String)
    at org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor.readWithMessageConverters(RequestResponseBodyMethodProcessor.java:161)
    at org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor.resolveArgument(RequestResponseBodyMethodProcessor.java:131)
    at org.springframework.web.method.support.HandlerMethodArgumentResolverComposite.resolveArgument(HandlerMethodArgumentResolverComposite.java:121)
    at org.springframework.web.method.support.InvocableHandlerMethod.getMethodArgumentValues(InvocableHandlerMethod.java:167)
    at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:134)
    ...

控制器方法

@Controller
@RequestMapping("/foo")
public class FooController {
    @RequestMapping("/bar")
    public void bar(@RequestBody String content) {
        // do something
    }
}

curl请求

curl -X POST -H 'Content-Type: application/x-www-form-urlencoded' --data "abc=123&def=456" http://localhost:8080/foo/bar

环境

JDK 8
Spring Boot 2.2.13
Jetty Servlet 9.4.35

原因

当前版本的 Spring 在处理 POST 的 application/x-www-form-urlencoded 类型的请求时,获取 body 的操作会通过 javax.servlet.ServletRequest#getParameterMap 方法获取表单项后重新拼回 String,而不是调用 getInputStream()

找到关键的处理方法 org.springframework.http.server.ServletServerHttpRequest#getBodyFromServletRequestParameters,该方法的注释为

Use javax.servlet.ServletRequest.getParameterMap() to reconstruct the body of a form 'POST' providing a predictable outcome as opposed to reading from the body, which can fail if any other code has used the ServletRequest to access a parameter, thus causing the input stream to be "consumed".
翻译: 使用 javax.servlet.ServletRequest.getParameterMap() 重建表单 'POST' 的 body,提供可预测的结果,而不是从 body 中读取,如果任何其他代码使用 ServletRequest 访问参数,则可能失败,从而导致输入流被“消耗”。

在 jetty 的 HttpServletRequest 实现中,有一个 _inputState 内部标志,该标志在调用 getReader()getInputStream() 时被更新,之后在调用 getParameterMap() 方法时,会判断 _inputState 是否被更新过,如过被更新过,则视为被“消耗”了,这时 getParameterMap() 将返回空结果。

而我们的项目中设置了一个用于检查登录 token 的 servlet 过滤器,该过滤器包装了原来的 HttpServletRequest,将 getInputStream() 方法返回的内容缓存起来,以使 body 其可被重复读取。因为在该过滤器中已经调用过一次 getInputStream() 方法,用于获取 body 内容来检查是否存在 token,而这个操作会使 HttpServletRequest 内部的 _inputState 标志被更新,所以导致后续 Spring 无法获得表单内容。

解决

目前我的解决办法是修改控制器方法,直接从 HttpServletRequest 中获取 body 内容

@Controller
@RequestMapping("/foo")
public class FooController {
    @RequestMapping("/bar")
    public void bar(HttpServletRequest request) {
        ServletInputStream inputStream = request.getInputStream();
        // read content from inputStream
        // do something
    }
}
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容