charmbracelet/glow · error

unable to parse json: %w

Error message

unable to parse json: %w

What it means

The API body is unmarshalled with json.Unmarshal into a small struct with only download_url. This error means the bytes received were not valid JSON — glow does not check Content-Type or status before parsing, so any non-JSON 200-class or error page that reaches this point fails here. Note the status check happens after parsing, so even a 404 JSON body would parse fine and only fail later.

Source

Thrown at github.go:40

	}

	apiURL := fmt.Sprintf("https://api.%s/repos/%s/%s/readme", u.Hostname(), owner, repo)

	//nolint:bodyclose
	// it is closed on the caller
	res, err := http.Get(apiURL) //nolint: gosec,noctx
	if err != nil {
		return nil, fmt.Errorf("unable to get url: %w", err)
	}

	body, err := io.ReadAll(res.Body)
	if err != nil {
		return nil, fmt.Errorf("unable to read http response body: %w", err)
	}

	var result readme
	if err := json.Unmarshal(body, &result); err != nil {
		return nil, fmt.Errorf("unable to parse json: %w", err)
	}

	if res.StatusCode == http.StatusOK {
		//nolint:bodyclose
		// it is closed on the caller
		resp, err := http.Get(result.DownloadURL) //nolint: noctx
		if err != nil {
			return nil, fmt.Errorf("unable to get url: %w", err)
		}

		if resp.StatusCode == http.StatusOK {
			return &source{resp.Body, result.DownloadURL}, nil
		}
	}

	return nil, errors.New("can't find README in GitHub repository")
}

View on GitHub (pinned to e3970c813d)

Solutions

  1. Inspect what is actually returned: curl -s https://api.github.com/repos/OWNER/REPO/readme | head -c 200
  2. Remove/fix hosts-file or DNS overrides for api.github.com
  3. Accept the network's portal/proxy terms or route around the interception
  4. Use the raw.githubusercontent.com URL directly to sidestep the API response

Example fix

# before (hosts file maps api.github.com to a local stub serving HTML)
glow https://github.com/owner/repo
# Error: unable to parse json: invalid character '<' looking for beginning of value

# after
$ sudo sed -i '/api\.github\.com/d' /etc/hosts && glow https://github.com/owner/repo
Defensive patterns

Strategy: try-catch

Validate before calling

// sniff the body before JSON parsing: only parse responses that look like JSON
if len(body) == 0 || (body[0] != '{' && body[0] != '[') {
	return nil, fmt.Errorf("non-JSON response (first bytes: %.40q) — captive portal or proxy interception?", string(body))
}

Try / catch

var result readme
if err := json.Unmarshal(body, &result); err != nil {
	var synErr *json.SyntaxError
	if errors.As(err, &synErr) {
		// capture evidence for diagnosis instead of a bare 'parse' failure
		snippet := string(body)
		if len(snippet) > 120 {
			snippet = snippet[:120]
		}
		return nil, fmt.Errorf("unable to parse json at offset %d (body starts with: %s): %w", synErr.Offset, snippet, err)
	}
	var typeErr *json.UnmarshalTypeError
	if errors.As(err, &typeErr) {
		return nil, fmt.Errorf("API schema changed: field %s: %w", typeErr.Field, err)
	}
	return nil, fmt.Errorf("unable to parse json: %w", err)
}

Prevention

When it happens

Trigger: A captive portal or transparent proxy returning an HTML login/interception page; DNS or hosts-file entries sending api.{hostname} to a server that answers 200 with HTML; GitLab/GitHub mirrors or gateways that return plain-text errors; truncated bodies from intermediary truncation that still exit ReadAll cleanly.

Common situations: Hotel/airport Wi-Fi captive portals; corporate SSL-inspection appliances rewriting responses; custom hosts entries like 127.0.0.1 api.github.com during testing; GitHub Enterprise hosts routed into this function whose API prefix differs.

Understand the failure class

Related errors


AI-assisted analysis of charmbracelet/glow@e3970c813d (2026-08-15). Data as JSON: /api/errors/88b3293d184ceaa0. Report an issue: GitHub.