grafana/k6 · error

unmarshalling response body to JSON: %w

Error message

unmarshalling response body to JSON: %w

What it means

Response.JSON() fetches the body successfully but the bytes are not valid JSON: encoding/json's Unmarshal fails on the raw body. The response was retrieved fine; its content is simply not parseable JSON (HTML error page, plain text, empty or truncated payload, BOM-prefixed body).

Source

Thrown at internal/js/modules/k6/browser/common/http.go:770

	r.extraHeadersMu.RUnlock()
	return headers
}

// JSON returns the response body as JSON data.
func (r *Response) JSON() (any, error) {
	if r.cachedJSON != nil {
		return r.cachedJSON, nil
	}
	if err := r.fetchBody(); err != nil {
		return nil, fmt.Errorf("getting response body: %w", err)
	}

	r.bodyMu.RLock()
	defer r.bodyMu.RUnlock()

	var v any
	if err := json.Unmarshal(r.body, &v); err != nil {
		return nil, fmt.Errorf("unmarshalling response body to JSON: %w", err)
	}
	r.cachedJSON = v

	return v, nil
}

// Ok returns true if status code of response if considered ok, otherwise returns false.
func (r *Response) Ok() bool {
	if r.status == 0 || (r.status >= 200 && r.status <= 299) {
		return true
	}
	return false
}

// Request returns the request that led to this response.
func (r *Response) Request() *Request {
	return r.request
}

View on GitHub (pinned to 93accf6570)

Solutions

  1. Check res.status() and the response content-type header before calling res.json()
  2. For debugging, call res.text() and log the first ~200 bytes to see what actually came back
  3. Fix the upstream cause: re-authenticate, fix the URL, or handle the non-JSON branch explicitly
  4. If you must tolerate junk, wrap JSON.parse(res.text()) in your own try/catch instead of res.json()

Example fix

// before
const res = await page.goto('https://api.example.com/me');
const me = await res.json(); // throws on HTML login page

// after
const res = await page.goto('https://api.example.com/me');
const ct = (res.headers()['content-type'] || '');
if (!ct.includes('application/json')) {
  throw new Error('expected JSON, got: ' + ct);
}
const me = await res.json();
Defensive patterns

Strategy: validation

Validate before calling

const ct = res.headers()['content-type'] || '';
if (!ct.includes('application/json')) {
  throw new Error(`non-JSON response (${ct}): ${(await res.text()).slice(0, 120)}`);
}
const data = await res.json();

Try / catch

try { data = await res.json(); }
catch (e) { if (!/unmarshalling response body to JSON/.test(String(e.message))) throw e; /* handle non-JSON */ }

Prevention

When it happens

Trigger: Calling res.json() on a response whose body is an HTML login/error page, plain text, empty, or truncated; endpoints returning JSON with a BOM or trailing junk; receiving a 200 page from a captive portal or WAF instead of the API.

Common situations: API returns HTML after session expiry or when unauthenticated; 500 error pages with content-type text/html; asserting content-type mismatches; proxies injecting banners into responses.

Related errors


AI-assisted analysis of grafana/k6@93accf6570 (2026-08-15). Data as JSON: /api/errors/ba1a9f4dfce9a6d0. Report an issue: GitHub.