Reading HTTP request parameters within a Spring Boot HandlerInterceptor presents a speicfic challenge: the servlet input stream can only be consumed once. If a downstream controller or another filter reads the body, the interceptor will encounter an empty stream. To resolve this, the request must be wrapped to cache the payload, allowing multiple reads across the filter chain and interceptor stack.
Caching the Request Body
Extend HttpServletRequestWrapper to buffer the input stream in to memory. This implementation captures the raw bytes during initialization and serves them through a custom ServletInputStream on subsequent calls.
public class CachedBodyHttpServletRequest extends HttpServletRequestWrapper {
private final byte[] cachedPayload;
public CachedBodyHttpServletRequest(HttpServletRequest request) throws IOException {
super(request);
this.cachedPayload = StreamUtils.copyToByteArray(request.getInputStream());
}
@Override
public ServletInputStream getInputStream() throws IOException {
return new CachedServletInputStream(this.cachedPayload);
}
@Override
public BufferedReader getReader() throws IOException {
return new BufferedReader(new InputStreamReader(getInputStream(), StandardCharsets.UTF_8));
}
public String resolveCachedBody() {
return new String(this.cachedPayload, StandardCharsets.UTF_8);
}
private static class CachedServletInputStream extends ServletInputStream {
private final ByteArrayInputStream buffer;
public CachedServletInputStream(byte[] contents) {
this.buffer = new ByteArrayInputStream(contents);
}
@Override
public int read() throws IOException {
return buffer.read();
}
@Override
public boolean isFinished() {
return buffer.available() == 0;
}
@Override
public boolean isReady() {
return true;
}
@Override
public void setReadListener(ReadListener listener) {
throw new UnsupportedOperationException("Asynchronous reading not supported");
}
}
}
Applying the Wrapper via Filter
A servlet filter intercepts the encoming request before it reaches the dispatcher servlet. The filter conditionally wraps the request, bypassing multipart uploads to prevent interference with file handling.
@Component
public class RequestCachingFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
String contentType = httpRequest.getContentType();
// Skip wrapping for file uploads to avoid breaking multipart parsing
if (contentType != null && contentType.toLowerCase().contains("multipart/")) {
chain.doFilter(request, response);
} else {
chain.doFilter(new CachedBodyHttpServletRequest(httpRequest), response);
}
}
}
Interceptor with Parameter Extraction Strategy
The interceptor determines the payload format based on the Content-Type header and delegates extraction to a specific strategy. This separates form-urlencoded parsing from raw JSON retrieval.
public interface PayloadExtractor {
String extract(HttpServletRequest request);
}
public class FormDataExtractor implements PayloadExtractor {
@Override
public String extract(HttpServletRequest request) {
Map<String, String[]> paramMap = request.getParameterMap();
return paramMap.entrySet().stream()
.sorted(Map.Entry.comparingByKey())
.map(entry -> entry.getKey() + "=" + String.join(",", entry.getValue()))
.collect(Collectors.joining("&"));
}
}
public class JsonBodyExtractor implements PayloadExtractor {
@Override
public String extract(HttpServletRequest request) {
if (request instanceof CachedBodyHttpServletRequest) {
return ((CachedBodyHttpServletRequest) request).resolveCachedBody();
}
return "";
}
}
The interceptor orchestrates the extraction process during the preHandle phase.
@Component
public class ParameterInspectionInterceptor implements HandlerInterceptor {
private final Map<String, PayloadExtractor> extractorRegistry = new HashMap<>();
public ParameterInspectionInterceptor() {
extractorRegistry.put("json", new JsonBodyExtractor());
extractorRegistry.put("form", new FormDataExtractor());
}
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
String contentType = request.getContentType();
String strategyKey = (contentType != null && contentType.contains("application/json")) ? "json" : "form";
PayloadExtractor extractor = extractorRegistry.get(strategyKey);
String resolvedParams = extractor.extract(request);
System.out.println("Intercepted Request Data: " + resolvedParams);
return true;
}
}
Component Registration
Register the filter and interceptor within a Spring configuration class. The filter is assigned the highest precedence to ensure the request is wrapped before any other component processes it.
@Configuration
public class WebMvcInfrastructureConfig implements WebMvcConfigurer {
private final ParameterInspectionInterceptor inspectionInterceptor;
public WebMvcInfrastructureConfig(ParameterInspectionInterceptor inspectionInterceptor) {
this.inspectionInterceptor = inspectionInterceptor;
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(inspectionInterceptor)
.addPathPatterns("/api/**")
.excludePathPatterns("/api/public/**");
}
@Bean
public FilterRegistrationBean<RequestCachingFilter> registerCachingFilter(RequestCachingFilter filter) {
FilterRegistrationBean<RequestCachingFilter> registration = new FilterRegistrationBean<>();
registration.setFilter(filter);
registration.addUrlPatterns("/*");
registration.setOrder(Ordered.HIGHEST_PRECEDENCE);
return registration;
}
}