{"record":{"id":"572423e0519c2ab1","repo":"can1357/oh-my-pi","slug":"jsonl-stream-ended-unexpectedly","errorCode":null,"errorMessage":"JSONL stream ended unexpectedly","messagePattern":"JSONL stream ended unexpectedly","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/utils/src/stream.ts","lineNumber":48,"sourceCode":"\nexport async function* readJsonl<T>(stream: ReadableStream<Uint8Array>, signal?: AbortSignal): AsyncGenerator<T> {\n\tconst buffer = new ConcatSink();\n\tconst source = abortableSource(stream, signal);\n\ttry {\n\t\tfor await (const chunk of source) {\n\t\t\tyield* buffer.pullJSONL<T>(chunk, 0, chunk.length);\n\t\t}\n\t\tif (!buffer.isEmpty) {\n\t\t\tconst tail = buffer.flush();\n\t\t\tif (tail) {\n\t\t\t\tbuffer.clear();\n\t\t\t\tconst { values, error, done } = Bun.JSONL.parseChunk(tail, 0, tail.length);\n\t\t\t\tif (values.length > 0) {\n\t\t\t\t\tyield* values as T[];\n\t\t\t\t}\n\t\t\t\tif (error) throw error;\n\t\t\t\tif (!done) {\n\t\t\t\t\tthrow new Error(\"JSONL stream ended unexpectedly\");\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t} catch (err) {\n\t\t// Abort errors are expected — just stop the generator.\n\t\tif (signal?.aborted) return;\n\t\tthrow err;\n\t}\n}\n\n// =============================================================================\n// SSE (Server-Sent Events)\n// =============================================================================\n\nclass ConcatSink {\n\t#space?: Buffer;\n\t#length = 0;\n","sourceCodeStart":30,"sourceCodeEnd":66,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/utils/src/stream.ts#L30-L66","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Fix the producer to terminate each JSON record with a newline before closing the stream.","Check for network/proxy truncation (timeouts, content-length mismatches) upstream.","Use the signal parameter to abort cleanly, which is swallowed instead of thrown.","Catch the error and treat already-yielded values as a partial (best-effort) result."],"exampleFix":"// before\nfor await (const v of readJsonl(stream)) use(v);\n// after\nlet partial = [];\ntry {\n  for await (const v of readJsonl(stream, signal)) use(v);\n} catch (err) {\n  flushPartialResults(partial, err);\n}","handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"const results = [];\ntry {\n  for await (const v of readJsonl<T>(stream, signal)) results.push(v);\n} catch (err) {\n  if (String(err.message).includes('JSONL stream ended unexpectedly')) {\n    logger.warn('jsonl truncated; using partial results', { count: results.length });\n  } else throw err;\n}","preventionTips":["Ensure producers newline-terminate every JSON record before closing.","Always pass an AbortSignal so disconnects are treated as clean stops.","Monitor upstream proxies/CDNs for body truncation on long streams."],"tags":["jsonl","streaming","truncated-data"],"backgroundTag":"truncated-stream","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}