Search code examples
javaexceptionmonospring-webfluxflux

Returning Mono and Flux error always returns 500


I'm still trying to learn how WebFlux exceptions work, as far as I understand, when returning an object in a Flux or Mono, the consumer should receive the same exception which was sent from the server.

However, when I return an HTTPException 401 inside a Mono for example, the response body I receive in the consumer is different from what I sent, I read a 500 Internal Server error instead of 401.

Here is a simple controller class I made for the question

package com.example.demo;

import javax.xml.ws.http.HTTPException;

import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

import reactor.core.publisher.Mono;

@RestController
public class Controller {

@RequestMapping(
        path="/getExceptionMono",
        method = RequestMethod.GET,
        produces = MediaType.APPLICATION_JSON_VALUE)
    public Mono<String> getException(){
        return Mono.error(new HTTPException(401));
    }
}

Here is the consumer :

package com.example.demo;

import org.springframework.boot.CommandLineRunner;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.client.WebClient;

@Component
public class ReactiveDemoConsumer implements CommandLineRunner {
    @Override
    public void run(String... args) throws Exception {
        String url = "http://localhost:8080/getExceptionMono";
        WebClient.create(url)
            .get()
            .accept(MediaType.APPLICATION_JSON)
            .retrieve()
            .bodyToMono(String.class)
            .subscribe(value -> System.err.println("Received String: " + value),
                        err -> System.err.println("Received error " + err));

    }
}

This is the console log of the consumer

Received error org.springframework.web.reactive.function.client.WebClientResponseException$InternalServerError: 500 Internal Server Error from GET http://localhost:8080/getExceptionMono

How do I pass my exception so that the consumer will see the original one which I passed in the Mono? Hope my question is clear, thanks in advance


Solution

  • That is not the correct way to return a 4xx response from an application. Any kind of exception thrown within the application will be wrapped by WebClientResponseException and be received as a 500 Internal Server Error on the client side.

    One way to change that is having an exception handler in your controller like this:

      @ExceptionHandler({UnsupportedMediaTypeException.class})
      public ResponseEntity<String> exceptionHandler(Exception ex) {
        return ResponseEntity.badRequest().body(ex.getMessage());
      }
    

    Another way is to have a global exception handler in your code: (here Ex1 is something like HTTPException)

    @Component
    public class GlobalExceptionHandler implements ErrorWebExceptionHandler {
    
      @Override
      public Mono<Void> handle(ServerWebExchange exchange, Throwable ex) {
        ServerHttpResponse httpResponse = exchange.getResponse();
        setResponseStatus(httpResponse, ex);
        return httpResponse.writeWith(Mono.fromSupplier(() -> {
          DataBufferFactory bufferFactory = httpResponse.bufferFactory();
          try {
            //Not displaying any error msg to client for internal server error
            String errMsgToSend = (httpResponse.getStatusCode() == HttpStatus.INTERNAL_SERVER_ERROR) ? "" : ex.getMessage();
            return bufferFactory.wrap(new ObjectMapper().writeValueAsBytes(errMsgToSend));
          } catch (JsonProcessingException e) {
            return bufferFactory.wrap(new byte[0]);
          }
        }));
      }
    
      private void setResponseStatus(ServerHttpResponse httpResponse, Throwable ex) {
        if (ex instanceof Ex1 || ex instanceof Ex2 || ex instanceof Ex3) {
          httpResponse.setStatusCode(HttpStatus.BAD_REQUEST);
        } else {
          httpResponse.setStatusCode(HttpStatus.INTERNAL_SERVER_ERROR);
        }
      }
    }
    

    Or you can re-write your controller like this:

    @RequestMapping(
            path="/getExceptionMono",
            method = RequestMethod.GET,
            produces = MediaType.APPLICATION_JSON_VALUE)
        public Mono<ResponseEntity> getException(){
            return Mono.just(ResponseEntity.badRequest().build());
        }
    }
    

    Then in webclient code, you can do something like this:

    webClient
     .get()
     .uri("/some/url")
     .exchange()
     .flatMap(clientResponse -> {
         if (clientResponse.statusCode().is5xxServerError()) {
            //do something and return some mono
         }
         else if(clientResponse.statusCode().is4xxClientError()) {
            //do something and return some mono
         }
         else {
           //do something and return some mono  
         }
    });