From 3bb1562b7fd0a7bd569218d7f32fde19757ab503 Mon Sep 17 00:00:00 2001 From: Spencer Gibb Date: Fri, 15 May 2015 12:26:32 -0600 Subject: [PATCH] fix RibbonClientHttpRequestFactory to add headers and accept a body fixes gh-350 --- .../RibbonClientHttpRequestFactory.java | 55 ++++-- .../RibbonClientHttpRequestFactoryTests.java | 159 ++++++++++++++++++ 2 files changed, 200 insertions(+), 14 deletions(-) create mode 100644 spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/ribbon/RibbonClientHttpRequestFactoryTests.java diff --git a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/ribbon/RibbonClientHttpRequestFactory.java b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/ribbon/RibbonClientHttpRequestFactory.java index 4347d193..b1157283 100644 --- a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/ribbon/RibbonClientHttpRequestFactory.java +++ b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/ribbon/RibbonClientHttpRequestFactory.java @@ -16,6 +16,7 @@ package org.springframework.cloud.netflix.ribbon; +import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; @@ -25,7 +26,6 @@ import java.util.Map; import org.springframework.cloud.client.ServiceInstance; import org.springframework.cloud.client.loadbalancer.LoadBalancerClient; -import org.springframework.cloud.client.loadbalancer.LoadBalancerRequest; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; @@ -67,48 +67,58 @@ public class RibbonClientHttpRequestFactory implements ClientHttpRequestFactory //@formatter:off IClientConfig clientConfig = clientFactory.getClientConfig(instance.getServiceId()); RestClient client = clientFactory.getClient(instance.getServiceId(), RestClient.class); - HttpRequest request = HttpRequest.newBuilder() - .uri(uri) - .verb(HttpRequest.Verb.valueOf(httpMethod.name())) - .build(); + HttpRequest.Verb verb = HttpRequest.Verb.valueOf(httpMethod.name()); //@formatter:on - return new RibbonHttpRequest(request, client, clientConfig); + return new RibbonHttpRequest(uri, verb, client, clientConfig); } public class RibbonHttpRequest extends AbstractClientHttpRequest { - private HttpRequest request; + private HttpRequest.Builder builder; + private URI uri; + private HttpRequest.Verb verb; private RestClient client; private IClientConfig config; + private ByteArrayOutputStream outputStream = null; - @SuppressWarnings("deprecation") - public RibbonHttpRequest(HttpRequest request, RestClient client, + public RibbonHttpRequest(URI uri, HttpRequest.Verb verb, RestClient client, IClientConfig config) { - this.request = request; + this.uri = uri; + this.verb = verb; this.client = client; this.config = config; - request.getHeaders().putAll(getHeaders()); + this.builder = HttpRequest.newBuilder().uri(uri).verb(verb); } @Override public HttpMethod getMethod() { - return HttpMethod.valueOf(request.getVerb().name()); + return HttpMethod.valueOf(verb.name()); } @Override public URI getURI() { - return request.getUri(); + return uri; } @Override protected OutputStream getBodyInternal(HttpHeaders headers) throws IOException { - throw new RuntimeException("Not implemented"); + if (outputStream == null) { + outputStream = new ByteArrayOutputStream(); + } + return outputStream; } @Override + @SuppressWarnings("deprecation") protected ClientHttpResponse executeInternal(HttpHeaders headers) throws IOException { try { + addHeaders(headers); + if (outputStream != null) { + outputStream.close(); + builder.entity(outputStream.toByteArray()); + } + HttpRequest request = builder.build(); HttpResponse response = client.execute(request, config); return new RibbonHttpResponse(response); } catch (Exception e) { @@ -123,6 +133,23 @@ public class RibbonClientHttpRequestFactory implements ClientHttpRequestFactory } });*/ } + + private void addHeaders(HttpHeaders headers) { + for (String name : headers.keySet()) { + // apache http RequestContent pukes if there is a body and + // the dynamic headers are already present + if (!isDynamic(name) || outputStream == null) { + List values = headers.get(name); + for (String value : values) { + builder.header(name, value); + } + } + } + } + + private boolean isDynamic(String name) { + return name.equals("Content-Length") || name.equals("Transfer-Encoding"); + } } public class RibbonHttpResponse extends AbstractClientHttpResponse { diff --git a/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/ribbon/RibbonClientHttpRequestFactoryTests.java b/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/ribbon/RibbonClientHttpRequestFactoryTests.java new file mode 100644 index 00000000..8a231994 --- /dev/null +++ b/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/ribbon/RibbonClientHttpRequestFactoryTests.java @@ -0,0 +1,159 @@ +/* + * 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; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import java.net.URI; +import java.util.Arrays; + +import lombok.SneakyThrows; + +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.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.test.SpringApplicationConfiguration; +import org.springframework.boot.test.WebIntegrationTest; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpStatus; +import org.springframework.http.RequestEntity; +import org.springframework.http.ResponseEntity; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.web.bind.annotation.PathVariable; +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.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.client.RestTemplate; + +import com.netflix.loadbalancer.BaseLoadBalancer; +import com.netflix.loadbalancer.ILoadBalancer; +import com.netflix.loadbalancer.Server; + +/** + * @author Spencer Gibb + */ +@RunWith(SpringJUnit4ClassRunner.class) +@SpringApplicationConfiguration(classes = RibbonClientHttpRequestFactoryTests.App.class) +@WebIntegrationTest(value = { "spring.application.name=ribbonclienttest", + "spring.jmx.enabled=true" }, randomPort = true) +@DirtiesContext +public class RibbonClientHttpRequestFactoryTests { + + @Autowired + private RestTemplate restTemplate; + + @Test + public void requestFactoryIsRibbon() { + assertTrue("wrong RequestFactory type", restTemplate.getRequestFactory() instanceof RibbonClientHttpRequestFactory); + } + + @Test + public void vanillaRequestWorks() { + ResponseEntity response = restTemplate.getForEntity("http://simple/", + String.class); + assertEquals("wrong response code", HttpStatus.OK, response.getStatusCode()); + assertEquals("wrong response body", "hello", response.getBody()); + } + + @Test + public void requestWithPathParamWorks() { + ResponseEntity response = restTemplate.getForEntity("http://simple/path/{param}", + String.class, "world"); + assertEquals("wrong response code", HttpStatus.OK, response.getStatusCode()); + assertEquals("wrong response body", "hello world", response.getBody()); + } + + @Test + public void requestWithRequestParamWorks() { + ResponseEntity response = restTemplate.getForEntity("http://simple/request?param={param}", String.class, "world"); + assertEquals("wrong response code", HttpStatus.OK, response.getStatusCode()); + assertEquals("wrong response body", "hello world", response.getBody()); + } + + @Test + public void requestWithPostWorks() { + ResponseEntity response = restTemplate.postForEntity("http://simple/post", "world", String.class); + assertEquals("wrong response code", HttpStatus.OK, response.getStatusCode()); + assertEquals("wrong response body", "hello world", response.getBody()); + } + + @Test + @SneakyThrows + public void requestWithHeaderWorks() { + RequestEntity entity = RequestEntity.get(new URI("http://simple/header")) + .header("X-Param", "world") + .build(); + ResponseEntity response = restTemplate.exchange(entity, String.class); + assertEquals("wrong response code", HttpStatus.OK, response.getStatusCode()); + assertEquals("wrong response body", "hello world", response.getBody()); + } + + @Configuration + @EnableAutoConfiguration + @RestController + @RibbonClient(value = "simple", configuration = SimpleRibbonClientConfiguration.class) + protected static class App { + + @RequestMapping("/") + public String hi() { + return "hello"; + } + + @RequestMapping("/path/{param}") + public String hiParam(@PathVariable("param") String param) { + return "hello "+param; + } + + @RequestMapping("/request") + public String hiRequest(@RequestParam("param") String param) { + return "hello "+param; + } + + @RequestMapping(value = "/post", method = RequestMethod.POST) + public String hiPost(@RequestBody String param) { + return "hello "+param; + } + + @RequestMapping("/header") + public String hiHeader(@RequestHeader("X-Param") String param) { + return "hello "+param; + } + } +} + +@Configuration +class SimpleRibbonClientConfiguration { + + @Value("${local.server.port}") + private int port = 0; + + @Bean + public ILoadBalancer ribbonLoadBalancer() { + BaseLoadBalancer balancer = new BaseLoadBalancer(); + balancer.setServersList(Arrays.asList(new Server("localhost", port))); + return balancer; + } + +} \ No newline at end of file