Error handler threw an exception

Offset commit cannot be completed since the consumer is not part of an active group for auto partition assignment; it is likely that the consumer was kicked out of the group. Kafka is working if an...

Offset commit cannot be completed since the consumer is not part of an active group for auto partition assignment; it is likely that the consumer was kicked out of the group.

Kafka is working if and it is consuming message and the consumed message will send to a service via restTemplate but somehow restTemplate is failed due to connection but I’m getting below error for Kafka.
What is the problem for Kafka to consume when restTemplate is failed?

[org.springframework.kafka.KafkaListenerEndpointContainer#3-0-C-1] ERROR o.s.k.l.KafkaMessageListenerContainer$ListenerConsumer — Error handler threw an exception
org.apache.kafka.clients.consumer.CommitFailedException: Offset commit cannot be completed since the consumer is not part of an active group for auto partition assignment; it is likely that the consumer was kicked out of the group.
at org.apache.kafka.clients.consumer.internals.ConsumerCoordinator.sendOffsetCommitRequest(ConsumerCoordinator.java:1116)
at org.apache.kafka.clients.consumer.internals.ConsumerCoordinator.commitOffsetsSync(ConsumerCoordinator.java:983)
at org.apache.kafka.clients.consumer.KafkaConsumer.commitSync(KafkaConsumer.java:1510)
at org.springframework.kafka.listener.KafkaMessageListenerContainer$ListenerConsumer.doCommitSync(KafkaMessageListenerContainer.java:2209)
at org.springframework.kafka.listener.KafkaMessageListenerContainer$ListenerConsumer.commitSync(KafkaMessageListenerContainer.java:2204)
at org.springframework.kafka.listener.KafkaMessageListenerContainer$ListenerConsumer.commitIfNecessary(KafkaMessageListenerContainer.java:2190)
at org.springframework.kafka.listener.KafkaMessageListenerContainer$ListenerConsumer.processCommits(KafkaMessageListenerContainer.java:2004)
at org.springframework.kafka.listener.KafkaMessageListenerContainer$ListenerConsumer.invokeErrorHandler(KafkaMessageListenerContainer.java:1906)
at org.springframework.kafka.listener.KafkaMessageListenerContainer$ListenerConsumer.doInvokeRecordListener(KafkaMessageListenerContainer.java:1812)
at org.springframework.kafka.listener.KafkaMessageListenerContainer$ListenerConsumer.doInvokeWithRecords(KafkaMessageListenerContainer.java:1739)
at org.springframework.kafka.listener.KafkaMessageListenerContainer$ListenerConsumer.invokeRecordListener(KafkaMessageListenerContainer.java:1636)
at org.springframework.kafka.listener.KafkaMessageListenerContainer$ListenerConsumer.invokeListener(KafkaMessageListenerContainer.java:1366)
at org.springframework.kafka.listener.KafkaMessageListenerContainer$ListenerConsumer.pollAndInvoke(KafkaMessageListenerContainer.java:1082)
at org.springframework.kafka.listener.KafkaMessageListenerContainer$ListenerConsumer.run(KafkaMessageListenerContainer.java:990)
at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511)
at java.util.concurrent.FutureTask.run(FutureTask.java:266)

asked Oct 13, 2020 at 12:05

nitinsridar's user avatar

It generally means that the consumer took too long to process the records returned by the poll().

You need to decrease max.poll.records and/or increase max.poll.interval.ms so that all records returned by the poll are processed before the interval expires.

answered Oct 13, 2020 at 14:51

Gary Russell's user avatar

Gary RussellGary Russell

160k14 gold badges131 silver badges167 bronze badges

I have RabbitMQ running server A and a consumer of the rabbitMQ in server B .I’am using spring-boot rabbitMQ and this is my configuration.

  @Bean
    public Queue queue() {
        return new Queue(queueName, true);
    }

@Bean
public TopicExchange exchange() {
    return new TopicExchange(topicExchangeName);
}

@Bean
Binding binding(Queue queue, TopicExchange exchange) {
    return BindingBuilder.bind(queue).to(exchange).with(prefix);
}

@Bean
ConnectionFactory connectionFactory(){
    CachingConnectionFactory connectionFactory =new CachingConnectionFactory() ;
    connectionFactory.setCacheMode(CachingConnectionFactory.CacheMode.CHANNEL);
    connectionFactory.setHost(host);
    connectionFactory.setPort(port);
    connectionFactory.setUsername(userName);
    connectionFactory.setPassword(password);
    return connectionFactory;
}

@Bean
@DependsOn("connectionFactory")
SimpleMessageListenerContainer container(ConnectionFactory connectionFactory,
                                         MessageListenerAdapter listenerAdapter) {
    SimpleMessageListenerContainer container = new SimpleMessageListenerContainer();
    container.setConnectionFactory(connectionFactory);
    container.setQueueNames(queueName);
    container.setMessageListener(listenerAdapter);
    return container;
}

@Bean
MessageListenerAdapter listenerAdapter(RabbitConsumer receiver) {
    return new MessageListenerAdapter(receiver, "consumeMessage");
}

