Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add interception of RFC-7807 responses #31822

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -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);

}

}
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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;
Expand Down Expand Up @@ -99,6 +100,12 @@ protected void configureArgumentResolvers(ArgumentResolverConfigurer configurer)
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);
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -98,6 +100,9 @@ public class WebFluxConfigurationSupport implements ApplicationContextAware {
@Nullable
private BlockingExecutionConfigurer blockingExecutionConfigurer;

@Nullable
private List<ErrorResponse.Interceptor> errorResponseInterceptors;

@Nullable
private ViewResolverRegistry viewResolverRegistry;

Expand Down Expand Up @@ -498,7 +503,7 @@ public ResponseEntityResultHandler responseEntityResultHandler(
@Qualifier("webFluxContentTypeResolver") RequestedContentTypeResolver contentTypeResolver) {

return new ResponseEntityResultHandler(serverCodecConfigurer.getWriters(),
contentTypeResolver, reactiveAdapterRegistry);
contentTypeResolver, reactiveAdapterRegistry, getErrorResponseInterceptors());
}

@Bean
Expand All @@ -508,7 +513,7 @@ public ResponseBodyResultHandler responseBodyResultHandler(
@Qualifier("webFluxContentTypeResolver") RequestedContentTypeResolver contentTypeResolver) {

return new ResponseBodyResultHandler(serverCodecConfigurer.getWriters(),
contentTypeResolver, reactiveAdapterRegistry);
contentTypeResolver, reactiveAdapterRegistry, getErrorResponseInterceptors());
}

@Bean
Expand All @@ -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.
* <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.
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -16,13 +16,16 @@

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;
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;
Expand Down Expand Up @@ -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<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
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -95,6 +96,13 @@ public void configureArgumentResolvers(ArgumentResolverConfigurer configurer) {
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));
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -18,6 +18,7 @@

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Set;

Expand All @@ -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;
Expand All @@ -59,6 +61,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);

Expand All @@ -85,9 +89,24 @@ protected AbstractMessageWriterResultHandler(List<HttpMessageWriter<?>> messageW
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);
}


Expand All @@ -98,6 +117,29 @@ public List<HttpMessageWriter<?>> getMessageWriters() {
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}.
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -69,7 +71,21 @@ public ResponseBodyResultHandler(List<HttpMessageWriter<?>> writers, RequestedCo
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);
}

Expand All @@ -92,6 +108,7 @@ public Mono<Void> handleResult(ServerWebExchange exchange, HandlerResult result)
URI path = URI.create(exchange.getRequest().getPath().value());
detail.setInstance(path);
}
invokeErrorResponseInterceptors(detail, null);
}
return writeBody(body, bodyTypeParameter, exchange);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -18,6 +18,7 @@

import java.net.URI;
import java.time.Instant;
import java.util.Collections;
import java.util.List;
import java.util.Set;

Expand Down Expand Up @@ -78,7 +79,20 @@ public ResponseEntityResultHandler(List<HttpMessageWriter<?>> writers,
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);
}

Expand Down Expand Up @@ -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) {
Expand Down
Loading