Add RFC-7807 response interception

Closes gh-31822
This commit is contained in:
rstoyanchev
2024-02-27 16:53:50 +00:00
parent 3577e3b758
commit 38d5c0fed6
21 changed files with 407 additions and 30 deletions

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* Copyright 2002-2024 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.
@@ -26,6 +26,7 @@ import org.springframework.lang.Nullable;
import org.springframework.util.CollectionUtils;
import org.springframework.validation.MessageCodesResolver;
import org.springframework.validation.Validator;
import org.springframework.web.ErrorResponse;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.HandlerMethodReturnValueHandler;
import org.springframework.web.servlet.HandlerExceptionResolver;
@@ -133,6 +134,11 @@ public class DelegatingWebMvcConfiguration extends WebMvcConfigurationSupport {
this.configurers.extendHandlerExceptionResolvers(exceptionResolvers);
}
@Override
protected void configureErrorResponseInterceptors(List<ErrorResponse.Interceptor> interceptors) {
this.configurers.addErrorResponseInterceptors(interceptors);
}
@Override
@Nullable
protected Validator getValidator() {

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2024 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.
@@ -67,6 +67,7 @@ import org.springframework.validation.Errors;
import org.springframework.validation.MessageCodesResolver;
import org.springframework.validation.Validator;
import org.springframework.validation.beanvalidation.OptionalValidatorFactoryBean;
import org.springframework.web.ErrorResponse;
import org.springframework.web.HttpRequestHandler;
import org.springframework.web.accept.ContentNegotiationManager;
import org.springframework.web.bind.WebDataBinder;
@@ -251,6 +252,9 @@ public class WebMvcConfigurationSupport implements ApplicationContextAware, Serv
@Nullable
private List<HttpMessageConverter<?>> messageConverters;
@Nullable
private List<ErrorResponse.Interceptor> errorResponseInterceptors;
@Nullable
private Map<String, CorsConfiguration> corsConfigurations;
@@ -653,6 +657,7 @@ public class WebMvcConfigurationSupport implements ApplicationContextAware, Serv
adapter.setWebBindingInitializer(getConfigurableWebBindingInitializer(conversionService, validator));
adapter.setCustomArgumentResolvers(getArgumentResolvers());
adapter.setCustomReturnValueHandlers(getReturnValueHandlers());
adapter.setErrorResponseInterceptors(getErrorResponseInterceptors());
if (jackson2Present) {
adapter.setRequestBodyAdvice(Collections.singletonList(new JsonViewRequestBodyAdvice()));
@@ -1053,6 +1058,7 @@ public class WebMvcConfigurationSupport implements ApplicationContextAware, Serv
exceptionHandlerResolver.setMessageConverters(getMessageConverters());
exceptionHandlerResolver.setCustomArgumentResolvers(getArgumentResolvers());
exceptionHandlerResolver.setCustomReturnValueHandlers(getReturnValueHandlers());
exceptionHandlerResolver.setErrorResponseInterceptors(getErrorResponseInterceptors());
if (jackson2Present) {
exceptionHandlerResolver.setResponseBodyAdvice(
Collections.singletonList(new JsonViewResponseBodyAdvice()));
@@ -1079,6 +1085,29 @@ public class WebMvcConfigurationSupport implements ApplicationContextAware, Serv
return new ExceptionHandlerExceptionResolver();
}
/**
* Provide access to the list of {@link ErrorResponse.Interceptor}'s to apply
* when rendering error responses.
* <p>This method cannot be overridden; use {@link #configureErrorResponseInterceptors(List)} instead.
* @since 6.2
*/
protected final List<ErrorResponse.Interceptor> getErrorResponseInterceptors() {
if (this.errorResponseInterceptors == null) {
this.errorResponseInterceptors = new ArrayList<>();
configureErrorResponseInterceptors(this.errorResponseInterceptors);
}
return this.errorResponseInterceptors;
}
/**
* Override this method for control over the {@link ErrorResponse.Interceptor}'s
* to apply when rendering error responses.
* @param interceptors the list to add handlers to
* @since 6.2
*/
protected void configureErrorResponseInterceptors(List<ErrorResponse.Interceptor> interceptors) {
}
/**
* Register a {@link ViewResolverComposite} that contains a chain of view resolvers
* to use for view resolution.

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2024 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.
@@ -25,6 +25,7 @@ import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.lang.Nullable;
import org.springframework.validation.MessageCodesResolver;
import org.springframework.validation.Validator;
import org.springframework.web.ErrorResponse;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.HandlerMethodReturnValueHandler;
@@ -221,6 +222,16 @@ public interface WebMvcConfigurer {
default void extendHandlerExceptionResolvers(List<HandlerExceptionResolver> resolvers) {
}
/**
* Add to the list of {@link ErrorResponse.Interceptor}'s to apply when
* rendering an RFC 7807 {@link org.springframework.http.ProblemDetail}
* error response.
* @param interceptors the interceptors to use
* @since 6.2
*/
default void addErrorResponseInterceptors(List<ErrorResponse.Interceptor> interceptors) {
}
/**
* Provide a custom {@link Validator} instead of the one created by default.
* The default implementation, assuming JSR-303 is on the classpath, is:

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2016 the original author or authors.
* Copyright 2002-2024 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.
@@ -25,6 +25,7 @@ import org.springframework.lang.Nullable;
import org.springframework.util.CollectionUtils;
import org.springframework.validation.MessageCodesResolver;
import org.springframework.validation.Validator;
import org.springframework.web.ErrorResponse;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.HandlerMethodReturnValueHandler;
import org.springframework.web.servlet.HandlerExceptionResolver;
@@ -159,6 +160,13 @@ class WebMvcConfigurerComposite implements WebMvcConfigurer {
}
}
@Override
public void addErrorResponseInterceptors(List<ErrorResponse.Interceptor> interceptors) {
for (WebMvcConfigurer delegate : this.delegates) {
delegate.addErrorResponseInterceptors(interceptors);
}
}
@Override
public Validator getValidator() {
Validator selected = null;

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2024 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.
@@ -57,6 +57,7 @@ import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.MimeTypeUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.ErrorResponse;
import org.springframework.web.HttpMediaTypeNotAcceptableException;
import org.springframework.web.accept.ContentNegotiationManager;
import org.springframework.web.context.request.NativeWebRequest;
@@ -99,6 +100,8 @@ public abstract class AbstractMessageConverterMethodProcessor extends AbstractMe
private final List<MediaType> problemMediaTypes =
Arrays.asList(MediaType.APPLICATION_PROBLEM_JSON, MediaType.APPLICATION_PROBLEM_XML);
private final List<ErrorResponse.Interceptor> errorResponseInterceptors = new ArrayList<>();
private final Set<String> safeExtensions = new HashSet<>();
@@ -119,17 +122,32 @@ public abstract class AbstractMessageConverterMethodProcessor extends AbstractMe
}
/**
* Constructor with list of converters and ContentNegotiationManager as well
* as request/response body advice instances.
* Variant of {@link #AbstractMessageConverterMethodProcessor(List)}
* with an additional {@link ContentNegotiationManager} for return
* value handling.
*/
protected AbstractMessageConverterMethodProcessor(List<HttpMessageConverter<?>> converters,
@Nullable ContentNegotiationManager manager, @Nullable List<Object> requestResponseBodyAdvice) {
this(converters, manager, requestResponseBodyAdvice, Collections.emptyList());
}
/**
* Variant of {@link #AbstractMessageConverterMethodProcessor(List, ContentNegotiationManager, List)}
* with additional list of {@link ErrorResponse.Interceptor}s for return
* value handling.
* @since 6.2
*/
protected AbstractMessageConverterMethodProcessor(List<HttpMessageConverter<?>> converters,
@Nullable ContentNegotiationManager manager, @Nullable List<Object> requestResponseBodyAdvice,
List<ErrorResponse.Interceptor> interceptors) {
super(converters, requestResponseBodyAdvice);
this.contentNegotiationManager = (manager != null ? manager : new ContentNegotiationManager());
this.safeExtensions.addAll(this.contentNegotiationManager.getAllFileExtensions());
this.safeExtensions.addAll(SAFE_EXTENSIONS);
this.errorResponseInterceptors.addAll(interceptors);
}
@@ -144,6 +162,21 @@ public abstract class AbstractMessageConverterMethodProcessor extends AbstractMe
return new ServletServerHttpResponse(response);
}
/**
* Invoke the configured {@link ErrorResponse.Interceptor}'s.
* @since 6.2
*/
protected void invokeErrorResponseInterceptors(ProblemDetail detail, @Nullable ErrorResponse errorResponse) {
try {
for (ErrorResponse.Interceptor handler : this.errorResponseInterceptors) {
handler.handleError(detail, errorResponse);
}
}
catch (Throwable ex) {
// ignore
}
}
/**
* Writes the given return value to the given web request. Delegates to
* {@link #writeWithMessageConverters(Object, MethodParameter, ServletServerHttpRequest, ServletServerHttpResponse)}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2024 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.
@@ -39,6 +39,7 @@ import org.springframework.http.converter.StringHttpMessageConverter;
import org.springframework.http.converter.support.AllEncompassingFormHttpMessageConverter;
import org.springframework.lang.Nullable;
import org.springframework.ui.ModelMap;
import org.springframework.web.ErrorResponse;
import org.springframework.web.accept.ContentNegotiationManager;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.context.request.ServletWebRequest;
@@ -106,6 +107,8 @@ public class ExceptionHandlerExceptionResolver extends AbstractHandlerMethodExce
private final List<Object> responseBodyAdvice = new ArrayList<>();
private final List<ErrorResponse.Interceptor> errorResponseInterceptors = new ArrayList<>();
@Nullable
private ApplicationContext applicationContext;
@@ -239,6 +242,27 @@ public class ExceptionHandlerExceptionResolver extends AbstractHandlerMethodExce
}
}
/**
* Configure a list of {@link ErrorResponse.Interceptor}'s to apply when
* rendering an RFC 7807 {@link org.springframework.http.ProblemDetail}
* error response.
* @param interceptors the handlers to use
* @since 6.2
*/
public void setErrorResponseInterceptors(List<ErrorResponse.Interceptor> interceptors) {
this.errorResponseInterceptors.clear();
this.errorResponseInterceptors.addAll(interceptors);
}
/**
* Return the {@link #setErrorResponseInterceptors(List) configured}
* {@link ErrorResponse.Interceptor}'s.
* @since 6.2
*/
public List<ErrorResponse.Interceptor> getErrorResponseInterceptors() {
return this.errorResponseInterceptors;
}
@Override
public void setApplicationContext(@Nullable ApplicationContext applicationContext) {
this.applicationContext = applicationContext;
@@ -358,12 +382,14 @@ public class ExceptionHandlerExceptionResolver extends AbstractHandlerMethodExce
handlers.add(new ModelMethodProcessor());
handlers.add(new ViewMethodReturnValueHandler());
handlers.add(new HttpEntityMethodProcessor(
getMessageConverters(), this.contentNegotiationManager, this.responseBodyAdvice));
getMessageConverters(), this.contentNegotiationManager, this.responseBodyAdvice,
this.errorResponseInterceptors));
// Annotation-based return value types
handlers.add(new ServletModelAttributeMethodProcessor(false));
handlers.add(new RequestResponseBodyMethodProcessor(
getMessageConverters(), this.contentNegotiationManager, this.responseBodyAdvice));
getMessageConverters(), this.contentNegotiationManager, this.responseBodyAdvice,
this.errorResponseInterceptors));
// Multi-purpose return value types
handlers.add(new ViewNameMethodReturnValueHandler());

