Browse Source

MessageSource support for Spring MVC and WebFlux exceptions

See gh-28814
pull/29282/head
rstoyanchev 2 years ago
parent
commit
a4210854fb
  1. 48
      spring-web/src/main/java/org/springframework/web/ErrorResponse.java
  2. 36
      spring-web/src/main/java/org/springframework/web/ErrorResponseException.java
  3. 41
      spring-web/src/main/java/org/springframework/web/HttpMediaTypeException.java
  4. 13
      spring-web/src/main/java/org/springframework/web/HttpMediaTypeNotAcceptableException.java
  5. 9
      spring-web/src/main/java/org/springframework/web/HttpMediaTypeNotSupportedException.java
  6. 17
      spring-web/src/main/java/org/springframework/web/HttpRequestMethodNotSupportedException.java
  7. 63
      spring-web/src/main/java/org/springframework/web/bind/MethodArgumentNotValidException.java
  8. 2
      spring-web/src/main/java/org/springframework/web/bind/MissingMatrixVariableException.java
  9. 2
      spring-web/src/main/java/org/springframework/web/bind/MissingPathVariableException.java
  10. 2
      spring-web/src/main/java/org/springframework/web/bind/MissingRequestCookieException.java
  11. 2
      spring-web/src/main/java/org/springframework/web/bind/MissingRequestHeaderException.java
  12. 25
      spring-web/src/main/java/org/springframework/web/bind/MissingRequestValueException.java
  13. 2
      spring-web/src/main/java/org/springframework/web/bind/MissingServletRequestParameterException.java
  14. 61
      spring-web/src/main/java/org/springframework/web/bind/ServletRequestBindingException.java
  15. 25
      spring-web/src/main/java/org/springframework/web/bind/UnsatisfiedServletRequestParameterException.java
  16. 20
      spring-web/src/main/java/org/springframework/web/bind/support/WebExchangeBindException.java
  17. 5
      spring-web/src/main/java/org/springframework/web/multipart/support/MissingServletRequestPartException.java
  18. 11
      spring-web/src/main/java/org/springframework/web/server/MethodNotAllowedException.java
  19. 4
      spring-web/src/main/java/org/springframework/web/server/MissingRequestValueException.java
  20. 15
      spring-web/src/main/java/org/springframework/web/server/NotAcceptableStatusException.java
  21. 17
      spring-web/src/main/java/org/springframework/web/server/ResponseStatusException.java
  22. 6
      spring-web/src/main/java/org/springframework/web/server/ServerErrorException.java
  23. 15
      spring-web/src/main/java/org/springframework/web/server/ServerWebInputException.java
  24. 8
      spring-web/src/main/java/org/springframework/web/server/UnsatisfiedRequestParameterException.java
  25. 17
      spring-web/src/main/java/org/springframework/web/server/UnsupportedMediaTypeStatusException.java
  26. 189
      spring-web/src/test/java/org/springframework/web/ErrorResponseExceptionTests.java
  27. 30
      spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/ResponseEntityExceptionHandler.java
  28. 49
      spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/ResponseEntityExceptionHandlerTests.java
  29. 32
      spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ResponseEntityExceptionHandler.java
  30. 32
      spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ResponseEntityExceptionHandlerTests.java
  31. 3
      spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/support/DefaultHandlerExceptionResolverTests.java

48
spring-web/src/main/java/org/springframework/web/ErrorResponse.java

@ -16,9 +16,13 @@
package org.springframework.web; package org.springframework.web;
import java.util.Locale;
import org.springframework.context.MessageSource;
import org.springframework.http.HttpHeaders; import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatusCode; import org.springframework.http.HttpStatusCode;
import org.springframework.http.ProblemDetail; import org.springframework.http.ProblemDetail;
import org.springframework.lang.Nullable;
/** /**
@ -59,4 +63,48 @@ public interface ErrorResponse {
*/ */
ProblemDetail getBody(); ProblemDetail getBody();
/**
* Return a code to use to resolve the problem "detail" for this exception
* through a {@link org.springframework.context.MessageSource}.
* <p>By default this is initialized via
* {@link #getDefaultDetailMessageCode(Class, String)} but each exception
* overrides this to provide relevant data that that can be expanded into
* placeholders within the message.
*/
default String getDetailMessageCode() {
return getDefaultDetailMessageCode(getClass(), null);
}
/**
* Return the arguments to use to resolve the problem "detail" through a
* {@link MessageSource}.
*/
@Nullable
default Object[] getDetailMessageArguments() {
return null;
}
/**
* Variant of {@link #getDetailMessageArguments()} that uses the given
* {@link MessageSource} to resolve the message arguments.
* <p>By default this delegates to {@link #getDetailMessageArguments()}
* by concrete implementations may override it, for example in order to
* resolve validation errors through a {@code MessageSource}.
*/
@Nullable
default Object[] getDetailMessageArguments(MessageSource messageSource, Locale locale) {
return getDetailMessageArguments();
}
/**
* Build a message code for the given exception type, which consists of
* {@code "problemDetail."} followed by the full {@link Class#getName() class name}.
* @param exceptionType the exception type for which to build a code
* @param suffix an optional suffix, e.g. for exceptions that may have multiple
* error message with different arguments.
*/
static String getDefaultDetailMessageCode(Class<?> exceptionType, @Nullable String suffix) {
return "problemDetail." + exceptionType.getName() + (suffix != null ? "." + suffix : "");
}
} }

36
spring-web/src/main/java/org/springframework/web/ErrorResponseException.java

@ -46,6 +46,11 @@ public class ErrorResponseException extends NestedRuntimeException implements Er
private final ProblemDetail body; private final ProblemDetail body;
private final String messageDetailCode;
@Nullable
private final Object[] messageDetailArguments;
/** /**
* Constructor with a {@link HttpStatusCode}. * Constructor with a {@link HttpStatusCode}.
@ -66,11 +71,32 @@ public class ErrorResponseException extends NestedRuntimeException implements Er
* subclass of {@code ProblemDetail} with extended fields. * subclass of {@code ProblemDetail} with extended fields.
*/ */
public ErrorResponseException(HttpStatusCode status, ProblemDetail body, @Nullable Throwable cause) { public ErrorResponseException(HttpStatusCode status, ProblemDetail body, @Nullable Throwable cause) {
this(status, body, cause, null, null);
}
/**
* Constructor with a given {@link ProblemDetail}, and a
* {@link org.springframework.context.MessageSource} code and arguments to
* resolve the detail message with.
* @since 6.0
*/
protected ErrorResponseException(
HttpStatusCode status, ProblemDetail body, @Nullable Throwable cause,
@Nullable String messageDetailCode, @Nullable Object[] messageDetailArguments) {
super(null, cause); super(null, cause);
this.status = status; this.status = status;
this.body = body; this.body = body;
this.messageDetailCode = initMessageDetailCode(messageDetailCode);
this.messageDetailArguments = messageDetailArguments;
}
private String initMessageDetailCode(@Nullable String messageDetailCode) {
return (messageDetailCode != null ?
messageDetailCode : ErrorResponse.getDefaultDetailMessageCode(getClass(), null));
} }
@Override @Override
public HttpStatusCode getStatusCode() { public HttpStatusCode getStatusCode() {
return this.status; return this.status;
@ -133,6 +159,16 @@ public class ErrorResponseException extends NestedRuntimeException implements Er
return this.body; return this.body;
} }
@Override
public String getDetailMessageCode() {
return this.messageDetailCode;
}
@Override
public Object[] getDetailMessageArguments() {
return this.messageDetailArguments;
}
@Override @Override
public String getMessage() { public String getMessage() {
return this.status + (!this.headers.isEmpty() ? ", headers=" + this.headers : "") + ", " + this.body; return this.status + (!this.headers.isEmpty() ? ", headers=" + this.headers : "") + ", " + this.body;

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

@ -23,6 +23,7 @@ import jakarta.servlet.ServletException;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.http.ProblemDetail; import org.springframework.http.ProblemDetail;
import org.springframework.lang.Nullable;
/** /**
* Abstract base for exceptions related to media types. Adds a list of supported {@link MediaType MediaTypes}. * Abstract base for exceptions related to media types. Adds a list of supported {@link MediaType MediaTypes}.
@ -37,23 +38,49 @@ public abstract class HttpMediaTypeException extends ServletException implements
private final ProblemDetail body = ProblemDetail.forStatus(getStatusCode()); private final ProblemDetail body = ProblemDetail.forStatus(getStatusCode());
private final String messageDetailCode;
@Nullable
private final Object[] messageDetailArguments;
/** /**
* Create a new HttpMediaTypeException. * Create a new HttpMediaTypeException.
* @param message the exception message * @param message the exception message
* @deprecated as of 6.0
*/ */
@Deprecated
protected HttpMediaTypeException(String message) { protected HttpMediaTypeException(String message) {
super(message); this(message, Collections.emptyList());
this.supportedMediaTypes = Collections.emptyList();
} }
/** /**
* Create a new HttpMediaTypeException with a list of supported media types. * Create a new HttpMediaTypeException with a list of supported media types.
* @param supportedMediaTypes the list of supported media types * @param supportedMediaTypes the list of supported media types
* @deprecated as of 6.0
*/ */
@Deprecated
protected HttpMediaTypeException(String message, List<MediaType> supportedMediaTypes) { protected HttpMediaTypeException(String message, List<MediaType> supportedMediaTypes) {
this(message, supportedMediaTypes, null, null);
}
/**
* Create a new HttpMediaTypeException with a list of supported media types.
* @param supportedMediaTypes the list of supported media types
* @param messageDetailCode the code to use to resolve the problem "detail"
* through a {@link org.springframework.context.MessageSource}
* @param messageDetailArguments the arguments to make available when
* resolving the problem "detail" through a {@code MessageSource}
* @since 6.0
*/
protected HttpMediaTypeException(String message, List<MediaType> supportedMediaTypes,
@Nullable String messageDetailCode, @Nullable Object[] messageDetailArguments) {
super(message); super(message);
this.supportedMediaTypes = Collections.unmodifiableList(supportedMediaTypes); this.supportedMediaTypes = Collections.unmodifiableList(supportedMediaTypes);
this.messageDetailCode = (messageDetailCode != null ?
messageDetailCode : ErrorResponse.getDefaultDetailMessageCode(getClass(), null));
this.messageDetailArguments = messageDetailArguments;
} }
@ -69,4 +96,14 @@ public abstract class HttpMediaTypeException extends ServletException implements
return this.body; return this.body;
} }
@Override
public String getDetailMessageCode() {
return this.messageDetailCode;
}
@Override
public Object[] getDetailMessageArguments() {
return this.messageDetailArguments;
}
} }

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

