Skip to content

Docker event listener fails to parse large JSON events (chunked stream splitting) #3351

Description

Code of Conduct

  • I agree to follow this project's Code of Conduct

On what operating system are you seeing the problem?

macOS (Apple/arm64)

VS Code version

Description:

Extension version: Latest (as of 2026-03-27)
VS Code version: 1.113.0 (macOS, Apple Silicon)
Docker Desktop version: Latest

Summary
The EventListener.readValuesFromStream() method in src/docker/eventListener.ts does not buffer partial reads from the Docker event stream, causing JSON parse failures when a single event payload exceeds the ReadableStream chunk size (~2KB). This prevents the extension from detecting running Kafka containers.

These come in pairs: the first chunk is an unterminated JSON string, and the second chunk is the orphaned tail — neither is valid JSON on its own.

Expected Behavior
The extension should correctly detect the running confluentinc/confluent-local container regardless of how many Docker/OCI labels are present on the image.

Root Cause
Docker's /events endpoint returns newline-delimited JSON (NDJSON). Each event is a complete JSON object followed by \n. However, ReadableStream.getReader().read() returns data at arbitrary byte boundaries determined by TCP/HTTP chunking — not aligned to newline boundaries.

The current implementation in readValuesFromStream() decodes each chunk, splits on \n, and yields every fragment immediately:

When an event payload is larger than the read buffer (~2KB), it gets split across two read() calls. The tail of chunk N and the head of chunk N+1 are each yielded as separate strings. JSON.parse() then fails on both.

Containers with many labels (e.g., confluentinc/confluent-local which includes Confluent, Red Hat UBI, OCI, and Docker Compose labels) reliably produce events exceeding 2KB, triggering this bug consistently.

Proposed Fix
Buffer partial lines across reads and only yield complete lines (those terminated by \n):

Key changes:

A buffer string carries over incomplete data between reads
After split("\n"), lines.pop() retains the last element (which may lack a trailing \n and therefore be incomplete)
Only complete lines — those followed by a \n delimiter — are yielded
On stream end (done === true), any remaining buffer content is flushed
This is the standard pattern for consuming NDJSON over a ReadableStream. The fix is backwards-compatible — it changes no public API and the handleEvent() caller receives the same strings, just correctly reassembled.

Workaround
As a temporary workaround, users can configure the extension to match their container's exact image tag to ensure the ancestor filter works, and restart the container after VS Code launches. However, this does not fix the parse errors — it just increases the chance of the start event being small enough to fit in a single chunk.

Version of Confluent extension

v2.2.2

To Reproduce

Run a confluentinc/confluent-local:latest container via Docker Compose with many labels (Compose metadata + OCI image labels combine to produce events > 2KB)
Open VS Code with the Confluent extension
The extension fails to detect the container — no local Kafka cluster appears in the sidebar

Current vs. Expected behavior

Observed Behavior
The Output panel (Confluent channel) shows repeated errors like:

These come in pairs: the first chunk is an unterminated JSON string, and the second chunk is the orphaned tail — neither is valid JSON on its own.

Expected Behavior
The extension should correctly detect the running confluentinc/confluent-local container regardless of how many Docker/OCI labels are present on the image.

Relevant log output

