Add RFC-7807 response interception

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

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-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<ErrorResponse.Interceptor> interceptors) {
this.configurers.addErrorResponseInterceptors(interceptors);
}
@Override
protected void addResourceHandlers(ResourceHandlerRegistry registry) {
this.configurers.addResourceHandlers(registry);

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -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<ErrorResponse.Interceptor> 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.
* <p>This method cannot be overridden; use {@link #configureErrorResponseInterceptors(List)} instead.
* @since 6.2
*/
protected final List<ErrorResponse.Interceptor> getErrorResponseInterceptors() {
if (this.errorResponseInterceptors == null) {
this.errorResponseInterceptors = new ArrayList<>();
configureErrorResponseInterceptors(this.errorResponseInterceptors);
}
return this.errorResponseInterceptors;
}
/**
* Override this method for control over the {@link ErrorResponse.Interceptor}'s
* to apply in result handling when rendering error responses.
* @param interceptors the list to add handlers to
* @since 6.2
*/
protected void configureErrorResponseInterceptors(List<ErrorResponse.Interceptor> interceptors) {
}
/**
* Callback for building the {@link ViewResolverRegistry}. This method is final,
* use {@link #configureViewResolvers} to customize view resolvers.

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -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<ErrorResponse.Interceptor> 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

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -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<ErrorResponse.Interceptor> interceptors) {
for (WebFluxConfigurer delegate : this.delegates) {
delegate.addErrorResponseInterceptors(interceptors);
}
}
@Override
public void configureViewResolvers(ViewResolverRegistry registry) {
this.delegates.forEach(delegate -> delegate.configureViewResolvers(registry));

View File

@@ -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<HttpMessageWriter<?>> messageWriters;
private final List<ErrorResponse.Interceptor> errorResponseInterceptors = new ArrayList<>();
private final List<MediaType> problemMediaTypes =
Arrays.asList(MediaType.APPLICATION_PROBLEM_JSON, MediaType.APPLICATION_PROBLEM_XML);
@@ -86,9 +90,24 @@ public abstract class AbstractMessageWriterResultHandler extends HandlerResultHa
protected AbstractMessageWriterResultHandler(List<HttpMessageWriter<?>> 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<HttpMessageWriter<?>> messageWriters,
RequestedContentTypeResolver contentTypeResolver, ReactiveAdapterRegistry adapterRegistry,
List<ErrorResponse.Interceptor> 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<ErrorResponse.Interceptor> 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}.

View File

@@ -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<HttpMessageWriter<?>> 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<HttpMessageWriter<?>> writers,
RequestedContentTypeResolver resolver, ReactiveAdapterRegistry registry,
List<ErrorResponse.Interceptor> 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);
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -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<HttpMessageWriter<?>> 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<HttpMessageWriter<?>> writers,
RequestedContentTypeResolver resolver, ReactiveAdapterRegistry registry,
List<ErrorResponse.Interceptor> 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) {

View File

@@ -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<ErrorResponse.Interceptor> 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));