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 c83bcdb1c4b8..c293b02c0260 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 @@ default ErrorResponse build(@Nullable MessageSource messageSource, Locale locale } + + /** + * Callback to perform an action before an RFC-7807 {@link ProblemDetail} + * response is rendered. + * + * @author Rossen Stoyanchev + * @since 6.2 + */ + interface Interceptor { + + /** + * Handle the {@code ProblemDetail} to be rendered along with a full + * {@code ErrorResponse} if used for rendering. + * @param detail the {@code ProblemDetail} that will be rendered + * @param errorResponse the full {@code ErrorResponse} if available + */ + 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 de6d56565b93..5a49fa247eef 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.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 @@ protected void configureArgumentResolvers(ArgumentResolverConfigurer configurer) 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 ac71dc84675d..ae451e4d753b 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.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 ResponseEntityResultHandler responseEntityResultHandler( @Qualifier("webFluxContentTypeResolver") RequestedContentTypeResolver contentTypeResolver) { return new ResponseEntityResultHandler(serverCodecConfigurer.getWriters(), - contentTypeResolver, reactiveAdapterRegistry); + contentTypeResolver, reactiveAdapterRegistry, getErrorResponseInterceptors()); } @Bean @@ -508,7 +513,7 @@ public ResponseBodyResultHandler responseBodyResultHandler( @Qualifier("webFluxContentTypeResolver") RequestedContentTypeResolver contentTypeResolver) { return new ResponseBodyResultHandler(serverCodecConfigurer.getWriters(), - contentTypeResolver, reactiveAdapterRegistry); + contentTypeResolver, reactiveAdapterRegistry, getErrorResponseInterceptors()); } @Bean @@ -534,6 +539,29 @@ public ServerResponseResultHandler serverResponseResultHandler(ServerCodecConfig 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 a89c48131473..408077cef5a3 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.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 @@ default void configurePathMatching(PathMatchConfigurer configurer) { 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 b28810f95d31..7d37cad16b20 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.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 void configureArgumentResolvers(ArgumentResolverConfigurer configurer) { 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 b7e7339948b2..94fbad062393 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 @@ -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 @@ import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.List; import java.util.Set; @@ -39,6 +40,7 @@ import org.springframework.lang.Nullable; import org.springframework.util.Assert; 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; @@ -59,6 +61,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); @@ -85,9 +89,24 @@ protected AbstractMessageWriterResultHandler(List> messageW 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); } @@ -98,6 +117,29 @@ public List> getMessageWriters() { 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 be977767f128..aa4eb2b6899d 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.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 ResponseBodyResultHandler(List> writers, RequestedCo 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 Mono handleResult(ServerWebExchange exchange, HandlerResult result) 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 ea158774a0ae..ea0bccc79f13 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 @@ 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 ResponseEntityResultHandler(List> writers, 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 @@ else if (returnValue instanceof HttpHeaders headers) { " 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 2b14bfb72d9f..2cc6f6e720cc 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 @@ -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. @@ -35,8 +35,10 @@ 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 void responseBodyResultHandler() { verify(webFluxConfigurer).configureContentTypeResolver(any(RequestedContentTypeResolverBuilder.class)); } + @Test + public 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 public 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 5970d26c7563..457ce6b291e1 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.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 @@ protected void extendHandlerExceptionResolvers(List ex 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 0f210c42c558..aa36e781382e 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.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; @@ -1053,6 +1057,7 @@ protected final void addDefaultHandlerExceptionResolvers(ListThis 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 eb329f47e0da..97bacaa1ebb9 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.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 @@ default void configureHandlerExceptionResolvers(List r 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 d8680e1578d4..7effc268cf6a 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.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 @@ public void extendHandlerExceptionResolvers(List excep } } + @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 3bf4dafbb201..d160ef9d6a22 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.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 @@ protected AbstractMessageConverterMethodProcessor(List> } /** - * 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 @@ protected ServletServerHttpResponse createOutputMessage(NativeWebRequest webRequ 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 3e5d1f0dd8ac..2bcf7303d985 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.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 void setResponseBodyAdvice(@Nullable List> response } } + /** + * 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 @@ protected List getDefaultReturnValueHandlers() 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 28f8b7ec5c0c..fda6f3adbbda 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 HttpEntityMethodProcessor(List> converters, } /** - * 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 HttpEntityMethodProcessor(List> converters, 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 @@ else if (returnValue instanceof ProblemDetail detail) { " 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 36c8013c4ca8..09d0b51bc476 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 @@ -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. @@ -54,6 +54,7 @@ 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; @@ -165,6 +166,8 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter @Nullable private WebBindingInitializer webBindingInitializer; + private final List errorResponseInterceptors = new ArrayList<>(); + @Nullable private MethodValidator methodValidator; @@ -394,6 +397,27 @@ public WebBindingInitializer getWebBindingInitializer() { 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 @@ -745,7 +769,7 @@ private List getDefaultReturnValueHandlers() { 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()); @@ -754,7 +778,7 @@ private List getDefaultReturnValueHandlers() { // 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 064c7cb3215c..a30f5e657d34 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.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 RequestResponseBodyMethodProcessor(List> converte } /** - * 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 RequestResponseBodyMethodProcessor(List> converte 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 void handleReturnValue(@Nullable Object returnValue, MethodParameter retu 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 44e38864cba3..ef1005ba3b26 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 @@ -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. @@ -33,6 +33,7 @@ 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,28 @@ public void configureHandlerExceptionResolvers(List re (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)); + + HandlerExceptionResolverComposite composite = + (HandlerExceptionResolverComposite) webMvcConfig + .handlerExceptionResolver(webMvcConfig.mvcContentNegotiationManager()); + + ExceptionHandlerExceptionResolver resolver = + (ExceptionHandlerExceptionResolver) composite.getExceptionResolvers().get(0); + + assertThat(resolver.getErrorResponseInterceptors()).containsExactly(interceptor); } @Test