but when i deploy and run it , i keep getting this error

 2020-03-04 | 07:54:17.344 | container-1 | ERROR | o.s.a.r.l.SimpleMessageListenerContainer | Execution of Rabbit message listener failed, and the error handler threw an exception
org.springframework.amqp.AmqpRejectAndDontRequeueException: Error Handler converted exception to fatal
    at org.springframework.amqp.rabbit.listener.ConditionalRejectingErrorHandler.handleError(ConditionalRejectingErrorHandler.java:105) ~[spring-rabbit-2.1.5.RELEASE.jar:2.1.5.RELEASE]
    at org.springframework.amqp.rabbit.listener.AbstractMessageListenerContainer.invokeErrorHandler(AbstractMessageListenerContainer.java:1378) [spring-rabbit-2.1.5.RELEASE.jar:2.1.5.RELEASE]
    at org.springframework.amqp.rabbit.listener.AbstractMessageListenerContainer.handleListenerException(AbstractMessageListenerContainer.java:1631) [spring-rabbit-2.1.5.RELEASE.jar:2.1.5.RELEASE]
    at org.springframework.amqp.rabbit.listener.AbstractMessageListenerContainer.executeListener(AbstractMessageListenerContainer.java:1424) [spring-rabbit-2.1.5.RELEASE.jar:2.1.5.RELEASE]
    at org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer.doReceiveAndExecute(SimpleMessageListenerContainer.java:870) [spring-rabbit-2.1.5.RELEASE.jar:2.1.5.RELEASE]
    at org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer.receiveAndExecute(SimpleMessageListenerContainer.java:854) [spring-rabbit-2.1.5.RELEASE.jar:2.1.5.RELEASE]
    at org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer.access$1600(SimpleMessageListenerContainer.java:78) [spring-rabbit-2.1.5.RELEASE.jar:2.1.5.RELEASE]
    at org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer$AsyncMessageProcessingConsumer.mainLoop(SimpleMessageListenerContainer.java:1137) [spring-rabbit-2.1.5.RELEASE.jar:2.1.5.RELEASE]
    at org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer$AsyncMessageProcessingConsumer.run(SimpleMessageListenerContainer.java:1043) [spring-rabbit-2.1.5.RELEASE.jar:2.1.5.RELEASE]
    at java.lang.Thread.run(Thread.java:748) [na:1.8.0_242]
Caused by: org.springframework.amqp.rabbit.listener.exception.ListenerExecutionFailedException: Listener threw exception
    at org.springframework.amqp.rabbit.listener.AbstractMessageListenerContainer.wrapToListenerExecutionFailedExceptionIfNeeded(AbstractMessageListenerContainer.java:1651) [spring-rabbit-2.1.5.RELEASE.jar:2.1.5.RELEASE]
    at org.springframework.amqp.rabbit.listener.AbstractMessageListenerContainer.doInvokeListener(AbstractMessageListenerContainer.java:1555) [spring-rabbit-2.1.5.RELEASE.jar:2.1.5.RELEASE]
    at org.springframework.amqp.rabbit.listener.AbstractMessageListenerContainer.actualInvokeListener(AbstractMessageListenerContainer.java:1478) [spring-rabbit-2.1.5.RELEASE.jar:2.1.5.RELEASE]
    at org.springframework.amqp.rabbit.listener.AbstractMessageListenerContainer.invokeListener(AbstractMessageListenerContainer.java:1466) [spring-rabbit-2.1.5.RELEASE.jar:2.1.5.RELEASE]
    at org.springframework.amqp.rabbit.listener.AbstractMessageListenerContainer.doExecuteListener(AbstractMessageListenerContainer.java:1461) [spring-rabbit-2.1.5.RELEASE.jar:2.1.5.RELEASE]
    at org.springframework.amqp.rabbit.listener.AbstractMessageListenerContainer.executeListener(AbstractMessageListenerContainer.java:1410) [spring-rabbit-2.1.5.RELEASE.jar:2.1.5.RELEASE]
    ... 6 common frames omitted
Caused by: org.springframework.amqp.support.converter.MessageConversionException: failed to convert serialized Message content
    at org.springframework.amqp.support.converter.SimpleMessageConverter.fromMessage(SimpleMessageConverter.java:114) ~[spring-amqp-2.1.5.RELEASE.jar:2.1.5.RELEASE]
    at org.springframework.amqp.rabbit.listener.adapter.AbstractAdaptableMessageListener.extractMessage(AbstractAdaptableMessageListener.java:285) ~[spring-rabbit-2.1.5.RELEASE.jar:2.1.5.RELEASE]
    at org.springframework.amqp.rabbit.listener.adapter.MessageListenerAdapter.onMessage(MessageListenerAdapter.java:282) ~[spring-rabbit-2.1.5.RELEASE.jar:2.1.5.RELEASE]
    at org.springframework.amqp.rabbit.listener.AbstractMessageListenerContainer.doInvokeListener(AbstractMessageListenerContainer.java:1552) [spring-rabbit-2.1.5.RELEASE.jar:2.1.5.RELEASE]
    ... 10 common frames omitted
