Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Fallback cause is propagated to fallback provider
Cause can now be propagated to fallback provider in order to respond accordingly to a exception type. New interface is introduced in order not to break any existing falback providers.
- Loading branch information
1 parent
6dbdb1d
commit 4c53fd2
Showing
4 changed files
with
324 additions
and
2 deletions.
There are no files selected for viewing
36 changes: 36 additions & 0 deletions
36
.../src/main/java/org/springframework/cloud/netflix/zuul/filters/route/FallbackProvider.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
/* | ||
* Copyright 2017 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.netflix.zuul.filters.route; | ||
|
||
import org.springframework.http.client.ClientHttpResponse; | ||
|
||
/** | ||
* Extension of {@link ZuulFallbackProvider} which adds possibility to choose proper response | ||
* based on the exception which caused the main method to fail. | ||
* | ||
* @author Dominik Mostek | ||
*/ | ||
public interface FallbackProvider extends ZuulFallbackProvider { | ||
|
||
/** | ||
* Provides a fallback response based on the cause of the failed execution. | ||
* | ||
* @param cause cause of the main method failure | ||
* @return the fallback response | ||
*/ | ||
ClientHttpResponse fallbackResponse(Throwable cause); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
264 changes: 264 additions & 0 deletions
264
...k/cloud/netflix/zuul/filters/route/support/RibbonCommandCauseFallbackPropagationTest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,264 @@ | ||
/* | ||
* Copyright 2017 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.netflix.zuul.filters.route.support; | ||
|
||
import com.netflix.client.AbstractLoadBalancerAwareClient; | ||
import com.netflix.client.ClientException; | ||
import com.netflix.client.ClientRequest; | ||
import com.netflix.client.IResponse; | ||
import com.netflix.client.RequestSpecificRetryHandler; | ||
import com.netflix.client.config.IClientConfig; | ||
import com.netflix.client.http.HttpResponse; | ||
import com.netflix.hystrix.HystrixCommandProperties; | ||
import com.netflix.hystrix.exception.HystrixTimeoutException; | ||
import org.junit.Test; | ||
import org.springframework.cloud.netflix.zuul.filters.ZuulProperties; | ||
import org.springframework.cloud.netflix.zuul.filters.route.ZuulFallbackProvider; | ||
import org.springframework.cloud.netflix.zuul.filters.route.FallbackProvider; | ||
import org.springframework.http.HttpHeaders; | ||
import org.springframework.http.HttpStatus; | ||
import org.springframework.http.client.ClientHttpResponse; | ||
|
||
import java.io.ByteArrayInputStream; | ||
import java.io.IOException; | ||
import java.io.InputStream; | ||
|
||
import static org.assertj.core.api.Assertions.assertThat; | ||
|
||
/** | ||
* @author Dominik Mostek | ||
*/ | ||
public class RibbonCommandCauseFallbackPropagationTest { | ||
|
||
@Test | ||
public void providerIsCalledInCaseOfException() throws Exception { | ||
TestZuulFallbackProviderWithoutCause provider = new TestZuulFallbackProviderWithoutCause(HttpStatus.INTERNAL_SERVER_ERROR); | ||
RuntimeException exception = new RuntimeException("Failed!"); | ||
TestRibbonCommand testCommand = new TestRibbonCommand(new TestClient(exception), provider); | ||
|
||
ClientHttpResponse response = testCommand.execute(); | ||
|
||
assertThat(response).isNotNull(); | ||
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR); | ||
} | ||
|
||
@Test | ||
public void causeIsProvidedForNewInterface() throws Exception { | ||
TestFallbackProvider provider = TestFallbackProvider.withResponse(HttpStatus.NOT_FOUND); | ||
RuntimeException exception = new RuntimeException("Failed!"); | ||
TestRibbonCommand testCommand = new TestRibbonCommand(new TestClient(exception), provider); | ||
|
||
ClientHttpResponse response = testCommand.execute(); | ||
|
||
assertThat(response).isNotNull(); | ||
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND); | ||
Throwable cause = provider.getCause(); | ||
assertThat(cause.getClass()).isEqualTo(exception.getClass()); | ||
assertThat(cause.getMessage()).isEqualTo(exception.getMessage()); | ||
} | ||
|
||
@Test | ||
public void executionExceptionIsUsedInsteadWhenFailedExceptionIsNull() throws Exception { | ||
TestFallbackProvider provider = TestFallbackProvider.withResponse(HttpStatus.BAD_GATEWAY); | ||
final RuntimeException exception = new RuntimeException("Failed!"); | ||
TestRibbonCommand testCommand = new TestRibbonCommand(new TestClient(exception), provider) { | ||
@Override | ||
public Throwable getFailedExecutionException() { | ||
return null; | ||
} | ||
|
||
@Override | ||
public Throwable getExecutionException() { | ||
return exception; | ||
} | ||
}; | ||
|
||
ClientHttpResponse response = testCommand.execute(); | ||
|
||
assertThat(response).isNotNull(); | ||
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.BAD_GATEWAY); | ||
} | ||
|
||
@Test | ||
public void timeoutExceptionIsPropagated() throws Exception { | ||
TestFallbackProvider provider = TestFallbackProvider.withResponse(HttpStatus.CONFLICT); | ||
RuntimeException exception = new RuntimeException("Failed!"); | ||
TestRibbonCommand testCommand = new TestRibbonCommand(new TestClient(exception), provider, 1) { | ||
@Override | ||
protected ClientRequest createRequest() throws Exception { | ||
Thread.sleep(5); | ||
return super.createRequest(); | ||
} | ||
}; | ||
|
||
ClientHttpResponse response = testCommand.execute(); | ||
|
||
assertThat(response).isNotNull(); | ||
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CONFLICT); | ||
assertThat(provider.getCause()).isNotNull(); | ||
assertThat(provider.getCause().getClass()).isEqualTo(HystrixTimeoutException.class); | ||
} | ||
|
||
public static class TestRibbonCommand | ||
extends AbstractRibbonCommand<AbstractLoadBalancerAwareClient<ClientRequest, HttpResponse>, ClientRequest, HttpResponse> { | ||
|
||
public TestRibbonCommand(AbstractLoadBalancerAwareClient<ClientRequest, HttpResponse> client, ZuulFallbackProvider fallbackProvider) { | ||
this(client, new ZuulProperties(), fallbackProvider); | ||
} | ||
|
||
public TestRibbonCommand(AbstractLoadBalancerAwareClient<ClientRequest, HttpResponse> client, | ||
ZuulProperties zuulProperties, | ||
ZuulFallbackProvider fallbackProvider) { | ||
super("testCommand", client, null, zuulProperties, fallbackProvider); | ||
} | ||
|
||
public TestRibbonCommand(AbstractLoadBalancerAwareClient<ClientRequest, HttpResponse> client, | ||
ZuulFallbackProvider fallbackProvider, | ||
int timeout) { | ||
// different name is used because of properties caching | ||
super(getSetter("testCommand2", new ZuulProperties()).andCommandPropertiesDefaults(defauts(timeout)), | ||
client, null, fallbackProvider, null); | ||
} | ||
|
||
private static HystrixCommandProperties.Setter defauts(final int timeout) { | ||
return HystrixCommandProperties.Setter().withExecutionTimeoutEnabled(true) | ||
.withExecutionIsolationStrategy(HystrixCommandProperties.ExecutionIsolationStrategy.THREAD) | ||
.withExecutionTimeoutInMilliseconds(timeout); | ||
} | ||
|
||
@Override | ||
protected ClientRequest createRequest() throws Exception { | ||
return null; | ||
} | ||
} | ||
|
||
public static class TestClient extends AbstractLoadBalancerAwareClient { | ||
|
||
private final RuntimeException exception; | ||
|
||
public TestClient(RuntimeException exception) { | ||
super(null); | ||
this.exception = exception; | ||
} | ||
|
||
@Override | ||
public IResponse executeWithLoadBalancer(final ClientRequest request, final IClientConfig requestConfig) throws ClientException { | ||
throw exception; | ||
} | ||
|
||
@Override | ||
public RequestSpecificRetryHandler getRequestSpecificRetryHandler(final ClientRequest clientRequest, final IClientConfig iClientConfig) { | ||
return null; | ||
} | ||
|
||
@Override | ||
public IResponse execute(final ClientRequest clientRequest, final IClientConfig iClientConfig) throws Exception { | ||
return null; | ||
} | ||
} | ||
|
||
public static class TestFallbackProvider implements FallbackProvider { | ||
|
||
private final ClientHttpResponse response; | ||
private Throwable cause; | ||
|
||
public TestFallbackProvider(final ClientHttpResponse response) { | ||
this.response = response; | ||
} | ||
|
||
@Override | ||
public ClientHttpResponse fallbackResponse(final Throwable cause) { | ||
this.cause = cause; | ||
return response; | ||
} | ||
|
||
@Override | ||
public String getRoute() { | ||
return "test-route"; | ||
} | ||
|
||
@Override | ||
public ClientHttpResponse fallbackResponse() { | ||
throw new UnsupportedOperationException("fallback without cause is not supported"); | ||
} | ||
|
||
public Throwable getCause() { | ||
return cause; | ||
} | ||
|
||
public static TestFallbackProvider withResponse(final HttpStatus status) { | ||
return new TestFallbackProvider(getClientHttpResponse(status)); | ||
} | ||
} | ||
|
||
public static class TestZuulFallbackProviderWithoutCause implements ZuulFallbackProvider { | ||
|
||
private final ClientHttpResponse response; | ||
|
||
public TestZuulFallbackProviderWithoutCause(final ClientHttpResponse response) { | ||
this.response = response; | ||
} | ||
|
||
public TestZuulFallbackProviderWithoutCause(final HttpStatus status) { | ||
this(getClientHttpResponse(status)); | ||
} | ||
|
||
@Override | ||
public String getRoute() { | ||
return "test-route"; | ||
} | ||
|
||
@Override | ||
public ClientHttpResponse fallbackResponse() { | ||
return response; | ||
} | ||
} | ||
|
||
private static ClientHttpResponse getClientHttpResponse(final HttpStatus status) { | ||
return new ClientHttpResponse() { | ||
@Override | ||
public HttpStatus getStatusCode() throws IOException { | ||
return status; | ||
} | ||
|
||
@Override | ||
public int getRawStatusCode() throws IOException { | ||
return getStatusCode().value(); | ||
} | ||
|
||
@Override | ||
public String getStatusText() throws IOException { | ||
return getStatusCode().getReasonPhrase(); | ||
} | ||
|
||
@Override | ||
public void close() { | ||
} | ||
|
||
@Override | ||
public InputStream getBody() throws IOException { | ||
return new ByteArrayInputStream("test".getBytes()); | ||
} | ||
|
||
@Override | ||
public HttpHeaders getHeaders() { | ||
return new HttpHeaders(); | ||
} | ||
}; | ||
} | ||
} |