mirror of
https://github.com/FxEmbed/FxEmbed.git
synced 2026-10-02 01:54:54 +08:00
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:
co-authored by
Cursor Agent
parent
9fa89e2a78
commit
c2c9da2ca5
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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' });
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user