Caused by: java.lang.IllegalArgumentException: Could not deserialize object
    at org.springframework.amqp.utils.SerializationUtils.deserialize(SerializationUtils.java:95) ~[spring-amqp-2.1.5.RELEASE.jar:2.1.5.RELEASE]
    at org.springframework.amqp.support.converter.SimpleMessageConverter.fromMessage(SimpleMessageConverter.java:110) ~[spring-amqp-2.1.5.RELEASE.jar:2.1.5.RELEASE]
    ... 13 common frames omitted
Caused by: java.io.InvalidClassException: org.hibernate.collection.internal.AbstractPersistentCollection; local class incompatible: stream classdesc serialVersionUID = -7009541904449236102, local class serialVersionUID = 6275967693128102740
    at java.io.ObjectStreamClass.initNonProxy(ObjectStreamClass.java:699) ~[na:1.8.0_242]
    at java.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:1940) ~[na:1.8.0_242]
    at java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1806) ~[na:1.8.0_242]
    at java.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:1940) ~[na:1.8.0_242]
    at java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1806) ~[na:1.8.0_242]
    at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:2097) ~[na:1.8.0_242]
    at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1625) ~[na:1.8.0_242]
    at java.io.ObjectInputStream.defaultReadFields(ObjectInputStream.java:2342) ~[na:1.8.0_242]
    at java.io.ObjectInputStream.readSerialData(ObjectInputStream.java:2222) ~[na:1.8.0_242]
    at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:2124) ~[na:1.8.0_242]
    at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1625) ~[na:1.8.0_242]
    at java.io.ObjectInputStream.defaultReadFields(ObjectInputStream.java:2342) ~[na:1.8.0_242]
    at java.io.ObjectInputStream.readSerialData(ObjectInputStream.java:2222) ~[na:1.8.0_242]
    at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:2124) ~[na:1.8.0_242]
    at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1625) ~[na:1.8.0_242]
    at java.io.ObjectInputStream.readObject(ObjectInputStream.java:465) ~[na:1.8.0_242]
    at java.io.ObjectInputStream.readObject(ObjectInputStream.java:423) ~[na:1.8.0_242]
    at org.springframework.amqp.utils.SerializationUtils.deserialize(SerializationUtils.java:92) ~[spring-amqp-2.1.5.RELEASE.jar:2.1.5.RELEASE]
    ... 14 common frames omitted

i have tried making the consumer accept both byte array and string but to no vain.
i’am

it’s been 2 days and i’am stuck on this issue.

1. Overview

This tutorial will illustrate how to implement Exception Handling with Spring for a REST API. We’ll also get a bit of historical overview and see which new options the different versions introduced.

Before Spring 3.2, the two main approaches to handling exceptions in a Spring MVC application were HandlerExceptionResolver or the @ExceptionHandler annotation. Both have some clear downsides.

Since 3.2, we’ve had the @ControllerAdvice annotation to address the limitations of the previous two solutions and to promote a unified exception handling throughout a whole application.

Now Spring 5 introduces the ResponseStatusException class — a fast way for basic error handling in our REST APIs.

All of these do have one thing in common: They deal with the separation of concerns very well. The app can throw exceptions normally to indicate a failure of some kind, which will then be handled separately.

Finally, we’ll see what Spring Boot brings to the table and how we can configure it to suit our needs.

2. Solution 1: the Controller-Level @ExceptionHandler

The first solution works at the @Controller level. We will define a method to handle exceptions and annotate that with @ExceptionHandler:

public class FooController{
    
    //...
    @ExceptionHandler({ CustomException1.class, CustomException2.class })
    public void handleException() {
        //
    }
}

This approach has a major drawback: The @ExceptionHandler annotated method is only active for that particular Controller, not globally for the entire application. Of course, adding this to every controller makes it not well suited for a general exception handling mechanism.

We can work around this limitation by having all Controllers extend a Base Controller class.

However, this solution can be a problem for applications where, for whatever reason, that isn’t possible. For example, the Controllers may already extend from another base class, which may be in another jar or not directly modifiable, or may themselves not be directly modifiable.

Next, we’ll look at another way to solve the exception handling problem — one that is global and doesn’t include any changes to existing artifacts such as Controllers.

3. Solution 2: the HandlerExceptionResolver

The second solution is to define an HandlerExceptionResolver. This will resolve any exception thrown by the application. It will also allow us to implement a uniform exception handling mechanism in our REST API.

Before going for a custom resolver, let’s go over the existing implementations.

3.1. ExceptionHandlerExceptionResolver

This resolver was introduced in Spring 3.1 and is enabled by default in the DispatcherServlet. This is actually the core component of how the @ExceptionHandler mechanism presented earlier works.

3.2. DefaultHandlerExceptionResolver

This resolver was introduced in Spring 3.0, and it’s enabled by default in the DispatcherServlet.

It’s used to resolve standard Spring exceptions to their corresponding HTTP Status Codes, namely Client error 4xx and Server error 5xx status codes. Here’s the full list of the Spring Exceptions it handles and how they map to status codes.

While it does set the Status Code of the Response properly, one limitation is that it doesn’t set anything to the body of the Response. And for a REST API — the Status Code is really not enough information to present to the Client — the response has to have a body as well, to allow the application to give additional information about the failure.

This can be solved by configuring view resolution and rendering error content through ModelAndView, but the solution is clearly not optimal. That’s why Spring 3.2 introduced a better option that we’ll discuss in a later section.

