Skip to content

Commit

Permalink
ErrorResponse support in Spring MVC exception hierarchy
Browse files Browse the repository at this point in the history
All Spring MVC exceptions from spring-web, now implement ErrorResponse
and expose HTTP error response information, including an RFC 7807 body.

See gh-27052
  • Loading branch information
rstoyanchev committed Feb 28, 2022
1 parent 3efedef commit 76be637
Show file tree
Hide file tree
Showing 22 changed files with 230 additions and 49 deletions.
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2013 the original author or authors.
* Copyright 2002-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -22,6 +22,7 @@
import jakarta.servlet.ServletException;

import org.springframework.http.MediaType;
import org.springframework.http.ProblemDetail;

/**
* Abstract base for exceptions related to media types. Adds a list of supported {@link MediaType MediaTypes}.
Expand All @@ -30,10 +31,12 @@
* @since 3.0
*/
@SuppressWarnings("serial")
public abstract class HttpMediaTypeException extends ServletException {
public abstract class HttpMediaTypeException extends ServletException implements ErrorResponse {

private final List<MediaType> supportedMediaTypes;

private final ProblemDetail body = ProblemDetail.forRawStatusCode(getRawStatusCode());


/**
* Create a new HttpMediaTypeException.
Expand Down Expand Up @@ -61,4 +64,9 @@ public List<MediaType> getSupportedMediaTypes() {
return this.supportedMediaTypes;
}

@Override
public ProblemDetail getBody() {
return this.body;
}

}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2012 the original author or authors.
* Copyright 2002-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -18,10 +18,14 @@

import java.util.List;

import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.util.CollectionUtils;

/**
* Exception thrown when the request handler cannot generate a response that is acceptable by the client.
* Exception thrown when the request handler cannot generate a response that is
* acceptable by the client.
*
* @author Arjen Poutsma
* @since 3.0
Expand All @@ -30,19 +34,36 @@
public class HttpMediaTypeNotAcceptableException extends HttpMediaTypeException {

/**
* Create a new HttpMediaTypeNotAcceptableException.
* @param message the exception message
* Constructor for when the {@code Accept} header cannot be parsed.
* @param message the parse error message
*/
public HttpMediaTypeNotAcceptableException(String message) {
super(message);
getBody().setDetail("Could not parse Accept header");
}

/**
* Create a new HttpMediaTypeNotSupportedException.
* @param supportedMediaTypes the list of supported media types
*/
public HttpMediaTypeNotAcceptableException(List<MediaType> supportedMediaTypes) {
super("Could not find acceptable representation", supportedMediaTypes);
super("No acceptable representation", supportedMediaTypes);
}


@Override
public int getRawStatusCode() {
return HttpStatus.NOT_ACCEPTABLE.value();
}

@Override
public HttpHeaders getHeaders() {
if (CollectionUtils.isEmpty(getSupportedMediaTypes())) {
return HttpHeaders.EMPTY;
}
HttpHeaders headers = new HttpHeaders();
headers.setAccept(this.getSupportedMediaTypes());
return headers;
}

}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2017 the original author or authors.
* Copyright 2002-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -18,14 +18,19 @@

import java.util.List;

import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.lang.Nullable;
import org.springframework.util.CollectionUtils;

/**
* Exception thrown when a client POSTs, PUTs, or PATCHes content of a type
* not supported by request handler.
*
* @author Arjen Poutsma
* @author Rossen Stoyanchev
* @since 3.0
*/
@SuppressWarnings("serial")
Expand All @@ -34,6 +39,9 @@ public class HttpMediaTypeNotSupportedException extends HttpMediaTypeException {
@Nullable
private final MediaType contentType;

@Nullable
private final HttpMethod httpMethod;


/**
* Create a new HttpMediaTypeNotSupportedException.
Expand All @@ -42,6 +50,8 @@ public class HttpMediaTypeNotSupportedException extends HttpMediaTypeException {
public HttpMediaTypeNotSupportedException(String message) {
super(message);
this.contentType = null;
this.httpMethod = null;
getBody().setDetail("Could not parse Content-Type");
}

/**
Expand All @@ -50,21 +60,38 @@ public HttpMediaTypeNotSupportedException(String message) {
* @param supportedMediaTypes the list of supported media types
*/
public HttpMediaTypeNotSupportedException(@Nullable MediaType contentType, List<MediaType> supportedMediaTypes) {
this(contentType, supportedMediaTypes, "Content type '" +
(contentType != null ? contentType : "") + "' not supported");
this(contentType, supportedMediaTypes, null);
}

/**
* Create a new HttpMediaTypeNotSupportedException.
* @param contentType the unsupported content type
* @param supportedMediaTypes the list of supported media types
* @param msg the detail message
* @param httpMethod the HTTP method of the request
* @since 6.0
*/
public HttpMediaTypeNotSupportedException(@Nullable MediaType contentType,
List<MediaType> supportedMediaTypes, String msg) {
List<MediaType> supportedMediaTypes, @Nullable HttpMethod httpMethod) {

this(contentType, supportedMediaTypes, httpMethod,
"Content-Type " + (contentType != null ? "'" + contentType + "' " : "") + "is not supported");
}

super(msg, supportedMediaTypes);
/**
* Create a new HttpMediaTypeNotSupportedException.
* @param contentType the unsupported content type
* @param supportedMediaTypes the list of supported media types
* @param httpMethod the HTTP method of the request
* @param message the detail message
* @since 6.0
*/
public HttpMediaTypeNotSupportedException(@Nullable MediaType contentType,
List<MediaType> supportedMediaTypes, @Nullable HttpMethod httpMethod, String message) {

super(message, supportedMediaTypes);
this.contentType = contentType;
this.httpMethod = httpMethod;
getBody().setDetail("Content-Type " + this.contentType + " is not supported");
}


Expand All @@ -76,4 +103,22 @@ public MediaType getContentType() {
return this.contentType;
}

@Override
public int getRawStatusCode() {
return HttpStatus.UNSUPPORTED_MEDIA_TYPE.value();
}

@Override
public HttpHeaders getHeaders() {
if (CollectionUtils.isEmpty(getSupportedMediaTypes())) {
return HttpHeaders.EMPTY;
}
HttpHeaders headers = new HttpHeaders();
headers.setAccept(getSupportedMediaTypes());
if (HttpMethod.PATCH.equals(this.httpMethod)) {
headers.setAcceptPatch(getSupportedMediaTypes());
}
return headers;
}

}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2020 the original author or authors.
* Copyright 2002-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -22,8 +22,12 @@

import jakarta.servlet.ServletException;

import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.ProblemDetail;
import org.springframework.lang.Nullable;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;

/**
Expand All @@ -34,13 +38,15 @@
* @since 2.0
*/
@SuppressWarnings("serial")
public class HttpRequestMethodNotSupportedException extends ServletException {
public class HttpRequestMethodNotSupportedException extends ServletException implements ErrorResponse {

private final String method;

@Nullable
private final String[] supportedMethods;

private final ProblemDetail body;


/**
* Create a new HttpRequestMethodNotSupportedException.
Expand Down Expand Up @@ -74,7 +80,7 @@ public HttpRequestMethodNotSupportedException(String method, @Nullable Collectio
* @param supportedMethods the actually supported HTTP methods (may be {@code null})
*/
public HttpRequestMethodNotSupportedException(String method, @Nullable String[] supportedMethods) {
this(method, supportedMethods, "Request method '" + method + "' not supported");
this(method, supportedMethods, "Request method '" + method + "' is not supported");
}

/**
Expand All @@ -87,6 +93,8 @@ public HttpRequestMethodNotSupportedException(String method, @Nullable String[]
super(msg);
this.method = method;
this.supportedMethods = supportedMethods;
this.body = ProblemDetail.forRawStatusCode(getRawStatusCode())
.withDetail("Method '" + method + "' is not supported");
}


Expand Down Expand Up @@ -123,4 +131,24 @@ public Set<HttpMethod> getSupportedHttpMethods() {
return supportedMethods;
}

@Override
public int getRawStatusCode() {
return HttpStatus.METHOD_NOT_ALLOWED.value();
}

@Override
public HttpHeaders getHeaders() {
if (ObjectUtils.isEmpty(this.supportedMethods)) {
return HttpHeaders.EMPTY;
}
HttpHeaders headers = new HttpHeaders();
headers.add(HttpHeaders.ALLOW, StringUtils.arrayToDelimitedString(this.supportedMethods, ", "));
return headers;
}

@Override
public ProblemDetail getBody() {
return this.body;
}

}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2021 the original author or authors.
* Copyright 2002-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -17,9 +17,12 @@
package org.springframework.web.bind;

import org.springframework.core.MethodParameter;
import org.springframework.http.HttpStatus;
import org.springframework.http.ProblemDetail;
import org.springframework.validation.BindException;
import org.springframework.validation.BindingResult;
import org.springframework.validation.ObjectError;
import org.springframework.web.ErrorResponse;

/**
* Exception to be thrown when validation on an argument annotated with {@code @Valid} fails.
Expand All @@ -30,10 +33,12 @@
* @since 3.1
*/
@SuppressWarnings("serial")
public class MethodArgumentNotValidException extends BindException {
public class MethodArgumentNotValidException extends BindException implements ErrorResponse {

private final MethodParameter parameter;

private final ProblemDetail body;


/**
* Constructor for {@link MethodArgumentNotValidException}.
Expand All @@ -43,9 +48,20 @@ public class MethodArgumentNotValidException extends BindException {
public MethodArgumentNotValidException(MethodParameter parameter, BindingResult bindingResult) {
super(bindingResult);
this.parameter = parameter;
this.body = ProblemDetail.forRawStatusCode(getRawStatusCode()).withDetail(initMessage(parameter));
}


@Override
public int getRawStatusCode() {
return HttpStatus.BAD_REQUEST.value();
}

@Override
public ProblemDetail getBody() {
return this.body;
}

/**
* Return the method parameter that failed validation.
*/
Expand All @@ -55,9 +71,13 @@ public final MethodParameter getParameter() {

@Override
public String getMessage() {
return initMessage(this.parameter);
}

private String initMessage(MethodParameter parameter) {
StringBuilder sb = new StringBuilder("Validation failed for argument [")
.append(this.parameter.getParameterIndex()).append("] in ")
.append(this.parameter.getExecutable().toGenericString());
.append(parameter.getParameterIndex()).append("] in ")
.append(parameter.getExecutable().toGenericString());
BindingResult bindingResult = getBindingResult();
if (bindingResult.getErrorCount() > 1) {
sb.append(" with ").append(bindingResult.getErrorCount()).append(" errors");
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2021 the original author or authors.
* Copyright 2002-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -57,6 +57,7 @@ public MissingMatrixVariableException(
super("", missingAfterConversion);
this.variableName = variableName;
this.parameter = parameter;
getBody().setDetail("Required path parameter '" + this.variableName + "' is not present");
}


Expand Down
Loading

0 comments on commit 76be637

Please sign in to comment.