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

  1. Ensure POST bodies are plain JSON-serializable objects passed through JSON.stringify before calling fetch.
  2. 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.
  3. 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

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.

Related errors


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)