3.3. ResponseStatusExceptionResolver

This resolver was also introduced in Spring 3.0 and is enabled by default in the DispatcherServlet.

Its main responsibility is to use the @ResponseStatus annotation available on custom exceptions and to map these exceptions to HTTP status codes.

Such a custom exception may look like:

@ResponseStatus(value = HttpStatus.NOT_FOUND)
public class MyResourceNotFoundException extends RuntimeException {
    public MyResourceNotFoundException() {
        super();
    }
    public MyResourceNotFoundException(String message, Throwable cause) {
        super(message, cause);
    }
    public MyResourceNotFoundException(String message) {
        super(message);
    }
    public MyResourceNotFoundException(Throwable cause) {
        super(cause);
    }
}

The same as the DefaultHandlerExceptionResolver, this resolver is limited in the way it deals with the body of the response — it does map the Status Code on the response, but the body is still null.

3.4. Custom HandlerExceptionResolver

The combination of DefaultHandlerExceptionResolver and ResponseStatusExceptionResolver goes a long way toward providing a good error handling mechanism for a Spring RESTful Service. The downside is, as mentioned before, no control over the body of the response.

Ideally, we’d like to be able to output either JSON or XML, depending on what format the client has asked for (via the Accept header).

This alone justifies creating a new, custom exception resolver:

@Component
public class RestResponseStatusExceptionResolver extends AbstractHandlerExceptionResolver {

    @Override
    protected ModelAndView doResolveException(
      HttpServletRequest request, 
      HttpServletResponse response, 
      Object handler, 
      Exception ex) {
        try {
            if (ex instanceof IllegalArgumentException) {
                return handleIllegalArgument(
                  (IllegalArgumentException) ex, response, handler);
            }
            ...
        } catch (Exception handlerException) {
            logger.warn("Handling of [" + ex.getClass().getName() + "] 
              resulted in Exception", handlerException);
        }
        return null;
    }

    private ModelAndView 
      handleIllegalArgument(IllegalArgumentException ex, HttpServletResponse response) 
      throws IOException {
        response.sendError(HttpServletResponse.SC_CONFLICT);
        String accept = request.getHeader(HttpHeaders.ACCEPT);
        ...
        return new ModelAndView();
    }
}

One detail to notice here is that we have access to the request itself, so we can consider the value of the Accept header sent by the client.

For example, if the client asks for application/json, then, in the case of an error condition, we’d want to make sure we return a response body encoded with application/json.

The other important implementation detail is that we return a ModelAndView — this is the body of the response, and it will allow us to set whatever is necessary on it.

This approach is a consistent and easily configurable mechanism for the error handling of a Spring REST Service.

It does, however, have limitations: It’s interacting with the low-level HtttpServletResponse and fits into the old MVC model that uses ModelAndView, so there’s still room for improvement.

4. Solution 3: @ControllerAdvice

Spring 3.2 brings support for a global @ExceptionHandler with the @ControllerAdvice annotation.

This enables a mechanism that breaks away from the older MVC model and makes use of ResponseEntity along with the type safety and flexibility of @ExceptionHandler:

@ControllerAdvice
public class RestResponseEntityExceptionHandler 
  extends ResponseEntityExceptionHandler {

    @ExceptionHandler(value 
      = { IllegalArgumentException.class, IllegalStateException.class })
    protected ResponseEntity<Object> handleConflict(
      RuntimeException ex, WebRequest request) {
        String bodyOfResponse = "This should be application specific";
        return handleExceptionInternal(ex, bodyOfResponse, 
          new HttpHeaders(), HttpStatus.CONFLICT, request);
    }
}

The@ControllerAdvice annotation allows us to consolidate our multiple, scattered @ExceptionHandlers from before into a single, global error handling component.

The actual mechanism is extremely simple but also very flexible:

  • It gives us full control over the body of the response as well as the status code.
  • It provides mapping of several exceptions to the same method, to be handled together.
  • It makes good use of the newer RESTful ResposeEntity response.

One thing to keep in mind here is to match the exceptions declared with @ExceptionHandler to the exception used as the argument of the method.

If these don’t match, the compiler will not complain — no reason it should — and Spring will not complain either.

However, when the exception is actually thrown at runtime, the exception resolving mechanism will fail with:

java.lang.IllegalStateException: No suitable resolver for argument [0] [type=...]
HandlerMethod details: ...

5. Solution 4: ResponseStatusException (Spring 5 and Above)

Spring 5 introduced the ResponseStatusException class.

We can create an instance of it providing an HttpStatus and optionally a reason and a cause:

@GetMapping(value = "/{id}")
public Foo findById(@PathVariable("id") Long id, HttpServletResponse response) {
    try {
        Foo resourceById = RestPreconditions.checkFound(service.findOne(id));

        eventPublisher.publishEvent(new SingleResourceRetrievedEvent(this, response));
        return resourceById;
     }
    catch (MyResourceNotFoundException exc) {
         throw new ResponseStatusException(
           HttpStatus.NOT_FOUND, "Foo Not Found", exc);
    }
}

What are the benefits of using ResponseStatusException?

