{"record":{"id":"1d93ace89467e25e","repo":"jackwener/OpenCLI","slug":"failed-to-unsave-http-r2-status","errorCode":null,"errorMessage":"Failed to unsave: HTTP ' + r2.status","messagePattern":"Failed to unsave: HTTP ' \\+ r2\\.status","errorType":"http","errorClass":null,"httpStatus":null,"severity":"error","filePath":"clis/instagram/unsave.js","lineNumber":61,"sourceCode":"    return { pk, caption };\n  }\n  function assertOkStatus(payload, label) {\n    if (!payload || typeof payload !== 'object' || payload.status !== 'ok') {\n      throw new Error(label + ' returned no success evidence');\n    }\n  }\n\n  // web_profile_info answers HTTP 400 for business accounts; feed-by-username needs no user id. See #2234.\n  const r1 = await fetch('https://www.instagram.com/api/v1/feed/user/' + encodeURIComponent(username) + '/username/?count=' + (idx + 1), opts);\n  if (!r1.ok) throw new Error(r1.status === 404 ? 'User not found: ' + username : 'HTTP ' + r1.status + ' - make sure you are logged in to Instagram');\n  const { pk, caption } = getPostFromFeed(await readInstagramJson(r1, 'Instagram feed-by-username'), 'Instagram feed-by-username');\n\n  const csrf = document.cookie.match(/csrftoken=([^;]+)/)?.[1] || '';\n  const r2 = await fetch('https://www.instagram.com/api/v1/web/save/' + pk + '/unsave/', {\n    method: 'POST', credentials: 'include',\n    headers: { ...headers, 'X-CSRFToken': csrf, 'Content-Type': 'application/x-www-form-urlencoded' },\n  });\n  if (!r2.ok) throw new Error('Failed to unsave: HTTP ' + r2.status);\n  assertOkStatus(await readInstagramJson(r2, 'Instagram unsave'), 'Instagram unsave');\n  return [{ status: 'Unsaved', user: username, post: caption || '(post #' + (idx+1) + ')' }];\n})()\n` },\n    ],\n});\n","sourceCodeStart":43,"sourceCodeEnd":68,"githubUrl":"https://github.com/jackwener/OpenCLI/blob/49907e53dc3ade5c223ff0c4c2c2785687cec4e6/clis/instagram/unsave.js#L43-L68","documentation":"The unsave POST /api/v1/web/save/{pk}/unsave/ returned a non-2xx HTTP status. This is a transport-level failure before the body contract is even checked — typically 401/403 for auth or 429 for rate limiting. The status code is included in the message.","triggerScenarios":"Session cookies expired (401/403), CSRF token mismatch, or unsave throttling (429) on the POST.","commonSituations":"Long-lived CLI profile where Instagram rotated the session; bulk unsaving triggering 429; missing csrftoken cookie so the request is rejected.","solutions":["Re-login in the browser profile to refresh sessionid and csrftoken, then retry.","If status is 429, wait (minutes to hours) and reduce request rate.","Confirm csrftoken cookie exists before the call and is sent as X-CSRFToken."],"exampleFix":"// before\nconst csrf = document.cookie.match(/csrftoken=([^;]+)/)?.[1] || '';\n// after\nconst csrf = document.cookie.match(/csrftoken=([^;]+)/)?.[1];\nif (!csrf) throw new Error('Not logged in: csrftoken cookie missing');","handlingStrategy":"retry","validationCode":"if (!document.cookie.includes('csrftoken') || !document.cookie.includes('sessionid')) {\n  throw new Error('Instagram session cookies missing — log in first');\n}","typeGuard":null,"tryCatchPattern":"try {\n  await cli.unsave(user, index);\n} catch (e) {\n  const m = e.message.match(/HTTP (\\d+)/);\n  if (m && m[1] === '429') { await delay(15 * 60_000); return retry(); }\n  if (m && (m[1] === '401' || m[1] === '403')) { await relogin(); return retry(); }\n  throw e;\n}","preventionTips":["Keep the browser profile logged in","Back off on 429 before retrying","Refresh cookies after password changes or logouts elsewhere"],"tags":["instagram","http-error","authentication","rate-limit"],"backgroundTag":"http-request-failed","analyzedSha":"49907e53dc3ade5c223ff0c4c2c2785687cec4e6","analyzedAt":"2026-08-29T08:14:47.543Z","schemaVersion":2},"datasetVersion":"2026-08-29T17:17:51.833Z"}