spring-rest-exception-handler

A convenient Spring MVC exception handler for RESTful APIs.

APACHE-2.0 License

Stars
355
Committers
6

= Spring REST Exception handler :source-language: java // Project meta :name: spring-rest-exception-handler :version: 1.2.0 :group-id: cz.jirutka.spring :artifact-id: {name} :gh-name: jirutka/{name} :gh-branch: master :codacy-id: ca5dbac87d564725b6640a67b2b7ea35 // URIs :src-base: link:src/main/java/cz/jirutka/spring/exhandler :spring-jdoc-uri: https://docs.spring.io/spring/docs/current/javadoc-api/org/springframework

ifdef::env-github[] image:https://travis-ci.org/{gh-name}.svg?branch={gh-branch}["Build Status", link="https://travis-ci.org/{gh-name}"] image:https://coveralls.io/repos/github/{gh-name}/badge.svg?branch={gh-branch}[Coverage Status, link="https://coveralls.io/github/{gh-name}"] image:https://api.codacy.com/project/badge/grade/{codacy-id}["Codacy code quality", link="https://www.codacy.com/app/{gh-name}"] image:https://maven-badges.herokuapp.com/maven-central/{group-id}/{artifact-id}/badge.svg[Maven Central, link="https://maven-badges.herokuapp.com/maven-central/{group-id}/{artifact-id}"] endif::env-github[]

The aim of this project is to provide a convenient exception handler (resolver) for RESTful APIs that meets a best-practices for error responses without repeating yourself. It’s very easy to handle custom exceptions, customize error responses and even localize them. Also solves some pitfalls footnote:[Nothing terrible, Spring MVC is still a far better then JAX-RS for RESTful APIs! ;)] in Spring MVC with a content negotiation when producing error responses.

== Error message

Error messages generated by ErrorMessageRestExceptionHandler follows the http://tools.ietf.org/html/draft-nottingham-http-problem-06[Problem Details for HTTP APIs] specification.

For example, the following error message describes a validation exception.

In JSON format:

[source, json]

{
"type": "http://example.org/errors/validation-failed",
"title": "Validation Failed",
"status": 422,
"detail": "The content you've send contains 2 validation errors.",
"errors": [{
"field": "title",
"message": "must not be empty"
}, {
"field": "quantity",
"rejected": -5,
"message": "must be greater than zero"
}]
}

… or in XML:

[source,xml]

== How does it work?

=== RestHandlerExceptionResolver

The core class of this library that resolves all exceptions is {src-base}/RestHandlerExceptionResolver.java[RestHandlerExceptionResolver]. It holds a registry of RestExceptionHandlers.

When your controller throws an exception, the RestHandlerExceptionResolver will:

. Find an exception handler by the thrown exception type (or its supertype, supertype of the supertype… up to the Exception class if no more specific handler is found) and invoke it. . Find the best matching media type to produce (using {spring-jdoc-uri}/web/accept/ContentNegotiationManager.html[ContentNegotiationManager], utilises Accept header by default). When the requested media type is not supported, then fallback to the configured default media type. . Write the response.

=== RestExceptionHandler

Implementations of the {src-base}/handlers/RestExceptionHandler.java[RestExceptionHandler] interface are responsible for converting the exception into Spring’s {spring-jdoc-uri}/http/ResponseEntity.html[ResponseEntity] instance that contains a body, headers and a HTTP status code.

The main implementation is {src-base}/handlers/ErrorMessageRestExceptionHandler.java[ErrorMessageRestExceptionHandler] that produces the ErrorMessage body (see above for example). All the attributes (besides status) are loaded from a properties file (see the section <>). This class also logs the exception (see the <> section).

== Configuration

=== Java-based configuration

[source]

@EnableWebMvc @Configuration public class RestContextConfig extends WebMvcConfigurerAdapter {

@Override
public void configureHandlerExceptionResolvers(List<HandlerExceptionResolver> resolvers) {
    resolvers.add( exceptionHandlerExceptionResolver() ); // resolves @ExceptionHandler
    resolvers.add( restExceptionResolver() );
}

@Bean
public RestHandlerExceptionResolver restExceptionResolver() {
    return RestHandlerExceptionResolver.builder()
            .messageSource( httpErrorMessageSource() )
            .defaultContentType(MediaType.APPLICATION_JSON)
            .addErrorMessageHandler(EmptyResultDataAccessException.class, HttpStatus.NOT_FOUND)
            .addHandler(MyException.class, new MyExceptionHandler())
            .build();
}

@Bean
public MessageSource httpErrorMessageSource() {
    ReloadableResourceBundleMessageSource m = new ReloadableResourceBundleMessageSource();
    m.setBasename("classpath:/org/example/messages");
    m.setDefaultEncoding("UTF-8");
    return m;
}

@Bean
public ExceptionHandlerExceptionResolver exceptionHandlerExceptionResolver() {
    ExceptionHandlerExceptionResolver resolver = new ExceptionHandlerExceptionResolver();
    resolver.setMessageConverters(HttpMessageConverterUtils.getDefaultHttpMessageConverters());
    return resolver;
}

}

