Microservices - Exception Handling
- Chinthaka Dinadasa
- 18 Nov, 2021
Exception handling in microservices is a challenging concept while using a microservices architecture since by design microservices are well-distributed ecosystem. So if there is a failure inside the ecosystem we should handle those and return a proper result to the end user.
Here in this article, I’ll explain how we can configure exception handling into a spring boot microservices application using @ControllerAdvice and feign error decoder to bring any error inside the system to the end-user.
Brief Solution
As I discussed earlier, We are using Spring Cloud Openfeign for internal microservices communication. Hence with this setup, there are 2 main components that act behind the scene. Which are,
- Global exception handler
- Feign Error Decoder
Global exception handler will capture any error or exception inside a given microservice and throws it. Feign error decoder will capture any incoming exception and decode it to a common pattern.
Common Exception Pattern
In this setup, we are going to set up a common exception pattern, which will have an exception code (Eg:- BANKING-CORE-SERVICE-1000) and an exception message. The reason behind using an error code is that we need to have a way of identifying where exactly the given issue is happening, basically the service name. In addition to that this will return a proper error message output as well.
{
"code": "Requested entity not present in the DB.",
"message": "BANKING-CORE-SERVICE-1000"
}
Global Exception Handling In Spring Boot
First, we need to set up global exception handling inside every microservice. Todo that, we can use @ControllerAdvice based global exception handler.
I’ve discussed the same topic in depth in my other article on Exception Handling Spring Boot REST API.
Here In this tutorial, I’ll demonstrate the basics with user registration API. So if any user needs to register with internet banking, They should be present on the core banking system under that given Identification. So we can check the given ID and throw a different error from core banking service to user service.
You can get the source code for this tutorial from our GitHub repository, Please checkout to feature/microservices-exception-handling in order to go forward with the steps below.
Core Banking Service
First, we need to set up the capability of throwing exceptions on core banking service errors. Open core banking service and follow the steps.
Create a common exception class were we going to extend RuntimeException.
package com.javatodev.finance.exception;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public class SimpleBankingGlobalException extends RuntimeException {
private String code;
private String message;
public SimpleBankingGlobalException(String message) {
super(message);
}
}
GlobalErrorCode class to manage exception codes,
package com.javatodev.finance.exception;
public class GlobalErrorCode {
public static final String ERROR_ENTITY_NOT_FOUND = "BANKING-CORE-SERVICE-1000";
public static final String INSUFFICIENT_FUNDS = "BANKING-CORE-SERVICE-1001";
}
After that, we can create custom runtime exceptions to use with this API. Here I’m creating EntityNotFoundException which we could use on an entity not present on querying the DB.
package com.javatodev.finance.exception;
public class EntityNotFoundException extends SimpleBankingGlobalException {
public EntityNotFoundException() {
super("Requested entity not present in the DB.", GlobalErrorCode.ERROR_ENTITY_NOT_FOUND);
}
public EntityNotFoundException (String message) {
super(message, GlobalErrorCode.ERROR_ENTITY_NOT_FOUND);
}
}
Now create the global exception handler to capture any exception including handled exceptions and other exceptions. Here we need to have a supporting class such as ErrorResponse.java which brings only the error message and error code in response to API failure. I will use that class instead of SimpleBankingGlobalException since it has more details inheriting from RuntimeException which is unwanted to show to the end-user.
package com.javatodev.finance.exception;
import lombok.*;
@Getter
@Setter
@Builder
public class ErrorResponse {
private String code;
private String message;
}
package com.javatodev.finance.exception;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
import java.util.Locale;
@ControllerAdvice
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {
@ExceptionHandler(SimpleBankingGlobalException.class)
protected ResponseEntity handleGlobalException(SimpleBankingGlobalException simpleBankingGlobalException, Locale locale) {
return ResponseEntity
.badRequest()
.body(ErrorResponse.builder()
.code(simpleBankingGlobalException.getCode())
.message(simpleBankingGlobalException.getMessage())
.build());
}
@ExceptionHandler({Exception.class})
protected ResponseEntity handleException(Exception e, Locale locale) {
return ResponseEntity
.badRequest()
.body("Exception occur inside API " + e);
}
}
Finally, let’s throw the correct exception where we need,
public User readUser(String identification) {
UserEntity userEntity = userRepository.findByIdentificationNumber(identification).orElseThrow(EntityNotFoundException::new);
return userMapper.convertToDto(userEntity);
}
All done with core banking service, and now it has the capability to capture any exception inside the application and throw it. Let’s focus on places where we call this core banking service and handle these errors.
User Service
Now since the banking core service throws errors, we need to handle those in other services where we directly call on application requests. Eg:- User service on user registrations we call banking core and check given ID is available for registrations.
First, we need to create the same global error handling mechanism inside the user service as well. Just create the necessary classes including Custom Exceptions and global exception handler as we did in banking core service.
In this scenario, I will create 2 different exceptions to handle the validity of given user identification and email.
package com.javatodev.finance.exception;
public class InvalidBankingUserException extends SimpleBankingGlobalException {
public InvalidBankingUserException(String message, String code) {
super(message, code);
}
}
package com.javatodev.finance.exception;
public class InvalidEmailException extends SimpleBankingGlobalException {
public InvalidEmailException(String message, String code) {
super(message, code);
}
}
And do the implementations as well to throw correct exceptions in business logic,
public User createUser(User user) {
List<UserRepresentation> userRepresentations = keycloakUserService.readUserByEmail(user.getEmail());
if (userRepresentations.size() > 0) {
throw new UserAlreadyRegisteredException("This email already registered as a user. Please check and retry.", GlobalErrorCode.ERROR_EMAIL_REGISTERED);
}
UserResponse userResponse = bankingCoreRestClient.readUser(user.getIdentification());
if (userResponse.getId() != null) {
if (!userResponse.getEmail().equals(user.getEmail())) {
throw new InvalidEmailException("Incorrect email. Please check and retry.", GlobalErrorCode.ERROR_INVALID_EMAIL);
}
UserRepresentation userRepresentation = new UserRepresentation();
userRepresentation.setEmail(userResponse.getEmail());
userRepresentation.setEmailVerified(false);
userRepresentation.setEnabled(false);
userRepresentation.setUsername(userResponse.getEmail());
CredentialRepresentation credentialRepresentation = new CredentialRepresentation();
credentialRepresentation.setValue(user.getPassword());
credentialRepresentation.setTemporary(false);
userRepresentation.setCredentials(Collections.singletonList(credentialRepresentation));
Integer userCreationResponse = keycloakUserService.createUser(userRepresentation);
if (userCreationResponse == 201) {
log.info("User created under given username {}", user.getEmail());
List<UserRepresentation> userRepresentations1 = keycloakUserService.readUserByEmail(user.getEmail());
user.setAuthId(userRepresentations1.get(0).getId());
user.setStatus(Status.PENDING);
user.setIdentification(userResponse.getIdentificationNumber());
UserEntity save = userRepository.save(userMapper.convertToEntity(user));
return userMapper.convertToDto(save);
}
}
throw new InvalidBankingUserException("We couldn't find user under given identification. Please check and retry", GlobalErrorCode.ERROR_USER_NOT_FOUND_UNDER_NIC);
}
Now we can focus on configuring OpenFeign to handle microservices exceptions.
As of now, the communication layer has been developed using spring cloud OpenFeign and it comes with a handy way of handling API client exceptions name ErrorDecoder. Let’s configure that with the OpenFeign client.
Create the following custom error decoder in order to capture incoming error responses from other API on HTTP requests, Here all the Bad Request 400 responses are captured with this decoder and throw in a uniform exception pattern (BankingCoreGlobalException), Additionally, other exceptions like 401 (Unauthorized), 404 (Not found) also getting handled from here.
package com.javatodev.finance.configuration;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.javatodev.finance.exception.SimpleBankingGlobalException;
import feign.Response;
import feign.codec.ErrorDecoder;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.IOUtils;
import java.io.IOException;
import java.io.Reader;
import java.nio.charset.StandardCharsets;
@Slf4j
public class CustomFeignErrorDecoder implements ErrorDecoder {
@Override
public Exception decode(String methodKey, Response response) {
SimpleBankingGlobalException simpleBankingGlobalException = extractBankingCoreGlobalException(response);
switch (response.status()) {
case 400:
log.error("Error in request went through feign client {} ", simpleBankingGlobalException.getMessage() + " - " + simpleBankingGlobalException.getCode());
return simpleBankingGlobalException;
case 401:
log.error("Unauthorized Request Through Feign");
return new Exception("Unauthorized Request Through Feign");
case 404:
log.error("Unidentified Request Through Feign ");
return new Exception("Unidentified Request Through Feign");
default:
log.error("Error in request went through feign client");
return new Exception("Common Feign Exception");
}
}
private SimpleBankingGlobalException extractBankingCoreGlobalException(Response response) {
SimpleBankingGlobalException exceptionMessage = null;
Reader reader = null;
//capturing error message from response body.
try {
reader = response.body().asReader(StandardCharsets.UTF_8);
String result = IOUtils.toString(reader);
ObjectMapper mapper = new ObjectMapper();
mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
exceptionMessage = mapper.readValue(result,
SimpleBankingGlobalException.class);
} catch (IOException e) {
log.error("IO Exception on reading exception message feign client" + e);
} finally {
try {
if (reader != null) {
reader.close();
}
} catch (IOException e) {
log.error("IO Exception on reading exception message feign client" + e);
}
}
return exceptionMessage;
}
}
Finally, introduce this custom error decoder using feign client configurations as below,
package com.javatodev.finance.configuration;
import feign.codec.ErrorDecoder;
import org.springframework.cloud.openfeign.FeignClientProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class CustomFeignClientConfiguration extends FeignClientProperties.FeignClientConfiguration {
@Bean
public ErrorDecoder errorDecoder() {
return new CustomFeignErrorDecoder();
}
}
And finally, don’t forget to set this custom configuration into the feign clients which communicate with other APIs.
@FeignClient(name = "core-banking-service", configuration = CustomFeignClientConfiguration.class)
public interface BankingCoreRestClient
All done, Let’s create a few users and check the API setup.
API Testing Using Postman
Here is the response for invalid user identification which will throw from the banking core service.
After that let’s try with correct identification and incorrect email. This should be validated and thrown an error from the user-service saying the email is invalid.
Finally successful user registration on a correct data request.
Conclusion
Thanks for reading our latest article on Microservices – Exception Handling with practical usage.
If you are looking for spring boot practical application development tutorials, just check our article series.
You can get the source code for this tutorial from our GitHub repository.