{"id":"d127d048ccc4c9c3","repo":"apache/kafka","slug":"invalid-leader-epoch-leaderepoch-must-be-non-ne","errorCode":null,"errorMessage":"Invalid leader epoch {leaderEpoch} (must be non-negative)","messagePattern":"Invalid leader epoch (.+?) \\(must be non-negative\\)","errorType":"validation","errorClass":"IllegalArgumentException","httpStatus":null,"severity":"error","filePath":"clients/src/main/java/org/apache/kafka/clients/Metadata.java","lineNumber":235,"sourceCode":"        this.needPartialUpdate = true;\n        this.equivalentResponseCount = 0;\n        this.requestVersion++;\n        return this.updateVersion;\n    }\n\n    /**\n     * Request an update for the partition metadata if and only if we have seen a newer leader epoch. This is called by the client\n     * any time it handles a response from the broker that includes leader epoch, except for update via Metadata RPC which\n     * follows a different code path ({@link #update}).\n     *\n     * @param topicPartition The partition for which to update the last seen leader epoch.\n     * @param leaderEpoch    The leader epoch received from the broker.\n     * @return {@code true} if we updated the last seen epoch, {@code false} otherwise.\n     */\n    public synchronized boolean updateLastSeenEpochIfNewer(TopicPartition topicPartition, int leaderEpoch) {\n        Objects.requireNonNull(topicPartition, \"TopicPartition cannot be null\");\n        if (leaderEpoch < 0)\n            throw new IllegalArgumentException(\"Invalid leader epoch \" + leaderEpoch + \" (must be non-negative)\");\n\n        Integer oldEpoch = lastSeenLeaderEpochs.get(topicPartition);\n        log.trace(\"Determining if we should replace existing epoch {} with new epoch {} for partition {}\", oldEpoch, leaderEpoch, topicPartition);\n\n        final boolean updated;\n        if (oldEpoch == null) {\n            log.debug(\"Not replacing null epoch with new epoch {} for partition {}\", leaderEpoch, topicPartition);\n            updated = false;\n        } else if (leaderEpoch > oldEpoch) {\n            log.debug(\"Updating last seen epoch from {} to {} for partition {}\", oldEpoch, leaderEpoch, topicPartition);\n            lastSeenLeaderEpochs.put(topicPartition, leaderEpoch);\n            updated = true;\n        } else {\n            log.debug(\"Not replacing existing epoch {} with new epoch {} for partition {}\", oldEpoch, leaderEpoch, topicPartition);\n            updated = false;\n        }\n\n        this.needFullUpdate = this.needFullUpdate || updated;","sourceCodeStart":217,"sourceCodeEnd":253,"githubUrl":"https://github.com/apache/kafka/blob/c31c9215e131f8c17e79f8901b48c13ee6aa8e7a/clients/src/main/java/org/apache/kafka/clients/Metadata.java#L217-L253","documentation":"IllegalArgumentException thrown by Metadata.updateLastSeenEpochIfNewer when the supplied leaderEpoch is negative. Leader epochs are monotonic non-negative counters used by the client to detect stale metadata and partition leadership changes; a negative value indicates corrupt or fabricated protocol data. The guard rejects it before it can poison the lastSeenLeaderEpochs map.","triggerScenarios":"A broker response, mocked test, or hand-crafted protocol message supplies a leaderEpoch < 0 (often -1) for a TopicPartition through any code path that calls updateLastSeenEpochIfNewer (consumer offsets, producer metadata, Fetch/Produce responses, Metadata cache updates).","commonSituations":"Test fixtures passing -1 to indicate 'unknown' instead of skipping the call; a custom broker or proxy emitting non-conformant responses; older client code upgraded against a newer broker whose epoch semantics changed; off-by-one in a serializer.","solutions":["Find the caller of updateLastSeenEpochIfNewer in the stack trace and confirm where the negative epoch originated.","If the value legitimately means 'no epoch', skip the call rather than passing -1 (guard on leaderEpoch >= 0 before invoking).","If the value comes from a broker response, capture the response and verify the broker is conformant — an unexpected negative epoch usually indicates a buggy/proxy response.","In tests, use the real broker epoch (>= 0) or omit the update."],"exampleFix":"// before\nmetadata.updateLastSeenEpochIfNewer(tp, response.leaderEpoch()); // response.leaderEpoch() == -1\n// after\nif (response.leaderEpoch() >= 0) {\n    metadata.updateLastSeenEpochIfNewer(tp, response.leaderEpoch());\n}","handlingStrategy":"type-guard","validationCode":"// Metadata.updateLastSeenEpochIfNewer rejects negative leaderEpoch.\n// If your code derives an epoch from a committed offset or broker response,\n// guard the value before passing it in. Use NO_PARTITION_LEADER_EPOCH (-1)\n// or an empty Optional to mean 'unknown' rather than a negative number.\nimport org.apache.kafka.common.TopicPartition;\nimport java.util.Optional;\n\nvoid updateEpoch(Metadata metadata, TopicPartition tp, int leaderEpoch) {\n    if (leaderEpoch < 0) {\n        // Unknown epoch — skip the update rather than throw.\n        log.debug(\"Skipping epoch update for {}: no epoch known\", tp);\n        return;\n    }\n    metadata.updateLastSeenEpochIfNewer(tp, leaderEpoch);\n}","typeGuard":"// Narrow an Optional<Integer> epoch down to a known-valid int before call.\nstatic Optional<Integer> validLeaderEpoch(int raw) {\n    return raw >= 0 ? Optional.of(raw) : Optional.empty();\n}\n\n// validLeaderEpoch(raw).ifPresent(e -> metadata.updateLastSeenEpochIfNewer(tp, e));","tryCatchPattern":"// IllegalArgumentException — programming bug, not a transient fault.\ntry {\n    metadata.updateLastSeenEpochIfNewer(tp, leaderEpoch);\n} catch (IllegalArgumentException e) {\n    log.warn(\"Ignoring bad leader epoch {} for {}\", leaderEpoch, tp, e);\n    // do not retry with the same value; the value is wrong, not the call.\n}","preventionTips":["Do not synthesise leader epochs yourself — they come from broker responses (MetadataResponse, OffsetForLeaderEpochResponse, FetchResponse). If you have no epoch, pass no epoch (absent Optional), not -1.","Treat any epoch < 0 as 'unknown' at every external boundary (offset stores, checkpoints, serializers) and normalise to Optional.empty().","Most application code never calls Metadata directly; if you find yourself doing so, prefer the higher-level Consumer/Producer/Admin APIs which manage epochs internally.","Unit-test your epoch-handling code with boundary inputs (-1, 0, Integer.MAX_VALUE)."],"tags":["metadata","leader-epoch","protocol","validation"],"analyzedSha":"c31c9215e131f8c17e79f8901b48c13ee6aa8e7a","analyzedAt":"2026-08-03T12:34:05.770Z","schemaVersion":2}