Skip to content

fix: Emulate removed Redgifs /info endpoint - #100

Open
KingOfPoptart wants to merge 9 commits into
wchill:devfrom
KingOfPoptart:fix/redgifs-info-endpoint
Open

fix: Emulate removed Redgifs /info endpoint#100
KingOfPoptart wants to merge 9 commits into
wchill:devfrom
KingOfPoptart:fix/redgifs-info-endpoint

Conversation

@KingOfPoptart

Copy link
Copy Markdown

Summary

Sync for Reddit currently fails to open Redgifs links, showing "Error connecting to Redgifs" instead of playing the video.

Root cause: Sync's legacy Redgifs flow is a 3-step Volley request chain — get OAuth token → get client IP via GET /info → get gif via GET /v2/gifs/{id}. Redgifs has removed /info entirely (confirmed 404 as of 2026-08-12); it was only ever used to populate the legacy user-addr query parameter. Since step 2 now fails outright, the chain never reaches step 3, so the gif never resolves.

The existing FixRedgifsApiPatch already emulates Redgifs' old (also-removed) OAuth endpoint locally rather than hitting the dead network endpoint. This PR extends the same technique to /info: emulate a local {"remote-addr": "..."} response instead of proxying to the network. Current Redgifs endpoints accept but ignore the user-addr value derived from this, confirmed via direct testing against the live API (a placeholder value works fine), and cross-checked against how other actively-maintained Redgifs clients (yt-dlp, Voyager, Hydra, RedditRepostSleuth) handle auth — none of them use /info or any IP-lookup step at all.

The fix lives in the shared BaseFixRedgifsApiPatch/RedgifsTokenManager extension code, so it applies to Sync, Boost for Reddit, and BaconReader simultaneously, not just Sync.

Test plan

  • Built patcheddit from source and applied the patched bundle to com.laurencedawson.reddit_sync v23.06.30-13:39 with morphe-desktop
  • Decompiled the patched APK to confirm the new emulation code is present and correctly wired into the interceptor
  • Installed on a physical Pixel 8 Pro and confirmed Redgifs links open and play correctly, where they previously failed
  • Verified via direct API testing that the real /info endpoint 404s, and that the downstream /v2/gifs/{id} fetch succeeds regardless of what user-addr value it's given

Redgifs removed the /info endpoint, which was only used to populate the
legacy "user-addr" query parameter on gif requests. Since that endpoint
now 404s, apps whose legacy Redgifs flow depends on it (e.g. Sync for
Reddit) never reach the actual gif-fetch request, surfacing as a
connection error instead of playing the video.

Emulate the endpoint locally the same way the old OAuth endpoint is
already emulated, since current Redgifs endpoints accept but ignore the
"user-addr" value populated from it.
@KingOfPoptart

Copy link
Copy Markdown
Author

FYI for whoever picks this up: dev's build has been broken since the ExtensionPatches.kt/sharedExtensionPatch refactor landed (b4ac952, 2026-05-03) — confirmed via the repo's own CI (Release workflow shows conclusion: failure on that commit) and independently hit by #96 too.

Root cause: gradle/libs.versions.toml pins morphe-patches-library = "1.0.2", but that version only has sharedExtensionPatch(String, List<ExtensionHook>) as private. The call site needs the public sharedExtensionPatch(String, vararg ExtensionHook) overload, which exists in 1.0.2-dev.2 (already published to the registry). Bumping the pin to 1.0.2-dev.2 fixes the build — verified locally, full buildAndroid succeeds and the resulting bundle patches correctly.