  • Excellent for prototyping: We can implement a basic solution quite fast.
  • One type, multiple status codes: One exception type can lead to multiple different responses. This reduces tight coupling compared to the @ExceptionHandler.
  • We won’t have to create as many custom exception classes.
  • We have more control over exception handling since the exceptions can be created programmatically.

And what about the tradeoffs?

  • There’s no unified way of exception handling: It’s more difficult to enforce some application-wide conventions as opposed to @ControllerAdvice, which provides a global approach.
  • Code duplication: We may find ourselves replicating code in multiple controllers.

We should also note that it’s possible to combine different approaches within one application.

For example, we can implement a @ControllerAdvice globally but also ResponseStatusExceptions locally.

However, we need to be careful: If the same exception can be handled in multiple ways, we may notice some surprising behavior. A possible convention is to handle one specific kind of exception always in one way.

For more details and further examples, see our tutorial on ResponseStatusException.

6. Handle the Access Denied in Spring Security

The Access Denied occurs when an authenticated user tries to access resources that he doesn’t have enough authorities to access.

6.1. REST and Method-Level Security

Finally, let’s see how to handle Access Denied exception thrown by method-level security annotations – @PreAuthorize, @PostAuthorize, and @Secure.

Of course, we’ll use the global exception handling mechanism that we discussed earlier to handle the AccessDeniedException as well:

@ControllerAdvice
public class RestResponseEntityExceptionHandler 
  extends ResponseEntityExceptionHandler {

    @ExceptionHandler({ AccessDeniedException.class })
    public ResponseEntity<Object> handleAccessDeniedException(
      Exception ex, WebRequest request) {
        return new ResponseEntity<Object>(
          "Access denied message here", new HttpHeaders(), HttpStatus.FORBIDDEN);
    }
    
    ...
}

7. Spring Boot Support

Spring Boot provides an ErrorController implementation to handle errors in a sensible way.

In a nutshell, it serves a fallback error page for browsers (a.k.a. the Whitelabel Error Page) and a JSON response for RESTful, non-HTML requests:

{
    "timestamp": "2019-01-17T16:12:45.977+0000",
    "status": 500,
    "error": "Internal Server Error",
    "message": "Error processing the request!",
    "path": "/my-endpoint-with-exceptions"
}

As usual, Spring Boot allows configuring these features with properties:

  • server.error.whitelabel.enabled: can be used to disable the Whitelabel Error Page and rely on the servlet container to provide an HTML error message
  • server.error.include-stacktrace: with an always value; includes the stacktrace in both the HTML and the JSON default response
  • server.error.include-message: since version 2.3, Spring Boot hides the message field in the response to avoid leaking sensitive information; we can use this property with an always value to enable it

Apart from these properties, we can provide our own view-resolver mapping for /error, overriding the Whitelabel Page.

We can also customize the attributes that we want to show in the response by including an ErrorAttributes bean in the context. We can extend the DefaultErrorAttributes class provided by Spring Boot to make things easier:

@Component
public class MyCustomErrorAttributes extends DefaultErrorAttributes {

    @Override
    public Map<String, Object> getErrorAttributes(
      WebRequest webRequest, ErrorAttributeOptions options) {
        Map<String, Object> errorAttributes = 
          super.getErrorAttributes(webRequest, options);
        errorAttributes.put("locale", webRequest.getLocale()
            .toString());
        errorAttributes.remove("error");

        //...

        return errorAttributes;
    }
}

If we want to go further and define (or override) how the application will handle errors for a particular content type, we can register an ErrorController bean.

Again, we can make use of the default BasicErrorController provided by Spring Boot to help us out.

For example, imagine we want to customize how our application handles errors triggered in XML endpoints. All we have to do is define a public method using the @RequestMapping, and stating it produces application/xml media type:

@Component
public class MyErrorController extends BasicErrorController {

    public MyErrorController(
      ErrorAttributes errorAttributes, ServerProperties serverProperties) {
        super(errorAttributes, serverProperties.getError());
    }

    @RequestMapping(produces = MediaType.APPLICATION_XML_VALUE)
    public ResponseEntity<Map<String, Object>> xmlError(HttpServletRequest request) {
        
    // ...

    }
}

Note: here we’re still relying on the server.error.* Boot properties we might have been defined in our project, which are bound to the ServerProperties bean.

8. Conclusion

This article discussed several ways to implement an exception handling mechanism for a REST API in Spring, starting with the older mechanism and continuing with the Spring 3.2 support and into 4.x and 5.x.

As always, the code presented in this article is available over on GitHub.

For the Spring Security-related code, you can check the spring-security-rest module.

Get started with Spring 5 and Spring Boot 2, through the Learn Spring course:

>> THE COURSE

    This section describes the error handling capabilities provided by the CAP Java SDK.

    Content

    Overview

    The CAP Java SDK provides two different ways to indicate errors:

    • By throwing an exception: This completely aborts the event processing and rollbacks the transaction.
    • By using the Messages API: This adds errors, warnings, info, or success messages to the currently processed request, but doesn’t affect the event processing or the transaction.

    The message texts for both exceptions and the Messages API can use formatting and localization.

    Exceptions