2026-03-27 20:09:22.391 [error] [docker.eventListener] error parsing event {"error":"Unexpected token 'i', \"images.\",\"\"... is not valid JSON","eventString":"images.\",\"desktop.docker.io/ports.scheme\":\"v2\",\"desktop.docker.io/ports/9092/tcp\":\":9092\",\"distribution-scope\":\"public\",\"execID\":\"b5e209326ca1a15ed7205b88c5c9bdef41f59433ba23c27f2752fb030a049b51\",\"image\":\"confluentinc/confluent-local:latest\",\"io.buildah.version\":\"1.42.2\",\"io.confluent.docker\":\"true\",\"io.confluent.docker.build.number\":\"f2ed3370\",\"io.confluent.docker.git.id\":\"7113046\",\"io.confluent.docker.git.repo\":\"confluentinc/kafka-images\",\"io.k8s.description\":\"The Universal Base Image Minimal is a stripped down image that uses microdnf as a package manager. This base image is freely redistributable, but Red Hat only supports Red Hat technologies through subscriptions for Red Hat products. This image is maintained by Red Hat and updated regularly.\",\"io.k8s.display-name\":\"Red Hat Universal Base Image 9 Minimal\",\"io.openshift.expose-services\":\"\",\"io.openshift.tags\":\"minimal rhel9\",\"maintainer\":\"partner-support@confluent.io\",\"name\":\"kafka\",\"org.opencontainers.image.created\":\"2026-02-17T16:45:31Z\",\"org.opencontainers.image.revision\":\"0ced2bbee24d5463d4530756a57f8db895246c48\",\"release\":\"8.2.0-68\",\"summary\":\"Kafka with Rest Proxy\",\"url\":\"https://catalog.redhat.com/en/search?searchType=containers\",\"vcs-ref\":\"0ced2bbee24d5463d4530756a57f8db895246c48\",\"vcs-type\":\"git\",\"vendor\":\"Confluent\",\"version\":\"7113046\"},\"ID\":\"21e248a847653e31cf0fa7cb9232e2d66d9b6ed49cb6d67b5a80283fc7b0eb7c\"},\"Type\":\"container\",\"from\":\"confluentinc/confluent-local:latest\",\"id\":\"21e248a847653e31cf0fa7cb9232e2d66d9b6ed49cb6d67b5a80283fc7b0eb7c\",\"scope\":\"local\",\"status\":\"exec_start: /bin/sh -c kafka-topics --bootstrap-server localhost:9092 --list\",\"time\":1774638562,\"timeNano\":1774638562385722000}"}

Which area(s) are affected? (Select all that apply)

Local

Additional context

Root Cause
Docker's /events endpoint returns newline-delimited JSON (NDJSON). Each event is a complete JSON object followed by \n. However, ReadableStream.getReader().read() returns data at arbitrary byte boundaries determined by TCP/HTTP chunking — not aligned to newline boundaries.

The current implementation in readValuesFromStream() decodes each chunk, splits on \n, and yields every fragment immediately:

When an event payload is larger than the read buffer (~2KB), it gets split across two read() calls. The tail of chunk N and the head of chunk N+1 are each yielded as separate strings. JSON.parse() then fails on both.

Containers with many labels (e.g., confluentinc/confluent-local which includes Confluent, Red Hat UBI, OCI, and Docker Compose labels) reliably produce events exceeding 2KB, triggering this bug consistently.

Proposed Fix
Buffer partial lines across reads and only yield complete lines (those terminated by \n):


async *readValuesFromStream(
  stream: ReadableStream<Uint8Array>,
  maxWaitTimeSec?: number,
): AsyncGenerator<string> {
  logger.debug("reading from stream...");
  const reader: ReadableStreamDefaultReader<Uint8Array> = stream.getReader();
  const decoder = new TextDecoder();
  const startTime = Date.now();
  let buffer = "";
  while (true) {
    const { done, value } = await reader.read();
    if (done) {
      // Flush any remaining buffered data
      const remaining = buffer.trim();
      if (remaining) {
        yield remaining;
      }
      logger.debug("stream ended");
      break;
    }
    if (this.stopped) {
      logger.warn("listener stopped, exiting early");
      break;
    }
    if (!value) {
      logger.debug("got empty value from stream");
      continue;
    }

    // Append decoded chunk to the carry-over buffer
    buffer += decoder.decode(value, { stream: true });

    // Split on newlines; the last element may be incomplete
    const lines = buffer.split("\n");
    buffer = lines.pop() ?? "";  // Keep the incomplete tail for the next read

    for (const line of lines) {
      const trimmed = line.trim();
      if (!trimmed) continue;
      yield trimmed;
    }

    if (
      maxWaitTimeSec !== undefined &&
      Date.now() - startTime > maxWaitTimeSec * 1000
    ) {
      logger.debug("max wait time reached, exiting stream reader");
      break;
    }
  }
}

Key changes:

A buffer string carries over incomplete data between reads
After split("\n"), lines.pop() retains the last element (which may lack a trailing \n and therefore be incomplete)
Only complete lines — those followed by a \n delimiter — are yielded
On stream end (done === true), any remaining buffer content is flushed
This is the standard pattern for consuming NDJSON over a ReadableStream. The fix is backwards-compatible — it changes no public API and the handleEvent() caller receives the same strings, just correctly reassembled.

Workaround
As a temporary workaround, users can configure the extension to match their container's exact image tag to ensure the ancestor filter works, and restart the container after VS Code launches. However, this does not fix the parse errors — it just increases the chance of the start event being small enough to fit in a single chunk.

Metadata

Metadata

Assignees

No one assigned

    Labels

    needs triageTeam needs to triage this issue

    Type

    Fields

    No fields configured for Bug.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions