Skip to content

Commit 38d5c0f

Browse files
committed
Add RFC-7807 response interception
Closes gh-31822
1 parent 3577e3b commit 38d5c0f

File tree

21 files changed

+407
-30
lines changed

21 files changed

+407
-30
lines changed

Diff for: framework-docs/modules/ROOT/pages/web/webflux/ann-rest-exceptions.adoc

+4
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,10 @@ has an `@ExceptionHandler` method that handles any `ErrorResponse` exception, wh
4444
includes all built-in web exceptions. You can add more exception handling methods, and
4545
use a protected method to map any exception to a `ProblemDetail`.
4646

47+
You can register `ErrorResponse` interceptors through the
48+
xref:web/webflux/config.adoc[WebFlux Config] with a `WebFluxConfigurer`. Use that to intercept
49+
any RFC 7807 response and take some action.
50+
4751

4852

4953
[[webflux-ann-rest-exceptions-non-standard]]

Diff for: framework-docs/modules/ROOT/pages/web/webmvc/mvc-ann-rest-exceptions.adoc

+4
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,10 @@ has an `@ExceptionHandler` method that handles any `ErrorResponse` exception, wh
4444
includes all built-in web exceptions. You can add more exception handling methods, and
4545
use a protected method to map any exception to a `ProblemDetail`.
4646

47+
You can register `ErrorResponse` interceptors through the
48+
xref:web/webmvc/mvc-config.adoc[MVC Config] with a `WebMvcConfigurer`. Use that to intercept
49+
any RFC 7807 response and take some action.
50+
4751

4852

4953
[[mvc-ann-rest-exceptions-non-standard]]

Diff for: spring-web/src/main/java/org/springframework/web/ErrorResponse.java

+21-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2023 the original author or authors.
2+
* Copyright 2002-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -333,4 +333,24 @@ default ErrorResponse build(@Nullable MessageSource messageSource, Locale locale
333333

334334
}
335335

336+
337+
/**
338+
* Callback to perform an action before an RFC-7807 {@link ProblemDetail}
339+
* response is rendered.
340+
*
341+
* @author Rossen Stoyanchev
342+
* @since 6.2
343+
*/
344+
interface Interceptor {
345+
346+
/**
347+
* Handle the given {@code ProblemDetail} that's going to be rendered,
348+
* and the {@code ErrorResponse} it originates from, if applicable.
349+
* @param detail the {@code ProblemDetail} to be rendered
350+
* @param errorResponse the {@code ErrorResponse}, or {@code null} if there isn't one
351+
*/
352+
void handleError(ProblemDetail detail, @Nullable ErrorResponse errorResponse);
353+
354+
}
355+
336356
}

Diff for: spring-webflux/src/main/java/org/springframework/web/reactive/config/DelegatingWebFluxConfiguration.java

+8-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2023 the original author or authors.
2+
* Copyright 2002-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -25,6 +25,7 @@
2525
import org.springframework.util.CollectionUtils;
2626
import org.springframework.validation.MessageCodesResolver;
2727
import org.springframework.validation.Validator;
28+
import org.springframework.web.ErrorResponse;
2829
import org.springframework.web.reactive.accept.RequestedContentTypeResolverBuilder;
2930
import org.springframework.web.reactive.result.method.annotation.ArgumentResolverConfigurer;
3031
import org.springframework.web.reactive.socket.server.WebSocketService;
@@ -99,6 +100,12 @@ protected void configureArgumentResolvers(ArgumentResolverConfigurer configurer)
99100
this.configurers.configureArgumentResolvers(configurer);
100101
}
101102

