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

  1. Confirm the endpoint is a real OpenAI/SSE streaming endpoint and that nothing in between mutates the stream.
  2. If the upstream sends legitimate SSE comments or alternate prefixes, pre-filter lines in your own SSE reader before passing to this parser.
  3. Check for 'data:' (no space) vs 'data: ' mismatch - normalize upstream to the standard prefix.
  4. 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

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

Related errors


AI-assisted analysis of janhq/jan@fad3f12a14 (2026-08-12). Data as JSON: /api/errors/8b5edfe9f25bde4e. Report an issue: GitHub.