Rewrite Spring Boot Request Body: Filter, RequestBodyAdvice, and Custom Processor
This article compares three Spring Boot techniques—using a Filter with a custom HttpServletRequestWrapper, a global RequestBodyAdvice, and a custom RequestResponseBodyMethodProcessor—to transparently decrypt or modify the request body, providing code samples, configuration tips, and testing guidance for each approach.
Problem
In Spring Boot the HTTP request body can be read only once. When the body needs to be transformed (e.g., decryption, desensitization, replacement) the original stream cannot be reused, which makes such processing difficult.
Environment
Spring Boot 3.5.0
Solution Overview
Three mainstream ways to modify the request body globally are presented:
A Filter that wraps the HttpServletRequest with a custom HttpServletRequestWrapper.
A global RequestBodyAdvice implementation that intercepts the raw bytes before @RequestBody deserialization.
A custom RequestResponseBodyMethodProcessor that overrides the low‑level input‑message creation.
1. Filter + HttpServletRequestWrapper
Wrap the original request, cache the input stream, transform the bytes (e.g., decrypt), and provide a new ServletInputStream. The wrapper is applied in a OncePerRequestFilter which replaces the original request before the filter chain continues.
@RestController
@RequestMapping("/modifybody")
public class ModifyBodyController {
@PostMapping
public User create(@RequestBody User user) {
return user;
}
public static record User(Long id, String name) {}
} public class ModifyBodyRequestWrapper extends HttpServletRequestWrapper {
private final ServletInputStream originalStream;
private final byte[] body;
public ModifyBodyRequestWrapper(HttpServletRequest request) {
super(request);
try {
this.originalStream = request.getInputStream();
// read original bytes, decrypt, then store
String decrypted = AesUtil.decrypt(new String(StreamUtils.copyToByteArray(this.originalStream)));
this.body = decrypted.getBytes(StandardCharsets.UTF_8);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
@Override
public ServletInputStream getInputStream() throws IOException {
ByteArrayInputStream bais = new ByteArrayInputStream(body);
return new ServletInputStream() {
@Override public int read() throws IOException { return bais.read(); }
@Override public void setReadListener(ReadListener listener) { originalStream.setReadListener(listener); }
@Override public boolean isReady() { return originalStream.isReady(); }
@Override public boolean isFinished() { return originalStream.isFinished(); }
};
}
} @WebFilter("/modifybody/*")
public class ModifyBodyFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws ServletException, IOException {
HttpServletRequest wrapped = new ModifyBodyRequestWrapper(request);
chain.doFilter(wrapped, response);
}
}Enable the filter registration with @ServletComponentScan on the Spring Boot application class.
2. Global RequestBodyAdvice
Implement RequestBodyAdviceAdapter and annotate it with @ControllerAdvice. The beforeBodyRead method receives the raw HttpInputMessage, decrypts the body, rebuilds a new HttpInputMessage and returns it. A custom annotation (e.g., @Modify) can limit the advice to specific controller methods.
@ControllerAdvice
public class ModifyBodyRequestBodyAdvice extends RequestBodyAdviceAdapter {
@Override
public boolean supports(MethodParameter methodParameter, Type targetType,
Class<? extends HttpMessageConverter<?>> converterType) {
return methodParameter.hasMethodAnnotation(Modify.class);
}
@Override
public HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage, MethodParameter parameter,
Type targetType,
Class<? extends HttpMessageConverter<?>> converterType)
throws IOException {
byte[] raw = StreamUtils.copyToByteArray(inputMessage.getBody());
String decrypted = AesUtil.decrypt(new String(raw));
byte[] newBody = decrypted.getBytes(StandardCharsets.UTF_8);
return new HttpInputMessage() {
@Override public HttpHeaders getHeaders() { return inputMessage.getHeaders(); }
@Override public InputStream getBody() { return new ByteArrayInputStream(newBody); }
};
}
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Modify {}Usage on a controller method:
@Modify
@PostMapping
public User create(@RequestBody User user) { return user; }3. Custom RequestResponseBodyMethodProcessor
Extend RequestResponseBodyMethodProcessor to override createInputMessage. The overridden method reads the original stream, decrypts it, and returns a ServletServerHttpRequest whose getBody() supplies the transformed bytes. Register this processor as a bean to replace the default one.
public class ModifyBodyMethodProcessor extends RequestResponseBodyMethodProcessor {
public ModifyBodyMethodProcessor(List<HttpMessageConverter<?>> converters,
List<Object> requestResponseBodyAdvice) {
super(converters, requestResponseBodyAdvice);
}
@Override
protected ServletServerHttpRequest createInputMessage(NativeWebRequest webRequest) {
HttpServletRequest servletRequest = webRequest.getNativeRequest(HttpServletRequest.class);
return new ServletServerHttpRequest(servletRequest) {
@Override
public InputStream getBody() throws IOException {
byte[] raw = StreamUtils.copyToByteArray(super.getBody());
String decrypted = AesUtil.decrypt(new String(raw));
return new ByteArrayInputStream(decrypted.getBytes(StandardCharsets.UTF_8));
}
};
}
}In a @Configuration class, expose the custom processor as a bean and ensure it replaces the default one (e.g., by ordering or by removing the original bean).
Key Points & Caveats
All three approaches are transparent to controller code; the controller receives the already‑processed object.
The Filter solution works at the servlet container level and affects every request matching the filter URL pattern.
RequestBodyAdvice is lighter weight and can be limited to annotated methods, but it only intercepts requests that are handled by @RequestBody converters.
Custom MethodProcessor gives the deepest control, allowing modification before any message converter runs, but requires replacing the default MVC processor bean.
Remember to add @ServletComponentScan when using @WebFilter, and to register the custom processor bean in the MVC configuration.
Spring Full-Stack Practical Cases
Full-stack Java development with Vue 2/3 front-end suite; hands-on examples and source code analysis for Spring, Spring Boot 2/3, and Spring Cloud.
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.
