Search code examples
springspring-boothttprequest

Is there any way of getting request body in a interceptor but it can still be accessed by the controllers?


I want to access the HttpServletRequestBody in an interceptor such that it can still be accessed by the downstream controllers of my application as the request.getInputStream(); can only be used once.

I tried to implement a custom class ClonedServletRequestWrapper by extending HttpServletRequestWrapper as following.

public class ClonedHttpServletRequest extends HttpServletRequestWrapper {


    private final byte[] body;
    private final Map<String,String> customHeaders = new HashMap<>();

    public ClonedHttpServletRequest(HttpServletRequest request) throws IOException {
        super(request);
        // deep cloning request body
        try (InputStream requestInputStream = request.getInputStream()) {
            this.body = requestInputStream.readAllBytes();
        }
        // deep cloning headers
        Collections.list(request.getHeaderNames()).forEach( headerName ->
                customHeaders.put(headerName, request.getHeader(headerName))
        );
    }

    @Override
    public ServletInputStream getInputStream() throws IOException {
        return new ClonedServletInputStream(this.body);
    }

    public byte[] getBody() {
        return body;
    }

    public Map<String, String> getCustomHeaders() {
        return customHeaders;
    }

    public String getHeader(String name) {
        return customHeaders.get(name);
    }

    public Enumeration<String> getHeaderNames() {
        return Collections.enumeration(customHeaders.keySet());
    }

    public Enumeration<String> getHeaders(String name) {
        return Collections.enumeration(Collections.singleton(customHeaders.get(name)));
    }
}
class ClonedServletInputStream extends ServletInputStream {

    private final InputStream cachedBodyInputStream;

    public ClonedServletInputStream(byte[] cachedBody) {
        this.cachedBodyInputStream = new ByteArrayInputStream(cachedBody);
    }

    @Override
    public boolean isFinished() {
        try {
            return cachedBodyInputStream.available() == 0;
        } catch (IOException e) {
            return true;
        }
    }

    @Override
    public boolean isReady() {
        return true;
    }

    @Override
    public void setReadListener(ReadListener readListener) {

    }


    @Override
    public int read() throws IOException {
        return cachedBodyInputStream.read();
    }
}

And following is the interceptor code in which i am accessing it.

@Component
public class IncomingRequestsAttributesInterceptor implements HandlerInterceptor {

    private static final ThreadLocal<ClonedHttpServletResponse> currentResponse = new ThreadLocal<>();

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        ClonedHttpServletRequest clonedHttpServletRequest = new ClonedHttpServletRequest(request);
        ClonedHttpServletResponse clonedHttpServletResponse = new ClonedHttpServletResponse(response);
        Span span = Span.current();
        span.setAttribute("request.payload",new String(clonedHttpServletRequest.getBody()));
        currentResponse.set(clonedHttpServletResponse);
        return true;
    }


    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        Span span = Span.current();
        Optional<ClonedHttpServletResponse> optionalClonedHttpServletResponse = Optional.ofNullable(currentResponse.get());
        optionalClonedHttpServletResponse.ifPresent(servletResponse -> span.setAttribute("response.payload", new String(servletResponse.getBody())));
        currentResponse.remove();
    }
}

Here in interceptor its being accessed and successfully being added in span of Opentelemtery(Actual purpose of accessing it in interceptor). But after this my controller won't be able to access it. I am facing request body is empty error.

I even override the getInputStream() method but still something crucial is being missed, please can someone help me this regard?


Solution

  • You should wrap the request in the Filter and put the wrapped request in the next filter chain. The request wrapped in the interceptor will not be passed to the controller, and the controller will still get HttpServletRequest.