Global error handling using Spring boot + WebFlux
Asked Answered
J

4

7

How can we handle exceptions globally when using reactive programming in Spring boot rest controller?

I would assume that @ControllerAdvice will not work because I have tried this and it was unsuccessful.

My other try is currently this option, using custom attributes:

@Component
public class OsvcErrorAttributes extends DefaultErrorAttributes {
    public OsvcErrorAttributes() {
        super(true);
    }

    @Override
    public Map<String, Object> getErrorAttributes(ServerRequest request, boolean includeStackTrace) {
        return assembleError(request);
    }

    private Map<String, Object> assembleError(ServerRequest request) {
        ServerException serverException = (ServerException)getError(request);

        Map<String, Object> errorAttributes = new HashMap<>();
        errorAttributes.put("message", serverException.getMessage());
        errorAttributes.put("errors", serverException.getErrorMap());
        return errorAttributes;
    }
}

and WebExceptionHandler like this:

@Component
@Order(-2)
public class OsvcErrorHandler extends AbstractErrorWebExceptionHandler {
    public OsvcErrorHandler(ErrorAttributes errorAttributes,
                            ResourceProperties resourceProperties,
                            ApplicationContext applicationContext) {
        super(errorAttributes, resourceProperties, applicationContext);

        // TODO: 25.06.2019 temporary workaround
        ServerCodecConfigurer serverCodecConfigurer = new DefaultServerCodecConfigurer();
        setMessageWriters(serverCodecConfigurer.getWriters());
        setMessageReaders(serverCodecConfigurer.getReaders());
    }

    @Override
    protected RouterFunction<ServerResponse> getRoutingFunction(ErrorAttributes errorAttributes) {
        return RouterFunctions.route(RequestPredicates.all(), this::renderErrorResponse);
    }

    private Mono<ServerResponse> renderErrorResponse(ServerRequest serverRequest) {

        final Map<String, Object> errorAttributes = getErrorAttributes(serverRequest, true);
        return ServerResponse.status(HttpStatus.BAD_REQUEST)
                .contentType(MediaType.APPLICATION_JSON_UTF8)
                .body(BodyInserters.fromObject(errorAttributes));
    }
}

Code that generates an error:

@Data
@Service
public class ContactService {
    private final ContactRepository contactRepository;

    public Mono<Business> saveNewContact(Business business) {
        return contactRepository.save(business)
                .onErrorMap(throwable ->
                    ServerException.create(throwable.getMessage())
                        .persistError("ico", business.getIco(), "ICO is probably duplicate"));
    }
}

Problem is that this does not work either. I did follow this tutorial and I cannot see if I am wrong with something or not.

Jovitajovitah answered 26/6, 2019 at 16:58 Comment(5)
"Problem is that this does not work either." can you please tell WHAT is NOT workingWilds
@ThomasAndolf This means that configured global error handling is not triggeredMacaco
why do you have a @Data in your ContactService. In lombok that means that there is a getter and setter provided, but the ContactRepository needs to be @AutowiredWilds
There should be RequireArgsConstructor since auto wiring is done in constructor level so no Autowired is required. This is however not a problem.Macaco
If you have both spring-boot-starter-web and spring-boot-starter-webflux modules in your application it results in Spring Boot auto-configuring Spring MVC, not WebFlux. In that case, your global error handler won't be called. If you set the application type to SpringApplication.setWebApplicationType(WebApplicationType.REACTIVE). it should work.Macroclimate
J
3

You just use ServerCodecConfigurer injection in you global error handler constructor like this.

public OsvcErrorHandler(GlobalErrorAttributes errorAttributes, ApplicationContext applicationContext, 
                ServerCodecConfigurer serverCodecConfigurer) {
   super(errorAttributes, new ResourceProperties(), applicationContext);
   super.setMessageWriters(serverCodecConfigurer.getWriters());
   super.setMessageReaders(serverCodecConfigurer.getReaders());
}

Please find the code example in the git repository.

Joann answered 21/7, 2019 at 10:23 Comment(4)
I have already tried this -> I had to create bean DefaultServerCodecConfigurer because there was no bean of type ServerCodecConfigurer. Still, no luck.Maybe I am missing some dependency - for this purpose I have spring-boot-webflux without webMacaco
You can view this example in my git repository, sharing with the previous post.Joann
I get org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'org.springframework.http.codec.ServerCodecConfigurer' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {} Please, see my code github.com/Drezir/osvcMacaco
Delete your repository cache and run gradle clean build again. It is like conflicting any jar file. It is working to me. I have taken your ErrorAttributes and ErrorWebExceptionHandler class. I am using Java 1.8, and gradle 4.10.3 version.Joann
C
1

You need to define and implement ErrorWebExceptionHandler as a bean and set an @Order annotation with value less than -1, because that is the default of the Spring DefaultErrorWebExceptionHandler

Here is a sample implementation:

public class GlobalErrorHandler extends DefaultErrorWebExceptionHandler {
    public GlobalErrorHandler(
            final ErrorAttributes errorAttributes,
            final WebProperties.Resources resources,
            final ErrorProperties errorProperties,
            final ApplicationContext applicationContext) {
        super(errorAttributes, resources, errorProperties, applicationContext);
    }