    Any exception that is thrown by an event handler method aborts the processing of the current event and causes any active transaction to be rolled back.
    To indicate further details about the error, such as a suggested mapping to an HTTP response code, the CAP Java SDK provides a generic unchecked exception class, called ServiceException.
    It’s recommended to use this exception class, when throwing an exception in an event handler.

    When creating a new instance of ServiceException you can specify an ErrorStatus object, through which an internal error code and a mapping to an HTTP status code can be indicated.
    An enum ErrorStatuses exists, which lists many useful HTTP error codes already.
    If no such error status is set when creating the ServiceException, it defaults to an internal server error (HTTP status code 500).

    // default error status
    throw new ServiceException("An internal server error occurred", originalException);
    // specifying an error status
    throw new ServiceException(ErrorStatuses.CONFLICT, "Not enough stock available")
    // specifying an error status and the original exception
    throw new ServiceException(ErrorStatuses.BAD_REQUEST, "No book title specified", originalException);
    

    The OData V4 adapter turns all exceptions into an OData error response to indicate the error to the client.

    Messages

    The Messages API allows event handlers to add errors, warnings, info, or success messages to the currently processed request. Adding info, warning or success messages doesn’t affect the event processing or the transaction. For error messages by default a ServiceException is thrown at the end of the Before handler phase. You can change this by setting cds.errors.combined to false.

    The Messages interface provides a logger-like API to collect these messages. Additional optional details can be added to the Message using a builder API.
    You can access the Messages API from the Event Context:

    context.getMessages().success("The order was successfully placed");
    

    In Spring, you can also access it using Dependency Injection:

    @Autowired
    Messages messages;
    
    messages.warn("No book title specified");
    messages.error("The book is no longer available").code("BNA").longTextUrl("/help/book-not-available");
    

    The OData V4 adapter collects these messages and writes them into the sap-messages HTTP header by default.
    However, when an OData V4 error response is returned, because the request was aborted by an exception, the messages are instead written into the details section of the error response.
    Writing the messages into explicitly modeled messages properties isn’t yet supported.

    SAP Fiori uses these messages to display detailed information on the UI. The style how a message appears on the UI depends on the severity of the message.

    Throwing a ServiceException from Error Messages

    It is also possible to throw a ServiceException from error messages. This can, for example, be useful to cancel a request after collecting multiple validation errors. The individual validation checks will collect error messages in the Messages API. After the validation checks have been run, you call the throwIfError() method. Only if error messages have been collected, this method cancels the request with a ServiceException:

    // throw a ServiceException, if any error messages have been added to the current request
    messages.throwIfError();
    

    If there are any collected error messages, this method creates a ServiceException from one of these error messages.
    The OData V4 adapter turns this exception into an OData error response to indicate the error to the client. The remaining error messages are written into the details section of the error response.

    If the CDS property cds.errors.combined is set to true (default), Messages.throwIfError() is automatically called at the end of the Before handler phase to abort the event processing in case of errors. It is recommended to use the Messages API for validation errors and rely on the framework calling Messages.throwIfError() automatically, instead of throwing a ServiceException.

    Formatting and Localization

    Texts passed to both ServiceException and the Messages API can be formatted and localized.
    By default you can use SLF4J’s messaging formatting style to format strings passed to both APIs.

    // message with placeholders
    messages.warn("Can't order {} books: Not enough on stock", orderQuantity);
    // on ServiceException last argument can always be the causing exception
    throw new ServiceException(ErrorStatuses.BAD_REQUEST, "Invalid number: '{}'", wrongNumber, originalException);
    

    You can localize these strings, by putting them into property files and passing the key of the message from the properties file to the API instead of the message text.

    When running your application on Spring, the CAP Java SDK integrates with Spring’s support for handling text resource bundles. This handling by default expects translated texts in a messages.properties file under src/main/resources.

    The texts defined in the resource bundles can be formatted based on the syntax defined by java.text.MessageFormat.
    When the message or exception text is sent to the client it’s localized using the client’s locale, as described here.

    messages.properties

    my.message.key = This is a localized message with {0} parameters
    

    messages_de.properties

    my.message.key = Das ist ein übersetzter Text mit {0} Parametern
    
    // localized message with placeholders
    messages.warn("my.message.key", paramNumber);
    // localized message with placeholders and additional exception
    throw new ServiceException(ErrorStatuses.BAD_REQUEST, "my.message.key", paramNumber, originalException);
    

    Exporting the Default Messages

    As of CAP Java 1.10.0, you can extract the available default messages as a resource bundle file for further processing (for example, translation). Therefore, the delivery artifact cds-services-utils contains a resource bundle cds-messages-template.properties with all available error codes and default messages. Application developers can use this template to customize error messages thrown by the CAP Java SDK in the application.

    1. Download the artifact or get it from the local Maven repository in ~/.m2/repository/com/sap/cds/cds-services-utils/<VERSION>/cds-services-utils-<VERSION>.jar.
    2. Extract the file.
       jar -f cds-services-utils-<VERSION>.jar -x cds-messages-template.properties
      

      <VERSION> is the version of CAP Java you’re using in your project.

    3. Rename the extracted file cds-messages-template.properties appropriately (for example, to cds-messages.properties) and move it to the resource directory of your application.
    4. In your Spring Boot application, you have to register this additional resource bundle accordingly.

