alibaba/ARouter · error · IllegalStateException

Interceptor timeout has already been scheduled.

Error message

Interceptor timeout has already been scheduled.

What it means

InterceptorChain.scheduleTimeout arms a watchdog on the chain's timeout executor and uses a CAS flag to guarantee it is armed at most once per chain. Throwing IllegalStateException here means scheduleTimeout was invoked twice on the same chain instance, which is an internal misuse of the chain's lifecycle.

Source

Thrown at arouter-api/src/main/java/com/alibaba/android/arouter/core/InterceptorChain.java:71

                     ScheduledThreadPoolExecutor timeoutExecutor) {
        this(interceptors, postcard, completionCallback, timeoutExecutor, DEFAULT_COMPLETION_EXECUTOR);
    }

    InterceptorChain(List<IInterceptor> interceptors,
                     Postcard postcard,
                     InterceptorCallback completionCallback,
                     ScheduledThreadPoolExecutor timeoutExecutor,
                     ExecutorService completionExecutor) {
        this.interceptors = new ArrayList<IInterceptor>(interceptors);
        this.postcard = postcard;
        this.completionCallback = completionCallback;
        this.timeoutExecutor = timeoutExecutor;
        this.completionExecutor = completionExecutor;
    }

    void scheduleTimeout(long timeout, TimeUnit unit) {
        if (!timeoutScheduled.compareAndSet(false, true)) {
            throw new IllegalStateException("Interceptor timeout has already been scheduled.");
        }

        ScheduledFuture<?> future = timeoutExecutor.schedule(new Runnable() {
            @Override
            public void run() {
                onTimeout();
            }
        }, timeout, unit);
        timeoutFuture = future;

        // A zero-length timeout may finish before schedule() returns.
        if (completed.get()) {
            cancelTimeout(future);
        }
    }

    @Override
    public void run() {

View on GitHub (pinned to 84f451d244)

Solutions

  1. Arm the timeout exactly once, when the chain is created or started, and never in callback/continue paths
  2. Check that no racing code path (callback vs interrupt) both schedules; pick one owner for scheduling
  3. If you cannot be sure, drop the second call instead of relying on the throw (the CAS is a guard, not a feature)

Example fix

// before
chain.scheduleTimeout(5, TimeUnit.SECONDS);
// ...later on callback resume
chain.scheduleTimeout(5, TimeUnit.SECONDS); // throws
// after
if (!chainStarted) {
    chain.scheduleTimeout(5, TimeUnit.SECONDS);
    chainStarted = true;
}
Defensive patterns

Strategy: try-catch

Validate before calling

if (timeoutScheduledOnce) {
    throw new IllegalStateException("timeout already armed for this chain");
}

Try / catch

try {
    chain.scheduleTimeout(timeout, unit);
} catch (IllegalStateException e) {
    // second arm attempt: ignore, the original timeout still governs the chain
    Log.w(TAG, "timeout already scheduled for chain", e);
}

Prevention

When it happens

Trigger: Calling scheduleTimeout more than once on the same InterceptorChain — e.g. scheduling a timeout both when the chain starts and again after a callback resume, or double-arming from both a racing callback path and the timeout-cancel path.

Common situations: Custom interceptor hosts or tests that wrap/forward the chain and inadvertently re-invoke scheduleTimeout; async interceptors that call continue() and then also trigger a re-schedule in their completion path.

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


AI-assisted analysis of alibaba/ARouter@84f451d244 (2026-09-06). Data as JSON: /api/errors/2f504204c73615ad. Report an issue: GitHub.