Skip to content

Instantly share code, notes, and snippets.

@dariahervieux
Created March 21, 2019 10:57
Show Gist options
  • Save dariahervieux/57aeb1292e1f55be87b06a845d264704 to your computer and use it in GitHub Desktop.
Save dariahervieux/57aeb1292e1f55be87b06a845d264704 to your computer and use it in GitHub Desktop.
Spring MVC: return a RFC7807 problem object for Spring controller exception
package fr.da_sha1.exceptions;
//... imports are skipped for simplicity
/**
* Model class to send error details to a client in a form of a Problem details object, described in RFC7807.
* Inspired * by <a href="https://github.com/zalando/problem">zalando/problem</a> library.
* For more information please refer to https://tools.ietf.org/html/rfc7807
* @author Daria HERVIEUX
*/
public class Problem {
private static final URI DEFAULT_URI = URI.create("about:blank");
private final URI type;
private final String title;
private final String detail;
private final HttpStatus status;
private final Map<String, Object> customAttributes;
/**
* Instantiates a problem object.
*
* @param type a unique uri referencing the type of the problem
* @param title title of the type of the problem
* @param detail details of the particular instance of the problem
* @param status Https status code associated
* @param customAttributes a map of attributes associated with the particular instance of the problem
*/
private Problem(URI type, String title, String detail, HttpStatus status,
Map<String, Object> customAttributes) {
this.type = type == null ? DEFAULT_URI : type;
this.title = title;
this.detail = detail;
this.status = status;
this.customAttributes = customAttributes;
}
/**
* An absolute URI that identifies the problem type. When dereferenced, it SHOULD provide human-readable
* documentation for the problem type (e.g., using HTML). When this member is not present, its value is assumed to
* be "about:blank".
*
* @return an absolute URI that identifies this problem's type
*/
public URI getType() {
return type;
}
/**
* A short, human-readable summary of the problem type. It SHOULD NOT change from occurrence to occurrence of the
* problem, except for purposes of localisation.
*
* @return a short, human-readable summary of this problem
*/
public String getTitle() {
return title;
}
/**
* A human readable explanation specific to this occurrence of the problem.
*
* @return A human readable explaination of this problem
*/
public String getDetail() {
return detail;
}
/**
* The HTTP status code generated by the origin server for this occurrence of the problem.
*
* @return the HTTP status code
*/
public HttpStatus getStatus() {
return status;
}
/**
* Optional, additional attributes of the problem.
*
* @return additional parameters
*/
public Map<String, Object> getCustomAttributes() {
return new LinkedHashMap<>(customAttributes);
}
public static final class ProblemBuilder {
private static final Set<String> RESERVED_PROPERTIES = new HashSet<>(Arrays.asList(
"type", "title", "status", "detail", "instance", "cause"
));
private final Map<String, Object> parameters = new LinkedHashMap<>();
private URI type;
private String title;
private HttpStatus status;
private String detail;
private ProblemBuilder() {
}
public static ProblemBuilder newBuilder() {
return new ProblemBuilder();
}
public ProblemBuilder withType(@Nullable final URI type) {
this.type = type;
return this;
}
public ProblemBuilder withTitle(@Nullable final String title) {
this.title = title;
return this;
}
public ProblemBuilder withStatus(@Nullable final HttpStatus status) {
this.status = status;
return this;
}
public ProblemBuilder withDetail(@Nullable final String detail) {
this.detail = detail;
return this;
}
/**
* Adds a custom attribute corresponding to an instance of the problem.
*
* @param key property name
* @param value property value
* @return problem builder current instance for chaining
*/
public ProblemBuilder with(final String key, final Object value) {
if (RESERVED_PROPERTIES.contains(key)) {
throw new IllegalArgumentException("Property " + key + " is reserved");
}
parameters.put(key, value);
return this;
}
public Problem build() {
return new Problem(type, title, detail, status, new LinkedHashMap<>(parameters));
}
}
}
package fr.da_sha1.exceptions;
//... imports are skipped for simplicity
/**
* Spring MVC exception handler for exception thrown by Spring controllers.
* @author Daria HERVIEUX
*/
@ControllerAdvice
public class RestResponseExceptionHandler extends ResponseEntityExceptionHandler {
private static final String PROBLEMS_SCHEME = "https";
private static final String PROBLEMS_HOST = "errors.da-sha1.fr";
private static final String BEGINNING_MSG_IMPOSSIBLE_FULFILL_REQUEST = "Impossible to fulfill the request. %s";
private static final Logger log = LoggerFactory.getLogger(RestResponseExceptionHandler.class);
// application specific exceptions
@ExceptionHandler(value = {IllegalArgumentException.class, IllegalStateException.class})
protected ResponseEntity<Object> handleInternalError(RuntimeException ex, WebRequest request) {
return getObjectResponseEntity(ex, request, HttpStatus.INTERNAL_SERVER_ERROR,
ProblemType.UNEXPECTED_ERROR,
ex.getMessage());
}
@ExceptionHandler(value = {OperationFailedException.class})
protected ResponseEntity<Object> handleOperationFailedException(OperationFailedException ex, WebRequest request) {
return getObjectResponseEntity(ex, request, HttpStatus.INTERNAL_SERVER_ERROR,
ProblemType.UNEXPECTED_ERROR,
String.format(BEGINNING_MSG_IMPOSSIBLE_FULFILL_REQUEST, ex.getMessage()));
}
//... the full list of exception is skipped for simplicity
// overriding Spring exception handling
@Override
protected ResponseEntity<Object> handleMethodArgumentNotValid(MethodArgumentNotValidException ex,
HttpHeaders headers, HttpStatus status, WebRequest request) {
return getObjectResponseEntity(ex, request, status, ProblemType.INVALID_INPUT, ex.getMessage());
}
@Override
protected ResponseEntity<Object> handleHttpMessageNotReadable(HttpMessageNotReadableException ex,
HttpHeaders headers, HttpStatus status, WebRequest request) {
return getObjectResponseEntity(ex, request, status, ProblemType.INVALID_INPUT, ex.getMessage());
}
//... the full list of overrides is skipped for simplicity
@Override
@Nullable
protected ResponseEntity<Object> handleAsyncRequestTimeoutException(AsyncRequestTimeoutException ex,
HttpHeaders headers, HttpStatus status, WebRequest request) {
if (super.handleAsyncRequestTimeoutException(ex, headers, status, request) != null) {
return getObjectResponseEntity(ex, request, status, ProblemType.UNEXPECTED_ERROR, ex.getMessage());
}
return null;
}
private ResponseEntity<Object> getObjectResponseEntity(Exception ex, WebRequest request,
HttpStatus code,
ProblemType type,
String problemDetails) {
log.error("Exception message : {} - HttpStatus code = {}", problemDetails, code, ex);
String details = (problemDetails == null) ? "" : (": " + problemDetails);
URI uri = null;
try {
uri = new URI(PROBLEMS_SCHEME, PROBLEMS_HOST, "/" + type.getUriPath(), null);
} catch (URISyntaxException e) {
log.warn("Impossible to create a problem type URI for the code='{}' and the problem='{}'", code, type,e);
}
// creating RFC7807 compliant problem object
Problem bodyOfResponse = Problem.ProblemBuilder
.newBuilder()
.withType(uri)
.withTitle(type.getTitle())
.withDetail(details)
.withStatus(code)
.build();
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_PROBLEM_JSON_UTF8);
// using super-class method
return handleExceptionInternal(ex, bodyOfResponse,
headers, code, request);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment