diff --git a/spring-cloud/spring-cloud-zuul/spring-zuul-ui/src/main/java/com/baeldung/spring/cloud/zuul/filter/CustomZuulErrorApplication.java b/spring-cloud/spring-cloud-zuul/spring-zuul-ui/src/main/java/com/baeldung/spring/cloud/zuul/filter/CustomZuulErrorApplication.java new file mode 100644 index 0000000000..ae8c3040ef --- /dev/null +++ b/spring-cloud/spring-cloud-zuul/spring-zuul-ui/src/main/java/com/baeldung/spring/cloud/zuul/filter/CustomZuulErrorApplication.java @@ -0,0 +1,15 @@ +package com.baeldung.spring.cloud.zuul.filter; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.web.servlet.support.SpringBootServletInitializer; +import org.springframework.cloud.netflix.zuul.EnableZuulProxy; + +@EnableZuulProxy +@SpringBootApplication +public class CustomZuulErrorApplication extends SpringBootServletInitializer { + + public static void main(String[] args) { + SpringApplication.run(CustomZuulErrorApplication.class, args); + } +} \ No newline at end of file diff --git a/spring-cloud/spring-cloud-zuul/spring-zuul-ui/src/main/java/com/baeldung/spring/cloud/zuul/filter/CustomZuulErrorFilter.java b/spring-cloud/spring-cloud-zuul/spring-zuul-ui/src/main/java/com/baeldung/spring/cloud/zuul/filter/CustomZuulErrorFilter.java new file mode 100644 index 0000000000..d3e52843eb --- /dev/null +++ b/spring-cloud/spring-cloud-zuul/spring-zuul-ui/src/main/java/com/baeldung/spring/cloud/zuul/filter/CustomZuulErrorFilter.java @@ -0,0 +1,62 @@ +package com.baeldung.spring.cloud.zuul.filter; + +import java.net.ConnectException; +import java.time.Instant; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; + +import com.netflix.zuul.ZuulFilter; +import com.netflix.zuul.context.RequestContext; +import com.netflix.zuul.exception.ZuulException; + +@Component +public class CustomZuulErrorFilter extends ZuulFilter { + + private static final Logger log = LoggerFactory.getLogger(CustomZuulErrorFilter.class); + + private static final String RESPONSE_BODY = "{\n" + " \"timestamp\": " + "\"" + Instant.now() + .toString() + "\"" + ",\n" + " \"status\": 503,\n" + " \"error\": \"Service Unavailable\"\n" + "}"; + + @Override + public Object run() { + RequestContext context = RequestContext.getCurrentContext(); + Throwable throwable = context.getThrowable(); + + if (throwable instanceof ZuulException) { + final ZuulException zuulException = (ZuulException) throwable; + log.error("Zuul exception: " + zuulException.getMessage()); + + if (throwable.getCause().getCause().getCause() instanceof ConnectException) { + + // reset throwable to prevent further error handling in follow up filters + context.remove("throwable"); + + // set custom response attributes + context.setResponseBody(RESPONSE_BODY); + context.getResponse() + .setContentType("application/json"); + context.setResponseStatusCode(503); + } + + } + return null; + } + + @Override + public boolean shouldFilter() { + return true; + } + + @Override + public int filterOrder() { + return -1; + } + + @Override + public String filterType() { + return "error"; + } + +} diff --git a/spring-cloud/spring-cloud-zuul/spring-zuul-ui/src/main/java/com/baeldung/spring/cloud/zuul/filter/ZuulConfiguration.java b/spring-cloud/spring-cloud-zuul/spring-zuul-ui/src/main/java/com/baeldung/spring/cloud/zuul/filter/ZuulConfiguration.java new file mode 100644 index 0000000000..3122862050 --- /dev/null +++ b/spring-cloud/spring-cloud-zuul/spring-zuul-ui/src/main/java/com/baeldung/spring/cloud/zuul/filter/ZuulConfiguration.java @@ -0,0 +1,98 @@ +package com.baeldung.spring.cloud.zuul.filter; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.config.BeanPostProcessor; +import org.springframework.boot.web.servlet.error.ErrorController; +import org.springframework.cglib.proxy.Callback; +import org.springframework.cglib.proxy.CallbackFilter; +import org.springframework.cglib.proxy.Enhancer; +import org.springframework.cglib.proxy.MethodInterceptor; +import org.springframework.cglib.proxy.MethodProxy; +import org.springframework.cglib.proxy.NoOp; +import org.springframework.cloud.netflix.zuul.filters.RouteLocator; +import org.springframework.cloud.netflix.zuul.web.ZuulController; +import org.springframework.cloud.netflix.zuul.web.ZuulHandlerMapping; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * Fix for Zuul configuration with Spring Boot 2.5.x + Zuul - "NoSuchMethodError: ErrorController.getErrorPath()": + */ +@Configuration +public class ZuulConfiguration { + /** + * The path returned by ErrorController.getErrorPath() with Spring Boot < 2.5 + * (and no longer available on Spring Boot >= 2.5). + */ + private static final String ERROR_PATH = "/error"; + private static final String METHOD = "lookupHandler"; + + /** + * Constructs a new bean post-processor for Zuul. + * + * @param routeLocator the route locator. + * @param zuulController the Zuul controller. + * @param errorController the error controller. + * @return the new bean post-processor. + */ + @Bean + public ZuulPostProcessor zuulPostProcessor(@Autowired RouteLocator routeLocator, @Autowired ZuulController zuulController, @Autowired(required = false) ErrorController errorController) { + return new ZuulPostProcessor(routeLocator, zuulController, errorController); + } + + private enum LookupHandlerCallbackFilter implements CallbackFilter { + INSTANCE; + + @Override + public int accept(Method method) { + if (METHOD.equals(method.getName())) { + return 0; + } + return 1; + } + } + + private enum LookupHandlerMethodInterceptor implements MethodInterceptor { + INSTANCE; + + @Override + public Object intercept(Object target, Method method, Object[] args, MethodProxy methodProxy) throws Throwable { + if (ERROR_PATH.equals(args[0])) { + // by entering this branch we avoid the ZuulHandlerMapping.lookupHandler method to trigger the + // NoSuchMethodError + return null; + } + return methodProxy.invokeSuper(target, args); + } + } + + private static final class ZuulPostProcessor implements BeanPostProcessor { + + private final RouteLocator routeLocator; + private final ZuulController zuulController; + private final boolean hasErrorController; + + ZuulPostProcessor(RouteLocator routeLocator, ZuulController zuulController, ErrorController errorController) { + this.routeLocator = routeLocator; + this.zuulController = zuulController; + this.hasErrorController = (errorController != null); + } + + @Override + public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { + if (hasErrorController && (bean instanceof ZuulHandlerMapping)) { + Enhancer enhancer = new Enhancer(); + enhancer.setSuperclass(ZuulHandlerMapping.class); + enhancer.setCallbackFilter(LookupHandlerCallbackFilter.INSTANCE); // only for lookupHandler + enhancer.setCallbacks(new Callback[] { LookupHandlerMethodInterceptor.INSTANCE, NoOp.INSTANCE }); + Constructor ctor = ZuulHandlerMapping.class.getConstructors()[0]; + return enhancer.create(ctor.getParameterTypes(), new Object[] { routeLocator, zuulController }); + } + return bean; + } + } +} \ No newline at end of file diff --git a/spring-cloud/spring-cloud-zuul/spring-zuul-ui/src/main/resources/application.yml b/spring-cloud/spring-cloud-zuul/spring-zuul-ui/src/main/resources/application.yml index 855020a40e..bd517b8a6b 100644 --- a/spring-cloud/spring-cloud-zuul/spring-zuul-ui/src/main/resources/application.yml +++ b/spring-cloud/spring-cloud-zuul/spring-zuul-ui/src/main/resources/application.yml @@ -1,5 +1,8 @@ zuul: + SendErrorFilter: + post: + disable: true routes: foos: path: /foos/** - url: http://localhost:8081/spring-zuul-foos-resource/foos + url: http://localhost:8081/spring-zuul-foos-resource/foos \ No newline at end of file diff --git a/spring-cloud/spring-cloud-zuul/spring-zuul-ui/src/test/java/com/baeldung/spring/cloud/zuul/filter/CustomZuulErrorFilterLiveTest.java b/spring-cloud/spring-cloud-zuul/spring-zuul-ui/src/test/java/com/baeldung/spring/cloud/zuul/filter/CustomZuulErrorFilterLiveTest.java new file mode 100644 index 0000000000..da2588966d --- /dev/null +++ b/spring-cloud/spring-cloud-zuul/spring-zuul-ui/src/test/java/com/baeldung/spring/cloud/zuul/filter/CustomZuulErrorFilterLiveTest.java @@ -0,0 +1,24 @@ +package com.baeldung.spring.cloud.zuul.filter; + +import static org.junit.Assert.assertEquals; + +import org.junit.Test; + +import io.restassured.RestAssured; +import io.restassured.response.Response; + +public class CustomZuulErrorFilterLiveTest { + + @Test + public void whenSendRequestWithoutService_thenCustomError() { + final Response response = RestAssured.get("http://localhost:8080/foos/1"); + assertEquals(503, response.getStatusCode()); + } + + @Test + public void whenSendRequestWithoutCustomErrorFilter_thenError() { + final Response response = RestAssured.get("http://localhost:8080/foos/1"); + assertEquals(500, response.getStatusCode()); + } + +}