=== XML-based configuration

[source, xml]

=== Another resolvers

The {spring-jdoc-uri}/web/servlet/mvc/method/annotation/ExceptionHandlerExceptionResolver.html[ExceptionHandlerExceptionResolver] is used to resolve exceptions through {spring-jdoc-uri}/web/bind/annotation/ExceptionHandler.html[@ExceptionHandler] methods. It must be registered before the RestHandlerExceptionResolver. If you don’t have any @ExceptionHandler methods, then you can omit the exceptionHandlerExceptionResolver bean declaration.

=== Default handlers

Builder and FactoryBean registers a set of the default handlers by default. This can be disabled by setting withDefaultHandlers to false.

=== Localizable error messages

Message values are read from a properties file through the provided {spring-jdoc-uri}/context/MessageSource.html[MessageSource], so it can be simply customized and localized. Library contains a default link:src/main/resources/cz/jirutka/spring/exhandler/messages.properties[messages.properties] file that is implicitly set as a parent (i.e. fallback) of the provided message source. This can be disabled by setting withDefaultMessageSource to false (on a builder or factory bean).

The key name is prefixed with a fully qualified class name of the Java exception, or default for the default value; this is used when no value for a particular exception class exists (even in the parent message source).

Value is a message template that may contain https://docs.spring.io/spring/docs/current/spring-framework-reference/html/expressions.html[SpEL] expressions delimited by #{ and }. Inside an expression, you can access the exception being handled and the current request (instance of http://docs.oracle.com/javaee/7/api/javax/servlet/http/HttpServletRequest.html[HttpServletRequest]) under the ex, resp. req variables.

For example:

[source, properties]

org.springframework.web.HttpMediaTypeNotAcceptableException.type=http://httpstatus.es/406
org.springframework.web.HttpMediaTypeNotAcceptableException.title=Not Acceptable
org.springframework.web.HttpMediaTypeNotAcceptableException.detail=
This resource provides #{ex.supportedMediaTypes}, but you've requested #{req.getHeader('Accept')}.

=== Exception logging

Exceptions handled with status code 5×× are logged on ERROR level (incl. stack trace), other exceptions are logged on INFO level without a stack trace, or on DEBUG level with a stack trace if enabled. The logger name is cz.jirutka.spring.exhandler.handlers.RestExceptionHandler and a Marker is set to the exception’s full qualified name.

=== Why is 404 bypassing exception handler?

When the {spring-jdoc-uri}/web/servlet/DispatcherServlet.html[DispatcherServlet] is unable to determine a corresponding handler for an incoming HTTP request, it sends 404 directly without bothering to call an exception handler (see http://stackoverflow.com/a/22751886/2217862[on StackOverflow]). This behaviour can be changed, since Spring 4.0.0, using throwExceptionIfNoHandlerFound init parameter. You should set this to true for a consistent error responses.

When using WebApplicationInitializer:

[source]

public class AppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {

protected void customizeRegistration(ServletRegistration.Dynamic reg) {
    reg.setInitParameter("throwExceptionIfNoHandlerFound", "true");
}
...

}

…or classic web.xml:

[source, xml]

== How to get it?

Released versions are available in jCenter and the Central Repository. Just add this artifact to your project:

.Maven
[source, xml, subs="verbatim, attributes"]

.Gradle [source, groovy, subs="verbatim, attributes"] compile '{group-id}:{artifact-id}:{version}'

However if you want to use the last snapshot version, you have to add the JFrog OSS repository:

.Maven
[source, xml]

.Gradle
[source, groovy]

repositories {
maven {
url 'https://oss.jfrog.org/oss-snapshot-local'
}
}

== Requirements

  • Spring 3.2.0.RELEASE and newer is supported, but 4.× is highly recommended.
  • Jackson 1.× and 2.× are both supported and optional.

== References

== License

This project is licensed under http://www.apache.org/licenses/LICENSE-2.0.html[Apache License 2.0].