spring-cloud / spring-cloud-gateway

An API Gateway built on Spring Framework and Spring Boot providing routing and more.
http://cloud.spring.io
Apache License 2.0
4.55k stars 3.33k forks source link

Query Params (`request.params()`) being decoded while URI (`request.uri()`) is not when before calling downstream services #3494

Open jluiz20 opened 3 months ago

jluiz20 commented 3 months ago

Describe the bug When passing certain query parameters with encoded characters (like %26) I am expecting that they are passed encoded like this to the downstream services but what is happening is that they are being decoded.

For example, when using the example https://github.com/spencergibb/spring-cloud-gateway-mvc-sample src/main/java/com/example/gatewaymvcsample/Route01FirstRoute.java endpoint passing some encoded characters, we can see that they are being decoded in the parameters

GET localhost:8080/anything/first?company=H%26M&search=one%20two

image

The other weird part is that the URI is not encoded.

image

with the & in the parameters, when it calls the downstream services it does not behave well as it thinks is a parameter separation character and not part of the company parameter.

I was able to make it work by creating a before filter and encoding the query parameters before calling the downstream services but for me, it seems a bit of a workaround.

 public static Function<ServerRequest, ServerRequest> encodeRequestParameters() {
        return request -> ServerRequest.from(request)
                .params(queryParams -> queryParams.forEach((key, values) -> {
                    List<String> modifiedValues = values.stream()
                            .map(value -> UriUtils.encodeQueryParam(value, StandardCharsets.UTF_8))
                            .toList();
                    queryParams.put(key, modifiedValues);
                }))
                .build();
    }

I am not sure which behavior is correct (I think it should remain encoded) but the weird thing is that the parameters and URI are different (one encoded, the other not)

Is this the expected behavior?

Versions: org.springframework.boot:3.3.2 org.springframework.cloud:spring-cloud-dependencies:2023.0.3 org.springframework.cloud:spring-cloud-gateway-server-mvc:4.1.5

Sample You can just call the GET localhost:8080/anything/first?company=H%26M&search=one%20two for Route01FirstRoute in the sample project https://github.com/spencergibb/spring-cloud-gateway-mvc-sample

mdaepp commented 1 month ago

Possible fix:

public class ProxyExchangeHandlerFunction
        implements HandlerFunction<ServerResponse>, ApplicationListener<ContextRefreshedEvent> {

[...]

  @Override
  public ServerResponse handle(ServerRequest serverRequest) {
    URI uri = uriResolver.apply(serverRequest);
    boolean encoded = containsEncodedQueryOrPath(serverRequest.uri());
    // @formatter:off
    URI url = UriComponentsBuilder.fromUri(serverRequest.uri())
        .scheme(uri.getScheme())
        .host(uri.getHost())
        .port(uri.getPort())
        .replaceQueryParams(encoded ? UriUtils.encodeQueryParams(serverRequest.params()) : serverRequest.params())
        .build(encoded)
        .toUri();
    // @formatter:on

[...]

  private static boolean containsEncodedQueryOrPath(URI uri) {
    String rawQuery = uri.getRawQuery();
    return (rawQuery != null && rawQuery.contains("%")) || (uri.getRawPath() != null && uri.getRawPath().contains("%"));
  }

[...]

Of course, @jluiz20 would need to remove his workaround afterwards.