Verification tools
None of the tools below overlap perfectly, and none is close to complete on its own — the AI Bug Catalog’s “Caught by” line for each of its 21 entries names exactly which one (if any) of the tools below would have caught it, and most of those 21 name none of them at the point the bug is introduced. Every output below is real, freshly captured for this module against either kafka-reading-a (this module’s own 3-broker cluster) or this course’s shared harness broker.
tsc --noEmit — proves types, never durability or ordering
Section titled “tsc --noEmit — proves types, never durability or ordering”TypeScript compiling is the weakest and cheapest signal on this page, and it is important to be precise about what it actually rules out: a kafka.producer() call with no idempotent option compiles identically to one with idempotent: true — both are valid according to the type definitions, because leaving a boolean option unset is not a type error. Every “assumed default” entry in the catalog (4, 5, 11) compiles cleanly in both its buggy and fixed form:
$ tsc --noEmit src/reading-kafka/ai-bug-catalog/04-idempotent-default.ts$ echo $?0tsc is a real, useful, zero-cost gate — it is just a gate for a different class of bug (a typo’d option name, a wrong argument type) than almost everything in this module’s catalog.
kafka-configs.sh --describe --all — does the key exist, and where did its value actually come from?
Section titled “kafka-configs.sh --describe --all — does the key exist, and where did its value actually come from?”This is the single most useful command on this page for catalog entries 13 and 15. --all prints every config with its source — DEFAULT_CONFIG, STATIC_BROKER_CONFIG, or DYNAMIC_BROKER_CONFIG — which is the only way to know before attempting a change whether it is even dynamically alterable:
$ kafka-configs.sh --bootstrap-server "$BOOTSTRAP" --describe --entity-type brokers --entity-name 1 --all | grep auto.create auto.create.topics.enable=true sensitive=false synonyms={DEFAULT_CONFIG:auto.create.topics.enable=true}Only DEFAULT_CONFIG appears in synonyms — no DYNAMIC_BROKER_CONFIG entry exists for this key, which is the live signal that entry 15’s --alter attempt is doomed before it even runs. The same command against a topic’s min.insync.replicas (entry 13) shows the config was accepted with no cross-check against ReplicationFactor at all:
$ kafka-configs.sh --bootstrap-server "$BOOTSTRAP" --describe --entity-type topics --entity-name kip579-topic --allAll configs for topic kip579-topic are: min.insync.replicas=2 sensitive=false synonyms={DYNAMIC_TOPIC_CONFIG:min.insync.replicas=2, DYNAMIC_DEFAULT_BROKER_CONFIG:min.insync.replicas=1, DEFAULT_CONFIG:min.insync.replicas=1}kafka-topics.sh --describe (and --under-min-isr-partitions) — RF, ISR, and the one flag that flags the fragility
Section titled “kafka-topics.sh --describe (and --under-min-isr-partitions) — RF, ISR, and the one flag that flags the fragility”--describe alone never flags entry 13’s fragile combination as an error — it just prints ReplicationFactor: 1 and Isr: 2 side by side and leaves the reader to notice they don’t leave any room below min.insync.replicas=2. --under-min-isr-partitions is the one flag that actively calls it out, and only once the ISR count genuinely drops below the configured minimum:
$ kafka-topics.sh --bootstrap-server "$BOOTSTRAP" --describe --topic kip579-topicTopic: kip579-topic PartitionCount: 1 ReplicationFactor: 1 Configs: min.insync.replicas=2 Topic: kip579-topic Partition: 0 Leader: 2 Replicas: 2 Isr: 2For entry 14 (partition remap), --describe before and after --alter --partitions is the fastest way to confirm the count actually changed cluster-side — but it tells you nothing about which of your already-connected clients have refreshed their own metadata since; only a real producer.send() from that specific client instance answers that, exactly as the catalog entry proves.
kafka-consumer-groups.sh --describe / --list --type — offsets, lag, and which rebalance protocol a group actually joined
Section titled “kafka-consumer-groups.sh --describe / --list --type — offsets, lag, and which rebalance protocol a group actually joined”--describe --group <g> comparing CURRENT-OFFSET to LOG-END-OFFSET is the fastest live signal for catalog entries 8 and 9 (the off-by-one commit and the DLQ loop) — a permanent, unmoving LAG of exactly 1 is the tell in both cases:
$ kafka-consumer-groups.sh --bootstrap-server "$BOOTSTRAP" --describe --group off-by-one-group-3GROUP TOPIC PARTITION CURRENT-OFFSET LOG-END-OFFSET LAGoff-by-one-group-3 off-by-one-topic-3 0 2 3 1--list --type classic vs --type consumer is the only way to confirm entry 2’s claim directly — a group that joined with KafkaJS reports Classic no matter what Java-only group.protocol value was in the config object:
$ kafka-consumer-groups.sh --bootstrap-server "$BOOTSTRAP" --list --type classicquick-batch-group-2 Classic--list --type is strict about its valid values, which is itself a useful real signal — it does not accept share (share groups have their own dedicated tool, next):
$ kafka-consumer-groups.sh --bootstrap-server "$BOOTSTRAP" --list --type shareInvalid types list 'share'. Valid types are: Consumer, Classickafka-console-share-consumer.sh / kafka-share-groups.sh — the CLI KafkaJS has no equivalent of
Section titled “kafka-console-share-consumer.sh / kafka-share-groups.sh — the CLI KafkaJS has no equivalent of”Two real, separate tools this course’s broker image ships specifically for KIP-932 share groups, neither of which has any KafkaJS counterpart (catalog entry 21): kafka-console-share-consumer.sh actually consumes with ACCEPT/RELEASE/REJECT semantics, and kafka-share-groups.sh is the dedicated admin CLI (kafka-consumer-groups.sh cannot list or describe a share group at all):
$ kafka-share-groups.sh --bootstrap-server "$BOOTSTRAP" --listmy-share-group
$ kafka-share-groups.sh --bootstrap-server "$BOOTSTRAP" --describe --group my-share-groupGROUP TOPIC PARTITION START-OFFSET LAGmy-share-group share-group-topic 0 - -kafka-dump-log.sh — the actual bytes on disk, past what any client-side API reports
Section titled “kafka-dump-log.sh — the actual bytes on disk, past what any client-side API reports”--print-data-log reads a real segment file directly and prints every record’s real offset, size, and payload — the ground truth underneath every offset-related catalog entry (8, 9, 10):
$ kafka-dump-log.sh --deep-iteration --print-data-log --files /tmp/kafka-logs/off-by-one-topic-3-0/00000000000000000000.logDumping /tmp/kafka-logs/off-by-one-topic-3-0/00000000000000000000.logLog starting offset: 0baseOffset: 0 lastOffset: 2 count: 3 baseSequence: 0 lastSequence: 2 producerId: -1 producerEpoch: 0 partitionLeaderEpoch: 0 isTransactional: false isControl: false deleteHorizonMs: OptionalLong.empty position: 0 CreateTime: 1790650295771 size: 97 magic: 2 compresscodec: none crc: 200060204 isvalid: true| offset: 0 CreateTime: 1790650295771 keySize: -1 valueSize: 5 sequence: 0 headerKeys: [] payload: msg-0| offset: 1 CreateTime: 1790650295771 keySize: -1 valueSize: 5 sequence: 1 headerKeys: [] payload: msg-1| offset: 2 CreateTime: 1790650295771 keySize: -1 valueSize: 5 sequence: 2 headerKeys: [] payload: msg-2All three records are genuinely on disk here, isTransactional: false — this is the tool that would let a reader confirm, independent of any client library’s word for it, exactly what a segment holds.
kafka-features.sh describe — which KIPs are actually finalized on this exact cluster
Section titled “kafka-features.sh describe — which KIPs are actually finalized on this exact cluster”Feature flags (metadata.version, share.version, transaction.version, kraft.version, group.version, streams.version) each carry a FinalizedVersionLevel — the only way to confirm a KIP this module discusses (KIP-848’s group.version, KIP-932’s share.version) is actually active, rather than assumed from the Kafka version number alone:
$ kafka-features.sh --bootstrap-server "$BOOTSTRAP" describeFeature: group.version SupportedMinVersion: 0 SupportedMaxVersion: 1 FinalizedVersionLevel: 1 Epoch: 5926Feature: kraft.version SupportedMinVersion: 0 SupportedMaxVersion: 1 FinalizedVersionLevel: 0 Epoch: 5926Feature: metadata.version SupportedMinVersion: 3.3-IV3 SupportedMaxVersion: 4.3-IV0 FinalizedVersionLevel: 4.3-IV0 Epoch: 5926Feature: share.version SupportedMinVersion: 0 SupportedMaxVersion: 1 FinalizedVersionLevel: 1 Epoch: 5926Feature: transaction.version SupportedMinVersion: 0 SupportedMaxVersion: 2 FinalizedVersionLevel: 2 Epoch: 5926kafka-acls.sh — the fastest way to learn whether a cluster is checking permissions at all
Section titled “kafka-acls.sh — the fastest way to learn whether a cluster is checking permissions at all”Before trusting any ACL grant/deny reasoning, this one command answers the prior question catalog entry 18 is about — is an authorizer even configured:
$ kafka-acls.sh --bootstrap-server "$BOOTSTRAP" --listError while executing ACL command: org.apache.kafka.common.errors.SecurityDisabledException: No Authorizer is configured on the brokerThat exact error, unprompted, is the fastest possible confirmation that every ACL rule in a runbook is currently a no-op on this cluster; see Security and Production for the real, current TopicAuthorizationException a configured-but-ungranted principal gets once an authorizer is in place.
Connect REST: GET /connector-plugins and PUT /.../config/validate
Section titled “Connect REST: GET /connector-plugins and PUT /.../config/validate”/connector-plugins lists exactly what a running worker can discover — the direct fix-confirmation for catalog entry 19:
$ curl localhost:8083/connector-plugins[{"class":"org.apache.kafka.connect.file.FileStreamSinkConnector","type":"sink","version":"4.3.1"}, {"class":"org.apache.kafka.connect.file.FileStreamSourceConnector","type":"source","version":"4.3.1"}, {"class":"org.apache.kafka.connect.mirror.MirrorCheckpointConnector","type":"source","version":"4.3.1"}, {"class":"org.apache.kafka.connect.mirror.MirrorHeartbeatConnector","type":"source","version":"4.3.1"}, {"class":"org.apache.kafka.connect.mirror.MirrorSourceConnector","type":"source","version":"4.3.1"}]Both FileStreamSourceConnector and FileStreamSinkConnector are now listed — this is the exact output that changed once plugin.path=/opt/kafka/libs was added, confirming discovery live rather than reading a log line. PUT /connector-plugins/<class>/config/validate (the endpoint catalog entry 20 uses) checks a proposed config against a real, registered connector’s ConfigDef without ever starting it — it is the cheapest way to confirm errors.tolerance resolved to none, not assumed from documentation, before submitting anything for real.
Schema Registry: /compatibility — out of this module’s own docker budget, documented honestly
Section titled “Schema Registry: /compatibility — out of this module’s own docker budget, documented honestly”This module’s own hard rule is one docker stack, and kafka-reading-a’s three brokers already spend that budget — confluentinc/cp-schema-registry is a separate Confluent image this module did not pull. The /compatibility/subjects/<subject>/versions/<version> endpoint and its real 409 Conflict under BACKWARD compatibility are already proven, with real HTTP output, in Schema Registry — that lesson’s own broker+registry stack is the place to see it run, rather than this module re-proving the same fact on a stack that was never provisioned for it.
This course’s own harness: tools/verify-snippets.mjs
Section titled “This course’s own harness: tools/verify-snippets.mjs”Every fence in this module that declares a real path on its first line (// src/reading-kafka/ai-bug-catalog/<name>.ts, # scripts/.../<name>.sh, # config/.../<name>.properties) is collected and checked by this repository’s own harness — read tools/verify-snippets.mjs’s header comment (its first ~120 lines) before trusting any description of it secondhand, including this one:
npm run verify # tsc --noEmit + bash -n + properties-key check, over every collected fencenpm run verify -- --run reading-kafka/ai-bug-catalog # actually runs every // @run TypeScript fencenpm run verify -- --cli reading-kafka/ai-bug-catalog # actually runs every bash fence inside the broker containernpm run verify -- --props # properties-key check only, no broker neededA fence whose first line contains @expect-error — every buggy snippet in the AI Bug Catalog — is deliberately not collected: the harness skips it entirely, and the lesson prose carries the real, separately-captured output instead. This is the same convention the rest of this course uses, not something new to this module. Real, freshly run against this exact catalog:
$ npm run -s verify -- --run reading-kafka/ai-bug-catalogrunning .../ai-bug-catalog.mdx:fence #12 ... exit code: 0running .../ai-bug-catalog.mdx:fence #15 ... exit code: 0running .../ai-bug-catalog.mdx:fence #18 ... exit code: 0running .../ai-bug-catalog.mdx:fence #21 ... exit code: 0running .../ai-bug-catalog.mdx:fence #24 ... exit code: 0running .../ai-bug-catalog.mdx:fence #27 ... exit code: 0running .../ai-bug-catalog.mdx:fence #31 ... exit code: 0running .../ai-bug-catalog.mdx:fence #34 ... exit code: 0The single-broker limitation, stated honestly: the harness’s own broker (tools/probe/docker-compose.yml) is one combined broker+controller KRaft node on host port 19092 — every // @run fence in this catalog uses replicationFactor: 1 for exactly that reason. Entries whose real proof genuinely needs multiple brokers acting independently (this module’s own KIP-579 and partition-remap entries used a separate, temporary 3-broker kafka-reading-a cluster on ports 39092–39094, torn down after capturing the output above) cannot be re-run against the harness broker as-is — their PROOF sections are real output from that other, already-torn-down cluster, not from a fence this harness will ever re-execute. The harness’s --run mode only ever proves what a single broker can prove.
tsc --noEmit above ran from the command line against a saved file; here it runs live, in your browser, against KafkaJS’s real types. Check the clean consumer call first, then the broken one for the exact diagnostic a wrong argument shape produces.
A clean consumer.subscribe()/run() call
import { Kafka } from 'kafkajs';
const kafka = new Kafka({ clientId: 'orders-consumer', brokers: ['localhost:9092'] });const consumer = kafka.consumer({ groupId: 'orders-group' });
async function main() { await consumer.connect(); await consumer.subscribe({ topics: ['orders'], fromBeginning: false }); await consumer.run({ eachMessage: async ({ topic, partition, message }) => { console.log(`${topic}[${partition}] offset ${message.offset}: ${message.value?.toString()}`); }, });}
main(); Same call, one bug: topics is a string instead of an array
import { Kafka } from 'kafkajs';
const kafka = new Kafka({ clientId: 'orders-consumer', brokers: ['localhost:9092'] });const consumer = kafka.consumer({ groupId: 'orders-group' });
async function main() { await consumer.connect(); // bug: topics must be an array of strings/RegExp, not a bare string await consumer.subscribe({ topics: 'orders', fromBeginning: false }); await consumer.run({ eachMessage: async ({ topic, partition, message }) => { console.log(`${topic}[${partition}] offset ${message.offset}: ${message.value?.toString()}`); }, });}
main(); Mental Model
Section titled “Mental Model”flowchart TD
subgraph Static["Static (no broker needed)"]
Tsc["tsc --noEmit<br/>types only, never durability/ordering"]
Props["harness --props<br/>hallucinated config keys"]
end
subgraph Broker["Needs a real broker"]
Configs["kafka-configs.sh --describe --all<br/>does the key exist, where did it come from"]
Topics["kafka-topics.sh --describe /<br/>--under-min-isr-partitions"]
Groups["kafka-consumer-groups.sh /<br/>kafka-share-groups.sh"]
DumpLog["kafka-dump-log.sh<br/>real bytes on disk"]
Features["kafka-features.sh describe<br/>which KIPs are finalized"]
Acls["kafka-acls.sh<br/>is an authorizer even configured"]
Harness["harness --run / --cli<br/>this module's own proofs"]
end
subgraph Worker["Needs a real Connect worker"]
Plugins["GET /connector-plugins<br/>what can this worker actually load"]
Validate["PUT /.../config/validate<br/>errors.tolerance's real default"]
end
subgraph OutOfBudget["Out of THIS module's docker budget"]
SR["Schema Registry /compatibility<br/>proven in streams-connect-schema instead"]
end