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 index f4157883..2c1cb96c 100644 --- 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 @@ -16,6 +16,8 @@ package org.springframework.cloud.netflix.ribbon.apache; +import static org.springframework.cloud.netflix.ribbon.support.RibbonRequestCustomizer.Runner.customize; + import java.net.URI; import java.util.List; @@ -69,6 +71,8 @@ public class RibbonApacheHttpRequest extends ContextAwareRequest implements Clon builder.setEntity(entity); } + customize(this.context.getRequestCustomizers(), builder); + builder.setConfig(requestConfig); return builder.build(); } diff --git a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/ribbon/okhttp/OkHttpRibbonRequest.java b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/ribbon/okhttp/OkHttpRibbonRequest.java index fb83140b..fa81212d 100644 --- a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/ribbon/okhttp/OkHttpRibbonRequest.java +++ b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/ribbon/okhttp/OkHttpRibbonRequest.java @@ -16,6 +16,8 @@ package org.springframework.cloud.netflix.ribbon.okhttp; +import static org.springframework.cloud.netflix.ribbon.support.RibbonRequestCustomizer.Runner.customize; + import java.io.IOException; import java.io.InputStream; import java.net.URI; @@ -72,11 +74,14 @@ public class OkHttpRibbonRequest extends ContextAwareRequest implements Cloneabl requestBody = new InputStreamRequestBody(this.context.getRequestEntity(), mediaType, this.context.getContentLength()); } - return new Request.Builder() + Request.Builder builder = new Request.Builder() .url(url.build()) .headers(headers.build()) - .method(this.context.getMethod(), requestBody) - .build(); + .method(this.context.getMethod(), requestBody); + + customize(this.context.getRequestCustomizers(), builder); + + return builder.build(); } public OkHttpRibbonRequest withNewUri(final URI uri) { diff --git a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/ribbon/support/ContextAwareRequest.java b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/ribbon/support/ContextAwareRequest.java index af62d9bf..093ed84e 100644 --- a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/ribbon/support/ContextAwareRequest.java +++ b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/ribbon/support/ContextAwareRequest.java @@ -41,8 +41,8 @@ public abstract class ContextAwareRequest extends ClientRequest { protected RibbonCommandContext newContext(URI uri) { RibbonCommandContext commandContext = new RibbonCommandContext(this.context.getServiceId(), this.context.getMethod(), uri.toString(), this.context.getRetryable(), - this.context.getHeaders(), this.context.getParams(), this.context.getRequestEntity()); - commandContext.setContentLength(this.context.getContentLength()); + this.context.getHeaders(), this.context.getParams(), this.context.getRequestEntity(), + this.context.getRequestCustomizers(), this.context.getContentLength()); return commandContext; } } diff --git a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/ribbon/support/RibbonRequestCustomizer.java b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/ribbon/support/RibbonRequestCustomizer.java new file mode 100644 index 00000000..4e416a1b --- /dev/null +++ b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/ribbon/support/RibbonRequestCustomizer.java @@ -0,0 +1,40 @@ +/* + * Copyright 2013-2016 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.support; + +import java.util.List; + +/** + * @author Spencer Gibb + */ +public interface RibbonRequestCustomizer { + boolean accepts(Class builderClass); + void customize(B builder); + + class Runner { + + @SuppressWarnings("unchecked") + public static void customize(List customizers, Object builder) { + for (RibbonRequestCustomizer customizer : customizers) { + if (customizer.accepts(builder.getClass())) { + customizer.customize(builder); + } + } + } + } +} diff --git a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/ZuulProxyConfiguration.java b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/ZuulProxyConfiguration.java index a21d30e0..4ac23b6d 100644 --- a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/ZuulProxyConfiguration.java +++ b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/ZuulProxyConfiguration.java @@ -16,6 +16,9 @@ package org.springframework.cloud.netflix.zuul; +import java.util.Collections; +import java.util.List; + import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.actuate.endpoint.Endpoint; import org.springframework.boot.actuate.trace.TraceRepository; @@ -30,6 +33,7 @@ import org.springframework.cloud.client.discovery.event.HeartbeatMonitor; import org.springframework.cloud.client.discovery.event.InstanceRegisteredEvent; import org.springframework.cloud.client.discovery.event.ParentHeartbeatEvent; import org.springframework.cloud.netflix.ribbon.SpringClientFactory; +import org.springframework.cloud.netflix.ribbon.support.RibbonRequestCustomizer; import org.springframework.cloud.netflix.zuul.filters.ProxyRequestHelper; import org.springframework.cloud.netflix.zuul.filters.RouteLocator; import org.springframework.cloud.netflix.zuul.filters.TraceProxyRequestHelper; @@ -38,12 +42,12 @@ import org.springframework.cloud.netflix.zuul.filters.discovery.DiscoveryClientR import org.springframework.cloud.netflix.zuul.filters.discovery.ServiceRouteMapper; import org.springframework.cloud.netflix.zuul.filters.discovery.SimpleServiceRouteMapper; import org.springframework.cloud.netflix.zuul.filters.pre.PreDecorationFilter; -import org.springframework.cloud.netflix.zuul.filters.route.okhttp.OkHttpRibbonCommandFactory; import org.springframework.cloud.netflix.zuul.filters.route.RestClientRibbonCommandFactory; import org.springframework.cloud.netflix.zuul.filters.route.RibbonCommandFactory; import org.springframework.cloud.netflix.zuul.filters.route.RibbonRoutingFilter; import org.springframework.cloud.netflix.zuul.filters.route.SimpleHostRoutingFilter; import org.springframework.cloud.netflix.zuul.filters.route.apache.HttpClientRibbonCommandFactory; +import org.springframework.cloud.netflix.zuul.filters.route.okhttp.OkHttpRibbonCommandFactory; import org.springframework.cloud.netflix.zuul.web.ZuulHandlerMapping; import org.springframework.context.ApplicationEvent; import org.springframework.context.ApplicationListener; @@ -57,6 +61,9 @@ import org.springframework.context.annotation.Configuration; @Configuration public class ZuulProxyConfiguration extends ZuulConfiguration { + @Autowired(required = false) + private List requestCustomizers = Collections.emptyList(); + @Autowired private DiscoveryClient discovery; @@ -122,7 +129,7 @@ public class ZuulProxyConfiguration extends ZuulConfiguration { public RibbonRoutingFilter ribbonRoutingFilter(ProxyRequestHelper helper, RibbonCommandFactory ribbonCommandFactory) { RibbonRoutingFilter filter = new RibbonRoutingFilter(helper, - ribbonCommandFactory); + ribbonCommandFactory, this.requestCustomizers); return filter; } diff --git a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/filters/route/RestClientRibbonCommand.java b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/filters/route/RestClientRibbonCommand.java index 3acb2dd3..2631a3b2 100644 --- a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/filters/route/RestClientRibbonCommand.java +++ b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/filters/route/RestClientRibbonCommand.java @@ -17,16 +17,19 @@ package org.springframework.cloud.netflix.zuul.filters.route; -import com.netflix.client.http.HttpRequest; -import com.netflix.client.http.HttpResponse; -import com.netflix.niws.client.http.RestClient; -import org.springframework.cloud.netflix.zuul.filters.route.support.AbstractRibbonCommand; -import org.springframework.util.MultiValueMap; +import static org.springframework.cloud.netflix.ribbon.support.RibbonRequestCustomizer.Runner.customize; import java.io.InputStream; import java.net.URI; import java.util.List; +import org.springframework.cloud.netflix.zuul.filters.route.support.AbstractRibbonCommand; +import org.springframework.util.MultiValueMap; + +import com.netflix.client.http.HttpRequest; +import com.netflix.client.http.HttpResponse; +import com.netflix.niws.client.http.RestClient; + /** * Hystrix wrapper around Eureka Ribbon command * @@ -79,7 +82,7 @@ public class RestClientRibbonCommand extends AbstractRibbonCommand headers; + @NonNull private final MultiValueMap params; private final InputStream requestEntity; + @NonNull + private final List requestCustomizers; private Long contentLength; + public RibbonCommandContext(String serviceId, String method, String uri, Boolean retryable, + MultiValueMap headers, MultiValueMap params, + InputStream requestEntity) { + this(serviceId, method, uri, retryable, headers, params, requestEntity, + new ArrayList(), null); + } + public URI uri() { try { return new URI(this.uri); diff --git a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/filters/route/RibbonRoutingFilter.java b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/filters/route/RibbonRoutingFilter.java index 95f7ec5c..8e355424 100644 --- a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/filters/route/RibbonRoutingFilter.java +++ b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/filters/route/RibbonRoutingFilter.java @@ -18,11 +18,13 @@ package org.springframework.cloud.netflix.zuul.filters.route; import java.io.IOException; import java.io.InputStream; +import java.util.List; import java.util.Map; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import org.springframework.cloud.netflix.ribbon.support.RibbonRequestCustomizer; import org.springframework.cloud.netflix.zuul.filters.ProxyRequestHelper; import org.springframework.http.HttpStatus; import org.springframework.http.client.ClientHttpResponse; @@ -42,15 +44,18 @@ public class RibbonRoutingFilter extends ZuulFilter { private static final String ERROR_STATUS_CODE = "error.status_code"; protected ProxyRequestHelper helper; protected RibbonCommandFactory ribbonCommandFactory; + protected List requestCustomizers; public RibbonRoutingFilter(ProxyRequestHelper helper, - RibbonCommandFactory ribbonCommandFactory) { + RibbonCommandFactory ribbonCommandFactory, + List requestCustomizers) { this.helper = helper; this.ribbonCommandFactory = ribbonCommandFactory; + this.requestCustomizers = requestCustomizers; } public RibbonRoutingFilter(RibbonCommandFactory ribbonCommandFactory) { - this(new ProxyRequestHelper(), ribbonCommandFactory); + this(new ProxyRequestHelper(), ribbonCommandFactory, null); } @Override @@ -115,7 +120,7 @@ public class RibbonRoutingFilter extends ZuulFilter { uri = uri.replace("//", "/"); return new RibbonCommandContext(serviceId, verb, uri, retryable, headers, params, - requestEntity); + requestEntity, this.requestCustomizers); } protected ClientHttpResponse forward(RibbonCommandContext context) throws Exception { diff --git a/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/ribbon/apache/RibbonApacheHttpRequestTests.java b/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/ribbon/apache/RibbonApacheHttpRequestTests.java index 4b1e74d7..11c9a150 100644 --- a/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/ribbon/apache/RibbonApacheHttpRequestTests.java +++ b/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/ribbon/apache/RibbonApacheHttpRequestTests.java @@ -29,12 +29,15 @@ import java.io.ByteArrayInputStream; import java.io.IOException; import java.net.URI; import java.nio.charset.Charset; +import java.util.Collections; import org.apache.http.HttpEntity; import org.apache.http.HttpEntityEnclosingRequest; import org.apache.http.client.config.RequestConfig; import org.apache.http.client.methods.HttpUriRequest; +import org.apache.http.client.methods.RequestBuilder; import org.junit.Test; +import org.springframework.cloud.netflix.ribbon.support.RibbonRequestCustomizer; import org.springframework.cloud.netflix.zuul.filters.route.RibbonCommandContext; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.StreamUtils; @@ -85,7 +88,21 @@ public class RibbonApacheHttpRequestTests { headers.add("Content-Length", lengthString); length = (long) entityValue.length(); } - RibbonCommandContext context = new RibbonCommandContext("example", method, uri.toString(), false, headers, new LinkedMultiValueMap(), requestEntity); + + RibbonRequestCustomizer requestCustomizer = new RibbonRequestCustomizer() { + @Override + public boolean accepts(Class builderClass) { + return builderClass == RequestBuilder.class; + } + + @Override + public void customize(RequestBuilder builder) { + builder.addHeader("from-customizer", "foo"); + } + }; + RibbonCommandContext context = new RibbonCommandContext("example", method, + uri.toString(), false, headers, new LinkedMultiValueMap(), + requestEntity, Collections.singletonList(requestCustomizer)); context.setContentLength(length); RibbonApacheHttpRequest httpRequest = new RibbonApacheHttpRequest(context); @@ -98,6 +115,9 @@ public class RibbonApacheHttpRequestTests { assertThat("Content-Length is wrong", request.getFirstHeader("Content-Length").getValue(), is(equalTo(lengthString))); } + assertThat("from-customizer is missing", request.getFirstHeader("from-customizer"), is(notNullValue())); + assertThat("from-customizer is wrong", request.getFirstHeader("from-customizer").getValue(), + is(equalTo("foo"))); HttpEntityEnclosingRequest entityRequest = (HttpEntityEnclosingRequest) request; assertThat("entity is missing", entityRequest.getEntity(), is(notNullValue())); @@ -108,3 +128,4 @@ public class RibbonApacheHttpRequestTests { assertThat("content is wrong", string, is(equalTo(entityValue))); } } + diff --git a/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/ribbon/okhttp/OkHttpRibbonRequestTests.java b/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/ribbon/okhttp/OkHttpRibbonRequestTests.java index 94144d08..4c6c84fd 100644 --- a/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/ribbon/okhttp/OkHttpRibbonRequestTests.java +++ b/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/ribbon/okhttp/OkHttpRibbonRequestTests.java @@ -26,8 +26,10 @@ import static org.junit.Assert.assertThat; import java.io.ByteArrayInputStream; import java.io.IOException; +import java.util.Collections; import org.junit.Test; +import org.springframework.cloud.netflix.ribbon.support.RibbonRequestCustomizer; import org.springframework.cloud.netflix.zuul.filters.route.RibbonCommandContext; import org.springframework.util.LinkedMultiValueMap; @@ -80,8 +82,20 @@ public class OkHttpRibbonRequestTests { headers.add("Content-Length", lengthString); length = (long) entityValue.length(); } + + RibbonRequestCustomizer requestCustomizer = new RibbonRequestCustomizer() { + @Override + public boolean accepts(Class builderClass) { + return builderClass == Request.Builder.class; + } + + @Override + public void customize(Request.Builder builder) { + builder.addHeader("from-customizer", "foo"); + } + }; RibbonCommandContext context = new RibbonCommandContext("example", method, uri, false, - headers, new LinkedMultiValueMap(), requestEntity); + headers, new LinkedMultiValueMap(), requestEntity, Collections.singletonList(requestCustomizer)); context.setContentLength(length); OkHttpRibbonRequest httpRequest = new OkHttpRibbonRequest(context); @@ -92,6 +106,8 @@ public class OkHttpRibbonRequestTests { assertThat("Content-Length is wrong", request.header("Content-Length"), is(equalTo(lengthString))); } + assertThat("from-customizer is wrong", request.header("from-customizer"), + is(equalTo("foo"))); if (!method.equalsIgnoreCase("get")) { assertThat("body is null", request.body(), is(notNullValue())); @@ -104,3 +120,4 @@ public class OkHttpRibbonRequestTests { } } } + diff --git a/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/zuul/filters/route/RestClientRibbonCommandTests.java b/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/zuul/filters/route/RestClientRibbonCommandTests.java new file mode 100644 index 00000000..f8d43345 --- /dev/null +++ b/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/zuul/filters/route/RestClientRibbonCommandTests.java @@ -0,0 +1,118 @@ +/* + * Copyright 2013-2016 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 static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.startsWith; +import static org.junit.Assert.assertThat; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.net.URI; +import java.nio.charset.Charset; +import java.util.Collections; + +import org.junit.Test; +import org.springframework.cloud.netflix.ribbon.support.RibbonRequestCustomizer; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.StreamUtils; + +import com.netflix.client.http.HttpRequest; + +/** + * @author Spencer Gibb + */ +public class RestClientRibbonCommandTests { + + @Test + public void testNullEntity() throws Exception { + String uri = "http://example.com"; + LinkedMultiValueMap headers = new LinkedMultiValueMap<>(); + headers.add("my-header", "my-value"); + LinkedMultiValueMap params = new LinkedMultiValueMap<>(); + params.add("myparam", "myparamval"); + RestClientRibbonCommand command = new RestClientRibbonCommand("cmd", null, new RibbonCommandContext("example", "GET", uri, false, + headers, params, null)); + + HttpRequest request = command.createRequest(); + + assertThat("uri is wrong", request.getUri().toString(), startsWith(uri)); + assertThat("my-header is wrong", request.getHttpHeaders().getFirstValue("my-header"), is(equalTo("my-value"))); + assertThat("myparam is missing", request.getQueryParams().get("myparam").iterator().next(), is(equalTo("myparamval"))); + } + + @Test + // this situation happens, see https://github.com/spring-cloud/spring-cloud-netflix/issues/1042#issuecomment-227723877 + public void testEmptyEntityGet() throws Exception { + String entityValue = ""; + testEntity(entityValue, new ByteArrayInputStream(entityValue.getBytes()), false, "GET"); + } + + @Test + public void testNonEmptyEntityPost() throws Exception { + String entityValue = "abcd"; + testEntity(entityValue, new ByteArrayInputStream(entityValue.getBytes()), true, "POST"); + } + + void testEntity(String entityValue, ByteArrayInputStream requestEntity, boolean addContentLengthHeader, String method) throws Exception { + String lengthString = String.valueOf(entityValue.length()); + Long length = null; + URI uri = URI.create("http://example.com"); + LinkedMultiValueMap headers = new LinkedMultiValueMap<>(); + if (addContentLengthHeader) { + headers.add("Content-Length", lengthString); + length = (long) entityValue.length(); + } + + RibbonRequestCustomizer requestCustomizer = new RibbonRequestCustomizer() { + @Override + public boolean accepts(Class builderClass) { + return builderClass == HttpRequest.Builder.class; + } + + @Override + public void customize(HttpRequest.Builder builder) { + builder.header("from-customizer", "foo"); + } + }; + RibbonCommandContext context = new RibbonCommandContext("example", method, + uri.toString(), false, headers, new LinkedMultiValueMap(), + requestEntity, Collections.singletonList(requestCustomizer)); + context.setContentLength(length); + RestClientRibbonCommand command = new RestClientRibbonCommand("cmd", null, context); + + HttpRequest request = command.createRequest(); + + assertThat("uri is wrong", request.getUri().toString(), startsWith(uri.toString())); + if (addContentLengthHeader) { + assertThat("Content-Length is wrong", request.getHttpHeaders().getFirstValue("Content-Length"), + is(equalTo(lengthString))); + } + assertThat("from-customizer is wrong", request.getHttpHeaders().getFirstValue("from-customizer"), + is(equalTo("foo"))); + + + assertThat("entity is missing", request.getEntity(), is(notNullValue())); + assertThat("entity is wrong type", InputStream.class.isAssignableFrom(request.getEntity().getClass()), is(true)); + InputStream entity = (InputStream) request.getEntity(); + String string = StreamUtils.copyToString(entity, Charset.forName("UTF-8")); + assertThat("content is wrong", string, is(equalTo(entityValue))); + } +}