Browse Source

Adds BeforeFilterFunctions.fallbackHeaders()

Adds request attribute in CircuitBreakerFilterFunctions.circuitBreaker()
for use in fallbackHeaders().

See gh-2949
pull/3006/head
sgibb 2 years ago
parent
commit
7b0184ec97
No known key found for this signature in database
GPG Key ID: 7788A47380690861
  1. 5
      spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/common/MvcUtils.java
  2. 3
      spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/filter/AfterFilterFunctions.java
  3. 106
      spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/filter/BeforeFilterFunctions.java
  4. 4
      spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/filter/CircuitBreakerFilterFunctions.java
  5. 12
      spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/filter/FilterFunctions.java
  6. 43
      spring-cloud-gateway-server-mvc/src/test/java/org/springframework/cloud/gateway/server/mvc/ServerMvcIntegrationTests.java

5
spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/common/MvcUtils.java

@ -35,6 +35,11 @@ import static org.springframework.web.servlet.function.RouterFunctions.URI_TEMPL @@ -35,6 +35,11 @@ import static org.springframework.web.servlet.function.RouterFunctions.URI_TEMPL
// TODO: maybe rename to ServerRequestUtils?
public abstract class MvcUtils {
/**
* CircuitBreaker execution exception attribute name.
*/
public static final String CIRCUITBREAKER_EXECUTION_EXCEPTION_ATTR = qualify("circuitBreakerExecutionException");
/**
* Gateway request URL attribute name.
*/

3
spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/filter/AfterFilterFunctions.java

@ -100,7 +100,8 @@ public abstract class AfterFilterFunctions { @@ -100,7 +100,8 @@ public abstract class AfterFilterFunctions {
}
public static BiFunction<ServerRequest, ServerResponse, ServerResponse> rewriteLocationResponseHeader() {
return RewriteLocationResponseHeaderFilterFunctions.rewriteLocationResponseHeader(config -> {});
return RewriteLocationResponseHeaderFilterFunctions.rewriteLocationResponseHeader(config -> {
});
}
public static BiFunction<ServerRequest, ServerResponse, ServerResponse> rewriteLocationResponseHeader(

106
spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/filter/BeforeFilterFunctions.java

@ -18,10 +18,12 @@ package org.springframework.cloud.gateway.server.mvc.filter; @@ -18,10 +18,12 @@ package org.springframework.cloud.gateway.server.mvc.filter;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.regex.Pattern;
@ -44,6 +46,7 @@ import org.springframework.web.servlet.function.ServerRequest; @@ -44,6 +46,7 @@ import org.springframework.web.servlet.function.ServerRequest;
import org.springframework.web.util.UriComponentsBuilder;
import org.springframework.web.util.UriTemplate;
import static org.springframework.cloud.gateway.server.mvc.common.MvcUtils.CIRCUITBREAKER_EXECUTION_EXCEPTION_ATTR;
import static org.springframework.util.CollectionUtils.unmodifiableMultiValueMap;
// TODO: can TokenRelay be here and not cause CNFE?
@ -57,6 +60,26 @@ public abstract class BeforeFilterFunctions { @@ -57,6 +60,26 @@ public abstract class BeforeFilterFunctions {
private static final String REQUEST_SIZE_ERROR_MSG = "Request size is larger than permissible limit. Request size is %s where permissible limit is %s";
/**
* Default CircuitBreaker Fallback Execution Exception Type header name.
*/
public static final String CB_EXECUTION_EXCEPTION_TYPE = "Execution-Exception-Type";
/**
* Default CircuitBreaker Fallback Execution Exception Message header name.
*/
public static final String CB_EXECUTION_EXCEPTION_MESSAGE = "Execution-Exception-Message";
/**
* Default CircuitBreaker Root Cause Execution Exception Type header name.
*/
public static final String CB_ROOT_CAUSE_EXCEPTION_TYPE = "Root-Cause-Exception-Type";
/**
* Default CircuitBreaker Root Cause Execution Exception Message header name.
*/
public static final String CB_ROOT_CAUSE_EXCEPTION_MESSAGE = "Root-Cause-Exception-Message";
private BeforeFilterFunctions() {
}
@ -98,6 +121,45 @@ public abstract class BeforeFilterFunctions { @@ -98,6 +121,45 @@ public abstract class BeforeFilterFunctions {
};
}
public static Function<ServerRequest, ServerRequest> fallbackHeaders() {
return fallbackHeaders(config -> {
});
}
public static Function<ServerRequest, ServerRequest> fallbackHeaders(
Consumer<FallbackHeadersConfig> configConsumer) {
FallbackHeadersConfig config = new FallbackHeadersConfig();
configConsumer.accept(config);
return request -> request.attribute(CIRCUITBREAKER_EXECUTION_EXCEPTION_ATTR).map(Throwable.class::cast)
.map(throwable -> ServerRequest.from(request).headers(httpHeaders -> {
httpHeaders.add(config.getExecutionExceptionTypeHeaderName(), throwable.getClass().getName());
if (throwable.getMessage() != null) {
httpHeaders.add(config.getExecutionExceptionMessageHeaderName(), throwable.getMessage());
}
Throwable rootCause = getRootCause(throwable);
if (rootCause != null) {
httpHeaders.add(config.getRootCauseExceptionTypeHeaderName(), rootCause.getClass().getName());
if (rootCause.getMessage() != null) {
httpHeaders.add(config.getRootCauseExceptionMessageHeaderName(), rootCause.getMessage());
}
}
}).build()).orElse(request);
}
private static Throwable getRootCause(Throwable throwable) {
List<Throwable> list = getThrowableList(throwable);
return list.isEmpty() ? null : list.get(list.size() - 1);
}
private static List<Throwable> getThrowableList(Throwable throwable) {
List<Throwable> list = new ArrayList<>();
while (throwable != null && !list.contains(throwable)) {
list.add(throwable);
throwable = throwable.getCause();
}
return list;
}
public static Function<ServerRequest, ServerRequest> mapRequestHeader(String fromHeader, String toHeader) {
return request -> {
if (request.headers().asHttpHeaders().containsKey(fromHeader)) {
@ -324,4 +386,48 @@ public abstract class BeforeFilterFunctions { @@ -324,4 +386,48 @@ public abstract class BeforeFilterFunctions {
};
}
public static class FallbackHeadersConfig {
private String executionExceptionTypeHeaderName = CB_EXECUTION_EXCEPTION_TYPE;
private String executionExceptionMessageHeaderName = CB_EXECUTION_EXCEPTION_MESSAGE;
private String rootCauseExceptionTypeHeaderName = CB_ROOT_CAUSE_EXCEPTION_TYPE;
private String rootCauseExceptionMessageHeaderName = CB_ROOT_CAUSE_EXCEPTION_MESSAGE;
public String getExecutionExceptionTypeHeaderName() {
return executionExceptionTypeHeaderName;
}
public void setExecutionExceptionTypeHeaderName(String executionExceptionTypeHeaderName) {
this.executionExceptionTypeHeaderName = executionExceptionTypeHeaderName;
}
public String getExecutionExceptionMessageHeaderName() {
return executionExceptionMessageHeaderName;
}
public void setExecutionExceptionMessageHeaderName(String executionExceptionMessageHeaderName) {
this.executionExceptionMessageHeaderName = executionExceptionMessageHeaderName;
}
public String getRootCauseExceptionTypeHeaderName() {
return rootCauseExceptionTypeHeaderName;
}
public void setRootCauseExceptionTypeHeaderName(String rootCauseExceptionTypeHeaderName) {
this.rootCauseExceptionTypeHeaderName = rootCauseExceptionTypeHeaderName;
}
public String getRootCauseExceptionMessageHeaderName() {
return rootCauseExceptionMessageHeaderName;
}
public void setRootCauseExceptionMessageHeaderName(String rootCauseExceptionMessageHeaderName) {
this.rootCauseExceptionMessageHeaderName = rootCauseExceptionMessageHeaderName;
}
}
}

4
spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/filter/CircuitBreakerFilterFunctions.java

@ -95,6 +95,10 @@ public abstract class CircuitBreakerFilterFunctions { @@ -95,6 +95,10 @@ public abstract class CircuitBreakerFilterFunctions {
throwable);
}
// add the throwable as an attribute. That way, if the fallback is a different gateway
// route, it can use the fallbackHeaders() filter to convert it to headers.
request.attributes().put(MvcUtils.CIRCUITBREAKER_EXECUTION_EXCEPTION_ATTR, throwable);
// handle fallback
return GatewayServerResponse.ok().build((httpServletRequest, httpServletResponse) -> {
try {

12
spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/filter/FilterFunctions.java

@ -20,11 +20,13 @@ import java.lang.reflect.Method; @@ -20,11 +20,13 @@ import java.lang.reflect.Method;
import java.net.URI;
import java.util.Arrays;
import java.util.Collection;
import java.util.function.Consumer;
import org.springframework.cloud.gateway.server.mvc.common.HttpStatusHolder;
import org.springframework.cloud.gateway.server.mvc.common.KeyValues;
import org.springframework.cloud.gateway.server.mvc.common.Shortcut;
import org.springframework.cloud.gateway.server.mvc.filter.AfterFilterFunctions.DedupeStrategy;
import org.springframework.cloud.gateway.server.mvc.filter.BeforeFilterFunctions.FallbackHeadersConfig;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatusCode;
import org.springframework.util.Assert;
@ -72,6 +74,16 @@ public interface FilterFunctions { @@ -72,6 +74,16 @@ public interface FilterFunctions {
return ofResponseProcessor(AfterFilterFunctions.dedupeResponseHeader(name, strategy));
}
@Shortcut
static HandlerFilterFunction<ServerResponse, ServerResponse> fallbackHeaders() {
return ofRequestProcessor(BeforeFilterFunctions.fallbackHeaders());
}
static HandlerFilterFunction<ServerResponse, ServerResponse> fallbackHeaders(
Consumer<FallbackHeadersConfig> configConsumer) {
return ofRequestProcessor(BeforeFilterFunctions.fallbackHeaders(configConsumer));
}
@Shortcut
static HandlerFilterFunction<ServerResponse, ServerResponse> mapRequestHeader(String fromHeader, String toHeader) {
return ofRequestProcessor(BeforeFilterFunctions.mapRequestHeader(fromHeader, toHeader));

43
spring-cloud-gateway-server-mvc/src/test/java/org/springframework/cloud/gateway/server/mvc/ServerMvcIntegrationTests.java

@ -79,6 +79,9 @@ import static org.springframework.cloud.gateway.server.mvc.filter.AfterFilterFun @@ -79,6 +79,9 @@ import static org.springframework.cloud.gateway.server.mvc.filter.AfterFilterFun
import static org.springframework.cloud.gateway.server.mvc.filter.AfterFilterFunctions.rewriteResponseHeader;
import static org.springframework.cloud.gateway.server.mvc.filter.AfterFilterFunctions.setResponseHeader;
import static org.springframework.cloud.gateway.server.mvc.filter.AfterFilterFunctions.setStatus;
import static org.springframework.cloud.gateway.server.mvc.filter.BeforeFilterFunctions.CB_EXECUTION_EXCEPTION_MESSAGE;
import static org.springframework.cloud.gateway.server.mvc.filter.BeforeFilterFunctions.CB_EXECUTION_EXCEPTION_TYPE;
import static org.springframework.cloud.gateway.server.mvc.filter.BeforeFilterFunctions.fallbackHeaders;
import static org.springframework.cloud.gateway.server.mvc.filter.BeforeFilterFunctions.mapRequestHeader;
import static org.springframework.cloud.gateway.server.mvc.filter.BeforeFilterFunctions.preserveHost;
import static org.springframework.cloud.gateway.server.mvc.filter.BeforeFilterFunctions.removeRequestParameter;
@ -235,6 +238,15 @@ public class ServerMvcIntegrationTests { @@ -235,6 +238,15 @@ public class ServerMvcIntegrationTests {
.expectBody(String.class).isEqualTo("Hello");
}
@Test
public void circuitBreakerGatewayFallbackWorks() {
restClient.get().uri("/anything/circuitbreakergatewayfallback").exchange().expectStatus().isOk()
.expectBody(Map.class).consumeWith(res -> {
Map<String, Object> headers = getMap(res.getResponseBody(), "headers");
assertThat(headers).containsKeys(CB_EXECUTION_EXCEPTION_TYPE, CB_EXECUTION_EXCEPTION_MESSAGE);
});
}
@Test
public void circuitBreakerNoFallbackWorks() {
restClient.get().uri("/anything/circuitbreakernofallback").exchange().expectStatus()
@ -643,9 +655,25 @@ public class ServerMvcIntegrationTests { @@ -643,9 +655,25 @@ public class ServerMvcIntegrationTests {
@Bean
public RouterFunction<ServerResponse> gatewayRouterFunctionsCircuitBreakerFallback() {
// @formatter:off
return route(path("/anything/circuitbreakerfallback"), http(URI.create("https://nonexistantdomain.com1234")))
return route("testcircuitbreakerfallback")
.route(path("/anything/circuitbreakerfallback"), http(URI.create("https://nonexistantdomain.com1234")))
.filter(circuitBreaker("mycb1", "/hello"))
.withAttribute(MvcUtils.GATEWAY_ROUTE_ID_ATTR, "testcircuitbreakerfallback");
.build();
// @formatter:on
}
@Bean
public RouterFunction<ServerResponse> gatewayRouterFunctionsCircuitBreakerFallbackToGatewayRoute() {
// @formatter:off
return route("testcircuitbreakergatewayfallback")
.route(path("/anything/circuitbreakergatewayfallback"), http(URI.create("https://nonexistantdomain.com1234")))
.filter(circuitBreaker("mycb2", "/anything/gatewayfallback"))
.build()
.and(route("testgatewayfallback")
.route(path("/anything/gatewayfallback"), http())
.before(new HttpbinUriResolver())
.before(fallbackHeaders())
.build());
// @formatter:on
}
@ -654,7 +682,7 @@ public class ServerMvcIntegrationTests { @@ -654,7 +682,7 @@ public class ServerMvcIntegrationTests {
// @formatter:off
return route(path("/anything/circuitbreakernofallback"), http())
.filter(new HttpbinUriResolver())
.filter(circuitBreaker("mycb1", null))
.filter(circuitBreaker("mycb3", null))
.filter(setPath("/delay/5"))
.withAttribute(MvcUtils.GATEWAY_ROUTE_ID_ATTR, "testcircuitbreakernofallback");
// @formatter:on
@ -975,7 +1003,14 @@ public class ServerMvcIntegrationTests { @@ -975,7 +1003,14 @@ public class ServerMvcIntegrationTests {
protected static class TestHandler {
public ServerResponse hello(ServerRequest request) {
return ServerResponse.ok().body("Hello");
ServerResponse.BodyBuilder response = ServerResponse.ok();
if (request.headers().asHttpHeaders().containsKey(CB_EXECUTION_EXCEPTION_TYPE)) {
String exceptionType = request.headers().firstHeader(CB_EXECUTION_EXCEPTION_TYPE);
response.header(CB_EXECUTION_EXCEPTION_TYPE, exceptionType);
String exceptionMessage = request.headers().firstHeader(CB_EXECUTION_EXCEPTION_MESSAGE);
response.header(CB_EXECUTION_EXCEPTION_MESSAGE, exceptionMessage);
}
return response.body("Hello");
}
}

Loading…
Cancel
Save