Created
March 21, 2019 10:57
-
-
Save dariahervieux/57aeb1292e1f55be87b06a845d264704 to your computer and use it in GitHub Desktop.
Spring MVC: return a RFC7807 problem object for Spring controller exception
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)); | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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