Skip to content

Instantly share code, notes, and snippets.

@espartero
Last active November 16, 2021 09:41
Show Gist options
  • Save espartero/d38e64847d584775f708086b85cef2d2 to your computer and use it in GitHub Desktop.
Save espartero/d38e64847d584775f708086b85cef2d2 to your computer and use it in GitHub Desktop.
Attempt to fix Netflix Zuul - NoSuchMethodError: ErrorController.getErrorPath()
package org.eurecat.xcare.core.gateway.spring.config;
import com.netflix.zuul.context.RequestContext;
import org.springframework.cloud.netflix.zuul.filters.RefreshableRouteLocator;
import org.springframework.cloud.netflix.zuul.filters.Route;
import org.springframework.cloud.netflix.zuul.filters.RouteLocator;
import org.springframework.cloud.netflix.zuul.web.ZuulController;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.PathMatcher;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.servlet.HandlerExecutionChain;
import org.springframework.web.servlet.handler.AbstractUrlHandlerMapping;
import javax.servlet.http.HttpServletRequest;
import java.util.Collection;
public class Spring5ZuulHandlerMapping extends AbstractUrlHandlerMapping {
private final RouteLocator routeLocator;
private final ZuulController zuul;
private final String errorPath;
private PathMatcher pathMatcher = new AntPathMatcher();
private volatile boolean dirty = true;
public Spring5ZuulHandlerMapping(RouteLocator routeLocator, ZuulController zuul, String errorPath) {
this.routeLocator = routeLocator;
this.zuul = zuul;
this.errorPath = errorPath;
}
@Override
protected HandlerExecutionChain getCorsHandlerExecutionChain(
HttpServletRequest request, HandlerExecutionChain chain,
CorsConfiguration config) {
if (config == null) {
// Allow CORS requests to go to the backend
return chain;
}
return super.getCorsHandlerExecutionChain(request, chain, config);
}
public void setDirty(boolean dirty) {
this.dirty = dirty;
if (this.routeLocator instanceof RefreshableRouteLocator) {
((RefreshableRouteLocator) this.routeLocator).refresh();
}
}
@Override
protected Object lookupHandler(String urlPath, HttpServletRequest request)
throws Exception {
if (urlPath.equals(this.errorPath)) {
return null;
}
if (isIgnoredPath(urlPath, this.routeLocator.getIgnoredPaths())) {
return null;
}
RequestContext ctx = RequestContext.getCurrentContext();
if (ctx.containsKey("forward.to")) {
return null;
}
if (this.dirty) {
synchronized (this) {
if (this.dirty) {
registerHandlers();
this.dirty = false;
}
}
}
return super.lookupHandler(urlPath, request);
}
private boolean isIgnoredPath(String urlPath, Collection<String> ignored) {
if (ignored != null) {
for (String ignoredPath : ignored) {
if (this.pathMatcher.match(ignoredPath, urlPath)) {
return true;
}
}
}
return false;
}
private void registerHandlers() {
Collection<Route> routes = this.routeLocator.getRoutes();
if (routes.isEmpty()) {
this.logger.warn("No routes found from RouteLocator");
} else {
for (Route route : routes) {
registerHandler(route.getFullPath(), this.zuul);
}
}
}
}
package org.eurecat.xcare.core.gateway.spring.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.client.discovery.event.HeartbeatEvent;
import org.springframework.cloud.client.discovery.event.HeartbeatMonitor;
import org.springframework.cloud.client.discovery.event.InstanceRegisteredEvent;
import org.springframework.cloud.client.discovery.event.ParentHeartbeatEvent;
import org.springframework.cloud.context.scope.refresh.RefreshScopeRefreshedEvent;
import org.springframework.cloud.netflix.zuul.RoutesRefreshedEvent;
import org.springframework.cloud.netflix.zuul.ZuulServerAutoConfiguration;
import org.springframework.cloud.netflix.zuul.filters.RouteLocator;
import org.springframework.cloud.netflix.zuul.web.ZuulController;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.servlet.DispatcherServlet;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import java.util.List;
import java.util.Map;
import static java.util.Collections.emptyList;
@Configuration
public class ZuulConfiguration {
private Map<String, CorsConfiguration> corsConfigurations;
@Autowired(required = false)
private List<WebMvcConfigurer> configurers = emptyList();
// Extracted from org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController
@Value("${server.error.path:${error.path:/error}}")
private String errorPath;
@Bean
public Spring5ZuulHandlerMapping spring5ZuulHandlerMapping(RouteLocator routes, ZuulController zuulController) {
Spring5ZuulHandlerMapping mapping = new Spring5ZuulHandlerMapping(routes, zuulController, this.errorPath);
mapping.setCorsConfigurations(getCorsConfigurations());
return mapping;
}
@Bean(name = DispatcherServlet.HANDLER_MAPPING_BEAN_NAME)
public ZuulExcludedCompositeHandlerMapping zuulExcludedCompositeHandlerMapping(DispatcherServlet dispatcherServlet) {
// Improve this!!!
dispatcherServlet.setDetectAllHandlerMappings(false);
return new ZuulExcludedCompositeHandlerMapping();
}
@Bean
public ApplicationListener<ApplicationEvent> zuulRefreshRoutesListener() {
return new Spring5ZuulRefreshListener();
}
protected final Map<String, CorsConfiguration> getCorsConfigurations() {
if (this.corsConfigurations == null) {
ZuulCorsRegistry registry = new ZuulCorsRegistry();
this.configurers.forEach(configurer -> configurer.addCorsMappings(registry));
this.corsConfigurations = registry.getCorsConfigurations();
}
return this.corsConfigurations;
}
private static class ZuulCorsRegistry extends CorsRegistry {
@Override
protected Map<String, CorsConfiguration> getCorsConfigurations() {
return super.getCorsConfigurations();
}
}
private static class Spring5ZuulRefreshListener implements ApplicationListener<ApplicationEvent> {
@Autowired
private Spring5ZuulHandlerMapping zuulHandlerMapping;
private HeartbeatMonitor heartbeatMonitor = new HeartbeatMonitor();
@Override
public void onApplicationEvent(ApplicationEvent event) {
if (event instanceof ContextRefreshedEvent
|| event instanceof RefreshScopeRefreshedEvent
|| event instanceof RoutesRefreshedEvent
|| event instanceof InstanceRegisteredEvent) {
reset();
} else if (event instanceof ParentHeartbeatEvent) {
ParentHeartbeatEvent e = (ParentHeartbeatEvent) event;
resetIfNeeded(e.getValue());
} else if (event instanceof HeartbeatEvent) {
HeartbeatEvent e = (HeartbeatEvent) event;
resetIfNeeded(e.getValue());
}
}
private void resetIfNeeded(Object value) {
if (this.heartbeatMonitor.update(value)) {
reset();
}
}
private void reset() {
this.zuulHandlerMapping.setDirty(true);
}
}
}
package org.eurecat.xcare.core.gateway.spring.config;
import org.springframework.beans.factory.ListableBeanFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.netflix.zuul.web.ZuulHandlerMapping;
import org.springframework.core.annotation.AnnotationAwareOrderComparator;
import org.springframework.web.servlet.HandlerExecutionChain;
import org.springframework.web.servlet.HandlerMapping;
import javax.servlet.http.HttpServletRequest;
import java.util.ArrayList;
import java.util.List;
/**
* Same that {@link org.springframework.boot.actuate.autoconfigure.web.servlet.CompositeHandlerMapping} but excluding {@link ZuulHandlerMapping}.
*/
public class ZuulExcludedCompositeHandlerMapping implements HandlerMapping {
@Autowired
private ListableBeanFactory beanFactory;
private List<HandlerMapping> mappings;
@Override
public HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
for (HandlerMapping mapping : getMappings()) {
HandlerExecutionChain handler = mapping.getHandler(request);
if (handler != null) {
return handler;
}
}
return null;
}
@Override
public boolean usesPathPatterns() {
for (HandlerMapping mapping : getMappings()) {
if (mapping.usesPathPatterns()) {
return true;
}
}
return false;
}
private List<HandlerMapping> getMappings() {
if (this.mappings == null) {
this.mappings = extractMappings();
}
return this.mappings;
}
private List<HandlerMapping> extractMappings() {
List<HandlerMapping> list = new ArrayList<>(this.beanFactory.getBeansOfType(HandlerMapping.class).values());
list.remove(this);
list.removeIf(handlerMapping -> handlerMapping.getClass().equals(ZuulHandlerMapping.class));
AnnotationAwareOrderComparator.sort(list);
return list;
}
}
@espartero
Copy link
Author

