How to Capture and Reuse HTTP Request/Response Bodies in Spring with Custom Filters

This article explains why servlet request/response streams are single‑read, introduces Spring's ContentCaching wrappers, and provides step‑by‑step code for creating a reusable HttpBodyRecorderFilter and a pattern‑matching proxy to record bodies based on HTTP status codes.

Programmer DD
Programmer DD
Programmer DD
How to Capture and Reuse HTTP Request/Response Bodies in Spring with Custom Filters

When developing web applications you often need to inspect or log the HTTP request and response bodies. In a servlet, the requestBody or responseBody stream can be read only once, which makes repeated processing impossible.

Spring solves this problem with ContentCachingRequestWrapper and ContentCachingResponseWrapper , which cache the body content so it can be accessed multiple times.

Below is a reusable abstract filter HttpBodyRecorderFilter that wraps the request and response with these caching wrappers, captures the payload, and provides two abstract methods for custom handling:

public abstract class HttpBodyRecorderFilter extends OncePerRequestFilter {
    private static final int DEFAULT_MAX_PAYLOAD_LENGTH = 1024 * 512;
    private int maxPayloadLength = DEFAULT_MAX_PAYLOAD_LENGTH;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {
        boolean isFirstRequest = !isAsyncDispatch(request);
        HttpServletRequest requestToUse = request;
        if (isFirstRequest && !(request instanceof ContentCachingRequestWrapper)
                && (request.getMethod().equals(HttpMethod.PUT.name()) || request.getMethod().equals(HttpMethod.POST.name()))) {
            requestToUse = new ContentCachingRequestWrapper(request);
        }
        HttpServletResponse responseToUse = response;
        if (!(response instanceof ContentCachingResponseWrapper)
                && (request.getMethod().equals(HttpMethod.PUT.name()) || request.getMethod().equals(HttpMethod.POST.name()))) {
            responseToUse = new ContentCachingResponseWrapper(response);
        }
        boolean hasException = false;
        try {
            filterChain.doFilter(requestToUse, responseToUse);
        } catch (Exception e) {
            hasException = true;
            throw e;
        } finally {
            int code = hasException ? 500 : response.getStatus();
            if (!isAsyncStarted(requestToUse) && (this.codeMatched(code, AdvancedHunterConfigManager.recordCode()))) {
                recordBody(createRequest(requestToUse), createResponse(responseToUse));
            } else {
                writeResponseBack(responseToUse);
            }
        }
    }

    protected String createRequest(HttpServletRequest request) {
        String payload = "";
        ContentCachingRequestWrapper wrapper = WebUtils.getNativeRequest(request, ContentCachingRequestWrapper.class);
        if (wrapper != null) {
            byte[] buf = wrapper.getContentAsByteArray();
            payload = genPayload(payload, buf, wrapper.getCharacterEncoding());
        }
        return payload;
    }

    protected String createResponse(HttpServletResponse resp) {
        String response = "";
        ContentCachingResponseWrapper wrapper = WebUtils.getNativeResponse(resp, ContentCachingResponseWrapper.class);
        if (wrapper != null) {
            byte[] buf = wrapper.getContentAsByteArray();
            try { wrapper.copyBodyToResponse(); } catch (IOException e) { e.printStackTrace(); }
            response = genPayload(response, buf, wrapper.getCharacterEncoding());
        }
        return response;
    }

    protected void writeResponseBack(HttpServletResponse resp) {
        ContentCachingResponseWrapper wrapper = WebUtils.getNativeResponse(resp, ContentCachingResponseWrapper.class);
        if (wrapper != null) {
            try { wrapper.copyBodyToResponse(); } catch (IOException e) { LOG.error("Fail to write response body back", e); }
        }
    }

    private String genPayload(String payload, byte[] buf, String characterEncoding) {
        if (buf.length > 0 && buf.length < getMaxPayloadLength()) {
            try { payload = new String(buf, 0, buf.length, characterEncoding); }
            catch (UnsupportedEncodingException ex) { payload = "[unknown]"; }
        }
        return payload;
    }

    public int getMaxPayloadLength() { return maxPayloadLength; }

    private boolean codeMatched(int responseStatus, String statusCode) {
        if (statusCode.matches("^[0-9,]*$")) {
            String[] filteredCode = statusCode.split(",");
            return java.util.stream.Stream.of(filteredCode).map(Integer::parseInt).collect(java.util.stream.Collectors.toList()).contains(responseStatus);
        } else {
            return false;
        }
    }

    protected abstract void recordBody(String payload, String response);
    protected abstract String recordCode();
}

To use this filter, create a subclass that implements recordBody with your own processing logic. The recordCode method lets you specify which HTTP status codes should trigger body recording (e.g., only 400 and 500 for error detection).

If you need URL‑pattern matching similar to Spring MVC, you can wrap the filter with PatternMappingFilterProxy, which uses AntPathMatcher to apply Ant‑style patterns.

class PatternMappingFilterProxy implements Filter {
    private final Filter delegate;
    private final List<String> pathUrlPatterns = new ArrayList<>();
    private PathMatcher pathMatcher;

    public PatternMappingFilterProxy(Filter delegate, String... urlPatterns) {
        Assert.notNull(delegate, "A delegate Filter is required");
        this.delegate = delegate;
        this.pathMatcher = new AntPathMatcher();
        for (String pattern : urlPatterns) {
            this.pathUrlPatterns.add(pattern);
        }
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain)
            throws IOException, ServletException {
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        String path = httpRequest.getRequestURI();
        if (matches(path)) {
            delegate.doFilter(request, response, filterChain);
        } else {
            filterChain.doFilter(request, response);
        }
    }

    private boolean matches(String requestPath) {
        for (String pattern : pathUrlPatterns) {
            if (pathMatcher.match(pattern, requestPath)) {
                return true;
            }
        }
        return false;
    }

    @Override public void init(FilterConfig filterConfig) throws ServletException { delegate.init(filterConfig); }
    @Override public void destroy() { delegate.destroy(); }
    public List<String> getPathUrlPatterns() { return pathUrlPatterns; }
    public void setPathUrlPatterns(List<String> urlPatterns) { pathUrlPatterns.clear(); pathUrlPatterns.addAll(urlPatterns); }
}

Example: for a controller method @PostMapping("/test/{id}"), you can register the proxy with a pattern like /test/{id:[0-9]+} so that only matching URLs are processed by the body‑recording filter.

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.

JavaBackend DevelopmentHTTPfilter
Programmer DD
Written by

Programmer DD

A tinkering programmer and author of "Spring Cloud Microservices in Action"

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.