Error handling - for me this phrase associates with angry frontend developers accusing backend team of throwing exceptions without any unified response format into ether.

Let’s imagine someone has failed during the development or planning process, and now we need to fix the things - how to unify the error responses with Spring Boot? My favourite is usage of @ControllerAdvice. It is annotation used to define class that contains @ExceptionHandler annotated methods which define how to handle exceptions.

Custom error responses

To unify the logic we need to prepare:

  1. Response body class - used in each error response.
  2. Common method to return the same format for each handled exception.
  3. Exception handler classes to define how handle given exceptions.

Preparation

Assuming we are working on errors for employee manager and we have employees/{id} endpoint which throws EmployeeNotFoundException when employee doesn’t exist, let’s create/add spock integration test in EmployeesControllerTest:

    def "should respond with NOT_FOUND Http status code when user with given firstName doesn't exist"() {
        when:
        def resultingStatusCode
        def resultingBody
        try {
            restTemplate.getForEntity("http://localhost:$runningServerPort/employees/test-id", EmployeesDto)
        } catch (HttpStatusCodeException e) {
            resultingStatusCode = e.statusCode
            resultingBody = new JsonSlurper().parseText(e.getResponseBodyAsString())
        }

        then:
        resultingStatusCode == HttpStatus.NOT_FOUND
        with(resultingBody) {
            message == "Employee (id = 'test-id') doesn't exist."
            code == "employee_not_found"
            details == [[employeeId: "test-id"]]
        }
    }

Of course test will fail as the response is completely different than what we expect: generated test class generated test class

Response body

First of all we need to prepare body of exceptional response. Let’s name it ErrorResponse and put in the root of the project:

class ErrorResponse {
    private final String message;
    private final String code;          // code for frontend to tell which message it needs to show for user 
    private final List<Object> details; // ie. details which field in input failed
    
    // ... constructors, builders, getters, setters, equals, hashcode, toString ... but I prefer to use Lombok instead ;)
}

Don’t you think it would be nice to define enum with error codes, just in case if there will be other developers working with that code? Let’s create it:

enum ErrorCode {
   EMPLOYEE_NOT_FOUND("employee_not_found");

   private final String code;

   ErrorCode(String code) {
       this.code = code;
   }

   public String getCode() {
       return this.code;
   }
}

Common response

Then we need to create abstract class with method which will define response. Where? It depends on the project, but probably on the project’s root as we need to access it from each feature’s package.

public abstract class HttpResponseExceptionHandler {
    protected ResponseEntity<ErrorResponse> getErrorResponseEntity(
            Exception e,
            String errorCode,
            List<Object> details,
            HttpStatus status) {
        ErrorResponse errorResponse = new ErrorResponse(e.getMessage(), errorCode, details);
        MultiValueMap<String, String> headers = new LinkedMultiValueMap<>();
        headers.set("Content-type", MediaType.APPLICATION_JSON_VALUE);
        return new ResponseEntity<>(errorResponse, headers, status);
    }
}

Exception handler

Next thing is to prepare @ControllerAdvice annotated classes for each functionality package. It is our place to define exception handlers for functionality errors:

@ControllerAdvice
class EmployeeExceptionHandler extends HttpResponseExceptionHandler {
    
    @ExceptionHandler(value = {EmployeeNotFoundException.class})
    ResponseEntity<ErrorResponse> handleCustomerAlreadyExists(EmployeeNotFoundException e) {
        Map<String, String> detailsMap = Collections.singletonMap("employeeId", e.getEmployeeId());
        return getErrorResponseEntity(
                e,
                ErrorCode.EMPLOYEE_NOT_FOUND.getCode(),
                Collections.singletonList(detailsMap),
                HttpStatus.NOT_FOUND);
    }
}

Of course in @ExceptionHandler’s value argument you can define more Exception classes if they fit the same mapping.

Verification

Run previously prepared test: generated test class

Now check the response: generated test class

It works!

Overriding default Spring error responses

You’ve probably noticed that there are some default spring exception handlers. They were created to simplify responses and catch possible exceptions like HttpRequestMethodNotSupportedException. Let’s override mentioned one.

Traditionally, create test:

    def "should override default spring response for NotSupportedMethodException"() {
        when:
        def resultingStatusCode
        def resultingBody
        try {
            restTemplate.put("http://localhost:$runningServerPort/employees/test-id", EmployeesDto)
        } catch (HttpStatusCodeException e) {
            resultingStatusCode = e.statusCode
            resultingBody = new JsonSlurper().parseText(e.getResponseBodyAsString())
        }

        then:
        resultingStatusCode == HttpStatus.METHOD_NOT_ALLOWED
        with(resultingBody) {
            message == "Not supported HTTP method. Available methods are: [GET]"
            code == "not_supported_http_method"
        }
    }

generated test class Test is failing so we need to override handleHttpRequestMethodNotSupported method from ResponseEntityExceptionHandler:

@ControllerAdvice
class SpringRESTExceptionHandler extends ResponseEntityExceptionHandler {

    @Override
    protected ResponseEntity<Object> handleHttpRequestMethodNotSupported(
            HttpRequestMethodNotSupportedException ex,
            HttpHeaders headers,
            HttpStatus status,
            WebRequest request) {

        pageNotFoundLogger.warn(ex.getMessage());

        Set<HttpMethod> supportedMethods = ex.getSupportedHttpMethods();
        if (!CollectionUtils.isEmpty(supportedMethods)) {
            headers.setAllow(supportedMethods);
        }
        ErrorResponse errorResponse = new ErrorResponse(
                String.format("Not supported HTTP method. Available methods are: %s", supportedMethods),
                "not_supported_http_method",
                null);
        return handleExceptionInternal(ex, errorResponse, headers, HttpStatus.METHOD_NOT_ALLOWED, request);
    }
}

Of course you should replace "not_supported_http_method" with some enum or constant

Run the test: generated test class

Let’s check the request now: generated test class

Conclusion

As you can see it’s really straightforward solution. You can simply replace default Spring error responses as well as your custom exception error responses. Once the response format and @ControllerAdvice classes are defined, the only thing you have to do is to prepare new handler methods.

Updated:

Leave a comment