Error handling – for me this phrase associates with angry frontend developers accusing backend team of throwing exceptions without any unified response format into ether.
Table of contents:
- Custom error responses
- Preparation
- Response body
- Common response
- Exception handler
- Verification
- Overriding default Spring error responses
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.