- Published on
fanbox: post.info 403 — ofetch blocked, puppeteer needed?
- Authors

- Name
- aimode.news
- @aimode_news
fanbox: post.info 403 — ofetch blocked, puppeteer needed? #21699
Description
Routes
/fanbox/:creator
Full routes
/fanbox/official
Related documentation
What is expected?
RSS XML feed of the creator's posts
What is actually happening?
FetchError: [GET] "https://api.fanbox.cc/post.info?postId=11609848": 403 Forbidden
(Since around 2026-04-10, api.fanbox.cc/post.info
returns 403 to ofetch
?)
Deployment information
Self-hosted
Deployment information (for self-hosted)
Docker, diygod/rsshub:chromium-bundled
(2026-04-11).
Additional info
Investigation, testing, and write-up entirely done by Claude Code (AI agent).
Human only provided the prompts.
== Suggested fix ==
In `parseItem` (lib/routes/fanbox/utils.tsx), replace the `ofetch` call to
`post.info` with a puppeteer call via the existing `lib/utils/puppeteer.ts`
utility. The other three fanbox endpoints can stay on ofetch.
(Observations below explain why ofetch can't be salvaged with a UA tweak,
and why the puppeteer call may still need an explicit `page.setUserAgent`
in WS-endpoint mode.)
== Test environment ==
Fresh `diygod/rsshub:chromium-bundled` container, 2026-04-11, NODE_ENV=production,
redis cache. All tests use public creator `official`, post id 11609848.
== Observation 1: only post.info is blocked ==
ofetch / default fetch UA, 3 runs each:
creator.get?creatorId=official -> 200 (JSON) x 3
plan.listCreator?creatorId=official -> 200 (JSON) x 3
post.listCreator?creatorId=official&limit=3 -> 200 (JSON) x 3
post.info?postId=11609848 -> 403 (HTML) x 3
The same `post.info` call was retried via ofetch / node fetch / curl with
5 different User-Agent strings (default, `config.trueUA`, Chrome 136 Win,
Chrome 139 Mac, Firefox 130), 3 runs each — all 15 attempts return 403.
UA at the ofetch layer is irrelevant.
== Observation 2: the UA filter on real browsers ==
Switching to `rebrowser-puppeteer` (the package already bundled in
`diygod/rsshub:chromium-bundled`), the `post.info` request succeeds for
every UA tested **except** two patterns:
1. UA contains the case-sensitive substring `HeadlessChrome/`
(the slash must be immediately after `HeadlessChrome`)
2. UA equals the empty string
The first pattern is exactly the literal puppeteer / chromium-headless
default UA (`Mozilla/5.0 ... HeadlessChrome/136.0.0.0 Safari/537.36`), so
naively calling `puppeteer.newPage()` without `page.setUserAgent(...)` is
also blocked. Anything else tested (any normal `Chrome/...`, `Firefox/...`,
`RSSHub/1.0...`, lowercase `headlesschrome/1.0`, `Headless` without slash,
`HeadlessChromeFoo/1.0`, etc.) is accepted.
Note for users on the plain `diygod/rsshub` image with
`PUPPETEER_WS_ENDPOINT=...browserless...`: `lib/utils/puppeteer.ts` only
injects `--user-agent=${config.ua}` into the local `puppeteer.launch(...)`
path, not into the `puppeteer.connect(...)` path used in WS-endpoint mode.
The remote browser's own default UA (often `HeadlessChrome/
through and triggers the block, so `parseItem` should still call
`page.setUserAgent(...)` itself rather than relying on the launch arg.
This is not a duplicated issue
- I have searched existing issues to ensure this bug has not already been reported