diff --git a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/ribbon/apache/RibbonApacheHttpRequest.java b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/ribbon/apache/RibbonApacheHttpRequest.java new file mode 100644 index 00000000..922e32d5 --- /dev/null +++ b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/ribbon/apache/RibbonApacheHttpRequest.java @@ -0,0 +1,92 @@ +/* + * Copyright 2013-2015 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.ribbon.apache; + +import java.io.InputStream; +import java.net.URI; +import java.util.List; + +import org.apache.http.client.config.RequestConfig; +import org.apache.http.client.methods.HttpUriRequest; +import org.apache.http.client.methods.RequestBuilder; +import org.apache.http.entity.BasicHttpEntity; +import org.springframework.util.MultiValueMap; + +import com.netflix.client.ClientRequest; + +import lombok.Getter; + +/** + * @author Christian Lohmann + */ +@Getter +public class RibbonApacheHttpRequest extends ClientRequest implements Cloneable { + + private final String method; + + private final MultiValueMap headers; + + private final MultiValueMap params; + + private final InputStream requestEntity; + + public RibbonApacheHttpRequest(final String method, final URI uri, + final Boolean retryable, final MultiValueMap headers, + final MultiValueMap params, final InputStream requestEntity) { + + this.method = method; + this.uri = uri; + this.isRetriable = retryable; + this.headers = headers; + this.params = params; + this.requestEntity = requestEntity; + } + + public HttpUriRequest toRequest(final RequestConfig requestConfig) { + final RequestBuilder builder = RequestBuilder.create(this.method); + builder.setUri(this.uri); + for (final String name : this.headers.keySet()) { + final List values = this.headers.get(name); + for (final String value : values) { + builder.addHeader(name, value); + } + } + + for (final String name : this.params.keySet()) { + final List values = this.params.get(name); + for (final String value : values) { + builder.addParameter(name, value); + } + } + + if (this.requestEntity != null) { + final BasicHttpEntity entity; + entity = new BasicHttpEntity(); + entity.setContent(this.requestEntity); + builder.setEntity(entity); + } + + builder.setConfig(requestConfig); + return builder.build(); + } + + public RibbonApacheHttpRequest withNewUri(final URI uri) { + return new RibbonApacheHttpRequest(this.method, uri, this.isRetriable, + this.headers, this.params, this.requestEntity); + } + +} diff --git a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/ribbon/apache/RibbonApacheHttpResponse.java b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/ribbon/apache/RibbonApacheHttpResponse.java new file mode 100644 index 00000000..3cd0c037 --- /dev/null +++ b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/ribbon/apache/RibbonApacheHttpResponse.java @@ -0,0 +1,161 @@ +/* + * Copyright 2013-2015 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.ribbon.apache; + +import java.io.IOException; +import java.io.InputStream; +import java.lang.reflect.Type; +import java.net.URI; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.apache.http.Header; +import org.apache.http.HttpResponse; + +import com.google.common.reflect.TypeToken; +import com.netflix.client.ClientException; +import com.netflix.client.http.CaseInsensitiveMultiMap; +import com.netflix.client.http.HttpHeaders; + +/** + * @author Christian Lohmann + */ +public class RibbonApacheHttpResponse implements com.netflix.client.http.HttpResponse { + + private HttpResponse httpResponse; + private URI uri; + + public RibbonApacheHttpResponse(final HttpResponse httpResponse, final URI uri) { + this.httpResponse = httpResponse; + this.uri = uri; + } + + @Override + public Object getPayload() throws ClientException { + try { + return this.httpResponse.getEntity().getContent(); + } + catch (final IOException e) { + throw new ClientException(e.getMessage(), e); + } + } + + @Override + public boolean hasPayload() { + return this.httpResponse.getEntity() != null; + } + + @Override + public boolean isSuccess() { + return this.httpResponse.getStatusLine().getStatusCode() == 200; + } + + @Override + public URI getRequestedURI() { + return this.uri; + } + + public int getStatus() { + return httpResponse.getStatusLine().getStatusCode(); + } + + public String getStatusLine() { + return httpResponse.getStatusLine().toString(); + } + + @Override + public Map> getHeaders() { + final Map> headers = new HashMap<>(); + for (final Header header : this.httpResponse.getAllHeaders()) { + if (headers.containsKey(header.getName())) { + headers.get(header.getName()).add(header.getValue()); + } + else { + final List values = new ArrayList<>(); + values.add(header.getValue()); + headers.put(header.getName(), values); + } + } + + return headers; + } + + @Override + public HttpHeaders getHttpHeaders() { + final CaseInsensitiveMultiMap headers = new CaseInsensitiveMultiMap(); + for (final Header header : httpResponse.getAllHeaders()) { + headers.addHeader(header.getName(), header.getValue()); + } + + return headers; + } + + @Override + public void close() { + if (this.httpResponse != null && this.httpResponse.getEntity() != null) { + try { + this.httpResponse.getEntity().getContent().close(); + } + catch (final IOException e) { + throw new RuntimeException(e.getMessage(), e); + } + } + + } + + @Override + public InputStream getInputStream() { + try { + return this.httpResponse.getEntity().getContent(); + } + catch (final IOException e) { + throw new RuntimeException(e.getMessage(), e); + } + } + + @Override + public boolean hasEntity() { + return hasPayload(); + } + + /** + * Not used + */ + @Override + public T getEntity(final Class type) throws Exception { + return null; + } + + /** + * Not used + */ + @Override + public T getEntity(final Type type) throws Exception { + return null; + } + + /** + * Not used + */ + @Override + public T getEntity(final TypeToken type) throws Exception { + return null; + } +} diff --git a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/ribbon/apache/RibbonLoadBalancingHttpClient.java b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/ribbon/apache/RibbonLoadBalancingHttpClient.java new file mode 100644 index 00000000..ad9b4f98 --- /dev/null +++ b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/ribbon/apache/RibbonLoadBalancingHttpClient.java @@ -0,0 +1,115 @@ +/* + * Copyright 2013-2015 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.ribbon.apache; + +import java.net.URI; + +import org.apache.http.HttpResponse; +import org.apache.http.client.HttpClient; +import org.apache.http.client.config.RequestConfig; +import org.apache.http.client.methods.HttpUriRequest; +import org.apache.http.impl.client.HttpClientBuilder; +import org.springframework.web.util.UriComponentsBuilder; + +import com.netflix.client.AbstractLoadBalancerAwareClient; +import com.netflix.client.RequestSpecificRetryHandler; +import com.netflix.client.RetryHandler; +import com.netflix.client.config.CommonClientConfigKey; +import com.netflix.client.config.IClientConfig; +import com.netflix.loadbalancer.ILoadBalancer; + +import lombok.Setter; + +/** + * @author Christian Lohmann + */ +public class RibbonLoadBalancingHttpClient + extends AbstractLoadBalancerAwareClient { + private final HttpClient delegate = HttpClientBuilder.create().build(); + + @Setter + private int connectTimeout; + + @Setter + private int readTimeout; + + @Setter + private boolean secure; + + @Setter + private IClientConfig clientConfig; + + public RibbonLoadBalancingHttpClient() { + super(null); + this.setRetryHandler(RetryHandler.DEFAULT); + } + + public RibbonLoadBalancingHttpClient(final ILoadBalancer lb) { + super(lb); + this.setRetryHandler(RetryHandler.DEFAULT); + } + + @Override + public RequestSpecificRetryHandler getRequestSpecificRetryHandler( + final RibbonApacheHttpRequest request, final IClientConfig requestConfig) { + if (this.clientConfig.get(CommonClientConfigKey.OkToRetryOnAllOperations, false)) { + return new RequestSpecificRetryHandler(true, true, this.getRetryHandler(), + requestConfig); + } + + if (!request.getMethod().equals("GET")) { + return new RequestSpecificRetryHandler(true, false, this.getRetryHandler(), + requestConfig); + } + else { + return new RequestSpecificRetryHandler(true, true, this.getRetryHandler(), + requestConfig); + } + } + + @Override + public RibbonApacheHttpResponse execute(RibbonApacheHttpRequest request, + final IClientConfig configOverride) throws Exception { + final RequestConfig.Builder builder = RequestConfig.custom(); + if (configOverride != null) { + builder.setConnectTimeout(configOverride.get( + CommonClientConfigKey.ConnectTimeout, this.connectTimeout)); + builder.setConnectionRequestTimeout(configOverride.get( + CommonClientConfigKey.ReadTimeout, this.readTimeout)); + } + else { + builder.setConnectTimeout(this.connectTimeout); + builder.setConnectionRequestTimeout(this.readTimeout); + } + + final RequestConfig requestConfig = builder.build(); + + if (isSecure(configOverride)) { + final URI secureUri = UriComponentsBuilder.fromUri(request.getUri()) + .scheme("https").build().toUri(); + request = request.withNewUri(secureUri); + } + + final HttpUriRequest httpUriRequest = request.toRequest(requestConfig); + final HttpResponse httpResponse = this.delegate.execute(httpUriRequest); + return new RibbonApacheHttpResponse(httpResponse, httpUriRequest.getURI()); + } + + private boolean isSecure(final IClientConfig config) { + return (config != null) ? config.get(CommonClientConfigKey.IsSecure) : secure; + } +} diff --git a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/filters/route/apache/HttpClientRibbonCommand.java b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/filters/route/apache/HttpClientRibbonCommand.java new file mode 100644 index 00000000..ca564d54 --- /dev/null +++ b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/filters/route/apache/HttpClientRibbonCommand.java @@ -0,0 +1,123 @@ +/* + * Copyright 2013-2015 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.apache; + +import java.io.InputStream; +import java.net.URI; + +import org.springframework.cloud.netflix.ribbon.RibbonHttpResponse; +import org.springframework.cloud.netflix.ribbon.apache.RibbonApacheHttpRequest; +import org.springframework.cloud.netflix.ribbon.apache.RibbonApacheHttpResponse; +import org.springframework.cloud.netflix.ribbon.apache.RibbonLoadBalancingHttpClient; +import org.springframework.cloud.netflix.zuul.filters.route.RibbonCommand; +import org.springframework.http.client.ClientHttpResponse; +import org.springframework.util.MultiValueMap; + +import com.netflix.config.DynamicIntProperty; +import com.netflix.config.DynamicPropertyFactory; +import com.netflix.hystrix.HystrixCommand; +import com.netflix.hystrix.HystrixCommandGroupKey; +import com.netflix.hystrix.HystrixCommandKey; +import com.netflix.hystrix.HystrixCommandProperties; +import com.netflix.zuul.constants.ZuulConstants; +import com.netflix.zuul.context.RequestContext; + +/** + * @author Christian Lohmann + */ +public class HttpClientRibbonCommand extends HystrixCommand implements + RibbonCommand { + + private final RibbonLoadBalancingHttpClient client; + private final String method; + private final String uri; + private final MultiValueMap headers; + private final MultiValueMap params; + private final InputStream requestEntity; + private final Boolean retryable; + + public HttpClientRibbonCommand(final RibbonLoadBalancingHttpClient client, + final String method, final String uri, + final MultiValueMap headers, + final MultiValueMap params, final InputStream requestEntity, + final Boolean retryable) { + this("default", client, method, uri, headers, params, requestEntity, retryable); + } + + public HttpClientRibbonCommand(final String commandKey, + final RibbonLoadBalancingHttpClient client, final String method, + final String uri, final MultiValueMap headers, + final MultiValueMap params, final InputStream requestEntity, + final Boolean retryable) { + super(getSetter(commandKey)); + this.client = client; + this.method = method; + this.uri = uri; + this.headers = headers; + this.params = params; + this.requestEntity = requestEntity; + this.retryable = retryable; + } + + protected static Setter getSetter(final String commandKey) { + + // we want to default to semaphore-isolation since this wraps + // 2 others commands that are already thread isolated + final String name = ZuulConstants.ZUUL_EUREKA + commandKey + + ".semaphore.maxSemaphores"; + final DynamicIntProperty value = DynamicPropertyFactory.getInstance() + .getIntProperty(name, 100); + final HystrixCommandProperties.Setter setter = HystrixCommandProperties + .Setter() + .withExecutionIsolationStrategy( + HystrixCommandProperties.ExecutionIsolationStrategy.SEMAPHORE) + .withExecutionIsolationSemaphoreMaxConcurrentRequests(value.get()); + return Setter + .withGroupKey(HystrixCommandGroupKey.Factory.asKey("RibbonCommand")) + .andCommandKey( + HystrixCommandKey.Factory.asKey(commandKey + "RibbonCommand")) + .andCommandPropertiesDefaults(setter); + } + + @Override + protected ClientHttpResponse run() throws Exception { + return forward(); + } + + protected ClientHttpResponse forward() throws Exception { + final RequestContext context = RequestContext.getCurrentContext(); + URI uriInstance = new URI(this.uri); + RibbonApacheHttpRequest request = new RibbonApacheHttpRequest(this.method, + uriInstance, this.retryable, this.headers, this.params, + this.requestEntity); + final RibbonApacheHttpResponse response = this.client + .executeWithLoadBalancer(request); + context.set("ribbonResponse", response); + + // Explicitly close the HttpResponse if the Hystrix command timed out to + // release the underlying HTTP connection held by the response. + // + if (this.isResponseTimedOut()) { + if (response != null) { + response.close(); + } + } + + return new RibbonHttpResponse(response); + } + +} diff --git a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/filters/route/apache/HttpClientRibbonCommandFactory.java b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/filters/route/apache/HttpClientRibbonCommandFactory.java new file mode 100644 index 00000000..59db82c9 --- /dev/null +++ b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/filters/route/apache/HttpClientRibbonCommandFactory.java @@ -0,0 +1,49 @@ +/* + * Copyright 2013-2015 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.apache; + +import org.springframework.cloud.netflix.ribbon.SpringClientFactory; +import org.springframework.cloud.netflix.ribbon.apache.RibbonLoadBalancingHttpClient; +import org.springframework.cloud.netflix.zuul.filters.route.RibbonCommandContext; +import org.springframework.cloud.netflix.zuul.filters.route.RibbonCommandFactory; + +import lombok.RequiredArgsConstructor; + +/** + * @author Christian Lohmann + */ +@RequiredArgsConstructor +public class HttpClientRibbonCommandFactory implements + RibbonCommandFactory { + + private final SpringClientFactory clientFactory; + + @Override + public HttpClientRibbonCommand create(final RibbonCommandContext context) { + final String serviceId = context.getServiceId(); + final RibbonLoadBalancingHttpClient client = clientFactory.getClient(serviceId, + RibbonLoadBalancingHttpClient.class); + client.setLoadBalancer(this.clientFactory.getLoadBalancer(serviceId)); + client.setClientConfig(this.clientFactory.getClientConfig(serviceId)); + + final HttpClientRibbonCommand httpClientRibbonCommand = new HttpClientRibbonCommand( + serviceId, client, context.getVerb(), context.getUri(), + context.getHeaders(), context.getParams(), context.getRequestEntity(), + context.getRetryable()); + return httpClientRibbonCommand; + } +} diff --git a/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/zuul/SampleZuulProxyAppTestsWithHttpClient.java b/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/zuul/SampleZuulProxyAppTestsWithHttpClient.java new file mode 100644 index 00000000..ac975178 --- /dev/null +++ b/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/zuul/SampleZuulProxyAppTestsWithHttpClient.java @@ -0,0 +1,339 @@ +/* + * Copyright 2013-2015 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; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.test.IntegrationTest; +import org.springframework.boot.test.SpringApplicationConfiguration; +import org.springframework.boot.test.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.filters.ProxyRouteLocator; +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.apache.HttpClientRibbonCommandFactory; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.web.WebAppConfiguration; +import org.springframework.util.MultiValueMap; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import com.netflix.zuul.ZuulFilter; +import com.netflix.zuul.context.RequestContext; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +@RunWith(SpringJUnit4ClassRunner.class) +@SpringApplicationConfiguration(classes = SampleHttpClientZuulProxyApplication.class) +@WebAppConfiguration +@IntegrationTest({ "server.port: 0", + "zuul.routes.other: /test/**=http://localhost:7777/local", + "zuul.routes.another: /another/twolevel/**", "zuul.routes.simple: /simple/**" }) +@DirtiesContext +public class SampleZuulProxyAppTestsWithHttpClient { + + @Value("${local.server.port}") + private int port; + + @Autowired + private ProxyRouteLocator routes; + + @Autowired + private RoutesEndpoint endpoint; + + @Autowired + private RibbonCommandFactory ribbonCommandFactory; + + @Test + public void bindRouteUsingPhysicalRoute() { + assertEquals("http://localhost:7777/local", + this.routes.getRoutes().get("/test/**")); + } + + @Test + public void bindRouteUsingOnlyPath() { + assertEquals("simple", this.routes.getRoutes().get("/simple/**")); + } + + @Test + public void getOnSelfViaRibbonRoutingFilter() { + ResponseEntity result = new TestRestTemplate().exchange( + "http://localhost:" + this.port + "/simple/local/1", HttpMethod.GET, + new HttpEntity<>((Void) null), String.class); + assertEquals(HttpStatus.OK, result.getStatusCode()); + assertEquals("Gotten 1!", result.getBody()); + } + + @Test + public void patchOnSelfViaRibbonRoutingFilter() { + ResponseEntity result = new TestRestTemplate().exchange( + "http://localhost:" + this.port + "/simple/local/1", HttpMethod.PATCH, + new HttpEntity<>("TestPatch"), String.class); + assertEquals(HttpStatus.OK, result.getStatusCode()); + assertEquals("Patched 1!", result.getBody()); + } + + @Test + public void postOnSelfViaRibbonRoutingFilter() { + ResponseEntity result = new TestRestTemplate().exchange( + "http://localhost:" + this.port + "/simple/local/1", HttpMethod.POST, + new HttpEntity<>("TestPost"), String.class); + assertEquals(HttpStatus.OK, result.getStatusCode()); + assertEquals("Posted 1!", result.getBody()); + } + + @Test + public void deleteOnSelfViaRibbonRoutingFilter() { + ResponseEntity result = new TestRestTemplate().exchange( + "http://localhost:" + this.port + "/simple/local/1", HttpMethod.DELETE, + new HttpEntity<>((Void) null), String.class); + assertEquals(HttpStatus.OK, result.getStatusCode()); + assertEquals("Deleted 1!", result.getBody()); + } + + @Test + public void deleteOnSelfViaSimpleHostRoutingFilter() { + this.routes.addRoute("/self/**", "http://localhost:" + this.port + "/local"); + this.endpoint.reset(); + + ResponseEntity result = new TestRestTemplate().exchange( + "http://localhost:" + this.port + "/self/1", HttpMethod.DELETE, + new HttpEntity<>((Void) null), String.class); + assertEquals(HttpStatus.OK, result.getStatusCode()); + assertEquals("Deleted 1!", result.getBody()); + } + + @Test + public void patchOnSelfViaSimpleHostRoutingFilter() { + this.routes.addRoute("/self/**", "http://localhost:" + this.port + "/local"); + this.endpoint.reset(); + + ResponseEntity result = new TestRestTemplate().exchange( + "http://localhost:" + this.port + "/self/1", HttpMethod.PATCH, + new HttpEntity<>("TestPatch"), String.class); + assertEquals(HttpStatus.OK, result.getStatusCode()); + assertEquals("Patched 1!", result.getBody()); + } + + @Test + public void stripPrefixFalseAppendsPath() { + this.routes.addRoute(new ZuulProperties.ZuulRoute("strip", "/strip/**", "strip", + "http://localhost:" + this.port + "/local", false, false)); + this.endpoint.reset(); + + ResponseEntity result = new TestRestTemplate().exchange( + "http://localhost:" + this.port + "/strip", HttpMethod.GET, + new HttpEntity<>((Void) null), String.class); + assertEquals(HttpStatus.OK, result.getStatusCode()); + + // Prefix not stripped to it goes to /local/strip + assertEquals("Gotten strip!", result.getBody()); + } + + @Test + public void testNotFound() { + ResponseEntity result = new TestRestTemplate().exchange( + "http://localhost:" + this.port + "/myinvalidpath", HttpMethod.GET, + new HttpEntity<>((Void) null), String.class); + assertEquals(HttpStatus.NOT_FOUND, result.getStatusCode()); + } + + @Test + public void getSecondLevel() { + ResponseEntity result = new TestRestTemplate().exchange( + "http://localhost:" + this.port + "/another/twolevel/local/1", + HttpMethod.GET, new HttpEntity<>((Void) null), String.class); + assertEquals(HttpStatus.OK, result.getStatusCode()); + assertEquals("Gotten 1!", result.getBody()); + } + + @Test + public void ribbonRouteWithSpace() { + ResponseEntity result = new TestRestTemplate().exchange( + "http://localhost:" + this.port + "/simple/spa ce", HttpMethod.GET, + new HttpEntity<>((Void) null), String.class); + assertEquals(HttpStatus.OK, result.getStatusCode()); + assertEquals("Hello space", result.getBody()); + } + + @Test + public void simpleHostRouteWithSpace() { + routes.addRoute("/self/**", "http://localhost:" + this.port); + this.endpoint.reset(); + + ResponseEntity result = new TestRestTemplate().exchange( + "http://localhost:" + this.port + "/self/spa ce", HttpMethod.GET, + new HttpEntity<>((Void) null), String.class); + assertEquals(HttpStatus.OK, result.getStatusCode()); + assertEquals("Hello space", result.getBody()); + } + + @Test + public void simpleHostRouteWithOriginalQString() { + routes.addRoute("/self/**", "http://localhost:" + this.port); + this.endpoint.reset(); + + ResponseEntity result = new TestRestTemplate().exchange( + "http://localhost:" + this.port + + "/self/qstring?original=value1&original=value2", + HttpMethod.GET, new HttpEntity<>((Void) null), String.class); + assertEquals(HttpStatus.OK, result.getStatusCode()); + assertEquals("Received {original=[value1, value2]}", result.getBody()); + } + + @Test + public void simpleHostRouteWithOverriddenQString() { + routes.addRoute("/self/**", "http://localhost:" + this.port); + this.endpoint.reset(); + + ResponseEntity result = new TestRestTemplate().exchange( + "http://localhost:" + this.port + + "/self/qstring?override=true&different=key", HttpMethod.GET, + new HttpEntity<>((Void) null), String.class); + assertEquals(HttpStatus.OK, result.getStatusCode()); + assertEquals("Received {key=[overridden]}", result.getBody()); + } + + @Test + public void ribbonCommandFactoryOverridden() { + assertTrue("ribbonCommandFactory not a MyRibbonCommandFactory", + ribbonCommandFactory instanceof HttpClientRibbonCommandFactory); + } + +} + +// Don't use @SpringBootApplication because we don't want to component scan +@Configuration +@EnableAutoConfiguration +@RestController +@EnableZuulProxy +@RibbonClients({ + @RibbonClient(name = "simple", configuration = SimpleRibbonClientConfiguration.class), + @RibbonClient(name = "another", configuration = AnotherRibbonClientConfiguration.class) }) +class SampleHttpClientZuulProxyApplication { + + public static void main(final String[] args) { + SpringApplication.run(SampleZuulProxyApplication.class, args); + } + + @RequestMapping("/testing123") + public String testing123() { + throw new RuntimeException("myerror"); + } + + @RequestMapping("/local") + public String local() { + return "Hello local"; + } + + @RequestMapping(value = "/local/{id}", method = RequestMethod.DELETE) + public String delete(@PathVariable final String id) { + return "Deleted " + id + "!"; + } + + @RequestMapping(value = "/local/{id}", method = RequestMethod.PATCH) + public String patch(@PathVariable final String id, @RequestBody final String body) { + return "Patched " + id + "!"; + } + + @RequestMapping(value = "/local/{id}", method = RequestMethod.GET) + public String get(@PathVariable final String id) { + return "Gotten " + id + "!"; + } + + @RequestMapping(value = "/local/{id}", method = RequestMethod.POST) + public String post(@PathVariable final String id, @RequestBody final String body) { + return "Posted " + id + "!"; + } + + @RequestMapping(value = "/qstring") + public String qstring(@RequestParam final MultiValueMap params) { + return "Received " + params.toString(); + } + + @RequestMapping("/") + public String home() { + return "Hello world"; + } + + @RequestMapping("/spa ce") + public String space() { + return "Hello space"; + } + + @Bean + public RibbonCommandFactory ribbonCommandFactory( + final SpringClientFactory clientFactory) { + return new HttpClientRibbonCommandFactory(clientFactory); + } + + @Bean + public ZuulFilter sampleFilter() { + return new ZuulFilter() { + @Override + public String filterType() { + return "pre"; + } + + @Override + public boolean shouldFilter() { + return true; + } + + @Override + public Object run() { + if (RequestContext.getCurrentContext().getRequest().getParameterMap() + .containsKey("override")) { + Map> overridden = new HashMap<>(); + overridden.put("key", Arrays.asList("overridden")); + RequestContext.getCurrentContext().setRequestQueryParams(overridden); + } + + return null; + } + + @Override + public int filterOrder() { + return 0; + } + }; + } + +}