Global exception handling in micronaut Java

Viewed 3900

Looking for a better solution to handle the global exception in micronaut https://docs.micronaut.io/latest/guide/index.html#errorHandling

Controller

@Controller("/category")
public class CategoryController {
@Delete(uri = "/{id}")
public Maybe<HttpResponse> delete(@NotBlank String id) {
            LOG.info(String.format("API --> Deleting the specified category"));
            return iCategoryManager.Count(id).flatMap(item -> {
                if (item > 0) {
                    iCategoryManager.Delete(id).subscribe();
                    return Maybe.just(HttpResponse.noContent());
                } else
                    return Maybe.just(HttpResponse.notFound());
            });
    }
}

iCategoryManager.Count(id) cause an exception as below, How can I catch the exception on GlobalExceptionHandler

io.micronaut.core.serialize.exceptions.SerializationException: Incorrect message body size to deserialize to a Long
    at io.micronaut.rabbitmq.serdes.JavaLangRabbitMessageSerDes$LongSerDes.deserialize(JavaLangRabbitMessageSerDes.java:314)
    at io.micronaut.rabbitmq.serdes.JavaLangRabbitMessageSerDes$LongSerDes.deserialize(JavaLangRabbitMessageSerDes.java:306)
    at io.micronaut.rabbitmq.serdes.JavaLangRabbitMessageSerDes.deserialize(JavaLangRabbitMessageSerDes.java:81)
    at io.micronaut.rabbitmq.intercept.RabbitMQIntroductionAdvice.deserialize(RabbitMQIntroductionAdvice.java:323)
    at io.micronaut.rabbitmq.intercept.RabbitMQIntroductionAdvice.lambda$intercept$22(RabbitMQIntroductionAdvice.java:268)
    at io.reactivex.internal.operators.flowable.FlowableFlatMap$MergeSubscriber.onNext(FlowableFlatMap.java:132)
    at io.micronaut.reactive.rxjava2.RxInstrumentedSubscriber.onNext(RxInstrumentedSubscriber.java:59)
    at io.reactivex.internal.operators.flowable.FlowableTimeoutTimed$TimeoutSubscriber.onNext(FlowableTimeoutTimed.java:101)
    at io.micronaut.reactive.rxjava2.RxInstrumentedSubscriber.onNext(RxInstrumentedSubscriber.java:59)
    at io.reactivex.internal.subscriptions.DeferredScalarSubscription.complete(DeferredScalarSubscription.java:132)
    at io.reactivex.internal.operators.single.SingleToFlowable$SingleToFlowableObserver.onSuccess(SingleToFlowable.java:62)
    at io.micronaut.reactive.rxjava2.RxInstrumentedSingleObserver.onSuccess(RxInstrumentedSingleObserver.java:65)
    at io.reactivex.internal.operators.single.SingleFlatMap$SingleFlatMapCallback$FlatMapSingleObserver.onSuccess(SingleFlatMap.java:111)
    at io.micronaut.reactive.rxjava2.RxInstrumentedSingleObserver.onSuccess(RxInstrumentedSingleObserver.java:65)
    at io.reactivex.internal.operators.single.SingleDoFinally$DoFinallyObserver.onSuccess(SingleDoFinally.java:73)
    at io.micronaut.reactive.rxjava2.RxInstrumentedSingleObserver.onSuccess(RxInstrumentedSingleObserver.java:65)
    at io.reactivex.internal.operators.single.SingleCreate$Emitter.onSuccess(SingleCreate.java:67)
    at io.micronaut.rabbitmq.reactive.RxJavaReactivePublisher$3.handleDelivery(RxJavaReactivePublisher.java:324)
    at com.rabbitmq.client.impl.ConsumerDispatcher$5.run(ConsumerDispatcher.java:149)
    at com.rabbitmq.client.impl.ConsumerWorkService$WorkPoolRunnable.run(ConsumerWorkService.java:104)

Global exception handling

@Produces
@Singleton
@Requires(classes = {GlobalException.class, ExceptionHandler.class})
public class GlobalExceptionHandler implements ExceptionHandler<GlobalException, HttpResponse> {

    @Override
    public HttpResponse handle(HttpRequest request, GlobalException exception) {
        return HttpResponse.ok(0);
    }
}

public class GlobalException extends RuntimeException{
}

How do I hook GlobalExceptionHandler. Any exception that occur on the application should be caught at GlobalExceptionHandler

4 Answers

When an exception occurs on the server an ExceptionHandler is looked up in the bean context that most closely matches the thrown exception (by the type arguments of the handler). To handle all exceptions it would be required to replace all provided exception handlers. There are around a dozen or so of them.

One option is to replace the default error response processor. That is what is responsible for the body being HATEOS for example. See the documentation on error handling for the specifics.

This solution works for me

@Produces
@Singleton
@Requires(classes = {GlobalException.class, ExceptionHandler.class})
public class GlobalExceptionHandler implements ExceptionHandler<GlobalException, HttpResponse> {
    private static final Logger LOG = LoggerFactory.getLogger(GlobalExceptionHandler.class);

    @Override
    public HttpResponse handle(HttpRequest request, GlobalException exception) {
        LOG.error(exception.getLocalizedMessage());
        LOG.error(exception.getCause().getMessage());
        Arrays.stream(exception.getStackTrace()).forEach(item ->  LOG.error(item.toString()));
        return HttpResponse.serverError(exception.getLocalizedMessage());
    }
}

