1

I'm configuring a custom error handling in order to log every bad request to my API. In my scenario, is important to know the payload passed in the request so I can fix / test / reprocess it as needed.

I have built a simple @RestControllerAdvise but I'm having a hard time on getting the request body from it:

@RestControllerAdvice
class ExceptionHandler {

    companion object {
        private val logger = getLogger()
    }

    @ExceptionHandler
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    fun badRequest(e: Exception, request: HttpServletRequest): Exception {
        logger.error("Received a bad request with body: ${request.getBodyPlease()}", e) // Note getBodyPlease() is not a real method
        return e
    }
}

I have tried reading the InputStream from the request but at this point it is already closed. Another question suggests injecting a RequestContext and setting it on Controller. This wouldn't work in a Bad Request scenario for it wouldn't execute the controller. Also it doesn't make much sense to set it in every controller.

Thanks in advance,

Edit

As @BeUndead suggested, I tried implementing a filter to wrap the request:

@Component
class RequestWrapperFilter : Filter {
    override fun doFilter(req: ServletRequest, res: ServletResponse, chain: FilterChain) {
        val reqWrapper = ContentCachingRequestWrapper(req as HttpServletRequest)
        chain.doFilter(reqWrapper, res)
    }
}

Then I'm trying to get body like request.reader.lines().collect(Collectors.joining()). When I try on the filter, with the ServletRequest it works fine, I see the body. But when I try on the @ExceptionHandler with the ContentCachingRequestWrapper I get an empty string,

João Menighin
  • 3,083
  • 6
  • 38
  • 80
  • 1
    Can you add a `Filter` which wraps the `HttpServletRequest` into a `ContentCachingRequestWrapper`, and _then_ get the `InputStream` from the request? – BeUndead Feb 13 '20 at 15:31
  • @BeUndead I have tried implementing but still not getting it right. Can you check the edit in the question please? – João Menighin Feb 13 '20 at 18:33
  • @BeUndead found out. As stated [here](https://github.com/spring-projects/spring-boot/issues/10452), even with `ContentCachingRequestWrapper`, we can still read from stream only once. But we can do it as many time as we want from the `contentAsByteArray` property. Thanks! – João Menighin Feb 13 '20 at 19:26

1 Answers1

0

As suggested by @BeUndead, I implemented a filter to wrap my request in a ContentCachingRequestWrapper:

@Component
class RequestWrapperFilter : Filter {
    override fun doFilter(req: ServletRequest, res: ServletResponse, chain: FilterChain) {
        val reqWrapper = ContentCachingRequestWrapper(req as HttpServletRequest)
        chain.doFilter(reqWrapper, res)
    }
}

And then I'm able to ready the body as many time as I want from the contentAsByteArrayProperty:

@RestControllerAdvice
class ExceptionHandler {

    companion object {
        private val logger = getLogger()
    }

    @ExceptionHandler
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    fun badRequest(e: Exception, request: HttpServletRequest): Exception {
        val body = (request as ContentCachingRequestWrapper).contentAsByteArray.toString(Charsets.UTF_8)
        logger.error("Received a bad request with body: $body", e)
        return e
    }
}
João Menighin
  • 3,083
  • 6
  • 38
  • 80
  • Glad this worked for you. Hopefully caching the request data isn't too memory hungry an operation in your use case. You can always make the filter be selective about mappings if it turns out to be. – BeUndead Feb 13 '20 at 20:35
  • Yeah, was thinking about that as well. But the caching is only for the lifetime of that request, right? – João Menighin Feb 13 '20 at 21:13
  • 1
    It's available for garbage collection when the filter chain finishes execution. But I don't think there's any explicit clean up so you're waiting until garbage collection's done. – BeUndead Feb 13 '20 at 22:48