From dfae445499571c79e71f5aa9fc7e2ea235584f17 Mon Sep 17 00:00:00 2001 From: Dave Syer Date: Thu, 4 Dec 2014 11:40:13 +0000 Subject: [PATCH] Strip prefixes from zuul routes by default The default behaviour is now to strip the prefixes (global and route-specific) by default. E.g. zuul: prefix: /api routes: customers: /customers/** Will forward /api/customers/101 to /101 on the customers service See gh-43 --- .../main/asciidoc/spring-cloud-netflix.adoc | 50 +++++++++++++------ .../cloud/netflix/zuul/ProxyRouteLocator.java | 6 +-- .../cloud/netflix/zuul/ZuulProperties.java | 4 +- .../netflix/zuul/ProxyRouteLocatorTests.java | 29 +++++++++-- .../zuul/SampleZuulProxyApplication.java | 6 +-- .../filters/pre/PreDecorationFilterTests.java | 4 +- 6 files changed, 72 insertions(+), 27 deletions(-) diff --git a/docs/src/main/asciidoc/spring-cloud-netflix.adoc b/docs/src/main/asciidoc/spring-cloud-netflix.adoc index c7cd020e..8f54c077 100644 --- a/docs/src/main/asciidoc/spring-cloud-netflix.adoc +++ b/docs/src/main/asciidoc/spring-cloud-netflix.adoc @@ -500,10 +500,26 @@ Zuul's rule engine allows rules and filters to be written in essentially any JVM [[netflix-zuul-reverse-proxy]] === Embedded Zuul Reverse Proxy -Spring Cloud has created an embedded Zuul proxy to ease the development of a very common use case where a UI application wants to proxy calls to one or more back end services. To enable it, annotate a Spring Boot main class with `@EnableZuulProxy`, and this forwards local calls to the appropriate service. By convention, a service with the Eureka ID "users", will receive requests from the proxy located at `/users`. The proxy uses Ribbon to locate an instance to forward to via Eureka. To skip having a service automatically added from eureka, set `zuul.ignored-services = service1`. - -To augment or change the proxy routes, you can add external -configuration like the following: +Spring Cloud has created an embedded Zuul proxy to ease the +development of a very common use case where a UI application wants to +proxy calls to one or more back end services. This feature is useful +for a user interface to proxy to the backend services it requires, +avoiding the need to manage CORS and authentication concerns +independently for all the backends. + +To enable it, annotate a Spring Boot main class with +`@EnableZuulProxy`, and this forwards local calls to the appropriate +service. By convention, a service with the Eureka ID "users", will +receive requests from the proxy located at `/users` (with the prefix +stripped). The proxy uses Ribbon to locate an instance to forward to +via Eureka, and all requests are executed in a hystrix command, so +failures will show up in Hystrix metrics, and once the circuit is open +the proxy will not try to contact the service. + +To skip having a service automatically added, set +`zuul.ignored-services` to a list of service ids. To augment or change +the proxy routes, you can add external configuration like the +following: .application.yml [source,yaml] @@ -514,9 +530,7 @@ configuration like the following: ---- This means that http calls to "/myusers" get forwarded to the "users" -service. This configuration is useful for a user interface to proxy to -the backend services it requires (avoiding the need to manage CORS and -authentication concerns independently for all the backends). +service (for example "/myusers/101" is forwarded to "/101"). To get more fine-grained control over a route you can specify the path and the serviceId independently: @@ -549,9 +563,11 @@ The location of the backend can be specified as either a "serviceId" url: http://example.com/users_service ---- -Forwarding to the service is protected by a Hystrix circuit breaker so if a service is down the client will see an error, but once the circuit is open the proxy will not try to contact the service. - -To add a prefix to all mappings, set `zuul.prefix` to a value, such as `/api`. To strip the proxy prefix from the request before the request is forwarded set `zuul.stripPrefix = true`. You can also strip the non-wildcard prefix from individual routes, e.g. +To add a prefix to all mappings, set `zuul.prefix` to a value, such as +`/api`. The proxy prefix is stripped from the request before the +request is forwarded by default (switch this behaviour off with +`zuul.stripPrefix=false`). You can also switch off the stripping of +the service-specific prefix from individual routes, e.g. .application.yml [source,yaml] @@ -560,12 +576,18 @@ To add a prefix to all mappings, set `zuul.prefix` to a value, such as `/api`. routes: users: path: /myusers/** - stripPrefix: true + stripPrefix: false ---- -In this example requests to "/myusers/101" will be forwarded to "/101" on the "users" service (the path is stripped up to the first wildcard character). +In this example requests to "/myusers/101" will be forwarded to "/myusers/101" on the "users" service. -The `X-Forwarded-Host` header added to the forwarded requests by default. To turn it off set `zuul.addProxyHeaders = false`. +The `X-Forwarded-Host` header added to the forwarded requests by +default. To turn it off set `zuul.addProxyHeaders = false`. The +prefix path is stripped by default, and the request to the backend +picks up a header "X-Forwarded-Prefix" ("/myusers" in the examples +above). -An application with the `@EnableZuulProxy` could act as a standalone server if you set a default route ("/"), for example `zuul.route.home: /` would route all traffic (i.e. "/**") to the "home" service. +An application with the `@EnableZuulProxy` could act as a standalone +server if you set a default route ("/"), for example `zuul.route.home: +/` would route all traffic (i.e. "/**") to the "home" service. diff --git a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/ProxyRouteLocator.java b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/ProxyRouteLocator.java index 7c84241a..e082acef 100644 --- a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/ProxyRouteLocator.java +++ b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/ProxyRouteLocator.java @@ -85,9 +85,9 @@ public class ProxyRouteLocator { if (route.isStripPrefix()) { int index = route.getPath().indexOf("*") - 1; if (index > 0) { - targetPath = path.substring(prefix.length() + index); - prefix = prefix - + path.substring(prefix.length(), prefix.length() + index); + String routePrefix = route.getPath().substring(0, index); + targetPath = targetPath.replaceFirst(routePrefix, ""); + prefix = prefix + routePrefix; } } break; diff --git a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/ZuulProperties.java b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/ZuulProperties.java index 2f7d6de3..f7241bf9 100644 --- a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/ZuulProperties.java +++ b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/ZuulProperties.java @@ -21,7 +21,7 @@ import org.springframework.util.StringUtils; @ConfigurationProperties("zuul") public class ZuulProperties { private String prefix = ""; - private boolean stripPrefix = false; + private boolean stripPrefix = true; private Map routes = new LinkedHashMap(); private boolean addProxyHeaders = true; private List ignoredServices = new ArrayList(); @@ -43,7 +43,7 @@ public class ZuulProperties { private String path; private String serviceId; private String url; - private boolean stripPrefix = false; + private boolean stripPrefix = true; public ZuulRoute(String text) { String location = null; diff --git a/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/zuul/ProxyRouteLocatorTests.java b/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/zuul/ProxyRouteLocatorTests.java index 7946ed20..2c8e2532 100644 --- a/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/zuul/ProxyRouteLocatorTests.java +++ b/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/zuul/ProxyRouteLocatorTests.java @@ -59,14 +59,37 @@ public class ProxyRouteLocatorTests { routeLocator.getRoutes(); // force refresh ProxyRouteSpec route = routeLocator.getMatchingRoute("/proxy/foo/1"); assertEquals("foo", route.getLocation()); + assertEquals("/1", route.getPath()); + } + + @Test + public void testGetMatchingPathWithNoPrefixStripping() throws Exception { + ProxyRouteLocator routeLocator = new ProxyRouteLocator(this.discovery, this.properties); + this.properties.getRoutes().put("foo", new ZuulRoute("/foo/**", "foo", null, false)); + this.properties.setStripPrefix(false); + this.properties.setPrefix("/proxy"); + routeLocator.getRoutes(); // force refresh + ProxyRouteSpec route = routeLocator.getMatchingRoute("/proxy/foo/1"); + assertEquals("foo", route.getLocation()); assertEquals("/proxy/foo/1", route.getPath()); } @Test - public void testGetMatchingPathWithPrefixStripping() throws Exception { + public void testGetMatchingPathWithLocalPrefixStripping() throws Exception { ProxyRouteLocator routeLocator = new ProxyRouteLocator(this.discovery, this.properties); - this.properties.getRoutes().put("foo", new ZuulRoute("/foo/**")); - this.properties.setStripPrefix(true); + this.properties.getRoutes().put("foo", new ZuulRoute("/foo/**", "foo")); + this.properties.setStripPrefix(false); + this.properties.setPrefix("/proxy"); + routeLocator.getRoutes(); // force refresh + ProxyRouteSpec route = routeLocator.getMatchingRoute("/proxy/foo/1"); + assertEquals("foo", route.getLocation()); + assertEquals("/proxy/1", route.getPath()); + } + + @Test + public void testGetMatchingPathWithGlobalPrefixStripping() throws Exception { + ProxyRouteLocator routeLocator = new ProxyRouteLocator(this.discovery, this.properties); + this.properties.getRoutes().put("foo", new ZuulRoute("/foo/**", "foo", null, false)); this.properties.setPrefix("/proxy"); routeLocator.getRoutes(); // force refresh ProxyRouteSpec route = routeLocator.getMatchingRoute("/proxy/foo/1"); diff --git a/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/zuul/SampleZuulProxyApplication.java b/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/zuul/SampleZuulProxyApplication.java index 82d8a94a..a27300d4 100644 --- a/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/zuul/SampleZuulProxyApplication.java +++ b/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/zuul/SampleZuulProxyApplication.java @@ -21,17 +21,17 @@ public class SampleZuulProxyApplication { throw new RuntimeException("myerror"); } - @RequestMapping("/local/self") + @RequestMapping("/local") public String local() { return "Hello local"; } - @RequestMapping(value="/local/self/{id}", method=RequestMethod.DELETE) + @RequestMapping(value="/local/{id}", method=RequestMethod.DELETE) public String delete() { return "Deleted!"; } - @RequestMapping(value="/local/self/{id}", method=RequestMethod.GET) + @RequestMapping(value="/local/{id}", method=RequestMethod.GET) public String get() { return "Gotten!"; } diff --git a/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/zuul/filters/pre/PreDecorationFilterTests.java b/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/zuul/filters/pre/PreDecorationFilterTests.java index e1c26b3e..5432d5e1 100644 --- a/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/zuul/filters/pre/PreDecorationFilterTests.java +++ b/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/zuul/filters/pre/PreDecorationFilterTests.java @@ -55,7 +55,7 @@ public class PreDecorationFilterTests { properties.setPrefix("/api"); properties.setStripPrefix(true); request.setRequestURI("/api/foo/1"); - routeLocator.addRoute("/foo/**", "foo"); + routeLocator.addRoute(new ZuulRoute("/foo/**", "foo", null, false)); filter.run(); RequestContext ctx = RequestContext.getCurrentContext(); assertEquals("/foo/1", ctx.get("requestURI")); @@ -69,7 +69,7 @@ public class PreDecorationFilterTests { properties.setPrefix("/api"); properties.setStripPrefix(true); request.setRequestURI("/api/foo/1"); - routeLocator.addRoute(new ZuulRoute("/foo/**", "foo", null, true)); + routeLocator.addRoute("/foo/**", "foo"); filter.run(); RequestContext ctx = RequestContext.getCurrentContext(); assertEquals("/1", ctx.get("requestURI"));