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.

Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
Rewrite Spring Boot Request Body: Filter, RequestBodyAdvice, and Custom Processor

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.

backendJavaSpring BootFilterRequest BodyRequestBodyAdviceCustom Processor
Spring Full-Stack Practical Cases
Written by

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.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.