public class GlobalException extends RuntimeException{
    public GlobalException(Throwable throwable){super(throwable);}
}

Controller

  public Maybe<FindProductCommand> get(ProductSearchCriteriaCommand searchCriteria) {
        LOG.info("Controller --> Finding all the products");
        return iProductManager.find(searchCriteria)
                .onErrorResumeNext(throwable ->  { return Maybe.error(new GlobalException(throwable));});
    }

Update with Micronaut 3 and project reactor

@Override
@SingleResult
public Flux<List<FindProductCommand>> freeTextSearch(String text) {
    LOG.info("Controller --> Finding all the products");
    return iProductManager.findFreeText(text)
            .onErrorMap(throwable -> {
                throw new GlobalException(throwable);
            });
}

We can use import io.micronaut.http.server.exceptions.ExceptionHandler for global error handling.

Create an Error Message:

public class ErrorMessage {

    private String message;
    private Boolean status;

    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message;
    }

    public Boolean getStatus() {
        return status;
    }

    public void setStatus(Boolean status) {
        this.status = status;
    }

}

Create the Custom Exception:

import java.io.Serializable;

public class CustomException extends RuntimeException 
    implements Serializable {

    private static final long serialVersionUID = 1L;

    public CustomException() {
    }

    public CustomException(String message) {
        super(message);
    }

    public CustomException(String message, Throwable cause) {
        super(message, cause);
    }

    public CustomException(Throwable cause) {
        super(cause);
    }

    public CustomException(String message, Throwable cause, 
    boolean enableSuppression, boolean writableStackTrace) {
        super(message, cause, enableSuppression, writableStackTrace);
    }
}

Create the Custom Exception Handler:

import io.micronaut.context.annotation.Requires;
import io.micronaut.http.HttpRequest;
import io.micronaut.http.HttpResponse;
import io.micronaut.http.HttpStatus;
import io.micronaut.http.annotation.Produces;
import io.micronaut.http.server.exceptions.ExceptionHandler;
import jakarta.inject.Singleton;

@Produces
@Singleton
@Requires(classes = { CustomException.class, ExceptionHandler.class })

public class CustomExceptionHandler 
    implements ExceptionHandler<CustomException, HttpResponse<ErrorMessage>> {

    @Override
    public HttpResponse<ErrorMessage> 
        handle(HttpRequest request, CustomException exception) {

        ErrorMessage message = new ErrorMessage();
        message.setMessage(exception.getMessage());
        message.setStatus(false);
        return HttpResponse.serverError(message).
                status(HttpStatus.BAD_REQUEST);
    }

}

Demo Controller:

import com.knf.dev.demo.data.UserData;
import com.knf.dev.demo.exception.CustomException;
import com.knf.dev.demo.model.User;
import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Get;

@Controller
public class UserController {

    protected final UserData userData;

    public UserController(UserData userData) {
        this.userData = userData;
    }

    @Get("/users/{id}")
    public User findUserById(String id) throws CustomException {
        Long user_id = null;
        try {
            user_id = Long.parseLong(id);
        } catch (NumberFormatException e) {
            throw new CustomException("User Id must be numeric");
        }
        User user = userData.getUserById(user_id);
        if (user == null) {
            throw new CustomException("Entity Not Found");
        }
        return user;
    }
}

Verify the REST APIs

Case 1: Invalid Request (Entity Not found): enter image description here

Case 2: Invalid Request(User Id must be numeric):

enter image description here

Case 3: Valid request: enter image description here

I will suggest to handle Exception/Throwable in the @Singleton handler instead of a custom exception and try catch everywhere in the code. Unlike springboot, Micronaut doesn't allow to handle multiple exception types in handler. So you can refer to below code:

@Error(global = true, exception = Exception::class)
override fun handle(request: HttpRequest<*>?, 
     exception: Exception?): HttpResponse<ErrResp> {

     var errorMessage: ErrResp

     when (exception) {
         is CustomException -> {
             errorMessage = exception?.error?.let {
                 ErrResp(
                     it.code,
                     it.message,
                     it.httpStatus
                 )
             }!!
         }
         is HttpStatusException -> {
             errorMessage = ErrResp(
                 500,
                 exception.message,
                 HttpStatus.SERVICE_UNAVAILABLE
             )
         }
         else -> {
             errorMessage = ErrResp(
                 500,
                 exception?.message,
                 HttpStatus.INTERNAL_SERVER_ERROR
             )
         }
     }
     return getResponse(errorMessage?.httpStatus, errorMessage)
 }

 private fun getResponse(httpStatus: HttpStatus?, 
    errorMessage: ErrorResponse): HttpResponse<ErrorResponse> {

    return if (httpStatus?.equals(HttpStatus.BAD_REQUEST) == true)
        HttpResponse.badRequest(errorMessage)

    else if (httpStatus?.equals(HttpStatus.INTERNAL_SERVER_ERROR) == true)

    else
        HttpResponse.serverError(errorMessage)

}

I have created a ErrorResponse class to send custom error response to the UI

data class ErrResp(
    var errorCode: String = "",

    var errorMessage: String? = "",

    var httpStatus: HttpStatus?,
)
Related