@ -16,8 +16,8 @@
package org.springframework.web; package org.springframework.web;
import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.stream.Collectors;
import org.springframework.http.HttpHeaders; import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
@ -35,12 +35,16 @@ import org.springframework.util.CollectionUtils;
@SuppressWarnings("serial") @SuppressWarnings("serial")
public class HttpMediaTypeNotAcceptableException extends HttpMediaTypeException { public class HttpMediaTypeNotAcceptableException extends HttpMediaTypeException {
private static final String PARSE_ERROR_DETAIL_CODE =
ErrorResponse.getDefaultDetailMessageCode(HttpMediaTypeNotAcceptableException.class, "parseError");
/** /**
* Constructor for when the {@code Accept} header cannot be parsed. * Constructor for when the {@code Accept} header cannot be parsed.
* @param message the parse error message * @param message the parse error message
*/ */
public HttpMediaTypeNotAcceptableException(String message) { public HttpMediaTypeNotAcceptableException(String message) {
super(message); super(message, Collections.emptyList(), PARSE_ERROR_DETAIL_CODE, null);
getBody().setDetail("Could not parse Accept header."); getBody().setDetail("Could not parse Accept header.");
} }
@ -49,9 +53,8 @@ public class HttpMediaTypeNotAcceptableException extends HttpMediaTypeException
* @param mediaTypes the list of supported media types * @param mediaTypes the list of supported media types
*/ */
public HttpMediaTypeNotAcceptableException(List<MediaType> mediaTypes) { public HttpMediaTypeNotAcceptableException(List<MediaType> mediaTypes) {
super("No acceptable representation", mediaTypes); super("No acceptable representation", mediaTypes, null, new Object[] {mediaTypes});
getBody().setDetail("Acceptable representations: " + getBody().setDetail("Acceptable representations: " + mediaTypes + ".");
mediaTypes.stream().map(MediaType::toString).collect(Collectors.joining(", ", "'", "'")) + ".");
} }

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

@ -16,6 +16,7 @@
package org.springframework.web; package org.springframework.web;
import java.util.Collections;
import java.util.List; import java.util.List;
import org.springframework.http.HttpHeaders; import org.springframework.http.HttpHeaders;
@ -37,6 +38,10 @@ import org.springframework.util.CollectionUtils;
@SuppressWarnings("serial") @SuppressWarnings("serial")
public class HttpMediaTypeNotSupportedException extends HttpMediaTypeException { public class HttpMediaTypeNotSupportedException extends HttpMediaTypeException {
private static final String PARSE_ERROR_DETAIL_CODE =
ErrorResponse.getDefaultDetailMessageCode(HttpMediaTypeNotSupportedException.class, "parseError");
@Nullable @Nullable
private final MediaType contentType; private final MediaType contentType;
@ -49,7 +54,7 @@ public class HttpMediaTypeNotSupportedException extends HttpMediaTypeException {
* @param message the exception message * @param message the exception message
*/ */
public HttpMediaTypeNotSupportedException(String message) { public HttpMediaTypeNotSupportedException(String message) {
super(message); super(message, Collections.emptyList(), PARSE_ERROR_DETAIL_CODE, null);
this.contentType = null; this.contentType = null;
this.httpMethod = null; this.httpMethod = null;
getBody().setDetail("Could not parse Content-Type."); getBody().setDetail("Could not parse Content-Type.");
@ -89,7 +94,7 @@ public class HttpMediaTypeNotSupportedException extends HttpMediaTypeException {
public HttpMediaTypeNotSupportedException(@Nullable MediaType contentType, public HttpMediaTypeNotSupportedException(@Nullable MediaType contentType,
List<MediaType> supportedMediaTypes, @Nullable HttpMethod httpMethod, String message) { List<MediaType> supportedMediaTypes, @Nullable HttpMethod httpMethod, String message) {
super(message, supportedMediaTypes); super(message, supportedMediaTypes, null, new Object[] {contentType, supportedMediaTypes});
this.contentType = contentType; this.contentType = contentType;
this.httpMethod = httpMethod; this.httpMethod = httpMethod;
getBody().setDetail("Content-Type '" + this.contentType + "' is not supported."); getBody().setDetail("Content-Type '" + this.contentType + "' is not supported.");

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

@ -52,7 +52,9 @@ public class HttpRequestMethodNotSupportedException extends ServletException imp
/** /**
* Create a new HttpRequestMethodNotSupportedException. * Create a new HttpRequestMethodNotSupportedException.
* @param method the unsupported HTTP request method * @param method the unsupported HTTP request method
* @deprecated 6.0 in favor of {@link #HttpRequestMethodNotSupportedException(String, Collection)}
*/ */
@Deprecated(since = "6.0", forRemoval = true)
public HttpRequestMethodNotSupportedException(String method) { public HttpRequestMethodNotSupportedException(String method) {
this(method, (String[]) null); this(method, (String[]) null);
} }
@ -61,7 +63,9 @@ public class HttpRequestMethodNotSupportedException extends ServletException imp
* Create a new HttpRequestMethodNotSupportedException. * Create a new HttpRequestMethodNotSupportedException.
* @param method the unsupported HTTP request method * @param method the unsupported HTTP request method
* @param msg the detail message * @param msg the detail message
* @deprecated in favor of {@link #HttpRequestMethodNotSupportedException(String, Collection)}
*/ */
@Deprecated(since = "6.0", forRemoval = true)
public HttpRequestMethodNotSupportedException(String method, String msg) { public HttpRequestMethodNotSupportedException(String method, String msg) {
this(method, null, msg); this(method, null, msg);
} }
@ -69,7 +73,7 @@ public class HttpRequestMethodNotSupportedException extends ServletException imp
/** /**
* Create a new HttpRequestMethodNotSupportedException. * Create a new HttpRequestMethodNotSupportedException.
* @param method the unsupported HTTP request method * @param method the unsupported HTTP request method
* @param supportedMethods the actually supported HTTP methods (may be {@code null}) * @param supportedMethods the actually supported HTTP methods (possibly {@code null})
*/ */
public HttpRequestMethodNotSupportedException(String method, @Nullable Collection<String> supportedMethods) { public HttpRequestMethodNotSupportedException(String method, @Nullable Collection<String> supportedMethods) {
this(method, (supportedMethods != null ? StringUtils.toStringArray(supportedMethods) : null)); this(method, (supportedMethods != null ? StringUtils.toStringArray(supportedMethods) : null));
@ -78,8 +82,10 @@ public class HttpRequestMethodNotSupportedException extends ServletException imp
/** /**
* Create a new HttpRequestMethodNotSupportedException. * Create a new HttpRequestMethodNotSupportedException.
* @param method the unsupported HTTP request method * @param method the unsupported HTTP request method
* @param supportedMethods the actually supported HTTP methods (may be {@code null}) * @param supportedMethods the actually supported HTTP methods (possibly {@code null})
* @deprecated in favor of {@link #HttpRequestMethodNotSupportedException(String, Collection)}
*/ */
@Deprecated(since = "6.0", forRemoval = true)
public HttpRequestMethodNotSupportedException(String method, @Nullable String[] supportedMethods) { public HttpRequestMethodNotSupportedException(String method, @Nullable String[] supportedMethods) {
this(method, supportedMethods, "Request method '" + method + "' is not supported"); this(method, supportedMethods, "Request method '" + method + "' is not supported");
} }
@ -89,7 +95,9 @@ public class HttpRequestMethodNotSupportedException extends ServletException imp
* @param method the unsupported HTTP request method * @param method the unsupported HTTP request method
* @param supportedMethods the actually supported HTTP methods * @param supportedMethods the actually supported HTTP methods
* @param msg the detail message * @param msg the detail message
* @deprecated in favor of {@link #HttpRequestMethodNotSupportedException(String, Collection)}
*/ */
@Deprecated(since = "6.0", forRemoval = true)
public HttpRequestMethodNotSupportedException(String method, @Nullable String[] supportedMethods, String msg) { public HttpRequestMethodNotSupportedException(String method, @Nullable String[] supportedMethods, String msg) {
super(msg); super(msg);
this.method = method; this.method = method;
@ -153,4 +161,9 @@ public class HttpRequestMethodNotSupportedException extends ServletException imp
return this.body; return this.body;
} }
@Override
public Object[] getDetailMessageArguments() {
return new Object[] {getMethod(), getSupportedHttpMethods()};
}
} }

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

@ -16,12 +16,20 @@
package org.springframework.web.bind; package org.springframework.web.bind;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.function.Function;
import org.springframework.context.MessageSource;
import org.springframework.core.MethodParameter; import org.springframework.core.MethodParameter;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.http.HttpStatusCode; import org.springframework.http.HttpStatusCode;
import org.springframework.http.ProblemDetail; import org.springframework.http.ProblemDetail;
import org.springframework.util.StringUtils;
import org.springframework.validation.BindException; import org.springframework.validation.BindException;
import org.springframework.validation.BindingResult; import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.validation.ObjectError; import org.springframework.validation.ObjectError;
import org.springframework.web.ErrorResponse; import org.springframework.web.ErrorResponse;
@ -52,6 +60,7 @@ public class MethodArgumentNotValidException extends BindException implements Er
this.body = ProblemDetail.forStatusAndDetail(getStatusCode(), "Invalid request content."); this.body = ProblemDetail.forStatusAndDetail(getStatusCode(), "Invalid request content.");
} }
@Override @Override
public HttpStatusCode getStatusCode() { public HttpStatusCode getStatusCode() {
return HttpStatus.BAD_REQUEST; return HttpStatus.BAD_REQUEST;
@ -85,4 +94,58 @@ public class MethodArgumentNotValidException extends BindException implements Er
return sb.toString(); return sb.toString();
} }
@Override
public Object[] getDetailMessageArguments() {
return new Object[] {
errorsToStringList(getBindingResult().getGlobalErrors()),
errorsToStringList(getBindingResult().getFieldErrors())
};
}
@Override
public Object[] getDetailMessageArguments(MessageSource messageSource, Locale locale) {
return new Object[] {
errorsToStringList(getBindingResult().getGlobalErrors(), messageSource, locale),
errorsToStringList(getBindingResult().getFieldErrors(), messageSource, locale)
};
}
/**
* Convert each given {@link ObjectError} to a single quote String, taking
* either an error's default message as a first choice, or its error code.
* @since 6.0
*/
public static List<String> errorsToStringList(List<? extends ObjectError> errors) {
return errorsToStringList(errors, error ->
error.getDefaultMessage() != null ? error.getDefaultMessage() : error.getCode());
}
/**
* Variant of {@link #errorsToStringList(List)} that uses the provided
* {@link MessageSource} to resolve the error code, or otherwise fall
* back on its default message.
* @since 6.0
*/
@SuppressWarnings("ConstantConditions")
public static List<String> errorsToStringList(
List<? extends ObjectError> errors, MessageSource source, Locale locale) {
return errorsToStringList(errors, error -> source.getMessage(
error.getCode(), error.getArguments(), error.getDefaultMessage(), locale));
}
private static List<String> errorsToStringList(
List<? extends ObjectError> errors, Function<ObjectError, String> formatter) {
List<String> result = new ArrayList<>(errors.size());
for (ObjectError error : errors) {
String value = formatter.apply(error);
if (StringUtils.hasText(value)) {
result.add(error instanceof FieldError fieldError ?
fieldError.getField() + ": '" + value + "'" : "'" + value + "'");
}
}
return result;
}
} }

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

@ -54,7 +54,7 @@ public class MissingMatrixVariableException extends MissingRequestValueException
public MissingMatrixVariableException( public MissingMatrixVariableException(
String variableName, MethodParameter parameter, boolean missingAfterConversion) { String variableName, MethodParameter parameter, boolean missingAfterConversion) {
super("", missingAfterConversion); super("", missingAfterConversion, null, new Object[] {variableName});
this.variableName = variableName; this.variableName = variableName;
this.parameter = parameter; this.parameter = parameter;
getBody().setDetail("Required path parameter '" + this.variableName + "' is not present."); getBody().setDetail("Required path parameter '" + this.variableName + "' is not present.");

2
spring-web/src/main/java/org/springframework/web/bind/MissingPathVariableException.java

@ -58,7 +58,7 @@ public class MissingPathVariableException extends MissingRequestValueException {
public MissingPathVariableException( public MissingPathVariableException(
String variableName, MethodParameter parameter, boolean missingAfterConversion) { String variableName, MethodParameter parameter, boolean missingAfterConversion) {
super("", missingAfterConversion); super("", missingAfterConversion, null, new Object[] {variableName});
this.variableName = variableName; this.variableName = variableName;
this.parameter = parameter; this.parameter = parameter;
getBody().setDetail("Required path variable '" + this.variableName + "' is not present."); getBody().setDetail("Required path variable '" + this.variableName + "' is not present.");

2
spring-web/src/main/java/org/springframework/web/bind/MissingRequestCookieException.java

@ -54,7 +54,7 @@ public class MissingRequestCookieException extends MissingRequestValueException
public MissingRequestCookieException( public MissingRequestCookieException(
String cookieName, MethodParameter parameter, boolean missingAfterConversion) { String cookieName, MethodParameter parameter, boolean missingAfterConversion) {
super("", missingAfterConversion); super("", missingAfterConversion, null, new Object[] {cookieName});
this.cookieName = cookieName; this.cookieName = cookieName;
this.parameter = parameter; this.parameter = parameter;
getBody().setDetail("Required cookie '" + this.cookieName + "' is not present."); getBody().setDetail("Required cookie '" + this.cookieName + "' is not present.");

2
spring-web/src/main/java/org/springframework/web/bind/MissingRequestHeaderException.java

@ -54,7 +54,7 @@ public class MissingRequestHeaderException extends MissingRequestValueException
public MissingRequestHeaderException( public MissingRequestHeaderException(
String headerName, MethodParameter parameter, boolean missingAfterConversion) { String headerName, MethodParameter parameter, boolean missingAfterConversion) {
super("", missingAfterConversion); super("", missingAfterConversion, null, new Object[] {headerName});
this.headerName = headerName; this.headerName = headerName;
this.parameter = parameter; this.parameter = parameter;
getBody().setDetail("Required header '" + this.headerName + "' is not present."); getBody().setDetail("Required header '" + this.headerName + "' is not present.");

25
spring-web/src/main/java/org/springframework/web/bind/MissingRequestValueException.java

@ -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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -16,6 +16,9 @@
package org.springframework.web.bind; package org.springframework.web.bind;
import org.springframework.http.ProblemDetail;
import org.springframework.lang.Nullable;
/** /**
* Base class for {@link ServletRequestBindingException} exceptions that could * Base class for {@link ServletRequestBindingException} exceptions that could
* not bind because the request value is required but is either missing or * not bind because the request value is required but is either missing or
@ -30,15 +33,35 @@ public class MissingRequestValueException extends ServletRequestBindingException
private final boolean missingAfterConversion; private final boolean missingAfterConversion;
/**
* Constructor with a message only.
*/
public MissingRequestValueException(String msg) { public MissingRequestValueException(String msg) {
this(msg, false); this(msg, false);
} }
/**
* Constructor with a message and a flag that indicates whether the value
* was not completely missing but became was {@code null} after conversion.
*/
public MissingRequestValueException(String msg, boolean missingAfterConversion) { public MissingRequestValueException(String msg, boolean missingAfterConversion) {
super(msg); super(msg);
this.missingAfterConversion = missingAfterConversion; this.missingAfterConversion = missingAfterConversion;
} }
/**
* Constructor with a given {@link ProblemDetail}, and a
* {@link org.springframework.context.MessageSource} code and arguments to
* resolve the detail message with.
* @since 6.0
*/
protected MissingRequestValueException(String msg, boolean missingAfterConversion,
@Nullable String messageDetailCode, @Nullable Object[] messageDetailArguments) {
super(msg, messageDetailCode, messageDetailArguments);
this.missingAfterConversion = missingAfterConversion;
}
/** /**
* Whether the request value was present but converted to {@code null}, e.g. via * Whether the request value was present but converted to {@code null}, e.g. via

2
spring-web/src/main/java/org/springframework/web/bind/MissingServletRequestParameterException.java

@ -49,7 +49,7 @@ public class MissingServletRequestParameterException extends MissingRequestValue
public MissingServletRequestParameterException( public MissingServletRequestParameterException(
String parameterName, String parameterType, boolean missingAfterConversion) { String parameterName, String parameterType, boolean missingAfterConversion) {
super("", missingAfterConversion); super("", missingAfterConversion, null, new Object[] {parameterName});
this.parameterName = parameterName; this.parameterName = parameterName;
this.parameterType = parameterType; this.parameterType = parameterType;
getBody().setDetail("Required parameter '" + this.parameterName + "' is not present."); getBody().setDetail("Required parameter '" + this.parameterName + "' is not present.");

61
spring-web/src/main/java/org/springframework/web/bind/ServletRequestBindingException.java

@ -21,6 +21,7 @@ import jakarta.servlet.ServletException;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.http.HttpStatusCode; import org.springframework.http.HttpStatusCode;
import org.springframework.http.ProblemDetail; import org.springframework.http.ProblemDetail;
import org.springframework.lang.Nullable;
import org.springframework.web.ErrorResponse; import org.springframework.web.ErrorResponse;
/** /**
@ -39,24 +40,68 @@ public class ServletRequestBindingException extends ServletException implements
private final ProblemDetail body = ProblemDetail.forStatus(getStatusCode()); private final ProblemDetail body = ProblemDetail.forStatus(getStatusCode());
private final String messageDetailCode;
@Nullable
private final Object[] messageDetailArguments;
/** /**
* Constructor for ServletRequestBindingException. * Constructor with a message only.
* @param msg the detail message * @param msg the detail message
*/ */
public ServletRequestBindingException(String msg) { public ServletRequestBindingException(String msg) {
super(msg); this(msg, null, null);
} }
/** /**
* Constructor for ServletRequestBindingException. * Constructor with a message and a cause.
* @param msg the detail message * @param msg the detail message
* @param cause the root cause * @param cause the root cause
*/ */
public ServletRequestBindingException(String msg, Throwable cause) { public ServletRequestBindingException(String msg, Throwable cause) {
this(msg, cause, null, null);
}
/**
* Constructor for ServletRequestBindingException.
* @param msg the detail message
* @param messageDetailCode the code to use to resolve the problem "detail"
* through a {@link org.springframework.context.MessageSource}
* @param messageDetailArguments the arguments to make available when
* resolving the problem "detail" through a {@code MessageSource}
* @since 6.0
*/
protected ServletRequestBindingException(
String msg, @Nullable String messageDetailCode, @Nullable Object[] messageDetailArguments) {
this(msg, null, messageDetailCode, messageDetailArguments);
}
/**
* Constructor for ServletRequestBindingException.
* @param msg the detail message
* @param cause the root cause
* @param messageDetailCode the code to use to resolve the problem "detail"
* through a {@link org.springframework.context.MessageSource}
* @param messageDetailArguments the arguments to make available when
* resolving the problem "detail" through a {@code MessageSource}
* @since 6.0
*/
protected ServletRequestBindingException(String msg, @Nullable Throwable cause,
@Nullable String messageDetailCode, @Nullable Object[] messageDetailArguments) {
super(msg, cause); super(msg, cause);
this.messageDetailCode = initMessageDetailCode(messageDetailCode);
this.messageDetailArguments = messageDetailArguments;
}
private String initMessageDetailCode(@Nullable String messageDetailCode) {
return (messageDetailCode != null ?
messageDetailCode : ErrorResponse.getDefaultDetailMessageCode(getClass(), null));
} }
@Override @Override
public HttpStatusCode getStatusCode() { public HttpStatusCode getStatusCode() {
return HttpStatus.BAD_REQUEST; return HttpStatus.BAD_REQUEST;
@ -67,4 +112,14 @@ public class ServletRequestBindingException extends ServletException implements
return this.body; return this.body;
} }
@Override
public String getDetailMessageCode() {
return this.messageDetailCode;
}
@Override
public Object[] getDetailMessageArguments() {
return this.messageDetailArguments;
}
} }

25
spring-web/src/main/java/org/springframework/web/bind/UnsatisfiedServletRequestParameterException.java

@ -19,6 +19,7 @@ package org.springframework.web.bind;
import java.util.Iterator; import java.util.Iterator;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.stream.Collectors;
import org.springframework.util.Assert; import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils; import org.springframework.util.ObjectUtils;
@ -56,30 +57,28 @@ public class UnsatisfiedServletRequestParameterException extends ServletRequestB
* @param actualParams the actual parameter Map associated with the ServletRequest * @param actualParams the actual parameter Map associated with the ServletRequest
* @since 4.2 * @since 4.2
*/ */
public UnsatisfiedServletRequestParameterException(List<String[]> paramConditions, public UnsatisfiedServletRequestParameterException(
Map<String, String[]> actualParams) { List<String[]> paramConditions, Map<String, String[]> actualParams) {
super(""); super("", null, new Object[] {paramsToStringList(paramConditions)});
Assert.notEmpty(paramConditions, "Parameter conditions must not be empty");
this.paramConditions = paramConditions; this.paramConditions = paramConditions;
this.actualParams = actualParams; this.actualParams = actualParams;
getBody().setDetail("Invalid request parameters."); getBody().setDetail("Invalid request parameters.");
} }
private static List<String> paramsToStringList(List<String[]> paramConditions) {
Assert.notEmpty(paramConditions, "Parameter conditions must not be empty");
return paramConditions.stream()
.map(c -> "\"" + StringUtils.arrayToDelimitedString(c, ", ") + "\"")
.collect(Collectors.toList());
}
@Override @Override
public String getMessage() { public String getMessage() {
StringBuilder sb = new StringBuilder("Parameter conditions "); StringBuilder sb = new StringBuilder("Parameter conditions ");
int i = 0; int i = 0;
for (String[] conditions : this.paramConditions) { sb.append(String.join(" OR ", paramsToStringList(this.paramConditions)));
if (i > 0) {
sb.append(" OR ");
}
sb.append('"');
sb.append(StringUtils.arrayToDelimitedString(conditions, ", "));
sb.append('"');
i++;
}
sb.append(" not met for actual request parameters: "); sb.append(" not met for actual request parameters: ");
sb.append(requestParameterMapToString(this.actualParams)); sb.append(requestParameterMapToString(this.actualParams));
return sb.toString(); return sb.toString();

20
spring-web/src/main/java/org/springframework/web/bind/support/WebExchangeBindException.java

@ -18,9 +18,11 @@ package org.springframework.web.bind.support;
import java.beans.PropertyEditor; import java.beans.PropertyEditor;
import java.util.List; import java.util.List;
import java.util.Locale;
import java.util.Map; import java.util.Map;
import org.springframework.beans.PropertyEditorRegistry; import org.springframework.beans.PropertyEditorRegistry;
import org.springframework.context.MessageSource;
import org.springframework.core.MethodParameter; import org.springframework.core.MethodParameter;
import org.springframework.lang.Nullable; import org.springframework.lang.Nullable;
import org.springframework.util.Assert; import org.springframework.util.Assert;
@ -29,6 +31,7 @@ import org.springframework.validation.BindingResult;
import org.springframework.validation.Errors; import org.springframework.validation.Errors;
import org.springframework.validation.FieldError; import org.springframework.validation.FieldError;
import org.springframework.validation.ObjectError; import org.springframework.validation.ObjectError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.server.ServerWebInputException; import org.springframework.web.server.ServerWebInputException;
/** /**
@ -45,11 +48,18 @@ public class WebExchangeBindException extends ServerWebInputException implements
public WebExchangeBindException(MethodParameter parameter, BindingResult bindingResult) { public WebExchangeBindException(MethodParameter parameter, BindingResult bindingResult) {
super("Validation failure", parameter); super("Validation failure", parameter, null, null, initMessageDetailArguments(bindingResult));
this.bindingResult = bindingResult; this.bindingResult = bindingResult;
getBody().setDetail("Invalid request content."); getBody().setDetail("Invalid request content.");
} }
private static Object[] initMessageDetailArguments(BindingResult bindingResult) {
return new Object[] {
MethodArgumentNotValidException.errorsToStringList(bindingResult.getGlobalErrors()),
MethodArgumentNotValidException.errorsToStringList(bindingResult.getFieldErrors())
};
}
/** /**
* Return the BindingResult that this BindException wraps. * Return the BindingResult that this BindException wraps.
@ -289,6 +299,14 @@ public class WebExchangeBindException extends ServerWebInputException implements
return sb.toString(); return sb.toString();
} }
@Override
public Object[] getDetailMessageArguments(MessageSource source, Locale locale) {
return new Object[] {
MethodArgumentNotValidException.errorsToStringList(this.bindingResult.getGlobalErrors(), source, locale),
MethodArgumentNotValidException.errorsToStringList(this.bindingResult.getFieldErrors(), source, locale)
};
}
@Override @Override
public boolean equals(@Nullable Object other) { public boolean equals(@Nullable Object other) {
return (this == other || this.bindingResult.equals(other)); return (this == other || this.bindingResult.equals(other));

5
spring-web/src/main/java/org/springframework/web/multipart/support/MissingServletRequestPartException.java

@ -84,4 +84,9 @@ public class MissingServletRequestPartException extends ServletException impleme
return this.body; return this.body;
} }
@Override
public Object[] getDetailMessageArguments() {
return new Object[] {getRequestPartName()};
}
} }

11
spring-web/src/main/java/org/springframework/web/server/MethodNotAllowedException.java

@ -20,7 +20,6 @@ import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.LinkedHashSet; import java.util.LinkedHashSet;
import java.util.Set; import java.util.Set;
import java.util.stream.Collectors;
import org.springframework.http.HttpHeaders; import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod; import org.springframework.http.HttpMethod;
@ -48,17 +47,17 @@ public class MethodNotAllowedException extends ResponseStatusException {
} }
public MethodNotAllowedException(String method, @Nullable Collection<HttpMethod> supportedMethods) { public MethodNotAllowedException(String method, @Nullable Collection<HttpMethod> supportedMethods) {
super(HttpStatus.METHOD_NOT_ALLOWED, "Request method '" + method + "' is not supported."); super(HttpStatus.METHOD_NOT_ALLOWED, "Request method '" + method + "' is not supported.",
null, null, new Object[] {method, supportedMethods});
Assert.notNull(method, "'method' is required"); Assert.notNull(method, "'method' is required");
if (supportedMethods == null) { if (supportedMethods == null) {
supportedMethods = Collections.emptySet(); supportedMethods = Collections.emptySet();
} }
this.method = method; this.method = method;
this.httpMethods = Collections.unmodifiableSet(new LinkedHashSet<>(supportedMethods)); this.httpMethods = Collections.unmodifiableSet(new LinkedHashSet<>(supportedMethods));
getBody().setDetail(this.httpMethods.isEmpty() ?
getBody().setDetail(this.httpMethods.isEmpty() ? getReason() : getReason() : "Supported methods: " + this.httpMethods);
"Supported methods: " + this.httpMethods.stream()
.map(HttpMethod::toString).collect(Collectors.joining("', '", "'", "'")));
} }

4
spring-web/src/main/java/org/springframework/web/server/MissingRequestValueException.java

@ -36,7 +36,9 @@ public class MissingRequestValueException extends ServerWebInputException {
public MissingRequestValueException(String name, Class<?> type, String label, MethodParameter parameter) { public MissingRequestValueException(String name, Class<?> type, String label, MethodParameter parameter) {
super("Required " + label + " '" + name + "' is not present.", parameter); super("Required " + label + " '" + name + "' is not present.", parameter,
null, null, new Object[] {label, name});
this.name = name; this.name = name;
this.type = type; this.type = type;
this.label = label; this.label = label;

15
spring-web/src/main/java/org/springframework/web/server/NotAcceptableStatusException.java

@ -18,12 +18,12 @@ package org.springframework.web.server;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.stream.Collectors;
import org.springframework.http.HttpHeaders; import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.util.CollectionUtils; import org.springframework.util.CollectionUtils;
import org.springframework.web.ErrorResponse;
/** /**
* Exception for errors that fit response status 406 (not acceptable). * Exception for errors that fit response status 406 (not acceptable).
@ -34,6 +34,10 @@ import org.springframework.util.CollectionUtils;
@SuppressWarnings("serial") @SuppressWarnings("serial")
public class NotAcceptableStatusException extends ResponseStatusException { public class NotAcceptableStatusException extends ResponseStatusException {
private static final String PARSE_ERROR_DETAIL_CODE =
ErrorResponse.getDefaultDetailMessageCode(NotAcceptableStatusException.class, "parseError");
private final List<MediaType> supportedMediaTypes; private final List<MediaType> supportedMediaTypes;
@ -41,7 +45,7 @@ public class NotAcceptableStatusException extends ResponseStatusException {
* Constructor for when the requested Content-Type is invalid. * Constructor for when the requested Content-Type is invalid.
*/ */
public NotAcceptableStatusException(String reason) { public NotAcceptableStatusException(String reason) {
super(HttpStatus.NOT_ACCEPTABLE, reason); super(HttpStatus.NOT_ACCEPTABLE, reason, null, PARSE_ERROR_DETAIL_CODE, null);
this.supportedMediaTypes = Collections.emptyList(); this.supportedMediaTypes = Collections.emptyList();
getBody().setDetail("Could not parse Accept header."); getBody().setDetail("Could not parse Accept header.");
} }
@ -50,10 +54,11 @@ public class NotAcceptableStatusException extends ResponseStatusException {
* Constructor for when the requested Content-Type is not supported. * Constructor for when the requested Content-Type is not supported.
*/ */
public NotAcceptableStatusException(List<MediaType> mediaTypes) { public NotAcceptableStatusException(List<MediaType> mediaTypes) {
super(HttpStatus.NOT_ACCEPTABLE, "Could not find acceptable representation"); super(HttpStatus.NOT_ACCEPTABLE,
"Could not find acceptable representation", null, null, new Object[] {mediaTypes});
this.supportedMediaTypes = Collections.unmodifiableList(mediaTypes); this.supportedMediaTypes = Collections.unmodifiableList(mediaTypes);
getBody().setDetail("Acceptable representations: " + getBody().setDetail("Acceptable representations: " + mediaTypes + ".");
mediaTypes.stream().map(MediaType::toString).collect(Collectors.joining(", ", "'", "'")) + ".");
} }

17
spring-web/src/main/java/org/springframework/web/server/ResponseStatusException.java

@ -18,6 +18,7 @@ package org.springframework.web.server;
import org.springframework.http.HttpHeaders; import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatusCode; import org.springframework.http.HttpStatusCode;
import org.springframework.http.ProblemDetail;
import org.springframework.lang.Nullable; import org.springframework.lang.Nullable;
import org.springframework.web.ErrorResponseException; import org.springframework.web.ErrorResponseException;
@ -78,6 +79,22 @@ public class ResponseStatusException extends ErrorResponseException {
this.reason = reason; this.reason = reason;
} }
/**
* Constructor with a {@link org.springframework.context.MessageSource}
* code and arguments to resolve the detail message with.
* @param status the HTTP status (required)
* @param reason the associated reason (optional)
* @param cause a nested exception (optional)
* @since 6.0
*/
protected ResponseStatusException(
HttpStatusCode status, @Nullable String reason, @Nullable Throwable cause,
@Nullable String messageDetailCode, @Nullable Object[] messageDetailArguments) {
super(status, ProblemDetail.forStatus(status), cause, messageDetailCode, messageDetailArguments);
this.reason = reason;
}
/** /**
* The reason explaining the exception (potentially {@code null} or empty). * The reason explaining the exception (potentially {@code null} or empty).

6
spring-web/src/main/java/org/springframework/web/server/ServerErrorException.java

@ -45,7 +45,7 @@ public class ServerErrorException extends ResponseStatusException {
* @since 5.0.5 * @since 5.0.5
*/ */
public ServerErrorException(String reason, @Nullable Throwable cause) { public ServerErrorException(String reason, @Nullable Throwable cause) {
super(HttpStatus.INTERNAL_SERVER_ERROR, reason, cause); super(HttpStatus.INTERNAL_SERVER_ERROR, reason, cause, null, new Object[] {reason});
this.handlerMethod = null; this.handlerMethod = null;
this.parameter = null; this.parameter = null;
} }
@ -55,7 +55,7 @@ public class ServerErrorException extends ResponseStatusException {
* @since 5.0.5 * @since 5.0.5
*/ */
public ServerErrorException(String reason, Method handlerMethod, @Nullable Throwable cause) { public ServerErrorException(String reason, Method handlerMethod, @Nullable Throwable cause) {
super(HttpStatus.INTERNAL_SERVER_ERROR, reason, cause); super(HttpStatus.INTERNAL_SERVER_ERROR, reason, cause, null, new Object[] {reason});
this.handlerMethod = handlerMethod; this.handlerMethod = handlerMethod;
this.parameter = null; this.parameter = null;
} }
@ -64,7 +64,7 @@ public class ServerErrorException extends ResponseStatusException {
* Constructor for a 500 error with a {@link MethodParameter} and an optional cause. * Constructor for a 500 error with a {@link MethodParameter} and an optional cause.
*/ */
public ServerErrorException(String reason, MethodParameter parameter, @Nullable Throwable cause) { public ServerErrorException(String reason, MethodParameter parameter, @Nullable Throwable cause) {
super(HttpStatus.INTERNAL_SERVER_ERROR, reason, cause); super(HttpStatus.INTERNAL_SERVER_ERROR, reason, cause, null, new Object[] {reason});
this.handlerMethod = parameter.getMethod(); this.handlerMethod = parameter.getMethod();
this.parameter = parameter; this.parameter = parameter;
} }

15
spring-web/src/main/java/org/springframework/web/server/ServerWebInputException.java

@ -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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -53,7 +53,18 @@ public class ServerWebInputException extends ResponseStatusException {
* Constructor for a 400 error with a root cause. * Constructor for a 400 error with a root cause.
*/ */
public ServerWebInputException(String reason, @Nullable MethodParameter parameter, @Nullable Throwable cause) { public ServerWebInputException(String reason, @Nullable MethodParameter parameter, @Nullable Throwable cause) {
super(HttpStatus.BAD_REQUEST, reason, cause); this(reason, parameter, cause, null, null);
}
/**
* Constructor with a {@link org.springframework.context.MessageSource} code
* and arguments to resolve the detail message with.
* @since 6.0
*/
protected ServerWebInputException(String reason, @Nullable MethodParameter parameter, @Nullable Throwable cause,
@Nullable String messageDetailCode, @Nullable Object[] messageDetailArguments) {
super(HttpStatus.BAD_REQUEST, reason, cause, messageDetailCode, messageDetailArguments);
this.parameter = parameter; this.parameter = parameter;
} }

8
spring-web/src/main/java/org/springframework/web/server/UnsatisfiedRequestParameterException.java

@ -36,12 +36,10 @@ public class UnsatisfiedRequestParameterException extends ServerWebInputExceptio
private final MultiValueMap<String, String> requestParams; private final MultiValueMap<String, String> requestParams;
public UnsatisfiedRequestParameterException( public UnsatisfiedRequestParameterException(List<String> conditions, MultiValueMap<String, String> params) {
List<String> conditions, MultiValueMap<String, String> requestParams) { super(initReason(conditions, params), null, null, null, new Object[] {conditions});
super(initReason(conditions, requestParams));
this.conditions = conditions; this.conditions = conditions;
this.requestParams = requestParams; this.requestParams = params;
getBody().setDetail("Invalid request parameters."); getBody().setDetail("Invalid request parameters.");
} }

17
spring-web/src/main/java/org/springframework/web/server/UnsupportedMediaTypeStatusException.java

@ -26,6 +26,7 @@ import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.lang.Nullable; import org.springframework.lang.Nullable;
import org.springframework.util.CollectionUtils; import org.springframework.util.CollectionUtils;
import org.springframework.web.ErrorResponse;
/** /**
* Exception for errors that fit response status 415 (unsupported media type). * Exception for errors that fit response status 415 (unsupported media type).
@ -36,6 +37,10 @@ import org.springframework.util.CollectionUtils;
@SuppressWarnings("serial") @SuppressWarnings("serial")
public class UnsupportedMediaTypeStatusException extends ResponseStatusException { public class UnsupportedMediaTypeStatusException extends ResponseStatusException {
private static final String PARSE_ERROR_DETAIL_CODE =
ErrorResponse.getDefaultDetailMessageCode(UnsupportedMediaTypeStatusException.class, "parseError");
@Nullable @Nullable
private final MediaType contentType; private final MediaType contentType;
@ -52,7 +57,7 @@ public class UnsupportedMediaTypeStatusException extends ResponseStatusException
* Constructor for when the specified Content-Type is invalid. * Constructor for when the specified Content-Type is invalid.
*/ */
public UnsupportedMediaTypeStatusException(@Nullable String reason) { public UnsupportedMediaTypeStatusException(@Nullable String reason) {
super(HttpStatus.UNSUPPORTED_MEDIA_TYPE, reason); super(HttpStatus.UNSUPPORTED_MEDIA_TYPE, reason, null, PARSE_ERROR_DETAIL_CODE, null);
this.contentType = null; this.contentType = null;
this.supportedMediaTypes = Collections.emptyList(); this.supportedMediaTypes = Collections.emptyList();
this.bodyType = null; this.bodyType = null;
@ -92,9 +97,8 @@ public class UnsupportedMediaTypeStatusException extends ResponseStatusException
public UnsupportedMediaTypeStatusException(@Nullable MediaType contentType, List<MediaType> supportedTypes, public UnsupportedMediaTypeStatusException(@Nullable MediaType contentType, List<MediaType> supportedTypes,
@Nullable ResolvableType bodyType, @Nullable HttpMethod method) { @Nullable ResolvableType bodyType, @Nullable HttpMethod method) {
super(HttpStatus.UNSUPPORTED_MEDIA_TYPE, super(HttpStatus.UNSUPPORTED_MEDIA_TYPE, initMessage(contentType, bodyType),
"Content type '" + (contentType != null ? contentType : "") + "' not supported" + null, null, new Object[] {contentType, supportedTypes});
(bodyType != null ? " for bodyType=" + bodyType : ""));
this.contentType = contentType; this.contentType = contentType;
this.supportedMediaTypes = Collections.unmodifiableList(supportedTypes); this.supportedMediaTypes = Collections.unmodifiableList(supportedTypes);
@ -104,6 +108,11 @@ public class UnsupportedMediaTypeStatusException extends ResponseStatusException
setDetail(contentType != null ? "Content-Type '" + contentType + "' is not supported." : null); setDetail(contentType != null ? "Content-Type '" + contentType + "' is not supported." : null);
} }
private static String initMessage(@Nullable MediaType contentType, @Nullable ResolvableType bodyType) {
return "Content type '" + (contentType != null ? contentType : "") + "' not supported" +
(bodyType != null ? " for bodyType=" + bodyType : "");
}
/** /**
* Return the request Content-Type header if it was parsed successfully, * Return the request Content-Type header if it was parsed successfully,

189
spring-web/src/test/java/org/springframework/web/ErrorResponseExceptionTests.java

@ -19,9 +19,12 @@ package org.springframework.web;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Locale;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.springframework.beans.testfixture.beans.TestBean;
import org.springframework.context.support.StaticMessageSource;
import org.springframework.core.MethodParameter; import org.springframework.core.MethodParameter;
import org.springframework.http.HttpHeaders; import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod; import org.springframework.http.HttpMethod;
@ -32,7 +35,6 @@ import org.springframework.lang.Nullable;
import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.LinkedMultiValueMap;
import org.springframework.validation.BindException; import org.springframework.validation.BindException;
import org.springframework.validation.BindingResult; import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.MissingMatrixVariableException; import org.springframework.web.bind.MissingMatrixVariableException;
import org.springframework.web.bind.MissingPathVariableException; import org.springframework.web.bind.MissingPathVariableException;
@ -46,13 +48,13 @@ import org.springframework.web.multipart.support.MissingServletRequestPartExcept
import org.springframework.web.server.MethodNotAllowedException; import org.springframework.web.server.MethodNotAllowedException;
import org.springframework.web.server.MissingRequestValueException; import org.springframework.web.server.MissingRequestValueException;
import org.springframework.web.server.NotAcceptableStatusException; import org.springframework.web.server.NotAcceptableStatusException;
import org.springframework.web.server.ServerErrorException;
import org.springframework.web.server.UnsatisfiedRequestParameterException; import org.springframework.web.server.UnsatisfiedRequestParameterException;
import org.springframework.web.server.UnsupportedMediaTypeStatusException; import org.springframework.web.server.UnsupportedMediaTypeStatusException;
import org.springframework.web.testfixture.method.ResolvableMethod; import org.springframework.web.testfixture.method.ResolvableMethod;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
/** /**
* Unit tests that verify the HTTP response details exposed by exceptions in the * Unit tests that verify the HTTP response details exposed by exceptions in the
* {@link ErrorResponse} hierarchy. * {@link ErrorResponse} hierarchy.
@ -71,12 +73,12 @@ public class ErrorResponseExceptionTests {
List<MediaType> mediaTypes = List<MediaType> mediaTypes =
Arrays.asList(MediaType.APPLICATION_JSON, MediaType.APPLICATION_CBOR); Arrays.asList(MediaType.APPLICATION_JSON, MediaType.APPLICATION_CBOR);
ErrorResponse ex = new HttpMediaTypeNotSupportedException( HttpMediaTypeNotSupportedException ex = new HttpMediaTypeNotSupportedException(
MediaType.APPLICATION_XML, mediaTypes, HttpMethod.PATCH, "Custom message"); MediaType.APPLICATION_XML, mediaTypes, HttpMethod.PATCH, "Custom message");
assertStatus(ex, HttpStatus.UNSUPPORTED_MEDIA_TYPE); assertStatus(ex, HttpStatus.UNSUPPORTED_MEDIA_TYPE);
assertDetail(ex, "Content-Type 'application/xml' is not supported."); assertDetail(ex, "Content-Type 'application/xml' is not supported.");
assertDetailMessageCode(ex, null, new Object[] {ex.getContentType(), ex.getSupportedMediaTypes()});
HttpHeaders headers = ex.getHeaders(); HttpHeaders headers = ex.getHeaders();
assertThat(headers.getAccept()).isEqualTo(mediaTypes); assertThat(headers.getAccept()).isEqualTo(mediaTypes);
@ -89,9 +91,10 @@ public class ErrorResponseExceptionTests {
ErrorResponse ex = new HttpMediaTypeNotSupportedException( ErrorResponse ex = new HttpMediaTypeNotSupportedException(
"Could not parse Accept header: Invalid mime type \"foo\": does not contain '/'"); "Could not parse Accept header: Invalid mime type \"foo\": does not contain '/'");
assertStatus(ex, HttpStatus.UNSUPPORTED_MEDIA_TYPE); assertStatus(ex, HttpStatus.UNSUPPORTED_MEDIA_TYPE);
assertDetail(ex, "Could not parse Content-Type."); assertDetail(ex, "Could not parse Content-Type.");
assertDetailMessageCode(ex, "parseError", null);
assertThat(ex.getHeaders()).isEmpty(); assertThat(ex.getHeaders()).isEmpty();
} }
@ -99,11 +102,11 @@ public class ErrorResponseExceptionTests {
void httpMediaTypeNotAcceptableException() { void httpMediaTypeNotAcceptableException() {
List<MediaType> mediaTypes = Arrays.asList(MediaType.APPLICATION_JSON, MediaType.APPLICATION_CBOR); List<MediaType> mediaTypes = Arrays.asList(MediaType.APPLICATION_JSON, MediaType.APPLICATION_CBOR);
ErrorResponse ex = new HttpMediaTypeNotAcceptableException(mediaTypes); HttpMediaTypeNotAcceptableException ex = new HttpMediaTypeNotAcceptableException(mediaTypes);
assertStatus(ex, HttpStatus.NOT_ACCEPTABLE); assertStatus(ex, HttpStatus.NOT_ACCEPTABLE);
assertDetail(ex, "Acceptable representations: 'application/json, application/cbor'."); assertDetail(ex, "Acceptable representations: [application/json, application/cbor].");
assertDetailMessageCode(ex, null, new Object[] {ex.getSupportedMediaTypes()});
assertThat(ex.getHeaders()).hasSize(1); assertThat(ex.getHeaders()).hasSize(1);
assertThat(ex.getHeaders().getAccept()).isEqualTo(mediaTypes); assertThat(ex.getHeaders().getAccept()).isEqualTo(mediaTypes);
@ -115,9 +118,10 @@ public class ErrorResponseExceptionTests {
ErrorResponse ex = new HttpMediaTypeNotAcceptableException( ErrorResponse ex = new HttpMediaTypeNotAcceptableException(
"Could not parse Accept header: Invalid mime type \"foo\": does not contain '/'"); "Could not parse Accept header: Invalid mime type \"foo\": does not contain '/'");
assertStatus(ex, HttpStatus.NOT_ACCEPTABLE); assertStatus(ex, HttpStatus.NOT_ACCEPTABLE);
assertDetail(ex, "Could not parse Accept header."); assertDetail(ex, "Could not parse Accept header.");
assertDetailMessageCode(ex, "parseError", null);
assertThat(ex.getHeaders()).isEmpty(); assertThat(ex.getHeaders()).isEmpty();
} }
@ -125,22 +129,23 @@ public class ErrorResponseExceptionTests {
void asyncRequestTimeoutException() { void asyncRequestTimeoutException() {
ErrorResponse ex = new AsyncRequestTimeoutException(); ErrorResponse ex = new AsyncRequestTimeoutException();
assertDetailMessageCode(ex, null, null);
assertStatus(ex, HttpStatus.SERVICE_UNAVAILABLE); assertStatus(ex, HttpStatus.SERVICE_UNAVAILABLE);
assertDetail(ex, null); assertDetail(ex, null);
assertThat(ex.getHeaders()).isEmpty(); assertThat(ex.getHeaders()).isEmpty();
} }
@Test @Test
void httpRequestMethodNotSupportedException() { void httpRequestMethodNotSupportedException() {
String[] supportedMethods = new String[] { "GET", "POST" }; HttpRequestMethodNotSupportedException ex =
ErrorResponse ex = new HttpRequestMethodNotSupportedException("PUT", supportedMethods, "Custom message"); new HttpRequestMethodNotSupportedException("PUT", Arrays.asList("GET", "POST"));
assertStatus(ex, HttpStatus.METHOD_NOT_ALLOWED); assertStatus(ex, HttpStatus.METHOD_NOT_ALLOWED);
assertDetail(ex, "Method 'PUT' is not supported."); assertDetail(ex, "Method 'PUT' is not supported.");
assertDetailMessageCode(ex, null, new Object[] {ex.getMethod(), ex.getSupportedHttpMethods()});
assertThat(ex.getHeaders()).hasSize(1); assertThat(ex.getHeaders()).hasSize(1);
assertThat(ex.getHeaders().getAllow()).containsExactly(HttpMethod.GET, HttpMethod.POST); assertThat(ex.getHeaders().getAllow()).containsExactly(HttpMethod.GET, HttpMethod.POST);
@ -149,90 +154,101 @@ public class ErrorResponseExceptionTests {
@Test @Test
void missingRequestHeaderException() { void missingRequestHeaderException() {
ErrorResponse ex = new MissingRequestHeaderException("Authorization", this.methodParameter); MissingRequestHeaderException ex = new MissingRequestHeaderException("Authorization", this.methodParameter);
assertStatus(ex, HttpStatus.BAD_REQUEST); assertStatus(ex, HttpStatus.BAD_REQUEST);
assertDetail(ex, "Required header 'Authorization' is not present."); assertDetail(ex, "Required header 'Authorization' is not present.");
assertDetailMessageCode(ex, null, new Object[] {ex.getHeaderName()});
assertThat(ex.getHeaders()).isEmpty(); assertThat(ex.getHeaders()).isEmpty();
} }
@Test @Test
void missingServletRequestParameterException() { void missingServletRequestParameterException() {
ErrorResponse ex = new MissingServletRequestParameterException("query", "String"); MissingServletRequestParameterException ex = new MissingServletRequestParameterException("query", "String");
assertStatus(ex, HttpStatus.BAD_REQUEST); assertStatus(ex, HttpStatus.BAD_REQUEST);
assertDetail(ex, "Required parameter 'query' is not present."); assertDetail(ex, "Required parameter 'query' is not present.");
assertDetailMessageCode(ex, null, new Object[] {ex.getParameterName()});
assertThat(ex.getHeaders()).isEmpty(); assertThat(ex.getHeaders()).isEmpty();
} }
@Test @Test
void missingMatrixVariableException() { void missingMatrixVariableException() {
ErrorResponse ex = new MissingMatrixVariableException("region", this.methodParameter); MissingMatrixVariableException ex = new MissingMatrixVariableException("region", this.methodParameter);
assertStatus(ex, HttpStatus.BAD_REQUEST); assertStatus(ex, HttpStatus.BAD_REQUEST);
assertDetail(ex, "Required path parameter 'region' is not present."); assertDetail(ex, "Required path parameter 'region' is not present.");
assertDetailMessageCode(ex, null, new Object[] {ex.getVariableName()});
assertThat(ex.getHeaders()).isEmpty(); assertThat(ex.getHeaders()).isEmpty();
} }
@Test @Test
void missingPathVariableException() { void missingPathVariableException() {
ErrorResponse ex = new MissingPathVariableException("id", this.methodParameter); MissingPathVariableException ex = new MissingPathVariableException("id", this.methodParameter);
assertStatus(ex, HttpStatus.INTERNAL_SERVER_ERROR); assertStatus(ex, HttpStatus.INTERNAL_SERVER_ERROR);
assertDetail(ex, "Required path variable 'id' is not present."); assertDetail(ex, "Required path variable 'id' is not present.");
assertDetailMessageCode(ex, null, new Object[] {ex.getVariableName()});
assertThat(ex.getHeaders()).isEmpty(); assertThat(ex.getHeaders()).isEmpty();
} }
@Test @Test
void missingRequestCookieException() { void missingRequestCookieException() {
ErrorResponse ex = new MissingRequestCookieException("oreo", this.methodParameter); MissingRequestCookieException ex = new MissingRequestCookieException("oreo", this.methodParameter);
assertStatus(ex, HttpStatus.BAD_REQUEST); assertStatus(ex, HttpStatus.BAD_REQUEST);
assertDetail(ex, "Required cookie 'oreo' is not present."); assertDetail(ex, "Required cookie 'oreo' is not present.");
assertDetailMessageCode(ex, null, new Object[] {ex.getCookieName()});
assertThat(ex.getHeaders()).isEmpty(); assertThat(ex.getHeaders()).isEmpty();
} }
@Test @Test
void unsatisfiedServletRequestParameterException() { void unsatisfiedServletRequestParameterException() {
ErrorResponse ex = new UnsatisfiedServletRequestParameterException( UnsatisfiedServletRequestParameterException ex = new UnsatisfiedServletRequestParameterException(
new String[] { "foo=bar", "bar=baz" }, Collections.singletonMap("q", new String[] {"1"})); new String[] { "foo=bar", "bar=baz" }, Collections.singletonMap("q", new String[] {"1"}));
assertStatus(ex, HttpStatus.BAD_REQUEST); assertStatus(ex, HttpStatus.BAD_REQUEST);
assertDetail(ex, "Invalid request parameters."); assertDetail(ex, "Invalid request parameters.");
assertDetailMessageCode(ex, null, new Object[] {List.of("\"foo=bar, bar=baz\"")});
assertThat(ex.getHeaders()).isEmpty(); assertThat(ex.getHeaders()).isEmpty();
} }
@Test @Test
void missingServletRequestPartException() { void missingServletRequestPartException() {
ErrorResponse ex = new MissingServletRequestPartException("file"); MissingServletRequestPartException ex = new MissingServletRequestPartException("file");
assertStatus(ex, HttpStatus.BAD_REQUEST); assertStatus(ex, HttpStatus.BAD_REQUEST);
assertDetail(ex, "Required part 'file' is not present."); assertDetail(ex, "Required part 'file' is not present.");
assertDetailMessageCode(ex, null, new Object[] {ex.getRequestPartName()});
assertThat(ex.getHeaders()).isEmpty(); assertThat(ex.getHeaders()).isEmpty();
} }
@Test @Test
void methodArgumentNotValidException() { void methodArgumentNotValidException() {
BindingResult bindingResult = new BindException(new Object(), "object"); MessageSourceTestHelper messageSourceHelper = new MessageSourceTestHelper(MethodArgumentNotValidException.class);
bindingResult.addError(new FieldError("object", "field", "message")); BindingResult bindingResult = messageSourceHelper.initBindingResult();
ErrorResponse ex = new MethodArgumentNotValidException(this.methodParameter, bindingResult); ErrorResponse ex = new MethodArgumentNotValidException(this.methodParameter, bindingResult);
assertStatus(ex, HttpStatus.BAD_REQUEST); assertStatus(ex, HttpStatus.BAD_REQUEST);
assertDetail(ex, "Invalid request content."); assertDetail(ex, "Invalid request content.");
messageSourceHelper.assertDetailMessage(ex);
assertThat(ex.getHeaders()).isEmpty(); assertThat(ex.getHeaders()).isEmpty();
} }
@ -242,11 +258,12 @@ public class ErrorResponseExceptionTests {
List<MediaType> mediaTypes = List<MediaType> mediaTypes =
Arrays.asList(MediaType.APPLICATION_JSON, MediaType.APPLICATION_CBOR); Arrays.asList(MediaType.APPLICATION_JSON, MediaType.APPLICATION_CBOR);
ErrorResponse ex = new UnsupportedMediaTypeStatusException( UnsupportedMediaTypeStatusException ex = new UnsupportedMediaTypeStatusException(
MediaType.APPLICATION_XML, mediaTypes, HttpMethod.PATCH); MediaType.APPLICATION_XML, mediaTypes, HttpMethod.PATCH);
assertStatus(ex, HttpStatus.UNSUPPORTED_MEDIA_TYPE); assertStatus(ex, HttpStatus.UNSUPPORTED_MEDIA_TYPE);
assertDetail(ex, "Content-Type 'application/xml' is not supported."); assertDetail(ex, "Content-Type 'application/xml' is not supported.");
assertDetailMessageCode(ex, null, new Object[] {ex.getContentType(), ex.getSupportedMediaTypes()});
HttpHeaders headers = ex.getHeaders(); HttpHeaders headers = ex.getHeaders();
assertThat(headers.getAccept()).isEqualTo(mediaTypes); assertThat(headers.getAccept()).isEqualTo(mediaTypes);
@ -261,19 +278,20 @@ public class ErrorResponseExceptionTests {
assertStatus(ex, HttpStatus.UNSUPPORTED_MEDIA_TYPE); assertStatus(ex, HttpStatus.UNSUPPORTED_MEDIA_TYPE);
assertDetail(ex, "Could not parse Content-Type."); assertDetail(ex, "Could not parse Content-Type.");
assertDetailMessageCode(ex, "parseError", null);
assertThat(ex.getHeaders()).isEmpty(); assertThat(ex.getHeaders()).isEmpty();
} }
@Test @Test
void notAcceptableStatusException() { void notAcceptableStatusException() {
List<MediaType> mediaTypes = List<MediaType> mediaTypes = Arrays.asList(MediaType.APPLICATION_JSON, MediaType.APPLICATION_CBOR);
Arrays.asList(MediaType.APPLICATION_JSON, MediaType.APPLICATION_CBOR); NotAcceptableStatusException ex = new NotAcceptableStatusException(mediaTypes);
ErrorResponse ex = new NotAcceptableStatusException(mediaTypes);
assertStatus(ex, HttpStatus.NOT_ACCEPTABLE); assertStatus(ex, HttpStatus.NOT_ACCEPTABLE);
assertDetail(ex, "Acceptable representations: 'application/json, application/cbor'."); assertDetail(ex, "Acceptable representations: [application/json, application/cbor].");
assertDetailMessageCode(ex, null, new Object[] {ex.getSupportedMediaTypes()});
assertThat(ex.getHeaders()).hasSize(1); assertThat(ex.getHeaders()).hasSize(1);
assertThat(ex.getHeaders().getAccept()).isEqualTo(mediaTypes); assertThat(ex.getHeaders().getAccept()).isEqualTo(mediaTypes);
@ -285,45 +303,65 @@ public class ErrorResponseExceptionTests {
ErrorResponse ex = new NotAcceptableStatusException( ErrorResponse ex = new NotAcceptableStatusException(
"Could not parse Accept header: Invalid mime type \"foo\": does not contain '/'"); "Could not parse Accept header: Invalid mime type \"foo\": does not contain '/'");
assertStatus(ex, HttpStatus.NOT_ACCEPTABLE); assertStatus(ex, HttpStatus.NOT_ACCEPTABLE);
assertDetail(ex, "Could not parse Accept header."); assertDetail(ex, "Could not parse Accept header.");
assertDetailMessageCode(ex, "parseError", null);
assertThat(ex.getHeaders()).isEmpty();
}
@Test
void serverErrorException() {
ServerErrorException ex = new ServerErrorException("Failure", null);
assertStatus(ex, HttpStatus.INTERNAL_SERVER_ERROR);
assertDetail(ex, null);
assertDetailMessageCode(ex, null, new Object[] {ex.getReason()});
assertThat(ex.getHeaders()).isEmpty(); assertThat(ex.getHeaders()).isEmpty();
} }
@Test @Test
void missingRequestValueException() { void missingRequestValueException() {
ErrorResponse ex = new MissingRequestValueException( MissingRequestValueException ex =
"foo", String.class, "header", this.methodParameter); new MissingRequestValueException("foo", String.class, "header", this.methodParameter);
assertStatus(ex, HttpStatus.BAD_REQUEST); assertStatus(ex, HttpStatus.BAD_REQUEST);
assertDetail(ex, "Required header 'foo' is not present."); assertDetail(ex, "Required header 'foo' is not present.");
assertDetailMessageCode(ex, null, new Object[] {ex.getLabel(), ex.getName()});
assertThat(ex.getHeaders()).isEmpty(); assertThat(ex.getHeaders()).isEmpty();
} }
@Test @Test
void unsatisfiedRequestParameterException() { void unsatisfiedRequestParameterException() {
ErrorResponse ex = new UnsatisfiedRequestParameterException( UnsatisfiedRequestParameterException ex =
Arrays.asList("foo=bar", "bar=baz"), new UnsatisfiedRequestParameterException(
new LinkedMultiValueMap<>(Collections.singletonMap("q", Arrays.asList("1", "2")))); Arrays.asList("foo=bar", "bar=baz"),
new LinkedMultiValueMap<>(Collections.singletonMap("q", Arrays.asList("1", "2"))));
assertStatus(ex, HttpStatus.BAD_REQUEST); assertStatus(ex, HttpStatus.BAD_REQUEST);
assertDetail(ex, "Invalid request parameters."); assertDetail(ex, "Invalid request parameters.");
assertDetailMessageCode(ex, null, new Object[] {ex.getConditions()});
assertThat(ex.getHeaders()).isEmpty(); assertThat(ex.getHeaders()).isEmpty();
} }
@Test @Test
void webExchangeBindException() { void webExchangeBindException() {
BindingResult bindingResult = new BindException(new Object(), "object"); MessageSourceTestHelper messageSourceHelper = new MessageSourceTestHelper(WebExchangeBindException.class);
bindingResult.addError(new FieldError("object", "field", "message")); BindingResult bindingResult = messageSourceHelper.initBindingResult();
ErrorResponse ex = new WebExchangeBindException(this.methodParameter, bindingResult); WebExchangeBindException ex = new WebExchangeBindException(this.methodParameter, bindingResult);
assertStatus(ex, HttpStatus.BAD_REQUEST); assertStatus(ex, HttpStatus.BAD_REQUEST);
assertDetail(ex, "Invalid request content."); assertDetail(ex, "Invalid request content.");
messageSourceHelper.assertDetailMessage(ex);
assertThat(ex.getHeaders()).isEmpty(); assertThat(ex.getHeaders()).isEmpty();
} }
@ -331,11 +369,11 @@ public class ErrorResponseExceptionTests {
void methodNotAllowedException() { void methodNotAllowedException() {
List<HttpMethod> supportedMethods = Arrays.asList(HttpMethod.GET, HttpMethod.POST); List<HttpMethod> supportedMethods = Arrays.asList(HttpMethod.GET, HttpMethod.POST);
ErrorResponse ex = new MethodNotAllowedException(HttpMethod.PUT, supportedMethods); MethodNotAllowedException ex = new MethodNotAllowedException(HttpMethod.PUT, supportedMethods);
assertStatus(ex, HttpStatus.METHOD_NOT_ALLOWED); assertStatus(ex, HttpStatus.METHOD_NOT_ALLOWED);
assertDetail(ex, "Supported methods: 'GET', 'POST'"); assertDetail(ex, "Supported methods: [GET, POST]");
assertDetailMessageCode(ex, null, new Object[] {ex.getHttpMethod(), supportedMethods});
assertThat(ex.getHeaders()).hasSize(1); assertThat(ex.getHeaders()).hasSize(1);
assertThat(ex.getHeaders().getAllow()).containsExactly(HttpMethod.GET, HttpMethod.POST); assertThat(ex.getHeaders().getAllow()).containsExactly(HttpMethod.GET, HttpMethod.POST);
@ -344,11 +382,12 @@ public class ErrorResponseExceptionTests {
@Test @Test
void methodNotAllowedExceptionWithoutSupportedMethods() { void methodNotAllowedExceptionWithoutSupportedMethods() {
ErrorResponse ex = new MethodNotAllowedException(HttpMethod.PUT, Collections.emptyList()); MethodNotAllowedException ex = new MethodNotAllowedException(HttpMethod.PUT, Collections.emptyList());
assertStatus(ex, HttpStatus.METHOD_NOT_ALLOWED); assertStatus(ex, HttpStatus.METHOD_NOT_ALLOWED);
assertDetail(ex, "Request method 'PUT' is not supported."); assertDetail(ex, "Request method 'PUT' is not supported.");
assertDetailMessageCode(ex, null, new Object[] {ex.getHttpMethod(), Collections.emptyList()});
assertThat(ex.getHeaders()).isEmpty(); assertThat(ex.getHeaders()).isEmpty();
} }
@ -368,8 +407,64 @@ public class ErrorResponseExceptionTests {
} }
} }
private void assertDetailMessageCode(
ErrorResponse ex, @Nullable String suffix, @Nullable Object[] arguments) {
assertThat(ex.getDetailMessageCode())
.isEqualTo(ErrorResponse.getDefaultDetailMessageCode(((Exception) ex).getClass(), suffix));
if (arguments != null) {
assertThat(ex.getDetailMessageArguments()).containsExactlyElementsOf(Arrays.asList(arguments));
}
else {
assertThat(ex.getDetailMessageArguments()).isNull();
}
}
@SuppressWarnings("unused") @SuppressWarnings("unused")
private void handle(String arg) {} private void handle(String arg) {}
private static class MessageSourceTestHelper {
private final String code;
public MessageSourceTestHelper(Class<? extends ErrorResponse> exceptionType) {
this.code = "problemDetail." + exceptionType.getName();
}
public BindingResult initBindingResult() {
BindingResult bindingResult = new BindException(new TestBean(), "myBean");
bindingResult.reject("bean.invalid.A", "Invalid bean message");
bindingResult.reject("bean.invalid.B");
bindingResult.rejectValue("name", "name.required", "Name is required");
bindingResult.rejectValue("age", "age.min");
return bindingResult;
}
private void assertDetailMessage(ErrorResponse ex) {
StaticMessageSource messageSource = new StaticMessageSource();
messageSource.addMessage(this.code, Locale.UK, "Failures {0}. nested failures: {1}");
messageSource.addMessage("bean.invalid.A", Locale.UK, "Bean A message");
messageSource.addMessage("bean.invalid.B", Locale.UK, "Bean B message");
messageSource.addMessage("name.required", Locale.UK, "Required name message");
messageSource.addMessage("age.min", Locale.UK, "Minimum age message");
String message = messageSource.getMessage(
ex.getDetailMessageCode(), ex.getDetailMessageArguments(), Locale.UK);
assertThat(message).isEqualTo("" +
"Failures ['Invalid bean message', 'bean.invalid.B']. " +
"nested failures: [name: 'Name is required', age: 'age.min']");
message = messageSource.getMessage(
ex.getDetailMessageCode(), ex.getDetailMessageArguments(messageSource, Locale.UK), Locale.UK);
assertThat(message).isEqualTo("" +
"Failures ['Bean A message', 'Bean B message']. " +
"nested failures: [name: 'Required name message', age: 'Minimum age message']");
}
}
} }

30
spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/ResponseEntityExceptionHandler.java

@ -16,10 +16,14 @@
package org.springframework.web.reactive.result.method.annotation; package org.springframework.web.reactive.result.method.annotation;
import java.util.Locale;
import org.apache.commons.logging.Log; import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory; import org.apache.commons.logging.LogFactory;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
import org.springframework.context.MessageSource;
import org.springframework.context.MessageSourceAware;
import org.springframework.http.HttpHeaders; import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatusCode; import org.springframework.http.HttpStatusCode;
import org.springframework.http.ProblemDetail; import org.springframework.http.ProblemDetail;
@ -55,13 +59,22 @@ import org.springframework.web.server.UnsupportedMediaTypeStatusException;
* @author Rossen Stoyanchev * @author Rossen Stoyanchev
* @since 6.0 * @since 6.0
*/ */
public abstract class ResponseEntityExceptionHandler { public abstract class ResponseEntityExceptionHandler implements MessageSourceAware {
/** /**
* Common logger for use in subclasses. * Common logger for use in subclasses.
*/ */
protected final Log logger = LogFactory.getLog(getClass()); protected final Log logger = LogFactory.getLog(getClass());
@Nullable
private MessageSource messageSource;
@Override
public void setMessageSource(MessageSource messageSource) {
this.messageSource = messageSource;
}
/** /**
* Handle all exceptions raised within Spring MVC handling of the request . * Handle all exceptions raised within Spring MVC handling of the request .
@ -306,12 +319,25 @@ public abstract class ResponseEntityExceptionHandler {
} }
if (body == null && ex instanceof ErrorResponse errorResponse) { if (body == null && ex instanceof ErrorResponse errorResponse) {
body = errorResponse.getBody(); body = resolveDetailViaMessageSource(errorResponse, exchange.getLocaleContext().getLocale());
} }
return createResponseEntity(body, headers, status, exchange); return createResponseEntity(body, headers, status, exchange);
} }
private ProblemDetail resolveDetailViaMessageSource(ErrorResponse response, @Nullable Locale locale) {
ProblemDetail body = response.getBody();
if (this.messageSource != null) {
locale = (locale != null ? locale : Locale.getDefault());
Object[] arguments = response.getDetailMessageArguments(this.messageSource, locale);
String detail = this.messageSource.getMessage(response.getDetailMessageCode(), arguments, null, locale);
if (detail != null) {
body.setDetail(detail);
}
}
return body;
}
/** /**
* Create the {@link ResponseEntity} to use from the given body, headers, * Create the {@link ResponseEntity} to use from the given body, headers,
* and statusCode. Subclasses can override this method to inspect and possibly * and statusCode. Subclasses can override this method to inspect and possibly

49
spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/ResponseEntityExceptionHandlerTests.java

@ -18,13 +18,15 @@ package org.springframework.web.reactive.result.method.annotation;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.net.URI;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Locale;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
import org.springframework.context.i18n.LocaleContextHolder;
import org.springframework.context.support.StaticMessageSource;
import org.springframework.http.HttpHeaders; import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod; import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
@ -33,6 +35,7 @@ import org.springframework.http.MediaType;
import org.springframework.http.ProblemDetail; import org.springframework.http.ProblemDetail;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.LinkedMultiValueMap;
import org.springframework.validation.BeanPropertyBindingResult;
import org.springframework.web.ErrorResponseException; import org.springframework.web.ErrorResponseException;
import org.springframework.web.bind.support.WebExchangeBindException; import org.springframework.web.bind.support.WebExchangeBindException;
import org.springframework.web.server.MethodNotAllowedException; import org.springframework.web.server.MethodNotAllowedException;
@ -97,7 +100,7 @@ public class ResponseEntityExceptionHandlerTests {
@Test @Test
void handleWebExchangeBindException() { void handleWebExchangeBindException() {
testException(new WebExchangeBindException(null, null)); testException(new WebExchangeBindException(null, new BeanPropertyBindingResult(new Object(), "foo")));
} }
@Test @Test
@ -120,20 +123,40 @@ public class ResponseEntityExceptionHandlerTests {
testException(new ErrorResponseException(HttpStatus.CONFLICT)); testException(new ErrorResponseException(HttpStatus.CONFLICT));
} }
@Test
void errorResponseProblemDetailViaMessageSource() {
@SuppressWarnings("unchecked") Locale locale = Locale.UK;
private ResponseEntity<ProblemDetail> testException(ErrorResponseException exception) { LocaleContextHolder.setLocale(locale);
ResponseEntity<?> responseEntity =
this.exceptionHandler.handleException(exception, this.exchange).block(); StaticMessageSource messageSource = new StaticMessageSource();
messageSource.addMessage(
"problemDetail." + UnsupportedMediaTypeStatusException.class.getName(), locale,
"Content-Type {0} not supported. Supported: {1}");
this.exceptionHandler.setMessageSource(messageSource);
Exception ex = new UnsupportedMediaTypeStatusException(MediaType.APPLICATION_JSON,
List.of(MediaType.APPLICATION_ATOM_XML, MediaType.APPLICATION_XML));
assertThat(responseEntity).isNotNull(); MockServerWebExchange exchange = MockServerWebExchange.from(MockServerHttpRequest.get("/")
assertThat(responseEntity.getStatusCode()).isEqualTo(exception.getStatusCode()); .acceptLanguageAsLocales(locale).build());
ResponseEntity<?> responseEntity = this.exceptionHandler.handleException(ex, exchange).block();
assertThat(responseEntity.getBody()).isNotNull().isInstanceOf(ProblemDetail.class);
ProblemDetail body = (ProblemDetail) responseEntity.getBody(); ProblemDetail body = (ProblemDetail) responseEntity.getBody();
assertThat(body.getType()).isEqualTo(URI.create(exception.getClass().getName())); assertThat(body.getDetail()).isEqualTo(
"Content-Type application/json not supported. Supported: [application/atom+xml, application/xml]");
}
return (ResponseEntity<ProblemDetail>) responseEntity; @SuppressWarnings("unchecked")
private ResponseEntity<ProblemDetail> testException(ErrorResponseException exception) {
ResponseEntity<?> entity = this.exceptionHandler.handleException(exception, this.exchange).block();
assertThat(entity).isNotNull();
assertThat(entity.getStatusCode()).isEqualTo(exception.getStatusCode());
assertThat(entity.getBody()).isNotNull().isInstanceOf(ProblemDetail.class);
return (ResponseEntity<ProblemDetail>) entity;
} }
@ -142,9 +165,7 @@ public class ResponseEntityExceptionHandlerTests {
private Mono<ResponseEntity<Object>> handleAndSetTypeToExceptionName( private Mono<ResponseEntity<Object>> handleAndSetTypeToExceptionName(
ErrorResponseException ex, HttpHeaders headers, HttpStatusCode status, ServerWebExchange exchange) { ErrorResponseException ex, HttpHeaders headers, HttpStatusCode status, ServerWebExchange exchange) {
ProblemDetail body = ex.getBody(); return handleExceptionInternal(ex, null, headers, status, exchange);
body.setType(URI.create(ex.getClass().getName()));
return handleExceptionInternal(ex, body, headers, status, exchange);
} }
@Override @Override

32
spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ResponseEntityExceptionHandler.java

@ -16,12 +16,17 @@
package org.springframework.web.servlet.mvc.method.annotation; package org.springframework.web.servlet.mvc.method.annotation;
import java.util.Locale;
import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpServletResponse;
import org.apache.commons.logging.Log; import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory; import org.apache.commons.logging.LogFactory;
import org.springframework.beans.ConversionNotSupportedException; import org.springframework.beans.ConversionNotSupportedException;
import org.springframework.beans.TypeMismatchException; import org.springframework.beans.TypeMismatchException;
import org.springframework.context.MessageSource;
import org.springframework.context.MessageSourceAware;
import org.springframework.context.i18n.LocaleContextHolder;
import org.springframework.http.HttpHeaders; import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.http.HttpStatusCode; import org.springframework.http.HttpStatusCode;
@ -64,7 +69,7 @@ import org.springframework.web.util.WebUtils;
* @author Rossen Stoyanchev * @author Rossen Stoyanchev
* @since 3.2 * @since 3.2
*/ */
public abstract class ResponseEntityExceptionHandler { public abstract class ResponseEntityExceptionHandler implements MessageSourceAware {
/** /**
* Log category to use when no mapped handler is found for a request. * Log category to use when no mapped handler is found for a request.
@ -84,6 +89,16 @@ public abstract class ResponseEntityExceptionHandler {
protected final Log logger = LogFactory.getLog(getClass()); protected final Log logger = LogFactory.getLog(getClass());
@Nullable
private MessageSource messageSource;
@Override
public void setMessageSource(MessageSource messageSource) {
this.messageSource = messageSource;
}
/** /**
* Handle all exceptions raised within Spring MVC handling of the request . * Handle all exceptions raised within Spring MVC handling of the request .
* @param ex the exception to handle * @param ex the exception to handle
@ -504,12 +519,25 @@ public abstract class ResponseEntityExceptionHandler {
} }
if (body == null && ex instanceof ErrorResponse errorResponse) { if (body == null && ex instanceof ErrorResponse errorResponse) {
body = errorResponse.getBody(); body = resolveDetailViaMessageSource(errorResponse);
} }
return createResponseEntity(body, headers, statusCode, request); return createResponseEntity(body, headers, statusCode, request);
} }
private ProblemDetail resolveDetailViaMessageSource(ErrorResponse response) {
ProblemDetail body = response.getBody();
if (this.messageSource != null) {
Locale locale = LocaleContextHolder.getLocale();
Object[] arguments = response.getDetailMessageArguments(this.messageSource, locale);
String detail = this.messageSource.getMessage(response.getDetailMessageCode(), arguments, null, locale);
if (detail != null) {
body.setDetail(detail);
}
}
return body;
}
/** /**
* Create the {@link ResponseEntity} to use from the given body, headers, * Create the {@link ResponseEntity} to use from the given body, headers,
* and statusCode. Subclasses can override this method to inspect and possibly * and statusCode. Subclasses can override this method to inspect and possibly

32
spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ResponseEntityExceptionHandlerTests.java

@ -19,18 +19,22 @@ package org.springframework.web.servlet.mvc.method.annotation;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Locale;
import jakarta.servlet.ServletException; import jakarta.servlet.ServletException;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.springframework.beans.ConversionNotSupportedException; import org.springframework.beans.ConversionNotSupportedException;
import org.springframework.beans.TypeMismatchException; import org.springframework.beans.TypeMismatchException;
import org.springframework.context.i18n.LocaleContextHolder;
import org.springframework.context.support.StaticMessageSource;
import org.springframework.core.MethodParameter; import org.springframework.core.MethodParameter;
import org.springframework.http.HttpHeaders; import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod; import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.http.HttpStatusCode; import org.springframework.http.HttpStatusCode;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.http.ProblemDetail;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.http.converter.HttpMessageNotReadableException; import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.http.converter.HttpMessageNotWritableException; import org.springframework.http.converter.HttpMessageNotWritableException;
@ -106,7 +110,7 @@ public class ResponseEntityExceptionHandlerTests {
} }
@Test @Test
public void handleHttpMediaTypeNotSupported() { public void httpMediaTypeNotSupported() {
ResponseEntity<Object> entity = testException(new HttpMediaTypeNotSupportedException( ResponseEntity<Object> entity = testException(new HttpMediaTypeNotSupportedException(
MediaType.APPLICATION_JSON, List.of(MediaType.APPLICATION_ATOM_XML, MediaType.APPLICATION_XML))); MediaType.APPLICATION_JSON, List.of(MediaType.APPLICATION_ATOM_XML, MediaType.APPLICATION_XML)));
@ -152,6 +156,32 @@ public class ResponseEntityExceptionHandlerTests {
testException(new ServletRequestBindingException("message")); testException(new ServletRequestBindingException("message"));
} }
@Test
public void errorResponseProblemDetailViaMessageSource() {
Locale locale = Locale.UK;
LocaleContextHolder.setLocale(locale);
try {
StaticMessageSource messageSource = new StaticMessageSource();
messageSource.addMessage(
"problemDetail." + HttpMediaTypeNotSupportedException.class.getName(), locale,
"Content-Type {0} not supported. Supported: {1}");
this.exceptionHandler.setMessageSource(messageSource);
ResponseEntity<?> entity = testException(new HttpMediaTypeNotSupportedException(
MediaType.APPLICATION_JSON, List.of(MediaType.APPLICATION_ATOM_XML, MediaType.APPLICATION_XML)));
ProblemDetail body = (ProblemDetail) entity.getBody();
assertThat(body.getDetail()).isEqualTo(
"Content-Type application/json not supported. Supported: [application/atom+xml, application/xml]");
}
finally {
LocaleContextHolder.resetLocaleContext();
}
}
@Test @Test
public void conversionNotSupported() { public void conversionNotSupported() {
testException(new ConversionNotSupportedException(new Object(), Object.class, null)); testException(new ConversionNotSupportedException(new Object(), Object.class, null));

3
spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/support/DefaultHandlerExceptionResolverTests.java

@ -17,6 +17,7 @@
package org.springframework.web.servlet.mvc.support; package org.springframework.web.servlet.mvc.support;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
@ -71,7 +72,7 @@ public class DefaultHandlerExceptionResolverTests {
@Test @Test
public void handleHttpRequestMethodNotSupported() { public void handleHttpRequestMethodNotSupported() {
HttpRequestMethodNotSupportedException ex = HttpRequestMethodNotSupportedException ex =
new HttpRequestMethodNotSupportedException("GET", new String[]{"POST", "PUT"}); new HttpRequestMethodNotSupportedException("GET", Arrays.asList("POST", "PUT"));
ModelAndView mav = exceptionResolver.resolveException(request, response, null, ex); ModelAndView mav = exceptionResolver.resolveException(request, response, null, ex);
assertThat(mav).as("No ModelAndView returned").isNotNull(); assertThat(mav).as("No ModelAndView returned").isNotNull();
assertThat(mav.isEmpty()).as("No Empty ModelAndView returned").isTrue(); assertThat(mav.isEmpty()).as("No Empty ModelAndView returned").isTrue();

Loading…
Cancel
Save