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 s
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.
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).
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\""
}
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();
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))
}
}
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