Spring Webflux – Functional Endpoints

I’m a big fan of Spring and was excited when Spring announced that Spring 5 had been released.

I was curious about the new Functional Endpoints that were introduced in Spring 5 and wanted to build a sample project to learn a bit more about how they are implemented.

This post will look at how to set up a simple router and get global error handling in place.

Initial Setup

Whenever I’m starting a new Spring Boot application I always head over to the Spring Initializer to bootstrap the project. We’re going to create a fairly simple project just to play around with the new functional endpoints.

You can see I’ve selected Gradle as my build tool, Kotlin as the programming language, and Spring Boot M5. I’ve also selected the Reactive Web and Dev Tools dependencies.

Routing and Handling

First, we’re going to define a simple handler that will display some users:

@Component
class UserHandler {
 
    val users = listOf(User("Alex"), User("Barbara"), User("Charlie"),
            User("Daisy"), User("Evan"), User("Faye"))
 
    fun getAllUsers(): Mono<ServerResponse> =
            ServerResponse.ok().body(users.toFlux(), User::class.java)
 
    fun getUser(username: String): Mono<ServerResponse> {
        val foundUsername = users.find { it.name == username } ?: throw NotFoundException("Could not find user: $username")
 
        return ServerResponse.ok().body(foundUsername.toMono(), User::class.java)
    }
}

I’m using a hardcoded list of users for the example. You could just as easily inject a service or the repository if you wanted to pull from an actual database.

Another thing to note is that all the methods return a Mono<ServerResponse>. This is expected as we only want to return a single response to each call. The body of the server response, however, is free to return a Mono (0 or 1 items) or a Flux (many items).

My data class for the User is very simple:

data class User(val name: String)

Now that we have something to handle requests let’s set up the actual routing using the new webflux.fn routing.

@Configuration
class RoutingConfig {
    private val usernamePathVariable = "username"
 
    @Bean
    fun routes(userHandler: UserHandler): RouterFunction<ServerResponse> = router {
        "/api".nest {
            GET("/"){
                userHandler.getAllUsers()
            }
            GET("/{$usernamePathVariable}") { req ->
                val username = req.pathVariable(usernamePathVariable)
                userHandler.getUser(username)
            }
        }
 
    }
}

This makes use of the Kotlin DSL for defining routes and allows nesting of routes. The above defines a route of “/api” with the nested routes being “/” and “/<username>”

It’s worth noting that I could have implemented the routes in the following way:

@Bean
fun routes(userHandler: UserHandler): RouterFunction<ServerResponse> = router {
    "/api".nest {
        GET("/", userHandler::getAllUsers)
        GET("/{username}", userHandler::getUser)
    }
}

and the handlers would have been implemented like:

@Component
class UserHandler {
 
    val users = listOf(User("Alex"), User("Barbara"), User("Charlie"),
            User("Daisy"), User("Evan"), User("Faye"))
 
    fun getAllUsers(request: ServerRequest): Mono<ServerResponse> =
            ServerResponse.ok().body(users.toFlux(), User::class.java)
 
    fun getUser(request: ServerRequest): Mono<ServerResponse> {
        val username = request.pathVariable("username")
        val foundUsername = users.find { it.name == username } ?: throw NotFoundException(reason = "Could not find user: $username")
 
        return ServerResponse.ok().body(foundUsername.toMono(), User::class.java)
    }
}

While this tidies up the router this also means that I would have to know in my handler what I had named the path parameter as in the router (username in my example) in order to extract the variable. I didn’t like having to know this in 2 separate areas and while a constant would have done the trick I chose to keep things local to the router.

Error Handling with Filters

The above code will now accept GET requests on localhost:8080/api, however, we are missing a very key component, which is error handling.

Initially looking at the Spring Boot Reference guide the error section refers to using @ControllerAdvice to globally capture errors which is the recommended way if you are using Annotations to specify routing. Unfortunately, this seems to only apply to @Controllers.

Digging around in the Spring 5 documentation lead me to determine that I could use a filter on the routes to apply global error handling:

With annotations, similar functionality can be achieved using @ControllerAdvice and/or a ServletFilter.

Defining a filter on the router was fairly trivial:

@Bean
fun routes(userHandler: UserHandler): RouterFunction<ServerResponse> = router {
    "/api".nest {
        GET("/"){
            userHandler.getAllUsers()
        }
        GET("/{$usernamePathVariable}") { req ->
            val username = req.pathVariable(usernamePathVariable)
            userHandler.getUser(username)
        }
    }
 
}.filter { request, next ->
    try {
        next.handle(request)
    } catch (ex: Exception) {
        log.error("An error occured", ex)
        when (ex) {
            is NotFoundException -> ServerResponse.notFound().build()
            else -> ServerResponse.status(HttpStatus.INTERNAL_SERVER_ERROR).build()
        }
    }
}

This did exactly what I’d wanted to do. Furthermore, I could extract the exceptionHandler into a common component to allow me to reuse the same global error handling should I create another routing bean.

Default Error Handling

I kept looking to see if there were any alternative ways to perform the error handling as it seemed odd that there wasn’t something baked into Spring Boot from the get-go. Imagine my surprise when the next day Spring announced Spring Boot 2.0 M6 complete with error handling for Webflux!

This version introduces the DefaultErrorWebExceptionHandler which takes care of converting exceptions into the familiar response:

{"timestamp":1510330188671,"path":"/test","message":"Response status 404","status":404,"error":"Not Found"}

The default error implementation checks for the presence of a ResponseStatusException to use to customize the JSON error responses. With this knowledge I can remove the filter from my router and define my NotFoundException as the following:

class NotFoundException(
        status: HttpStatus = HttpStatus.NOT_FOUND,
        reason: String? = null,
        throwable: Throwable? = null) : ResponseStatusException(status, reason, throwable)

This will produce the following output when a user is not found:

{"timestamp":1510330781517,"path":"/api/Alexa","message":"Response status 404 with reason \"Could not find user: Alexa\"","status":404,"error":"Not Found"}

There is definitely some more customization that can be done and by extending DefaultErrorWebExceptionHandler and providing a custom implementation of ErrorAttributes it should be possible to change the error format. Implementing AbstractErrorWebExceptionHandler should also allow us to customize a lot of the error handling if we wanted.

All of the code for this can be found on Github.

Let me know in the comments if anyone has a good example of custom error handling, I’m curious to see how others have used the new webflux.fn routing!

Leave a Reply

Your email address will not be published. Required fields are marked *

*

*