Not including that bump in this PR since it's unrelated to the Redgifs fix, but wanted to flag it since it's currently blocking CI on every PR into dev (including this one and #96). Happy to open a separate PR for just the version bump if useful.

In the meantime, I published an unofficial build with this fix + the version bump for anyone who wants to use it before this gets reviewed: https://github.com/KingOfPoptart/patcheddit/releases/tag/redgifs-fix-v1

Logs each intercepted request's path, which branch handled it (local
emulation vs proxied to the network), and on failure the actual
exception (DNS/TLS/timeout/etc.) or HTTP status code -- previously a
proceed() failure on the network paths propagated silently with no
log line at all, making it hard to tell why a user's Redgifs requests
were failing (e.g. reports of failures specifically over VPN).

Uses Logger.printInfo/printException, which write to logcat and the
extension log buffer unconditionally (unlike printDebug, they aren't
gated behind the DEBUG setting).
RedgifsToken.isValid() only checks time-based expiry, never whether the
server actually accepted the token. When a request using the app's own
Authorization header gets a 401, the existing retry path called
refreshToken() again, but since the cached token was still time-valid
it just handed back the exact same (already-rejected) token, so the
retry always failed identically to the first attempt.

Confirmed via a user's logcat output: two consecutive 401s for the same
gif request, ~150ms apart, both presumably using the same token, since
nothing forced a new one to be minted.

Invalidate the cache entry as soon as a 401 is observed on the
existing-Authorization path, so the subsequent refreshToken() call
mints a genuinely new token instead of resending the rejected one.
@KingOfPoptart

Copy link
Copy Markdown
Author

Update: a user reported still seeing Redgifs failures after this fix, so I added diagnostic logging (now included in this PR) to help track it down. Their adb logcat output revealed a real, separate bug: /v2/gifs/{id} requests were failing with 401, and the existing "refresh and retry" logic wasn't actually refreshing anything — RedgifsToken.isValid() only checks time-based expiry, never whether the server actually accepted the token, so the retry just resent the identical already-rejected token from cache.

Added RedgifsTokenManager.invalidateToken() and call it as soon as a 401 is observed on the existing-Authorization path, so the retry actually mints a fresh token. Also included in this PR now, since it's directly downstream of the same code path.

Updated build for anyone testing: https://github.com/KingOfPoptart/patcheddit/releases/tag/redgifs-fix-v1

New setting: Settings > Security > "Open Redgifs links in browser
view", off by default. When enabled, Redgifs links open in Sync's
existing in-app WebView (the same one used for login) instead of the
native player, bypassing the Redgifs API/token flow entirely.

Motivated by a user report of persistent "Error connecting to
Redgifs" that survived the token-retry fix. A real browser
demonstrably works for them; a WebView is a real browser engine
(same Chromium as the system browser), so this should behave the
same way when the native flow doesn't. Confirmed working live on a
Pixel 8 Pro: toggling the setting on/off correctly switches between
WebView and native player behavior.

Implementation notes:
- Reuses Sync's existing, already-manifest-declared WebViewActivity
  (K0(Context, String) just takes a URL) -- no new Activity or
  manifest patch needed.
- SettingsSingleton$Settings is NOT Gson/reflection backed:
  SettingsSingleton.p(Context) populates each field individually via
  SharedPreferences.getBoolean(key, default). A field added without
  a matching read here silently never populates from the stored
  preference, regardless of what the UI shows -- this was the actual
  bug in an earlier version of this patch (checkbox toggled fine,
  had zero runtime effect). Fixed by inserting a matching read right
  after the existing "doh" field's read in that same method.
- SettingsSingleton is a lazy singleton built once per app process,
  so toggling the setting requires a full app restart (not just
  background/resume) to take effect -- true of the existing "doh"
  setting too, not specific to this one.
- Off by default, so default behavior for users who don't opt in is
  completely unchanged (falls through to the original, unmodified
  method body when the setting is off).
pdscomp added a commit to pdscomp/patcheddit that referenced this pull request Aug 15, 2026
Emulate the removed /info endpoint, add diagnostic logging, and add an optional Sync WebView fallback. Keep the existing forced token refresh implementation instead of duplicating upstream cache invalidation, and adapt the WebView patch to this branch's Morphe 1.3 compatibility API.
pdscomp added a commit to pdscomp/patcheddit that referenced this pull request Aug 15, 2026
PR wchill#100 already repairs Sync native playback by emulating the removed Redgifs /info endpoint. The additional WebView toggle is therefore a workaround for the old failure mode rather than part of the API repair.

