Fetch Routing
How Fetch chooses between a headless and a headed browser, and the only case in which it tries twice.
Decision flow
graph TB
A[Fetch] --> B{Option.Headless}
B -->|true| C[headless, no retry]
B -->|false| D{session domain and GUI}
D -->|yes| E[headed, no retry]
D -->|no| F[headless first]
F --> G{403 / 429 / 503}
G -->|no| H[return result]
G -->|yes| I{GUI available}
I -->|no| H
I -->|yes| J[one headed retry]
| Branch | Condition | Attempts |
|---|---|---|
| Forced headless | Option.Headless is true |
1, headless |
| Session domain | Host is on the session list and a GUI is available | 1, headed |
| Default | Everything else | Headless, then one headed retry only when blocked |
What counts as blocked
Only three status codes: 403, 429, 503, read either from an *Error or from Result.Status. 404, 204 and no article extracted do not trigger a retry, because they mean the page is missing or extraction failed, and a different browser mode changes neither. Errors that are not *Error (launch failures, timeouts) are returned without a retry.
A challenge page title such as just a moment is turned into a 403 before this check, which is how Cloudflare-style interstitials reach the headed retry.
Session domains
Social and community sites that rarely render useful content headless go straight to a headed browser when a GUI exists. The list is embedded from core/embed/session_domains.json, and a host matches when it equals an entry or ends with . plus the entry:
facebook.com, fb.com, fb.watch, twitter.com, x.com, instagram.com, linkedin.com, threads.net, tiktok.com, pinterest.com, snapchat.com, weibo.com, weibo.cn, xiaohongshu.com, xhs.link, douyin.com, bilibili.com, zhihu.com, discord.com, telegram.org, t.me, vk.com, quora.com
Without a GUI these hosts follow the default branch.
GUI detection
hasDisplay() is always true on macOS; on Linux it is true when DISPLAY or WAYLAND_DISPLAY is non-empty. Containers and headless servers therefore never attempt to launch a headed browser. Headed windows are placed at -32000,-32000, off screen.