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

Events\DestinationRefused is dispatched for every refusal — at vetting (every hop, allows(), precheck() and the rule included) and when a request connected outside its vetted set (cannot_pin):

final readonly class DestinationRefused
{
    public Reason $reason;
    public string $host;          // canonical; '' when the URL had none the guard could read
    public ?string $blockedBy;    // for blocked: hostname, blocked_host, app_host, blocked_range, private_address
    public string $via;           // vet, allows, precheck, request, fetch, download
}

It never carries the URL (it can be a capability URL) and never a resolved address. via says where the URL was judged:

viaMeaning
vetSafeFetch::vet().
allowsSafeFetch::allows(), the SafeUrl rule and safe_url.
precheckSafeFetch::precheck() and SafeUrl::withoutLookup().
requestA hop of a guarded request (Http::safe(), guard(), http()).
fetch · downloadA hop of SafeFetch::fetch() or SafeFetch::download().

Refusals are what an SSRF attempt looks like — worth a listener that alerts, and one that can skip what a form refused:

use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\Log;
use RoundlyConsulting\SafeFetch\Events\DestinationRefused;

Event::listen(function (DestinationRefused $event): void {
    if (! in_array($event->via, ['allows', 'precheck'], true)) {
        Log::warning('Outbound request refused', ['reason' => $event->reason->value, 'host' => $event->host, 'rule' => $event->blockedBy, 'via' => $event->via]);
    }
});

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.