Skip to content

Commit 76be637

Browse files
committed
ErrorResponse support in Spring MVC exception hierarchy
All Spring MVC exceptions from spring-web, now implement ErrorResponse and expose HTTP error response information, including an RFC 7807 body. See gh-27052
1 parent 3efedef commit 76be637

22 files changed

+230
-49
lines changed

spring-web/src/main/java/org/springframework/web/HttpMediaTypeException.java

+10-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2013 the original author or authors.
2+
* Copyright 2002-2022 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -22,6 +22,7 @@
2222
import jakarta.servlet.ServletException;
2323

2424
import org.springframework.http.MediaType;
25+
import org.springframework.http.ProblemDetail;
2526

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

3536
private final List<MediaType> supportedMediaTypes;
3637

38+
private final ProblemDetail body = ProblemDetail.forRawStatusCode(getRawStatusCode());
39+
3740

3841
/**
3942
* Create a new HttpMediaTypeException.
@@ -61,4 +64,9 @@ public List<MediaType> getSupportedMediaTypes() {
6164
return this.supportedMediaTypes;
6265
}
6366

67+
@Override
68+
public ProblemDetail getBody() {
69+
return this.body;
70+
}
71+
6472
}

spring-web/src/main/java/org/springframework/web/HttpMediaTypeNotAcceptableException.java

+26-5
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2012 the original author or authors.
2+
* Copyright 2002-2022 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -18,10 +18,14 @@
1818

1919
import java.util.List;
2020

21+
import org.springframework.http.HttpHeaders;
22+
import org.springframework.http.HttpStatus;
2123
import org.springframework.http.MediaType;
24+
import org.springframework.util.CollectionUtils;
2225

2326
/**
24-
* Exception thrown when the request handler cannot generate a response that is acceptable by the client.
27+
* Exception thrown when the request handler cannot generate a response that is
28+
* acceptable by the client.
2529
*
2630
* @author Arjen Poutsma
2731
* @since 3.0
@@ -30,19 +34,36 @@
3034
public class HttpMediaTypeNotAcceptableException extends HttpMediaTypeException {
3135

3236
/**
33-
* Create a new HttpMediaTypeNotAcceptableException.
34-
* @param message the exception message
37+
* Constructor for when the {@code Accept} header cannot be parsed.
38+
* @param message the parse error message
3539
*/
3640
public HttpMediaTypeNotAcceptableException(String message) {
3741
super(message);
42+
getBody().setDetail("Could not parse Accept header");
3843
}
3944

4045
/**
4146
* Create a new HttpMediaTypeNotSupportedException.
4247
* @param supportedMediaTypes the list of supported media types
4348
*/
4449
public HttpMediaTypeNotAcceptableException(List<MediaType> supportedMediaTypes) {
45-
super("Could not find acceptable representation", supportedMediaTypes);
50+
super("No acceptable representation", supportedMediaTypes);
51+
}
52+
53+
54+
@Override
55+
public int getRawStatusCode() {
56+
return HttpStatus.NOT_ACCEPTABLE.value();
57+
}
58+
59+
@Override
60+
public HttpHeaders getHeaders() {
61+
if (CollectionUtils.isEmpty(getSupportedMediaTypes())) {
62+
return HttpHeaders.EMPTY;
63+
}
64+
HttpHeaders headers = new HttpHeaders();
65+
headers.setAccept(this.getSupportedMediaTypes());
66+
return headers;
4667
}
4768

4869
}

spring-web/src/main/java/org/springframework/web/HttpMediaTypeNotSupportedException.java

+51-6
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2017 the original author or authors.
2+
* Copyright 2002-2022 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -18,14 +18,19 @@
1818

1919
import java.util.List;
2020

21+
import org.springframework.http.HttpHeaders;
22+
import org.springframework.http.HttpMethod;
23+
import org.springframework.http.HttpStatus;
2124
import org.springframework.http.MediaType;
2225
import org.springframework.lang.Nullable;
26+
import org.springframework.util.CollectionUtils;
2327

