Adds support for returning a 404 when service instance not found in loadbalancer (#696)
Fixes gh-414
This commit is contained in:
committed by
Spencer Gibb
parent
6d2e381742
commit
1c8f080dd8
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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<Integer>() {
|
||||
@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
|
||||
|
||||
Reference in New Issue
Block a user