Remove its app-version-specific fingerprints, injected Settings field, and preference UI so this branch stays focused on the shared API fix. This reduces the surface area that can break when Sync bytecode or the patcher API changes while retaining native playback for every client covered by the shared interceptor.
pdscomp added a commit to pdscomp/patcheddit that referenced this pull request Aug 15, 2026
PR wchill#100 invalidates the cached token before reading the User-Agent from the response request. Another interceptor can rewrite that header, so the rejected token may be cached under a different key than the default user agent. Invalidating the default key can then leave the rejected token available for immediate reuse.

Capture a non-empty effective User-Agent first and pass an explicit force-refresh flag to the token manager. This always mints a replacement for the identity that actually failed, keeps unrelated cached tokens intact, and avoids replacing a usable fallback with a null response header. The existing refreshToken(String) entry point remains for callers that want normal cache behavior.
pdscomp added a commit to pdscomp/patcheddit that referenced this pull request Aug 15, 2026
The original Redgifs work was prepared against an earlier patches-library API and passed a single extension name followed by hook varargs. On the PR wchill#100 dependency baseline, the single-name/list helper is private while the supported public entry point accepts a list of extension names followed by hook varargs.

Wrap the extension name in a one-item list and expand the generated hooks into that public overload. This preserves the original per-client hook behavior while compiling against the same API generation used by PR wchill#100.
@pdscomp

pdscomp commented Aug 16, 2026

Copy link
Copy Markdown

Redgifs PR #100 Follow-up Fix

This update builds on PR #100 and is available from pdscomp/patcheddit:fix-redgifs.

PR #100 restored the removed Redgifs /info endpoint, but it did not fully fix recovery from a rejected cached token. Redgifs tokens are cached by User-Agent; the original retry path could invalidate the default User-Agent entry instead of the effective User-Agent used by the failed request. That left the rejected token cached and available for reuse.

Our update fixes this by:

  • Capturing the effective User-Agent from the failed request.
  • Falling back to the default only when that value is unavailable.
  • Force-refreshing the token stored under the exact cache key that failed.
  • Updating the shared-extension call to the current public patches-library API.

In short: the original PR repaired the endpoint, but did not resolve this token cache-key mismatch; our update does.

The branch was verified with:

./gradlew :patches:buildAndroid --no-daemon

Result: BUILD SUCCESSFUL

Pull the complete update

The branch is based directly on PR #100's head, so maintainers can pull it as a fast-forward from that commit:

git remote add pdscomp https://github.com/pdscomp/patcheddit.git
git fetch pdscomp fix-redgifs
git merge --ff-only pdscomp/fix-redgifs

For me, the original test build still failed with the same error sometimes, these updates made it 100% reliable in my testing.

Cheers!

-PD

PR wchill#100 invalidates the cached token before reading the User-Agent from the response request. Another interceptor can rewrite that header, so the rejected token may be cached under a different key than the default user agent. Invalidating the default key can then leave the rejected token available for immediate reuse.

Capture a non-empty effective User-Agent first and pass an explicit force-refresh flag to the token manager. This always mints a replacement for the identity that actually failed, keeps unrelated cached tokens intact, and avoids replacing a usable fallback with a null response header. The existing refreshToken(String) entry point remains for callers that want normal cache behavior.
The original Redgifs work was prepared against an earlier patches-library API and passed a single extension name followed by hook varargs. On the PR wchill#100 dependency baseline, the single-name/list helper is private while the supported public entry point accepts a list of extension names followed by hook varargs.

