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 &#64;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;

}

};

}


}