diff --git a/docs/src/main/asciidoc/spring-cloud-gateway.adoc b/docs/src/main/asciidoc/spring-cloud-gateway.adoc index db0192a6..892c10f9 100644 --- a/docs/src/main/asciidoc/spring-cloud-gateway.adoc +++ b/docs/src/main/asciidoc/spring-cloud-gateway.adoc @@ -970,6 +970,9 @@ spring: - Path=/service/** ---- +NOTE By default when a service instance cannot be found in the `LoadBalancer` a `503` will be returned. +You can configure the Gateway to return a `404` by setting `spring.cloud.gateway.loadbalancer.use404=true`. + === Netty Routing Filter The Netty Routing Filter runs if the url located in the `ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR` exchange attribute has a `http` or `https` scheme. It uses the Netty `HttpClient` to make the downstream proxy request. The response is put in the `ServerWebExchangeUtils.CLIENT_RESPONSE_ATTR` exchange attribute for use in a later filter. (There is an experimental `WebClientHttpRoutingFilter` that performs the same function, but does not require netty) diff --git a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/config/GatewayLoadBalancerClientAutoConfiguration.java b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/config/GatewayLoadBalancerClientAutoConfiguration.java index 119c3adc..2455f765 100644 --- a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/config/GatewayLoadBalancerClientAutoConfiguration.java +++ b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/config/GatewayLoadBalancerClientAutoConfiguration.java @@ -21,7 +21,9 @@ import org.springframework.boot.autoconfigure.AutoConfigureAfter; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.cloud.client.loadbalancer.LoadBalancerClient; +import org.springframework.cloud.gateway.discovery.DiscoveryLocatorProperties; import org.springframework.cloud.gateway.filter.LoadBalancerClientFilter; import org.springframework.cloud.netflix.ribbon.RibbonAutoConfiguration; import org.springframework.context.annotation.Bean; @@ -34,6 +36,7 @@ import org.springframework.web.reactive.DispatcherHandler; @Configuration @ConditionalOnClass({LoadBalancerClient.class, RibbonAutoConfiguration.class, DispatcherHandler.class}) @AutoConfigureAfter(RibbonAutoConfiguration.class) +@EnableConfigurationProperties(LoadBalancerProperties.class) public class GatewayLoadBalancerClientAutoConfiguration { // GlobalFilter beans @@ -41,7 +44,7 @@ public class GatewayLoadBalancerClientAutoConfiguration { @Bean @ConditionalOnBean(LoadBalancerClient.class) @ConditionalOnMissingBean(LoadBalancerClientFilter.class) - public LoadBalancerClientFilter loadBalancerClientFilter(LoadBalancerClient client) { - return new LoadBalancerClientFilter(client); + public LoadBalancerClientFilter loadBalancerClientFilter(LoadBalancerClient client, LoadBalancerProperties properties) { + return new LoadBalancerClientFilter(client, properties); } } diff --git a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/config/LoadBalancerProperties.java b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/config/LoadBalancerProperties.java new file mode 100644 index 00000000..fa004212 --- /dev/null +++ b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/config/LoadBalancerProperties.java @@ -0,0 +1,35 @@ +/* + * Copyright 2013-2018 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package org.springframework.cloud.gateway.config; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * @author Ryan Baxter + */ +@ConfigurationProperties("spring.cloud.gateway.loadbalancer") +public class LoadBalancerProperties { + private boolean use404; + + public boolean isUse404() { + return use404; + } + + public void setUse404(boolean use404) { + this.use404 = use404; + } +} diff --git a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/LoadBalancerClientFilter.java b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/LoadBalancerClientFilter.java index ff12c753..0d1977ef 100644 --- a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/LoadBalancerClientFilter.java +++ b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/LoadBalancerClientFilter.java @@ -24,13 +24,17 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.cloud.client.ServiceInstance; import org.springframework.cloud.client.loadbalancer.LoadBalancerClient; +import org.springframework.cloud.gateway.config.LoadBalancerProperties; +import org.springframework.cloud.gateway.discovery.DiscoveryLocatorProperties; import org.springframework.cloud.gateway.support.NotFoundException; import org.springframework.core.Ordered; +import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.server.ServerWebExchange; import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR; import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.GATEWAY_SCHEME_PREFIX_ATTR; import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.addOriginalRequestUrl; +import static org.springframework.http.HttpStatus.NOT_FOUND; import reactor.core.publisher.Mono; @@ -45,8 +49,11 @@ public class LoadBalancerClientFilter implements GlobalFilter, Ordered { protected final LoadBalancerClient loadBalancer; - public LoadBalancerClientFilter(LoadBalancerClient loadBalancer) { + private LoadBalancerProperties properties; + + public LoadBalancerClientFilter(LoadBalancerClient loadBalancer, LoadBalancerProperties properties) { this.loadBalancer = loadBalancer; + this.properties = properties; } @Override @@ -69,7 +76,11 @@ public class LoadBalancerClientFilter implements GlobalFilter, Ordered { final ServiceInstance instance = choose(exchange); if (instance == null) { - throw new NotFoundException("Unable to find instance for " + url.getHost()); + String msg = "Unable to find instance for " + url.getHost(); + if(properties.isUse404()) { + throw new FourOFourNotFoundException(msg); + } + throw new NotFoundException(msg); } URI uri = exchange.getRequest().getURI(); @@ -91,6 +102,12 @@ public class LoadBalancerClientFilter implements GlobalFilter, Ordered { protected ServiceInstance choose(ServerWebExchange exchange) { return loadBalancer.choose(((URI) exchange.getAttribute(GATEWAY_REQUEST_URL_ATTR)).getHost()); } + @ResponseStatus(value = NOT_FOUND, reason = "The service was not found.") + static class FourOFourNotFoundException extends RuntimeException { + public FourOFourNotFoundException(String msg) { + super(msg); + } + } class DelegatingServiceInstance implements ServiceInstance { final ServiceInstance delegate; diff --git a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/support/NotFoundException.java b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/support/NotFoundException.java index b8e83aae..267193b6 100644 --- a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/support/NotFoundException.java +++ b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/support/NotFoundException.java @@ -17,9 +17,14 @@ package org.springframework.cloud.gateway.support; +import org.springframework.web.bind.annotation.ResponseStatus; + +import static org.springframework.http.HttpStatus.SERVICE_UNAVAILABLE; + /** * @author Spencer Gibb */ +@ResponseStatus(value = SERVICE_UNAVAILABLE, reason = "The service or item was not found.") public class NotFoundException extends RuntimeException { public NotFoundException(String message) { super(message); diff --git a/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/discovery/GatewayDiscoveryClientAutoConfigurationTests.java b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/discovery/GatewayDiscoveryClientAutoConfigurationTests.java index 3c0df19a..6ed1ff6d 100644 --- a/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/discovery/GatewayDiscoveryClientAutoConfigurationTests.java +++ b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/discovery/GatewayDiscoveryClientAutoConfigurationTests.java @@ -24,6 +24,7 @@ import org.springframework.boot.SpringBootConfiguration; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.cloud.client.discovery.DiscoveryClient; +import org.springframework.cloud.gateway.config.LoadBalancerProperties; import org.springframework.context.annotation.Bean; import org.springframework.test.context.junit4.SpringRunner; @@ -35,18 +36,27 @@ public class GatewayDiscoveryClientAutoConfigurationTests { @RunWith(SpringRunner.class) @SpringBootTest(classes = Config.class, - properties = "spring.cloud.gateway.discovery.locator.enabled=true") + properties = {"spring.cloud.gateway.discovery.locator.enabled=true", + "spring.cloud.gateway.loadbalancer.use404=true"}) public static class EnabledByProperty { @Autowired(required = false) private DiscoveryClientRouteDefinitionLocator locator; + @Autowired(required = false) + private LoadBalancerProperties properties; + @Test public void routeLocatorBeanExists() { assertThat(locator) .as("DiscoveryClientRouteDefinitionLocator was null") .isNotNull(); } + + @Test + public void use404() { + assertThat(properties.isUse404()).isTrue(); + } } @RunWith(SpringRunner.class) diff --git a/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/filter/LoadBalancerClientFilterTests.java b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/filter/LoadBalancerClientFilterTests.java index fa40b217..b1be303f 100644 --- a/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/filter/LoadBalancerClientFilterTests.java +++ b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/filter/LoadBalancerClientFilterTests.java @@ -15,6 +15,8 @@ import org.mockito.junit.MockitoJUnitRunner; import org.springframework.cloud.client.DefaultServiceInstance; import org.springframework.cloud.client.ServiceInstance; import org.springframework.cloud.client.loadbalancer.LoadBalancerClient; +import org.springframework.cloud.gateway.config.LoadBalancerProperties; +import org.springframework.cloud.gateway.discovery.DiscoveryLocatorProperties; import org.springframework.cloud.gateway.support.NotFoundException; import org.springframework.cloud.netflix.ribbon.RibbonLoadBalancerClient; import org.springframework.cloud.netflix.ribbon.RibbonLoadBalancerContext; @@ -48,17 +50,20 @@ public class LoadBalancerClientFilterTests { private ServerWebExchange exchange; + private LoadBalancerProperties properties; + @Mock private GatewayFilterChain chain; @Mock private LoadBalancerClient loadBalancerClient; - @InjectMocks private LoadBalancerClientFilter loadBalancerClientFilter; @Before public void setup() { + properties = new LoadBalancerProperties(); + loadBalancerClientFilter = new LoadBalancerClientFilter(loadBalancerClient, properties); exchange = MockServerWebExchange.from(MockServerHttpRequest.get("loadbalancerclient.org").build()); } @@ -91,6 +96,14 @@ public class LoadBalancerClientFilterTests { loadBalancerClientFilter.filter(exchange, chain); } + @Test(expected = LoadBalancerClientFilter.FourOFourNotFoundException.class) + public void shouldThrow4O4ExceptionWhenNoServiceInstanceIsFound() { + URI uri = UriComponentsBuilder.fromUriString("lb://myservice").build().toUri(); + exchange.getAttributes().put(GATEWAY_REQUEST_URL_ATTR, uri); + properties.setUse404(true); + loadBalancerClientFilter.filter(exchange, chain); + } + @Test public void shouldFilter() { URI url = UriComponentsBuilder.fromUriString("lb://myservice").build().toUri(); @@ -252,7 +265,7 @@ public class LoadBalancerClientFilterTests { } }; - LoadBalancerClientFilter loadBalancerClientFilter = new LoadBalancerClientFilter(loadBalancerClient) { + LoadBalancerClientFilter loadBalancerClientFilter = new LoadBalancerClientFilter(loadBalancerClient, properties) { protected ServiceInstance choose(ServerWebExchange exchange) { URI attribute = (URI) exchange.getAttribute(GATEWAY_REQUEST_URL_ATTR); return loadBalancer.choose(attribute.getHost() + "<<>>" + attribute.getPort()); @@ -292,7 +305,7 @@ public class LoadBalancerClientFilterTests { RibbonLoadBalancerClient client = new RibbonLoadBalancerClient(clientFactory); - LoadBalancerClientFilter filter = new LoadBalancerClientFilter(client); + LoadBalancerClientFilter filter = new LoadBalancerClientFilter(client, properties); filter.filter(exchange, chain); return captor.getValue(); diff --git a/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/test/GatewayIntegrationTests.java b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/test/GatewayIntegrationTests.java index 5cdffdf7..f10a0987 100644 --- a/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/test/GatewayIntegrationTests.java +++ b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/test/GatewayIntegrationTests.java @@ -21,6 +21,8 @@ import java.util.Map; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.hamcrest.BaseMatcher; +import org.hamcrest.Description; import org.junit.Test; import org.junit.runner.RunWith; import reactor.core.publisher.Mono; @@ -146,7 +148,21 @@ public class GatewayIntegrationTests extends BaseWebClientTests { testClient.get().uri("/get") .header("Host", "www.loadbalancerclientempty.org") .exchange() - .expectStatus().is5xxServerError(); + .expectStatus().value(new BaseMatcher() { + @Override + public boolean matches(Object item) { + if (Integer.class.isInstance(item)) { + Integer toMatch = (Integer) item; + return toMatch.intValue() == 503; + } + return false; + } + + @Override + public void describeTo(Description description) { + description.appendText("Expected 503"); + } + }); } @Test