Wrap the extension name in a one-item list and expand the generated hooks into that public overload. This preserves the original per-client hook behavior while compiling against the same API generation used by PR wchill#100.
KingOfPoptart pushed a commit to KingOfPoptart/patcheddit that referenced this pull request Aug 16, 2026
PR wchill#100 invalidates the cached token before reading the User-Agent from the response request. Another interceptor can rewrite that header, so the rejected token may be cached under a different key than the default user agent. Invalidating the default key can then leave the rejected token available for immediate reuse.

Capture a non-empty effective User-Agent first and pass an explicit force-refresh flag to the token manager. This always mints a replacement for the identity that actually failed, keeps unrelated cached tokens intact, and avoids replacing a usable fallback with a null response header. The existing refreshToken(String) entry point remains for callers that want normal cache behavior.
KingOfPoptart pushed a commit to KingOfPoptart/patcheddit that referenced this pull request Aug 16, 2026
The original Redgifs work was prepared against an earlier patches-library API and passed a single extension name followed by hook varargs. On the PR wchill#100 dependency baseline, the single-name/list helper is private while the supported public entry point accepts a list of extension names followed by hook varargs.

Wrap the extension name in a one-item list and expand the generated hooks into that public overload. This preserves the original per-client hook behavior while compiling against the same API generation used by PR wchill#100.
@KingOfPoptart

Copy link
Copy Markdown
Author

Thanks for this — reviewed both fixes carefully.

The token cache-key fix is a real bug and I've adopted it (cherry-picked 962dc79). Traced through it independently before seeing your diff: invalidateToken(userAgent) was being called with the pre-reassignment userAgent, before the following line reassigns it to the actual effective User-Agent. Your fix (reorder + forceRefresh flag through refreshToken) is correct and cleaner than what was there. Credited via cherry-pick, commit history preserved.

The build fix is genuinely better than what I'd proposed. I'd worked around dev's broken build by pinning morphe-patches-library to an unpublished -dev prerelease — your fix (using the already-public List<String> overload at the call site) compiles clean against the currently-published stable version, no prerelease pin needed at all. Adopted this too, and reverted the version pin since it's no longer necessary. This is the better fix and probably worth flagging to wchill on its own.

I've kept the WebView fallback, though — didn't adopt the removal commit. The reasoning in that commit is that the /info fix already resolves native playback, so the fallback is redundant. But it isn't, for at least one case: a user on this thread kept hitting persistent 401s over VPN even with the /info fix and both retry fixes applied — never found a root cause for that one, despite testing 60+ VPN exit nodes trying to reproduce it. The WebView toggle is the confirmed-working answer for that specific case (verified live on-device, both directions). Happy to revisit if it turns out to be unnecessary, but there's a real user it's currently the only fix for.

Updated build's up now if you want to verify: https://github.com/KingOfPoptart/patcheddit/releases/tag/redgifs-fix-v1

@pdscomp

pdscomp commented Aug 16, 2026

Copy link
Copy Markdown

Awesome, thank you! Makes sense!

@pdscomp

pdscomp commented Aug 21, 2026

Copy link
Copy Markdown

Redgifs follow-up: true fallback + URL canonicalization

Follow-up to my earlier comment here — new work lives on pdscomp/patcheddit:fix-poptarts, rebased on KingOfPoptart's main. In testing this now handles every link shape we've thrown at it correctly: native playback wherever possible, with a WebView fallback that only fires as a fallback. Two changes vs. the old fix-redgifs branch:

1. The WebView fallback is now actually a fallback. The old branch short-circuited link handling, so every redgifs link opened in the WebView even when the native player would have worked. The new patch hooks the API error funnel instead — the native player gets first crack, and the WebView only opens when the request genuinely fails.

2. Fallback UX: consent persists. Sync wipes all cookies when opening its in-app WebView, so the cookie-consent wall had to be clicked through on every single fallback. The patch auto-clicks the CookieYes "agree" button and skips the cookie wipe for redgifs URLs, so consent survives between opens. If the consent wall or its markup ever changes upstream, the auto-click simply finds nothing and no-ops — the fallback still works, you'd just tap agree once like before.

