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;
}
};
}
}