Skip to content

fix: logged-in posts() via GraphQL timeline; current browser headers; scoped cookies; loud errors - #128

Merged
diezo merged 4 commits into
diezo:masterfrom
keunwoochoi:fix/web-feed-headers
Sep 13, 2026
Merged

diezo merged 4 commits into
diezo:masterfrom
keunwoochoi:fix/web-feed-headers

Conversation

@keunwoochoi

Copy link
Copy Markdown

Problem

Since early September 2026 Guest.posts() / Web.posts() fail for everyone:

  • Unauthenticated: /api/v1/feed/user/<name>/username/ answers 401 {"require_login": true, "igweb_rollout": true} from any IP. Instagram has login-walled the guest feed.
  • Logged in: the same endpoint (and the numeric /api/v1/feed/user/<id>/ variant) answers 302 -> https://www.instagram.com/, so ensta reported the misleading HTTP response doesn't include 'status' or 'items' node.

Two things in ensta made the logged-in path fail even where Instagram would have served it:

  1. Browser identity was stale and internally inconsistent: Chrome 119 UA, sec-ch-ua / sec-ch-ua-full-version-list set to the full UA string (not the brand list), a random x-ig-www-claim (hmac.<garbage>), a random x-csrftoken that never matched the cookie Instagram set, and 2023-era x-asbd-id values.
  2. Session cookies were set without a domain, so after the first response the jar held both the placeholder and Instagram's fresh .instagram.com copy and every request carried duplicate sessionid/csrftoken/rur/ds_user_id/ig_did cookies (RequestsCookieJar.get("csrftoken") even raises CookieConflictError).

Changes

  • ensta/lib/WebHeaders.py: single source for the UA, matching sec-ch-ua* client hints, x-ig-app-id, x-asbd-id; api_headers() builder; csrf_token_from() (prefers the .instagram.com cookie); describe_response() for error messages.
  • Guest, WebSession, Authentication use it instead of hand-copied headers. x-ig-www-claim starts at "0" and is updated from the x-ig-set-www-claim response header (WebSession installs a response hook).
  • Session cookies are scoped to .instagram.com.
  • WebSession.posts() now fetches the profile timeline the way the web app does: POST /graphql/query with the persisted PolarisProfilePostsQuery doc_id (34579740524958711; 7898261790222653 is retired and answers "execution error"). The request is deliberately minimal (variables, doc_id, server_timestamps; x-ig-app-id, x-csrftoken, referer) — adding the web app's own markers without its per-page LSD/DTSG tokens makes Instagram answer with the HTML app shell. Response nodes have the v1 media shape, so __process_post_data is reused; pagination follows page_info.end_cursor.
  • Guest.posts(): optional user_id= for the numeric endpoint; redirects are no longer followed; 401/require_login, non-JSON and unexpected bodies now include HTTP status, URL and a body snippet in the exception.
  • Tests: test_ensta/test_guest_posts.py, test_ensta/test_web_posts.py (request shape, pagination, cookie scoping, error reporting).

Verification

Live, with a real logged-in session on 2026-09-12: Web(...).posts("<public venue>", 3) returns 3 posts (codes, taken_at, captions, share URLs) with has_next_page=True. The unauthenticated Guest.posts() still cannot work — Instagram requires login now — but it fails with an actionable message instead of a misleading one.

https://claude.ai/code/session_01AbNFrVSELUP8MhgbS8iQXe

…ndpoint

Instagram now login-walls the /api/v1/feed/user/ endpoint and rejects the
browser identity ensta sent (Chrome 119 UA with sec-ch-ua set to the full UA
string, a random x-ig-www-claim, and 2023-era x-asbd-id). Even a valid
logged-in session was redirected to the login page.

- New ensta/lib/WebHeaders.py: single source for UA, matching sec-ch-ua*
  client hints, x-ig-app-id and x-asbd-id, plus an api_headers() builder.
- Guest/WebSession/Authentication use it instead of hand-copied headers.
- x-ig-www-claim starts at "0" and is updated from the x-ig-set-www-claim
  response header (WebSession installs a response hook), instead of a
  random "hmac.<garbage>" value.
- x-csrftoken is read from the cookie jar at request time so it matches
  the csrftoken cookie Instagram set.
- posts(): optional user_id= selects the numeric feed endpoint the web app
  itself calls; redirects are no longer followed and are reported as a
  login wall; 401/require_login, non-JSON and unexpected bodies now
  include HTTP status, URL and a body snippet in the exception message.
- Tests for the feed request shape and error reporting.

Claude-Session: https://claude.ai/code/session_01AbNFrVSELUP8MhgbS8iQXe
After the first real Instagram response the jar holds two "csrftoken"
cookies (ensta's domainless placeholder and the .instagram.com one), and
RequestsCookieJar.get() raises CookieConflictError. csrf_token_from()
prefers the instagram.com-scoped value. profile() now sends the real
csrftoken and the remembered www-claim and reports HTTP status + body
snippet on non-JSON responses.

Claude-Session: https://claude.ai/code/session_01AbNFrVSELUP8MhgbS8iQXe
…ookies

Instagram now answers the /api/v1/feed/user/ REST endpoint with a redirect
to the home page even for valid logged-in sessions (both the /username/
and numeric variants). The web app fetches the profile timeline through
POST /graphql/query with the persisted PolarisProfilePostsQuery doc_id, and
that still works for logged-in sessions, so WebSession.posts() now does
the same. The request is deliberately minimal (variables, doc_id,
server_timestamps; x-ig-app-id, x-csrftoken, referer): adding the web
app's own request markers without its per-page LSD/DTSG tokens makes
Instagram answer with the HTML app shell instead of JSON. The response
nodes have the v1 media shape, so the existing post parser is reused.
Pagination follows page_info.end_cursor.

Session cookies are now set with domain=.instagram.com. Set without a
domain they lingered next to the fresh domain-scoped cookies Instagram
sends back, and every request carried duplicate sessionid/csrftoken/rur/
ds_user_id/ig_did cookies.

Claude-Session: https://claude.ai/code/session_01AbNFrVSELUP8MhgbS8iQXe
Instagram answers the timeline query for a deleted/renamed account with
"execution error" and description "User lookup returned null". That was
being reported as "doc_id may be retired", which points operators at the
wrong fix. Error descriptions are now included in the message.

Claude-Session: https://claude.ai/code/session_01AbNFrVSELUP8MhgbS8iQXe
Comment thread ensta/lib/WebHeaders.py
for cookie in session.cookies:
if cookie.name != "csrftoken":
continue
if "instagram.com" in (cookie.domain or ""):
@diezo
diezo merged commit de21af5 into diezo:master Sep 13, 2026
3 of 6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants