MessageSource support for Spring MVC and WebFlux exceptions
See gh-28814
This commit is contained in:
@@ -16,9 +16,13 @@
|
||||
|
||||
package org.springframework.web;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
import org.springframework.context.MessageSource;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatusCode;
|
||||
import org.springframework.http.ProblemDetail;
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
|
||||
/**
|
||||
@@ -59,4 +63,48 @@ public interface ErrorResponse {
|
||||
*/
|
||||
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 : "");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -46,6 +46,11 @@ public class ErrorResponseException extends NestedRuntimeException implements Er
|
||||
|
||||
private final ProblemDetail body;
|
||||
|
||||
private final String messageDetailCode;
|
||||
|
||||
@Nullable
|
||||
private final Object[] messageDetailArguments;
|
||||
|
||||
|
||||
/**
|
||||
* Constructor with a {@link HttpStatusCode}.
|
||||
@@ -66,11 +71,32 @@ public class ErrorResponseException extends NestedRuntimeException implements Er
|
||||
* subclass of {@code ProblemDetail} with extended fields.
|
||||
*/
|
||||
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);
|
||||
this.status = status;
|
||||
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
|
||||
public HttpStatusCode getStatusCode() {
|
||||
return this.status;
|
||||
@@ -133,6 +159,16 @@ public class ErrorResponseException extends NestedRuntimeException implements Er
|
||||
return this.body;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDetailMessageCode() {
|
||||
return this.messageDetailCode;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object[] getDetailMessageArguments() {
|
||||
return this.messageDetailArguments;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getMessage() {
|
||||
return this.status + (!this.headers.isEmpty() ? ", headers=" + this.headers : "") + ", " + this.body;
|
||||
|
||||
@@ -23,6 +23,7 @@ import jakarta.servlet.ServletException;
|
||||
|
||||
import org.springframework.http.MediaType;
|
||||
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}.
|
||||
@@ -37,23 +38,49 @@ public abstract class HttpMediaTypeException extends ServletException implements
|
||||
|
||||
private final ProblemDetail body = ProblemDetail.forStatus(getStatusCode());
|
||||
|
||||
private final String messageDetailCode;
|
||||
|
||||
@Nullable
|
||||
private final Object[] messageDetailArguments;
|
||||
|
||||
|
||||
/**
|
||||
* Create a new HttpMediaTypeException.
|
||||
* @param message the exception message
|
||||
* @deprecated as of 6.0
|
||||
*/
|
||||
@Deprecated
|
||||
protected HttpMediaTypeException(String message) {
|
||||
super(message);
|
||||
this.supportedMediaTypes = Collections.emptyList();
|
||||
this(message, Collections.emptyList());
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new HttpMediaTypeException with a 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) {
|
||||
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);
|
||||
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;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDetailMessageCode() {
|
||||
return this.messageDetailCode;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object[] getDetailMessageArguments() {
|
||||
return this.messageDetailArguments;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -16,8 +16,8 @@
|
||||
|
||||
package org.springframework.web;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatus;
|
||||
@@ -35,12 +35,16 @@ import org.springframework.util.CollectionUtils;
|
||||
@SuppressWarnings("serial")
|
||||
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.
|
||||
* @param message the parse error message
|
||||
*/
|
||||
public HttpMediaTypeNotAcceptableException(String message) {
|
||||
super(message);
|
||||
super(message, Collections.emptyList(), PARSE_ERROR_DETAIL_CODE, null);
|
||||
getBody().setDetail("Could not parse Accept header.");
|
||||
}
|
||||
|
||||
@@ -49,9 +53,8 @@ public class HttpMediaTypeNotAcceptableException extends HttpMediaTypeException
|
||||
* @param mediaTypes the list of supported media types
|
||||
*/
|
||||
public HttpMediaTypeNotAcceptableException(List<MediaType> mediaTypes) {
|
||||
super("No acceptable representation", mediaTypes);
|
||||
getBody().setDetail("Acceptable representations: " +
|
||||
mediaTypes.stream().map(MediaType::toString).collect(Collectors.joining(", ", "'", "'")) + ".");
|
||||
super("No acceptable representation", mediaTypes, null, new Object[] {mediaTypes});
|
||||
getBody().setDetail("Acceptable representations: " + mediaTypes + ".");
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
package org.springframework.web;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.http.HttpHeaders;
|
||||
@@ -37,6 +38,10 @@ import org.springframework.util.CollectionUtils;
|
||||
@SuppressWarnings("serial")
|
||||
public class HttpMediaTypeNotSupportedException extends HttpMediaTypeException {
|
||||
|
||||
private static final String PARSE_ERROR_DETAIL_CODE =
|
||||
ErrorResponse.getDefaultDetailMessageCode(HttpMediaTypeNotSupportedException.class, "parseError");
|
||||
|
||||
|
||||
@Nullable
|
||||
private final MediaType contentType;
|
||||
|
||||
@@ -49,7 +54,7 @@ public class HttpMediaTypeNotSupportedException extends HttpMediaTypeException {
|
||||
* @param message the exception message
|
||||
*/
|
||||
public HttpMediaTypeNotSupportedException(String message) {
|
||||
super(message);
|
||||
super(message, Collections.emptyList(), PARSE_ERROR_DETAIL_CODE, null);
|
||||
this.contentType = null;
|
||||
this.httpMethod = null;
|
||||
getBody().setDetail("Could not parse Content-Type.");
|
||||
@@ -89,7 +94,7 @@ public class HttpMediaTypeNotSupportedException extends HttpMediaTypeException {
|
||||
public HttpMediaTypeNotSupportedException(@Nullable MediaType contentType,
|
||||
List<MediaType> supportedMediaTypes, @Nullable HttpMethod httpMethod, String message) {
|
||||
|
||||
super(message, supportedMediaTypes);
|
||||
super(message, supportedMediaTypes, null, new Object[] {contentType, supportedMediaTypes});
|
||||
this.contentType = contentType;
|
||||
this.httpMethod = httpMethod;
|
||||
getBody().setDetail("Content-Type '" + this.contentType + "' is not supported.");
|
||||
|
||||
@@ -52,7 +52,9 @@ public class HttpRequestMethodNotSupportedException extends ServletException imp
|
||||
/**
|
||||
* Create a new HttpRequestMethodNotSupportedException.
|
||||
* @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) {
|
||||
this(method, (String[]) null);
|
||||
}
|
||||
@@ -61,7 +63,9 @@ public class HttpRequestMethodNotSupportedException extends ServletException imp
|
||||
* Create a new HttpRequestMethodNotSupportedException.
|
||||
* @param method the unsupported HTTP request method
|
||||
* @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) {
|
||||
this(method, null, msg);
|
||||
}
|
||||
@@ -69,7 +73,7 @@ public class HttpRequestMethodNotSupportedException extends ServletException imp
|
||||
/**
|
||||
* Create a new HttpRequestMethodNotSupportedException.
|
||||
* @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) {
|
||||
this(method, (supportedMethods != null ? StringUtils.toStringArray(supportedMethods) : null));
|
||||
@@ -78,8 +82,10 @@ public class HttpRequestMethodNotSupportedException extends ServletException imp
|
||||
/**
|
||||
* Create a new HttpRequestMethodNotSupportedException.
|
||||
* @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) {
|
||||
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 supportedMethods the actually supported HTTP methods
|
||||
* @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) {
|
||||
super(msg);
|
||||
this.method = method;
|
||||
@@ -153,4 +161,9 @@ public class HttpRequestMethodNotSupportedException extends ServletException imp
|
||||
return this.body;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object[] getDetailMessageArguments() {
|
||||
return new Object[] {getMethod(), getSupportedHttpMethods()};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -16,12 +16,20 @@
|
||||
|
||||
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.http.HttpStatus;
|
||||
import org.springframework.http.HttpStatusCode;
|
||||
import org.springframework.http.ProblemDetail;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.validation.BindException;
|
||||
import org.springframework.validation.BindingResult;
|
||||
import org.springframework.validation.FieldError;
|
||||
import org.springframework.validation.ObjectError;
|
||||
import org.springframework.web.ErrorResponse;
|
||||
|
||||
@@ -52,6 +60,7 @@ public class MethodArgumentNotValidException extends BindException implements Er
|
||||
this.body = ProblemDetail.forStatusAndDetail(getStatusCode(), "Invalid request content.");
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public HttpStatusCode getStatusCode() {
|
||||
return HttpStatus.BAD_REQUEST;
|
||||
@@ -85,4 +94,58 @@ public class MethodArgumentNotValidException extends BindException implements Er
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -54,7 +54,7 @@ public class MissingMatrixVariableException extends MissingRequestValueException
|
||||
public MissingMatrixVariableException(
|
||||
String variableName, MethodParameter parameter, boolean missingAfterConversion) {
|
||||
|
||||
super("", missingAfterConversion);
|
||||
super("", missingAfterConversion, null, new Object[] {variableName});
|
||||
this.variableName = variableName;
|
||||
this.parameter = parameter;
|
||||
getBody().setDetail("Required path parameter '" + this.variableName + "' is not present.");
|
||||
|
||||
@@ -58,7 +58,7 @@ public class MissingPathVariableException extends MissingRequestValueException {
|
||||
public MissingPathVariableException(
|
||||
String variableName, MethodParameter parameter, boolean missingAfterConversion) {
|
||||
|
||||
super("", missingAfterConversion);
|
||||
super("", missingAfterConversion, null, new Object[] {variableName});
|
||||
this.variableName = variableName;
|
||||
this.parameter = parameter;
|
||||
getBody().setDetail("Required path variable '" + this.variableName + "' is not present.");
|
||||
|
||||
@@ -54,7 +54,7 @@ public class MissingRequestCookieException extends MissingRequestValueException
|
||||
public MissingRequestCookieException(
|
||||
String cookieName, MethodParameter parameter, boolean missingAfterConversion) {
|
||||
|
||||
super("", missingAfterConversion);
|
||||
super("", missingAfterConversion, null, new Object[] {cookieName});
|
||||
this.cookieName = cookieName;
|
||||
this.parameter = parameter;
|
||||
getBody().setDetail("Required cookie '" + this.cookieName + "' is not present.");
|
||||
|
||||
@@ -54,7 +54,7 @@ public class MissingRequestHeaderException extends MissingRequestValueException
|
||||
public MissingRequestHeaderException(
|
||||
String headerName, MethodParameter parameter, boolean missingAfterConversion) {
|
||||
|
||||
super("", missingAfterConversion);
|
||||
super("", missingAfterConversion, null, new Object[] {headerName});
|
||||
this.headerName = headerName;
|
||||
this.parameter = parameter;
|
||||
getBody().setDetail("Required header '" + this.headerName + "' is not present.");
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2021 the original author or authors.
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -16,6 +16,9 @@
|
||||
|
||||
package org.springframework.web.bind;
|
||||
|
||||
import org.springframework.http.ProblemDetail;
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
/**
|
||||
* Base class for {@link ServletRequestBindingException} exceptions that could
|
||||
* 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;
|
||||
|
||||
|
||||
/**
|
||||
* Constructor with a message only.
|
||||
*/
|
||||
public MissingRequestValueException(String msg) {
|
||||
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) {
|
||||
super(msg);
|
||||
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
|
||||
|
||||
@@ -49,7 +49,7 @@ public class MissingServletRequestParameterException extends MissingRequestValue
|
||||
public MissingServletRequestParameterException(
|
||||
String parameterName, String parameterType, boolean missingAfterConversion) {
|
||||
|
||||
super("", missingAfterConversion);
|
||||
super("", missingAfterConversion, null, new Object[] {parameterName});
|
||||
this.parameterName = parameterName;
|
||||
this.parameterType = parameterType;
|
||||
getBody().setDetail("Required parameter '" + this.parameterName + "' is not present.");
|
||||
|
||||
@@ -21,6 +21,7 @@ import jakarta.servlet.ServletException;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.HttpStatusCode;
|
||||
import org.springframework.http.ProblemDetail;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.web.ErrorResponse;
|
||||
|
||||
/**
|
||||
@@ -39,24 +40,68 @@ public class ServletRequestBindingException extends ServletException implements
|
||||
|
||||
private final ProblemDetail body = ProblemDetail.forStatus(getStatusCode());
|
||||
|
||||
private final String messageDetailCode;
|
||||
|
||||
@Nullable
|
||||
private final Object[] messageDetailArguments;
|
||||
|
||||
|
||||
/**
|
||||
* Constructor with a message only.
|
||||
* @param msg the detail message
|
||||
*/
|
||||
public ServletRequestBindingException(String msg) {
|
||||
this(msg, null, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor with a message and a cause.
|
||||
* @param msg the detail message
|
||||
* @param cause the root 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
|
||||
*/
|
||||
public ServletRequestBindingException(String msg) {
|
||||
super(msg);
|
||||
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
|
||||
*/
|
||||
public ServletRequestBindingException(String msg, Throwable cause) {
|
||||
protected ServletRequestBindingException(String msg, @Nullable Throwable cause,
|
||||
@Nullable String messageDetailCode, @Nullable Object[] messageDetailArguments) {
|
||||
|
||||
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
|
||||
public HttpStatusCode getStatusCode() {
|
||||
return HttpStatus.BAD_REQUEST;
|
||||
@@ -67,4 +112,14 @@ public class ServletRequestBindingException extends ServletException implements
|
||||
return this.body;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDetailMessageCode() {
|
||||
return this.messageDetailCode;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object[] getDetailMessageArguments() {
|
||||
return this.messageDetailArguments;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ package org.springframework.web.bind;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
@@ -56,30 +57,28 @@ public class UnsatisfiedServletRequestParameterException extends ServletRequestB
|
||||
* @param actualParams the actual parameter Map associated with the ServletRequest
|
||||
* @since 4.2
|
||||
*/
|
||||
public UnsatisfiedServletRequestParameterException(List<String[]> paramConditions,
|
||||
Map<String, String[]> actualParams) {
|
||||
public UnsatisfiedServletRequestParameterException(
|
||||
List<String[]> paramConditions, Map<String, String[]> actualParams) {
|
||||
|
||||
super("");
|
||||
Assert.notEmpty(paramConditions, "Parameter conditions must not be empty");
|
||||
super("", null, new Object[] {paramsToStringList(paramConditions)});
|
||||
this.paramConditions = paramConditions;
|
||||
this.actualParams = actualParams;
|
||||
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
|
||||
public String getMessage() {
|
||||
StringBuilder sb = new StringBuilder("Parameter conditions ");
|
||||
int i = 0;
|
||||
for (String[] conditions : this.paramConditions) {
|
||||
if (i > 0) {
|
||||
sb.append(" OR ");
|
||||
}
|
||||
sb.append('"');
|
||||
sb.append(StringUtils.arrayToDelimitedString(conditions, ", "));
|
||||
sb.append('"');
|
||||
i++;
|
||||
}
|
||||
sb.append(String.join(" OR ", paramsToStringList(this.paramConditions)));
|
||||
sb.append(" not met for actual request parameters: ");
|
||||
sb.append(requestParameterMapToString(this.actualParams));
|
||||
return sb.toString();
|
||||
|
||||
@@ -18,9 +18,11 @@ package org.springframework.web.bind.support;
|
||||
|
||||
import java.beans.PropertyEditor;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.beans.PropertyEditorRegistry;
|
||||
import org.springframework.context.MessageSource;
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
@@ -29,6 +31,7 @@ import org.springframework.validation.BindingResult;
|
||||
import org.springframework.validation.Errors;
|
||||
import org.springframework.validation.FieldError;
|
||||
import org.springframework.validation.ObjectError;
|
||||
import org.springframework.web.bind.MethodArgumentNotValidException;
|
||||
import org.springframework.web.server.ServerWebInputException;
|
||||
|
||||
/**
|
||||
@@ -45,11 +48,18 @@ public class WebExchangeBindException extends ServerWebInputException implements
|
||||
|
||||
|
||||
public WebExchangeBindException(MethodParameter parameter, BindingResult bindingResult) {
|
||||
super("Validation failure", parameter);
|
||||
super("Validation failure", parameter, null, null, initMessageDetailArguments(bindingResult));
|
||||
this.bindingResult = bindingResult;
|
||||
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.
|
||||
@@ -289,6 +299,14 @@ public class WebExchangeBindException extends ServerWebInputException implements
|
||||
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
|
||||
public boolean equals(@Nullable Object other) {
|
||||
return (this == other || this.bindingResult.equals(other));
|
||||
|
||||
@@ -84,4 +84,9 @@ public class MissingServletRequestPartException extends ServletException impleme
|
||||
return this.body;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object[] getDetailMessageArguments() {
|
||||
return new Object[] {getRequestPartName()};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -20,7 +20,6 @@ import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpMethod;
|
||||
@@ -48,17 +47,17 @@ public class MethodNotAllowedException extends ResponseStatusException {
|
||||
}
|
||||
|
||||
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");
|
||||
if (supportedMethods == null) {
|
||||
supportedMethods = Collections.emptySet();
|
||||
}
|
||||
this.method = method;
|
||||
this.httpMethods = Collections.unmodifiableSet(new LinkedHashSet<>(supportedMethods));
|
||||
|
||||
getBody().setDetail(this.httpMethods.isEmpty() ? getReason() :
|
||||
"Supported methods: " + this.httpMethods.stream()
|
||||
.map(HttpMethod::toString).collect(Collectors.joining("', '", "'", "'")));
|
||||
getBody().setDetail(this.httpMethods.isEmpty() ?
|
||||
getReason() : "Supported methods: " + this.httpMethods);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -36,7 +36,9 @@ public class MissingRequestValueException extends ServerWebInputException {
|
||||
|
||||
|
||||
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.type = type;
|
||||
this.label = label;
|
||||
|
||||
@@ -18,12 +18,12 @@ package org.springframework.web.server;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.web.ErrorResponse;
|
||||
|
||||
/**
|
||||
* Exception for errors that fit response status 406 (not acceptable).
|
||||
@@ -34,6 +34,10 @@ import org.springframework.util.CollectionUtils;
|
||||
@SuppressWarnings("serial")
|
||||
public class NotAcceptableStatusException extends ResponseStatusException {
|
||||
|
||||
private static final String PARSE_ERROR_DETAIL_CODE =
|
||||
ErrorResponse.getDefaultDetailMessageCode(NotAcceptableStatusException.class, "parseError");
|
||||
|
||||
|
||||
private final List<MediaType> supportedMediaTypes;
|
||||
|
||||
|
||||
@@ -41,7 +45,7 @@ public class NotAcceptableStatusException extends ResponseStatusException {
|
||||
* Constructor for when the requested Content-Type is invalid.
|
||||
*/
|
||||
public NotAcceptableStatusException(String reason) {
|
||||
super(HttpStatus.NOT_ACCEPTABLE, reason);
|
||||
super(HttpStatus.NOT_ACCEPTABLE, reason, null, PARSE_ERROR_DETAIL_CODE, null);
|
||||
this.supportedMediaTypes = Collections.emptyList();
|
||||
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.
|
||||
*/
|
||||
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);
|
||||
getBody().setDetail("Acceptable representations: " +
|
||||
mediaTypes.stream().map(MediaType::toString).collect(Collectors.joining(", ", "'", "'")) + ".");
|
||||
getBody().setDetail("Acceptable representations: " + mediaTypes + ".");
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ package org.springframework.web.server;
|
||||
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatusCode;
|
||||
import org.springframework.http.ProblemDetail;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.web.ErrorResponseException;
|
||||
|
||||
@@ -78,6 +79,22 @@ public class ResponseStatusException extends ErrorResponseException {
|
||||
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).
|
||||
|
||||
@@ -45,7 +45,7 @@ public class ServerErrorException extends ResponseStatusException {
|
||||
* @since 5.0.5
|
||||
*/
|
||||
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.parameter = null;
|
||||
}
|
||||
@@ -55,7 +55,7 @@ public class ServerErrorException extends ResponseStatusException {
|
||||
* @since 5.0.5
|
||||
*/
|
||||
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.parameter = null;
|
||||
}
|
||||
@@ -64,7 +64,7 @@ public class ServerErrorException extends ResponseStatusException {
|
||||
* Constructor for a 500 error with a {@link MethodParameter} and an optional 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.parameter = parameter;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2017 the original author or authors.
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -53,7 +53,18 @@ public class ServerWebInputException extends ResponseStatusException {
|
||||
* Constructor for a 400 error with a root 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;
|
||||
}
|
||||
|
||||
|
||||
@@ -36,12 +36,10 @@ public class UnsatisfiedRequestParameterException extends ServerWebInputExceptio
|
||||
private final MultiValueMap<String, String> requestParams;
|
||||
|
||||
|
||||
public UnsatisfiedRequestParameterException(
|
||||
List<String> conditions, MultiValueMap<String, String> requestParams) {
|
||||
|
||||
super(initReason(conditions, requestParams));
|
||||
public UnsatisfiedRequestParameterException(List<String> conditions, MultiValueMap<String, String> params) {
|
||||
super(initReason(conditions, params), null, null, null, new Object[] {conditions});
|
||||
this.conditions = conditions;
|
||||
this.requestParams = requestParams;
|
||||
this.requestParams = params;
|
||||
getBody().setDetail("Invalid request parameters.");
|
||||
}
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.web.ErrorResponse;
|
||||
|
||||
/**
|
||||
* Exception for errors that fit response status 415 (unsupported media type).
|
||||
@@ -36,6 +37,10 @@ import org.springframework.util.CollectionUtils;
|
||||
@SuppressWarnings("serial")
|
||||
public class UnsupportedMediaTypeStatusException extends ResponseStatusException {
|
||||
|
||||
private static final String PARSE_ERROR_DETAIL_CODE =
|
||||
ErrorResponse.getDefaultDetailMessageCode(UnsupportedMediaTypeStatusException.class, "parseError");
|
||||
|
||||
|
||||
@Nullable
|
||||
private final MediaType contentType;
|
||||
|
||||
@@ -52,7 +57,7 @@ public class UnsupportedMediaTypeStatusException extends ResponseStatusException
|
||||
* Constructor for when the specified Content-Type is invalid.
|
||||
*/
|
||||
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.supportedMediaTypes = Collections.emptyList();
|
||||
this.bodyType = null;
|
||||
@@ -92,9 +97,8 @@ public class UnsupportedMediaTypeStatusException extends ResponseStatusException
|
||||
public UnsupportedMediaTypeStatusException(@Nullable MediaType contentType, List<MediaType> supportedTypes,
|
||||
@Nullable ResolvableType bodyType, @Nullable HttpMethod method) {
|
||||
|
||||
super(HttpStatus.UNSUPPORTED_MEDIA_TYPE,
|
||||
"Content type '" + (contentType != null ? contentType : "") + "' not supported" +
|
||||
(bodyType != null ? " for bodyType=" + bodyType : ""));
|
||||
super(HttpStatus.UNSUPPORTED_MEDIA_TYPE, initMessage(contentType, bodyType),
|
||||
null, null, new Object[] {contentType, supportedTypes});
|
||||
|
||||
this.contentType = contentType;
|
||||
this.supportedMediaTypes = Collections.unmodifiableList(supportedTypes);
|
||||
@@ -104,6 +108,11 @@ public class UnsupportedMediaTypeStatusException extends ResponseStatusException
|
||||
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,
|
||||
|
||||
Reference in New Issue
Block a user