Last Update: 15.02.2018. By Jens in API Series | APIs | Newsletter
One way of handling errors in Spring MVC is by declaring a responsible class using the @ControllerAdvice annotation.
The class itself contains a method for handling errors using the @ExceptionHandler just like you could on each individual controller. Now they are handled globally and a used for all controllers.
@Order(Ordered.HIGHEST_PRECEDENCE)
@ControllerAdvice
public class KanbanControllerAdvice {
private Logger LOGGER = LoggerFactory.getLogger(KanbanControllerAdvice.class);
@ExceptionHandler(Exception.class)
public ResponseEntity<ApiError> defaultException(Exception e) {
final InternalApiError msg = new InternalApiError();
msg.setMsgCode(MessageCode.TECHNICAL_ERROR);
LOGGER.error("Server error with uniqueErrorId={}", msg.getUniqueErrorId(), e);
return new ResponseEntity<ApiError>(msg, HttpStatus.INTERNAL_SERVER_ERROR);
}
Newer Spring Boot version will return an own JSON error on a request, so we have to use the ResponseEntity as a return value here in the exception handler. The default one includes a timestamp, the error, and the stack trace. I would not use that in a customer facing production site. It’s never a good idea to expose too many internals like stack traces…
So, I created a simple ApiError class just return an application specific message and a message code of the error. The errors are defined in a MessageCode enum like I talked about before and look like:
TECHNICAL_ERROR("001", "technical error"),
As I only got one component, I skipped that. However, in a bigger system, I’d split these up into components too.
What’s left is to override the relevant and more specific exception cases like AccessDeniedException. See the code for more.
However, when using Spring Security, we must go one step further as the @ControllerAdvice is only involved by an exception thrown by our controllers. Some workflows in Spring Security are however handled ina Filter, which runs before any controller. To catch those, we must define a AuthenticationEntryPoint for exceptionHandling.
WebSecurityConfig:
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.cors().and()
.exceptionHandling().authenticationEntryPoint(restAuthenticationEntryPoint).
Whenever authentication fails in the Security Filters, it forwards the request to the AuthenticationEntryPoint which is now responsible for the answer.
Ours looks like:
@Component
public class RestAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Autowired
private ObjectMapper objetMapper;
@Override
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException) throws IOException {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.setContentType("application/json");
response.setCharacterEncoding("UTF-8");
response.getWriter().write(objetMapper.writeValueAsString(new ApiError(MessageCode.ACCESS_DENIED)));
}
}
We just return our APIError JSON with an appropriate message.