View File

@@ -103,8 +103,9 @@ public class HttpEntityMethodProcessor extends AbstractMessageConverterMethodPro
}
/**
* Complete constructor for resolving {@code HttpEntity} and handling
* {@code ResponseEntity}.
* Variant of {@link #HttpEntityMethodProcessor(List, List)}
* with an additional {@link ContentNegotiationManager} argument for return
* value handling.
*/
public HttpEntityMethodProcessor(List<HttpMessageConverter<?>> converters,
@Nullable ContentNegotiationManager manager, List<Object> requestResponseBodyAdvice) {
@@ -112,6 +113,19 @@ public class HttpEntityMethodProcessor extends AbstractMessageConverterMethodPro
super(converters, manager, requestResponseBodyAdvice);
}
/**
* Variant of {@link #HttpEntityMethodProcessor(List, ContentNegotiationManager, List)}
* with additional list of {@link ErrorResponse.Interceptor}s for return
* value handling.
* @since 6.2
*/
public HttpEntityMethodProcessor(List<HttpMessageConverter<?>> converters,
@Nullable ContentNegotiationManager manager, List<Object> requestResponseBodyAdvice,
List<ErrorResponse.Interceptor> interceptors) {
super(converters, manager, requestResponseBodyAdvice, interceptors);
}
@Override
public boolean supportsParameter(MethodParameter parameter) {
@@ -204,6 +218,8 @@ public class HttpEntityMethodProcessor extends AbstractMessageConverterMethodPro
" doesn't match the ProblemDetail status: " + detail.getStatus());
}
}
invokeErrorResponseInterceptors(
detail, (returnValue instanceof ErrorResponse response ? response : null));
}
HttpHeaders outputHeaders = outputMessage.getHeaders();