103+
@Override
104+
protected void configureErrorResponseInterceptors(List<ErrorResponse.Interceptor> interceptors) {
105+
this.configurers.addErrorResponseInterceptors(interceptors);
106+
}
107+
108+
102109
@Override
103110
protected void addResourceHandlers(ResourceHandlerRegistry registry) {
104111
this.configurers.addResourceHandlers(registry);

Diff for: spring-webflux/src/main/java/org/springframework/web/reactive/config/WebFluxConfigurationSupport.java

+31-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2023 the original author or authors.
2+
* Copyright 2002-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -16,6 +16,7 @@
1616

1717
package org.springframework.web.reactive.config;
1818

19+
import java.util.ArrayList;
1920
import java.util.List;
2021
import java.util.Map;
2122
import java.util.function.Predicate;
@@ -44,6 +45,7 @@
4445
import org.springframework.validation.MessageCodesResolver;
4546
import org.springframework.validation.Validator;
4647
import org.springframework.validation.beanvalidation.OptionalValidatorFactoryBean;
48+
import org.springframework.web.ErrorResponse;
4749
import org.springframework.web.bind.WebDataBinder;
4850
import org.springframework.web.bind.support.ConfigurableWebBindingInitializer;
4951
import org.springframework.web.cors.CorsConfiguration;
@@ -98,6 +100,9 @@ public class WebFluxConfigurationSupport implements ApplicationContextAware {
98100
@Nullable
99101
private BlockingExecutionConfigurer blockingExecutionConfigurer;
100102

103+
@Nullable
104+
private List<ErrorResponse.Interceptor> errorResponseInterceptors;
105+
101106
@Nullable
102107
private ViewResolverRegistry viewResolverRegistry;
103108

@@ -498,7 +503,7 @@ public ResponseEntityResultHandler responseEntityResultHandler(
498503
@Qualifier("webFluxContentTypeResolver") RequestedContentTypeResolver contentTypeResolver) {
499504

500505
return new ResponseEntityResultHandler(serverCodecConfigurer.getWriters(),
501-
contentTypeResolver, reactiveAdapterRegistry);
506+
contentTypeResolver, reactiveAdapterRegistry, getErrorResponseInterceptors());
502507
}
503508

504509
@Bean
@@ -508,7 +513,7 @@ public ResponseBodyResultHandler responseBodyResultHandler(
508513
@Qualifier("webFluxContentTypeResolver") RequestedContentTypeResolver contentTypeResolver) {
509514

510515
return new ResponseBodyResultHandler(serverCodecConfigurer.getWriters(),
511-
contentTypeResolver, reactiveAdapterRegistry);
516+
contentTypeResolver, reactiveAdapterRegistry, getErrorResponseInterceptors());
512517
}
513518

514519
@Bean
@@ -534,6 +539,29 @@ public ServerResponseResultHandler serverResponseResultHandler(ServerCodecConfig
534539
return handler;
535540
}
536541

