Affects: Spring Boot: 2.5.2


I'm unable to get the request body (for logging), when capturing a global exception using ErrorWebExceptionHandler, and using @RequestBody.

@PostMapping(value = "/item")
public Mono<Item> create(Item item) {
    // Unable to read Item, but able to get the request's body
    // ....
}
@PostMapping(value = "/item")
public Mono<Item> create(@RequestBody Item item) {
    // Could read Item, but unable to get the request's body
    // ....
}

Here's my ErrorWebExceptionHandler handle method

@Override
public Mono<Void> handle(ServerWebExchange serverWebExchange, Throwable throwable) {

        var reqBodyBuff = serverWebExchange.getRequest().getBody();
        ByteArrayOutputStream baos = new ByteArrayOutputStream();

        reqBodyBuff.collectList().subscribe(r -> {
            r.forEach(buffer -> {
                try {
                      Channels.newChannel(baos).write(buffer.asByteBuffer().asReadOnlyBuffer());
                      String bodyStr = new String(baos.toByteArray(), StandardCharsets.UTF_8);
                      System.out.println(bodyStr);
                } catch (IOException e) {
                      e.printStackTrace();
                } finally {
                      try {
                           baos.close();
                      } catch (IOException e) {
                           e.printStackTrace();
                      }
                }
            });
        });

        // ..........
}

Comment From: bug-hunterx

hi @MuizMahdi

I've created these tests trying to reproduce your issue, but both tests are green - means I can get request body in the exception handler in both cases.

Comment From: MuizMahdi

Hello @bagedan,

I run the tests and it worked, however, it didn't work when I attempted to send a request manually using Postman. Any idea what could be the issue?

The request body's DataBuffer list is empty whenever I send the request manually


reqBodyBuff.collectList().subscribe(r -> {
    System.out.println(r.isEmpty()); // True
});

Comment From: bug-hunterx

yes, you are right... Will check it further and let you know if I find anything.

Comment From: bclozel

Thanks for getting in touch, but it feels like this is a question that would be better suited to Stack Overflow. As mentioned in the guidelines for contributing, we prefer to use the issue tracker only for bugs and enhancements. Feel free to update this issue with a link to the re-posted question (so that other people can find it) or add some more details if you feel this is a genuine bug.

In this case, your code snippet is disconnecting the behavior you're expecting from the exchange pipeline. Calling subscribe effectively disconnects the consuming of the request body from the overall request handling and starts the work in a different context - by the time the handle method exits, the request information is deleted/recycled and the request content is gone.

Your best course of action is to ensure that you're getting a single reactive pipeline. As a rule of thumb, you should (almost) never call subscribe within a method that returns a reactive type. Returning a reactive type means that you're expected to participate in the pipeline and that branching for a new one will lead to unexpected results.

In your case, something like this:

@Override
public Mono<Void> handle(ServerWebExchange serverWebExchange, Throwable throwable) {

        var reqBodyBuff = serverWebExchange.getRequest().getBody();
        ByteArrayOutputStream baos = new ByteArrayOutputStream();

        return reqBodyBuff.collectList().map(r -> {
            r.forEach(buffer -> {
                try {
                      Channels.newChannel(baos).write(buffer.asByteBuffer().asReadOnlyBuffer());
                      String bodyStr = new String(baos.toByteArray(), StandardCharsets.UTF_8);
                      System.out.println(bodyStr);
                } catch (IOException e) {
                      e.printStackTrace();
                } finally {
                      try {
                           baos.close();
                      } catch (IOException e) {
                           e.printStackTrace();
                      }
                }
            });
        })
        .then();
}

Also, you should probably look into DataBufferUtils#write methods for that.

Comment From: rstoyanchev

Also keep in mind that working with raw buffers, and caching them in any way (such as via collectList) is tricky and could lead to memory leaks, e.g. if the request is cancelled for any reason. This is why we have DataBufferUtils#join and even then you should make sure to release the aggregated buffer after reading it.