View File

@@ -55,6 +55,7 @@ import org.springframework.util.ClassUtils;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ReflectionUtils.MethodFilter;
import org.springframework.validation.method.MethodValidator;
import org.springframework.web.ErrorResponse;
import org.springframework.web.accept.ContentNegotiationManager;
import org.springframework.web.bind.annotation.InitBinder;
import org.springframework.web.bind.annotation.ModelAttribute;
@@ -166,6 +167,8 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter
@Nullable
private WebBindingInitializer webBindingInitializer;
private final List<ErrorResponse.Interceptor> errorResponseInterceptors = new ArrayList<>();
@Nullable
private MethodValidator methodValidator;
@@ -395,6 +398,27 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter
return this.webBindingInitializer;
}
/**
* Configure a list of {@link ErrorResponse.Interceptor}'s to apply when
* rendering an RFC 7807 {@link org.springframework.http.ProblemDetail}
* error response.
* @param interceptors the interceptors to use
* @since 6.2
*/
public void setErrorResponseInterceptors(List<ErrorResponse.Interceptor> interceptors) {
this.errorResponseInterceptors.clear();
this.errorResponseInterceptors.addAll(interceptors);
}
/**
* Return the {@link #setErrorResponseInterceptors(List) configured}
* {@link ErrorResponse.Interceptor}'s.
* @since 6.2
*/
public List<ErrorResponse.Interceptor> getErrorResponseInterceptors() {
return this.errorResponseInterceptors;
}
/**
* Set the default {@link AsyncTaskExecutor} to use when a controller method
* return a {@link Callable}. Controller methods can override this default on
@@ -746,7 +770,7 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter
this.reactiveAdapterRegistry, this.taskExecutor, this.contentNegotiationManager));
handlers.add(new StreamingResponseBodyReturnValueHandler());
handlers.add(new HttpEntityMethodProcessor(getMessageConverters(),
this.contentNegotiationManager, this.requestResponseBodyAdvice));
this.contentNegotiationManager, this.requestResponseBodyAdvice, this.errorResponseInterceptors));
handlers.add(new HttpHeadersReturnValueHandler());
handlers.add(new CallableMethodReturnValueHandler());
handlers.add(new DeferredResultMethodReturnValueHandler());
@@ -755,7 +779,7 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter
// Annotation-based return value types
handlers.add(new ServletModelAttributeMethodProcessor(false));
handlers.add(new RequestResponseBodyMethodProcessor(getMessageConverters(),
this.contentNegotiationManager, this.requestResponseBodyAdvice));
this.contentNegotiationManager, this.requestResponseBodyAdvice, this.errorResponseInterceptors));
// Multi-purpose return value types
handlers.add(new ViewNameMethodReturnValueHandler());

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2024 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.
@@ -34,6 +34,7 @@ import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.http.server.ServletServerHttpResponse;
import org.springframework.lang.Nullable;
import org.springframework.validation.BindingResult;
import org.springframework.web.ErrorResponse;
import org.springframework.web.HttpMediaTypeNotAcceptableException;
import org.springframework.web.HttpMediaTypeNotSupportedException;
import org.springframework.web.accept.ContentNegotiationManager;
@@ -99,8 +100,9 @@ public class RequestResponseBodyMethodProcessor extends AbstractMessageConverter
}
/**
* Complete constructor for resolving {@code @RequestBody} and handling
* {@code @ResponseBody}.
* Variant of {@link #RequestResponseBodyMethodProcessor(List, List)}
* with an additional {@link ContentNegotiationManager} argument, for return
* value handling.
*/
public RequestResponseBodyMethodProcessor(List<HttpMessageConverter<?>> converters,
@Nullable ContentNegotiationManager manager, @Nullable List<Object> requestResponseBodyAdvice) {
@@ -108,6 +110,19 @@ public class RequestResponseBodyMethodProcessor extends AbstractMessageConverter
super(converters, manager, requestResponseBodyAdvice);
}
/**
* Variant of{@link #RequestResponseBodyMethodProcessor(List, ContentNegotiationManager, List)}
* with an additional {@link ErrorResponse.Interceptor} argument for return
* value handling.
* @since 6.2
*/
public RequestResponseBodyMethodProcessor(List<HttpMessageConverter<?>> converters,
@Nullable ContentNegotiationManager manager, List<Object> requestResponseBodyAdvice,
List<ErrorResponse.Interceptor> interceptors) {
super(converters, manager, requestResponseBodyAdvice, interceptors);
}
@Override
public boolean supportsParameter(MethodParameter parameter) {
@@ -184,6 +199,7 @@ public class RequestResponseBodyMethodProcessor extends AbstractMessageConverter
URI path = URI.create(inputMessage.getServletRequest().getRequestURI());
detail.setInstance(path);
}
invokeErrorResponseInterceptors(detail, null);
}
// Try even with null return value. ResponseBodyAdvice could get involved.

