diff --git a/spring-cloud-gateway-server-webmvc/src/main/java/org/springframework/cloud/gateway/server/mvc/common/MvcUtils.java b/spring-cloud-gateway-server-webmvc/src/main/java/org/springframework/cloud/gateway/server/mvc/common/MvcUtils.java index 871d0cbf7..93f834371 100644 --- a/spring-cloud-gateway-server-webmvc/src/main/java/org/springframework/cloud/gateway/server/mvc/common/MvcUtils.java +++ b/spring-cloud-gateway-server-webmvc/src/main/java/org/springframework/cloud/gateway/server/mvc/common/MvcUtils.java @@ -342,9 +342,14 @@ public static String stripContextPath(ServerRequest request, String path) { public static MultiValueMap encodeQueryParams(MultiValueMap params) { MultiValueMap encodedQueryParams = new LinkedMultiValueMap<>(params.size()); for (Map.Entry> entry : params.entrySet()) { - for (String value : entry.getValue()) { - encodedQueryParams.add(UriUtils.encode(entry.getKey(), StandardCharsets.UTF_8), - UriUtils.encode(value, StandardCharsets.UTF_8)); + String key = UriUtils.encode(entry.getKey(), StandardCharsets.UTF_8); + if (CollectionUtils.isEmpty(entry.getValue())) { + encodedQueryParams.put(key, entry.getValue()); + } + else { + for (String value : entry.getValue()) { + encodedQueryParams.add(key, UriUtils.encode(value, StandardCharsets.UTF_8)); + } } } return CollectionUtils.unmodifiableMultiValueMap(encodedQueryParams); diff --git a/spring-cloud-gateway-server-webmvc/src/test/java/org/springframework/cloud/gateway/server/mvc/ServerMvcIntegrationTests.java b/spring-cloud-gateway-server-webmvc/src/test/java/org/springframework/cloud/gateway/server/mvc/ServerMvcIntegrationTests.java index 81d4c8c1e..a8d2b269b 100644 --- a/spring-cloud-gateway-server-webmvc/src/test/java/org/springframework/cloud/gateway/server/mvc/ServerMvcIntegrationTests.java +++ b/spring-cloud-gateway-server-webmvc/src/test/java/org/springframework/cloud/gateway/server/mvc/ServerMvcIntegrationTests.java @@ -27,6 +27,7 @@ import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.function.Function; import java.util.function.Predicate; import com.github.benmanes.caffeine.cache.Caffeine; @@ -1023,6 +1024,44 @@ public void queryParamWithSpecialCharactersWorks() { }); } + @Test + public void queryParamWithoutValueWorks() { + restClient.get() + .uri("/queryParamWithoutValue?myparam") + .exchange() + .expectStatus() + .isOk() + .expectBody(Map.class) + .consumeWith(result -> { + Map responseBody = result.getResponseBody(); + assertThat(responseBody).containsKey("args"); + Map args = getMap(responseBody, "args"); + assertThat(args).containsKey("myparam"); + assertThat(args.get("myparam")).isEqualTo(""); + String url = (String) responseBody.get("url"); + assertThat(url).endsWith("/get?myparam"); + }); + } + + @Test + public void queryParamWithEmptyValueWorks() { + restClient.get() + .uri("/get?myparam=") + .exchange() + .expectStatus() + .isOk() + .expectBody(Map.class) + .consumeWith(result -> { + Map responseBody = result.getResponseBody(); + assertThat(responseBody).containsKey("args"); + Map args = getMap(responseBody, "args"); + assertThat(args).containsKey("myparam"); + assertThat(args.get("myparam")).isEqualTo(""); + String url = (String) responseBody.get("url"); + assertThat(url).endsWith("/get?myparam="); + }); + } + @Test public void clientResponseBodyAttributeWorks() { restClient.get() @@ -1690,6 +1729,30 @@ public RouterFunction gatewayRouterFunctionsReadResponseBody() { // @formatter:on } + @Bean + public RouterFunction gatewayRouterFunctionsEmptyQueryParamFilter() { + Function transformQueryParamWithoutValue = request -> ServerRequest + .from(request) + .params(params -> { + // transform any query parameter with empty string value to not have + // any value at all + for (var param : params.entrySet()) { + if (param.getValue().size() == 1 && param.getValue().get(0).isEmpty()) { + params.put(param.getKey(), Collections.emptyList()); + } + } + }) + .build(); + // @formatter:off + return route("testQueryParamWithoutValue") + .GET("/queryParamWithoutValue", http()) + .before(transformQueryParamWithoutValue) + .filter(new HttpbinUriResolver()) + .filter(setPath("/get")) + .build(); + // @formatter:on + } + @Bean public FilterRegistrationBean myFilter() { FilterRegistrationBean reg = new FilterRegistrationBean<>(new MyFilter()); diff --git a/spring-cloud-gateway-server-webmvc/src/test/java/org/springframework/cloud/gateway/server/mvc/common/MvcUtilsTests.java b/spring-cloud-gateway-server-webmvc/src/test/java/org/springframework/cloud/gateway/server/mvc/common/MvcUtilsTests.java index 9db79ae06..563abd212 100644 --- a/spring-cloud-gateway-server-webmvc/src/test/java/org/springframework/cloud/gateway/server/mvc/common/MvcUtilsTests.java +++ b/spring-cloud-gateway-server-webmvc/src/test/java/org/springframework/cloud/gateway/server/mvc/common/MvcUtilsTests.java @@ -17,11 +17,13 @@ package org.springframework.cloud.gateway.server.mvc.common; import java.util.Collections; +import java.util.Map; import org.junit.jupiter.api.Test; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import org.springframework.util.MultiValueMap; import org.springframework.web.servlet.function.ServerRequest; import static org.assertj.core.api.Assertions.assertThat; @@ -62,4 +64,11 @@ void stripContextPathNoOpWithoutContextPath() { assertThat(MvcUtils.stripContextPath(request, "/path")).isEqualTo("/path"); } + @Test + void encodeQueryParamsKeepsParamWithoutValue() { + var queryParams = MultiValueMap.fromMultiValue(Map.of("test", Collections.emptyList())); + var encoded = MvcUtils.encodeQueryParams(queryParams); + assertThat(encoded).isEqualTo(queryParams); + } + }