Spring Boot Starter : Chapter 2, error handling

TSBS Project repository and archives

As a shortcut, TSBS stands for Tooling Spring Boot Starter until I find a better name.

You can find TSBS sources at GitHub project page and this chapter archive under its own tag Chapter 2 archive

See Chapter 1 introduction to read more about this project.

Objectives

In this chapter, we will try to intercept Spring errors and collect all application exceptions. Once catched, we wrap errors and exceptions into standardized JSON response and produce logs.

Those features will be implemented in tooling-spring-boot-starter, and we will notice that tooling-app integrates them automatically.

Check previous chapter if you missed it !

Error and Exception handling

We will create a couple of new classes :

  • ServiceException and HttpServiceException, as customized exception

  • ControllerCustomError to replace default White-Label errors

  • RestExceptionHandler to intercept all Java exceptions

  • Msg is an helper to create log alike messages with a pattern template

Exception classes

We’ll start by writing a set of custom Exceptions as part of tooling-commons module.

Next chapters will cover more about this exception management strategy.

ServiceException

This exception might be used in all @Service Spring context. Any service method throws ServiceException

(And all @Controller throw Exception)

ServiceException
package net.kprod.tooling.spring.commons.exception;

public class ServiceException extends Exception {
        public ServiceException() {
                super();
        }

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

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

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

HttpServiceException

A specialized variant of ServiceException. It adds HttpStatus over it, in order to return a consistent response to the client in web or rest context embedded in a JSON object.

HttpServiceException
package net.kprod.tooling.spring.commons.exception;

import org.springframework.http.HttpStatus;

public class HttpServiceException extends ServiceException {
    private HttpStatus status;

        public HttpServiceException(HttpStatus status) {
                super();
        }

        public HttpServiceException(HttpStatus status, String message, Throwable cause) {
                super(message, cause);
                this.status = status;
        }

        public HttpServiceException(HttpStatus status, String message) {
                super(message);
                this.status = status;
        }

        public HttpServiceException(HttpStatus status, Throwable cause) {
                super(cause);
                this.status = status;
        }

        /**
         * Get HttpStatus object
         * @return httpstatus
         */
        public HttpStatus getStatus() {
                return status;
        }

        /**
         * Get http status reason
         * @return reason text
         */
        public String getReason() {
                return status.getReasonPhrase();
        }

        /**
         * Get http status code
         * @return code
         */
        public int getCode() {
                return status.value();
        }
}

Msg utility class

Msg class is a log-like message formatter. Easier to use than a StringBuilder.

Msg class
package net.kprod.tooling.spring.commons.log;

import org.slf4j.helpers.MessageFormatter;

public class Msg {
    private Msg() {
    }

