I'm developing a Spring Boot application (with Spring Security) and need to add default headers (specifically, Content-Type and Content-Length) to requests targeting the endpoint /api/v1/mpos/set-token when those headers are missing. To achieve this, I wrote a custom filter that extends OncePerRequestFilter and wraps the incoming HttpServletRequest so that I can read and cache its body. I use the following code:
@Component@Order(Ordered.HIGHEST_PRECEDENCE)public class SetTokenDefaultHeadersFilter extends OncePerRequestFilter { private static final String DEFAULT_CONTENT_TYPE = "application/json"; private static final String TARGET_URI = "/api/v1/mpos/set-token"; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { if (TARGET_URI.equals(request.getRequestURI())&& "POST".equalsIgnoreCase(request.getMethod())&& (request.getHeader("Content-Type") == null || request.getHeader("Content-Length") == null)) { HttpServletRequest wrappedRequest = new HttpServletRequestWrapper(request) { private final byte[] cachedBody = toByteArray(request.getInputStream()); private byte[] toByteArray(InputStream input) throws IOException { ByteArrayOutputStream baos = new ByteArrayOutputStream(); byte[] buffer = new byte[1024]; int len; while ((len = input.read(buffer)) != -1) { baos.write(buffer, 0, len); } return baos.toByteArray(); } @Override public ServletInputStream getInputStream() { final ByteArrayInputStream bais = new ByteArrayInputStream(cachedBody); return new ServletInputStream() { @Override public int read() { return bais.read(); } @Override public boolean isFinished() { return bais.available() == 0; } @Override public boolean isReady() { return true; } @Override public void setReadListener(ReadListener readListener) { // Not implemented } }; } @Override public BufferedReader getReader() { return new BufferedReader(new InputStreamReader(getInputStream(), StandardCharsets.UTF_8)); } @Override public int getContentLength() { return cachedBody.length; } @Override public long getContentLengthLong() { return cachedBody.length; } @Override public String getHeader(String name) { if ("Content-Type".equalsIgnoreCase(name)) { String header = super.getHeader(name); return header == null ? DEFAULT_CONTENT_TYPE : header; } if ("Content-Length".equalsIgnoreCase(name)) { String header = super.getHeader(name); return header == null ? String.valueOf(cachedBody.length) : header; } return super.getHeader(name); } }; filterChain.doFilter(wrappedRequest, response); } else { filterChain.doFilter(request, response); } }}
I have verified (via logging and FilterRegistrationBean) that my filter is registered with the highest precedence and should run first. However, when I call input.read(buffer), it immediately returns -1 (i.e. EOF), and the cached body is an empty array.
What I've tried so far:
Verified the filter order – my filter is running first.
Checked for other filters that might be reading the body; the logs indicate that my filter is before them.
My questions:
What could be consuming the request body before my filter even gets a chance to read it?
How can I ensure that my filter has access to the full request body so I can wrap it and add the default headers?
Is there a recommended approach in Spring Boot to cache the request body before any processing?
Any insights or suggestions would be greatly appreciated.