Spring Boot Advanced Trick: Proper Way to Make RequestBody Readable Multiple Times
The article explains why Spring Boot's RequestBody can only be read once due to servlet stream constraints, and provides a complete solution using a custom HttpServletRequestWrapper and a high‑priority filter to cache and replay the body for annotations, validation, logging and other pre‑processing steps.
In enterprise Spring Boot applications, it is common to need pre‑processing of the request body before the controller, such as idempotency checks, signature verification, logging, security audits, or custom validation. However, the Servlet specification provides only a single‑use InputStream/Reader, so once the body is consumed it cannot be read again, causing downstream components (e.g., @RequestBody binding) to fail.
The article explains that this limitation stems from the fact that HTTP request bodies are streamed and the servlet API lacks a replay mechanism; getInputStream() and getReader() operate on the same underlying stream, which cannot be rewound.
A typical example is a @PostMapping endpoint annotated with a custom @RepeatSubmit annotation and @Valid. Both the annotation and validation need to read the RequestBody, and the controller’s @RequestBody binding also depends on it; if any of these reads occur early, the request fails.
To solve the problem, the author proposes three requirements: read the request body only once, cache it, and let subsequent reads use the cached copy. The concrete solution is a custom HttpServletRequestWrapper that reads the entire body into a byte array in its constructor and overrides getInputStream() and getReader() to return new streams based on that cached data.
package com.icoderoad.web.wrapper;
import jakarta.servlet.ReadListener;
import jakarta.servlet.ServletInputStream;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletRequestWrapper;
import java.io.*;
import java.nio.charset.StandardCharsets;
public class RepeatedlyRequestWrapper extends HttpServletRequestWrapper {
private final byte[] body;
public RepeatedlyRequestWrapper(HttpServletRequest request, ServletResponse response) throws IOException {
super(request);
request.setCharacterEncoding(StandardCharsets.UTF_8.name());
response.setCharacterEncoding(StandardCharsets.UTF_8.name());
this.body = readBody(request);
}
private byte[] readBody(HttpServletRequest request) throws IOException {
try (InputStream is = request.getInputStream()) {
return is.readAllBytes();
}
}
@Override
public BufferedReader getReader() {
return new BufferedReader(new InputStreamReader(getInputStream(), StandardCharsets.UTF_8));
}
@Override
public ServletInputStream getInputStream() {
ByteArrayInputStream inputStream = new ByteArrayInputStream(body);
return new ServletInputStream() {
@Override public int read() { return inputStream.read(); }
@Override public boolean isFinished() { return inputStream.available() == 0; }
@Override public boolean isReady() { return true; }
@Override public void setReadListener(ReadListener readListener) { /* no‑op */ }
};
}
}A high‑priority filter creates this wrapper for JSON requests and passes the wrapped request down the filter chain.
package com.icoderoad.web.filter;
import com.icoderoad.web.wrapper.RepeatedlyRequestWrapper;
import jakarta.servlet.*;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.http.MediaType;
import org.springframework.util.StringUtils;
import java.io.IOException;
public class RepeatableFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
ServletRequest wrappedRequest = request;
if (request instanceof HttpServletRequest httpRequest
&& StringUtils.hasText(httpRequest.getContentType())
&& httpRequest.getContentType().startsWith(MediaType.APPLICATION_JSON_VALUE)) {
wrappedRequest = new RepeatedlyRequestWrapper(httpRequest, response);
}
chain.doFilter(wrappedRequest, response);
}
}The filter is registered with a high order (e.g., 1) so that it runs before any component that reads the body.
package com.icoderoad.config;
import com.icoderoad.web.filter.RepeatableFilter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
@Configuration
public class FilterConfig {
@Bean
public FilterRegistrationBean<RepeatableFilter> repeatableFilterRegistration() {
FilterRegistrationBean<RepeatableFilter> registration = new FilterRegistrationBean<>();
registration.setFilter(new RepeatableFilter());
registration.addUrlPatterns("/*");
registration.setName("repeatableFilter");
registration.setOrder(1); // high priority
return registration;
}
}After applying the wrapper and filter, custom annotations, validation, and controller binding all work together; logging, signature verification, and idempotency checks can coexist without breaking the request handling, and the solution is fully compatible with Spring Boot 3.x and Spring Security 6.x.
The article concludes that the single‑read limitation is a design choice of the web specification, and a robust backend engineer should extend capabilities gracefully by using HttpServletRequestWrapper, request‑body caching, and a high‑priority filter, all without modifying business code.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
LuTiao Programming
LuTiao Programming is a friendly community offering free programming lessons. We inspire learners to explore new ideas and technologies and quickly acquire job-ready skills.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
