4 min readUpdated 2026-10-10

Debug API JSON Responses: HTML Errors, Empty Bodies and Wrong Types

Debug JSON parsing failures with repeatable examples for HTML error responses, 204 empty bodies, string-versus-object payloads and valid JSON that violates an API contract.

Maintained by Ali Aygör. Report a correction.

First separate the response from the parser

An API debugger should establish what the browser received before changing the JSON parser. Record the HTTP status, Content-Type, request identifier and a redacted body excerpt. A request can reach the server successfully while returning an application error; conversely, a network failure may leave you without a readable response at all.

Use the Network panel to identify the failing request. A login redirect, reverse-proxy error or wrong route can produce HTML where the client expects JSON. Formatting that HTML as JSON cannot repair the route or restore authentication. Compare a known successful request with the failure, including method, path and environment.

Expected: 200, application/json, {"ok":true}
Wrong route: 404, text/html, <!doctype html>...
Upstream failure: 502, text/html, <html>...
Application rejection: 422, application/json, {"error":"invalid_order"}

Reproduce parsing separately from HTTP success

Read the body once. In the controlled example below, text() preserves the representation for inspection before JSON.parse converts it. The examples are synthetic; they do not send a request or expose a production response. Response.json() also parses the body, so do not call both readers on the same response unless you deliberately clone it.

Treat an empty body and an HTTP error as explicit cases. A 204 response has no JSON value to parse. An error response can contain perfectly valid JSON, so parsing success must not be mistaken for a successful operation. Check the documented content type and API response contract; the header alone cannot prove that the body is valid.

async function inspectResponse(response) {
  const raw = await response.text();
  if (response.status === 204) return { status: 204, value: null };
  if (!raw.trim()) throw new Error("Unexpected empty response");
  let value;
  try { value = JSON.parse(raw); }
  catch { throw new Error(`HTTP ${response.status}: body is not JSON`); }
  return { status: response.status, httpOk: response.ok, value };
}

console.log(await inspectResponse(new Response(
  '{"error":"invalid_order"}', { status: 422 }
))); // httpOk: false, parsed error object
console.log(await inspectResponse(new Response(null, { status: 204 })));
// value: null; no parsing attempt

Check the parsed type before reading properties

JSON can represent a string, number, boolean, null, array or object. An object-shaped display inside a quoted string is still a string after the outer layer is parsed. An API contract expecting an object should reject null, an array or a string even though those representations are valid JSON.

If the producer serializes an object twice, establish where that extra layer was introduced. Parse a second time only when the documented field representation requires it. Repeatedly decoding until something looks useful can hide a producer bug or change intentional string content. The separate double-encoded JSON guide covers that narrower failure.

const raw = JSON.stringify(JSON.stringify({ ok: true }));
const value = JSON.parse(raw);
console.log(typeof value); // string
const isObject = value !== null && typeof value === "object"
  && !Array.isArray(value);
console.log(isObject); // false
console.log(JSON.parse(value).ok); // true; extra layer demonstrated

Validate syntax, then validate the contract

Paste a redacted response into JSON Validator to check syntax, then use JSON Schema Validator for field constraints. In the fixture below both bodies parse, but only the numeric count satisfies an integer contract. Formatter output is easier to read; it does not establish that a value is permitted by the application.

Keep a passing fixture and one failing variant for each rule you investigate. Change one property at a time so the observed rejection has a clear cause. Include missing fields, null values and unexpected properties when the contract distinguishes them. Business authorization and state transitions remain application checks.

{"count":1}
{"count":"1"}

Contract:
{"type":"object","required":["count"],
 "properties":{"count":{"type":"integer","minimum":0}},
 "additionalProperties":false}

Expected: numeric count passes; string count fails.

Decoding is not decryption

A search for JSON decode can mean parsing JSON text, decoding an encoded field or inspecting a serialized string. Identify the producer's representation before selecting a tool. Base64 and URL encoding change representation; they do not provide encryption. An opaque API key cannot be turned into a meaningful account record merely by choosing a decoder.

Never paste an active API key to test whether it decodes. Use a synthetic example and the issuing service's documentation. When a value is encrypted, this site's formatters and decoders cannot recover plaintext without the required cryptographic context. A successful Base64 decode also does not prove that Base64 was intended.

Turn the observation into a useful incident note

Record the request method and route, UTC time, HTTP status, representation, expected parsed type and smallest failing fixture. Link the same request identifier to gateway or application logs when available. Remove tokens, cookies, internal hostnames and customer values from shared output.

After changing the producer or client, rerun the passing and failing fixtures. Confirm both the HTTP outcome and the parsed shape. This prevents a parser workaround from masking the original failure while quietly accepting invalid responses.

Specifications and references

Continue the investigation

Related tools

Browse all developer tools

Related workflows