    Now, you’re able to customize the stack error messages in your application.

    With new CAP Java versions, there could be also new or changed error messages in the stack. To identify these changes, export cds-messages-template.properties from the new CAP Java version and compare it with the previous version using a diff tool.

    Target

    When SAP Fiori interprets messages it can handle an additional target property, which, for example, specifies which element of an entity the message refers to. SAP Fiori can use this information to display the message along the corresponding field on the UI.
    When specifying messages in the sap-messages HTTP header, SAP Fiori mostly ignores the target value.
    Therefore, specifying the target can only correctly be used when throwing a ServiceException as SAP Fiori correctly handles the target property in OData V4 error responses.

    A message target is always relative to an input parameter in the event context. For CRUD-based events this is usually the cqn parameter you can find in the underlying map of the event context. For action or function events you find their input parameters in the map, as well.

    Therefore, when creating a message target, one of these event context parameters needs to be selected to specify what the relative message target path refers to.

    By default a message target always refers to the CQN statement of the event. In case of CRUD events this is the targeted entity. In case of bound actions and functions this is the entity that the action or function was bound to.

    Let’s illustrate this with the following example:

    entity Books : cuid, managed {
        title  : localized String(111);
        descr  : localized String(1111);
        author : Association to Authors;
    }
    
    entity Authors : cuid, managed {
        name         : String(111);
        dateOfBirth  : Date;
        placeOfBirth : String;
        books        : Association to many Books
                           on books.author = $self;
    }
    
    entity Reviews : cuid, managed {
        book   : Association to Books;
        rating : Rating;
        title  : String(111);
        text   : String(1111);
    }
    
    service CatalogService {
        type Reviewer {
            firstName : String;
            lastName  : String;
        }
        entity Books as projection on my.Books excluding {
            createdBy,
            modifiedBy
        } actions {
            action addReview(reviewer : Reviewer, rating : Integer, title : String, text : String) returns Reviews;
        };
    }
    

    Here, we have a CatalogService that exposes et al. the Books entity and a Books bound action addReview.

    CRUD Events

    Within a Before handler that triggers on inserts of new books a message target can only refer to the cqn parameter:

    @Before
    public void validateTitle(CdsCreateEventContext context, Books book) {
    
        // ...
    
        // event context contains the "cqn" key
    
        // implicitly referring to cqn
        throw new ServiceException(ErrorStatuses.BAD_REQUEST, "No title specified")
            .messageTarget(b -> b.get("title"));
    
        // which is equivalent to explicitly referring to cqn
        throw new ServiceException(ErrorStatuses.BAD_REQUEST, "No title specified")
            .messageTarget("cqn", b -> b.get("title"));
    
        // which is the same as (using plain string)
        throw new ServiceException(ErrorStatuses.BAD_REQUEST, "No title specified")
            .messageTarget("title");
    
        // ...
    }
    

    Instead of using the generic API for creating the relative message target path, CAP Java SDK also provides a typed API backed by the CDS model:

    @Before
    public void validateTitle(CdsCreateEventContext context, Books book) {
        // ...
    
        // implicitly referring to cqn
        throw new ServiceException(ErrorStatuses.BAD_REQUEST, "No title specified")
            .messageTarget(Books_.class, b -> b.title());
    
        // ...
    }
    

    This also works for nested paths that with associations:

    @Before
    public void validateAuthorName(CdsCreateEventContext context, Books book) {
        // ...
    
        // using un-typed API
        throw new ServiceException(ErrorStatuses.BAD_REQUEST, "No title specified")
            .messageTarget(b -> b.to("author").get("name"));
    
        // using typed API
        throw new ServiceException(ErrorStatuses.BAD_REQUEST, "No author name specified")
            .messageTarget(Books_.class, b -> b.author().name());
    
        // ...
    }
    

    Bound Actions and Functions

    The same applies to message targets that refer to an action or function input parameter:

    @Before
    public void validateReview(AddReviewContext context) {
        // ...
    
        // event context contains the keys "reviewer", "rating", "title", "text",
        // which are the input parameters of the action "addReview"
    
        // referring to action parameter "reviewer", targeting "firstName"
        throw new ServiceException(ErrorStatuses.BAD_REQUEST, "Invalid reviewer first name")
            .messageTarget("reviewer", r -> r.get("firstName"));
    
        // which is equivalent to using the typed API
        throw new ServiceException(ErrorStatuses.BAD_REQUEST, "Invalid reviewer first name")
            .messageTarget("reviewer", Reviewer_.class, r -> r.firstName());
    
        // targeting "rating"
        throw new ServiceException(ErrorStatuses.BAD_REQUEST, "Invalid review rating")
            .messageTarget("rating");
    
        // targeting "title"
        throw new ServiceException(ErrorStatuses.BAD_REQUEST, "Invalid review title")
            .messageTarget("title");
    
         // targeting "text"
        throw new ServiceException(ErrorStatuses.BAD_REQUEST, "Invalid review text")
            .messageTarget("text");
    
        // ...
    }
    

    If a message target refers to the cqn of the event context, for bound actions and functions that means, that the message target path is relative to the bound entity.

