Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

What is the best way to validate request in a Spring Webflux functional application

In a traditional web application it is easy to validate the request body in the controller method, eg.

ResponseEntity create(@Valid @ResponseBody Post post) {
} 

If it is a MVC application, we can gather the errors by injecting a BindingResult, and decide if there is some validation errors from the input form.

In the pages, there are some helpers existed for Freemarker and Thymeleaf to display the messages.

But when I come to Webflux and try to use RouterFunction to define the routing in the applications. For example,

Mono<ServerResponse> create(ServerRequest req) {
    return req.bodyToMono(Post.class)
    .flatMap { this.posts.save(it) }
    .flatMap { ServerResponse.created(URI.create("/posts/".concat(it.getId()))).build() }
}

@Bean
RouterFunction<ServerResponse> routes(PostHandler postController) {
    return route(GET("/posts"), postController.&all)
    .andRoute(POST("/posts"), postController.&create)
    .andRoute(GET("/posts/{id}"), postController.&get)
    .andRoute(PUT("/posts/{id}"), postController.&update)
    .andRoute(DELETE("/posts/{id}"), postController.&delete)
}

A possible approach is converting the request data(Mono or Flux) to blocking and injecting a Validator and validate them manually.

But I think the codes will look a little ugly.

How to process the validation of request body or form data gracefully?

Is there a better to validate the request body or form data and do not lost the functional and reactive features for both WEB(rendering a view) and REST applications?

like image 349
Hantsy Avatar asked Dec 30 '17 06:12

Hantsy


People also ask

How do you validate a request in Spring?

Validate Request parameter When creating a route with Spring, adding an annotation rule to validate the input is possible. In our case, we will apply a Regex the validate the format of the reservation's code. Now run the application and test with a bad reservation code. Launch the application and test.

How does Hibernate Validator work?

Hibernate Validator allows to express and validate application constraints. The default metadata source are annotations, with the ability to override and extend through the use of XML. It is not tied to a specific application tier or programming model and is available for both server and client application programming.

Which is the default Validator of Spring?

Spring MVC Framework supports JSR-303 specs by default and all we need is to add JSR-303 and it's implementation dependencies in Spring MVC application.


2 Answers

I've developed "Yet Another Validator" for this porpose.

https://github.com/making/yavi

It would be great if YAVI could meet your expectation.

Validation code will look like following:

static RouterFunction<ServerResponse> routes() {
    return route(POST("/"), req -> req.bodyToMono(User.class) //
            .flatMap(body -> validator.validateToEither(body) //
                    .leftMap(violations -> {
                        Map<String, Object> error = new LinkedHashMap<>();
                        error.put("message", "Invalid request body");
                        error.put("details", violations.details());
                        return error;
                    })
                    .fold(error -> badRequest().syncBody(error), //
                          user -> ok().syncBody(user))));
}
like image 89
Toshiaki Maki Avatar answered Oct 06 '22 20:10

Toshiaki Maki


One of the ways I've managed to do it in my application is the following (code is in Kotlin but the idea is the same). I've declared RequestHandler class which performs validation:

@Component
class RequestHandler(private val validator: Validator) {

    fun <BODY> withValidBody(
            block: (Mono<BODY>) -> Mono<ServerResponse>,
            request: ServerRequest, bodyClass: Class<BODY>): Mono<ServerResponse> {

        return request
                .bodyToMono(bodyClass)
                .flatMap { body ->
                    val violations = validator.validate(body)
                    if (violations.isEmpty())
                        block.invoke(Mono.just(body))
                    else
                        throw ConstraintViolationException(violations)
                }
    }
}

Request objects can contain java validation annotations in this way:

data class TokenRequest constructor(@get:NotBlank val accessToken: String) {
    constructor() : this("")
}

And handler classes use RequestHandler to perform validation:

fun process(request: ServerRequest): Mono<ServerResponse> {
    return requestHandler.withValidBody({
        tokenRequest -> tokenRequest
                .flatMap { token -> tokenService.process(token.accessToken) }
                .map { result -> TokenResponse(result) }
                .flatMap { ServerResponse.ok()
                        .contentType(MediaType.APPLICATION_JSON_UTF8)
                        .body(Mono.just(it), TokenResponse::class.java)
                }
    }, request, TokenRequest::class.java)
}

Got the idea from this blog post.

like image 25
Yuriy Yunikov Avatar answered Oct 06 '22 22:10

Yuriy Yunikov