Error handling (Spring Boot < 3)
Introduction
Let's see a proposal of a common centralized way of handling REST operations exceptions for returning a pre-defined JSON document for them.
Note: For Spring Boot >= 3 use ProblemDetail instead.
Reference
Error Handling for REST with Spring (Baeldung)
https://www.baeldung.com/exception-handling-for-rest-with-spring
Spring – Log Incoming Requests
https://www.baeldung.com/spring-http-logging
Log Incoming Requests
Intercept and log requests.
Sample, using Spring Boot 3.0:
package edu.uoc.gpradoc;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.filter.CommonsRequestLoggingFilter;
/**
* Request logger interceptor.
*
* Sample log entry:<br>
* 2022-12-13T16:22:14.092Z DEBUG 1 --- [nio-8080-exec-9] o.s.w.f.CommonsRequestLoggingFilter :
* Before request [DELETE /ui/learning-object-deliverys/22, headers=[x-forwarded-proto:"http",
* x-forwarded-port:"80", host:"app-myapp-docback-test.cloudapps.cou.edu",
* x-amzn-trace-id:"Self=1-6398a6b6-0979fa5f04b9976e09701b0b;Root=1-6398a6b5-442396126388050f79d9f3cd",
* accept:"application/json", authorization:"Bearer
* KF9da7Irp73D1q8OzrmXndjxC4yAI4Eoku7VQeSSDy0.9wF-JXxEoehyTLJFd_KqirgV85FUFiEc-4OF4CKD038",
* user-agent:"curl/7.81.0",
* x-api-key:"431497da60291770bece04a88d833ab15e6e83c604ab86d06b2b26050ca775812ca0a329de4d97392aed848ff1d0a9a079ee2672daef2d61c1f6ac515ae48ceb",
* x-cou-scope:"gpra_doc.rw"]]
*
* <hr>
* https://www.baeldung.com/spring-http-logging
*
* Also requires us to set the log level to DEBUG. We can enable the DEBUG mode by adding the
* following in application.properties:
* logging.level.org.springframework.web.filter.CommonsRequestLoggingFilter= DEBUG
*
* Another way of enabling the DEBUG level log is by adding the below element in logback.xml:
* <logger name="org.springframework.web.filter.CommonsRequestLoggingFilter">
* <level value="DEBUG" /> </logger>
*/
@Configuration
public class RequestLoggingFilterConfig {
@Bean
public CommonsRequestLoggingFilter logFilter() {
CommonsRequestLoggingFilter filter = new CommonsRequestLoggingFilter();
filter.setIncludeQueryString(true);
filter.setIncludePayload(true);
filter.setMaxPayloadLength(10000);
filter.setIncludeHeaders(true);
filter.setAfterMessagePrefix("REQUEST APP DATA : ");
return filter;
}
}
Error DTO for the JSON response
Spring Boot defines standard error attributes at the class DefaultErrorAttributes.
The following class uses a few of them and adds an additional one, systemCode, for informing the application that has generated the error.
package edu.cou.myapp.dto;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.Date;
import java.util.List;
import org.springframework.validation.BindingResult;
import org.springframework.validation.ObjectError;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.Data;
/**
* Error DTO returned by REST operations.<br>
*
* @See CustomErrorAttributes
*/
@JsonIgnoreProperties({ "status", "error", "exception", "path", "requestId", "errors" })
@Data
public class ErrorDto {
/** Code of this system. */
private static final String SYSTEM_CODE = "MYAPP";
/**
* Custom. The system that generated the error (this system application)
*/
private String systemCode = SYSTEM_CODE;
/**
* The time that the errors were extracted
*/
private final Date timestamp = new Date();
/** Unused - The status code */
@JsonIgnore
private int status;
/** Unused - The error reason */
@JsonIgnore
private String error;
/** Unused - The class name of the root exception (if configured) */
@JsonIgnore
private String exception;
/**
* The exception message (can be shown to the final user)
*/
private String message;
/** Unused - Any {@link ObjectError}s from a {@link BindingResult} exception */
@JsonIgnore
private List<org.springframework.validation.ObjectError> errors;
/**
* The exception stack trace (technical info)
*/
private String trace;
/** Unused - The URL path when the exception was raised */
@JsonIgnore
private String path;
/** Unused - Unique ID associated with the current request */
@JsonIgnore
private String requestId;
/**
* Constructor
*
* @param message
*
* @param cause
*
*/
public ErrorDto(final String message, final Throwable cause) {
super();
this.message = message;
this.trace = getStackTrace(cause);
}
/**
* Constructor
*
* @param cause -
*/
public ErrorDto(final Throwable cause) {
this(cause.getLocalizedMessage(), cause);
}
/**
* Message and stack trace as String
*
* @param th
* @return -
*/
private String getStackTrace(final Throwable th) {
StringWriter stackTrace = new StringWriter();
th.printStackTrace(new PrintWriter(stackTrace));
stackTrace.flush();
return stackTrace.toString();
}
Custom error handler
This handler will intercept exceptions thrown by REST operations and it will return a JSON document based on the previous Error DTO.
package edu.cou.myapp;
import java.util.List;
import java.util.stream.Collectors;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
import edu.cou.myapp.dto.ErrorDto;
import edu.cou.myapp.exception.AppException;
import edu.cou.myapp.exception.ValAppException;
import lombok.val;
import lombok.extern.slf4j.Slf4j;
/**
* Global REST exception handling mechanism (custom error handler).
*/
@RestControllerAdvice
@Slf4j
public class ErrorHandler extends ResponseEntityExceptionHandler {
/**
* handle AppException
*
* @param ex
* @param request
* @return -
*/
@ExceptionHandler(value = { AppException.class })
public ResponseEntity<Object> handleAppException(AppException ex, WebRequest request) {
ErrorDto body = new ErrorDto(ex);
log.info(body.toString());
return handleExceptionInternal(ex, body, new HttpHeaders(), ex.getHttpStatus(), request);
}
/**
* handle method level security @PreAuthorize, @PostAuthorize, and @Secure Access Denied
*
* @param ex
* @param request
* @return -
*/
@ExceptionHandler({ AccessDeniedException.class })
public ResponseEntity<Object> handleAccessDeniedException(Exception ex, WebRequest request) {
ErrorDto body = new ErrorDto("No teniu permissos suficients per a executar aquesta operació",
ex);
log.info(body.toString());
return handleExceptionInternal(ex, body, new HttpHeaders(), HttpStatus.FORBIDDEN, request);
}
/**
* handle default exception
*
* @param ex
* @param request
* @return -
*/
@ExceptionHandler({ Exception.class })
public ResponseEntity<Object> handleDefaultException(Exception ex, WebRequest request) {
ErrorDto body = new ErrorDto(ex);
log.info(body.toString());
return handleExceptionInternal(ex, body, new HttpHeaders(), HttpStatus.INTERNAL_SERVER_ERROR,
request);
}
/**
* handle MethodArgumentNotValidException<br>
*
* <pre>
* for @Valid
* </pre>
*
* Note: Spring Boot < 3.0.0 uses HttpStatus instead of HttpStatusCode
*/
@Override
public ResponseEntity<Object> handleMethodArgumentNotValid(MethodArgumentNotValidException ex,
HttpHeaders headers, HttpStatusCode status, WebRequest request) {
// Get all errors
List<String> errors = ex.getBindingResult().getFieldErrors().stream().map(x -> x
.getDefaultMessage()).toList(); /*[<jdk17] .collect(Collectors.toList())*/
// Format message the same way ValAppException does
val ve = new ValAppException(errors);
//
ErrorDto body = new ErrorDto(ve.getMessage(), ex);
log.warn(body.toString());
return handleExceptionInternal(ex, body, headers, status, request);
}
}
ErrorConfig: default error attributes overriding
This optional configuration file never applies when the custom error handler intercepts all the possible exceptions.
Note: It uses class ErrorAttributeOptions introduced in Spring Boot 2.3
package edu.cou.app;
import java.util.Map;
import org.springframework.boot.web.error.ErrorAttributeOptions;
import org.springframework.boot.web.servlet.error.DefaultErrorAttributes;
import org.springframework.boot.web.servlet.error.ErrorAttributes;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.WebRequest;
/**
*
* It adds the "systemCode" to the DefaultErrorAttributes.<br>
* systemCode indicates the system that generated the error (this system application).<br>
* <br>
* REMARK: Doesn't execute if a custom ErrorErrorHandler intercepts the exception (that's fine).<br>
* <br>
* It can be tested with:<br>
* curl "http://localhost:8080/r/notexist"
*/
@Component
public class ErrorConfig {
/**
* Custom bean that implements ErrorAttributes to take control of the content.<br>
* Note: Implemented subclassing DefaultErrorAttributes.
*
* @return The 'systemCode' attribute added to the default ones
*/
@Bean
public ErrorAttributes errorAttributes() {
return new DefaultErrorAttributes() {
@Override
public Map<String, Object> getErrorAttributes(WebRequest webRequest,
ErrorAttributeOptions options) {
val errorAttributes = new LinkedHashMap<String, Object>();
errorAttributes.put("systemCode", "MYAPP");
errorAttributes.putAll(super.getErrorAttributes(webRequest, options));
return errorAttributes;
}
};
}
}