4
  • the final goal:

log request body string in RestController's @ExceptionHandler.

  • explanations

By default, when request is invalid json, springboot throws a HttpMessageNotReadableException, but the message is very generic, and not including specific request body. This makes investigating hard. On the other hand, I can log every request string using Filters, but this way logs will be flooded with too many success ones. I only want to log the request when it is invalid. What I really want is in @ExceptionHandler I'll get that string(previously got somewhere) and log as ERROR.

To illustrate the problem, I created a demo project in github.

  • the controller:
@RestController
public class GreetController {
    protected static final Logger log = LogManager.getLogger();

    @PostMapping("/")
    public String greet(@RequestBody final WelcomeMessage msg) {
        // if controller successfully returned (valid request),
        // then don't want any request body logged
        return "Hello " + msg.from;
    }

    @ExceptionHandler({HttpMessageNotReadableException.class})
    public String addStudent(HttpMessageNotReadableException e) {
        // this is what I really want!
        log.error("{the request body string got somewhere, such as Filters }"); 
        return "greeting from @ExceptionHandler";
    }
}
  • the client
    1. valid request curl -H "Content-Type: application/json" http://localhost:8080 --data '{"from":"jim","message":"nice to meet you!"}'

    2. invalid request(invalid json) curl -H "Content-Type: application/json" http://localhost:8080 --data '{"from":"jim","message""nice to meet you!"}'

I once tried HandlerInterceptor but will get some error like

'java.lang.IllegalStateException: Cannot call getInputStream() after getReader() has already been called for the current request'.

after some searching 1 2, I decided to use Filter with ContentCachingRequestWrapper.

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        final HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        ContentCachingRequestWrapper cachedRequest = new ContentCachingRequestWrapper(httpServletRequest);
        chain.doFilter(cachedRequest, response);
        String requestBody = IOUtils.toString(cachedRequest.getContentAsByteArray(), cachedRequest.getCharacterEncoding());
        log.info(requestBody);
    }

This code works well except that the log is after the RestController. if I change the order:

        String requestBody = IOUtils.toString(cachedRequest.getReader());
        log.info(requestBody);
        chain.doFilter(cachedRequest, response);

Works for invalid request, but when request is valid, got following exception:

com.example.demo.GreetController : Required request body is missing: public java.lang.String com.example.demo.GreetController.greet(com.example.demo.WelcomeMessage)

I also tried getContentAsByteArray, getInputStream and getReader methods since some tutorials say the framework checks for specific method call.

Tried CommonsRequestLoggingFilter as suggested by @M. Deinum.

But all in vain.

Now I'm bit confused. Can anyone explain the executing order of RestController and Filter, when request is valid and invalid? Is there any easier way(less code) to achive my ultimate goal? thanks!

I'm using springboot 2.6.3, jdk11.

Lei Yang
  • 3,970
  • 6
  • 38
  • 59
  • Ditch your filter and use one of the filters provided by Spring. The `CommonsRequestLoggingFilter` comes to mind for that. – M. Deinum Jan 27 '22 at 07:59
  • thanks, i just tried `CommonsRequestLoggingFilter` but still not working. 1. by default `CommonsRequestLoggingFilter` logs the request body **after** the `RestController`. 2. by default `beforeRequest` only prints `Before request [POST /]` and no body string. 3. i tried overriding `beforeRequest` method and get the request, but same issue 'Required request body is missing'. github update with this trial. – Lei Yang Jan 27 '22 at 09:10
  • Your description actually states that you want to log it after the method call, as you only want to log invalid requests by an exception handler (which is invoked after a method or while an exception during execution happens). You can just inject/use the `HttpServletRequest` in your exception handler and read the body (you will have to make sure you have a filter that wraps it in a `ContentCachingRequestWrapper`, check if the request is an instance and use the `getContentAsByteArray` to get the content that has been read. So you only need a filter for the wrapping nothing more. – M. Deinum Jan 27 '22 at 09:30

2 Answers2

6
  1. Create a filter that wraps your request in a ContentCachingRequestWrapper (nothing more nothing less).

  2. Use the HttpServletRequest as a parameter in your exception handling method as an argument

  3. Check if instance of ContentCachingRequestWrapper

  4. Use the getContentAsByteArray to get the content.

Something like this.

public class CachingFilter extends OncePerRequestFilter {

protected abstract void doFilterInternal(
            HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {

  filterChain.doFilter(new ContentCachingRequestWrapper(request), new ContentCachingResponseWrapper(response));
}

NOTE: I wrapped the response as well, just in case you wanted that as well.

Now in your exception handling method use the HttpServletRequest as an argument and use that to your advantage.

@ExceptionHandler({HttpMessageNotReadableException.class})
public String addStudent(HttpMessageNotReadableException e, HttpServletRequest req) {
  if (req instanceof ContentCachingRequestWrapper) {
    ContentCachingRequestWrapper wrapper = (ContentCachingRequestWrapper) req;
    log.error(new String(wrapper.getContentAsByteArray()));
  }
  return "greeting from @ExceptionHandler";
}

It could be that multiple filters add a wrapper to the HttpServletRequest so you might need to iterate over those wrappers, you could also use this

private Optional<ContentCachingRequestWrapper> findWrapper(ServletRequest req) {
    ServletRequest reqToUse = req;
    while (reqToUse instanceof ServletRequestWrapper) {
        if (reqToUse instanceof ContentCachingRequestWrapper) {
            return Optional.of((ContentCachingRequestWrapper) reqToUse);
        }
        reqToUse = ((ServletRequestWrapper) reqToUse).getRequest();
    }
    return Optional.empty();
}

Your exception handler would then look something like this

@ExceptionHandler({HttpMessageNotReadableException.class})
public String addStudent(HttpMessageNotReadableException e, HttpServletRequest req) {
  Optional<ContentCachingRequestWrapper) wrapper = findWrapper(req);
  wrapper.ifPresent(it -> log.error(new String(it.getContentAsByteArray())));
 
  return "greeting from @ExceptionHandler";
}

But that might depend on your filter order and if there are multiple filters adding wrappers.

M. Deinum
  • 115,695
  • 22
  • 220
  • 224
1

Following @M.Deinum's comments, I solved and hope useful for others:

  1. Add a Filter
public class MyFilter implements Filter {
  public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
    final HttpServletRequest httpServletRequest = (HttpServletRequest) request;
    ContentCachingRequestWrapper cachedRequest = new ContentCachingRequestWrapper(httpServletRequest);
    chain.doFilter(cachedRequest, response);
  }
}
  1. Inject the ContentCachingRequestWrapper in ExceptionHandler
  @ExceptionHandler({ HttpMessageNotReadableException.class })
  public String addStudent(HttpMessageNotReadableException e, ContentCachingRequestWrapper cachedRequest) {
    log.error(e.getMessage());
    try {
      String requestBody = IOUtils.toString(cachedRequest.getContentAsByteArray(), cachedRequest.getCharacterEncoding());
      log.error(requestBody);
    } catch (IOException ex) {
      ex.printStackTrace();
    }
    return "greeting from @ExceptionHandler";
  }
Lei Yang
  • 3,970
  • 6
  • 38
  • 59