Implementing Custom Filters and Error Handling with Spring Cloud Zuul

Understanding Zuul Filter Mechanisms

The core functionality of the Zuul API Gateway is driven by its filter system, which intercepts HTTP requests and responses to handle cross-cutting concerns. Filters are constructed to manipulate the RequestContext, which holds the state for the current request, including request and response objects, routing data, and error information.

Zuul defines four standard filter types that correspond to the request lifecycle:

  • PRE: Filters are executed before the request is routed to the origin server. They are typically used for authentication, request validation, or choosing the route.
  • ROUTING: These filters handle the actual routing of the request to the downstream microservice using clients like Apache HttpClient or Netflix Ribbon.
  • POST: Executed after the request has been routed and the response is received. They are used for adding standard headers to responses, collecting metrics, or modifying the response body.
  • ERROR: Triggered when an error occurs during any other phase of processing.

Developing a Custom Pre-Filter

To implement custom logic, such as security checks, a class must extend ZuulFilter. The following example demonstrates an AccessControlFilter that validates the presence of a specific API Key header. If the header is missing, the filter blocks the request and returns a 401 Unauthorized status immediately.

@Component
public class AccessControlFilter extends ZuulFilter {

    @Override
    public String filterType() {
        return FilterConstants.PRE_TYPE;
    }

    @Override
    public int filterOrder() {
        return 5;
    }

    @Override
    public boolean shouldFilter() {
        return true;
    }

    @Override
    public Object run() throws ZuulException {
        RequestContext ctx = RequestContext.getCurrentContext();
        HttpServletRequest request = ctx.getRequest();
        
        String apiKey = request.getHeader("X-API-Key");
        
        if (StringUtils.isEmpty(apiKey)) {
            ctx.setSendZuulResponse(false);
            ctx.setResponseStatusCode(HttpStatus.UNAUTHORIZED.value());
            ctx.setResponseBody("{\"error\":\"API Key is missing\"}");
            ctx.getResponse().setContentType("application/");
        }
        return null;
    }
}

Customizing Error Handling

Zuul includes a default SendErrorFilter that forwards errors to the /error endpoint. To customize the error response format, you can extend this class. However, you must disable the default filter to prevent duplicate processing. This is done via configuration and by overriding the run method in your custom filter.

To disable the built-in filter, add the following to your application.properties:

zuul.SendErrorFilter.error.disable=true

The custom error filter implementation:

@Component
public class CustomErrorFilter extends SendErrorFilter {

    @Override
    public Object run() {
        RequestContext context = RequestContext.getCurrentContext();
        try {
            ExceptionHolder exception = findZuulException(context.getThrowable());
            
            // Log the specific error cause
            System.out.println("Error detected: " + exception.getErrorCause());

            HttpServletResponse response = context.getResponse();
            response.setCharacterEncoding("UTF-8");
            response.setContentType("application/");
            
            String errorMessage = "{\"status\":\"error\", \"details\":\"Request processing failed\"}";
            response.getOutputStream().write(errorMessage.getBytes());
            
        } catch (IOException ex) {
            ReflectionUtils.rethrowRuntimeException(ex);
        }
        return null;
    }
}

Implementing Hystrix Fallbacks

When a downstream service becomes unavailable or times out, Zuul (integrated with Hystrix) can provide a fallback response. This is achieved by implementing the FallbackProvider interface. This allows for graceful degradation of service rather than throwing a raw exception to the client.

@Component
public class ServiceFallbackProvider implements FallbackProvider {

    private static final String TARGET_SERVICE = "inventory-service";

    @Override
    public String getRoute() {
        // Return "*" to apply to all services, or a specific service ID
        return TARGET_SERVICE;
    }

    @Override
    public ClientHttpResponse fallbackResponse(String route, Throwable cause) {
        if (cause instanceof HystrixTimeoutException) {
            return generateResponse(HttpStatus.GATEWAY_TIMEOUT, "Service timed out");
        } else {
            return generateResponse(HttpStatus.INTERNAL_SERVER_ERROR, "Service unavailable");
        }
    }

    private ClientHttpResponse generateResponse(final HttpStatus status, final String message) {
        return new ClientHttpResponse() {
            @Override
            public HttpStatus getStatusCode() throws IOException {
                return status;
            }

            @Override
            public int getRawStatusCode() throws IOException {
                return status.value();
            }

            @Override
            public String getStatusText() throws IOException {
                return status.getReasonPhrase();
            }

            @Override
            public void close() {
            }

            @Override
            public InputStream getBody() throws IOException {
                String body = "{\"code\":" + status.value() + ", \"message\":\"" + message + "\"}";
                return new ByteArrayInputStream(body.getBytes());
            }

            @Override
            public HttpHeaders getHeaders() {
                HttpHeaders headers = new HttpHeaders();
                headers.setContentType(MediaType.APPLICATION_JSON);
                return headers;
            }
        };
    }
}

Tags: Spring Cloud Zuul java microservices API Gateway

Posted on Wed, 13 May 2026 05:30:47 +0000 by Rupuz