Skip to content

zidane/tools/fetch-url

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

import { … } from 'zidane/tools/fetch-url'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.

TypeScript
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#

clearFetchUrlCacheFunction#

Test seam — drop all cached entries.

TypeScript
function clearFetchUrlCache(): void

src/tools/fetch-url.ts:567

fetchWithSsrfGuardFunction#

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.

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

URL to fetch.

headersRecord<string, string>

Request headers.

signalAbortSignal

Aborts the request.

maxBufferBytesnumber

Body bytes to keep per response.

deps?FetchSsrfDeps{}

Test seam for the resolver and the request function.

allowHosts?readonly string[]

Allowed host names. Empty or undefined allows every host.

Promise<PinnedResponse>

src/tools/fetch-url.ts:433

isBlockedAddressFunction#

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.

TypeScript
function isBlockedAddress(ip: string): boolean
Parameters
NameTypeDescription
ipstring

IPv4 or IPv6 address literal.

boolean

src/tools/fetch-url.ts:120

isHostAllowedFunction#

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.

TypeScript
function isHostAllowed(
  hostname: string,
  allowHosts: readonly string[] | undefined,
): boolean
Parameters
NameTypeDescription
hostnamestring

Host to check.

allowHostsreadonly string[] | undefined

Allowed host names. Empty or undefined allows every host.

boolean

src/tools/fetch-url.ts:168

pinnedRequestFunction#

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.

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

URL to request.

pinnedIpstring

IP address to connect to.

family4 | 6

IP version of pinnedIp.

headersRecord<string, string>

Request headers.

signalAbortSignal

Aborts the request.

maxBufferBytesnumber

Body bytes to keep. The rest is dropped and truncated is set.

Promise<PinnedResponse>

src/tools/fetch-url.ts:276

Constants#

__fetchCacheTestApiConstant#

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.

TypeScript
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 }

src/tools/fetch-url.ts:577

fetchUrlConstant#

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

TypeScript
const fetchUrl: ToolDef

src/tools/fetch-url.ts:588

Interfaces#

FetchSsrfDepsInterface#

Test seams for fetchWithSsrfGuard — production callers use the defaults. Hosts that want to restrict egress should set behavior.fetchUrlAllowHosts rather than reaching into these.

TypeScript
interface FetchSsrfDeps
FetchSsrfDeps properties
NameTypeDescription
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.

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.

src/tools/fetch-url.ts:410

ResolvedAddressInterface#

One resolved address of a host.

TypeScript
interface ResolvedAddress
ResolvedAddress properties
NameTypeDescription
addressstring

IP address literal.

family4 | 6

IP version of address.

src/tools/fetch-url.ts:183