How to Read the Request Body Multiple Times in Spring Boot 3

This guide explains why Spring MVC can read the request body only once and provides a custom solution using a RepeatReadBodyMethodProcessor and RepeatBodyHttpRequest to enable multiple @RequestBody parameters in Spring Boot 3.5.0.

Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
How to Read the Request Body Multiple Times in Spring Boot 3

Spring MVC reads the request body as a single InputStream, which is closed after the first read. Therefore a controller method cannot have multiple @RequestBody parameters by default.

Custom implementation to allow repeated reads

1. Custom argument resolver

public class RepeatReadBodyMethodProcessor extends RequestResponseBodyMethodProcessor {
    public RepeatReadBodyMethodProcessor(List<HttpMessageConverter<?>> converters,
                                         List<Object> requestResponseBodyAdvice) {
        super(converters, requestResponseBodyAdvice);
    }

    @Override
    protected ServletServerHttpRequest createInputMessage(NativeWebRequest webRequest) {
        HttpServletRequest servletRequest = webRequest.getNativeRequest(HttpServletRequest.class);
        Assert.state(servletRequest != null, "No HttpServletRequest");
        return new RepeatBodyHttpRequest(servletRequest);
    }
}

2. Request wrapper that caches the body

public class RepeatBodyHttpRequest extends ServletServerHttpRequest {
    private static final String REQ_BODY_CONTENT = "req_body_content";
    private final HttpServletRequest request;

    public RepeatBodyHttpRequest(HttpServletRequest request) {
        super(request);
        this.request = request;
    }

    @Override
    public InputStream getBody() throws IOException {
        byte[] body = (byte[]) this.request.getAttribute(REQ_BODY_CONTENT);
        if (body != null) {
            return new ByteArrayInputStream(body);
        }
        InputStream is = super.getBody();
        body = StreamUtils.copyToByteArray(is);
        this.request.setAttribute(REQ_BODY_CONTENT, body);
        return new ByteArrayInputStream(body);
    }
}

3. Register the resolver with higher priority

Adding the resolver via WebMvcConfigurer#addArgumentResolvers gives it lower precedence than the default. Instead, modify the RequestMappingHandlerAdapter bean after construction and insert the custom processor before the existing RequestResponseBodyMethodProcessor instances.

@Component
public class ConfigArgumentProcessor {
    private final RequestMappingHandlerAdapter requestMappingHandlerAdapter;

    public ConfigArgumentProcessor(RequestMappingHandlerAdapter requestMappingHandlerAdapter) {
        this.requestMappingHandlerAdapter = requestMappingHandlerAdapter;
    }

    @PostConstruct
    private void init() {
        List<HandlerMethodArgumentResolver> original = requestMappingHandlerAdapter.getArgumentResolvers();
        List<HandlerMethodArgumentResolver> newResolvers = new ArrayList<>();
        for (HandlerMethodArgumentResolver resolver : original) {
            if (resolver instanceof RequestResponseBodyMethodProcessor) {
                try {
                    Field convertersField = RequestMappingHandlerAdapter.class.getDeclaredField("messageConverters");
                    convertersField.setAccessible(true);
                    List<HttpMessageConverter<?>> converters = (List<HttpMessageConverter<?>>) convertersField.get(requestMappingHandlerAdapter);

                    Field adviceField = RequestMappingHandlerAdapter.class.getDeclaredField("requestResponseBodyAdvice");
                    adviceField.setAccessible(true);
                    List<Object> advice = (List<Object>) adviceField.get(requestMappingHandlerAdapter);

                    newResolvers.add(new RepeatReadBodyMethodProcessor(converters, advice));
                } catch (Exception e) {
                    // handle reflection errors if needed
                }
            }
            newResolvers.add(resolver);
        }
        requestMappingHandlerAdapter.setArgumentResolvers(newResolvers);
    }
}

4. Example controller

@RestController
@RequestMapping("/repeatbody")
public class RepeatBodyController {
    @PostMapping
    public Map<String, Object> create(@RequestBody User user, @RequestBody Address address) {
        return Map.of("user", user, "address", address);
    }

    public static record User(String name, Integer age, String email) {}
    public static record Address(String province, String city, String county) {}
}

Running the application with Spring Boot 3.5.0 returns a JSON object containing both User and Address fields, confirming that the request body can be read multiple times without affecting other MVC functionality.

Conclusion

By replacing the default RequestResponseBodyMethodProcessor with a custom implementation that caches the request payload in a request attribute, developers can safely use multiple @RequestBody parameters in a single controller method while preserving the original request‑processing behavior.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

JavaSpring BootHTTP Request@RequestBodyCustom Argument Resolver
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.