How to Implement Unified Exception Handling with Zuul Error Filters

This article explains why exceptions thrown in a Zuul pre‑filter are silent, shows how to set error parameters in the request context, and provides two solutions—adding try‑catch logic to the filter or creating a dedicated error‑filter—to ensure that error details are correctly propagated and returned to the client.

Programmer DD
Programmer DD
Programmer DD
How to Implement Unified Exception Handling with Zuul Error Filters

Background

The previous article introduced several core Zuul filters but omitted the error filter, leaving no unified way to handle exceptions. When a pre‑filter throws an exception, the gateway logs the filter name but returns no response.

Default Exception Behavior

Example of a pre‑filter that throws a RuntimeException:

public class ThrowExceptionFilter extends ZuulFilter {
    private static Logger log = LoggerFactory.getLogger(ThrowExceptionFilter.class);
    @Override
    public String filterType() { return "pre"; }
    @Override
    public int filterOrder() { return 0; }
    @Override
    public boolean shouldFilter() { return true; }
    @Override
    public Object run() {
        log.info("This is a pre filter, it will throw a RuntimeException");
        doSomething();
        return null;
    }
    private void doSomething() {
        throw new RuntimeException("Exist some errors...");
    }
}

Running the gateway shows the log message but no error details in the response because the SendErrorFilter is never invoked.

Why SendErrorFilter Is Not Triggered

The SendErrorFilter checks the request context for the key error.status_code:

public boolean shouldFilter() {
    RequestContext ctx = RequestContext.getCurrentContext();
    return ctx.containsKey("error.status_code") &&
           !ctx.getBoolean(SEND_ERROR_FILTER_RAN, false);
}

Since the original ThrowExceptionFilter does not set this key, the error filter skips processing.

How Existing Filters Set Error Information

Filters such as RibbonRoutingFilter catch exceptions and populate three context parameters: error.status_code – HTTP status code error.exception – the exception object error.message – error message

public Object run() {
    RequestContext context = RequestContext.getCurrentContext();
    try {
        // normal routing logic
    } catch (ZuulException ex) {
        context.set(ERROR_STATUS_CODE, ex.nStatusCode);
        context.set("error.message", ex.errorCause);
        context.set("error.exception", ex);
    } catch (Exception ex) {
        context.set("error.status_code", HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
        context.set("error.exception", ex);
    }
    return null;
}

Solution 1: Add Try‑Catch Logic to the Pre‑Filter

Modify the pre‑filter to catch exceptions and set the required error parameters:

public Object run() {
    log.info("This is a pre filter, it will throw a RuntimeException");
    RequestContext ctx = RequestContext.getCurrentContext();
    try {
        doSomething();
    } catch (Exception e) {
        ctx.set("error.status_code", HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
        ctx.set("error.exception", e);
    }
    return null;
}

After redeploying, a request that triggers the exception returns a JSON payload such as:

{
    "timestamp": 1481674980376,
    "status": 500,
    "error": "Internal Server Error",
    "exception": "java.lang.RuntimeException",
    "message": "Exist some errors..."
}

Solution 2: Create a Dedicated error Filter

Implement an error filter that runs when any exception bubbles up, extracts the throwable from the context, and populates the same error keys:

public class ErrorFilter extends ZuulFilter {
    Logger log = LoggerFactory.getLogger(ErrorFilter.class);
    @Override
    public String filterType() { return "error"; }
    @Override
    public int filterOrder() { return 10; }
    @Override
    public boolean shouldFilter() { return true; }
    @Override
    public Object run() {
        RequestContext ctx = RequestContext.getCurrentContext();
        Throwable throwable = ctx.getThrowable();
        log.error("this is an ErrorFilter : {}", throwable.getCause().getMessage());
        ctx.set("error.status_code", HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
        ctx.set("error.exception", throwable.getCause());
        // optional custom message
        // ctx.set("error.message", "Custom error message");
        return null;
    }
}

When this filter is added to the gateway, the same exception now produces the expected error response, and the gateway console logs the exception details.

Key Takeaways

Zuul only forwards error information to SendErrorFilter if error.status_code is present in the request context.

Either enrich your own filters with try‑catch blocks that set the error keys, or implement a dedicated error filter to handle uncaught exceptions uniformly.

You can customize the message returned to the client by setting error.message in the context.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

BackendJavaException HandlingSpring CloudZuulError Filter
Programmer DD
Written by

Programmer DD

A tinkering programmer and author of "Spring Cloud Microservices in Action"

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.