# Fetch Routing

How `Fetch` chooses between a headless and a headed browser, and the only case in which it tries twice.

## Decision flow

```mermaid
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.