3. URL canonicalization at the extraction point. Some links render fine in a browser (or the WebView) but produce mangled IDs for the API: the site's web server tolerates junk in the path, but the API route-matches strictly and 404s. Worse, the app's ID extractor strips the trailing slash before the query string, so a trailing query can wipe the ID entirely and the request goes out with an empty one. The patch replaces extraction with: strip query/fragment from the full URL first, then take the last path segment.

Cases this fixes/handles (fabricated IDs, but real shapes seen in the wild):

  • …/watch/someexampleid/?utm_source=share/ — query + trailing slash previously produced an empty ID → "Error connecting to Redgifs". Now canonicalized → native playback.
  • i.redgifs.com/i/someexampleid.jpg — direct-image links to image-type posts, which have no video URLs in the API at all. Deliberately still 404s the API → fallback WebView shows the image (feeding the resolved JPEG to the video player would just show "Could not load video").
  • Plain …/watch/someexampleid — unchanged, native playback as before.

Verified with ./gradlew :patches:buildAndroid --no-daemon (BUILD SUCCESSFUL) and applied clean against the real APK.

Cheers!

-PD

…accepted consent

Two related changes to the "Open Redgifs links in browser view" setting:

1. Make it a fallback instead of a default. The setting used to
   short-circuit LinkHelper at link-open time, so every Redgifs link
   used the WebView even when the API would have worked. Now hook
   ImageViewerFragment$g0.onErrorResponse -- the single failure funnel
   for Sync's Redgifs playback flow (OAuth, /info, and /v2/gifs/<id>
   requests all deliver errors there, and its 'a' field holds the
   original redgifs.com URL). The native/API player is always attempted
   first; the WebView only opens on an actual failure. On fallback the
   player activity is finished and its error handling is skipped: no
   error flash before the WebView appears, and Back from the WebView
   returns to the page that opened the player. Setting off = unchanged
   behavior.

2. Auto-accept the CookieYes consent banner on redgifs pages and
   persist it. Sync's WebViewFragment wipes all cookies on every fresh
   WebView open, so the banner returned every time. WebViewFragment$a
   .onPageFinished now auto-clicks the CookieYes accept button
   ([data-cky-tag=accept-button], legacy .cky-btn-accept fallback), and
   the removeAllCookie wipe in onViewCreated is replaced with a helper
   call that skips it for redgifs URLs. Host-exact matching
   (redgifs.com and subdomains only); non-redgifs WebView behavior is
   unchanged, and if the banner markup changes the click no-ops.

Scope: main player flow only; long-press peek preview has its own error
listener and is unchanged.
LinkHandler.getGfycatId (y7.a.d) strips the trailing slash before the
query/fragment, so URLs like redgifs.com/watch/<id>/?92/ leave ?92 as
the last path segment and the query-strip then reduces the ID to an
empty string -- the API call goes out as /v2/gifs/ and 404s, showing
"Error connecting to Redgifs" for links that work fine on the site.

Redirect the method to a corrected extension implementation: strip
query/fragment from the full URL first, then take the last segment.
Suffix cleanup is parity-identical to the original (verified against a
matrix of gfycat/redgifs URL shapes). Notably the ".jpg" suffix is NOT
stripped: type-2 image posts resolve to .jpg URLs that Sync feeds to
its video player (error 14 "Could not load video"), so keeping the
suffix lets the API 404 and the WebView fallback display the image.
Video URLs with malformed paths now play in the native player without
needing the fallback.
@KingOfPoptart

Copy link
Copy Markdown
Author

Pulled in both commits from your fix-poptarts follow-up (thanks again, @pdscomp) -- cherry-picked cleanly onto this branch and onto our fork's main, no conflicts. Went through the same verification pass as everything else in this PR: full build, decompile the patched APK to confirm the new fingerprints/instructions landed exactly as written, then live-tested on a real device against the real target APK and the live redgifs.com. Results:

