Search code examples
javaspring-bootexceptionmicroserviceswebclient

Spring Boot: Throwing exception in WebClient does not caught on my exception controller handler


I'm creating a component class that overrides a reactive method that calls another microservice "uaa" that validates a token, but when I verify that the token is invalid I throw an exception, but that exception does not catch in my exception controller handler here is my component class

@Slf4j
@Component
@RequiredArgsConstructor
public class AuthFilter implements GlobalFilter {

    private final JwtTokenProviderService jwtTokenProviderService;
    private final TokenStatusDaoService tokenStatusDaoService;
    private final WebClient webClient;

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        log.info("something in the way");
        List<String> headers = exchange.getRequest().getHeaders().get(HttpHeaders.AUTHORIZATION);
        if(CollectionUtils.isEmpty(headers)) {
            log.trace("Request came without token");
            return chain.filter(exchange);
        } else {
            String authToken = headers.get(0);
            log.trace("Request holds a token");
            log.debug("Check if token has expired ...");
            if(jwtTokenProviderService.isTokenExpired(authToken)) {
                log.debug("Token has expired will throw an error");
                throw new AuthorizationForbiddenException(AuthorizationForbiddenExceptionTitleEnum.TOKEN_HAS_EXPIRED, "Token has expired");

            }else {
                log.debug("Check if token is valid and already saved");
                String userId = jwtTokenProviderService.getClaimsFromToken(authToken).get(SecurityUtils.IDENTIFIER_KEY).toString();
                if(!tokenStatusDaoService.exists(TokenStatusSpecification.withToken(authToken).and(TokenStatusSpecification.withUserId(Long.parseLong(userId))))) {
                    return webClient.get()
                            .uri("http://uaa", uriBuilder -> uriBuilder
                                    .path("/validate-token")
                                    .queryParam("token", authToken).build()).retrieve()
                            .bodyToMono(TokenValidationGetResource.class)
                            .map(tokenValidationGetResource -> {
                                if (!tokenValidationGetResource.isValid()) {
                                    log.debug("token is not valid");
                                    throw new AuthorizationForbiddenException(AuthorizationForbiddenExceptionTitleEnum.TOKEN_NOT_VALID, "Token is not valid");
                                } else {
                                    log.debug("token is valid");
                                    TokenStatusEntity tokenStatusEntity;
                                    try {
                                        tokenStatusEntity = tokenStatusDaoService.findOne(TokenStatusSpecification.withUserId(Long.parseLong(userId)));
                                    } catch (Exception e) {
                                        log.debug("No token defined for user: {}. Will save a new one ...", userId);
                                        tokenStatusEntity = new TokenStatusEntity();
                                    }
                                    tokenStatusEntity.setToken(authToken);
                                    tokenStatusEntity.setUserId(Long.parseLong(userId));
                                    tokenStatusEntity.setStatus(TokenStatusEnum.VALID);
                                    tokenStatusDaoService.save(tokenStatusEntity);
                                    log.debug("Token status entity: {}", tokenStatusEntity);
                                    return exchange;
                                }
                            }).flatMap(chain::filter);
                } else {
                    log.debug("Token exists in DB");
                    return chain.filter(exchange);
                }
            }
        }
    }
}

and here is my exception controller handler:

@ControllerAdvice
public class ExceptionControllerImpl implements ExceptionController {

    @Override
    @ExceptionHandler({
            AuthorizationForbiddenException.class
    })
    public ResponseEntity<ErrorDetailResource> handleGenericExceptions(
            AbstractBaseException e, HttpServletRequest request) {
        ErrorDetailResource errorDetailResource = new ErrorDetailResource();
        errorDetailResource.setTimestamp(Instant.now().toEpochMilli());
        errorDetailResource.setTitle(e.getTitle().toString());
        errorDetailResource.setCode(e.getTitle().getCode());
        errorDetailResource.setDeveloperMessage(e.getClass().getName());
        errorDetailResource.setStatus(e.getStatus().value());
        errorDetailResource.setDetail(e.getMessage());
        return new ResponseEntity<>(errorDetailResource, e.getStatus());
    }
}

Solution

  • Hello Those exceptions are thrown on a mono method in a reactive manner, so they can not be caught by controller advice, instead of doing that create a class which will extends the abstract class AbstractErrorWebExceptionHandler

    @Component
    @Order(-2)
    public class GlobalErrorWebExceptionHandler extends AbstractErrorWebExceptionHandler {
    
        public GlobalErrorWebExceptionHandler(GlobalErrorAttributes globalErrorAttributes,
                ApplicationContext applicationContext,
                ServerCodecConfigurer serverCodecConfigurer) {
            super(globalErrorAttributes, new WebProperties.Resources(), applicationContext);
            super.setMessageWriters(serverCodecConfigurer.getWriters());
            super.setMessageReaders(serverCodecConfigurer.getReaders());
        }
    
        @Override
        protected RouterFunction<ServerResponse> getRoutingFunction(ErrorAttributes errorAttributes) {
            return RouterFunctions.route(RequestPredicates.all(), this::renderErrorResponse);
        }
    
        private Mono<ServerResponse> renderErrorResponse(ServerRequest request) {
    
            final Map<String, Object> errorPropertiesMap = getErrorAttributes(request, ErrorAttributeOptions.defaults());
            Throwable error = null;
            // here is your abstract base exception
            AbstractBaseException baseException = null;
            try {
                baseException = (AbstractBaseException) getError(request);
            } catch (Exception e) {
                error = getError(request);
            }
    
            HttpStatus statusCode = baseException != null ? baseException.getStatus() : HttpStatus.INTERNAL_SERVER_ERROR;
            return ServerResponse.status(statusCode)
                    .contentType(MediaType.APPLICATION_JSON)
                    .body(BodyInserters.fromValue(errorPropertiesMap));
        }
    }
    

    And of course do not forget to add DefaultErrorAttributes

    @Component
    public class GlobalErrorAttributes extends DefaultErrorAttributes {
    
        @Override
        public Map<String, Object> getErrorAttributes(ServerRequest request, ErrorAttributeOptions options) {
            Throwable error = null;
            // here is your abstract base exception
            // cast the error to your exception class
            AbstractBaseException baseException = null;
            try {
                baseException = (AbstractBaseException) getError(request);
            } catch (Exception e) {
                error = getError(request);
            }
    
            Map<String, Object> errorResources = new HashMap<>();
    
            // Define the attribute that you want to return in response body
            errorResources.put("attribute1", Instant.now().toEpochMilli());
            errorResources.put("attribute2", baseException != null ? baseException.getStatus() : HttpStatus.INTERNAL_SERVER_ERROR);
    
            return errorResources;
        }
    }