View File

@@ -33,6 +33,7 @@ import org.springframework.http.converter.StringHttpMessageConverter;
import org.springframework.util.PathMatcher;
import org.springframework.validation.DefaultMessageCodesResolver;
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
import org.springframework.web.ErrorResponse;
import org.springframework.web.bind.support.ConfigurableWebBindingInitializer;
import org.springframework.web.context.support.GenericWebApplicationContext;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
@@ -198,8 +199,35 @@ public class DelegatingWebMvcConfigurationTests {
(HandlerExceptionResolverComposite) webMvcConfig
.handlerExceptionResolver(webMvcConfig.mvcContentNegotiationManager());
assertThat(composite.getExceptionResolvers())
.as("Only one custom converter is expected").hasSize(1);
assertThat(composite.getExceptionResolvers()).hasSize(1);
}
@Test
public void addErrorResponseInterceptors() {
ErrorResponse.Interceptor interceptor = (detail, errorResponse) -> {};
WebMvcConfigurer configurer = new WebMvcConfigurer() {
@Override
public void addErrorResponseInterceptors(List<ErrorResponse.Interceptor> interceptors) {
interceptors.add(interceptor);
}
};
webMvcConfig.setConfigurers(Collections.singletonList(configurer));
RequestMappingHandlerAdapter adapter = webMvcConfig.requestMappingHandlerAdapter(
webMvcConfig.mvcContentNegotiationManager(),
webMvcConfig.mvcConversionService(),
webMvcConfig.getValidator());
assertThat(adapter.getErrorResponseInterceptors()).containsExactly(interceptor);
HandlerExceptionResolverComposite composite =
(HandlerExceptionResolverComposite) webMvcConfig.handlerExceptionResolver(
webMvcConfig.mvcContentNegotiationManager());
ExceptionHandlerExceptionResolver resolver =
(ExceptionHandlerExceptionResolver) composite.getExceptionResolvers().get(0);
assertThat(resolver.getErrorResponseInterceptors()).containsExactly(interceptor);
}
@Test