Commit 384c9b97 by Ryan Baxter
parents 981fa5db c417a9a5
......@@ -976,6 +976,7 @@ Spring Cloud Netflix _does not_ provide the following beans by default for feign
* `ErrorDecoder`
* `Request.Options`
* `Collection<RequestInterceptor>`
* `SetterFactory`
Creating a bean of one of those type and placing it in a `@FeignClient` configuration (such as `FooConfiguration` above) allows you to override each one of the beans described. Example:
......@@ -1768,6 +1769,56 @@ zuul:
customers: /customers/**
----
If you would like to provide a default fallback for all routes than you can create a bean of
type `ZuulFallbackProvider` and have the `getRoute` method return `*` or `null`.
[source,java]
----
class MyFallbackProvider implements ZuulFallbackProvider {
@Override
public String getRoute() {
return "*";
}
@Override
public ClientHttpResponse fallbackResponse() {
return new ClientHttpResponse() {
@Override
public HttpStatus getStatusCode() throws IOException {
return HttpStatus.OK;
}
@Override
public int getRawStatusCode() throws IOException {
return 200;
}
@Override
public String getStatusText() throws IOException {
return "OK";
}
@Override
public void close() {
}
@Override
public InputStream getBody() throws IOException {
return new ByteArrayInputStream("fallback".getBytes());
}
@Override
public HttpHeaders getHeaders() {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
return headers;
}
};
}
}
----
[[zuul-developer-guide]]
=== Zuul Developer Guide
......
......@@ -17,14 +17,17 @@
package org.springframework.cloud.netflix.feign;
import org.springframework.util.Assert;
import feign.Feign;
import feign.Target;
import feign.hystrix.FallbackFactory;
import feign.hystrix.HystrixFeign;
import org.springframework.util.Assert;
import feign.hystrix.SetterFactory;
/**
* @author Spencer Gibb
* @author Erik Kringen
*/
@SuppressWarnings("unchecked")
class HystrixTargeter implements Targeter {
......@@ -36,6 +39,11 @@ class HystrixTargeter implements Targeter {
return feign.target(target);
}
feign.hystrix.HystrixFeign.Builder builder = (feign.hystrix.HystrixFeign.Builder) feign;
SetterFactory setterFactory = getOptional(factory.getName(), context,
SetterFactory.class);
if (setterFactory != null) {
builder.setterFactory(setterFactory);
}
Class<?> fallback = factory.getFallback();
if (fallback != void.class) {
return targetWithFallback(factory.getName(), context, target, builder, fallback);
......@@ -95,4 +103,9 @@ class HystrixTargeter implements Targeter {
}
return (T) fallbackInstance;
}
private <T> T getOptional(String feignClientName, FeignContext context,
Class<T> beanType) {
return context.getInstance(feignClientName, beanType);
}
}
......@@ -28,7 +28,6 @@ import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import org.springframework.beans.BeanUtils;
import org.springframework.cloud.netflix.feign.AnnotatedParameterProcessor;
import org.springframework.cloud.netflix.feign.annotation.PathVariableParameterProcessor;
import org.springframework.cloud.netflix.feign.annotation.RequestHeaderParameterProcessor;
......@@ -324,9 +323,7 @@ public class SpringMvcContract extends Contract.BaseContract
// has a parameter name
return parameterNames != null && parameterNames.length > parameterIndex
// has a type
&& parameterTypes != null && parameterTypes.length > parameterIndex
// and it is a simple property
&& BeanUtils.isSimpleProperty(parameterTypes[parameterIndex].getClass());
&& parameterTypes != null && parameterTypes.length > parameterIndex;
}
private class SimpleAnnotatedParameterContext
......
......@@ -71,7 +71,7 @@ public class RibbonUtils {
public static URI updateToHttpsIfNeeded(URI uri, IClientConfig config, ServerIntrospector serverIntrospector,
Server server) {
String scheme = uri.getScheme();
if (!"https".equals(scheme) && isSecure(config, serverIntrospector, server)) {
if (!"".equals(uri.toString()) && !"https".equals(scheme) && isSecure(config, serverIntrospector, server)) {
UriComponentsBuilder uriComponentsBuilder = UriComponentsBuilder.fromUri(uri).scheme("https");
if (uri.getRawQuery() != null) {
// When building the URI, UriComponentsBuilder verify the allowed characters and does not
......
......@@ -17,6 +17,7 @@
package org.springframework.cloud.netflix.zuul.filters.post;
import java.io.ByteArrayInputStream;
import java.io.Closeable;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
......@@ -184,30 +185,35 @@ public class SendResponseFilter extends ZuulFilter {
}
}
finally {
/**
* Closing the wrapping InputStream itself has no effect on closing the underlying tcp connection since it's a wrapped stream. I guess for http
* keep-alive. When closing the wrapping stream it tries to reach the end of the current request, which is impossible for infinite http streams. So
* instead of closing the InputStream we close the HTTP response.
*
* @author Johannes Edmeier
*/
try {
if (is != null) {
is.close();
Object zuulResponse = RequestContext.getCurrentContext()
.get("zuulResponse");
if (zuulResponse instanceof Closeable) {
((Closeable) zuulResponse).close();
}
outStream.flush();
// The container will close the stream for us
}
catch (IOException ex) {
log.warn("Error while sending response to client: " + ex.getMessage());
}
}
}
private void writeResponse(InputStream zin, OutputStream out) throws Exception {
try {
byte[] bytes = buffers.get();
int bytesRead = -1;
while ((bytesRead = zin.read(bytes)) != -1) {
out.write(bytes, 0, bytesRead);
}
}
catch(IOException ioe) {
log.warn("Error while sending response to client: "+ioe.getMessage());
}
}
private void addResponseHeaders() {
RequestContext context = RequestContext.getCurrentContext();
......
......@@ -31,6 +31,7 @@ import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpOutputMessage;
import org.springframework.http.InvalidMediaTypeException;
import org.springframework.http.MediaType;
import org.springframework.http.converter.FormHttpMessageConverter;
import org.springframework.http.converter.support.AllEncompassingFormHttpMessageConverter;
import org.springframework.util.Assert;
import org.springframework.util.MultiValueMap;
......@@ -52,10 +53,16 @@ import static org.springframework.cloud.netflix.zuul.filters.support.FilterConst
*/
public class FormBodyWrapperFilter extends ZuulFilter {
private FormHttpMessageConverter formHttpMessageConverter;
private Field requestField;
private Field servletRequestField;
public FormBodyWrapperFilter() {
this(new AllEncompassingFormHttpMessageConverter());
}
public FormBodyWrapperFilter(FormHttpMessageConverter formHttpMessageConverter) {
this.formHttpMessageConverter = formHttpMessageConverter;
this.requestField = ReflectionUtils.findField(HttpServletRequestWrapper.class,
"req", HttpServletRequest.class);
this.servletRequestField = ReflectionUtils.findField(ServletRequestWrapper.class,
......@@ -139,8 +146,6 @@ public class FormBodyWrapperFilter extends ZuulFilter {
private int contentLength;
private AllEncompassingFormHttpMessageConverter converter = new AllEncompassingFormHttpMessageConverter();
public FormBodyRequestWrapper(HttpServletRequest request) {
super(request);
this.request = request;
......@@ -184,7 +189,7 @@ public class FormBodyWrapperFilter extends ZuulFilter {
this.contentType = MediaType.valueOf(this.request.getContentType());
data.getHeaders().setContentType(this.contentType);
this.converter.write(builder, this.contentType, data);
FormBodyWrapperFilter.this.formHttpMessageConverter.write(builder, this.contentType, data);
// copy new content type including multipart boundary
this.contentType = data.getHeaders().getContentType();
this.contentData = data.getInput();
......
......@@ -218,6 +218,7 @@ public class RibbonRoutingFilter extends ZuulFilter {
protected void setResponse(ClientHttpResponse resp)
throws ClientException, IOException {
RequestContext.getCurrentContext().set("zuulResponse", resp);
this.helper.setResponse(resp.getStatusCode().value(),
resp.getBody() == null ? null : resp.getBody(), resp.getHeaders());
}
......
......@@ -42,10 +42,10 @@ import org.apache.http.HttpHost;
import org.apache.http.HttpRequest;
import org.apache.http.HttpResponse;
import org.apache.http.ProtocolException;
import org.apache.http.client.HttpClient;
import org.apache.http.client.RedirectStrategy;
import org.apache.http.client.config.CookieSpecs;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPatch;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpPut;
......@@ -192,8 +192,8 @@ public class SimpleHostRoutingFilter extends ZuulFilter {
this.helper.addIgnoredHeaders();
try {
HttpResponse response = forward(this.httpClient, verb, uri, request, headers,
params, requestEntity);
CloseableHttpResponse response = forward(this.httpClient, verb, uri, request,
headers, params, requestEntity);
setResponse(response);
}
catch (Exception ex) {
......@@ -278,8 +278,8 @@ public class SimpleHostRoutingFilter extends ZuulFilter {
}).build();
}
private HttpResponse forward(HttpClient httpclient, String verb, String uri,
HttpServletRequest request, MultiValueMap<String, String> headers,
private CloseableHttpResponse forward(CloseableHttpClient httpclient, String verb,
String uri, HttpServletRequest request, MultiValueMap<String, String> headers,
MultiValueMap<String, String> params, InputStream requestEntity)
throws Exception {
Map<String, Object> info = this.helper.debug(verb, uri, headers, params,
......@@ -301,7 +301,8 @@ public class SimpleHostRoutingFilter extends ZuulFilter {
try {
log.debug(httpHost.getHostName() + " " + httpHost.getPort() + " "
+ httpHost.getSchemeName());
HttpResponse zuulResponse = forwardRequest(httpclient, httpHost, httpRequest);
CloseableHttpResponse zuulResponse = forwardRequest(httpclient, httpHost,
httpRequest);
this.helper.appendDebug(info, zuulResponse.getStatusLine().getStatusCode(),
revertHeaders(zuulResponse.getAllHeaders()));
return zuulResponse;
......@@ -379,8 +380,8 @@ public class SimpleHostRoutingFilter extends ZuulFilter {
return list.toArray(new BasicHeader[0]);
}
private HttpResponse forwardRequest(HttpClient httpclient, HttpHost httpHost,
HttpRequest httpRequest) throws IOException {
private CloseableHttpResponse forwardRequest(CloseableHttpClient httpclient,
HttpHost httpHost, HttpRequest httpRequest) throws IOException {
return httpclient.execute(httpHost, httpRequest);
}
......@@ -407,6 +408,7 @@ public class SimpleHostRoutingFilter extends ZuulFilter {
}
private void setResponse(HttpResponse response) throws IOException {
RequestContext.getCurrentContext().set("zuulResponse", response);
this.helper.setResponse(response.getStatusLine().getStatusCode(),
response.getEntity() == null ? null : response.getEntity().getContent(),
revertHeaders(response.getAllHeaders()));
......
......@@ -30,15 +30,25 @@ import org.springframework.cloud.netflix.zuul.filters.route.ZuulFallbackProvider
public abstract class AbstractRibbonCommandFactory implements RibbonCommandFactory {
private Map<String, ZuulFallbackProvider> fallbackProviderCache;
private ZuulFallbackProvider defaultFallbackProvider = null;
public AbstractRibbonCommandFactory(Set<ZuulFallbackProvider> fallbackProviders){
this.fallbackProviderCache = new HashMap<>();
for(ZuulFallbackProvider provider : fallbackProviders) {
fallbackProviderCache.put(provider.getRoute(), provider);
String route = provider.getRoute();
if("*".equals(route) || route == null) {
defaultFallbackProvider = provider;
} else {
fallbackProviderCache.put(route, provider);
}
}
}
protected ZuulFallbackProvider getFallbackProvider(String route) {
return fallbackProviderCache.get(route);
ZuulFallbackProvider provider = fallbackProviderCache.get(route);
if(provider == null) {
provider = defaultFallbackProvider;
}
return provider;
}
}
......@@ -298,6 +298,19 @@ public class SpringMvcContractTests {
}
@Test
public void testProcessAnnotations_ListParamsWithoutName() throws Exception {
Method method = TestTemplate_ListParamsWithoutName.class.getDeclaredMethod("getTest",
List.class);
MethodMetadata data = this.contract
.parseAndValidateMetadata(method.getDeclaringClass(), method);
assertEquals("/test", data.template().url());
assertEquals("GET", data.template().method());
assertEquals("[{id}]", data.template().queries().get("id").toString());
assertNotNull(data.indexToExpander().get(0));
}
@Test
public void testProcessAnnotations_MapParams() throws Exception {
Method method = TestTemplate_MapParams.class.getDeclaredMethod("getTest",
Map.class);
......@@ -457,6 +470,11 @@ public class SpringMvcContractTests {
ResponseEntity<TestObject> getTest(@RequestParam("id") List<String> id);
}
public interface TestTemplate_ListParamsWithoutName {
@RequestMapping(value = "/test", method = RequestMethod.GET)
ResponseEntity<TestObject> getTest(@RequestParam List<String> id);
}
public interface TestTemplate_MapParams {
@RequestMapping(value = "/test", method = RequestMethod.GET)
ResponseEntity<TestObject> getTest(@RequestParam Map<String, String> params);
......
......@@ -25,6 +25,7 @@ import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.text.ParseException;
import java.util.ArrayList;
......@@ -68,14 +69,19 @@ import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import com.netflix.hystrix.HystrixCommand;
import com.netflix.hystrix.HystrixCommandGroupKey;
import com.netflix.hystrix.HystrixCommandKey;
import com.netflix.loadbalancer.Server;
import com.netflix.loadbalancer.ServerList;
import feign.Client;
import feign.Feign;
import feign.Logger;
import feign.RequestInterceptor;
import feign.RequestTemplate;
import feign.Target;
import feign.hystrix.FallbackFactory;
import feign.hystrix.SetterFactory;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
......@@ -85,6 +91,7 @@ import rx.Single;
/**
* @author Spencer Gibb
* @author Jakub Narloch
* @author Erik Kringen
*/
@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest(classes = FeignClientTests.Application.class, webEnvironment = WebEnvironment.RANDOM_PORT, value = {
......@@ -124,6 +131,9 @@ public class FeignClientTests {
@Qualifier("localapp3FeignClient")
HystrixClient namedHystrixClient;
@Autowired
HystrixSetterFactoryClient hystrixSetterFactoryClient;
protected enum Arg {
A, B;
......@@ -298,18 +308,47 @@ public class FeignClientTests {
}
}
@FeignClient(name = "localapp5", configuration = TestHystrixSetterFactoryClientConfig.class)
protected interface HystrixSetterFactoryClient {
@RequestMapping(method = RequestMethod.GET, path = "/hellos")
HystrixCommand<List<Hello>> getHellosHystrix();
}
public static class TestHystrixSetterFactoryClientConfig {
public static final String SETTER_PREFIX = "SETTER-";
@Bean
public SetterFactory commandKeyIsRequestLineSetterFactory() {
return new SetterFactory() {
@Override public HystrixCommand.Setter create(Target<?> target,
Method method) {
String groupKey = SETTER_PREFIX + target.name();
RequestMapping requestMapping = method
.getAnnotation(RequestMapping.class);
String commandKey =
SETTER_PREFIX + requestMapping.method()[0] + " " + requestMapping
.path()[0];
return HystrixCommand.Setter
.withGroupKey(HystrixCommandGroupKey.Factory.asKey(groupKey))
.andCommandKey(HystrixCommandKey.Factory.asKey(commandKey));
}
};
}
}
@Configuration
@EnableAutoConfiguration
@RestController
@EnableFeignClients(clients = { TestClientServiceId.class, TestClient.class,
DecodingTestClient.class, HystrixClient.class, HystrixClientWithFallBackFactory.class },
DecodingTestClient.class, HystrixClient.class, HystrixClientWithFallBackFactory.class,
HystrixSetterFactoryClient.class},
defaultConfiguration = TestDefaultFeignConfig.class)
@RibbonClients({
@RibbonClient(name = "localapp", configuration = LocalRibbonClientConfiguration.class),
@RibbonClient(name = "localapp1", configuration = LocalRibbonClientConfiguration.class),
@RibbonClient(name = "localapp2", configuration = LocalRibbonClientConfiguration.class),
@RibbonClient(name = "localapp3", configuration = LocalRibbonClientConfiguration.class),
@RibbonClient(name = "localapp4", configuration = LocalRibbonClientConfiguration.class)
@RibbonClient(name = "localapp4", configuration = LocalRibbonClientConfiguration.class),
@RibbonClient(name = "localapp5", configuration = LocalRibbonClientConfiguration.class)
})
protected static class Application {
......@@ -525,12 +564,16 @@ public class FeignClientTests {
}
@Test
public void testHystrixCommand() {
public void testHystrixCommand() throws NoSuchMethodException {
HystrixCommand<List<Hello>> command = this.testClient.getHellosHystrix();
assertNotNull("command was null", command);
assertEquals(
"Hystrix command group name should match the name of the feign client",
"localapp", command.getCommandGroup().name());
String configKey = Feign.configKey(TestClient.class,
TestClient.class.getMethod("getHellosHystrix", (Class<?>[]) null));
assertEquals("Hystrix command key name should match the feign config key",
configKey, command.getCommandKey().name());
List<Hello> hellos = command.execute();
assertNotNull("hellos was null", hellos);
assertEquals("hellos didn't match", hellos, getHelloList());
......@@ -658,6 +701,25 @@ public class FeignClientTests {
assertNotNull("namedHystrixClient was null", this.namedHystrixClient);
}
@Test
public void testHystrixSetterFactory() {
HystrixCommand<List<Hello>> command = this.hystrixSetterFactoryClient
.getHellosHystrix();
assertNotNull("command was null", command);
String setterPrefix = TestHystrixSetterFactoryClientConfig.SETTER_PREFIX;
assertEquals(
"Hystrix command group name should match the name of the feign client with a prefix of "
+ setterPrefix, setterPrefix + "localapp5",
command.getCommandGroup().name());
assertEquals(
"Hystrix command key name should match the request method (space) request path with a prefix of "
+ setterPrefix, setterPrefix + "GET /hellos",
command.getCommandKey().name());
List<Hello> hellos = command.execute();
assertNotNull("hellos was null", hellos);
assertEquals("hellos didn't match", hellos, getHelloList());
}
@Data
@AllArgsConstructor
@NoArgsConstructor
......
......@@ -110,6 +110,14 @@ public class RibbonUtilsTests {
"https://foo/%20bar?hello=1%202")));
}
@Test
public void emptyStringUri() throws URISyntaxException {
URI original = new URI("");
URI updated = updateToHttpsIfNeeded(original, SECURE_CONFIG, SECURE_INTROSPECTOR, SERVER);
Assert.assertThat("URI should be the emptry string", updated, is(new URI(
"")));
}
static DefaultClientConfigImpl getConfig(boolean value) {
DefaultClientConfigImpl config = new DefaultClientConfigImpl();
config.setProperty(CommonClientConfigKey.IsSecure, value);
......
......@@ -17,8 +17,13 @@
package org.springframework.cloud.netflix.zuul.filters.post;
import java.io.ByteArrayInputStream;
import java.io.Closeable;
import java.io.IOException;
import java.lang.reflect.UndeclaredThrowableException;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.junit.After;
import org.junit.Before;
......@@ -34,9 +39,16 @@ import com.netflix.zuul.constants.ZuulConstants;
import com.netflix.zuul.context.Debug;
import com.netflix.zuul.context.RequestContext;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.Matchers.equalTo;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
import static org.mockito.Matchers.anyInt;
import static org.mockito.Matchers.isA;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.springframework.cloud.netflix.zuul.filters.support.FilterConstants.X_ZUUL_DEBUG_HEADER;
/**
......@@ -96,6 +108,34 @@ public class SendResponseFilterTests {
assertThat("wrong origin content length", contentLength, equalTo("6"));
}
@Test
public void closeResponseOutpusStreamError() throws Exception {
HttpServletResponse response = mock(HttpServletResponse.class);
RequestContext context = new RequestContext();
context.setRequest(new MockHttpServletRequest());
context.setResponse(response);
context.setResponseDataStream(new ByteArrayInputStream("Hello\n".getBytes("UTF-8")));
Closeable zuulResponse = mock(Closeable.class);
context.set("zuulResponse", zuulResponse);
RequestContext.testSetCurrentContext(context);
SendResponseFilter filter = new SendResponseFilter();
ServletOutputStream zuuloutputstream = mock(ServletOutputStream.class);
doThrow(new IOException("Response to client closed")).when(zuuloutputstream).write(isA(byte[].class), anyInt(), anyInt());
when(response.getOutputStream()).thenReturn(zuuloutputstream);
try {
filter.run();
} catch (UndeclaredThrowableException ex) {
assertThat(ex.getUndeclaredThrowable().getMessage(), is("Response to client closed"));
}
verify(zuulResponse).close();
}
private void runFilter(String characterEncoding, String content, boolean streamContent) throws Exception {
MockHttpServletResponse response = new MockHttpServletResponse();
SendResponseFilter filter = createFilter(content, characterEncoding, response, streamContent);
......
......@@ -33,7 +33,7 @@ import static org.springframework.boot.test.context.SpringBootTest.WebEnvironmen
* @author Ryan Baxter
*/
@RunWith(SpringRunner.class)
@SpringBootTest(classes = HttpClientRibbonCommandIntegrationTests.TestConfig.class, webEnvironment = RANDOM_PORT,
@SpringBootTest(classes = RibbonCommandFallbackTests.TestConfig.class, webEnvironment = RANDOM_PORT,
properties = {
"zuul.routes.simple: /simple/**", "zuul.routes.another: /another/twolevel/**",
"ribbon.ReadTimeout: 1"})
......
......@@ -31,7 +31,7 @@ import com.netflix.zuul.context.RequestContext;
* @author Ryan Baxter
*/
@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest(classes = OkHttpRibbonCommandIntegrationTests.TestConfig.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, value = {
@SpringBootTest(classes = RibbonCommandFallbackTests.TestConfig.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, value = {
"zuul.routes.simple: /simple/**", "zuul.routes.another: /another/twolevel/**",
"ribbon.ReadTimeout: 1"})
@DirtiesContext
......
......@@ -31,7 +31,7 @@ import com.netflix.zuul.context.RequestContext;
* @author Ryan Baxter
*/
@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest(classes = RestClientRibbonCommandIntegrationTests.TestConfig.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, value = {
@SpringBootTest(classes = RibbonCommandFallbackTests.TestConfig.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, value = {
"zuul.routes.simple: /simple/**", "zuul.routes.another: /another/twolevel/**",
"ribbon.ReadTimeout: 1"})
@DirtiesContext
......
......@@ -18,13 +18,35 @@
package org.springframework.cloud.netflix.zuul.filters.route.support;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Collections;
import java.util.Set;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.web.ErrorAttributes;
import org.springframework.boot.context.embedded.LocalServerPort;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.cloud.netflix.ribbon.RibbonClient;
import org.springframework.cloud.netflix.ribbon.RibbonClients;
import org.springframework.cloud.netflix.ribbon.SpringClientFactory;
import org.springframework.cloud.netflix.zuul.EnableZuulProxy;
import org.springframework.cloud.netflix.zuul.filters.ZuulProperties;
import org.springframework.cloud.netflix.zuul.filters.route.RibbonCommandFactory;
import org.springframework.cloud.netflix.zuul.filters.route.ZuulFallbackProvider;
import org.springframework.cloud.netflix.zuul.filters.route.apache.HttpClientRibbonCommandFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.web.bind.annotation.RestController;
import static org.junit.Assert.assertEquals;
......@@ -47,11 +69,90 @@ public abstract class RibbonCommandFallbackTests {
}
@Test
public void noFallback() {
public void defaultFallback() {
String uri = "/another/twolevel/slow";
ResponseEntity<String> result = new TestRestTemplate().exchange(
"http://localhost:" + this.port + uri, HttpMethod.GET,
new HttpEntity<>((Void) null), String.class);
assertEquals(HttpStatus.INTERNAL_SERVER_ERROR, result.getStatusCode());
assertEquals(HttpStatus.OK, result.getStatusCode());
assertEquals("default fallback", result.getBody());
}
// Don't use @SpringBootApplication because we don't want to component scan
@Configuration
@EnableAutoConfiguration
@RestController
@EnableZuulProxy
@RibbonClients({
@RibbonClient(name = "simple", configuration = ZuulProxyTestBase.SimpleRibbonClientConfiguration.class),
@RibbonClient(name = "another", configuration = ZuulProxyTestBase.AnotherRibbonClientConfiguration.class)})
public static class TestConfig extends ZuulProxyTestBase.AbstractZuulProxyApplication {
@Autowired(required = false)
private Set<ZuulFallbackProvider> zuulFallbackProviders = Collections.emptySet();
@Bean
public RibbonCommandFactory<?> ribbonCommandFactory(
final SpringClientFactory clientFactory) {
return new HttpClientRibbonCommandFactory(clientFactory, new ZuulProperties(),
zuulFallbackProviders);
}
@Bean
public ZuulProxyTestBase.MyErrorController myErrorController(
ErrorAttributes errorAttributes) {
return new ZuulProxyTestBase.MyErrorController(errorAttributes);
}
@Bean
public ZuulFallbackProvider defaultFallbackProvider() {
return new DefaultFallbackProvider();
}
}
public static class DefaultFallbackProvider implements ZuulFallbackProvider {
@Override
public String getRoute() {
return "*";
}
@Override
public ClientHttpResponse fallbackResponse() {
return new ClientHttpResponse() {
@Override
public HttpStatus getStatusCode() throws IOException {
return HttpStatus.OK;
}
@Override
public int getRawStatusCode() throws IOException {
return 200;
}
@Override
public String getStatusText() throws IOException {
return null;
}
@Override
public void close() {
}
@Override
public InputStream getBody() throws IOException {
return new ByteArrayInputStream("default fallback".getBytes());
}
@Override
public HttpHeaders getHeaders() {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.TEXT_HTML);
return headers;
}
};
}
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment