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.53k stars 3.32k forks source link

gateway MVC - Multi-part + query string not working #3220

Open florentm35 opened 9 months ago

florentm35 commented 9 months ago

Describe the bug

With spring cloud gateway mvc 2023.0.0 when we perform an post request with multi part and query string, the query string was loose.

Exemple with an test server on port 9008 and a gateway on port 9007 if i point on the server directly : image image if i use the gateway : image image

Sample http-interceptor : (it's juste a test server to print the query param)

public class MainStandAlone {

    public static void main(String[] args) throws IOException {

        ThreadPoolExecutor threadPoolExecutor = (ThreadPoolExecutor) Executors.newFixedThreadPool(10);

        HttpServer server = HttpServer.create(new InetSocketAddress("localhost", 9008), 0);
          server.createContext("/", exchange -> {
            System.out.println(exchange.getRequestURI());

            exchange.getResponseBody().write("OK".getBytes(StandardCharsets.UTF_8));
            exchange.sendResponseHeaders(200, "OK".length());
            exchange.getResponseBody().flush();
            exchange.getResponseBody().close();
        });
        server.setExecutor(threadPoolExecutor);
        server.start();
        System.out.println(" Server started on port 9008");
    }
}

gateway : (spring initializr)

@SpringBootApplication
public class DemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }

    @Configuration
    public static class GatewayConfiguration {

        @Bean
        public RouterFunction<ServerResponse> routerFunction() {
            return route()
                    .POST("/test", http("http://localhost:9008"))
                    .build();
        }
    }
}

EDIT:

Moreover the header Content-Length are delete by the gateway

imdr0id commented 8 months ago

I'm experiencing the same issue with spring cloud gateway mvc.

@spencergibb kindly help to check this issue

spencergibb commented 8 months ago

There have been a number of fixes in this are recently, can either of you try with 2023.0.1-SNAPSHOT?

florentm35 commented 7 months ago

i will try tomorrow

florentm35 commented 7 months ago

After test, the problem is the same : image image

lohoso commented 7 months ago

Hi, after removing below codes in GatewayMultipartHttpServletRequest of file GatewayMvcMultipartResolver.java ,
the query string can work now. why do we have below logics, for performane?