542+
/**
543+
* Provide access to the list of {@link ErrorResponse.Interceptor}'s to apply
544+
* in result handlers when rendering error responses.
545+
* <p>This method cannot be overridden; use {@link #configureErrorResponseInterceptors(List)} instead.
546+
* @since 6.2
547+
*/
548+
protected final List<ErrorResponse.Interceptor> getErrorResponseInterceptors() {
549+
if (this.errorResponseInterceptors == null) {
550+
this.errorResponseInterceptors = new ArrayList<>();
551+
configureErrorResponseInterceptors(this.errorResponseInterceptors);
552+
}
553+
return this.errorResponseInterceptors;
554+
}
555+
556+
/**
557+
* Override this method for control over the {@link ErrorResponse.Interceptor}'s
558+
* to apply in result handling when rendering error responses.
559+
* @param interceptors the list to add handlers to
560+
* @since 6.2
561+
*/
562+
protected void configureErrorResponseInterceptors(List<ErrorResponse.Interceptor> interceptors) {
563+
}
564+
537565
/**
538566
* Callback for building the {@link ViewResolverRegistry}. This method is final,
539567
* use {@link #configureViewResolvers} to customize view resolvers.

Diff for: spring-webflux/src/main/java/org/springframework/web/reactive/config/WebFluxConfigurer.java

+14-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2023 the original author or authors.
2+
* Copyright 2002-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -16,13 +16,16 @@
1616

1717
package org.springframework.web.reactive.config;
1818

19+
import java.util.List;
20+
1921
import org.springframework.core.convert.converter.Converter;
2022
import org.springframework.format.Formatter;
2123
import org.springframework.format.FormatterRegistry;
2224
import org.springframework.http.codec.ServerCodecConfigurer;
2325
import org.springframework.lang.Nullable;
2426
import org.springframework.validation.MessageCodesResolver;
2527
import org.springframework.validation.Validator;
28+
import org.springframework.web.ErrorResponse;
2629
import org.springframework.web.cors.CorsConfiguration;
2730
import org.springframework.web.reactive.accept.RequestedContentTypeResolverBuilder;
2831
import org.springframework.web.reactive.result.method.annotation.ArgumentResolverConfigurer;
@@ -133,6 +136,16 @@ default void configurePathMatching(PathMatchConfigurer configurer) {
133136
default void configureArgumentResolvers(ArgumentResolverConfigurer configurer) {
134137
}
135138

139+
/**
140+
* Add to the list of {@link ErrorResponse.Interceptor}'s to invoke when
141+
* rendering an RFC 7807 {@link org.springframework.http.ProblemDetail}
142+
* error response.
143+
* @param interceptors the handlers to use
144+
* @since 6.2
145+
*/
146+
default void addErrorResponseInterceptors(List<ErrorResponse.Interceptor> interceptors) {
147+
}
148+
136149
/**
137150
* Configure view resolution for rendering responses with a view and a model,
138151
* where the view is typically an HTML template but could also be based on

Diff for: spring-webflux/src/main/java/org/springframework/web/reactive/config/WebFluxConfigurerComposite.java

+9-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2023 the original author or authors.
2+
* Copyright 2002-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -27,6 +27,7 @@
2727
import org.springframework.util.CollectionUtils;
2828
import org.springframework.validation.MessageCodesResolver;
2929
import org.springframework.validation.Validator;
30+
import org.springframework.web.ErrorResponse;
3031
import org.springframework.web.reactive.accept.RequestedContentTypeResolverBuilder;
3132
import org.springframework.web.reactive.result.method.annotation.ArgumentResolverConfigurer;
3233
import org.springframework.web.reactive.socket.server.WebSocketService;
@@ -95,6 +96,13 @@ public void configureArgumentResolvers(ArgumentResolverConfigurer configurer) {
9596
this.delegates.forEach(delegate -> delegate.configureArgumentResolvers(configurer));
9697
}
9798

99+
@Override
100+
public void addErrorResponseInterceptors(List<ErrorResponse.Interceptor> interceptors) {
101+
for (WebFluxConfigurer delegate : this.delegates) {
102+
delegate.addErrorResponseInterceptors(interceptors);
103+
}
104+
}
105+
98106
@Override
99107
public void configureViewResolvers(ViewResolverRegistry registry) {
100108
this.delegates.forEach(delegate -> delegate.configureViewResolvers(registry));

Diff for: spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractMessageWriterResultHandler.java

+42
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
import java.util.ArrayList;
2020
import java.util.Arrays;
21+
import java.util.Collections;
2122
import java.util.List;
2223
import java.util.Set;
2324

@@ -40,6 +41,7 @@
4041
import org.springframework.util.Assert;
4142
import org.springframework.util.ClassUtils;
4243
import org.springframework.util.CollectionUtils;
44+
import org.springframework.web.ErrorResponse;
4345
import org.springframework.web.reactive.HandlerMapping;
4446
import org.springframework.web.reactive.accept.RequestedContentTypeResolver;
4547
import org.springframework.web.reactive.result.HandlerResultHandlerSupport;
@@ -60,6 +62,8 @@ public abstract class AbstractMessageWriterResultHandler extends HandlerResultHa
6062

6163
private final List<HttpMessageWriter<?>> messageWriters;
6264

65+
private final List<ErrorResponse.Interceptor> errorResponseInterceptors = new ArrayList<>();
66+
6367
private final List<MediaType> problemMediaTypes =
6468
Arrays.asList(MediaType.APPLICATION_PROBLEM_JSON, MediaType.APPLICATION_PROBLEM_XML);
6569

@@ -86,9 +90,24 @@ protected AbstractMessageWriterResultHandler(List<HttpMessageWriter<?>> messageW
8690
protected AbstractMessageWriterResultHandler(List<HttpMessageWriter<?>> messageWriters,
8791
RequestedContentTypeResolver contentTypeResolver, ReactiveAdapterRegistry adapterRegistry) {
8892

93+
this(messageWriters, contentTypeResolver, adapterRegistry, Collections.emptyList());
94+
}
95+
96+
/**
97+
* Variant of
98+
* {@link #AbstractMessageWriterResultHandler(List, RequestedContentTypeResolver, ReactiveAdapterRegistry)}
99+
* with additional list of {@link ErrorResponse.Interceptor}s for return
100+
* value handling.
101+
* @since 6.2
102+
*/
103+
protected AbstractMessageWriterResultHandler(List<HttpMessageWriter<?>> messageWriters,
104+
RequestedContentTypeResolver contentTypeResolver, ReactiveAdapterRegistry adapterRegistry,
105+
List<ErrorResponse.Interceptor> interceptors) {
106+
89107
super(contentTypeResolver, adapterRegistry);
90108
Assert.notEmpty(messageWriters, "At least one message writer is required");
91109
this.messageWriters = messageWriters;
110+
this.errorResponseInterceptors.addAll(interceptors);
92111
}
93112

