18
votes

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

6

6 Answers

12
votes

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).

11
votes

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();
6
votes

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.

6
votes

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\""
}
5
votes

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))
  }
}
0
votes

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