Introduction
Spring Boot controllers usually read request bodies once, so body transformation must happen before the controller consumes input. This is common for tasks like payload normalization, encrypted body decoding, schema adaptation, and tenant metadata injection. The safest technique is a filter plus request wrapper that replaces the readable body stream.
Core Sections
Why a wrapper is required
HttpServletRequest input streams are one time read by default. If middleware reads the body without wrapping, controllers may see empty content. A custom HttpServletRequestWrapper lets you store modified bytes and expose them through getInputStream and getReader.
1import jakarta.servlet.ReadListener;
2import jakarta.servlet.ServletInputStream;
3import jakarta.servlet.http.HttpServletRequest;
4import jakarta.servlet.http.HttpServletRequestWrapper;
5
6import java.io.*;
7import java.nio.charset.StandardCharsets;
8
9public class CachedBodyRequestWrapper extends HttpServletRequestWrapper {
10 private final byte[] body;
11
12 public CachedBodyRequestWrapper(HttpServletRequest request, byte[] body) {
13 super(request);
14 this.body = body;
15 }
16
17 @Override
18 public ServletInputStream getInputStream() {
19 ByteArrayInputStream bais = new ByteArrayInputStream(body);
20 return new ServletInputStream() {
21 @Override
22 public int read() {
23 return bais.read();
24 }
25
26 @Override
27 public boolean isFinished() {
28 return bais.available() == 0;
29 }
30
31 @Override
32 public boolean isReady() {
33 return true;
34 }
35
36 @Override
37 public void setReadListener(ReadListener readListener) {
38 // not used for this synchronous wrapper
39 }
40 };
41 }
42
43 @Override
44 public BufferedReader getReader() {
45 return new BufferedReader(new InputStreamReader(getInputStream(), StandardCharsets.UTF_8));
46 }
47}
Build a filter that modifies JSON body
Use OncePerRequestFilter so execution happens once per request dispatch. Read original bytes, transform, then forward wrapped request.
1import com.fasterxml.jackson.databind.ObjectMapper;
2import jakarta.servlet.FilterChain;
3import jakarta.servlet.ServletException;
4import jakarta.servlet.http.HttpServletRequest;
5import jakarta.servlet.http.HttpServletResponse;
6import org.springframework.web.filter.OncePerRequestFilter;
7
8import java.io.IOException;
9import java.nio.charset.StandardCharsets;
10import java.util.Map;
11
12public class BodyTransformFilter extends OncePerRequestFilter {
13 private final ObjectMapper mapper = new ObjectMapper();
14
15 @Override
16 protected void doFilterInternal(HttpServletRequest request,
17 HttpServletResponse response,
18 FilterChain filterChain) throws ServletException, IOException {
19
20 if (!"application/json".equalsIgnoreCase(request.getContentType())) {
21 filterChain.doFilter(request, response);
22 return;
23 }
24
25 byte[] original = request.getInputStream().readAllBytes();
26 Map<String, Object> payload = mapper.readValue(original, Map.class);
27 payload.put("source", "gateway");
28
29 byte[] modified = mapper.writeValueAsString(payload).getBytes(StandardCharsets.UTF_8);
30 CachedBodyRequestWrapper wrapped = new CachedBodyRequestWrapper(request, modified);
31 filterChain.doFilter(wrapped, response);
32 }
33}
Register the filter with a FilterRegistrationBean when ordering matters relative to security and logging filters.
Alternative with RequestBodyAdvice
If you only need to transform objects after deserialization and before controller method execution, RequestBodyAdvice can be cleaner than raw byte manipulation.
1import org.springframework.core.MethodParameter;
2import org.springframework.http.HttpInputMessage;
3import org.springframework.http.converter.HttpMessageConverter;
4import org.springframework.web.bind.annotation.ControllerAdvice;
5import org.springframework.web.servlet.mvc.method.annotation.RequestBodyAdviceAdapter;
6
7@ControllerAdvice
8public class PayloadAdvice extends RequestBodyAdviceAdapter {
9 @Override
10 public boolean supports(MethodParameter methodParameter,
11 java.lang.reflect.Type targetType,
12 Class<? extends HttpMessageConverter<?>> converterType) {
13 return true;
14 }
15
16 @Override
17 public Object afterBodyRead(Object body,
18 HttpInputMessage inputMessage,
19 MethodParameter parameter,
20 java.lang.reflect.Type targetType,
21 Class<? extends HttpMessageConverter<?>> converterType) {
22 return body;
23 }
24}
Use filter plus wrapper for raw transport level changes. Use body advice for object level policy changes.
Common Pitfalls
Reading request body in a filter without wrapping and forwarding modified bytes. Controllers then receive empty body.
Transforming every content type as JSON without checks. Gate logic by content type and path.
Ignoring filter order relative to security and tracing filters. Set explicit order in registration.
Performing expensive parsing in global filters unnecessarily. Narrow scope to matching endpoints.
Mixing transport and domain transformations in one class. Separate byte level and object level concerns.
Summary
Request body modification must happen before controller consumption.
A custom HttpServletRequestWrapper is the core technique for replacing request body bytes.
OncePerRequestFilter provides predictable interception points.
RequestBodyAdvice is useful for post deserialization object transformation.
Clear layering and filter ordering prevent subtle production bugs.
Additional implementation notes: verify behavior with realistic tests, document assumptions, and keep boundary handling explicit so long term maintenance stays predictable.