fix(instagram): rotate accounts on private API status=fail (#2391)

* fix(instagram): treat private API status=fail as rotation failure

Parseable 2xx bodies with status: fail (checkpoint, spam block) were
returned as ok: true, which hid empty results and skipped the next
configured account. Match the Threads proxy: mark them failed and rotate.

Co-authored-by: dangered wolf <d@ngeredwolf.me>

* style: prettier Instagram status=fail test fixture

Co-authored-by: dangered wolf <d@ngeredwolf.me>

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
This commit is contained in:
dangered wolf
2026-08-28 01:06:38 -04:00
committed by GitHub
co-authored by Cursor Agent
parent 9fa89e2a78
commit c2c9da2ca5
3 changed files with 60 additions and 9 deletions
@@ -189,7 +189,7 @@ Mixing the two is the usual cause of an unexpected checkpoint, so keep this cons
2. Open DevTools → **Application → Cookies → `https://www.instagram.com`**.
3. Copy the `sessionid`, `ds_user_id`, `csrftoken`, `mid` and `ig_did` values into the fields above, leaving `platform` as `web`.
Sessions are long-lived but not permanent: logging the account out, changing its password, or an Instagram-side checkpoint invalidates the cookie. FxEmbed rotates to the next configured account on `401`, `403` and `429`, and treats an HTML login page as a dead session, so a stale entry degrades one account rather than the whole deployment. Use accounts you're willing to lose, not a personal one.
Sessions are long-lived but not permanent: logging the account out, changing its password, or an Instagram-side checkpoint invalidates the cookie. FxEmbed rotates to the next configured account on `401`, `403` and `429`, on an HTML login page, and on a 200 `{ status: 'fail' }` body (checkpoint / spam block), so a stale entry degrades one account rather than the whole deployment. Use accounts you're willing to lose, not a personal one.
### Encryption and Deployment
@@ -115,8 +115,9 @@ export type InstagramPrivateApiResult = {
/**
* Calls an `i.instagram.com/api/v1/…` endpoint through a proxy account, rotating accounts on
* auth/rate-limit failures. Returns `{ ok: false, status: 0 }` when no proxy account is configured
* so callers can fall back to their logged-out path.
* auth/rate-limit failures and on a 200 `{ status: 'fail' }` body (checkpoint / spam block).
* Returns `{ ok: false, status: 0 }` when no proxy account is configured so callers can fall
* back to their logged-out path.
*/
export async function instagramPrivateApiRequest(
path: string,
@@ -189,16 +190,28 @@ export async function instagramPrivateApiRequest(
last = { ok: false, status: res.status, json: null, accountUsed: account.username };
continue;
}
let parsed: unknown;
try {
return {
ok: true,
status: res.status,
json: JSON.parse(text) as unknown,
accountUsed: account.username
};
parsed = JSON.parse(text) as unknown;
} catch {
last = { ok: false, status: res.status, json: null, accountUsed: account.username };
continue;
}
// The private API answers 200 with `{ status: 'fail' }` for soft failures (checkpoint,
// spam block, feedback_required). Rotate rather than surfacing an empty page as success.
if (
parsed &&
typeof parsed === 'object' &&
(parsed as { status?: unknown }).status === 'fail'
) {
console.error('[instagram] private API returned status=fail', {
path,
account: account.username
});
last = { ok: false, status: 502, json: parsed, accountUsed: account.username };
continue;
}
return { ok: true, status: res.status, json: parsed, accountUsed: account.username };
}
return last;
}
+38
View File
@@ -178,4 +178,42 @@ describe('instagram account proxy', () => {
expect(res.accountUsed).toBe('android_account');
expect(fetchSpy).toHaveBeenCalledTimes(2);
});
it('treats a 200 `status: fail` body as a failure worth rotating past', async () => {
installProxyRuntime([webAccount, androidAccount]);
const fetchSpy = vi.fn(async (_url: string, init: RequestInit) => {
const cookie = (init.headers as Record<string, string>)['Cookie'];
if (cookie.includes('web-session')) {
return new Response(JSON.stringify({ status: 'fail', message: 'checkpoint_required' }), {
status: 200
});
}
return new Response(JSON.stringify({ status: 'ok', items: [] }), { status: 200 });
});
vi.stubGlobal('fetch', fetchSpy);
const res = await instagramPrivateApiRequest('/media/1/info/', { credentialKey: 'key' });
expect(res.ok).toBe(true);
expect(res.json).toEqual({ status: 'ok', items: [] });
expect(res.accountUsed).toBe('android_account');
expect(fetchSpy).toHaveBeenCalledTimes(2);
});
it('reports 502 when every account answers `status: fail`', async () => {
installProxyRuntime([webAccount]);
vi.stubGlobal(
'fetch',
vi.fn(
async () =>
new Response(JSON.stringify({ status: 'fail', message: 'feedback_required' }), {
status: 200
})
)
);
const res = await instagramPrivateApiRequest('/friendships/1/followers/', {
credentialKey: 'key'
});
expect(res.ok).toBe(false);
expect(res.status).toBe(502);
expect(res.json).toEqual({ status: 'fail', message: 'feedback_required' });
});
});