jackwener/OpenCLI · error
Instagram unfollow returned invalid JSON
Error message
Instagram unfollow returned invalid JSON
What it means
Thrown in clis/instagram/unfollow.js when the response body from the friendships/destroy call cannot be parsed as JSON (r2.json() throws, caught by the try/catch). The unfollow flow requires structured confirmation, so an HTML error page, empty body, or non-JSON response is treated as a failure.
Source
Thrown at clis/instagram/unfollow.js:38
{ evaluate: `(async () => {
const username = \${{ args.username | json }};
const headers = { 'X-IG-App-ID': '936619743392459' };
const opts = { credentials: 'include', headers };
${buildResolveInstagramUserIdJs()}
const csrf = document.cookie.match(/csrftoken=([^;]+)/)?.[1] || '';
const r2 = await fetch('https://www.instagram.com/api/v1/friendships/destroy/' + userId + '/', {
method: 'POST',
credentials: 'include',
headers: { ...headers, 'X-CSRFToken': csrf, 'Content-Type': 'application/x-www-form-urlencoded' },
});
if (!r2.ok) throw new Error('Failed to unfollow: HTTP ' + r2.status);
let d2;
try {
d2 = await r2.json();
} catch {
throw new Error('Instagram unfollow returned invalid JSON');
}
if (!d2 || typeof d2 !== 'object' || d2.status !== 'ok') {
throw new Error('Instagram unfollow returned no success evidence');
}
return [{ status: 'Unfollowed', username }];
})()
` },
],
});
View on GitHub (pinned to 49907e53dc)
Solutions
- Capture res.text() in the catch to inspect what was actually returned (usually HTML = auth/bot challenge)
- Re-login to refresh the session so Instagram stops redirecting to the login page
- Handle any consent/challenge interstitial in the browser before retrying
- Check whether Instagram changed the endpoint response format and update parsing
Example fix
// before
catch {
throw new Error('Instagram unfollow returned invalid JSON');
}
// after
catch (e) {
const text = await r2.text().catch(() => '');
throw new Error('Unfollow response not JSON (HTTP ' + r2.status + '): ' + text.slice(0, 200));
} Defensive patterns
Strategy: type-guard
Validate before calling
const text = await r2.text();
let d2;
try { d2 = JSON.parse(text); } catch { throw new Error('Non-JSON response (login page or challenge?): ' + text.slice(0, 200)); } Type guard
function isUnfollowSuccess(d) {
return !!d && typeof d === 'object' && d.status === 'ok';
} Try / catch
try {
await unfollow(username);
} catch (e) {
if (/invalid JSON/.test(e.message)) {
await relogin(); // body was likely an HTML login/consent page
await unfollow(username);
} else throw e;
} Prevention
- Detect HTML bodies (text startsWith '<!DOCTYPE' or '<html') as an auth/challenge signal
- Refresh sessions before long batch unfollow runs
- Handle consent/bot-challenge interstitials in the browser before API calls
- Always validate d2.status === 'ok' even when JSON parses
When it happens
Trigger: The POST returned r2.ok but the body is not JSON — e.g. a login HTML redirect page, an empty 200 response, a Cloudflare challenge page, or Instagram returning plain-text errors.
Common situations: Session expired mid-flow so Instagram serves the login page with 200, bot-protection/consent interstitials, network proxies injecting HTML, or Instagram API shape changes.
Understand the failure class
Background: "Invalid JSON response" and "Failed to parse response" errors: when an API answers 200 but the body isn't the JSON your library expected — this error's family across 28 libraries.
- Parsing and encoding errors: unexpected token, malformed input — why parsers reject input and how to find the real culprit.
Related errors
- archive search returned malformed JSON: ${error?.message ||
- archive wayback returned malformed JSON: ${error?.message ||
- coingecko returned malformed JSON: ${error?.message || error
- COMMAND_EXEC
- ${label} returned malformed JSON: ${err?.message ?? err}
AI-assisted analysis of jackwener/OpenCLI@49907e53dc (2026-08-29).
Data as JSON: /api/errors/25c2de2ad59c1b82.
Report an issue: GitHub.