{"id":"44f0bd9eb5329cfa","repo":"apache/kafka","slug":"seek-offset-must-not-be-a-negative-number","errorCode":null,"errorMessage":"seek offset must not be a negative number","messagePattern":"seek offset must not be a negative number","errorType":"validation","errorClass":"java.lang.IllegalArgumentException","httpStatus":null,"severity":"error","filePath":"clients/src/main/java/org/apache/kafka/clients/consumer/internals/AsyncKafkaConsumer.java","lineNumber":1156,"sourceCode":"        throwIfGroupIdNotDefined();\n        offsetCommitCallbackInvoker.executeCallbacks();\n\n        if (commitEvent.offsets().isPresent() && commitEvent.offsets().get().isEmpty()) {\n            return CompletableFuture.completedFuture(null);\n        }\n\n        applicationEventHandler.add(commitEvent);\n\n        // This blocks until the background thread retrieves allConsumed positions to commit if none were explicitly specified.\n        // This operation will ensure that the offsets to commit are not affected by fetches which may start after this\n        ConsumerUtils.getResult(commitEvent.offsetsReady(), defaultApiTimeoutMs.toMillis());\n        return commitEvent.future();\n    }\n\n    @Override\n    public void seek(TopicPartition partition, long offset) {\n        if (offset < 0)\n            throw new IllegalArgumentException(\"seek offset must not be a negative number\");\n\n        acquireAndEnsureOpen();\n        try {\n            log.info(\"Seeking to offset {} for partition {}\", offset, partition);\n            SeekUnvalidatedEvent seekUnvalidatedEventEvent = new SeekUnvalidatedEvent(\n                defaultApiTimeoutDeadlineMs(),\n                partition,\n                offset,\n                Optional.empty()\n            );\n            applicationEventHandler.addAndGet(seekUnvalidatedEventEvent);\n        } finally {\n            release();\n        }\n    }\n\n    @Override\n    public void seek(TopicPartition partition, OffsetAndMetadata offsetAndMetadata) {","sourceCodeStart":1138,"sourceCodeEnd":1174,"githubUrl":"https://github.com/apache/kafka/blob/c31c9215e131f8c17e79f8901b48c13ee6aa8e7a/clients/src/main/java/org/apache/kafka/clients/consumer/internals/AsyncKafkaConsumer.java#L1138-L1174","documentation":"IllegalArgumentException thrown at the entry of AsyncKafkaConsumer.seek(TopicPartition, long) when the supplied offset is < 0. Offsets are non-negative monotonically increasing positions within a partition; a negative offset has no valid meaning and is rejected before any network I/O. This guard runs before acquireAndEnsureOpen(), so it fires regardless of consumer state.","triggerScenarios":"Calling consumer.seek(partition, -1) or passing a computed offset that became negative (e.g. committedOffset - N with N > committedOffset, or defaulting an unknown/uninitialized offset to -1). Also triggered by seek to a sentinel like -2 / -1 that older code used to mean 'beginning'/'end' (use seekToBeginning/seekToEnd instead).","commonSituations":"Offset arithmetic bugs (committedOffset - lookback where lookback exceeds committed); using -1 as a 'not yet set' placeholder that leaks into seek; porting code from an API that accepted negative sentinels; caching layer returning -1 on miss and feeding it to seek; off-by-one in tests.","solutions":["Validate the offset is >= 0 before calling seek; if it is -1 or unknown, call seekToBeginning/seekToEnd or skip the seek.","Fix the upstream offset computation: clamp at 0, or branch when the source offset is uninitialized.","Replace sentinel-style usage (-1, -2) with the dedicated seekToBeginning(Collection) / seekToEnd(Collection) APIs."],"exampleFix":"// before\nlong offset = committedOffset != null ? committedOffset - lookback : -1;\nconsumer.seek(tp, offset);\n\n// after\nif (committedOffset == null) {\n    consumer.seekToBeginning(List.of(tp));\n} else {\n    consumer.seek(tp, Math.max(0, committedOffset - lookback));\n}","handlingStrategy":"type-guard","validationCode":"// seek(TopicPartition, long) — guard the offset at the call site.\nstatic void safeSeek(Consumer<?, ?> c, TopicPartition tp, long offset) {\n    if (offset < 0)\n        throw new IllegalArgumentException(\"seek offset for \" + tp + \" is negative: \" + offset);\n    c.seek(tp, offset);\n}\n\n// If the offset originates from external state (DB, file), coerce defensively:\nlong parsed = Long.parseLong(stored);\nif (parsed < 0) throw new IllegalStateException(\"Stored offset corrupt (negative): \" + stored);\nsafeSeek(consumer, tp, parsed);","typeGuard":"// Use a non-negative wrapper type so the compiler rejects bad values at the source.\n// In Java, a small value class with a static factory that validates:\npublic final class NonNegativeOffset {\n    private final long value;\n    private NonNegativeOffset(long v) { this.value = v; }\n    public static NonNegativeOffset of(long v) {\n        if (v < 0) throw new IllegalArgumentException(\"offset must be >= 0, got \" + v);\n        return new NonNegativeOffset(v);\n    }\n    public long value() { return value; }\n}\n\n// TypeScript analogue:\n//   type NonNegativeLong = number & { __brand: 'NonNegative' };\n//   function nonNegative(n: number): NonNegativeLong {\n//     if (!Number.isInteger(n) || n < 0) throw new RangeError('negative offset');\n//     return n as NonNegativeLong;\n//   }\n//   function seek(c: Consumer, tp: TopicPartition, off: NonNegativeLong): void { c.seek(tp, off); }","tryCatchPattern":"// IllegalArgumentException from seek is a programmer error; catch only to enrich\n// diagnostics, then propagate (do not retry the same value).\ntry {\n    consumer.seek(tp, offset);\n} catch (IllegalArgumentException e) {\n    throw new IllegalStateException(\n        \"Refusing to seek \" + tp + \" to offset \" + offset + \"; check the source of this offset\", e);\n}","preventionTips":["Treat offsets as unsigned-like quantities: validate >= 0 at every external boundary (DB read, file parse, queue message).","Persist offsets alongside a checksum or sentinel so a corrupted store is detectable before it reaches seek().","Default to seekToBeginning/seekToEnd when the stored offset is unusable, rather than passing a sentinel like -1.","Unit-test seek paths with offset 0 (boundary) and a large positive value to confirm your validation matches the library's."],"tags":["consumer","seek","offset","validation"],"analyzedSha":"c31c9215e131f8c17e79f8901b48c13ee6aa8e7a","analyzedAt":"2026-08-03T12:34:05.770Z","schemaVersion":2}