2428
/**
2529
* Exception thrown when a client POSTs, PUTs, or PATCHes content of a type
2630
* not supported by request handler.
2731
*
2832
* @author Arjen Poutsma
33+
* @author Rossen Stoyanchev
2934
* @since 3.0
3035
*/
3136
@SuppressWarnings("serial")
@@ -34,6 +39,9 @@ public class HttpMediaTypeNotSupportedException extends HttpMediaTypeException {
3439
@Nullable
3540
private final MediaType contentType;
3641

42+
@Nullable
43+
private final HttpMethod httpMethod;
44+
3745

3846
/**
3947
* Create a new HttpMediaTypeNotSupportedException.
@@ -42,6 +50,8 @@ public class HttpMediaTypeNotSupportedException extends HttpMediaTypeException {
4250
public HttpMediaTypeNotSupportedException(String message) {
4351
super(message);
4452
this.contentType = null;
53+
this.httpMethod = null;
54+
getBody().setDetail("Could not parse Content-Type");
4555
}
4656

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

5766
/**
5867
* Create a new HttpMediaTypeNotSupportedException.
5968
* @param contentType the unsupported content type
6069
* @param supportedMediaTypes the list of supported media types
61-
* @param msg the detail message
70+
* @param httpMethod the HTTP method of the request
71+
* @since 6.0
6272
*/
6373
public HttpMediaTypeNotSupportedException(@Nullable MediaType contentType,
64-
List<MediaType> supportedMediaTypes, String msg) {
74+
List<MediaType> supportedMediaTypes, @Nullable HttpMethod httpMethod) {
75+
76+
this(contentType, supportedMediaTypes, httpMethod,
77+
"Content-Type " + (contentType != null ? "'" + contentType + "' " : "") + "is not supported");
78+
}
6579

66-
super(msg, supportedMediaTypes);
80+
/**
81+
* Create a new HttpMediaTypeNotSupportedException.
82+
* @param contentType the unsupported content type
83+
* @param supportedMediaTypes the list of supported media types
84+
* @param httpMethod the HTTP method of the request
85+
* @param message the detail message
86+
* @since 6.0
87+
*/
88+
public HttpMediaTypeNotSupportedException(@Nullable MediaType contentType,
89+
List<MediaType> supportedMediaTypes, @Nullable HttpMethod httpMethod, String message) {
90+
91+
super(message, supportedMediaTypes);
6792
this.contentType = contentType;
93+
this.httpMethod = httpMethod;
94+
getBody().setDetail("Content-Type " + this.contentType + " is not supported");
6895
}
6996

7097

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

106+
@Override
107+
public int getRawStatusCode() {
108+
return HttpStatus.UNSUPPORTED_MEDIA_TYPE.value();
109+
}
110+
111+
@Override
112+
public HttpHeaders getHeaders() {
113+
if (CollectionUtils.isEmpty(getSupportedMediaTypes())) {
114+
return HttpHeaders.EMPTY;
115+
}
116+
HttpHeaders headers = new HttpHeaders();
117+
headers.setAccept(getSupportedMediaTypes());
118+
if (HttpMethod.PATCH.equals(this.httpMethod)) {
119+
headers.setAcceptPatch(getSupportedMediaTypes());
120+
}
121+
return headers;
122+
}
123+
79124
}

spring-web/src/main/java/org/springframework/web/HttpRequestMethodNotSupportedException.java

+31-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2020 the original author or authors.
2+
* Copyright 2002-2022 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -22,8 +22,12 @@
2222

2323
import jakarta.servlet.ServletException;
2424

25+
import org.springframework.http.HttpHeaders;
2526
import org.springframework.http.HttpMethod;
27+
import org.springframework.http.HttpStatus;
28+
import org.springframework.http.ProblemDetail;
2629
import org.springframework.lang.Nullable;
30+
import org.springframework.util.ObjectUtils;
2731
import org.springframework.util.StringUtils;
2832