static class GatewayMultipartHttpServletRequest extends StandardMultipartHttpServletRequest { @Override protected void initializeMultipart() { //if (!isGatewayRequest(getRequest())) { //super.initializeMultipart(); //} } @Override public Map<String, String[]> getParameterMap() { //if (isGatewayRequest(getRequest())) { //return Collections.emptyMap(); //} return super.getParameterMap(); }

spencergibb commented 7 months ago

Run the full build with that removed to see tests fail

YoungTakhin commented 6 months ago

I have the same problem! How should this problem be solved? I use Spring Cloud Gateway MVC 4.1.2 and Spring Boot 3.2.4

dada1804 commented 6 months ago

I am having the same issue, I use Spring Cloud Gateway MVC 4.1.2 and Spring 3.2.3

dada1804 commented 5 months ago

@spencergibb Any leads on this?

dada1804 commented 5 months ago

Hi, after removing below codes in GatewayMultipartHttpServletRequest of file GatewayMvcMultipartResolver.java , the query string can work now. why do we have below logics, for performane?

static class GatewayMultipartHttpServletRequest extends StandardMultipartHttpServletRequest { @OverRide protected void initializeMultipart() { //if (!isGatewayRequest(getRequest())) { //super.initializeMultipart(); //} } @OverRide public Map<String, String[]> getParameterMap() { //if (isGatewayRequest(getRequest())) { //return Collections.emptyMap(); //} return super.getParameterMap(); }

How do I edit this? @lohoso

dada1804 commented 5 months ago

@lohoso @spencergibb The project is on hold for a long time now, I need to get this done. Can somebody help me with the same?

diegodcp commented 4 months ago

same issue with Spring Cloud Gateway MVC 4.1.4 and Spring Boot 3.2.5, any update or workaround on this? Thanks

imdr0id commented 4 months ago

I had the same issue, i ended up overriding the ProxyExchangeHandlerFunction, `package com.apigateway;

import lombok.extern.Slf4j.Slf4j; import org.springframework.beans.factory.ObjectProvider; import org.springframework.cloud.gateway.server.mvc.common.MvcUtils; import org.springframework.cloud.gateway.server.mvc.filter.HttpHeadersFilter; import org.springframework.cloud.gateway.server.mvc.handler.ProxyExchange; import org.springframework.cloud.gateway.server.mvc.handler.ProxyExchangeHandlerFunction; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; import org.springframework.stereotype.Component; import org.springframework.web.client.HttpClientErrorException; import org.springframework.web.servlet.function.ServerRequest; import org.springframework.web.servlet.function.ServerResponse; import org.springframework.web.util.UriComponentsBuilder;

import java.net.URI; import java.util.Set; import java.util.function.Function; import java.util.stream.Stream;

@Slf4j @Component public class ProxyExchangeHandler extends ProxyExchangeHandlerFunction {

private static final String X_METHOD = "X-METHOD";

private final Set<HttpMethod> httpMethodSet = Set.of(HttpMethod.GET, HttpMethod.OPTIONS, HttpMethod.HEAD, HttpMethod.POST, HttpMethod.PUT, HttpMethod.PATCH, HttpMethod.DELETE);

private final ProxyExchange proxyExchange;

private final ObjectProvider<HttpHeadersFilter.RequestHttpHeadersFilter> requestHttpHeadersFilters;
private final ObjectProvider<HttpHeadersFilter.ResponseHttpHeadersFilter> responseHttpHeadersFilters;
private final ProxyExchangeHandlerFunction.URIResolver uriResolver;

public ProxyExchangeHandler(ProxyExchange proxyExchange, ObjectProvider<HttpHeadersFilter.RequestHttpHeadersFilter> requestHttpHeadersFilters, ObjectProvider<HttpHeadersFilter.ResponseHttpHeadersFilter> responseHttpHeadersFilters) {
    super(proxyExchange, requestHttpHeadersFilters, responseHttpHeadersFilters);
    this.proxyExchange = proxyExchange;
    this.requestHttpHeadersFilters = requestHttpHeadersFilters;
    this.responseHttpHeadersFilters = responseHttpHeadersFilters;
    uriResolver = (request) -> {
        return (URI) request.attribute(MvcUtils.GATEWAY_REQUEST_URL_ATTR).orElseThrow(() -> {
            return new IllegalStateException("No route resolved");
        });
    };
}

public ServerResponse hanfle(ServerRequest serverRequest) {
    URI uri = uriResolver.apply(serverRequest);
    String method= serverRequest.headers().firstHeader(X_METHOD);
    boolean encoded = containsEncodedQuery(uri);

    if(method==null){
        method=serverRequest.method().name();
    }

    HttpMethod httpMethod = HttpMethod.valueOf(method.toUpperCase());
    if(!httpMethodSet.contains(httpMethod)){
        throw new HttpClientErrorException(HttpStatus.BAD_REQUEST,"Invalid method value: "+method+" in "+X_METHOD+" header");
    }

    URI url = UriComponentsBuilder.fromUri(uri)
            .scheme(uri.getScheme())
            .host(uri.getHost())
            .port(uri.getPort())
            //.replaceQueryParams(serverRequest.params())
            .build(encoded)
            .toUri();

    HttpHeaders filteredRequestHeaders = filterHeaders(this.requestHttpHeadersFilters.orderedStream().map(Function.identity()), serverRequest.headers().asHttpHeaders(), serverRequest);

    boolean preserveHost = (boolean) serverRequest.attributes()
            .getOrDefault(MvcUtils.PRESERVE_HOST_HEADER_ATTRIBUTE, false);
    if (preserveHost) {
        filteredRequestHeaders.set(HttpHeaders.HOST, serverRequest.headers().firstHeader(HttpHeaders.HOST));
    } else {
        filteredRequestHeaders.remove(HttpHeaders.HOST);
    }
    ProxyExchange.Request proxyRequest = proxyExchange.request(serverRequest).uri(uri)
            .headers(filteredRequestHeaders)
            .responseConsumer((response, serverResponse) -> {
                HttpHeaders headers = filterHeaders(this.responseHttpHeadersFilters.orderedStream().map(Function.identity()), response.getHeaders(), serverResponse);
                serverResponse.headers().putAll(headers);
            }).build();

    return proxyExchange.exchange(proxyRequest);

}

private <TYPE> HttpHeaders filterHeaders(Stream<HttpHeadersFilter<TYPE>> filters, HttpHeaders headers, TYPE t) {
    HttpHeaders filtered = headers;
    for (var filter : filters.toList()) {
        filtered = filter.apply(filtered, t);
    }
    return filtered;

}

private static boolean containsEncodedQuery(URI uri) {
    boolean encoded = (uri.getRawQuery() != null && uri.getRawQuery().contains("%"))
            || (uri.getRawPath() != null && uri.getRawPath().contains("%"));
    if (encoded) {
        try {
            UriComponentsBuilder.fromUri(uri).build(true);
            return true;
        } catch (IllegalArgumentException ignored) {
            if (log.isTraceEnabled()) {
                log.trace("Error in containsEncodedParts", ignored);
            }
        }
        return false;
    }
    return encoded;
}

} `

dada1804 commented 3 months ago

@lohoso @spencergibb Any leads around the flow?

rworsnop commented 3 months ago

I worked around this by providing my own MultipartResolver bean.

@Component
public class PassthroughMultiPartResolver implements MultipartResolver {
    @Override
    public boolean isMultipart(HttpServletRequest request) {
        // Always pass through multipart requests without parsing
        return false;
    }

    @Override
    public MultipartHttpServletRequest resolveMultipart(HttpServletRequest request) throws MultipartException {
        throw new UnsupportedOperationException();
    }

    @Override
    public void cleanupMultipart(MultipartHttpServletRequest request) {
        throw new UnsupportedOperationException();
    }
}