Skip to content

Instantly share code, notes, and snippets.

@matsev
Last active September 28, 2018 16:22
Show Gist options
  • Save matsev/3104749 to your computer and use it in GitHub Desktop.
Save matsev/3104749 to your computer and use it in GitHub Desktop.
Enhanced error feedback from a Spring Controller
@XmlRootElement
public class ErrorMessage {
private List<String> errors;
public ErrorMessage() {
}
public ErrorMessage(List<String> errors) {
this.errors = errors;
}
public ErrorMessage(String error) {
this(Collections.singletonList(error));
}
public ErrorMessage(String ... errors) {
this(Arrays.asList(errors));
}
public List<String> getErrors() {
return errors;
}
public void setErrors(List<String> errors) {
this.errors = errors;
}
}
/**
* Factory interface for creating {@link ErrorMessage} based on a specific {@code Exception}.
* @param <T> The specific exception type.
*/
public interface ErrorMessageFactory<T extends Exception> {
/**
* Gets the exception class used for this factory.
* @return An exception class.
*/
Class<T> getExceptionClass();
/**
* Creates an {@link ErrorMessage} from an exception.
* @param ex The exception to get data from.
* @return An error message.
*/
ErrorMessage getErrorMessage(T ex);
/**
* Gets the HTTP status response code that will be written to the response when the message occurs.
* @return An HTTP status response.
*/
int getResponseCode();
}
public class ErrorMessageHandlerExceptionResolver extends AbstractHandlerExceptionResolver {
private static final int DEFAULT_ORDER = 0;
private Map<Class<? extends Exception>, ErrorMessageFactory> errorMessageFactories;
private HttpMessageConverter<?>[] messageConverters;
public ErrorMessageHandlerExceptionResolver() {
setOrder(DEFAULT_ORDER);
}
public void setErrorMessageFactories(ErrorMessageFactory[] errorMessageFactories) {
this.errorMessageFactories = new HashMap<>(errorMessageFactories.length);
for (ErrorMessageFactory<?> errorMessageFactory : errorMessageFactories) {
this.errorMessageFactories.put(errorMessageFactory.getExceptionClass(), errorMessageFactory);
}
}
public void setMessageConverters(HttpMessageConverter<?>[] messageConverters) {
this.messageConverters = messageConverters;
}
@SuppressWarnings("unchecked")
@Override
protected ModelAndView doResolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
ErrorMessageFactory errorMessageFactory = errorMessageFactories.get(ex.getClass());
if (errorMessageFactory != null) {
response.setStatus(errorMessageFactory.getResponseCode());
ErrorMessage errorMessage = errorMessageFactory.getErrorMessage(ex);
ServletWebRequest webRequest = new ServletWebRequest(request, response);
try {
return handleResponseBody(errorMessage, webRequest);
} catch (Exception handlerException) {
logger.warn("Handling of [" + ex.getClass().getName() + "] resulted in Exception", handlerException);
}
}
return null;
}
/**
* Copied from {@link org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerExceptionResolver}
*/
@SuppressWarnings("unchecked")
private ModelAndView handleResponseBody(Object returnValue, ServletWebRequest webRequest)
throws ServletException, IOException {
HttpInputMessage inputMessage = new ServletServerHttpRequest(webRequest.getRequest());
List<MediaType> acceptedMediaTypes = inputMessage.getHeaders().getAccept();
if (acceptedMediaTypes.isEmpty()) {
acceptedMediaTypes = Collections.singletonList(MediaType.ALL);
}
MediaType.sortByQualityValue(acceptedMediaTypes);
HttpOutputMessage outputMessage = new ServletServerHttpResponse(webRequest.getResponse());
Class<?> returnValueType = returnValue.getClass();
if (this.messageConverters != null) {
for (MediaType acceptedMediaType : acceptedMediaTypes) {
for (HttpMessageConverter messageConverter : this.messageConverters) {
if (messageConverter.canWrite(returnValueType, acceptedMediaType)) {
messageConverter.write(returnValue, acceptedMediaType, outputMessage);
return new ModelAndView();
}
}
}
}
if (logger.isWarnEnabled()) {
logger.warn("Could not find HttpMessageConverter that supports return type [" + returnValueType + "] and " +
acceptedMediaTypes);
}
return null;
}
}
@ExceptionHandler
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ResponseBody
ErrorMessage handleException(MethodArgumentNotValidException ex) {
List<FieldError> fieldErrors = ex.getBindingResult().getFieldErrors();
List<ObjectError> globalErrors = ex.getBindingResult().getGlobalErrors();
List<String> errors = new ArrayList<>(fieldErrors.size() + globalErrors.size());
String error;
for (FieldError fieldError : fieldErrors) {
error = fieldError.getField() + ", " + fieldError.getDefaultMessage();
errors.add(error);
}
for (ObjectError objectError : globalErrors) {
error = objectError.getObjectName() + ", " + objectError.getDefaultMessage();
errors.add(error);
}
return new ErrorMessage(errors);
}
public class HttpMediaTypeNotSupportedExceptionErrorMessageFactory implements ErrorMessageFactory<HttpMediaTypeNotSupportedException> {
@Override
public int getResponseCode() {
return HttpServletResponse.SC_UNSUPPORTED_MEDIA_TYPE;
}
@Override
public Class<HttpMediaTypeNotSupportedException> getExceptionClass() {
return HttpMediaTypeNotSupportedException.class;
}
@Override
public ErrorMessage getErrorMessage(HttpMediaTypeNotSupportedException ex) {
String unsupported = "Unsupported content type: " + ex.getContentType();
String supported = "Supported content types: " + MediaType.toString(ex.getSupportedMediaTypes());
return new ErrorMessage(unsupported, supported);
}
}
public class HttpMessageNotReadableExceptionErrorMessageFactory implements ErrorMessageFactory<HttpMessageNotReadableException> {
@Override
public int getResponseCode() {
return HttpServletResponse.SC_BAD_REQUEST;
}
@Override
public Class<HttpMessageNotReadableException> getExceptionClass() {
return HttpMessageNotReadableException.class;
}
@Override
public ErrorMessage getErrorMessage(HttpMessageNotReadableException ex) {
Throwable mostSpecificCause = ex.getMostSpecificCause();
if (mostSpecificCause != null) {
String exceptionName = mostSpecificCause.getClass().getName();
String message = mostSpecificCause.getMessage();
return new ErrorMessage(exceptionName, message);
}
return new ErrorMessage(ex.getMessage());
}
}
public class MethodArgumentNotValidExceptionErrorMessageFactory implements ErrorMessageFactory<MethodArgumentNotValidException> {
@Override
public Class<MethodArgumentNotValidException> getExceptionClass() {
return MethodArgumentNotValidException.class;
}
@Override
public int getResponseCode() {
return HttpServletResponse.SC_BAD_REQUEST;
}
@Override
public ErrorMessage getErrorMessage(MethodArgumentNotValidException ex) {
List<FieldError> fieldErrors = ex.getBindingResult().getFieldErrors();
List<ObjectError> globalErrors = ex.getBindingResult().getGlobalErrors();
List<String> errors = new ArrayList<>(fieldErrors.size() + globalErrors.size());
String error;
for (FieldError fieldError : fieldErrors) {
error = fieldError.getField() + ", " + fieldError.getDefaultMessage();
errors.add(error);
}
for (ObjectError objectError : globalErrors) {
error = objectError.getObjectName() + ", " + objectError.getDefaultMessage();
errors.add(error);
}
return new ErrorMessage(errors);
}
}
@XmlRootElement
public class User {
@NotBlank
@Length(min = 3, max = 30)
private String name;
@Email
private String email;
// Getters and setters omitted
}
@RequestMapping(value = "/user/{userId}",
method = RequestMethod.PUT,
consumes = {MediaType.APPLICATION_JSON_VALUE, MediaType.APPLICATION_XML_VALUE})
@ResponseStatus(HttpStatus.NO_CONTENT)
void update(@PathVariable("userId") long userId,
@RequestBody @Valid User user) {
userService.update(userId, user);
}
{
"errors": [
"name, may not be empty",
"email, not a well-formed email address"
]
}
@matsev
Copy link
Author

matsev commented Jan 12, 2013

The code in this gist shows how the response from a Spring Controller can be enhanced to provide more information to the client. It is explained in more detail in two blog posts, http://www.jayway.com/2012/09/16/improve-your-spring-rest-api-part-i and http://www.jayway.com/2012/09/23/improve-your-spring-rest-api-part-ii .

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment