22

I've been doing some research using spring-webflux and I like to understand what should be the right way to handle errors using Router Functions.

I've created an small project to test a couple of scenarios, and I like to get feedback about it, and see what other people is doing.

So far what I doing is.

Giving the following routing function:

@Component
public class HelloRouter {
    @Bean
    RouterFunction<?> helloRouterFunction() {
        HelloHandler handler = new HelloHandler();
        ErrorHandler error = new ErrorHandler();

        return nest(path("/hello"),
                nest(accept(APPLICATION_JSON),
                        route(GET("/"), handler::defaultHello)
                                .andRoute(POST("/"), handler::postHello)
                                .andRoute(GET("/{name}"), handler::getHello)
                )).andOther(route(RequestPredicates.all(), error::notFound));
    }
}

I've do this on my handler

class HelloHandler {

    private ErrorHandler error;

    private static final String DEFAULT_VALUE = "world";

    HelloHandler() {
        error = new ErrorHandler();
    }

    private Mono<ServerResponse> getResponse(String value) {
        if (value.equals("")) {
            return Mono.error(new InvalidParametersException("bad parameters"));
        }
        return ServerResponse.ok().body(Mono.just(new HelloResponse(value)), HelloResponse.class);
    }

    Mono<ServerResponse> defaultHello(ServerRequest request) {
        return getResponse(DEFAULT_VALUE);
    }

    Mono<ServerResponse> getHello(ServerRequest request) {
        return getResponse(request.pathVariable("name"));
    }

    Mono<ServerResponse> postHello(ServerRequest request) {
        return request.bodyToMono(HelloRequest.class).flatMap(helloRequest -> getResponse(helloRequest.getName()))
                .onErrorResume(error::badRequest);
    }
}

Them my error handler do:

class ErrorHandler {

    private static Logger logger = LoggerFactory.getLogger(ErrorHandler.class);

    private static BiFunction<HttpStatus,String,Mono<ServerResponse>> response =
    (status,value)-> ServerResponse.status(status).body(Mono.just(new ErrorResponse(value)),
            ErrorResponse.class);

    Mono<ServerResponse> notFound(ServerRequest request){
        return response.apply(HttpStatus.NOT_FOUND, "not found");
    }

    Mono<ServerResponse> badRequest(Throwable error){
        logger.error("error raised", error);
        return response.apply(HttpStatus.BAD_REQUEST, error.getMessage());
    }
}

Here is the full sample repo:

https://github.com/LearningByExample/reactive-ms-example

Akhil Bojedla
  • 1,968
  • 12
  • 19
Juan Medina
  • 533
  • 1
  • 5
  • 12

7 Answers7

12

Spring 5 provides a WebHandler, and in the JavaDoc, there's the line:

Use HttpWebHandlerAdapter to adapt a WebHandler to an HttpHandler. The WebHttpHandlerBuilder provides a convenient way to do that while also optionally configuring one or more filters and/or exception handlers.

Currently, the official documentation suggests that we should wrap the router function into an HttpHandler before booting up any server:

HttpHandler httpHandler = RouterFunctions.toHttpHandler(routerFunction);

With the help of WebHttpHandlerBuilder, we can configure custom exception handlers:

HttpHandler httpHandler = WebHttpHandlerBuilder.webHandler(toHttpHandler(routerFunction))
  .prependExceptionHandler((serverWebExchange, exception) -> {

      /* custom handling goes here */
      return null;

  }).build();
aietcn
  • 346
  • 3
  • 9
12

If you think, router functions are not the right place to handle exceptions, you throw HTTP Exceptions, that will result in the correct HTTP Error codes. For Spring-Boot (also webflux) this is:

  import org.springframework.http.HttpStatus;
  import org.springframework.web.server.ResponseStatusException;
  .
  .
  . 

  new ResponseStatusException(HttpStatus.NOT_FOUND,  "Collection not found");})

spring securities AccessDeniedException will be handled correctly, too (403/401 response codes).

If you have a microservice, and want to use REST for it, this can be a good option, since those http exceptions are quite close to business logic, and should be placed near the business logic in this case. And since in a microservice you shouldn't have to much businesslogic and exceptions, it shouldn't clutter your code, too... (but of course, it all depends).

Frischling
  • 2,100
  • 14
  • 34
7

Why not do it the old fashioned way by throwing exceptions from handler functions and implementing your own WebExceptionHandler to catch 'em all:

@Component
class ExceptionHandler : WebExceptionHandler {
    override fun handle(exchange: ServerWebExchange?, ex: Throwable?): Mono<Void> {
        /* Handle different exceptions here */
        when(ex!!) {
            is NoSuchElementException -> exchange!!.response.statusCode = HttpStatus.NOT_FOUND
            is Exception -> exchange!!.response.statusCode = HttpStatus.INTERNAL_SERVER_ERROR
        }

        /* Do common thing like logging etc... */

        return Mono.empty()
    }
}

