9

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

San Jaisy
  • 15,327
  • 34
  • 171
  • 290
  • why not create an exception handler specifically for `io.micronaut.core.serialize.exceptions.SerializationException` – Archmede May 08 '21 at 23:39
  • I tried creating a handler for `Exception` but it didn't work, so I instead did `class ConversionErrorHandler : ExceptionHandler` and it worked fine – Archmede May 08 '21 at 23:41
  • 1
    Also, you should try annotating your class with `import io.micronaut.context.annotation.Primary` so it favours your exception handler over others. – Archmede May 08 '21 at 23:41
  • @Archmede can you please post answer – San Jaisy May 09 '21 at 14:59
  • Check my answer here https://stackoverflow.com/questions/56676486/constraintviolationexception-handler-isnt-executed-in-micronaut/67453167#67453167 – Archmede May 09 '21 at 23:45

4 Answers4

3

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

Sibin Rasiya
  • 1,132
  • 1
  • 11
  • 15
2

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.

James Kleeh
  • 12,094
  • 5
  • 34
  • 61
  • 1
    Well, the above code doesn't catch any exception during the runtime, but if I throw an exception manually it has been caught. I have a rabbit producer which throws an exception, but those exception not been caught – San Jaisy Jul 13 '21 at 02:18
2

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);
            });
}
San Jaisy
  • 15,327
  • 34
  • 171
  • 290
0

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?,
)
  • Where did you declare this method? i'm trying to follow this approach but it's not working even when I'm declaring same method inside a @Singleton class – Luis Miguel Oct 10 '22 at 16:33