94113

@@ -99,6 +118,29 @@ public List<HttpMessageWriter<?>> getMessageWriters() {
99118
return this.messageWriters;
100119
}
101120

121+
/**
122+
* Return the configured {@link ErrorResponse.Interceptor}'s.
123+
* @since 6.2
124+
*/
125+
public List<ErrorResponse.Interceptor> getErrorResponseInterceptors() {
126+
return this.errorResponseInterceptors;
127+
}
128+
129+
130+
/**
131+
* Invoke the configured {@link ErrorResponse.Interceptor}'s.
132+
* @since 6.2
133+
*/
134+
protected void invokeErrorResponseInterceptors(ProblemDetail detail, @Nullable ErrorResponse errorResponse) {
135+
try {
136+
for (ErrorResponse.Interceptor handler : this.errorResponseInterceptors) {
137+
handler.handleError(detail, errorResponse);
138+
}
139+
}
140+
catch (Throwable ex) {
141+
// ignore
142+
}
143+
}
102144

103145
/**
104146
* Write a given body to the response with {@link HttpMessageWriter}.

Diff for: spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/ResponseBodyResultHandler.java

+19-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2022 the original author or authors.
2+
* Copyright 2002-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -17,6 +17,7 @@
1717
package org.springframework.web.reactive.result.method.annotation;
1818

1919
import java.net.URI;
20+
import java.util.Collections;
2021
import java.util.List;
2122

2223
import reactor.core.publisher.Mono;
@@ -27,6 +28,7 @@
2728
import org.springframework.http.HttpStatusCode;
2829
import org.springframework.http.ProblemDetail;
2930
import org.springframework.http.codec.HttpMessageWriter;
31+
import org.springframework.web.ErrorResponse;
3032
import org.springframework.web.bind.annotation.ResponseBody;
3133
import org.springframework.web.reactive.HandlerResult;
3234
import org.springframework.web.reactive.HandlerResultHandler;
@@ -69,7 +71,21 @@ public ResponseBodyResultHandler(List<HttpMessageWriter<?>> writers, RequestedCo
6971
public ResponseBodyResultHandler(List<HttpMessageWriter<?>> writers,
7072
RequestedContentTypeResolver resolver, ReactiveAdapterRegistry registry) {
7173

72-
super(writers, resolver, registry);
74+
this(writers, resolver, registry, Collections.emptyList());
75+
}
76+
77+
/**
78+
* Variant of
79+
* {@link #ResponseBodyResultHandler(List, RequestedContentTypeResolver, ReactiveAdapterRegistry)}
80+
* with additional list of {@link ErrorResponse.Interceptor}s for return
81+
* value handling.
82+
* @since 6.2
83+
*/
84+
public ResponseBodyResultHandler(List<HttpMessageWriter<?>> writers,
85+
RequestedContentTypeResolver resolver, ReactiveAdapterRegistry registry,
86+
List<ErrorResponse.Interceptor> interceptors) {
87+
88+
super(writers, resolver, registry, interceptors);
7389
setOrder(100);
7490
}
7591

@@ -92,6 +108,7 @@ public Mono<Void> handleResult(ServerWebExchange exchange, HandlerResult result)
92108
URI path = URI.create(exchange.getRequest().getPath().value());
93109
detail.setInstance(path);
94110
}
111+
invokeErrorResponseInterceptors(detail, null);
95112
}
96113
return writeBody(body, bodyTypeParameter, exchange);
97114
}

0 commit comments

Comments
 (0)