Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Spring reading request body twice

In spring I have a controller with an endpoint like so:

@RequestMapping(method = RequestMethod.POST)
@ResponseStatus(HttpStatus.CREATED)
@ResponseBody
public OutputStuff createStuff(@RequestBody Stuff stuff) {
    //my logic here
}

This way if doing a POST on this endpoint, the JSON in request body will be automatically deserialized to my model (Stuff). The problem is, I just got a requirement to log the raw JSON as it is coming in! I tried different approaches.

  1. Inject HttpServletRequest into createStuff, read the body there and log:

Code:

@RequestMapping(method = RequestMethod.POST)
@ResponseStatus(HttpStatus.CREATED)
@ResponseBody
public OutputStuff createStuff(@RequestBody Stuff stuff, HttpServletRequest req) {
    StringBuilder sb = new StringBuilder();
    req.getReader().getLines().forEach(line -> {
        sb.append(line);
    });
    //log sb.toString();
    //my logic here
}

The problem with this is that by the time I execute this, the reader's InputStream would have already been executed to deserialize JSON into Stuff. So I will get an error because I can't read the same input stream twice.

  1. Use custom HandlerInterceptorAdapter that would log raw JSON before the actual handler is called.

Code (part of it):

public class RawRequestLoggerInterceptor extends HandlerInterceptorAdapter {

    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        StringBuilder sb = new StringBuilder();
        req.getReader().getLines().forEach(line -> {
            sb.append(line);
        });
        //log sb.toString();

        return true;
    }
}

The problem with this tho is, that by the time the deserialization to stuff happens, the InputStream from the request would have been read already! So I would get an exception again.

  1. Another option I considered, but not implemented yet, would be somehow forcing Spring to use my custom implementation of HttpServletRequest that would cache the input stream and allow multiple read of it. I have no idea if this is doable tho and I can't find any documentation or examples of that!

  2. Yet another option would be not to read Stuff on my endpoint, but rather read the request body as String, log it and then deserialize it to Stuff using ObjectMapper or something like that. I do not like this idea either tho.

Are there better solutions, that I did not mention and/or am not aware of? I would appreciate help. I am using the latest release of SpringBoot.

like image 770
Daniel Gruszczyk Avatar asked Sep 24 '15 14:09

Daniel Gruszczyk


2 Answers

To read the request body multiple times, we must cache the initial payload. Because once the original InputStream is consumed we can't read it again.

Firstly, Spring MVC provides the ContentCachingRequestWrapper class which stores the original content. So we can retrieve the body multiple times calling the getContentAsByteArray() method.

So in your case, you can make use of this class in a Filter:

@Component
public class CachingRequestBodyFilter extends GenericFilterBean {

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain)
  throws IOException, ServletException {
        HttpServletRequest currentRequest = (HttpServletRequest) servletRequest;
        ContentCachingRequestWrapper wrappedRequest = new ContentCachingRequestWrapper(currentRequest);
        // Other details

        chain.doFilter(wrappedRequest, servletResponse);
    }
}

Alternatively, you can register CommonsRequestLoggingFilter in your application. This filter uses ContentCachingRequestWrapper behind the scenes and is designed for logging the requests.

like image 199
isaolmez Avatar answered Sep 18 '22 23:09

isaolmez


As referenced in this post: How to Log HttpRequest and HttpResponse in a file?, spring provides the AbstractRequestLoggingFilter you can use to log the request.

AbstractRequestLoggingFilter API Docs, found here

like image 36
Nate Avatar answered Sep 19 '22 23:09

Nate