From 38d5c0fed66494648cea05207f3427209aad6299 Mon Sep 17 00:00:00 2001 From: rstoyanchev Date: Tue, 27 Feb 2024 16:53:50 +0000 Subject: [PATCH] Add RFC-7807 response interception Closes gh-31822 --- .../web/webflux/ann-rest-exceptions.adoc | 4 ++ .../web/webmvc/mvc-ann-rest-exceptions.adoc | 4 ++ .../springframework/web/ErrorResponse.java | 22 +++++++++- .../DelegatingWebFluxConfiguration.java | 9 +++- .../config/WebFluxConfigurationSupport.java | 34 +++++++++++++-- .../reactive/config/WebFluxConfigurer.java | 15 ++++++- .../config/WebFluxConfigurerComposite.java | 10 ++++- .../AbstractMessageWriterResultHandler.java | 42 +++++++++++++++++++ .../annotation/ResponseBodyResultHandler.java | 21 +++++++++- .../ResponseEntityResultHandler.java | 20 ++++++++- .../DelegatingWebFluxConfigurationTests.java | 21 ++++++++++ .../DelegatingWebMvcConfiguration.java | 8 +++- .../WebMvcConfigurationSupport.java | 31 +++++++++++++- .../config/annotation/WebMvcConfigurer.java | 13 +++++- .../annotation/WebMvcConfigurerComposite.java | 10 ++++- ...stractMessageConverterMethodProcessor.java | 39 +++++++++++++++-- .../ExceptionHandlerExceptionResolver.java | 32 ++++++++++++-- .../annotation/HttpEntityMethodProcessor.java | 20 ++++++++- .../RequestMappingHandlerAdapter.java | 28 ++++++++++++- .../RequestResponseBodyMethodProcessor.java | 22 ++++++++-- .../DelegatingWebMvcConfigurationTests.java | 32 +++++++++++++- 21 files changed, 407 insertions(+), 30 deletions(-) diff --git a/framework-docs/modules/ROOT/pages/web/webflux/ann-rest-exceptions.adoc b/framework-docs/modules/ROOT/pages/web/webflux/ann-rest-exceptions.adoc index f775e28e2a..ddc37153d1 100644 --- a/framework-docs/modules/ROOT/pages/web/webflux/ann-rest-exceptions.adoc +++ b/framework-docs/modules/ROOT/pages/web/webflux/ann-rest-exceptions.adoc @@ -44,6 +44,10 @@ has an `@ExceptionHandler` method that handles any `ErrorResponse` exception, wh includes all built-in web exceptions. You can add more exception handling methods, and use a protected method to map any exception to a `ProblemDetail`. +You can register `ErrorResponse` interceptors through the +xref:web/webflux/config.adoc[WebFlux Config] with a `WebFluxConfigurer`. Use that to intercept +any RFC 7807 response and take some action. + [[webflux-ann-rest-exceptions-non-standard]] diff --git a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-ann-rest-exceptions.adoc b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-ann-rest-exceptions.adoc index 66bfbc0e76..b131dcf625 100644 --- a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-ann-rest-exceptions.adoc +++ b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-ann-rest-exceptions.adoc @@ -44,6 +44,10 @@ has an `@ExceptionHandler` method that handles any `ErrorResponse` exception, wh includes all built-in web exceptions. You can add more exception handling methods, and use a protected method to map any exception to a `ProblemDetail`. +You can register `ErrorResponse` interceptors through the +xref:web/webmvc/mvc-config.adoc[MVC Config] with a `WebMvcConfigurer`. Use that to intercept +any RFC 7807 response and take some action. + [[mvc-ann-rest-exceptions-non-standard]] diff --git a/spring-web/src/main/java/org/springframework/web/ErrorResponse.java b/spring-web/src/main/java/org/springframework/web/ErrorResponse.java index c83bcdb1c4..920bd31aec 100644 --- a/spring-web/src/main/java/org/springframework/web/ErrorResponse.java +++ b/spring-web/src/main/java/org/springframework/web/ErrorResponse.java @@ -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. @@ -333,4 +333,24 @@ public interface ErrorResponse { } + + /** + * Callback to perform an action before an RFC-7807 {@link ProblemDetail} + * response is rendered. + * + * @author Rossen Stoyanchev + * @since 6.2 + */ + interface Interceptor { + + /** + * Handle the given {@code ProblemDetail} that's going to be rendered, + * and the {@code ErrorResponse} it originates from, if applicable. + * @param detail the {@code ProblemDetail} to be rendered + * @param errorResponse the {@code ErrorResponse}, or {@code null} if there isn't one + */ + void handleError(ProblemDetail detail, @Nullable ErrorResponse errorResponse); + + } + } diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/config/DelegatingWebFluxConfiguration.java b/spring-webflux/src/main/java/org/springframework/web/reactive/config/DelegatingWebFluxConfiguration.java index de6d56565b..5a49fa247e 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/config/DelegatingWebFluxConfiguration.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/config/DelegatingWebFluxConfiguration.java @@ -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.codec.ServerCodecConfigurer; import org.springframework.util.CollectionUtils; import org.springframework.validation.MessageCodesResolver; import org.springframework.validation.Validator; +import org.springframework.web.ErrorResponse; import org.springframework.web.reactive.accept.RequestedContentTypeResolverBuilder; import org.springframework.web.reactive.result.method.annotation.ArgumentResolverConfigurer; import org.springframework.web.reactive.socket.server.WebSocketService; @@ -99,6 +100,12 @@ public class DelegatingWebFluxConfiguration extends WebFluxConfigurationSupport this.configurers.configureArgumentResolvers(configurer); } + @Override + protected void configureErrorResponseInterceptors(List interceptors) { + this.configurers.addErrorResponseInterceptors(interceptors); + } + + @Override protected void addResourceHandlers(ResourceHandlerRegistry registry) { this.configurers.addResourceHandlers(registry); diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/config/WebFluxConfigurationSupport.java b/spring-webflux/src/main/java/org/springframework/web/reactive/config/WebFluxConfigurationSupport.java index ac71dc8467..ae451e4d75 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/config/WebFluxConfigurationSupport.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/config/WebFluxConfigurationSupport.java @@ -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. @@ -16,6 +16,7 @@ package org.springframework.web.reactive.config; +import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.function.Predicate; @@ -44,6 +45,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.bind.WebDataBinder; import org.springframework.web.bind.support.ConfigurableWebBindingInitializer; import org.springframework.web.cors.CorsConfiguration; @@ -98,6 +100,9 @@ public class WebFluxConfigurationSupport implements ApplicationContextAware { @Nullable private BlockingExecutionConfigurer blockingExecutionConfigurer; + @Nullable + private List errorResponseInterceptors; + @Nullable private ViewResolverRegistry viewResolverRegistry; @@ -498,7 +503,7 @@ public class WebFluxConfigurationSupport implements ApplicationContextAware { @Qualifier("webFluxContentTypeResolver") RequestedContentTypeResolver contentTypeResolver) { return new ResponseEntityResultHandler(serverCodecConfigurer.getWriters(), - contentTypeResolver, reactiveAdapterRegistry); + contentTypeResolver, reactiveAdapterRegistry, getErrorResponseInterceptors()); } @Bean @@ -508,7 +513,7 @@ public class WebFluxConfigurationSupport implements ApplicationContextAware { @Qualifier("webFluxContentTypeResolver") RequestedContentTypeResolver contentTypeResolver) { return new ResponseBodyResultHandler(serverCodecConfigurer.getWriters(), - contentTypeResolver, reactiveAdapterRegistry); + contentTypeResolver, reactiveAdapterRegistry, getErrorResponseInterceptors()); } @Bean @@ -534,6 +539,29 @@ public class WebFluxConfigurationSupport implements ApplicationContextAware { return handler; } + /** + * Provide access to the list of {@link ErrorResponse.Interceptor}'s to apply + * in result handlers when rendering error responses. + *

This method cannot be overridden; use {@link #configureErrorResponseInterceptors(List)} instead. + * @since 6.2 + */ + protected final List 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 in result handling when rendering error responses. + * @param interceptors the list to add handlers to + * @since 6.2 + */ + protected void configureErrorResponseInterceptors(List interceptors) { + } + /** * Callback for building the {@link ViewResolverRegistry}. This method is final, * use {@link #configureViewResolvers} to customize view resolvers. diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/config/WebFluxConfigurer.java b/spring-webflux/src/main/java/org/springframework/web/reactive/config/WebFluxConfigurer.java index a89c481314..408077cef5 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/config/WebFluxConfigurer.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/config/WebFluxConfigurer.java @@ -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. @@ -16,6 +16,8 @@ package org.springframework.web.reactive.config; +import java.util.List; + import org.springframework.core.convert.converter.Converter; import org.springframework.format.Formatter; import org.springframework.format.FormatterRegistry; @@ -23,6 +25,7 @@ import org.springframework.http.codec.ServerCodecConfigurer; 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.reactive.accept.RequestedContentTypeResolverBuilder; import org.springframework.web.reactive.result.method.annotation.ArgumentResolverConfigurer; @@ -133,6 +136,16 @@ public interface WebFluxConfigurer { default void configureArgumentResolvers(ArgumentResolverConfigurer configurer) { } + /** + * Add to the list of {@link ErrorResponse.Interceptor}'s to invoke when + * rendering an RFC 7807 {@link org.springframework.http.ProblemDetail} + * error response. + * @param interceptors the handlers to use + * @since 6.2 + */ + default void addErrorResponseInterceptors(List interceptors) { + } + /** * Configure view resolution for rendering responses with a view and a model, * where the view is typically an HTML template but could also be based on diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/config/WebFluxConfigurerComposite.java b/spring-webflux/src/main/java/org/springframework/web/reactive/config/WebFluxConfigurerComposite.java index b28810f95d..7d37cad16b 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/config/WebFluxConfigurerComposite.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/config/WebFluxConfigurerComposite.java @@ -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. @@ -27,6 +27,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.reactive.accept.RequestedContentTypeResolverBuilder; import org.springframework.web.reactive.result.method.annotation.ArgumentResolverConfigurer; import org.springframework.web.reactive.socket.server.WebSocketService; @@ -95,6 +96,13 @@ public class WebFluxConfigurerComposite implements WebFluxConfigurer { this.delegates.forEach(delegate -> delegate.configureArgumentResolvers(configurer)); } + @Override + public void addErrorResponseInterceptors(List interceptors) { + for (WebFluxConfigurer delegate : this.delegates) { + delegate.addErrorResponseInterceptors(interceptors); + } + } + @Override public void configureViewResolvers(ViewResolverRegistry registry) { this.delegates.forEach(delegate -> delegate.configureViewResolvers(registry)); diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractMessageWriterResultHandler.java b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractMessageWriterResultHandler.java index 25556ee76c..e5f0672eec 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractMessageWriterResultHandler.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractMessageWriterResultHandler.java @@ -18,6 +18,7 @@ package org.springframework.web.reactive.result.method.annotation; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.List; import java.util.Set; @@ -40,6 +41,7 @@ import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.CollectionUtils; +import org.springframework.web.ErrorResponse; import org.springframework.web.reactive.HandlerMapping; import org.springframework.web.reactive.accept.RequestedContentTypeResolver; import org.springframework.web.reactive.result.HandlerResultHandlerSupport; @@ -60,6 +62,8 @@ public abstract class AbstractMessageWriterResultHandler extends HandlerResultHa private final List> messageWriters; + private final List errorResponseInterceptors = new ArrayList<>(); + private final List problemMediaTypes = Arrays.asList(MediaType.APPLICATION_PROBLEM_JSON, MediaType.APPLICATION_PROBLEM_XML); @@ -86,9 +90,24 @@ public abstract class AbstractMessageWriterResultHandler extends HandlerResultHa protected AbstractMessageWriterResultHandler(List> messageWriters, RequestedContentTypeResolver contentTypeResolver, ReactiveAdapterRegistry adapterRegistry) { + this(messageWriters, contentTypeResolver, adapterRegistry, Collections.emptyList()); + } + + /** + * Variant of + * {@link #AbstractMessageWriterResultHandler(List, RequestedContentTypeResolver, ReactiveAdapterRegistry)} + * with additional list of {@link ErrorResponse.Interceptor}s for return + * value handling. + * @since 6.2 + */ + protected AbstractMessageWriterResultHandler(List> messageWriters, + RequestedContentTypeResolver contentTypeResolver, ReactiveAdapterRegistry adapterRegistry, + List interceptors) { + super(contentTypeResolver, adapterRegistry); Assert.notEmpty(messageWriters, "At least one message writer is required"); this.messageWriters = messageWriters; + this.errorResponseInterceptors.addAll(interceptors); } @@ -99,6 +118,29 @@ public abstract class AbstractMessageWriterResultHandler extends HandlerResultHa return this.messageWriters; } + /** + * Return the configured {@link ErrorResponse.Interceptor}'s. + * @since 6.2 + */ + public List getErrorResponseInterceptors() { + return this.errorResponseInterceptors; + } + + + /** + * 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 + } + } /** * Write a given body to the response with {@link HttpMessageWriter}. diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/ResponseBodyResultHandler.java b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/ResponseBodyResultHandler.java index be977767f1..aa4eb2b689 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/ResponseBodyResultHandler.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/ResponseBodyResultHandler.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 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. @@ -17,6 +17,7 @@ package org.springframework.web.reactive.result.method.annotation; import java.net.URI; +import java.util.Collections; import java.util.List; import reactor.core.publisher.Mono; @@ -27,6 +28,7 @@ import org.springframework.core.annotation.AnnotatedElementUtils; import org.springframework.http.HttpStatusCode; import org.springframework.http.ProblemDetail; import org.springframework.http.codec.HttpMessageWriter; +import org.springframework.web.ErrorResponse; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.reactive.HandlerResult; import org.springframework.web.reactive.HandlerResultHandler; @@ -69,7 +71,21 @@ public class ResponseBodyResultHandler extends AbstractMessageWriterResultHandle public ResponseBodyResultHandler(List> writers, RequestedContentTypeResolver resolver, ReactiveAdapterRegistry registry) { - super(writers, resolver, registry); + this(writers, resolver, registry, Collections.emptyList()); + } + + /** + * Variant of + * {@link #ResponseBodyResultHandler(List, RequestedContentTypeResolver, ReactiveAdapterRegistry)} + * with additional list of {@link ErrorResponse.Interceptor}s for return + * value handling. + * @since 6.2 + */ + public ResponseBodyResultHandler(List> writers, + RequestedContentTypeResolver resolver, ReactiveAdapterRegistry registry, + List interceptors) { + + super(writers, resolver, registry, interceptors); setOrder(100); } @@ -92,6 +108,7 @@ public class ResponseBodyResultHandler extends AbstractMessageWriterResultHandle URI path = URI.create(exchange.getRequest().getPath().value()); detail.setInstance(path); } + invokeErrorResponseInterceptors(detail, null); } return writeBody(body, bodyTypeParameter, exchange); } diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/ResponseEntityResultHandler.java b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/ResponseEntityResultHandler.java index ea158774a0..ea0bccc79f 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/ResponseEntityResultHandler.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/ResponseEntityResultHandler.java @@ -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. @@ -18,6 +18,7 @@ package org.springframework.web.reactive.result.method.annotation; import java.net.URI; import java.time.Instant; +import java.util.Collections; import java.util.List; import java.util.Set; @@ -78,7 +79,20 @@ public class ResponseEntityResultHandler extends AbstractMessageWriterResultHand public ResponseEntityResultHandler(List> writers, RequestedContentTypeResolver resolver, ReactiveAdapterRegistry registry) { - super(writers, resolver, registry); + this(writers, resolver, registry, Collections.emptyList()); + } + + /** + * Constructor with an {@link ReactiveAdapterRegistry} instance. + * @param writers the writers for serializing to the response body + * @param resolver to determine the requested content type + * @param registry for adaptation to reactive types + */ + public ResponseEntityResultHandler(List> writers, + RequestedContentTypeResolver resolver, ReactiveAdapterRegistry registry, + List interceptors) { + + super(writers, resolver, registry, interceptors); setOrder(0); } @@ -166,6 +180,8 @@ public class ResponseEntityResultHandler extends AbstractMessageWriterResultHand " doesn't match the ProblemDetail status: " + detail.getStatus()); } } + invokeErrorResponseInterceptors( + detail, (returnValue instanceof ErrorResponse response ? response : null)); } if (httpEntity instanceof ResponseEntity responseEntity) { diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/config/DelegatingWebFluxConfigurationTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/config/DelegatingWebFluxConfigurationTests.java index 2a068aaa2a..bb50ecaa84 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/config/DelegatingWebFluxConfigurationTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/config/DelegatingWebFluxConfigurationTests.java @@ -35,8 +35,10 @@ import org.springframework.http.codec.HttpMessageWriter; import org.springframework.http.codec.ServerCodecConfigurer; import org.springframework.validation.Validator; import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean; +import org.springframework.web.ErrorResponse; import org.springframework.web.bind.support.ConfigurableWebBindingInitializer; import org.springframework.web.reactive.accept.RequestedContentTypeResolverBuilder; +import org.springframework.web.reactive.result.method.annotation.ResponseBodyResultHandler; import org.springframework.web.reactive.socket.server.WebSocketService; import org.springframework.web.reactive.socket.server.support.WebSocketHandlerAdapter; @@ -153,6 +155,25 @@ public class DelegatingWebFluxConfigurationTests { verify(webFluxConfigurer).configureContentTypeResolver(any(RequestedContentTypeResolverBuilder.class)); } + @Test + void addErrorResponseInterceptors() { + ErrorResponse.Interceptor interceptor = (detail, errorResponse) -> {}; + WebFluxConfigurer configurer = new WebFluxConfigurer() { + @Override + public void addErrorResponseInterceptors(List interceptors) { + interceptors.add(interceptor); + } + }; + delegatingConfig.setConfigurers(Collections.singletonList(configurer)); + + ResponseBodyResultHandler resultHandler = delegatingConfig.responseBodyResultHandler( + delegatingConfig.webFluxAdapterRegistry(), + delegatingConfig.serverCodecConfigurer(), + delegatingConfig.webFluxContentTypeResolver()); + + assertThat(resultHandler.getErrorResponseInterceptors()).containsExactly(interceptor); + } + @Test void viewResolutionResultHandler() { delegatingConfig.setConfigurers(Collections.singletonList(webFluxConfigurer)); diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/DelegatingWebMvcConfiguration.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/DelegatingWebMvcConfiguration.java index 5970d26c75..457ce6b291 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/DelegatingWebMvcConfiguration.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/DelegatingWebMvcConfiguration.java @@ -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 interceptors) { + this.configurers.addErrorResponseInterceptors(interceptors); + } + @Override @Nullable protected Validator getValidator() { diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupport.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupport.java index 0f210c42c5..f78779465a 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupport.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupport.java @@ -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> messageConverters; + @Nullable + private List errorResponseInterceptors; + @Nullable private Map 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. + *

This method cannot be overridden; use {@link #configureErrorResponseInterceptors(List)} instead. + * @since 6.2 + */ + protected final List 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 interceptors) { + } + /** * Register a {@link ViewResolverComposite} that contains a chain of view resolvers * to use for view resolution. diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurer.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurer.java index eb329f47e0..97bacaa1eb 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurer.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurer.java @@ -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 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 interceptors) { + } + /** * Provide a custom {@link Validator} instead of the one created by default. * The default implementation, assuming JSR-303 is on the classpath, is: diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurerComposite.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurerComposite.java index d8680e1578..7effc268cf 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurerComposite.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurerComposite.java @@ -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 interceptors) { + for (WebMvcConfigurer delegate : this.delegates) { + delegate.addErrorResponseInterceptors(interceptors); + } + } + @Override public Validator getValidator() { Validator selected = null; diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/AbstractMessageConverterMethodProcessor.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/AbstractMessageConverterMethodProcessor.java index 3bf4dafbb2..d160ef9d6a 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/AbstractMessageConverterMethodProcessor.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/AbstractMessageConverterMethodProcessor.java @@ -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 problemMediaTypes = Arrays.asList(MediaType.APPLICATION_PROBLEM_JSON, MediaType.APPLICATION_PROBLEM_XML); + private final List errorResponseInterceptors = new ArrayList<>(); + private final Set 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> converters, @Nullable ContentNegotiationManager manager, @Nullable List 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> converters, + @Nullable ContentNegotiationManager manager, @Nullable List requestResponseBodyAdvice, + List 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)} diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ExceptionHandlerExceptionResolver.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ExceptionHandlerExceptionResolver.java index a441082126..ccccc43a4f 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ExceptionHandlerExceptionResolver.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ExceptionHandlerExceptionResolver.java @@ -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 responseBodyAdvice = new ArrayList<>(); + private final List 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 interceptors) { + this.errorResponseInterceptors.clear(); + this.errorResponseInterceptors.addAll(interceptors); + } + + /** + * Return the {@link #setErrorResponseInterceptors(List) configured} + * {@link ErrorResponse.Interceptor}'s. + * @since 6.2 + */ + public List 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()); diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/HttpEntityMethodProcessor.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/HttpEntityMethodProcessor.java index 28f8b7ec5c..fda6f3adbb 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/HttpEntityMethodProcessor.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/HttpEntityMethodProcessor.java @@ -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> converters, @Nullable ContentNegotiationManager manager, List 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> converters, + @Nullable ContentNegotiationManager manager, List requestResponseBodyAdvice, + List 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(); diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerAdapter.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerAdapter.java index 17776f3fe9..b43aaeb074 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerAdapter.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerAdapter.java @@ -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 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 interceptors) { + this.errorResponseInterceptors.clear(); + this.errorResponseInterceptors.addAll(interceptors); + } + + /** + * Return the {@link #setErrorResponseInterceptors(List) configured} + * {@link ErrorResponse.Interceptor}'s. + * @since 6.2 + */ + public List 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()); diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestResponseBodyMethodProcessor.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestResponseBodyMethodProcessor.java index 064c7cb321..a30f5e657d 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestResponseBodyMethodProcessor.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestResponseBodyMethodProcessor.java @@ -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> converters, @Nullable ContentNegotiationManager manager, @Nullable List 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> converters, + @Nullable ContentNegotiationManager manager, List requestResponseBodyAdvice, + List 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. diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/DelegatingWebMvcConfigurationTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/DelegatingWebMvcConfigurationTests.java index 3958eae2d3..1027d9e4c9 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/DelegatingWebMvcConfigurationTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/DelegatingWebMvcConfigurationTests.java @@ -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 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