spring-cloud-gateway icon indicating copy to clipboard operation
spring-cloud-gateway copied to clipboard

gateway MVC - Multi-part + query string not working

Open florentm35 opened this issue 1 year ago • 15 comments

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

florentm35 avatar Jan 16 '24 09:01 florentm35

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

@spencergibb kindly help to check this issue

imdr0id avatar Feb 15 '24 09:02 imdr0id

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

spencergibb avatar Mar 11 '24 20:03 spencergibb

i will try tomorrow

florentm35 avatar Mar 15 '24 14:03 florentm35

After test, the problem is the same : image image

florentm35 avatar Mar 16 '24 08:03 florentm35

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(); }

lohoso avatar Apr 05 '24 02:04 lohoso

Run the full build with that removed to see tests fail

spencergibb avatar Apr 05 '24 02:04 spencergibb

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

YoungTakhin avatar Apr 15 '24 14:04 YoungTakhin

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

dada1804 avatar May 02 '24 06:05 dada1804

@spencergibb Any leads on this?

dada1804 avatar May 20 '24 06:05 dada1804

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 avatar May 21 '24 13:05 dada1804

@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?

dada1804 avatar Jun 10 '24 05:06 dada1804

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

diegodcp avatar Jul 10 '24 20:07 diegodcp

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;
}

} `

imdr0id avatar Jul 11 '24 03:07 imdr0id

@lohoso @spencergibb Any leads around the flow?

dada1804 avatar Jul 16 '24 10:07 dada1804

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();
    }
}

rworsnop avatar Aug 06 '24 21:08 rworsnop