# zidane/tools/fetch-url

> The opt-in `fetch_url` tool, with an SSRF guard on every redirect hop.

Import: `import { … } from 'zidane/tools/fetch-url'`. Source: [src/tools/fetch-url.ts](https://github.com/Tahul/zidane/blob/main/src/tools/fetch-url.ts).

Use `fetchUrl` when the agent reads a web page or calls an HTTP API. It accepts only `http` and `https` URLs and turns HTML into plain text. On every redirect hop, it refuses loopback, private, link-local and cloud-metadata addresses. Limit it to certain hosts with `behavior.fetchUrlAllowHosts`. The entry also exports the guard helpers (`fetchWithSsrfGuard`, `isBlockedAddress`, `isHostAllowed`) for your own network code.

```ts
import { createAgent } from 'zidane'
import { anthropic } from 'zidane/providers/anthropic'
import { fetchUrl } from 'zidane/tools/fetch-url'

const agent = createAgent({
  provider: anthropic(),
  tools: { fetch_url: fetchUrl },
  behavior: { fetchUrlAllowHosts: ['example.com'] },
})

await agent.run({ prompt: 'Fetch https://example.com and summarize it.' })
```

## Functions

### `clearFetchUrlCache`

Function.

Test seam — drop all cached entries.

```ts
function clearFetchUrlCache(): void
```

Source: [src/tools/fetch-url.ts:567](https://github.com/Tahul/zidane/blob/main/src/tools/fetch-url.ts#L567)

### `fetchWithSsrfGuard`

Function.

Fetches a URL, following redirects and checking every target host against the SSRF blocklist before connecting. Throws on a blocked host or too many redirects.

Walks up to `MAX_REDIRECTS` hops and re-validates the target host of
each 3xx response before following it.

```ts
function fetchWithSsrfGuard(
  startUrl: URL,
  headers: Record<string, string>,
  signal: AbortSignal,
  maxBufferBytes: number,
  deps?: FetchSsrfDeps,
  allowHosts?: readonly string[],
): Promise<PinnedResponse>
```

**Parameters**

- `startUrl` (`URL`): URL to fetch.
- `headers` (`Record<string, string>`): Request headers.
- `signal` (`AbortSignal`): Aborts the request.
- `maxBufferBytes` (`number`): Body bytes to keep per response.
- `deps?` (`FetchSsrfDeps`, default `{}`): Test seam for the resolver and the request function.
- `allowHosts?` (`readonly string[]`): Allowed host names. Empty or `undefined` allows every host.

**Returns** `Promise<PinnedResponse>`

Source: [src/tools/fetch-url.ts:433](https://github.com/Tahul/zidane/blob/main/src/tools/fetch-url.ts#L433)

### `isBlockedAddress`

Function.

Tells whether an IP address is in a range the SSRF guard refuses: loopback, private, link-local, multicast and similar. Anything that is not an IP literal is refused too.

```ts
function isBlockedAddress(ip: string): boolean
```

**Parameters**

- `ip` (`string`): IPv4 or IPv6 address literal.

**Returns** `boolean`

Source: [src/tools/fetch-url.ts:120](https://github.com/Tahul/zidane/blob/main/src/tools/fetch-url.ts#L120)

### `isHostAllowed`

Function.

Host-suffix allowlist check. An empty / undefined list means "no allowlist"
→ every host passes (the SSRF blocklist is the only gate). Otherwise the
host must equal, or be a subdomain of, one of the entries. Subdomain match
is on a dot boundary so `example.com` matches `docs.example.com` but not
`notexample.com`.

```ts
function isHostAllowed(
  hostname: string,
  allowHosts: readonly string[] | undefined,
): boolean
```

**Parameters**

- `hostname` (`string`): Host to check.
- `allowHosts` (`readonly string[] | undefined`): Allowed host names. Empty or `undefined` allows every host.

**Returns** `boolean`

Source: [src/tools/fetch-url.ts:168](https://github.com/Tahul/zidane/blob/main/src/tools/fetch-url.ts#L168)

### `pinnedRequest`

Function.

Sends one GET request to a fixed IP address while keeping the original host name for the `Host` header and TLS server name. Resolves with the status, body and any redirect location.

Connects to `pinnedIp` directly (bypassing Node/Bun's own DNS resolution).
This is the concrete defense against DNS rebinding — see the file-level
docstring. Reads up to `maxBufferBytes` of body and then forcibly closes the
connection.

```ts
function pinnedRequest(
  url: URL,
  pinnedIp: string,
  family: 4 | 6,
  headers: Record<string, string>,
  signal: AbortSignal,
  maxBufferBytes: number,
): Promise<PinnedResponse>
```

**Parameters**

- `url` (`URL`): URL to request.
- `pinnedIp` (`string`): IP address to connect to.
- `family` (`4 | 6`): IP version of `pinnedIp`.
- `headers` (`Record<string, string>`): Request headers.
- `signal` (`AbortSignal`): Aborts the request.
- `maxBufferBytes` (`number`): Body bytes to keep. The rest is dropped and `truncated` is set.

**Returns** `Promise<PinnedResponse>`

Source: [src/tools/fetch-url.ts:276](https://github.com/Tahul/zidane/blob/main/src/tools/fetch-url.ts#L276)

## Constants

### `__fetchCacheTestApi`

Constant.

Test seam — exercise the cache get/set/eviction in isolation without
standing up the full SSRF + HTTP path. Not part of the public tool surface;
`execute` uses the module-private functions directly.

```ts
const __fetchCacheTestApi: { bytes: () => number; get: (key: string) => string | null; key: (url: string, cap: number, allowHosts?: readonly string[]) => string; set: (key: string, output: string, ttlMs: number) => void; size: () => number }
```

Source: [src/tools/fetch-url.ts:577](https://github.com/Tahul/zidane/blob/main/src/tools/fetch-url.ts#L577)

### `fetchUrl`

Constant.

The `fetch_url` tool. Fetches an HTTP or HTTPS URL and returns its body as text, with an SSRF guard on every hop.

```ts
const fetchUrl: ToolDef
```

Source: [src/tools/fetch-url.ts:588](https://github.com/Tahul/zidane/blob/main/src/tools/fetch-url.ts#L588)

## Interfaces

### `FetchSsrfDeps`

Interface.

Test seams for [`fetchWithSsrfGuard`](https://zidane.si/docs/reference/tools-fetch-url.md#fetchWithSsrfGuard) — production callers use the
defaults. Hosts that want to restrict egress should set
`behavior.fetchUrlAllowHosts` rather than reaching into these.

```ts
interface FetchSsrfDeps
```

**Properties**

- `requestImpl?` (`(url: URL, pinnedIp: string, family: 4 | 6, headers: Record<string, string>, signal: AbortSignal, maxBufferBytes: number) => Promise<PinnedResponse>`): Execute a single pinned HTTP request. Defaults to [`pinnedRequest`](https://zidane.si/docs/reference/tools-fetch-url.md#pinnedRequest).
- `resolver?` (`(hostname: string) => Promise<ResolvedAddress | ResolvedAddress[]>`): Resolve + validate a hostname, in connect-preference order. Defaults to `resolveAndCheck`.
  A single address is also accepted, as a one-element list.

Source: [src/tools/fetch-url.ts:410](https://github.com/Tahul/zidane/blob/main/src/tools/fetch-url.ts#L410)

### `ResolvedAddress`

Interface.

One resolved address of a host.

```ts
interface ResolvedAddress
```

**Properties**

- `address` (`string`): IP address literal.
- `family` (`4 | 6`): IP version of `address`.

Source: [src/tools/fetch-url.ts:183](https://github.com/Tahul/zidane/blob/main/src/tools/fetch-url.ts#L183)
