23

I want to handle the Exception of my api by adding a WebExceptionHandler. I can change the status code, but I am stuck when i want to change the body of the response : ex adding the exception message or a custom object.

Does anyone have exemple ?

How I add my WebExceptionHandler :

HttpHandler httpHandler = WebHttpHandlerBuilder.webHandler(toHttpHandler(routerFunction))
  .prependExceptionHandler((serverWebExchange, exception) -> {

      exchange.getResponse().setStatusCode(myStatusGivenTheException);
      exchange.getResponse().writeAndFlushWith(??)
      return Mono.empty();

  }).build();
Dariusz Bacinski
  • 8,324
  • 9
  • 38
  • 47
adrien le roy
  • 823
  • 2
  • 7
  • 12

5 Answers5

30

WebExceptionHandler is rather low level, so you have to directly deal with the request/response exchange.

Note that:

  • the Mono<Void> return type should signal the end of the response handling; this is why it should be connected to the Publisher writing the response
  • at this level, you're dealing directly with data buffers (no serialization support available)

Your WebExceptionHandler could look like this:

(serverWebExchange, exception) -> {

  exchange.getResponse().setStatusCode(myStatusGivenTheException);
  byte[] bytes = "Some text".getBytes(StandardCharsets.UTF_8);
  DataBuffer buffer = exchange.getResponse().bufferFactory().wrap(bytes);
  return exchange.getResponse().writeWith(Flux.just(buffer));
}
Brian Clozel
  • 56,583
  • 15
  • 167
  • 176
  • When I try this example the IDE highlights that Flux.just(buffer) is an incompatible type with the writeAndFlushWith param. This is expecting: Mono writeAndFlushWith(Publisher extends Publisher extends DataBuffer>> body); Has this signature changed? – Ellis Jan 18 '18 at 21:46
  • 1
    Fixed a typo in my answer. Thanks! – Brian Clozel Jan 19 '18 at 07:05
5

Given the answer, to serialize object I use this way :

 Mono<DataBuffer> db = commonsException.getErrorsResponse().map(errorsResponse -> {

     ObjectMapper objectMapper = new ObjectMapper();
     try {
         return objectMapper.writeValueAsBytes(errorsResponse);
     } catch (JsonProcessingException e) {
          return e.getMessage().getBytes();
     }
}).map(s -> exchange.getResponse().bufferFactory().wrap(s));

exchange.getResponse().getHeaders().add("Content-Type", "application/json");
exchange.getResponse().setStatusCode(commonsException.getHttpStatus());
return exchange.getResponse().writeWith(db);
adrien le roy
  • 823
  • 2
  • 7
  • 12
5

ServerResponse has a method writeTo which can be used to write your body to ServerExchange (Spring framework does it this way). Only problem is that you have to provide Contextas a second parameter, so I have just copied HandlerStrategiesResponseContext from framework implementation.

Make sure that you are using at least Spring Boot 2.0.0 M2, before this version WebExceptionHandler was not registered while using RouterFunctions.

import org.springframework.http.HttpStatus
import org.springframework.http.HttpStatus.*
import org.springframework.http.codec.HttpMessageWriter
import org.springframework.stereotype.Component
import org.springframework.web.reactive.function.server.HandlerStrategies
import org.springframework.web.reactive.function.server.ServerResponse
import org.springframework.web.reactive.result.view.ViewResolver
import org.springframework.web.server.ServerWebExchange
import org.springframework.web.server.WebExceptionHandler


@Component
class GlobalErrorHandler() : WebExceptionHandler {

    override fun handle(exchange: ServerWebExchange, ex: Throwable): Mono<Void> =
        handle(ex)
                .flatMap {
                    it.writeTo(exchange, HandlerStrategiesResponseContext(HandlerStrategies.withDefaults()))
                }
                .flatMap {
                    Mono.empty<Void>()
                }

    fun handle(throwable: Throwable): Mono<ServerResponse> {

        return when (throwable) {
            is EntityNotFoundException -> {
                createResponse(NOT_FOUND, "NOT_FOUND", "Entity not found, details: ${throwable.message}")
            }
            else -> {
                createResponse(INTERNAL_SERVER_ERROR, "GENERIC_ERROR", "Unhandled exception")
            }
        }
    }

    fun createResponse(httpStatus: HttpStatus, code: String, message: String): Mono<ServerResponse> =
        ServerResponse.status(httpStatus).syncBody(ApiError(code, message))
}

private class HandlerStrategiesResponseContext(val strategies: HandlerStrategies) : ServerResponse.Context {

    override fun messageWriters(): List<HttpMessageWriter<*>> {
        return this.strategies.messageWriters()
    }

    override fun viewResolvers(): List<ViewResolver> {
        return this.strategies.viewResolvers()
    }
}
Dariusz Bacinski
  • 8,324
  • 9
  • 38
  • 47
2

for anyone looking at a way to write JSON response body, here's a Kotlin code sample:

fun writeBodyJson(body: Any, exchange: ServerWebExchange) =
    exchange.response.writeWith(
        Jackson2JsonEncoder().encode(
            Mono.just(body),
            exchange.response.bufferFactory(),
            ResolvableType.forInstance(body),
            MediaType.APPLICATION_JSON_UTF8,
            Hints.from(Hints.LOG_PREFIX_HINT, exchange.logPrefix)
        )
    )

not 100% sure that's the way to go though, would like to get some opinions.

hylowaker
  • 966
  • 2
  • 9
  • 24
Hartmut
  • 725
  • 9
  • 11
0

I needed to render a view and couldn't find a way to do this via the exception handler (only found examples using controllers which would require a needless redirect) so I used the ServerResponseResultHandler to render it from the handler. This gives the required Mono<Void> response.

/** Used when creating the {@link HandlerResult}
 *  Taken from https://github.com/spring-projects/spring-framework/blob/5.3.x/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/support/HandlerFunctionAdapter.java
 */
private static final MethodParameter RETURN_TYPE;

static {
    try {
        Method method = ServerAccessDeniedHandler.class.getMethod("handle", ServerWebExchange.class, AccessDeniedException.class);
        RETURN_TYPE = new MethodParameter(method, -1);
    }
    catch (NoSuchMethodException ex) {
        throw new IllegalStateException(ex);
    }
}

@Autowired
private ServerResponseResultHandler responseHandler;

public class CustomAccessDeniedHandler implements ServerAccessDeniedHandler {
    @Override
    public Mono<Void> handle(ServerWebExchange exchange, AccessDeniedException ex) {
        return ReactiveSecurityContextHolder.getContext()
            .map(SecurityContext::getAuthentication)
            .filter(auth -> auth != null && auth.isAuthenticated())
            .switchIfEmpty(Mono.empty())
            .flatMap(auth -> ServerResponse
                .status(HttpStatus.FORBIDDEN)
                // Give name of view and model attributes it requires
                .render("unauthorized", Map.of("varName", varValue))
                .flatMap(response -> responseHandler.handleResult(exchange,
                    new HandlerResult(this, response, RETURN_TYPE)))
            );
    }
}
Sherwin F
  • 658
  • 7
  • 13