NewWe open-sourced 50+ Laravel packages
Custom AI apps, agents and automation — Roundly ConsultingRoundly
All packages
Safe Fetch for Laravel

The SafeFetch facade

Everything goes through one facade, RoundlyConsulting\SafeFetch\Facades\SafeFetch (global alias SafeFetch), sugar over the SafeFetchManager singleton, plus the Http::safe() macro on Laravel’s HTTP client:

use Illuminate\Support\Facades\Http;
use RoundlyConsulting\SafeFetch\Facades\SafeFetch;

Http::safe()->withToken($token)->post($url, $payload);          // guard any Laravel request
SafeFetch::guard(Http::withToken($token), $policy)->post($url);  // the same, without the macro
SafeFetch::http($policy)->get($url);                             // a new guarded request

$vetted = SafeFetch::vet($url);              // VettedDestination, or DestinationRejected / DestinationUnavailable
SafeFetch::allows($url);                     // bool — never throws a verdict
SafeFetch::precheck($url);                   // CanonicalUrl — every rule except the DNS lookup

$page = SafeFetch::fetch($url, maxBytes: 1_000_000);              // a capped GET into memory
$file = SafeFetch::download($url, $path, maxBytes: 50_000_000);   // the same GET, streamed to a file
SafeFetch::truncated($response);             // was a guarded response cut at maxResponseBytes?

SafeFetch::policy();                         // the configured DestinationPolicy, to tighten or loosen per call
SafeFetch::networks()->isPrivate('169.254.169.254');                    // true — cloud metadata
SafeFetch::urls()->canonicalise('HTTPS://Example.COM.:443/a?b#frag');   // CanonicalUrl: https://example.com/a?b

Manager methods

MethodReturnsDoes
vet(string $url, ?DestinationPolicy $policy = null)VettedDestinationJudge the URL under the policy (the configured one by default): its rebuilt URL and every address it resolved to. Throws DestinationRejected, or DestinationUnavailable when the host does not resolve.
allows(string $url, ?DestinationPolicy $policy = null)boolWhether the policy lets the URL through. Never throws a verdict.
precheck(string $url, ?DestinationPolicy $policy = null)CanonicalUrlEvery rule except the DNS lookup — for save time and forms. Throws DestinationRejected.
http(?DestinationPolicy $policy = null)PendingRequestA new guarded request from Laravel’s HTTP client factory — Http::safe() without the macro.
guard(PendingRequest $request, ?DestinationPolicy $policy = null)PendingRequestGuard a request you built (Http::withToken($t)) — vetted, pinned and checked on every hop. What Http::safe() calls.
fetch(string $url, ?int $maxBytes = null, ?int $timeout = null, ?string $accept = null, array $headers = [], ?bool $followRedirects = null, ?DestinationPolicy $policy = null)FetchedResponseA guarded GET into memory, its body capped. Throws DestinationRejected, DestinationUnavailable or FetchFailed.
download(string $url, string $path, ?int $maxBytes = null, ?int $timeout = null, ?string $accept = null, array $headers = [], ?bool $followRedirects = null, ?DestinationPolicy $policy = null)DownloadedFileThe same GET streamed to the file at $path; the directory must exist.
truncated(Response|ResponseInterface $response)boolWhether a guarded response was cut at the policy’s maxResponseBytes. Stateless: reads the Safe-Fetch-Truncated header.
policy()DestinationPolicyThe configured default policy, to tighten or loosen per call.
networks()Handles\NetworksThe blocked-network table: isPrivate(), isBlockedHostname(), blockedRanges().
urls()Handles\UrlsThe guard’s URL parser and redirect resolver: canonicalise(), resolveRedirect().
SafeFetch::fake(array $dns = [])Testing\SafeFetchFakeFacade only — the recording fake (see Testing).
SafeFetch::fakeDns(array $records = [])Testing\FakeHostResolverFacade only — answer the guard’s lookups from a table.

Once SafeFetch::fake() is installed, every assertion of the fake is callable on the facade too — see Testing. vetHop() is public on the manager but internal: it is the guard middleware’s per-hop call, so the fake sees every hop. SafeFetchManager::TRUNCATED_HEADER ('Safe-Fetch-Truncated') names the truncation header.

The Http::safe() macro

PendingRequest::safe(?DestinationPolicy $policy = null): PendingRequest gives Http::safe(), Http::withToken($t)->safe() and $pool->safe(). Another package registering a safe macro after this one would replace it silently, so security-critical code calls SafeFetch::guard() or SafeFetch::http() instead.

Data objects

In RoundlyConsulting\SafeFetch\DataTransferObjects, all final readonly:

ClassFields and methods
DestinationPolicyThe rules on top of the blocked-network table — see Destination policy.
RedirectPolicyenabled, max, statuses, allowDowngrade, forwardHeaders, statusesGiven; none(), methodPreserving(), enabled(), withStatuses(), forwardHeaders(), allowingDowngrade(), max(), follows().
CanonicalUrlscheme, host, port, ipLiteral, pathAndQuery; toString(), origin().
VettedDestinationurl (rebuilt), scheme, host, port, ipLiteral, addresses (every vetted address); origin(), resolveEntry(): ?string, pinningOptions(): array (cannot_pin without curl), connectsTo(string $address): bool.
FetchOptionsmaxBytes, timeout, accept, headers, followRedirects — null means the configured default.
FetchedResponsestatus, body, contentType, url, headers; header(string $name): ?string, json(?string $key = null, mixed $default = null): mixed.
DownloadedFilestatus, path, bytes, contentType, url, headers; header(string $name): ?string.

Other public types: Enums\Reason and the exceptions (Verdicts and exceptions), Contracts\HostResolver and Support\SystemHostResolver, Support\PrivateNetworks (URLs and blocked networks), Rules\SafeUrl, Commands\CheckCommand, Events\DestinationRefused, and Testing\SafeFetchFake, Testing\FakeHostResolver and Testing\RecordedVet. Everything else under Support\ and Http\ is internal.

Show your open-source love

This package is free and MIT-licensed. If it saves you time, a one-off donation or a Patreon membership keeps it maintained, tested and documented.

More ways to support, including crypto

By donating, you agree to our donation terms.

Want this built into your product?

We integrate our packages into custom Laravel and AI builds. Tell us what you're working on and we'll reply within 48 hours.