2933
/**
@@ -34,13 +38,15 @@
3438
* @since 2.0
3539
*/
3640
@SuppressWarnings("serial")
37-
public class HttpRequestMethodNotSupportedException extends ServletException {
41+
public class HttpRequestMethodNotSupportedException extends ServletException implements ErrorResponse {
3842

3943
private final String method;
4044

4145
@Nullable
4246
private final String[] supportedMethods;
4347

48+
private final ProblemDetail body;
49+
4450

4551
/**
4652
* Create a new HttpRequestMethodNotSupportedException.
@@ -74,7 +80,7 @@ public HttpRequestMethodNotSupportedException(String method, @Nullable Collectio
7480
* @param supportedMethods the actually supported HTTP methods (may be {@code null})
7581
*/
7682
public HttpRequestMethodNotSupportedException(String method, @Nullable String[] supportedMethods) {
77-
this(method, supportedMethods, "Request method '" + method + "' not supported");
83+
this(method, supportedMethods, "Request method '" + method + "' is not supported");
7884
}
7985

8086
/**
@@ -87,6 +93,8 @@ public HttpRequestMethodNotSupportedException(String method, @Nullable String[]
8793
super(msg);
8894
this.method = method;
8995
this.supportedMethods = supportedMethods;
96+
this.body = ProblemDetail.forRawStatusCode(getRawStatusCode())
97+
.withDetail("Method '" + method + "' is not supported");
9098
}
9199

92100

@@ -123,4 +131,24 @@ public Set<HttpMethod> getSupportedHttpMethods() {
123131
return supportedMethods;
124132
}
125133

134+
@Override
135+
public int getRawStatusCode() {
136+
return HttpStatus.METHOD_NOT_ALLOWED.value();
137+
}
138+
139+
@Override
140+
public HttpHeaders getHeaders() {
141+
if (ObjectUtils.isEmpty(this.supportedMethods)) {
142+
return HttpHeaders.EMPTY;
143+
}
144+
HttpHeaders headers = new HttpHeaders();
145+
headers.add(HttpHeaders.ALLOW, StringUtils.arrayToDelimitedString(this.supportedMethods, ", "));
146+
return headers;
147+
}
148+
149+
@Override
150+
public ProblemDetail getBody() {
151+
return this.body;
152+
}
153+
126154
}

spring-web/src/main/java/org/springframework/web/bind/MethodArgumentNotValidException.java

+24-4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2021 the original author or authors.
2+
* Copyright 2002-2022 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -17,9 +17,12 @@
1717
package org.springframework.web.bind;
1818

1919
import org.springframework.core.MethodParameter;
20+
import org.springframework.http.HttpStatus;
21+
import org.springframework.http.ProblemDetail;
2022
import org.springframework.validation.BindException;
2123
import org.springframework.validation.BindingResult;
2224
import org.springframework.validation.ObjectError;
25+
import org.springframework.web.ErrorResponse;
2326

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

3538
private final MethodParameter parameter;
3639

40+
private final ProblemDetail body;
41+
3742

3843
/**
3944
* Constructor for {@link MethodArgumentNotValidException}.
@@ -43,9 +48,20 @@ public class MethodArgumentNotValidException extends BindException {
4348
public MethodArgumentNotValidException(MethodParameter parameter, BindingResult bindingResult) {
4449
super(bindingResult);
4550
this.parameter = parameter;
51+
this.body = ProblemDetail.forRawStatusCode(getRawStatusCode()).withDetail(initMessage(parameter));
4652
}
4753

4854

55+
@Override
56+
public int getRawStatusCode() {
57+
return HttpStatus.BAD_REQUEST.value();
58+
}
59+
60+
@Override
61+
public ProblemDetail getBody() {
62+
return this.body;
63+
}
64+
4965
/**
5066
* Return the method parameter that failed validation.
5167
*/
@@ -55,9 +71,13 @@ public final MethodParameter getParameter() {
5571

5672
@Override
5773
public String getMessage() {
74+
return initMessage(this.parameter);
75+
}
76+
77+
private String initMessage(MethodParameter parameter) {
5878
StringBuilder sb = new StringBuilder("Validation failed for argument [")
59-
.append(this.parameter.getParameterIndex()).append("] in ")
60-
.append(this.parameter.getExecutable().toGenericString());
79+
.append(parameter.getParameterIndex()).append("] in ")
80+
.append(parameter.getExecutable().toGenericString());
6181
BindingResult bindingResult = getBindingResult();
6282
if (bindingResult.getErrorCount() > 1) {
6383
sb.append(" with ").append(bindingResult.getErrorCount()).append(" errors");

spring-web/src/main/java/org/springframework/web/bind/MissingMatrixVariableException.java

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2021 the original author or authors.
2+
* Copyright 2002-2022 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -57,6 +57,7 @@ public MissingMatrixVariableException(
5757
super("", missingAfterConversion);
5858
this.variableName = variableName;
5959
this.parameter = parameter;
60+
getBody().setDetail("Required path parameter '" + this.variableName + "' is not present");
6061
}
6162

6263

0 commit comments

Comments
 (0)