Servlet读取POST请求体时遇Stream Closed异常排查求助
解决HttpServletRequest请求体读取时“Stream Closed”异常的问题
兄弟,我太懂你踩的这个坑了!先给你把问题根源说透,再给你实打实的解决办法:
为啥会出现“Stream Closed”?
HttpServletRequest的输入流(不管是getInputStream()还是getReader())天生只能被读取一次!因为HTTP请求的流是单向的,读取完之后指针就走到末尾了,而且Servlet容器(比如Tomcat)在流读取完毕后会自动关闭它。
你遇到这个问题,大概率是在调用你的getBody()方法之前,已经有其他组件先把请求体读走了:
- 比如Spring MVC的
@RequestBody注解,框架会自动读取请求体解析成Java对象; - 或者你项目里的某个过滤器、拦截器提前读取了流;
- 甚至是一些参数解析组件,比如处理
@RequestParam的时候,也可能会读取表单类型的请求体。
这些操作都会把流“用掉”,等你再调用getInputStream()时,自然就会抛出“Stream Closed”异常。
解决方案分两种场景
场景1:可以避免重复读取
先排查你的代码流程:
- 检查在调用
getBody()之前,有没有用@RequestBody、@RequestParam这类自动解析参数的注解?有的话,把这些注解去掉,改用你的getBody()方法手动处理; - 查看项目里的过滤器/拦截器,有没有提前读取请求体的逻辑?如果有,调整它们的执行顺序或者去掉读取操作。
场景2:必须多次读取请求体(比如日志记录+业务处理)
这种情况下,我们需要把请求体的内容缓存起来,用自定义的HttpServletRequestWrapper包装原始请求,让后续所有读取操作都从缓存里拿。
步骤1:写一个缓存请求体的Wrapper类
public class CachedBodyHttpServletRequestWrapper extends HttpServletRequestWrapper { private final byte[] cachedBody; public CachedBodyHttpServletRequestWrapper(HttpServletRequest request) throws IOException { super(request); // 把原始请求体读取到字节数组里缓存 this.cachedBody = StreamUtils.copyToByteArray(request.getInputStream()); } @Override public ServletInputStream getInputStream() throws IOException { // 返回基于缓存的输入流 return new ServletInputStream() { private final ByteArrayInputStream byteStream = new ByteArrayInputStream(cachedBody); @Override public int read() throws IOException { return byteStream.read(); } @Override public boolean isFinished() { return byteStream.available() == 0; } @Override public boolean isReady() { return true; } @Override public void setReadListener(ReadListener readListener) { // 简单场景下空实现即可,复杂场景可以按需处理 } }; } @Override public BufferedReader getReader() throws IOException { return new BufferedReader(new InputStreamReader(getInputStream(), getCharacterEncoding())); } // 直接获取缓存的请求体字符串,方便调用 public String getCachedBody() { return new String(cachedBody, getCharacterEncoding()); } }
步骤2:编写过滤器来包装请求
这个过滤器要在所有可能读取请求体的组件之前执行,确保请求被提前缓存:
public class CachedBodyFilter implements Filter { @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { if (request instanceof HttpServletRequest) { HttpServletRequest httpRequest = (HttpServletRequest) request; // 只对POST请求做缓存,按需调整请求方法 if ("POST".equalsIgnoreCase(httpRequest.getMethod())) { CachedBodyHttpServletRequestWrapper wrappedRequest = new CachedBodyHttpServletRequestWrapper(httpRequest); chain.doFilter(wrappedRequest, response); return; } } chain.doFilter(request, response); } @Override public void init(FilterConfig filterConfig) throws ServletException {} @Override public void destroy() {} }
步骤3:注册过滤器(以Spring Boot为例)
把过滤器注册成Bean,设置最高优先级,保证它最先执行:
@Configuration public class FilterConfig { @Bean public FilterRegistrationBean<CachedBodyFilter> cachedBodyFilter() { FilterRegistrationBean<CachedBodyFilter> registrationBean = new FilterRegistrationBean<>(); registrationBean.setFilter(new CachedBodyFilter()); // 拦截所有请求,按需调整URL规则 registrationBean.addUrlPatterns("/*"); // 设置最高优先级,确保在其他过滤器之前执行 registrationBean.setOrder(Ordered.HIGHEST_PRECEDENCE); return registrationBean; } }
步骤4:修改你的getBody()方法
现在可以直接用缓存的内容,不用再担心流关闭的问题:
public static String getBody(HttpServletRequest request) throws IOException { // 如果是包装后的请求,直接拿缓存的字符串 if (request instanceof CachedBodyHttpServletRequestWrapper) { return ((CachedBodyHttpServletRequestWrapper) request).getCachedBody(); } // 兼容未被包装的情况(比如GET请求) StringBuilder bodyBuilder = new StringBuilder(); try (BufferedReader reader = request.getReader()) { char[] buffer = new char[128]; int bytesRead; while ((bytesRead = reader.read(buffer)) != -1) { bodyBuilder.append(buffer, 0, bytesRead); } } return bodyBuilder.toString(); }
注意事项
- 如果你的请求体特别大(比如上传大文件),内存缓存可能会导致OOM,这时候可以改成用临时文件来缓存请求体;
- 一定要确保过滤器的优先级足够高,否则其他组件先读取了流,缓存就没意义了。
内容的提问来源于stack exchange,提问作者Udi Kantzuker
相关产品推荐
相关产品推荐

