janhq/jan · error · Error
Malformed chunk
Error message
Malformed chunk
What it means
Thrown by the SSE line parser when a non-empty trimmed line is neither 'data: [DONE]', does not start with 'data: ', nor with 'error: '. The OpenAI-compatible SSE protocol the router speaks only emits those line shapes; anything else means the upstream is emitting a non-conformant event (or the line splitter got out of sync). The parser deliberately fails fast rather than silently dropping chunks.
Source
Thrown at extensions/llamacpp-extension/src/index.ts:3700
// Process complete lines in the buffer
const lines = buffer.split('\n')
buffer = lines.pop() || '' // Keep the last incomplete line in the buffer
for (const line of lines) {
const trimmedLine = line.trim()
if (!trimmedLine || trimmedLine === 'data: [DONE]') {
continue
}
if (trimmedLine.startsWith('data: ')) {
jsonStr = trimmedLine.slice(6)
} else if (trimmedLine.startsWith('error: ')) {
jsonStr = trimmedLine.slice(7)
const error = JSON.parse(jsonStr)
throw new Error(error.message)
} else {
// it should not normally reach here
throw new Error('Malformed chunk')
}
try {
const data = JSON.parse(jsonStr)
const chunk = data as chatCompletionChunk
yield chunk
} catch (e) {
logger.error('Error parsing JSON from stream or server error:', e)
// re‑throw so the async iterator terminates with an error
throw e
}
}
}
} finally {
reader.releaseLock()
}
}
View on GitHub (pinned to fad3f12a14)
Solutions
- Confirm the endpoint is a real OpenAI/SSE streaming endpoint and that nothing in between mutates the stream.
- If the upstream sends legitimate SSE comments or alternate prefixes, pre-filter lines in your own SSE reader before passing to this parser.
- Check for 'data:' (no space) vs 'data: ' mismatch - normalize upstream to the standard prefix.
- Disable proxy buffering/rewriting and any injected heartbeat on the streaming route.
Example fix
// before - upstream sends ': keepalive' comments -> Malformed chunk
for await (const c of await provider.chat(opts, ac)) { /* ... */ }
// after - the parser is strict; ensure the route delivers only data:/error:/[DONE]
// In nginx: proxy_buffering off; proxy_cache off; add_header X-Accel-Buffering no;
// If you cannot change upstream, wrap with a sanitizing transform:
async function* sanitize(iter) {
for await (const c of iter) { /* re-emit only well-formed chunks */ yield c }
} Defensive patterns
Strategy: validation
Validate before calling
// If you control the upstream, sanitize emitted lines to data:/error:/[DONE] only. // As a caller you cannot pre-validate; the guard is at the transport layer (proxy config). // Document the SSE contract for your upstream: // allowed line prefixes: 'data: ', 'error: ', and the literal 'data: [DONE]'.
Type guard
function isAllowedSseLine(line: string): boolean {
const t = line.trim()
return !t || t === 'data: [DONE]' || t.startsWith('data: ') || t.startsWith('error: ')
} Try / catch
try { for await (const c of await provider.chat(opts, ac)) emit(c) }
catch (e) {
if (/Malformed chunk/.test(String(e))) { /* disable streaming, retry non-streaming */ const r = await provider.chat({ ...opts, stream: false }, ac); emit(r) }
else throw e
} Prevention
- Ensure the SSE route passes through untouched: no buffering, no rewriting, no injected heartbeats.
- If your upstream emits comments or alternate prefixes, normalize them upstream of this parser.
- Talk only to OpenAI-compatible endpoints whose SSE dialect matches data:/error:/[DONE].
When it happens
Trigger: Router/upstream emits a comment line ': ping' (SSE comment) or an event field without a prefix. A keep-alive newline/byte corrupted the framing. A proxy rewrote the stream (added HTML, changed prefixes). Wrong endpoint hit (non-SSE JSON returned line-by-line). Buffer split left a stray token after a partial line. A 'data:' (no space) or 'data: ' variant that does not match the exact prefix.
Common situations: CDN/proxy injecting heartbeat comments. Misconfigured reverse proxy not passing SSE through untouched. Talking to an OpenAI-compatible server with a slightly different SSE dialect (e.g. 'data:' with no trailing space, or 'event: ...'). A debug log line leaked into the response stream.
Understand the failure class
- Parsing and encoding errors: unexpected token, malformed input — why parsers reject input and how to find the real culprit.
Related errors
- Response body is null
- ${error.message}
- API request failed with status ${response.status}: ${JSON.st
- No active MLX session found for model: ${opts.model}
- Invalid backend string: ${targetBackendString} supplied to u
AI-assisted analysis of janhq/jan@fad3f12a14 (2026-08-12).
Data as JSON: /api/errors/8b5edfe9f25bde4e.
Report an issue: GitHub.