  //return a pattern formatted message with varargs
  public static String format(String pattern, Object... objects) {
    return MessageFormatter.arrayFormat(pattern, objects).getMessage();
  }
}

Spring White Label errors

White-Label Spring errors is a default Spring behavior. Instead of displaying the default error page, shown under /error path, we customize it and throw a custom HttpServiceException each time. This would be intercepted later by our exception handler.

ControllerCustomError class intercepts all white label errors and rethrow them as a HttpServiceException categorized as an http 500 error.

ControllerCustomError class
package net.kprod.tooling.spring.starter.controller;

//imports

@RestController("ErrorController"
public class ControllerCustomError implements ErrorController {
        private static final String PATH = "/error";
        public static final String SPRING_ERROR_MAP_ERROR = "error";
        public static final String SPRING_ERROR_MAP_PATH = "path";
        public static final String SPRING_ERROR_MAP_MESSAGE = "message";
        public static final String SPRING_ERROR_MAP_TRACE = "trace";

        @Value("${tooling.error.stacktrace.include:false}")
        private boolean includeStackTrace;

        @Autowired
        private ErrorAttributes errorAttributes;

        @RequestMapping(value = PATH)
        void error(WebRequest webRequest, HttpServletResponse response) throws Exception {
                //Get Spring http status
                HttpStatus status = HttpStatus.valueOf(response.getStatus());

                //Get Spring error attributes
                Optional<Map<String, Object>> mapErrorAttribute = this.getErrorAttributes(webRequest);

                //Default message
                String message = Msg.format("Spring error (no details) status [{}]", status.toString());

                //Message with attributes if present in map
                if(mapErrorAttribute.isPresent()) {
                        message = Msg.format("Spring error [{}] status [{}] path [{}] message [{}] trace [{}]",
                                mapErrorAttribute.get().get(SPRING_ERROR_MAP_ERROR),
                                status.toString(),
                                mapErrorAttribute.get().get(SPRING_ERROR_MAP_PATH),
                                mapErrorAttribute.get().get(SPRING_ERROR_MAP_MESSAGE),
                                mapErrorAttribute.get().get(SPRING_ERROR_MAP_TRACE));
                }
                //Throw custom exception
                throw new HttpServiceException(status, message);
        }

        @Override
        public String getErrorPath() {
                return PATH;
        }

        //get a map of error attributes from WebRequest
        private Optional<Map<String, Object>> getErrorAttributes(WebRequest webReques) {
                if(errorAttributes == null) {
                        return Optional.empty();
                }
                return Optional.of(errorAttributes.getErrorAttributes(webRequest, includeStackTrace));
        }
}

Exception handler

RestExceptionHandler class catch all Spring application exception and process them.

This class will grow, as it’s currently only catch generic Exception. Any catched exception will send a standard JSON error response to the client.

RestExceptionHandler class
package net.kprod.tooling.spring.starter.controller;

//imports

//this annotation allows exception handling
@ControllerAdvice
public class RestExceptionHandler {
        @Value("${tooling.error.stacktrace.include:false}")
        private boolean includeStackTrace;

  //Handle unexpected exceptions
  @ExceptionHandler(value = Exception.class)
  public ResponseEntity<ResponseException> handleUnexpectedException(Exception exception) {
      //Translate exception into a HttpServiceException
      //This includes Http status 500 as this exception might be unexcepted
      HttpServiceException translatedException = new HttpServiceException(
        HttpStatus.INTERNAL_SERVER_ERROR,
        Msg.format("Unexpected exception [{}:{}]",
                exception.getClass().getSimpleName(),
                exception.getMessage()),
        exception);

      return createResponse(translatedException, HttpStatus.INTERNAL_SERVER_ERROR, exception);
    }

    //Create error response
    private ResponseEntity<ResponseException> createResponse(HttpServiceException e, HttpStatus httpStatus, Exception parentException) {
        //Create response
        ResponseException responseException = this.processException(e, parentException);
        //Return response entity with proper status
        return ResponseEntity.status(httpStatus).body(responseException);
    }

    //Transform exception to a ResponseException
    private ResponseException processException(HttpServiceException translatedException, Exception parentException) {
        ResponseException responseException = new ResponseException();
        responseException.setMessage(Msg.format("Exception message [{}] translated to [{}] status [{}]",
          parentException.getMessage(),
          translatedException.getMessage(),
          translatedException.getReason()));
                                if(includeStackTrace) {
                                        responseException.setStacktrace(getStacktraceAsString(parentException));
                                } else {
                                        responseException.setStacktrace(STACKTRACE_UNAVAILABLE_MESSAGE);
                                }
                                LOG.error(responseException.getMessage());
        return responseException;
    }

    //Transform exception stacktrace to a string
    private String getStacktraceAsString(Exception e) {
        StringWriter sw = new StringWriter();
        PrintWriter pw = new PrintWriter(sw);
        e.printStackTrace(pw);
        return sw.toString();
    }
}

Test Application

DummyService

DummyService class only got one method foo, to return bar as a String.

DummyService class
@Service
public class DummyServiceImpl implements DummyService {
        @Override
        public String foo(String name) {
                return "foo " + name;
        }
}

A test Controller

ControllerRoot class purpose is to provide a few test paths :

  • /foo : returns DummyService#foo result

  • /exception : force a NullPointerException to be thrown

ControllerRoot class
@RestController
@RequestMapping("/")
public class ControllerRoot {

        //Autowiring Service from Tooling Starter service
        @Autowired
        private DummyService dummyService;

        // /foo will return a json response embed to ResponseEntity as a 200 response
        @RequestMapping(value = "/foo", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
        public ResponseEntity<Response> foo() throws Exception {
                return ResponseEntity.ok(new Response(
                        dummyService.foo("bar")));
        }

        // /exception will throw a NPE
        @RequestMapping(value = "/exception", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
        public ResponseEntity<Response> exception() throws Exception {
                throw new NullPointerException("bang");
        }
}

What can we do now with that ?

Spoiler : Nothing impressive so far.

First, we’ll start our server

cd tooling-app
mvn spring-boot:run

Nominal case

Using our browser (or maybe postman), we will test localhost:8080/foo and get a foo bar response as JSON. Great.

Exception catching

Let’s now request localhost:8080/exception.

As response, we got our standard error response.

Thrown NullPointerException from ControllerRoot is effectively catched by RestExceptionHandler at top-level, without any intermediary exception management processing inside tooling-app.

Exception JSON response
{
  "message": "Exception message [bang] translated to [Unexpected exception [NullPointerException:bang]] status [Internal Server Error]",
  "stacktrace": "java.lang.NullPointerException: bang\n\tat net.kprod.tooling.spring.app.controller.ControllerRoot.exception(ControllerRoot.java:41)[...]"
}

Have a look to Spring logs to view our ERROR log line.

Exception log
2019-04-24 21:31:27.276 ERROR 11954 --- [nio-8080-exec-1] n.k.t.s.s.c.RestExceptionHandler         : Exception message [bang] translated to [Unexpected exception [NullPointerException:bang]] status [Internal Server Error]

404 Not Found (Custom White Label)

Let’s try with localhost:8080/nothing. This mapping is not set, so Spring would catch this request.

As previously, a standard response embed as JSON. The standard White label page is replaced with a JSON response.

404 JSON response
{
  "message": "Exception message [Spring error [Not Found] status [404 NOT_FOUND] path [/nothing] message [No message available] trace [null]] translated to [Spring error [Not Found] status [404 NOT_FOUND] path [/nothing] message [No message available] trace [null]] status [Not Found]",
  "stacktrace": "net.kprod.tooling.spring.commons.exception.HttpServiceException: Spring error [Not Found] status [404 NOT_FOUND] [...]"
}
404 log
2019-04-24 21:32:38.889 ERROR 11954 --- [nio-8080-exec-2] n.k.t.s.s.c.RestExceptionHandler         : Exception message [Spring error [Not Found] status [404 NOT_FOUND] path [/nothing] message [No message available] trace [null]] translated to [Spring error [Not Found] status [404 NOT_FOUND] path [/nothing] message [No message available] trace [null]] status [Not Found]

Summary of this chapter

We’ve created centralized exception and error management features available to any Spring Application which specify tooling-spring-boot-starter as a dependency.

We’ve tested this features availability from our tooling-app without further configuration, through a test Controller.

To be continued…​

In chapter 3 we will explore MDC features and a unique process id system.

All available chapters

  • chapter 1 : Custom Spring Boot 2 Starter with Spring 5, chapter 1

  • chapter 2 : Spring Boot Starter : Chapter 2, error handling

  • chapter 3 : Spring Boot Starter : Chapter 3, logger and process unique id

  • chapter 4 : Spring Boot Starter : Chapter 4, aspects and async processing

comments powered by Disqus