diff --git a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/filters/ProxyRequestHelper.java b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/filters/ProxyRequestHelper.java index 0f99a99e..85bdb599 100644 --- a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/filters/ProxyRequestHelper.java +++ b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/filters/ProxyRequestHelper.java @@ -249,6 +249,12 @@ public class ProxyRequestHelper { MultiValueMap headers) { } + /** + * Get url encoded query string. Pay special attention to single parameters with no values + * and parameter names with colon (:) from use of UriTemplate. + * @param params Un-encoded request parameters + * @return + */ public String getQueryString(MultiValueMap params) { if (params.isEmpty()) { return ""; @@ -260,10 +266,21 @@ public class ProxyRequestHelper { for (String value : params.get(param)) { query.append("&"); query.append(param); - if (!"".equals(value)) { - singles.put(param + i, value); + if (!"".equals(value)) { // don't add =, if original is ?wsdl, output is not ?wsdl= + String key = param; + // if form feed is already part of param name double + // since form feed is used as the colon replacement below + if (key.contains("\f")) { + key = (key.replaceAll("\f", "\f\f")); + } + // colon is special to UriTemplate + if (key.contains(":")) { + key = key.replaceAll(":", "\f"); + } + key = key + i; + singles.put(key, value); query.append("={"); - query.append(param + i); + query.append(key); query.append("}"); } i++; diff --git a/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/zuul/filters/ProxyRequestHelperTests.java b/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/zuul/filters/ProxyRequestHelperTests.java index a3f6f6ab..4d211dc0 100644 --- a/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/zuul/filters/ProxyRequestHelperTests.java +++ b/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/zuul/filters/ProxyRequestHelperTests.java @@ -259,6 +259,28 @@ public class ProxyRequestHelperTests { assertThat(queryString, is("?wsdl")); } + @Test + public void getQueryStringEncoded() { + MultiValueMap params = new LinkedMultiValueMap<>(); + params.add("foo", "weird#chars"); + + String queryString = new ProxyRequestHelper().getQueryString(params); + + assertThat(queryString, is("?foo=weird%23chars")); + } + + @Test + public void getQueryParamNameWithColon() { + MultiValueMap params = new LinkedMultiValueMap<>(); + params.add("foo:bar", "baz"); + params.add("foobar", "bam"); + params.add("foo\fbar", "bat"); // form feed is the colon replacement char + + String queryString = new ProxyRequestHelper().getQueryString(params); + + assertThat(queryString, is("?foo:bar=baz&foobar=bam&foo%0Cbar=bat")); + } + @Test public void buildZuulRequestURIWithUTF8() throws Exception { String encodedURI = "/resource/esp%C3%A9cial-char"; diff --git a/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/zuul/filters/route/restclient/RestClientRibbonCommandIntegrationTests.java b/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/zuul/filters/route/restclient/RestClientRibbonCommandIntegrationTests.java index 826a574b..7b1a4d8b 100644 --- a/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/zuul/filters/route/restclient/RestClientRibbonCommandIntegrationTests.java +++ b/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/zuul/filters/route/restclient/RestClientRibbonCommandIntegrationTests.java @@ -186,6 +186,17 @@ public class RestClientRibbonCommandIntegrationTests extends ZuulProxyTestBase { assertEquals("/query?foo=weird#chars", result.getBody()); } + @Test + public void simpleHostRouteWithColonParamNames() { + this.routes.addRoute("/self/**", "http://localhost:" + this.port + "/"); + this.endpoint.reset(); + ResponseEntity result = new TestRestTemplate().exchange( + "http://localhost:" + this.port + "/self/colonquery?foo:bar={foobar0}&foobar={foobar1}", HttpMethod.GET, + new HttpEntity<>((Void) null), String.class, "baz", "bam"); + assertEquals(HttpStatus.OK, result.getStatusCode()); + assertEquals("/colonquery?foo:bar=baz&foobar=bam", result.getBody()); + } + @Test public void simpleHostRouteWithContentType() { this.routes.addRoute("/self/**", "http://localhost:" + this.port + "/"); @@ -295,10 +306,15 @@ public class RestClientRibbonCommandIntegrationTests extends ZuulProxyTestBase { } @RequestMapping("/query") - public String addQuery(HttpServletRequest request, @RequestParam String foo) { + public String query(HttpServletRequest request, @RequestParam String foo) { return request.getRequestURI() + "?foo=" + foo; } + @RequestMapping("/colonquery") + public String colonQuery(HttpServletRequest request, @RequestParam(name = "foo:bar") String foobar0, @RequestParam(name = "foobar") String foobar1) { + return request.getRequestURI() + "?foo:bar=" + foobar0 + "&foobar=" + foobar1; + } + @RequestMapping("/matrix/{name}/{another}") public String matrix(@PathVariable("name") String name, @MatrixVariable(value = "p", pathVar = "name") int p,