Fallback-only behavior -- confirmed working. With the setting off, nothing changes. With it on, verified via logcat that the native/API path is always attempted first and WebViewActivity only launches after a real 404, not by default. This directly fixes the earlier "it should be a fallback, not a default" concern.

URL canonicalization -- confirmed working. Fed it redgifs.com/watch/<id>/?utm_source=share/ (the malformed shape from your writeup) against a real, currently-live gif ID -- extracted correctly, played natively. Clean fix.

Cookie auto-accept -- did not fire in testing. The patch's JS is correctly wired in (confirmed via decompile: WebViewFragment$a.onPageFinished calls it unconditionally), and I waited up to 15s against the live site, but the CookieYes banner never dismissed itself -- I had to tap it manually. Couldn't get WebView DevTools access in my test environment to confirm whether it's a selector mismatch against the current CookieYes markup or the 5s poll window losing a timing race against the async CookieYes script load on a real connection. Either way it degrades safely (falls through to the pre-existing "tap agree once" experience, no crash, no regression).

Cookie persistence -- confirmed working, once accepted manually. After clearing the age-gate and consent banner by hand once, force-killed the app and re-triggered a fresh WebView fallback: no banner the second time, straight to content. So the "skip the wipe for redgifs URLs" half of the fix genuinely works -- it's specifically the auto-click half that isn't landing on the current live site.

One more thing found along the way, not part of your PR: Sync's gif-ID extraction (both the pre-existing code and your corrected version, since you kept it parity-identical there) never lowercases the ID, but Redgifs' /v2/gifs/{id} endpoint is case-sensitive. Confirmed with a real ID: .../watch/DrearyWhitesmokeMaggot 404s, .../watch/drearywhitesmokemaggot succeeds. Since Sync's own generated watch-page URLs are commonly title-case, this looks like a meaningful real-world failure mode independent of everything else here -- flagging it rather than folding a fix into this PR, since it's out of scope for what you were fixing.

Shipped in the unofficial build too: redgifs-fix-v1, 1.4.1-dev.6.

…onditional

The setting added a second layer of gating on top of the fallback
already being fallback-only (native player always tried first, WebView
only opens after a real failure) -- users had to know the toggle
existed and turn it on before the fallback would ever help them.

Removes the SettingsSingleton$Settings field, its SharedPreferences
read in the loader method, and the Settings > Security checkbox
entirely. The WebView fallback now applies unconditionally whenever
this patch is included, matching how every other fix patch in this
bundle behaves (opt in/out at patch time via Manager, not via an
in-app setting).

Verified live: a working link plays natively with nothing touched: a
failing link falls through to the WebView with a fresh, never-configured
install.
@KingOfPoptart

Copy link
Copy Markdown
Author

One more change on top of the above: removed the in-app "Open Redgifs links in browser view" Settings > Security toggle entirely. The WebView fallback is now unconditional whenever this patch is applied -- native player is always tried first, WebView opens automatically on a real failure, nothing to configure. Reasoning: the fallback was already fallback-only (only fires on genuine failure, never replaces a working native playback), so gating it behind a second, easy-to-miss opt-in setting didn't add safety, just meant most users who'd actually benefit from it never knew to turn it on.

Removed the SettingsSingleton$Settings field, its SharedPreferences read, and the checkbox resource patch -- one less thing to keep wired correctly, and one less place for a future markup/library change to break silently.

Verified live on a completely fresh install (no settings ever touched): a working Redgifs link plays natively, and a genuinely-failing one falls through to the WebView automatically.

Shipped in redgifs-fix-v1, 1.4.1-dev.7.

@pdscomp

pdscomp commented Aug 22, 2026

Copy link
Copy Markdown

I was thinking about suggesting that exact change but it slipped my mind. Sweet!

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.

2 participants