From 1cd06e657e62e1a84e679e46513ba7e5e331ab83 Mon Sep 17 00:00:00 2001 From: Spencer Gibb Date: Wed, 5 Nov 2014 18:08:23 -0700 Subject: [PATCH] Don't swallow execeptions with zuul by reusing the spring boot error facilities. fixes gh-40 --- .../main/asciidoc/spring-cloud-netflix.adoc | 11 ++ .../zuul/AbstractZuulConfiguration.java | 11 ++ .../zuul/filters/post/SendErrorFilter.java | 51 +++++++ .../zuul/filters/pre/PreDecorationFilter.java | 3 +- .../filters/pre/Servlet30WrapperFilter.java | 130 ++++++++++++++++++ .../filters/route/RibbonRoutingFilter.java | 5 +- .../zuul/sample/ZuulProxyApplication.java | 2 +- .../src/test/resources/application.yml | 3 +- .../src/test/resources/application.yml | 3 + 9 files changed, 215 insertions(+), 4 deletions(-) create mode 100644 spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/filters/post/SendErrorFilter.java create mode 100644 spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/filters/pre/Servlet30WrapperFilter.java diff --git a/docs/src/main/asciidoc/spring-cloud-netflix.adoc b/docs/src/main/asciidoc/spring-cloud-netflix.adoc index 577543db..66e61251 100644 --- a/docs/src/main/asciidoc/spring-cloud-netflix.adoc +++ b/docs/src/main/asciidoc/spring-cloud-netflix.adoc @@ -484,3 +484,14 @@ This means that http calls to /users get forwarded to the users service. Since zuul, by default, intercepts all requests (`/*`), to enable actuator, you should set the `management.port`. +To use the Spring Boot error facilities while using the standalone Zuul server, please set the following properties + +---- +# moves the Spring Dispatch Servlet to a path below `/` +server: + servletPath: /app +# sets the error path to use the Dispatch Servlet to resolve the error view +error: + path: ${server.servletPath}/error +---- + diff --git a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/AbstractZuulConfiguration.java b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/AbstractZuulConfiguration.java index 85acfbf2..edff3cf8 100644 --- a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/AbstractZuulConfiguration.java +++ b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/AbstractZuulConfiguration.java @@ -6,9 +6,11 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.actuate.trace.TraceRepository; import org.springframework.boot.context.embedded.FilterRegistrationBean; import org.springframework.boot.context.embedded.ServletRegistrationBean; +import org.springframework.cloud.netflix.zuul.filters.post.SendErrorFilter; 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.PreDecorationFilter; +import org.springframework.cloud.netflix.zuul.filters.pre.Servlet30WrapperFilter; import org.springframework.cloud.netflix.zuul.filters.route.RibbonRoutingFilter; import org.springframework.context.annotation.Bean; @@ -62,6 +64,11 @@ public abstract class AbstractZuulConfiguration { return new PreDecorationFilter(); } + @Bean + public Servlet30WrapperFilter servlet30WrapperFilter() { + return new Servlet30WrapperFilter(); + } + // route filters @Bean public RibbonRoutingFilter ribbonRoutingFilter() { @@ -78,4 +85,8 @@ public abstract class AbstractZuulConfiguration { return new SendResponseFilter(); } + @Bean + public SendErrorFilter sendErrorFilter() { + return new SendErrorFilter(); + } } diff --git a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/filters/post/SendErrorFilter.java b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/filters/post/SendErrorFilter.java new file mode 100644 index 00000000..736faa28 --- /dev/null +++ b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/filters/post/SendErrorFilter.java @@ -0,0 +1,51 @@ +package org.springframework.cloud.netflix.zuul.filters.post; + +import com.google.common.base.Throwables; +import com.netflix.zuul.ZuulFilter; +import com.netflix.zuul.context.RequestContext; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; + +/** + * @author Spencer Gibb + */ +@Slf4j +public class SendErrorFilter extends ZuulFilter { + + @Value("${error.path:/error}") + private String errorPath; + + @Override + public String filterType() { + return "post"; + } + + @Override + public int filterOrder() { + return 0; + } + + @Override + public boolean shouldFilter() { + RequestContext ctx = RequestContext.getCurrentContext(); + return ctx.containsKey("error.status_code"); + } + + @Override + public Object run() { + try { + RequestContext ctx = RequestContext.getCurrentContext(); + int statusCode = (Integer)ctx.get("error.status_code"); + if (ctx.containsKey("error.exception")) { + Object e = ctx.get("error.exception"); + log.warn("Error during filtering", Throwable.class.cast(e)); + ctx.getRequest().setAttribute("javax.servlet.error.exception", e); + } + ctx.getRequest().setAttribute("javax.servlet.error.status_code", statusCode); + ctx.getRequest().getRequestDispatcher(errorPath).forward(ctx.getRequest(), ctx.getResponse()); + } catch (Exception e) { + Throwables.propagate(e); + } + return null; + } +} diff --git a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/filters/pre/PreDecorationFilter.java b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/filters/pre/PreDecorationFilter.java index f342e6f3..5c449dc6 100644 --- a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/filters/pre/PreDecorationFilter.java +++ b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/filters/pre/PreDecorationFilter.java @@ -12,6 +12,7 @@ import org.springframework.cloud.netflix.zuul.Routes; import org.springframework.cloud.netflix.zuul.ZuulProperties; import javax.annotation.Nullable; +import javax.servlet.http.HttpServletResponse; import java.util.LinkedHashMap; public class PreDecorationFilter extends ZuulFilter { @@ -78,7 +79,7 @@ public class PreDecorationFilter extends ZuulFilter { } } else { LOG.warn("No route found for uri: "+requestURI); - //TODO: 404 + ctx.set("error.status_code", HttpServletResponse.SC_NOT_FOUND); } return null; } diff --git a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/filters/pre/Servlet30WrapperFilter.java b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/filters/pre/Servlet30WrapperFilter.java new file mode 100644 index 00000000..2ed2c199 --- /dev/null +++ b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/filters/pre/Servlet30WrapperFilter.java @@ -0,0 +1,130 @@ +package org.springframework.cloud.netflix.zuul.filters.pre; + +import com.google.common.base.Throwables; +import com.netflix.zuul.ZuulFilter; +import com.netflix.zuul.context.RequestContext; +import com.netflix.zuul.http.HttpServletRequestWrapper; + +import javax.servlet.*; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.Part; +import java.io.IOException; +import java.lang.reflect.Field; +import java.util.Collection; + +/** + * @author Spencer Gibb + */ +public class Servlet30WrapperFilter extends ZuulFilter { + protected Field requestField = null; + + public Servlet30WrapperFilter() { + try { + requestField = HttpServletRequestWrapper.class.getDeclaredField("req"); + requestField.setAccessible(true); + } catch (NoSuchFieldException e) { + Throwables.propagate(e); + } + } + + @Override + public String filterType() { + return "pre"; + } + + @Override + public int filterOrder() { + return 0; + } + + @Override + public boolean shouldFilter() { + return true; //TODO: only if in servlet 3.0 env + } + + @Override + public Object run() { + RequestContext ctx = RequestContext.getCurrentContext(); + HttpServletRequest request = ctx.getRequest(); + if (request instanceof HttpServletRequestWrapper) { + try { + request = (HttpServletRequest) requestField.get(request); + } catch (IllegalAccessException e) { + Throwables.propagate(e); + } + } + ctx.setRequest(new Servlet30RequestWrapper(request)); + //ctx.setResponse(new HttpServletResponseWrapper(ctx.getResponse())); + return null; + } + + class Servlet30RequestWrapper extends HttpServletRequestWrapper { + private HttpServletRequest request; + + Servlet30RequestWrapper(HttpServletRequest request) { + super(request); + this.request = request; + } + + @Override + public boolean authenticate(HttpServletResponse response) throws IOException, ServletException { + return request.authenticate(response); + } + + @Override + public void login(String username, String password) throws ServletException { + request.login(username, password); + } + + @Override + public void logout() throws ServletException { + request.logout(); + } + + @Override + public Collection getParts() throws IOException, IllegalStateException, ServletException { + return request.getParts(); + } + + @Override + public Part getPart(String name) throws IOException, IllegalStateException, ServletException { + return request.getPart(name); + } + + @Override + public ServletContext getServletContext() { + return request.getServletContext(); + } + + @Override + public AsyncContext startAsync() { + return request.startAsync(); + } + + @Override + public AsyncContext startAsync(ServletRequest servletRequest, ServletResponse servletResponse) { + return request.startAsync(servletRequest, servletResponse); + } + + @Override + public boolean isAsyncStarted() { + return request.isAsyncStarted(); + } + + @Override + public boolean isAsyncSupported() { + return request.isAsyncSupported(); + } + + @Override + public AsyncContext getAsyncContext() { + return request.getAsyncContext(); + } + + @Override + public DispatcherType getDispatcherType() { + return request.getDispatcherType(); + } + } +} diff --git a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/filters/route/RibbonRoutingFilter.java b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/filters/route/RibbonRoutingFilter.java index 27e98c00..bf9b796c 100644 --- a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/filters/route/RibbonRoutingFilter.java +++ b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/filters/route/RibbonRoutingFilter.java @@ -10,6 +10,7 @@ import java.util.Map; import java.util.Map.Entry; import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; import javax.ws.rs.core.MultivaluedMap; import org.apache.commons.io.IOUtils; @@ -89,8 +90,10 @@ public class RibbonRoutingFilter extends SpringFilter { return response; } catch (Exception e) { - throw new RuntimeException(e); + context.set("error.status_code", HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + context.set("error.exception", e); } + return null; } private Map debug(Verb verb, String uri, diff --git a/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/zuul/sample/ZuulProxyApplication.java b/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/zuul/sample/ZuulProxyApplication.java index 38128e6a..56814652 100644 --- a/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/zuul/sample/ZuulProxyApplication.java +++ b/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/zuul/sample/ZuulProxyApplication.java @@ -17,7 +17,7 @@ public class ZuulProxyApplication { @RequestMapping("/testing123") public String testing123() { - return "testing123"; + throw new RuntimeException("myerror"); } @RequestMapping("/") diff --git a/spring-cloud-netflix-core/src/test/resources/application.yml b/spring-cloud-netflix-core/src/test/resources/application.yml index 37b93257..d87e656d 100644 --- a/spring-cloud-netflix-core/src/test/resources/application.yml +++ b/spring-cloud-netflix-core/src/test/resources/application.yml @@ -6,7 +6,8 @@ spring: eureka: server: enabled: false - +error: + path: /myerror zuul: proxy: mapping: /api diff --git a/spring-cloud-netflix-zuul-server/src/test/resources/application.yml b/spring-cloud-netflix-zuul-server/src/test/resources/application.yml index 5d9b04a2..6c06fc0f 100644 --- a/spring-cloud-netflix-zuul-server/src/test/resources/application.yml +++ b/spring-cloud-netflix-zuul-server/src/test/resources/application.yml @@ -1,10 +1,13 @@ server: port: 9876 + servletPath: /app management: port: 9877 spring: application: name: testzuulserver +error: + path: ${server.servletPath}/error zuul: server: