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 2c1cb96c..44a8768b 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 @@ -63,10 +63,11 @@ public class RibbonApacheHttpRequest extends ContextAwareRequest implements Clon entity.setContent(this.context.getRequestEntity()); // if the entity contentLength isn't set, transfer-encoding will be set // to chunked in org.apache.http.protocol.RequestContent. See gh-1042 - if (this.context.getContentLength() != null) { - entity.setContentLength(this.context.getContentLength()); - } else if ("GET".equals(this.context.getMethod())) { + Long contentLength = this.context.getContentLength(); + if ("GET".equals(this.context.getMethod()) && (contentLength == null || contentLength < 0)) { entity.setContentLength(0); + } else if (contentLength != null) { + entity.setContentLength(contentLength); } builder.setEntity(entity); } diff --git a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/filters/route/RibbonCommandContext.java b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/filters/route/RibbonCommandContext.java index 79c2e72c..e5c6e343 100644 --- a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/filters/route/RibbonCommandContext.java +++ b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/filters/route/RibbonCommandContext.java @@ -21,36 +21,24 @@ import java.net.URI; import java.net.URISyntaxException; import java.util.ArrayList; import java.util.List; +import java.util.Objects; import org.springframework.cloud.netflix.ribbon.support.RibbonRequestCustomizer; +import org.springframework.util.Assert; import org.springframework.util.MultiValueMap; import org.springframework.util.ReflectionUtils; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NonNull; -import lombok.RequiredArgsConstructor; - /** * @author Spencer Gibb */ -@Data -@RequiredArgsConstructor -@AllArgsConstructor public class RibbonCommandContext { - @NonNull private final String serviceId; - @NonNull private final String method; - @NonNull private final String uri; private final Boolean retryable; - @NonNull private final MultiValueMap headers; - @NonNull private final MultiValueMap params; private final InputStream requestEntity; - @NonNull private final List requestCustomizers; private Long contentLength; @@ -65,6 +53,34 @@ public class RibbonCommandContext { new ArrayList(), null); } + public RibbonCommandContext(String serviceId, String method, String uri, + Boolean retryable, MultiValueMap headers, + MultiValueMap params, InputStream requestEntity, + List requestCustomizers) { + this(serviceId, method, uri, retryable, headers, params, requestEntity, requestCustomizers, null); + } + + public RibbonCommandContext(String serviceId, String method, String uri, + Boolean retryable, MultiValueMap headers, + MultiValueMap params, InputStream requestEntity, + List requestCustomizers, Long contentLength) { + Assert.notNull(serviceId, "serviceId may not be null"); + Assert.notNull(method, "method may not be null"); + Assert.notNull(uri, "uri may not be null"); + Assert.notNull(headers, "headers may not be null"); + Assert.notNull(params, "params may not be null"); + Assert.notNull(requestCustomizers, "requestCustomizers may not be null"); + this.serviceId = serviceId; + this.method = method; + this.uri = uri; + this.retryable = retryable; + this.headers = headers; + this.params = params; + this.requestEntity = requestEntity; + this.requestCustomizers = requestCustomizers; + this.contentLength = contentLength; + } + public URI uri() { try { return new URI(this.uri); @@ -83,4 +99,81 @@ public class RibbonCommandContext { public String getVerb() { return this.method; } + + public String getServiceId() { + return serviceId; + } + + public String getMethod() { + return method; + } + + public String getUri() { + return uri; + } + + public Boolean getRetryable() { + return retryable; + } + + public MultiValueMap getHeaders() { + return headers; + } + + public MultiValueMap getParams() { + return params; + } + + public InputStream getRequestEntity() { + return requestEntity; + } + + public List getRequestCustomizers() { + return requestCustomizers; + } + + public Long getContentLength() { + return contentLength; + } + + public void setContentLength(Long contentLength) { + this.contentLength = contentLength; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + RibbonCommandContext that = (RibbonCommandContext) o; + return Objects.equals(serviceId, that.serviceId) && + Objects.equals(method, that.method) && + Objects.equals(uri, that.uri) && + Objects.equals(retryable, that.retryable) && + Objects.equals(headers, that.headers) && + Objects.equals(params, that.params) && + Objects.equals(requestEntity, that.requestEntity) && + Objects.equals(requestCustomizers, that.requestCustomizers) && + Objects.equals(contentLength, that.contentLength); + } + + @Override + public int hashCode() { + return Objects.hash(serviceId, method, uri, retryable, headers, params, requestEntity, requestCustomizers, contentLength); + } + + @Override + public String toString() { + final StringBuffer sb = new StringBuffer("RibbonCommandContext{"); + sb.append("serviceId='").append(serviceId).append('\''); + sb.append(", method='").append(method).append('\''); + sb.append(", uri='").append(uri).append('\''); + sb.append(", retryable=").append(retryable); + sb.append(", headers=").append(headers); + sb.append(", params=").append(params); + sb.append(", requestEntity=").append(requestEntity); + sb.append(", requestCustomizers=").append(requestCustomizers); + sb.append(", contentLength=").append(contentLength); + sb.append('}'); + return sb.toString(); + } } 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 749e7462..66d17dcc 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 @@ -124,7 +124,7 @@ public class RibbonRoutingFilter extends ZuulFilter { .buildZuulRequestQueryParams(request); String verb = getVerb(request); InputStream requestEntity = getRequestBody(request); - if (request.getContentLength() < 0) { + if (request.getContentLength() < 0 && !verb.equalsIgnoreCase("GET")) { context.setChunkedRequestBody(); } diff --git a/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/zuul/ZuulProxyApplicationTests.java b/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/zuul/ZuulProxyApplicationTests.java new file mode 100644 index 00000000..e3db36c8 --- /dev/null +++ b/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/zuul/ZuulProxyApplicationTests.java @@ -0,0 +1,122 @@ +/* + * Copyright 2013-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; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.context.embedded.LocalServerPort; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.cloud.netflix.ribbon.RibbonClient; +import org.springframework.cloud.netflix.ribbon.StaticServerList; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpEntity; +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.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RestController; + +import com.netflix.loadbalancer.Server; +import com.netflix.loadbalancer.ServerList; +import com.netflix.zuul.context.RequestContext; + +import static org.junit.Assert.assertEquals; + +@RunWith(SpringJUnit4ClassRunner.class) +@SpringBootTest(classes = ZuulProxyApplicationTests.ZuulProxyApplication.class, webEnvironment = WebEnvironment.RANDOM_PORT, properties = { + "zuul.routes.simple:/simple/**", "logging.level.org.apache.http: DEBUG" }) +@DirtiesContext +public class ZuulProxyApplicationTests { + + @LocalServerPort + private int port; + + @Before + public void setTestRequestcontext() { + RequestContext context = new RequestContext(); + RequestContext.testSetCurrentContext(context); + } + + @Test + public void getHasCorrectTransferEncoding() { + ResponseEntity result = new TestRestTemplate().getForEntity( + "http://localhost:" + this.port + "/simple/transferencoding", String.class); + assertEquals(HttpStatus.OK, result.getStatusCode()); + assertEquals("missing", result.getBody()); + } + + @Test + public void postHasCorrectTransferEncoding() { + ResponseEntity result = new TestRestTemplate().postForEntity( + "http://localhost:" + this.port + "/simple/transferencoding", new HttpEntity<>("hello"), + String.class); + assertEquals(HttpStatus.OK, result.getStatusCode()); + assertEquals("missing", result.getBody()); + } + + // Don't use @SpringBootApplication because we don't want to component scan + @Configuration + @EnableAutoConfiguration + @RestController + @EnableZuulProxy + @RibbonClient(name = "simple", configuration = TestRibbonClientConfiguration.class) + static class ZuulProxyApplication { + + @RequestMapping(value = "/transferencoding", method = RequestMethod.GET) + public String get(@RequestHeader(name = "Transfer-Encoding", required = false) String transferEncoding) { + if (transferEncoding == null) { + return "missing"; + } + return transferEncoding; + } + + @RequestMapping(value = "/transferencoding", method = RequestMethod.POST) + public String post(@RequestHeader(name = "Transfer-Encoding", required = false) String transferEncoding, + @RequestBody String hello) { + if (transferEncoding == null) { + return "missing"; + } + return transferEncoding; + } + + } + + // Load balancer with fixed server list for "simple" pointing to localhost + @Configuration + static class TestRibbonClientConfiguration { + + @LocalServerPort + private int port; + + @Bean + public ServerList ribbonServerList() { + return new StaticServerList<>(new Server("localhost", this.port)); + } + + } +} +