web_fetch does a plain HTTP GET and extracts readable content (HTML to markdown or text). It does not execute JavaScript. For JS-heavy sites or login-protected pages, use the Web Browser instead.

Quick start

Enabled by default, no configuration needed:

await web_fetch({ url: "https://example.com/article" });

Tool parameters

url string

URL to fetch. http(s) only.

extractMode 'markdown' | 'text'

Output format after main-content extraction.

maxChars number

Truncate output to this many characters. Clamped to tools.web.fetch.maxCharsCap.

Result

web_fetch returns a closed structured result with these fields:

length is the wrapped text length. rawLength is the extracted content length before external-content wrapping.

text, title, and warning share maxChars, including their content wrappers. Titles and warnings retain at most 256 characters each, with a combined wrapped allowance of at most 512 characters and half of maxChars. Warnings take priority over titles; unused metadata space stays available to the body. truncated is also true when metadata is shortened or omitted; metadata-only truncation does not create a body spill file.

JSON framing and protocol metadata are additional overhead. contentType, extractor, and fetchedAt are capped at 256, 128, and 64 characters respectively. URLs stay whole for follow-up requests: a returned redirect/provider URL longer than 2,048 characters falls back to the requested url and marks the result truncated. The caller's requested URL is preserved.

How it works

Fetch

Sends an HTTP GET with a Chrome-like User-Agent and `Accept-Language`
header. Blocks private/internal hostnames and re-checks redirects.

Extract

Runs Readability (main-content extraction) on the HTML response.

Fallback (optional)

If Readability fails and a fetch provider is available, retries through
that provider (for example Firecrawl's bot-circumvention mode).

Cache

Results are cached for 15 minutes (configurable) to reduce repeated
fetches of the same URL.

Set tools.web.fetch.cacheTtlMinutes: 0 to bypass Apus's fetch cache for both reads and writes. A positive value limits reuse by the current request's TTL; cached entries still expire at their original deadline. Provider-side caching, such as Firecrawl's maxAgeMs, is configured separately.

Progress updates

web_fetch emits a public progress line only when the fetch is still pending after five seconds:

Fetching page content...

Fast cache hits and quick network responses finish before the timer fires, so they never show a progress line. Canceling the call clears the timer. The progress line is channel UI state only and never contains fetched page content.

Apus passes cancellation to fallback providers. Providers that honor the signal can stop their requests; core rejects late results even when a provider ignores cancellation. Already-canceled calls reject even when a cached result exists. If cancellation occurs during fetching, fallback processing, or connection cleanup, Apus rejects the call instead of returning success or adding a result to the fetch cache.

Config

{
  tools: {
    web: {
      fetch: {
        enabled: true, // default: true
        provider: "firecrawl", // optional; omit for auto-detect
        maxChars: 20000, // default output chars; capped by maxCharsCap
        maxCharsCap: 20000, // hard cap for maxChars param
        maxResponseBytes: 750000, // max download size before truncation (32000-10000000)
        timeoutSeconds: 30,
        cacheTtlMinutes: 15,
        maxRedirects: 3,
        useTrustedEnvProxy: false, // let a trusted HTTP(S) env proxy resolve DNS
        readability: true, // use Readability extraction
        userAgent: "Mozilla/5.0 ...", // override User-Agent
        headers: {
          // optional; every value is treated as sensitive
          "X-Routing-Target": "staging",
        },
        ssrfPolicy: {
          dangerouslyAllowPrivateNetwork: false, // broad private-network opt-in; keep false by default
          allowedHostnames: ["internal.example"], // narrow exact host exception
          allowRfc2544BenchmarkRange: true, // opt-in for trusted fake-IP proxies using 198.18.0.0/15
          allowIpv6UniqueLocalRange: true, // opt-in for trusted fake-IP proxies using fc00::/7
        },
      },
    },
  },
}

Firecrawl fallback

If Readability extraction fails, web_fetch can fall back to Firecrawl for bot-circumvention and better extraction:

{
  tools: {
    web: {
      fetch: {
        provider: "firecrawl", // optional; omit for auto-detect from available credentials
      },
    },
  },
  plugins: {
    entries: {
      firecrawl: {
        enabled: true,
        config: {
          webFetch: {
            // apiKey: "fc-...", // optional; omit for keyless starter access
            baseUrl: "https://api.firecrawl.dev",
            onlyMainContent: true,
            maxAgeMs: 172800000, // cache duration (2 days)
            timeoutSeconds: 60,
          },
        },
      },
    },
  },
}

plugins.entries.firecrawl.config.webFetch.apiKey is optional and supports SecretRef objects. Legacy tools.web.fetch.firecrawl.* config auto-migrates to plugins.entries.firecrawl.config.webFetch via apus doctor --fix.

If you configure a Firecrawl API-key SecretRef and it is unresolved with no FIRECRAWL_API_KEY env fallback, gateway startup fails fast.

Firecrawl baseUrl overrides are locked down: hosted traffic uses https://api.firecrawl.dev; self-hosted overrides must target private or internal endpoints, and http:// is accepted only for those private targets.

Current runtime behavior:

Custom request headers

Set tools.web.fetch.headers when your deployment needs extra request metadata on outbound fetches, such as a routing or service-injection header that steers traffic to a gateway you control.

{
  tools: {
    web: {
      fetch: {
        headers: {
          "X-Routing-Target": "${WEB_FETCH_ROUTING_TARGET}",
        },
      },
    },
  },
}

Every configured value is treated as sensitive and redacted from exposed config and debug captures. The headers are still sent to every initial URL web_fetch requests, and the model chooses that URL. Configure credential headers only when that is the intended trust boundary.

Behavior worth knowing:

Trusted env proxy

If your deployment requires web_fetch to go through a trusted outbound HTTP(S) proxy, set tools.web.fetch.useTrustedEnvProxy: true.

In this mode, Apus still applies hostname-based SSRF checks before sending the request, but it lets the proxy resolve DNS instead of doing local DNS pinning. Enable this only when the proxy is operator-controlled and enforces outbound policy after DNS resolution.

If no HTTP(S) proxy env var is configured, or the target host is excluded by NO_PROXY, web_fetch falls back to the normal strict path with local DNS pinning. Hostname matching ignores case and a single trailing DNS dot in either the URL or the NO_PROXY entry.

Limits and safety

Tool profiles

If you use tool profiles or allowlists, add web_fetch or group:web:

{
  tools: {
    allow: ["web_fetch"],
    // or: allow: ["group:web"]  (includes web_fetch, web_search, and x_search)
  },
}

Related