janhq/jan · error
Failed to parse request body as JSON
Error message
Failed to parse request body as JSON: ${e instanceof Error ? e.message : String(e)} What it means
Thrown inside createCustomFetch's wrapped fetch when a POST request body cannot be JSON.parse'd before it is rebuilt via buildBody. The wrapper needs to parse, transform, and re-serialize the body, so a non-JSON body (form data, binary, malformed JSON) is a hard failure with the underlying parse error appended.
Solutions
- Ensure POST bodies are plain JSON-serializable objects passed through JSON.stringify before calling fetch.
- Skip the JSON path for uploads: set init.method explicitly and only pass bodies your custom fetch can parse, or use a fetch wrapper that bypasses this transform.
- Log init.body (and its typeof) when the error occurs to find which call site sends non-JSON bodies.
Example fix
// before
fetch(url, { method: 'POST', body: formData })
// after
fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name }) }) Defensive patterns
Strategy: validation
Validate before calling
if (typeof body !== 'string' || body.trim()[0] !== '{') throw new TypeError('POST body must be a JSON string')
JSON.parse(body) // dry-run parse before calling fetch Type guard
const isJsonString = (b) => typeof b === 'string' && (() => { try { JSON.parse(b); return true } catch { return false } })() Try / catch
try { await wrappedFetch(url, init) } catch (e) { if (String(e.message).startsWith('Failed to parse request body as JSON')) { console.error('body was:', typeof init.body, init.body); } else throw e } Prevention
- Always build bodies with JSON.stringify of a plain object.
- Never pass FormData/Blob through a custom fetch that parses the body.
- Unit-test custom fetch wrappers with the exact body shapes you send.
When it happens
Trigger: Calling the wrapped fetch with init.body set to a FormData, Blob, URLSearchParams, or ReadableStream object; passing a hand-built string that is not valid JSON; encoding bugs that emit truncated JSON.
Common situations: Swapping in a custom fetch for file uploads that send multipart bodies; injecting a raw string body with typos; middleware ordering where another wrapper already replaced the body with a stream.
Understand the failure class
Background: JSON parse error: "Unexpected token" / "not valid JSON" / "failed to parse" — what JSON parsers are really complaining about — this error's family across 45 libraries.
- Parsing and encoding errors: unexpected token, malformed input — why parsers reject input and how to find the real culprit.
Related errors
- Failed to fetch model catalog
- Cannot connect to at . Please check that the service is…
- Failed to fetch file
- Failed to parse update response
- Failed to serialize MCP settings
AI-assisted analysis of janhq/jan@7205d770c1 (2026-09-17).
Data as JSON: /api/errors/c68dc2f63a640735.
Report an issue: GitHub.
Appendix: source
Thrown at web-app/src/lib/model-factory.ts:525
// set max_output_tokens = 0 in assistant params mean "no cap", not
// "produce zero tokens" — coerce here, gated to llamacpp only because
// OpenAI/Anthropic reject negative values.
if (keepLlamacppOnly && merged.max_tokens === 0) {
merged.max_tokens = -1
}
decodeAudioSentinelsInBody(merged)
decodeVideoSentinelsInBody(merged)
decodeToolImageSentinelsInBody(merged)
return merged
}
return async (input: RequestInfo | URL, init?: RequestInit): Promise<Response> => {
let rawBody: Record<string, unknown> | null = null
if (init?.method === 'POST' || !init?.method) {
try {
rawBody = init?.body ? JSON.parse(init.body as string) : {}
} catch (e) {
throw new Error(
`Failed to parse request body as JSON: ${e instanceof Error ? e.message : String(e)}`
)
}
init = { ...init, body: JSON.stringify(buildBody(rawBody!, true)) }
}
let res: Response
try {
res = await baseFetch(input, init)
} catch (err) {
const friendly = describeTransportError(err)
if (!friendly) throw err
throw new Error(`${friendly} (${requestUrlOf(input)})`)
}
if (res.ok) {
// OpenAI-compatible servers may interleave custom named SSE events (e.g.
// tool-progress) with chat.completion.chunk data; the AI SDK validates
// every data line against the chunk schema, so strip non-default events.View on GitHub (pinned to 7205d770c1)