DrKLO/Telegram · critical · IllegalArgumentException

Unknown update op type for {}

Error message

Unknown update op type for {}

What it means

Thrown in consumeUpdatesInOneDeferredPass, the fallback path that applies ALL queued updates in a single pass when no pre-layout is needed. The switch handles ADD, MOVE, REMOVE, UPDATE and rejects anything else. Reaching the default means an UpdateOp in mPendingUpdates has a cmd value outside the four known constants — effectively corrupted state. Like errors 1 and 2, this is an internal invariant guard, not something a normal caller triggers directly.

Source

Thrown at TMessagesProj/src/main/java/androidx/recyclerview/widget/AdapterHelper.java:489

            Log.d(TAG, "postponing " + op);
        }
        mPostponedList.add(op);
        switch (op.cmd) {
            case UpdateOp.ADD:
                mCallback.offsetPositionsForAdd(op.positionStart, op.itemCount);
                break;
            case UpdateOp.MOVE:
                mCallback.offsetPositionsForMove(op.positionStart, op.itemCount);
                break;
            case UpdateOp.REMOVE:
                mCallback.offsetPositionsForRemovingLaidOutOrNewView(op.positionStart,
                        op.itemCount);
                break;
            case UpdateOp.UPDATE:
                mCallback.markViewHoldersUpdated(op.positionStart, op.itemCount, op.payload);
                break;
            default:
                throw new IllegalArgumentException("Unknown update op type for " + op);
        }
    }

    boolean hasPendingUpdates() {
        return mPendingUpdates.size() > 0;
    }

    boolean hasAnyUpdateTypes(int updateTypes) {
        return (mExistingUpdateTypes & updateTypes) != 0;
    }

    int findPositionOffset(int position) {
        return findPositionOffset(position, 0);
    }

    int findPositionOffset(int position, int firstPostponedItem) {
        int count = mPostponedList.size();
        for (int i = firstPostponedItem; i < count; ++i) {

View on GitHub (pinned to 45ab8f4308)

Solutions

  1. Keep UpdateOp and AdapterHelper source from the same AndroidX version in the fork.
  2. Add keep rules for the recyclerview widget package to prevent constant inlining issues under R8.
  3. Ensure all notify* calls originate from the main thread.
  4. If the error is reproducible, log the op.toString() at enqueue time to find which component built the bad op.
Defensive patterns

Strategy: validation

Validate before calling

// Reject unknown op before enqueue
static int requireKnownCmd(int cmd) {
    switch (cmd) {
        case UpdateOp.ADD:
        case UpdateOp.MOVE:
        case UpdateOp.REMOVE:
        case UpdateOp.UPDATE: return cmd;
        default: throw new IllegalArgumentException("bad cmd " + cmd);
    }
}

Prevention

When it happens

Trigger: UpdateOp object reuse where cmd was set to an invalid value; a fork that added a new UpdateOp command constant without updating this switch; R8/ProGuard obfuscation collapsing distinct int constants; a custom AdapterHelper.Callback synthesizing malformed ops.

Common situations: Fork drift between UpdateOp.java and AdapterHelper.java; an obfuscation bug in a stripped release build; memory corruption from a native side or Unsafe usage touching the op array; concurrent enqueue of ops from multiple threads without synchronization.

Related errors


AI-assisted analysis of DrKLO/Telegram@45ab8f4308 (2026-08-14). Data as JSON: /api/errors/8beeb954540bdfca. Report an issue: GitHub.