Add smarts for local route handling in Zuul proxy
User can provide a url in the route that starts with "forward:" (instead of "http:" etc.) to handle the request locally. Also properly ignores ignored routes, so they can be handled locally (rather than giving up and sending 404). Fixes gh-536
This commit is contained in:
@@ -1096,7 +1096,7 @@ server if you set a default route ("/"), for example `zuul.route.home:
|
||||
/` would route all traffic (i.e. "/**") to the "home" service.
|
||||
|
||||
If more fine-grained ignoring is needed, you can specify specific patterns to ignore.
|
||||
These patterns are being evaluated at the end of the route location process, which
|
||||
These patterns are being evaluated at the start of the route location process, which
|
||||
means prefixes should be included in the pattern to warrant a match. Ignored patterns
|
||||
span all services and supersede any other route specification.
|
||||
|
||||
@@ -1104,7 +1104,7 @@ span all services and supersede any other route specification.
|
||||
[source,yaml]
|
||||
----
|
||||
zuul:
|
||||
ignoredPatterns: */admin/**
|
||||
ignoredPatterns: /**/admin/**
|
||||
routes:
|
||||
users: /myusers/**
|
||||
----
|
||||
@@ -1112,6 +1112,47 @@ span all services and supersede any other route specification.
|
||||
This means that all calls such as "/myusers/101" will be forwarded to "/101" on the "users" service.
|
||||
But calls including "/admin/" will not resolve.
|
||||
|
||||
=== Strangulation Patterns and Local Forwards
|
||||
|
||||
A common pattern when migrating an existing application or API is to
|
||||
"strangle" old endpoints, slowly replacing them with different
|
||||
implementations. The Zuul proxy is a useful tool for this because you
|
||||
can use it to handle all traffic from clients of the old endpoints,
|
||||
but redirect some of the requests to new ones.
|
||||
|
||||
Example configuration:
|
||||
|
||||
.application.yml
|
||||
[source,yaml]
|
||||
----
|
||||
zuul:
|
||||
routes:
|
||||
first:
|
||||
path: /first/**
|
||||
url: http://first.example.com
|
||||
second:
|
||||
path: /second/**
|
||||
url: forward:/second
|
||||
third:
|
||||
path: /third/**
|
||||
url: forward:/3rd
|
||||
legacy:
|
||||
path: /**
|
||||
url: http://legacy.example.com
|
||||
----
|
||||
|
||||
In this example we are strangling the "legacy" app which is mapped to
|
||||
all requests that do not match one of the other patterns. Paths in
|
||||
`/first/**` have been extracted into a new service with an external
|
||||
URL. And paths in `/second/**` are forwared so they can be handled
|
||||
locally, e.g. with a normal Spring `@RequestMapping`. Paths in
|
||||
`/third/**` are also forwarded, but with a different prefix
|
||||
(i.e. `/third/foo` is forwarded to `/3rd/foo`).
|
||||
|
||||
NOTE: The ignored pattterns aren't completely ignored, they just
|
||||
aren't handled by the proxy (so they are also effectively forwarded
|
||||
locally).
|
||||
|
||||
=== Uploading Files through Zuul
|
||||
|
||||
If you `@EnableZuulProxy` you can use the proxy paths to
|
||||
|
||||
@@ -29,6 +29,7 @@ import org.springframework.cloud.netflix.zuul.filters.RouteLocator;
|
||||
import org.springframework.cloud.netflix.zuul.filters.SimpleRouteLocator;
|
||||
import org.springframework.cloud.netflix.zuul.filters.ZuulProperties;
|
||||
import org.springframework.cloud.netflix.zuul.filters.post.SendErrorFilter;
|
||||
import org.springframework.cloud.netflix.zuul.filters.post.SendForwardFilter;
|
||||
import org.springframework.cloud.netflix.zuul.filters.post.SendResponseFilter;
|
||||
import org.springframework.cloud.netflix.zuul.filters.pre.DebugFilter;
|
||||
import org.springframework.cloud.netflix.zuul.filters.pre.FormBodyWrapperFilter;
|
||||
@@ -121,6 +122,11 @@ public class ZuulConfiguration {
|
||||
return new SendErrorFilter();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public SendForwardFilter sendForwardFilter() {
|
||||
return new SendForwardFilter();
|
||||
}
|
||||
|
||||
@Configuration
|
||||
protected static class ZuulFilterConfiguration {
|
||||
|
||||
|
||||
@@ -23,10 +23,6 @@ import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.extern.apachecommons.CommonsLog;
|
||||
|
||||
import org.springframework.cloud.client.ServiceInstance;
|
||||
import org.springframework.cloud.client.discovery.DiscoveryClient;
|
||||
import org.springframework.cloud.netflix.zuul.filters.ZuulProperties.ZuulRoute;
|
||||
@@ -35,6 +31,10 @@ import org.springframework.util.PathMatcher;
|
||||
import org.springframework.util.PatternMatchUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.extern.apachecommons.CommonsLog;
|
||||
|
||||
/**
|
||||
* @author Spencer Gibb
|
||||
*/
|
||||
@@ -93,6 +93,11 @@ public class ProxyRouteLocator implements RouteLocator {
|
||||
return getRoutes().keySet();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<String> getIgnoredPaths() {
|
||||
return this.properties.getIgnoredPatterns();
|
||||
}
|
||||
|
||||
public Map<String, String> getRoutes() {
|
||||
if (this.routes.get() == null) {
|
||||
this.routes.set(locateRoutes());
|
||||
@@ -106,8 +111,8 @@ public class ProxyRouteLocator implements RouteLocator {
|
||||
}
|
||||
|
||||
public ProxyRouteSpec getMatchingRoute(String path) {
|
||||
log.info("Finding route for path: " + path);
|
||||
|
||||
log.info("Finding route for path: " + path);
|
||||
|
||||
String location = null;
|
||||
String targetPath = null;
|
||||
String id = null;
|
||||
|
||||
@@ -25,4 +25,6 @@ public interface RouteLocator {
|
||||
|
||||
Collection<String> getRoutePaths();
|
||||
|
||||
Collection<String> getIgnoredPaths();
|
||||
|
||||
}
|
||||
|
||||
@@ -41,4 +41,9 @@ public class SimpleRouteLocator implements RouteLocator {
|
||||
return paths;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<String> getIgnoredPaths() {
|
||||
return this.properties.getIgnoredPatterns();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
/*
|
||||
* Copyright 2013-2015 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.netflix.zuul.filters.post;
|
||||
|
||||
import javax.servlet.RequestDispatcher;
|
||||
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
|
||||
import com.netflix.zuul.ZuulFilter;
|
||||
import com.netflix.zuul.context.RequestContext;
|
||||
|
||||
/**
|
||||
* @author Dave Syer
|
||||
*/
|
||||
public class SendForwardFilter extends ZuulFilter {
|
||||
|
||||
protected static final String SEND_FORWARD_FILTER_RAN = "sendForwardFilter.ran";
|
||||
|
||||
@Override
|
||||
public String filterType() {
|
||||
return "post";
|
||||
}
|
||||
|
||||
@Override
|
||||
public int filterOrder() {
|
||||
return 2000;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldFilter() {
|
||||
RequestContext ctx = RequestContext.getCurrentContext();
|
||||
return ctx.containsKey("forward.to")
|
||||
&& !ctx.getBoolean(SEND_FORWARD_FILTER_RAN, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object run() {
|
||||
try {
|
||||
RequestContext ctx = RequestContext.getCurrentContext();
|
||||
String path = (String) ctx.get("forward.to");
|
||||
RequestDispatcher dispatcher = ctx.getRequest().getRequestDispatcher(path);
|
||||
if (dispatcher != null) {
|
||||
ctx.set(SEND_FORWARD_FILTER_RAN, true);
|
||||
if (!ctx.getResponse().isCommitted()) {
|
||||
dispatcher.forward(ctx.getRequest(), ctx.getResponse());
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex) {
|
||||
ReflectionUtils.rethrowRuntimeException(ex);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -19,19 +19,17 @@ package org.springframework.cloud.netflix.zuul.filters.pre;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URL;
|
||||
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import com.netflix.zuul.constants.ZuulHeaders;
|
||||
import lombok.extern.apachecommons.CommonsLog;
|
||||
|
||||
import org.springframework.cloud.netflix.zuul.filters.ProxyRouteLocator;
|
||||
import org.springframework.cloud.netflix.zuul.filters.ProxyRouteLocator.ProxyRouteSpec;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.util.UrlPathHelper;
|
||||
|
||||
import com.netflix.zuul.ZuulFilter;
|
||||
import com.netflix.zuul.constants.ZuulHeaders;
|
||||
import com.netflix.zuul.context.RequestContext;
|
||||
|
||||
import lombok.extern.apachecommons.CommonsLog;
|
||||
|
||||
@CommonsLog
|
||||
public class PreDecorationFilter extends ZuulFilter {
|
||||
|
||||
@@ -58,14 +56,15 @@ public class PreDecorationFilter extends ZuulFilter {
|
||||
|
||||
@Override
|
||||
public boolean shouldFilter() {
|
||||
return true;
|
||||
RequestContext ctx = RequestContext.getCurrentContext();
|
||||
return !ctx.containsKey("forward.to");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object run() {
|
||||
RequestContext ctx = RequestContext.getCurrentContext();
|
||||
final String requestURI = this.urlPathHelper.getPathWithinApplication(ctx
|
||||
.getRequest());
|
||||
final String requestURI = this.urlPathHelper
|
||||
.getPathWithinApplication(ctx.getRequest());
|
||||
ProxyRouteSpec route = this.routeLocator.getMatchingRoute(requestURI);
|
||||
if (route != null) {
|
||||
String location = route.getLocation();
|
||||
@@ -81,6 +80,12 @@ public class PreDecorationFilter extends ZuulFilter {
|
||||
ctx.setRouteHost(getUrl(location));
|
||||
ctx.addOriginResponseHeader("X-Zuul-Service", location);
|
||||
}
|
||||
else if (location.startsWith("forward:")) {
|
||||
ctx.set("forward.to", StringUtils.cleanPath(
|
||||
location.substring("forward:".length()) + route.getPath()));
|
||||
ctx.setRouteHost(null);
|
||||
return null;
|
||||
}
|
||||
else {
|
||||
// set serviceId for use in filters.route.RibbonRequest
|
||||
ctx.set("serviceId", location);
|
||||
@@ -88,12 +93,10 @@ public class PreDecorationFilter extends ZuulFilter {
|
||||
ctx.addOriginResponseHeader("X-Zuul-ServiceId", location);
|
||||
}
|
||||
if (this.addProxyHeaders) {
|
||||
ctx.addZuulRequestHeader(
|
||||
"X-Forwarded-Host",
|
||||
ctx.addZuulRequestHeader("X-Forwarded-Host",
|
||||
ctx.getRequest().getServerName() + ":"
|
||||
+ String.valueOf(ctx.getRequest().getServerPort()));
|
||||
ctx.addZuulRequestHeader(
|
||||
ZuulHeaders.X_FORWARDED_PROTO,
|
||||
ctx.addZuulRequestHeader(ZuulHeaders.X_FORWARDED_PROTO,
|
||||
ctx.getRequest().getScheme());
|
||||
if (StringUtils.hasText(route.getPrefix())) {
|
||||
ctx.addZuulRequestHeader("X-Forwarded-Prefix", route.getPrefix());
|
||||
@@ -103,7 +106,7 @@ public class PreDecorationFilter extends ZuulFilter {
|
||||
}
|
||||
else {
|
||||
log.warn("No route found for uri: " + requestURI);
|
||||
ctx.set("error.status_code", HttpServletResponse.SC_NOT_FOUND);
|
||||
ctx.set("forward.to", requestURI);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
import org.springframework.boot.autoconfigure.web.ErrorController;
|
||||
import org.springframework.cloud.netflix.zuul.filters.RouteLocator;
|
||||
import org.springframework.util.PatternMatchUtils;
|
||||
import org.springframework.web.servlet.handler.AbstractUrlHandlerMapping;
|
||||
|
||||
/**
|
||||
@@ -55,6 +56,10 @@ public class ZuulHandlerMapping extends AbstractUrlHandlerMapping {
|
||||
&& urlPath.equals(this.errorController.getErrorPath())) {
|
||||
return null;
|
||||
}
|
||||
String[] ignored = this.routeLocator.getIgnoredPaths().toArray(new String[0]);
|
||||
if (PatternMatchUtils.simpleMatch(ignored, urlPath)) {
|
||||
return null;
|
||||
}
|
||||
return super.lookupHandler(urlPath, request);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
* Copyright 2013-2015 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.netflix.zuul.filters.post;
|
||||
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
import org.junit.After;
|
||||
import org.junit.Test;
|
||||
import org.springframework.mock.web.MockHttpServletRequest;
|
||||
import org.springframework.mock.web.MockHttpServletResponse;
|
||||
|
||||
import com.netflix.zuul.context.RequestContext;
|
||||
|
||||
/**
|
||||
* @author Dave Syer
|
||||
*/
|
||||
public class SendForwardFilterTests {
|
||||
|
||||
@After
|
||||
public void reset() {
|
||||
RequestContext.testSetCurrentContext(null);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void runsNormally() {
|
||||
SendForwardFilter filter = createSendForwardFilter(new MockHttpServletRequest());
|
||||
assertTrue("shouldFilter returned false", filter.shouldFilter());
|
||||
filter.run();
|
||||
}
|
||||
|
||||
private SendForwardFilter createSendForwardFilter(HttpServletRequest request) {
|
||||
RequestContext context = new RequestContext();
|
||||
context.setRequest(request);
|
||||
context.setResponse(new MockHttpServletResponse());
|
||||
context.set("forward.to", "/foo");
|
||||
RequestContext.testSetCurrentContext(context);
|
||||
SendForwardFilter filter = new SendForwardFilter();
|
||||
return filter;
|
||||
}
|
||||
|
||||
@Test
|
||||
public void doesNotRunTwice() {
|
||||
SendForwardFilter filter = createSendForwardFilter(new MockHttpServletRequest());
|
||||
assertTrue("shouldFilter returned false", filter.shouldFilter());
|
||||
filter.run();
|
||||
assertFalse("shouldFilter returned true", filter.shouldFilter());
|
||||
}
|
||||
}
|
||||
@@ -16,12 +16,14 @@
|
||||
|
||||
package org.springframework.cloud.netflix.zuul.filters.pre;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.mockito.MockitoAnnotations.initMocks;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.mockito.Mock;
|
||||
import org.springframework.boot.autoconfigure.web.ServerProperties;
|
||||
import org.springframework.cloud.client.discovery.DiscoveryClient;
|
||||
import org.springframework.cloud.netflix.zuul.filters.ProxyRouteLocator;
|
||||
import org.springframework.cloud.netflix.zuul.filters.ZuulProperties;
|
||||
@@ -31,9 +33,6 @@ import org.springframework.mock.web.MockHttpServletRequest;
|
||||
import com.netflix.util.Pair;
|
||||
import com.netflix.zuul.context.RequestContext;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.mockito.MockitoAnnotations.initMocks;
|
||||
|
||||
/**
|
||||
* @author Dave Syer
|
||||
*/
|
||||
@@ -50,8 +49,6 @@ public class PreDecorationFilterTests {
|
||||
|
||||
private MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
|
||||
private ServerProperties server = new ServerProperties();
|
||||
|
||||
@Before
|
||||
public void init() {
|
||||
initMocks(this);
|
||||
@@ -59,6 +56,7 @@ public class PreDecorationFilterTests {
|
||||
this.properties);
|
||||
this.filter = new PreDecorationFilter(this.routeLocator, true);
|
||||
RequestContext ctx = RequestContext.getCurrentContext();
|
||||
ctx.clear();
|
||||
ctx.setRequest(this.request);
|
||||
}
|
||||
|
||||
@@ -85,6 +83,28 @@ public class PreDecorationFilterTests {
|
||||
assertEquals("foo", getHeader(ctx.getOriginResponseHeaders(), "x-zuul-serviceid"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void forwardRouteAddsLocation() throws Exception {
|
||||
this.properties.setPrefix("/api");
|
||||
this.properties.setStripPrefix(true);
|
||||
this.request.setRequestURI("/api/foo/1");
|
||||
this.routeLocator.addRoute(new ZuulRoute("foo", "/foo/**", null, "forward:/foo", true,
|
||||
null));
|
||||
this.filter.run();
|
||||
RequestContext ctx = RequestContext.getCurrentContext();
|
||||
assertEquals("/foo/1", ctx.get("forward.to"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void forwardWithoutStripPrefixAppendsPath() throws Exception {
|
||||
this.request.setRequestURI("/foo/1");
|
||||
this.routeLocator.addRoute(new ZuulRoute("foo", "/foo/**", null, "forward:/bar", false,
|
||||
null));
|
||||
this.filter.run();
|
||||
RequestContext ctx = RequestContext.getCurrentContext();
|
||||
assertEquals("/bar/foo/1", ctx.get("forward.to"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void prefixRouteWithRouteStrippingAddsHeader() throws Exception {
|
||||
this.properties.setPrefix("/api");
|
||||
|
||||
Reference in New Issue
Block a user