    @Override
    public Mono<Void> handle(final ServerWebExchange exchange, final Throwable ex) {
        final ServerHttpResponse response = exchange.getResponse();
        if (ex instanceof IllegalStateException
                && StringUtils.equals("Session was invalidated", ex.getMessage())
                && response.getStatusCode().is3xxRedirection()) {
            final DataBufferFactory bufferFactory = exchange.getResponse().bufferFactory();
            return response.writeWith(Mono.just(bufferFactory.wrap("Redirecting...".getBytes())));
        }
        return super.handle(exchange, ex);
    }
}

And here is a sample configuration based on org.springframework.boot.autoconfigure.web.reactive.error.ErrorWebFluxAutoConfiguration class:

@Configuration
public class ErrorWebFluxAutoConfiguration {

    private final ServerProperties serverProperties;

    public ErrorWebFluxAutoConfiguration(final ServerProperties serverProperties) {
        this.serverProperties = serverProperties;
    }

    @Bean
    @Order(-2)
    public ErrorWebExceptionHandler errorWebExceptionHandler(
            final ErrorAttributes errorAttributes,
            final org.springframework.boot.autoconfigure.web.ResourceProperties resourceProperties,
            final WebProperties webProperties,
            final ObjectProvider<ViewResolver> viewResolvers,
            final ServerCodecConfigurer serverCodecConfigurer,
            final ApplicationContext applicationContext) {
        final GlobalErrorHandler exceptionHandler =
                new GlobalErrorHandler(
                        errorAttributes,
                        resourceProperties.hasBeenCustomized()
                                ? resourceProperties
                                : webProperties.getResources(),
                        serverProperties.getError(),
                        applicationContext);
        exceptionHandler.setViewResolvers(viewResolvers.orderedStream().collect(Collectors.toList()));
        exceptionHandler.setMessageWriters(serverCodecConfigurer.getWriters());
        exceptionHandler.setMessageReaders(serverCodecConfigurer.getReaders());
        return exceptionHandler;
    }

    @Bean
    @ConditionalOnMissingBean(value = ErrorAttributes.class, search = SearchStrategy.CURRENT)
    public DefaultErrorAttributes errorAttributes() {
        return new DefaultErrorAttributes();
    }
}

Thanks to this article which points me to use ErrorWebExceptionHandler.

Countervail answered 12/9, 2021 at 7:48 Comment(0)
R
0

Try injecting the ServerCodecConfigurer instead of instantiating it. I also inject a ViewResolversProvider when doing this, although it might not be necessary.

    public OsvcErrorHandler(
            final CustomErrorAttributes customAttributes,
            final ResourceProperties resourceProperties,
            final ObjectProvider<List<ViewResolver>> viewResolversProvider,
            final ServerCodecConfigurer serverCodecConfigurer,
            final ApplicationContext applicationContext
    ) {
        super(customAttributes, resourceProperties, applicationContext);

        this.setViewResolvers(viewResolversProvider.getIfAvailable(Collections::emptyList));
        this.setMessageWriters(serverCodecConfigurer.getWriters());
        this.setMessageReaders(serverCodecConfigurer.getReaders());
    }
Ritualism answered 2/7, 2019 at 10:52 Comment(2)
I had to create Bean of DefaultServerCodecConfigurer to be able to inject ServerCodecConfigurer but still no luck with error handlingMacaco
I had the same issue and the problem was that I had both spring-boot-starter-web and spring-boot-starter-webflux as dependencies. Spring states that if you have both spring-web and spring-webflux on the classpath, the application will favor spring-web and per default start up a non-reactive application with an underlying tomcat server. This is why there is no instance of ServerCodecConfigurer. If you set the application type to SpringApplication.setWebApplicationType(WebApplicationType.REACTIVE). it should work.Macroclimate
S
0

You can use @ControllerAdvice for processing Custom exceptions but better to extend ResponseEntityExceptionHandler class which is used to handle exceptions that occur during the execution of a controller method.

Spring boot doc

The example is tested for Spring boot & WebFlux 3.1.5

@ControllerAdvice
@NonNullApi
public class LeaderboardExceptionHandler extends ResponseEntityExceptionHandler {

    private final Logger log = LoggerFactory.getLogger(LeaderboardExceptionHandler.class);

    @ExceptionHandler(ClientException.class)
    public Mono<ResponseEntity<CustomError>> handleClientException(ClientException clientException) {
        log.warn("Client exception with code [{}] and message [{}]",
                clientException.getCode(),
                clientException.getMessage());
        return Mono.just(ResponseEntity.status(HttpStatus.BAD_REQUEST.value())
                .body(new CustomError().code(clientException.getCode()).message(clientException.getMessage())));
    }

}

Here is how an can be produced an error in the logic

 private Mono<Game> validateInputDate(Game game) {
        // Some code here
        if (newGameMinDate < minGameStartTime) {
            return Mono.error(new ClientException(ErrorMessages.GAME_INVALID_INIT_DATE.getCode(),
                    String.format(ErrorMessages.GAME_INVALID_INIT_DATE.getMessage())));
        }
        return Mono.just(game);
    }

Note: Starting with Spring Framework 6 ProblemDetail is supported, so can use it, instead of creating of custom object like the methods inside ResponseEntityExceptionHandler class

Softspoken answered 9/1 at 9:35 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.