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

Forward Routing filter doesn't support path parameters for local forwarding #2092

Open Anmol24 opened 3 years ago

Anmol24 commented 3 years ago

Describe the bug I am using forward scheme in my route to forward the request to local controller defined in the gateway application. My controller's API path is something like this:

/api/v1/service/{id}/path where id is path parameter. As per the documentation of Forward Routing filter, the path part of the request URL is overridden with the path in the forward URL. That means that whatever was mentioned in the forward:/mycustompath will be added to the url. In this case, i can't route the request to my controller where id will change for each request.

Is there a way to achieve this? We were earlier using Spring cloud zuul and we are now migrating to spring cloud gateway and this is not working for us. I am trying to write a custom filter for this where i modify the correct path in the filter. Is this correct approach for this?

Sample image

ctlove0523 commented 3 years ago

RewritePathGatewayFilterFactory can help you?

Anmol24 commented 3 years ago

@ctlove0523 that wouldn't work for me as i need to forward the request to local endpoint within the gateway itself. The ForwardPathFilter modifies the request for me and i will have to write my own custom filter for this to handle the path as mentioned in the ticket above.

spencergibb commented 3 years ago

We should add some things to the exchange attributes like in ForwardPathFilter:

Anmol24 commented 3 years ago

Thanks @spencergibb that's exactly what i need to preserve the original path with request parameters. This was already being done in spring cloud zuul. Any idea if this will be available in spring cloud gateway anytime soon?

spencergibb commented 3 years ago

Not immediately. TBH a pull request will be there fastest way

Anmol24 commented 3 years ago

@spencergibb will simply adding the additional exchange attributes help? wouldn't we also have to modify the ForwardPathFilter to preserve the request and route it as it to the local endpoint.

Route route = exchange.getAttribute(GATEWAY_ROUTE_ATTR);
URI routeUri = route.getUri();
String scheme = routeUri.getScheme();
if (isAlreadyRouted(exchange) || !"forward".equals(scheme)) {
    return chain.filter(exchange);
}
exchange = exchange.mutate()
            .request(exchange.getRequest().mutate().path(routeUri.getPath()).build())
            .build();
return chain.filter(exchange);

As per the current implementation of ForwardPathFilter it just overrides anything after forward:/{newpath} and add this to the request path. What we want is to preserve the path and query parameters.

Another difference between spring cloud zuul and cloud gateway is the order of RoutePredicateHandlerMapping and ZuulHandlerMapping. ZuulHandlerMapping intercepts the request first with forward scheme, runs the zuul filters and then forwards the same request to local endpoint defined in the gateway application, where as in cloud gateway the request is intercepted first by RequestMappingHandlerMapping even if the gateway has defined a route with forward scheme. The spring cloud gateway filters are not run if the request is already handled by Request mapping handler.

spencergibb commented 3 years ago

The trouble is what part of the existing path should go there?

Anmol24 commented 3 years ago

if the original path has path variables and query parameters then ideally these should be added.

hwanders commented 2 years ago

I just found another use-case for this suggestion: We have multiple SPAs in our static resources, for each app all unmatched paths should be forwarded to index.html:

uri: forward:/app/{appName}/index.html
predicates:
  - Path=/app/{appName}/**

Currently we are using a workaround using a dummy uri (forward:...) with a SetPath filter instead.

Anmol24 commented 2 years ago

What i ended up doing in my code was to write a custom filter to route to the internal path to my uri.

uri: forward:/
predicates:
      - Path=/api/v1/abc/*/xyz
      - Method=GET

Here is the CustomForwardPathFilter that i ended up creating:

@Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        Route route = exchange.getAttribute(GATEWAY_ROUTE_ATTR);

        URI routeUri = route.getUri();
        String scheme = routeUri.getScheme();
        if (isAlreadyRouted(exchange) || !"forward".equals(scheme)) {
            return chain.filter(exchange);
        }
        ServerHttpRequest request = exchange.getRequest();
        String customPath = String.format("/internal%s", request.getPath().toString());

        return Mono.just(exchange.mutate()
                .request(exchange.getRequest().mutate().path(customPath).build())
                .build())
                .doOnEach(LogHelper.logOnNext(webExchange -> log.debug("Forwarding to URI: {}", webExchange.getRequest().getPath())))
                .flatMap(webExchange -> this.getDispatcherHandler().handle(webExchange));
    }

    @Override
    public int getOrder() {
        return 0;
    }

This should run before the ForwardPathFilter . Here is my mapping for the api defined in the gateway application.

@GetMapping(path = "/internal/api/v1/abc/{id}/xyz")
    public Mono<String> getResponse() {
       // custom code here
    }

The filter modifies the request from /api/v1/abx/*/xyz to /internal/api/v1/abx/*/xyz.

leventozkann commented 2 years ago

@Anmol24 can you please share the whole filter?

Anmol24 commented 2 years ago

@leventozkann You only need to add this additionally to use the dispatcher handler bean, rest of the code is same.

private final ObjectProvider<DispatcherHandler> dispatcherHandlerProvider;

    // do not use this dispatcherHandler directly, use getDispatcherHandler() instead.
    private volatile DispatcherHandler dispatcherHandler;

    public MyForwardPathFilter(
            ObjectProvider<DispatcherHandler> dispatcherHandlerProvider) {
        this.dispatcherHandlerProvider = dispatcherHandlerProvider;
    }

    private DispatcherHandler getDispatcherHandler() {
        if (dispatcherHandler == null) {
            dispatcherHandler = dispatcherHandlerProvider.getIfAvailable();
        }

        return dispatcherHandler;
    }
ctlove0523 commented 2 years ago

你好,你的邮件我已经收到,每天会在晚上查看邮件并回复。急事请打电话

deepak-chhetri commented 8 months ago

@spencergibb Is this feature released in 4.x spring cloud gateway version? If not then by when it will become available?