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:
- Response body class – used in each error response.
- Common method to return the same format for each handled exception.
- Exception handler classes to define how to 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:
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 a method that will define a 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:
Now check the response:
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"
}
}
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:
Let’s check the request now:
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.
Would you like to talk with our experts about custom software development?
Q: What is error handling in Spring Boot?
Error handling in Spring Boot refers to the process of defining how to handle exceptions that may occur in a Spring Boot application. This can be done using the @ControllerAdvice annotation to define a class that contains @ExceptionHandler annotated methods, which determine how to handle specific exceptions.
Q: What is the ErrorResponse class in Spring Boot error handling?
The ErrorResponse class is a class used to define the body of an exceptional response in Spring Boot error handling. It typically includes properties such as a message, code, and details about the error.
Q: What is the HttpResponseExceptionHandler class in Spring Boot error handling?
The HttpResponseExceptionHandler class is an abstract class that defines a method for creating a standardized response for exceptions in Spring Boot. This class can be extended by other exception handler classes to create a consistent format for error responses.