---
title: "Use in your own package — Safe Fetch for Laravel | Roundly"
description: "Recipes for packages that fetch their users’ URLs — a policy that drops the host’s carve-outs, the manager, throttling, retries and save-time prechecks."
url: https://roundly-consulting.com/open-source/docs/safe-fetch-for-laravel/consumer-packages
language: en
---

[All packages](https://roundly-consulting.com/open-source.md)

[Safe Fetch for Laravel](https://roundly-consulting.com/open-source/docs/safe-fetch-for-laravel.md)

# Use in your own package

Packages that send requests to URLs their own users supply — webhook senders, importers, link previews — build on Safe Fetch the same way.

## A policy of your own, on top of the host’s

Start from SafeFetch::policy() so the host’s blocks — blocked hosts and ranges, its own app hosts — still apply, and drop the host’s carve-outs: a name it trusts to be private (minio.internal), a range it opened for local development (127.0.0.1/32) and a host-wide allowlist were set up for the host’s own fetches, not for URLs your users type in:

```php
$policy = SafeFetch::policy()
    ->withoutHostAllowlist()->withoutPrivateHosts()->withoutAllowedRanges()
    ->blockAppHosts()->appHosts($appHosts)
    ->blockHosts($blockedHosts)->blockRanges($blockedRanges)->allowRanges($allowedRanges)
    ->ports($ports)->maxResponseBytes(4096)
    ->redirects(RedirectPolicy::methodPreserving($max, forwardHeaders: ['webhook-id', 'webhook-timestamp', 'webhook-signature']));
$policy = $allowedHosts === [] ? $policy : $policy->allowHosts($allowedHosts);
```

- blockAppHosts() turns the app-host block on whatever the host configured (blockAppHosts(false) turns it off).
- allowHosts() replaces the allowlist and refuses an empty list — branch on your own setting as above rather than passing a list that may be empty.
- RedirectPolicy::methodPreserving($max, forwardHeaders: \[...\]) follows 307/308 only and sends the named headers to another origin a redirect leads to; every other header and every credential stays with the first origin.

## Send through the manager, throttle first

Inject SafeFetchManager (or use the facade) and call guard() — never the Http::safe() macro, which another package could replace. Add a throttle before the guard so it runs once per call, before any DNS lookup ($throttle is any Guzzle middleware):

```php
$response = $safeFetch->guard(
    Http::withMiddleware($throttle)->connectTimeout(5)->timeout(10)->withHeaders($headers),
    $policy,
)->withBody($body, 'application/json')->post($url);
```

## Retries stop at a refusal

```php
$response = $safeFetch->guard(Http::retry(3, 100, fn (Throwable $e) => ! $e instanceof SafeFetchException), $policy)
    ->post($url, $payload);
```

## Terminal or retryable

```php
try {
    $response = $safeFetch->guard(Http::timeout(10), $policy)->post($url, $payload);
} catch (DestinationRejected $e) {
    $outcome = ['retry' => false, 'reason' => $e->reason()->value, 'rule' => $e->blockedBy()];   // never retry
} catch (DestinationUnavailable $e) {
    $outcome = ['retry' => true, 'reason' => $e->reason()->value];                              // unresolved, out of time
}
```

DestinationRejected is terminal — store reason() and blockedBy() for operators, never show the difference to the person who submitted the URL. A transport failure of a guarded request is Laravel’s own ConnectionException, whose message names the URL — do not echo it.

## A capped answer and where it came from

```php
$response = $safeFetch->guard(Http::timeout(10), $policy->maxResponseBytes(4096))->post($url, $payload);

$excerpt = $response->body();                                    // at most 4 096 bytes
$cut = $safeFetch->truncated($response);                         // true when the body was cut
$remoteAddress = $response->handlerStats()['primary_ip'] ?? null; // the vetted address it connected to
```

## Save time: every rule except the lookup

When a URL is stored rather than called (an endpoint a user registers), judge it without waiting on DNS:

```php
$request->validate(['endpoint' => ['required', 'string', SafeUrl::withoutLookup($policy)]]);
$stored = $safeFetch->precheck($request->input('endpoint'), $policy)->toString();
```

The guard judges the URL in full on every request; the testing recipes for a package are in Testing.

## 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](https://roundly-consulting.com/support-us.md)

By donating, you agree to our [donation terms](https://roundly-consulting.com/donation-terms.md).

[Support our open source work (opens in a new tab)](https://donate.stripe.com/dRmeVe8FX5PF1Qd9pXcEw00) [Join us on Patreon (opens in a new tab)](https://www.patreon.com/cw/roundly)

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

[Get a quote in 48 hours](https://roundly-consulting.com/contact.md) [Browse all packages](https://roundly-consulting.com/open-source/docs/safe-fetch-for-laravel.md)
