badges/shields · error · InvalidResponse

unparseable json response

Error message

unparseable json response

What it means

parseJson parses an upstream response buffer with JSON.parse. When the body is not valid JSON, Shields throws InvalidResponse with prettyMessage 'unparseable json response'. This distinguishes 'provider responded but sent garbage/HTML' from 'provider unreachable', typically an error page, login HTML, or truncated body.

Source

Thrown at core/base-service/json.js:20

import emojic from 'emojic'
import { InvalidResponse } from './errors.js'
import trace from './trace.js'

/**
 * Parse a JSON response buffer. Throws an `InvalidResponse` error when the
 * JSON is unparseable.
 *
 * @param {string|Buffer} buffer - The raw response body.
 * @returns {object|Array} The parsed JSON value.
 */
function parseJson(buffer) {
  const logTrace = (...args) => trace.logTrace('fetch', ...args)
  let json
  try {
    json = JSON.parse(buffer)
  } catch (err) {
    logTrace(emojic.dart, 'Response JSON (unparseable)', buffer)
    throw new InvalidResponse({
      prettyMessage: 'unparseable json response',
      underlyingError: err,
    })
  }
  logTrace(emojic.dart, 'Response JSON (before validation)', json, {
    deep: true,
  })
  return json
}

export { parseJson }

View on GitHub (pinned to 766fd8bc89)

Solutions

  1. Log/inspect the raw response body (Shields traces it via logTrace as 'Response JSON (unparseable)').
  2. Verify API credentials — an expired token often yields HTML error pages.
  3. Check the upstream URL and any API version in the path.
  4. Confirm the endpoint actually returns application/json (curl with -i).
  5. Add caching/rate limiting if the provider is throttling you into error pages.

Example fix

// before
const res = await fetch(url) // returns HTML login page
const json = await res.json() // InvalidResponse: unparseable json response
// after
const text = await res.text()
if (!text.trim().startsWith('{') && !text.trim().startsWith('[')) throw new Error('non-JSON response: ' + text.slice(0, 100))
const json = JSON.parse(text)
Defensive patterns

Strategy: validation

Validate before calling

const looksLikeJson = text => { const t = text.trimStart(); return t.startsWith('{') || t.startsWith('[') }
if (!looksLikeJson(buffer)) throw new Error('upstream returned non-JSON body')

Type guard

function isJsonObject(v) { return typeof v === 'object' && v !== null && !Array.isArray(v) }

Try / catch

try {
  const json = JSON.parse(buffer)
} catch (err) {
  console.error('non-JSON body (first 200 chars):', String(buffer).slice(0, 200))
  return fallbackBadge
}

Prevention

When it happens

Trigger: JSON.parse(buffer) throws inside parseJson — upstream returned HTML (login/error page), empty body, gzip/charset mangling, or a JSONL/truncated response where a JSON schema was expected.

Common situations: Provider rate-limits with an HTML error page, auth token expired causing a redirect to a login page, CDN/proxy injecting interstitials, upstream API version change altering content-type.

Related errors


AI-assisted analysis of badges/shields@766fd8bc89 (2026-08-30). Data as JSON: /api/errors/b2b9998828b7e593. Report an issue: GitHub.