aimode.news
Published on

fanbox: post.info 403 — ofetch blocked, puppeteer needed?

Authors

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/`) leaks

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

fanbox: post.info 403 — ofetch blocked, puppeteer needed? | aimode.news