DEV Community

engnzdl
engnzdl

Posted on

Upgrading a vendored anti-bot library across a major version: the patch ledger

I run the Instagram side of a social-automation platform, and it depends on aiograpi — the async port of instagrapi, the library people use to talk to Instagram's private API without the official Graph API. We vendor it (we have to, because we patch it), and recently we took it from 0.9.6 to 2.0.15.

That's not a minor bump. 2.0.15 is the instagrapi 3.x lineage — a rewrite underneath. And when you've been patching a vendored library for months, a major upgrade isn't pip install. It's an accounting exercise: which of my local patches does upstream now have, and which are still mine to carry forward by hand?

The patch ledger

Over the life of our 0.9.6 fork, five local patches had accumulated. Going in, the job was to sort them into two buckets.

Three had been absorbed upstream. The XDT media types (XDTGraphImage / XDTGraphVideo / XDTGraphSidecar — Instagram's newer Graph response shapes), a null guard on empty video_versions, a normalize on carousel-children pk extraction. Those just disappear on upgrade.

Two hadn't. Those are the ones worth describing, because they're the kind of bug that only surfaces in production, and only in an async client.

Patch one: the walrus-operator fix

The photo upload path retries its configure step up to ten times. The original code checked success by reading self.last_json — a shared field the client overwrites on every response. In a synchronous client that's already fragile. In an async client, where another coroutine's response can land between your configure call and your read, it's actively wrong: you're checking whether your upload succeeded by looking at some other request's response.

The fix is one line, and it's the whole lesson:

if (configure_result := await self.photo_configure(upload_id, width, height, caption, ...)):
    media = await self._extract_configured_media_or_recent(configure_result, ...)
Enter fullscreen mode Exit fullscreen mode

The walrus captures configure's return value into a local, so you test the response you actually received — not whatever last touched the shared field. This is the bug that makes photo uploads flake intermittently in ways that are miserable to reproduce: it only bites when two requests race.

Patch two: the authorization header Instagram sends you

Instagram hands the session's auth token back in a response header, ig-set-authorization. The code parsed its sibling — ig-set-x-mid, the machine id — but never parsed the authorization header into the client's authorization_data.

So the session looked logged in while the client quietly worked with an empty authorization, and the first privileged request failed in a way that pointed at everything except the real cause. The fix is a straight header parse:

auth = response.headers.get("ig-set-authorization")
if auth:
    self.authorization_data = self.parse_authorization(auth)
Enter fullscreen mode Exit fullscreen mode

It took a failing login to trace it there, because the symptom — "auth required" on the first action — is indistinguishable from a dozen other auth problems.

The bug the upgrade didn't cause, but the test run caught

None of this counts until it's exercised against a real account. The end-to-end run was: session login, reads (user info, search, friendship status), then writes — follow, like, unlike, unfollow.

The unlike action 404'd with "not-logged-in". It turned out _action_unlike only called the web client's unlike(), while the session we were holding was a mobile session — so the web client had no cookie and got a clean 404. The like path already had the right shape (mobile first, web fallback); the unlike path didn't. Fixed by mirroring it: try media_unlike on the mobile client, fall back to web.

After that, every write verified against the real account — follow reflected as following: true on Instagram, like / unlike / unfollow all clean.

Why I'm writing this

Because this is the unglamorous reality of building on top of reverse-engineered, unofficial APIs: the library is one platform change away from breaking, so you vendor it, you patch it, and when upstream finally catches up you run the ledger — what's absorbed, what's still yours, and what only shows up when you run it against a live account.

If your product sits on a private-API library and you're tired of the treadmill — the upgrades, the patch accounting, the 404s that mean something else entirely — this is the work I do. DM me on X (@engnzdl) or reach me at enginozdal.com.

Top comments (0)