Open anjuna opened 4 years ago
spring.webflux.base-path
is new so it doesn't surprise me that there are problems. Does Path=/myapp/api/**
work?
I'll leave the solution to later since I haven't looked at it yet.
Using Path=/myapp/api/**
does work although the StripPrefix
filter is then applied to the myapp
segment.
So for example, hitting http://localhost:8080/myapp/api/maps
returns a 404 from google saying "/api/maps was not found"
sure, you'd need to make it StripPrefix=2
So it works with:
spring.webflux.base-path: /myapp
spring.cloud.gateway:
routes:
- id: google-route
uri: https://www.google.com/
predicates:
- Path=${spring.webflux.base-path}/api/**
filters:
- StripPrefix=2
But then if base-path:
is set, ie. the normal case with no overriding base path, then only StripPrefix=1
works.
I've also got related problems with authentication endpoints set on ServerHttpSecurity
, but that might be covered by issue 21679 on spring-boot.
After discussing it, we don't want to add support for this in the various places it would need to go (because of our previous experience with zuul). Instead, we will add some documentation noting things mentioned in this issue.
This was a hard requirement for us so I implemented a workaround:
http
.addFilterAt(new StripsProxyPathFilter, SecurityWebFiltersOrder.FIRST)
//etc
Where the StripsProxyPathFilter
just takes the substring of the url without the basePath.
I'm interested why the team don't want to re-implement this - what's the past experience of zuul that influenced this decision?
Thanks.
This is my workaround , and it work with SCG and webflux. (Sorry i don't know how to fix the code layout.....).
@Bean
@Primary
public HttpHandler httpHandler() {
HttpHandler httpHandler = WebHttpHandlerBuilder.applicationContext(this.applicationContext).build();
return new ContextPathHttpHandler(httpHandler, "/yourContextPathHere");
}
class ContextPathHttpHandler implements HttpHandler {
private final HttpHandler delegate;
private final String contextPath;
public ContextPathHttpHandler(HttpHandler delegate, String contextPath) {
this.delegate = delegate;
this.contextPath = contextPath;
}
@Override
public Mono<Void> handle(ServerHttpRequest request, ServerHttpResponse response) {
return delegate.handle(withoutContextPath(request), withoutCache(response));
}
private ServerHttpRequest withoutContextPath(ServerHttpRequest request) {
String path = request.getPath().value();
if (path.startsWith(contextPath)) {
String pathWithApplication = path.substring(contextPath.length());
if(!StringUtils.hasText(pathWithApplication)){
pathWithApplication="/";
}
return request.mutate().path(pathWithApplication).build();
}
return request;
}
private ServerHttpResponse withoutCache(ServerHttpResponse response) {
response.getHeaders().set("cache-control", "no-store");
return response;
}
}
After some experiments and diving into how the filters work, I seem to have gotten around this issue. In case someone is still out there trying to see how to use spring.webflux.base-path
with Spring Cloud Gateway, here's what I tried to make it work.
The code is in Kotlin.
@Component
class StripContextAndPrefixGatewayFilterFactory :
AbstractGatewayFilterFactory<StripContextAndPrefixGatewayFilterFactory.Config>(Config::class.java) {
override fun apply(config: Config): GatewayFilter {
return GatewayFilter { exchange: ServerWebExchange, chain: GatewayFilterChain ->
val builder: ServerHttpRequest.Builder = exchange.request.mutate()
val request = exchange.request
ServerWebExchangeUtils.addOriginalRequestUrl(exchange, request.uri)
val path = request.uri.rawPath
val pathWithoutBase = path.removePrefix(config.basePath ?: "")
val suffixPath =
"/" + Arrays.stream(StringUtils.tokenizeToStringArray(pathWithoutBase, "/")).skip(config.parts.toLong())
.collect(Collectors.joining("/"))
chain.filter(
exchange.mutate().request(builder.contextPath(suffixPath).path(suffixPath).build()).build()
)
}
}
data class Config(
val basePath: String? = null,
val parts: Int = 0
)
}
This class is a modified version of StripPrefix
filter.
The value of base
in Config
is ${spring.webflux.basePath}
.
The trick is to make sure the path
contains the contextPath
of the request.
builder.contextPath(suffixPath).path(suffixPath).build()
did it for me.
I had similar issue and quite a similar solution as well, I created a filter to automatically strip the basePath in outgoing requests. This filter don't need any parameter.
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.stereotype.Component;
/**
* @author skerdudou Remove basePath from outgoing request
*/
@Component
public class StripBasePathGatewayFilterFactory extends AbstractGatewayFilterFactory<Object> {
@Value("${spring.webflux.base-path}")
private String basePath;
@Override
public GatewayFilter apply(Object config) {
return (exchange, chain) -> {
ServerHttpRequest req = exchange.getRequest();
String path = req.getURI().getRawPath();
String newPath = path.replaceFirst(basePath, "");
ServerHttpRequest request = req.mutate().path(newPath).contextPath(null).build();
return chain.filter(exchange.mutate().request(request).build());
};
}
}
The above solution provided by @skerdudou works just fine (though @sumitsarkar's solution also looks similar but I haven't checked on it). But for those wondering how to use (register) this custom filter, focus on the class name. The pattern in class name is like {filter name}GatewayFilterFactory
. So here, for StripBasePathGatewayFilterFactory
, filter name will be StripBasePath
, and to use it in application.properties
(or yaml) file, we need to define it like - spring.cloud.gateway.routes[0].filters=StripBasePath=1
Ok, this has literally had me stuck for a full week until I found this ticket. The "promised" documentation changes do not exist in the current Spring Cloud Gateway documentation, so if you add a spring.webflux.base-path things just plain stop working with no explanation of why.
Also, use of a base path is a requirement for our application, because we are adapting an existing Zuul API Gateway, with many people already having access to existing URLs. So, not using spring.webflux.base-path is not an option. And, trying to add the base path to all of the paths in the application also do not work, because things deeper like Actuator and Security start breaking if that is done.
Can someone please update the documentation to either include some of the workarounds in this ticket, or to give the "proper" way to have a base URL for the Spring Cloud Gateway?
Hello and thank you everyone for your work.
I want to mention that this problem still persists and we do not found an official way to handle this issue. When setting a spring.webflux.base-path property, we get a 404 on a simple (previously working) route:
routes:
- id: backend
uri: http://localhost:9090
predicates:
- Path=/api/**
filters:
- StripPrefix=1
We are on Spring-boot 3.0.5 & Spring-cloud 2022.0.2 (should be the latests at the moment).
Hi I am working with the spring.webflux.base-path property set to say "BasePath", and using a route such as
-id: service1
uri: https://xyz/
predicates:
-Path=/BasePath/api/**
filters:
-StripBasePath=1
And for the StripBasePath implementation I worked around the implementation given by @skerdudou . It works fine when I try to hit the url https://xyz/api/**
using the api server , https://abc/BasePath/api/**
but whenever I am trying to make any post request using the api gateway it takes me to https://abc/api/**
which won't work. Is there a way i can include the BasePath in the post requests but I do not want it when it hits the actual service.
I want to add parameters to determine whether to set the 'BasePath' prefix for '- Path=/api' based on the parameters. Can I contribute to the code. @spencergibb
Hi! We still need a solution to this problem! Is there any progress?
Indeed it's very weird that the base-path is pasted all over requests even on redirections, but not where we is needed.
The solution propose skerdudou works just fine for me.
My usecase adds routes programatly, and have a requirement to use contextpath, so here is part of solution I made using the proposed workaround :
@Value("${spring.webflux.base-path}")
private String contextPath;
..
String appSubContext = "myApp"
// NOTE : contextpath required on route or it wont match the incoming request otherwise
routes.route(r -> r.path(contextPath + apiRute)
.filters(addContext(appSubContext))
...
protected Function<GatewayFilterSpec, UriSpec> addContext(String context) {
// NOTE: order of filters matters
return f -> f
.filter(stripBasePathGatewayFilterFactory.apply(this))
.rewritePath("/(?<segment>.*)", context + "/${segment}");
}
Hope this gives more insights of the problem.
Since I have also wasted almost a week of my time because of this issue, here is my solution:
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
import lombok.Data;
import org.springframework.boot.autoconfigure.web.reactive.WebFluxProperties;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import org.springframework.cloud.gateway.support.ServerWebExchangeUtils;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
@Component
public class StripBasePathGatewayFilterFactory extends AbstractGatewayFilterFactory<StripBasePathGatewayFilterFactory.Config> {
private final String basePath;
public StripBasePathGatewayFilterFactory(WebFluxProperties webFluxProperties) {
super(Config.class);
this.basePath = webFluxProperties.getBasePath();
}
@Override
public GatewayFilter apply(Config config) {
return (exchange, chain) -> {
ServerWebExchangeUtils.addOriginalRequestUrl(exchange, exchange.getRequest().getURI());
String path = exchange.getRequest().getURI().getRawPath();
String pathWithoutBase = path.replaceFirst(basePath != null ? basePath : "", "");
String suffixPath = "/" + Arrays.stream(StringUtils.tokenizeToStringArray(pathWithoutBase, "/"))
.skip(config.getParts())
.collect(Collectors.joining("/"));
ServerHttpRequest modifiedRequest = exchange.getRequest().mutate()
.contextPath(suffixPath)
.path(suffixPath)
.build();
return chain.filter(exchange.mutate().request(modifiedRequest).build());
};
}
@Override
public List<String> shortcutFieldOrder() {
return List.of("parts");
}
@Data
public static class Config {
private int parts;
}
}
This filter must be used on all gateway routes with the correct number of parts. As of my understanding parts is the number of the gateway route path filter predicate.
spring:
webflux:
base-path: /api
cloud:
gateway:
routes:
- id: todo-service
uri: lb://todo-service
predicates:
- Path=/api/todo/**
filters:
- StripBasePath=1 # <-- one part because of /todo
- RewritePath=/todo/?(?<segment>.*), /$\{segment}
- id: todo-service-with-multi-part
uri: lb://todo-service
predicates:
- Path=/api/todo/service/**
filters:
- StripBasePath=2 # <-- two parts because of /todo/service
- RewritePath=/todo/service/?(?<segment>.*), /$\{segment}
Hope this helps others who encounter the same problem :)
Thanks @marhali! It works!
This is a fairly frustrating difference between how the MVC and Flux variants behave.
I started with an MVC gateway as I had a preexisting security implementation that was MVC based. I however encountered an issue with the MVC implementation so decided to try the Flux gateway.
After some time reworking my security layer, I came to find this issue. I have attempted a number of the suggested workarounds, however, with an ingress, Springdoc Flux, and other variables, I am stuck in a place where things are not working as expected. Things that were working fine with the MVC gateway.
The most desirable change would be to get the Flux variant to use the base-path
much like the MVC variant uses the servlet-context-path
.
I unfortunately will have to either revert back to MVC version and "deal" with the aforementioned bug, or consider an alternative api-gateway.
Describe the bug
When setting a
spring.webflux.base-path
property all routes usingPath
predicates return 404 resultsSample
Expected: Visit
localhost:8080/myapp/api
to see the google homepage (albeit stripped down). Actual: 404After digging a bit, I think this is because the
PathRoutePredicateFactory
uses the URIgetRawPath
.Instead it could use a
ServerHttpRequest
'sRequestPath
then callpathWithinApplication
, which is already set up correctly via theContextPathCompositeHandler
.It is this
ContextPathCompositeHandler
which rejects the request earlier if you visit justlocalhost:8080/api
, so thebase-path
property seems to be broken either way.Is this change sensible? Or is there a separate feature which allows this functionality?
Thanks for your time.