Skip to content

fix(crawler): anonymous Reddit fallback via JS-challenge solve (#2885)#2944

Open
beaglemoo wants to merge 5 commits into
karakeep-app:mainfrom
beaglemoo:fix/reddit-crawl-anonymous-fallback
Open

fix(crawler): anonymous Reddit fallback via JS-challenge solve (#2885)#2944
beaglemoo wants to merge 5 commits into
karakeep-app:mainfrom
beaglemoo:fix/reddit-crawl-anonymous-fallback

Conversation

@beaglemoo

Copy link
Copy Markdown

Fixes #2885.

Reddit now blocks unauthenticated .json metadata requests, so Reddit bookmarks fail to crawl. #2887 adds an OAuth client-credentials path, but Reddit closed self-serve API app creation under its Responsible Builder Policy, so many self-hosters can't obtain credentials.

This keeps the OAuth path (used when REDDIT_CLIENT_ID/REDDIT_CLIENT_SECRET are set) and adds an anonymous fallback: with a browser User-Agent, Reddit serves a lightweight JS challenge (solution = seed repeated twice) instead of a hard block. The metascraper warms up on the HTML post URL, solves the challenge, submits the hidden form to earn clearance cookies, and reuses them for the .json request. Cookies are threaded manually since fetchWithProxy (node-fetch) has no cookie jar. If the challenge format changes, it falls back to the existing DOM scraping.

Verified end-to-end on a self-hosted instance: Reddit bookmarks crawl with title, author, subreddit, preview image, and content, with no credentials configured.

Note: this deliberately solves Reddit's anti-bot challenge, flagging that explicitly in case it raises ToS concerns for the project.

…eep-app#2885)

Reddit now blocks unauthenticated .json metadata requests. PR karakeep-app#2887 adds
an OAuth client-credentials path, but Reddit closed self-serve API app
creation, so many self-hosters cannot obtain credentials.

This keeps the OAuth path (used when REDDIT_CLIENT_ID/SECRET are set) and
adds an anonymous fallback: with a browser User-Agent, Reddit serves a
lightweight JS challenge (solution = seed repeated twice) rather than a
hard block. The metascraper warms up on the HTML post URL, solves the
challenge, submits the hidden form to earn clearance cookies, and reuses
them for the .json request. Cookies are threaded manually since
fetchWithProxy (node-fetch) has no cookie jar. If the challenge format
changes it falls back to the existing DOM scraping.
@beaglemoo
beaglemoo marked this pull request as ready for review July 11, 2026 14:33
@greptile-apps

greptile-apps Bot commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds a Reddit metadata fallback for crawls without usable API credentials. The main changes are:

  • Optional Reddit OAuth credentials in shared config.
  • OAuth JSON fetching when credentials are configured.
  • Anonymous browser-style challenge solving with manual cookies.
  • Reddit comment extraction for readable content.

Confidence Score: 4/5

This is close, but one fallback path should be fixed before merging.

  • Non-OK OAuth responses other than 401 or 403 can still block the anonymous Reddit fallback.
  • The cached token remains active after those responses, so later crawls can keep failing on the same path.

apps/workers/metascraper-plugins/metascraper-reddit.ts

Important Files Changed

Filename Overview
apps/workers/metascraper-plugins/metascraper-reddit.ts Adds Reddit OAuth fetching, anonymous challenge solving, manual cookie handling, and comment-backed readable content.
packages/shared/config.ts Adds optional Reddit client credential fields to the shared server config.
Prompt To Fix All With AI
Fix the following 1 code review issue. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 1
apps/workers/metascraper-plugins/metascraper-reddit.ts:305
**OAuth errors skip fallback**

When Reddit returns a non-OK OAuth response other than 401 or 403, such as a 429 rate limit or transient 5xx from `oauth.reddit.com`, this branch returns that response instead of trying the anonymous challenge path. The cached token is left in place, so later Reddit crawls keep using the same OAuth path until local token expiry and continue returning failed metadata even though anonymous crawling may work.

Reviews (2): Last reviewed commit: "feat: include top Reddit comments in cra..." | Re-trigger Greptile

Comment thread apps/workers/metascraper-plugins/metascraper-reddit.ts Outdated
Comment thread apps/workers/metascraper-plugins/metascraper-reddit.ts
Comment thread apps/workers/metascraper-plugins/metascraper-reddit.ts Outdated
- Resolve the challenge form action with new URL(action, base) so absolute
  or protocol-relative actions no longer build a malformed submit URL.
- On a 401/403 from the OAuth path, drop the cached token and fall through
  to the anonymous challenge path instead of failing the crawl.
@beaglemoo

Copy link
Copy Markdown
Author

Thanks for the review. Addressed 2 of the 3 in the latest commit:

  • Absolute/protocol-relative form action — fixed with new URL(action, "https://www.reddit.com").
  • Rejected OAuth token skips fallback — on a 401/403 from the OAuth path, the cached token is now cleared and the code falls through to the anonymous challenge path.

On redirect cookies dropped: this is a real limitation but I've left it as-is deliberately. Capturing per-hop Set-Cookie would mean reimplementing fetchWithProxy's redirect loop (which also does per-hop SSRF revalidation and proxy-agent selection), and I'd rather not fork that logic here. In practice Reddit serves the challenge page and the challenge-submit response as terminal 200s and sets the clearance cookies there; the only redirect observed on the warmup is same-site trailing-slash canonicalization, which sets no auth cookie. If the challenge submit ever fails, the plugin falls back to the existing DOM scraping, so the failure mode is graceful. Happy to revisit if a maintainer would prefer a redirect-aware cookie jar.

If the JSON body fetch fails (e.g. Reddit rate-limits us), returning no
readable content let the generic readability pass scrape the rendered page,
which on Reddit captures the cookie-consent banner as the article body and
poisoned tagging/summaries. Fall back to the DOM post title instead so the
stored content stays meaningful and a later re-crawl can recover the body.
Many Reddit posts (link/discussion posts) have an empty body, so the useful
context lives in the comments. Parse the comments listing and fold the top
~10 comments (by score, with author) into readableContentHtml after the post
body, so the reader view and the tagger get real content instead of just the
title. HTML is sanitized downstream by DOMPurify.
// A rejected token (revoked, early-expired, or insufficient scope) should
// not fail the crawl - drop the cached token and fall through to the
// anonymous challenge path, which works without credentials.
if (response.status !== 401 && response.status !== 403) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 OAuth errors skip fallback

When Reddit returns a non-OK OAuth response other than 401 or 403, such as a 429 rate limit or transient 5xx from oauth.reddit.com, this branch returns that response instead of trying the anonymous challenge path. The cached token is left in place, so later Reddit crawls keep using the same OAuth path until local token expiry and continue returning failed metadata even though anonymous crawling may work.

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/workers/metascraper-plugins/metascraper-reddit.ts
Line: 305

Comment:
**OAuth errors skip fallback**

When Reddit returns a non-OK OAuth response other than 401 or 403, such as a 429 rate limit or transient 5xx from `oauth.reddit.com`, this branch returns that response instead of trying the anonymous challenge path. The cached token is left in place, so later Reddit crawls keep using the same OAuth path until local token expiry and continue returning failed metadata even though anonymous crawling may work.

How can I resolve this? If you propose a fix, please make it concise.

The OAuth path only fell back to the anonymous challenge on 401/403, so a
429 rate limit or transient 5xx from oauth.reddit.com returned the failing
response and kept reusing the cached token. Fall back on any non-OK status;
only drop the cached token on 401/403 (a rate limit or server error does not
mean the token is invalid).
@beaglemoo

Copy link
Copy Markdown
Author

Good catch on the OAuth fallback - fixed in the latest commit. The OAuth path now falls back to the anonymous challenge on any non-OK response (including 429 rate limits and 5xx), not just 401/403. The cached token is only dropped on 401/403, since a rate limit or transient server error doesn't mean the token itself is invalid.

@ncareau

ncareau commented Jul 20, 2026

Copy link
Copy Markdown

Hi @beaglemoo, looking forward to getting this merged. In the meantime, do you have a docker registry with this version on the web ? Would love to try it.

@beaglemoo

Copy link
Copy Markdown
Author

Hey @ncareau — sure thing. I pushed an unofficial build of this PR to GHCR so you can try it before it merges:

ghcr.io/beaglemoo/karakeep:reddit-pr2944

It's the standard all-in-one image, just built from this PR branch. Swap your existing karakeep aio image tag for that one in your compose and keep all your env/volumes the same.

A couple of caveats:

  • Unofficial, not for production — it just tracks this PR and I won't be keeping it updated. Once this merges, switch back to an official release.
  • amd64 only for now. If you're on arm64 (Pi, etc.) let me know and I'll build that too.

Note that AI tagging still needs your usual inference config; the fix itself just restores Reddit title/body/comments. Let me know how it goes!

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.

[Crawler] Reddit crawling is now getting blocked

2 participants