Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to write messages to http body in Spring webflux WebExceptionHandlder

With the help of this post, I got exception handling work partially in my Spring 5 WebFlux application via custom WebExceptionHandler, but when I want to convert the existing exception in friendly messages to client, it does not work.

My custom WebExceptionHandler looks like the following, the complete codes is here.

 WebExchangeBindException cvex = (WebExchangeBindException) ex;
            Map<String, String> errors = new HashMap<>();
            log.debug("errors:" + cvex.getFieldErrors());
            cvex.getFieldErrors().forEach(ev -> errors.put(ev.getField(), ev.getDefaultMessage()));

            log.debug("handled errors::" + errors);
            try {
                DataBuffer db = new DefaultDataBufferFactory().wrap(objectMapper.writeValueAsBytes(errors));
                exchange.getResponse().setStatusCode(HttpStatus.UNPROCESSABLE_ENTITY);
                exchange.getResponse().getHeaders().setContentType(MediaType.APPLICATION_JSON_UTF8);
                exchange.getResponse().writeWith(Mono.just(db));
                return exchange.getResponse().setComplete();

            } catch (JsonProcessingException e) {
                e.printStackTrace();
                return Mono.empty();
            }

The status code was set correctly, but the response content length is 0.

like image 988
Hantsy Avatar asked Jan 01 '18 06:01

Hantsy


2 Answers

In your code sample, you are calling both:

// write the given data buffer to the response
// and return a Mono that signals when it's done
exchange.getResponse().writeWith(Mono.just(db));
// marks the response as complete and forbids writing to it
exchange.getResponse().setComplete();

Since you're calling the first one and nothing is subscribing to it, then nothing is written to the response.

You can update your code to have:

exchange.getResponse().setStatusCode(HttpStatus.UNPROCESSABLE_ENTITY);
exchange.getResponse().getHeaders().setContentType(MediaType.APPLICATION_JSON_UTF);
return exchange.getResponse().writeWith(Mono.just(db));
like image 62
Brian Clozel Avatar answered Nov 15 '22 23:11

Brian Clozel


I advise you to use the standard Spring way with Codecs to serialize objects. I know at least 2 way for this:

  1. Use ServerCodecConfigurer class and EntityResponse object.

You could define ServerCodecConfigurer @Bean

@Bean
public ServerCodecConfigurer serverCodecConfigurer() {
    return new DefaultServerCodecConfigurer();
}

And use it in some kind of util method like this

public class ResponseUtil {

    @NotNull
    public static <T> Mono<Void> putResponseIntoWebExchange(ServerWebExchange exchange, ServerCodecConfigurer serverCodecConfigurer, Mono<EntityResponse<T>> responseMono) {

        return responseMono.flatMap(entityResponse ->
                entityResponse.writeTo(exchange, new ServerResponse.Context() {
                    @NotNull
                    @Override
                    public List<HttpMessageWriter<?>> messageWriters() {
                        return serverCodecConfigurer.getWriters();
                    }

                    @NotNull
                    @Override
                    public List<ViewResolver> viewResolvers() {
                        return Collections.emptyList();
                    }
                }));
    }

}

Your code will look like this

        WebExchangeBindException cvex = (WebExchangeBindException) ex;
        Errors errors = new Errors();//Class wrapper for Map with errors
        log.debug("errors:" + cvex.getFieldErrors());
        cvex.getFieldErrors().forEach(ev -> errors.put(ev.getField(), ev.getDefaultMessage()));

        log.debug("handled errors::" + errors);

        final Mono<EntityResponse<Errors>> responseMono = EntityResponse.fromObject(errors)
            .status(HttpStatus.UNPROCESSABLE_ENTITY)
            .contentType(APPLICATION_JSON)
            .build();

        return ResponseUtil.putResponseIntoWebExchange(exchange, serverCodecConfigurer, responseMono);
  1. Use standard way for error handling. By default Spring WebFlux using DefaultErrorAttributes class for handling all exceptions. You could simply customise it by overriding this class and defining bean of this class in spring context.

Like this:

@Component
public class CustomErrorAttributes extends DefaultErrorAttributes {

    @Override
    public Map<String, Object> getErrorAttributes(ServerRequest request, boolean includeStackTrace) {
        final Map<String, Object> errorAttributes = super.getErrorAttributes(request, includeStackTrace);

        Throwable error = getError(request);

        //Depends of error add or replace errorAttributes with your custom message, http status, etc
        if (error instanceof WebExchangeBindException) {
            errorAttributes.put("message", error.getMessage());

            Errors errors = new Errors();
            error.getFieldErrors().forEach(ev -> errors.put(ev.getField(), ev.getDefaultMessage()));

            errorAttributes.put("errors", error);
        }

        return errorAttributes;
    }

}

It's more native way for handling exceptions in Spring WebFlux. There is nice post about that - https://dzone.com/articles/exception-handling-in-spring-boot-webflux-reactive

like image 28
Boris Finkelshteyn Avatar answered Nov 15 '22 23:11

Boris Finkelshteyn