Above example is in Kotlin, since I just copy pasted it from a project I´m currently working on, and since the original question was not tagged for java anyway.

Alphaone
  • 570
  • 5
  • 6
  • 2
    Finally make it work, in none boot application, register it manually, see [here](https://github.com/hantsy/spring-reactive-sample/blob/master/exception-handler/src/main/java/com/example/demo/Application.java#L50) – Hantsy Jan 01 '18 at 02:35
  • 1
    @Hantsy You could also consider handling exceptions directly in handler functions with for example onErrorResume(...). I actually find this to be the best practice atm :) – Alphaone Feb 07 '18 at 13:43
7

You can write a Global exception handler with custom response data and response code as follows. The code is in Kotlin. But you can convert it to java easily:

@Component
@Order(-2)
class GlobalWebExceptionHandler(
  private val objectMapper: ObjectMapper
) : ErrorWebExceptionHandler {

  override fun handle(exchange: ServerWebExchange, ex: Throwable): Mono<Void> {

    val response = when (ex) {
      // buildIOExceptionMessage should build relevant exception message as a serialisable object
      is IOException -> buildIOExceptionMessage(ex)
      else -> buildExceptionMessage(ex)
    }

    // Or you can also set them inside while conditions
    exchange.response.headers.contentType = MediaType.APPLICATION_PROBLEM_JSON
    exchange.response.statusCode = HttpStatus.valueOf(response.status)
    val bytes = objectMapper.writeValueAsBytes(response)
    val buffer = exchange.response.bufferFactory().wrap(bytes)
    return exchange.response.writeWith(Mono.just(buffer))
  }
}
Akhil Bojedla
  • 1,968
  • 12
  • 19
  • This is a great solution, made even better by extending DefaultErrorWebExceptionHandler instead of ErrorWebExceptionHandler. – GhostBytes Jun 28 '21 at 15:00
5

A quick way to map your exceptions to http response status is to throw org.springframework.web.server.ResponseStatusException / or create your own subclasses...

Full control over http response status + spring will add a response body with the option to add a reason.

In Kotlin it could look as simple as

@Component
class MyHandler(private val myRepository: MyRepository) {

    fun getById(req: ServerRequest) = req.pathVariable("id").toMono()
            .map { id -> uuidFromString(id) }  // throws ResponseStatusException
            .flatMap { id -> noteRepository.findById(id) }
            .flatMap { entity -> ok().json().body(entity.toMono()) }
            .switchIfEmpty(notFound().build())  // produces 404 if not found

}

fun uuidFromString(id: String?) = try { UUID.fromString(id) } catch (e: Throwable) { throw BadRequestStatusException(e.localizedMessage) }

class BadRequestStatusException(reason: String) : ResponseStatusException(HttpStatus.BAD_REQUEST, reason)

Response Body:

{
    "timestamp": 1529138182607,
    "path": "/api/notes/f7b.491bc-5c86-4fe6-9ad7-111",
    "status": 400,
    "error": "Bad Request",
    "message": "For input string: \"f7b.491bc\""
}
Hartmut
  • 725
  • 9
  • 11
0

What I am currently doing is simply providing a bean my WebExceptionHandler :

@Bean
@Order(0)
public WebExceptionHandler responseStatusExceptionHandler() {
    return new MyWebExceptionHandler();
}

The advantage than creating the HttpHandler myself is that I have a better integration with WebFluxConfigurer if I provide my own ServerCodecConfigurer for example or using SpringSecurity

loki
  • 9,816
  • 7
  • 56
  • 82
adrien le roy
  • 823
  • 2
  • 7
  • 12
0

Extending Akhil's answer to implement ErrorWebExceptionHandler in java.

public Mono<Void> handle(ServerWebExchange exchange, Throwable ex) {
   return Mono.just(exchange.getResponse())
        .doOnNext(response -> response.getHeaders().setContentType(MediaType.APPLICATION_JSON))
        .doOnNext(response->response.setStatusCode(HttpStatus.<HTTP STATUS>))
        .flatMap(response -> {
            try {
                return response.writeWith(Mono.just(response.bufferFactory()
                                .wrap(new ObjectMapper().writeValueAsBytes(new CustomResponseObject()))));
            } catch (JsonProcessingException e) {
                throw new RuntimeException(e);
            }
        })
        .doOnSuccess(e -> log.info("Exception occurred", ex));
}

should replace HTTP STATUS and CustomResponseObject with expected one along with ObjectMapper instance.

elesg
  • 63
  • 1
  • 5