can1357/oh-my-pi · error

JSONL stream ended unexpectedly

Error message

JSONL stream ended unexpectedly

What it means

readJsonl buffers the stream and parses newline-delimited JSON incrementally. After the stream ends, any remaining buffered tail is flushed through Bun.JSONL.parseChunk; if the tail is non-empty but parseChunk reports not-done (an incomplete final record — e.g. a line cut off mid-JSON), the stream ended in the middle of a record and this error is thrown.

Source

Thrown at packages/utils/src/stream.ts:48

export async function* readJsonl<T>(stream: ReadableStream<Uint8Array>, signal?: AbortSignal): AsyncGenerator<T> {
	const buffer = new ConcatSink();
	const source = abortableSource(stream, signal);
	try {
		for await (const chunk of source) {
			yield* buffer.pullJSONL<T>(chunk, 0, chunk.length);
		}
		if (!buffer.isEmpty) {
			const tail = buffer.flush();
			if (tail) {
				buffer.clear();
				const { values, error, done } = Bun.JSONL.parseChunk(tail, 0, tail.length);
				if (values.length > 0) {
					yield* values as T[];
				}
				if (error) throw error;
				if (!done) {
					throw new Error("JSONL stream ended unexpectedly");
				}
			}
		}
	} catch (err) {
		// Abort errors are expected — just stop the generator.
		if (signal?.aborted) return;
		throw err;
	}
}

// =============================================================================
// SSE (Server-Sent Events)
// =============================================================================

class ConcatSink {
	#space?: Buffer;
	#length = 0;

View on GitHub (pinned to 9690622007)

Solutions

  1. Fix the producer to terminate each JSON record with a newline before closing the stream.
  2. Check for network/proxy truncation (timeouts, content-length mismatches) upstream.
  3. Use the signal parameter to abort cleanly, which is swallowed instead of thrown.
  4. Catch the error and treat already-yielded values as a partial (best-effort) result.

Example fix

// before
for await (const v of readJsonl(stream)) use(v);
// after
let partial = [];
try {
  for await (const v of readJsonl(stream, signal)) use(v);
} catch (err) {
  flushPartialResults(partial, err);
}
Defensive patterns

Strategy: try-catch

Try / catch

const results = [];
try {
  for await (const v of readJsonl<T>(stream, signal)) results.push(v);
} catch (err) {
  if (String(err.message).includes('JSONL stream ended unexpectedly')) {
    logger.warn('jsonl truncated; using partial results', { count: results.length });
  } else throw err;
}

Prevention

When it happens

Trigger: A JSONL producer closes the stream after a partial final line — truncated HTTP body, process killed mid-write, network drop without abort, or a server that doesn't end with a newline and sends malformed trailing bytes.

Common situations: Streaming Ollama/LLM responses where the connection drops mid-token; reading logs from a crashed process; premature response termination from proxies with body size limits.

Related errors


AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31). Data as JSON: /api/errors/572423e0519c2ab1. Report an issue: GitHub.