    For the addReview action that is the Books entity, as in the following example:

    @Before
    public void validateReview(AddReviewContext context) {
        // ...
    
        // referring to the bound entity `Books`
        throw new ServiceException(ErrorStatuses.BAD_REQUEST, "Invalid book description")
            .messageTarget(b -> b.get("descr"));
    
        // which is equivalent to
        throw new ServiceException(ErrorStatuses.BAD_REQUEST, "Invalid book description")
            .messageTarget(b -> b.descr());
    
        // or (using the typed API, referring to "cqn" implicitly)
        throw new ServiceException(ErrorStatuses.BAD_REQUEST, "Invalid book description")
            .messageTarget(Books_.class, b -> b.descr());
    
        // ...
    }
    

    The previous examples showcase the target creation with the ServiceException API, but the same can be done with the Message API and the respective target(...) methods.

    Error Handler

    An exception thrown in an event handler will stop the processing of the request. As part of that, protocol adapters trigger the ERROR_RESPONSE event of the Application Lifecycle Service. By default, this event combines the thrown exception and the messages from the RequestContext in a list to produce the error response. OData V4 and V2 protocol adapters will use this list to create an OData error response with the first entry being the main error and the remaining entries in the details section.

    You can add event handlers using the @After phase for the ERROR_RESPONSE event to augment or change the error responses:

    • Method getException() of ErrorResponseEventContext returns the exception that triggered the event.
    • Method getEventContexts() of ServiceException contains the list of event contexts, identifying the chain of processed events that led to the error. The first entry in the list is the context closest to the origin of the exception.

    You can use the exception and the list of events contexts (with service, entity and event name) to selectively apply your custom error response handling. Some exceptions, however, may not be associated with a context and the list of contexts will be empty for them.

    The list of messages available via getResult().getMessages() of the ErrorResponseEventContext contains the messages (see Messages API) the protocol adapter will use to generate the final error response. You can remove, reorder or add new messages to this list by using Message.create() . You can also override the resulting HTTP status with method getResult().setHttpStatus(). Use only statuses that indicate errors, meaning status code 400 or higher.

    Don’t create new messages in the Messages of the RequestContext (also available through context.getMessages()). They will not be included in the response. Only the result provided by the ErrorResponseEventContext is considered by the protocol adapter.

    In case your implementation of the error handler throws an exception, returns no messages or sets a non-error HTTP status, the error response will default to a generic internal server error with HTTP status 500 and will not display any error details.

    The following example of a simple error handler overrides the standard message text of authorization errors. Technically, it replaces the first message, that is the main error in OData, in the response with a new message that has a custom text, only for exceptions with error code CdsErrorStatuses.EVENT_FORBIDDEN.

    @Component
    @ServiceName(ApplicationLifecycleService.DEFAULT_NAME)
    public class SimpleExceptionHandler implements EventHandler {
    
      @After
      public void overrideMissingAuthMessage(ErrorResponseEventContext context) {
        if (context.getException().getErrorStatus().equals(CdsErrorStatuses.EVENT_FORBIDDEN)) {
            context.getResult().getMessages().set(0, Message.create(Severity.ERROR, "You cannot execute this action"));
        }
      }
    }
    

    The second example shows how to override validation messages triggered by the annotation @assert.range for a certain entity. The exception triggered by CAP contains a reference to the event context that can be used to identify the target entity. The target of each message can be used to identify the affected field, but keep in mind that targets are always relative to the root entity of the request. That means in case of deep inserts or updates, you need to match not only the entity that has annotations but also the parent entities.

    @Component
    @ServiceName(ApplicationLifecycleService.DEFAULT_NAME)
    public class ExceptionServiceErrorMessagesHandler implements EventHandler {
    
      @After
      public void overrideValidationMessages(ErrorResponseEventContext context) {
        context.getException().getEventContexts().stream().findFirst().ifPresent(originalContext -> {
          if (Books_.CDS_NAME.equals(originalContext.getTarget().getQualifiedName())) { // filter by entity
            List<Message> messages = context.getResult().getMessages();
            for(int i=0; i<messages.size(); ++i) {
              Message message = messages.get(i);
              if (CdsErrorStatuses.VALUE_OUT_OF_RANGE.getCodeString().equals(message.getCode())) { // filter by error code
                if (Books.PRICE.equals(message.getTarget().getRef().targetSegment().id())) { // filter by target
                  messages.set(i, Message.create(Severity.ERROR, "The exceptional price is not in defined range!", message));
                } else if (Books.STOCK.equals(message.getTarget().getRef().targetSegment().id())) {
                  messages.set(i, Message.create(Severity.ERROR, "The exceptional stock of specified items is not available!", message));
                }
              }
            }
          }
        });
      }
    }
    

    If you replace the message with a new one, make sure that you copy the code and target of the original. Otherwise, SAP Fiori clients may not be able to display them properly. Use method Message.create(Severity severity, String text, Message message) to create a new message and copy all additional attributes from the existing one.

    Понравилась статья? Поделить с друзьями:
  • Error handler stardew valley
  • Error handler re entered exiting now перевод
  • Error handler re entered exiting now autocad 2016 при печати
  • Error handler npm
  • Error handler kotlin