espartero commented Aug 13, 2021

Setting up a project with Spring Boot 2.5.2 and Netflix Zuul fails with NoSuchMethodError: ErrorController.getErrorPath().

This is commented here and suggested a fix here, but there's no plan to integrate it.

The idea is to remove the default ZuulHandlerMapping from the DispatchServlet (done in ZuulExcludedCompositeHandlerMapping), and register a new one (Spring5ZuulHandlerMapping) that it's just a copy of ZuulHandlerMapping but fixing the compilation error. Note that one application listener is also registered, implementing the same behavior that the old one. Not sure how to call dispatcherServlet.setDetectAllHandlerMappings(false), so ended up calling it on a configuration class, just for test purposes.

Not fully tested!!

@EugeneGoroschenyaOld
Copy link

Hi @espartero

... I was not able to disable {@link org.springframework.cloud.netflix.zuul.ZuulServerAutoConfiguration}.

Perhaps because there is also public class ZuulProxyAutoConfiguration extends ZuulServerAutoConfiguration.
Did you try with @SpringBootApplication(exclude = {ZuulServerAutoConfiguration.class, ZuulProxyAutoConfiguration.class}) ?

@espartero
Copy link
Author

Hi! Yes, tried it, but with the same result. So, not very sure ho is triggering this auto config.

Anyway, maybe it's not a good idea to disable it because there's a lot of necessary stuff there and there's no way to skip the HandlerMapping registration. I've removed this comment.

@EugeneGoroschenyaOld
Copy link

EugeneGoroschenyaOld commented Aug 17, 2021

yeah, excluding Zuul Auto Configuration requires including them back (and some other classes because of visibility) as copies from spring-cloud-netflix with the only fix for the BasicErrorController.

But I had to do this with the combination of your fix becasue your gist "as is" does not work for me.

See https://gist.github.com/EugeneGoroschenya/46e1b3bdb8861f110395b1847c03e68c

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment