All packages
Safe Fetch for Laravel
Redirects
Off by default: a redirect is returned to the caller unfollowed. Turn them on per call, or in the config (redirects.enabled):
use RoundlyConsulting\SafeFetch\DataTransferObjects\RedirectPolicy;
Http::safe(SafeFetch::policy()->followRedirects())->post($url, $payload); // 307/308, ≤ 3 hops
Http::safe(SafeFetch::policy()->redirects(RedirectPolicy::methodPreserving(5)))->post($url, $payload);
Http::safe(SafeFetch::policy()->redirects(RedirectPolicy::methodPreserving(3, forwardHeaders: ['webhook-signature'])))
->post($url, $payload);RedirectPolicy
Immutable — every wither returns a copy with one setting changed and keeps the others:
| Field | Default | Meaning |
|---|---|---|
enabled | false | Follow redirects at all. |
max | 3 | Hops followed at most, 0–10 → too_many_redirects. |
statuses | null | null: 307 and 308; otherwise a non-empty subset of 301, 302, 303, 307, 308. |
allowDowngrade | false | https → http (the policy must allow http too) → otherwise redirect_downgrade. |
forwardHeaders | [] | Header names, lowercased and validated, also sent to another origin. |
statusesGiven | — | Whether statuses were given or defaulted — fetch() and download() widen a defaulted set to all five. |
| Method | Does |
|---|---|
RedirectPolicy::none() | Redirects returned to the caller, never followed. |
RedirectPolicy::methodPreserving(int $max = 3, array $forwardHeaders = []) | Follow 307 and 308 (method and body kept), at most $max hops, forwarding the named headers to another origin. |
enabled(bool $enabled = true, ?int $max = null) | The same policy, followed or not. |
withStatuses(array $statuses) | Follow exactly these statuses. |
forwardHeaders(array $headers) | Replaces the forwarded list. Credentials never go: name only headers safe to repeat. |
allowingDowngrade(bool $allow = true) | Follow https → http or not. |
max(int $max) | At most $max hops, 0–10. |
follows(int $status) | Whether a $status answer with a Location is followed. |
$redirects = RedirectPolicy::methodPreserving(3)
->withStatuses([301, 302, 303, 307, 308]) // follow all five
->forwardHeaders(['webhook-signature']) // replaces the list
->max(5); // 0–10
RedirectPolicy::none(); // returned unfollowed — the default
$redirects->follows(302); // trueWhen redirects are on
- Which statuses: 307 and 308 by default (method and body kept). A policy may name any subset of 301, 302, 303, 307 and 308; fetch() and download() (GET only) follow all five when the policy names none.
- 301 and 302 after a POST, and 303 after anything but HEAD, switch to GET and drop the body and its Content-* headers. Every other followed redirect keeps the method; one that sends a body again rewinds it first, and a body that cannot be rewound leaves that redirect returned unfollowed.
- Every hop is vetted and pinned again, exactly like the first; a refusal stops the chain.
- At most max hops (0–10, default 3) → too_many_redirects.
- No https → http unless allowDowngrade (and the policy allows http) → redirect_downgrade.
- One time budget covers every hop.
- Location is resolved by RFC 3986 §5.2 against the rebuilt URL (SafeFetch::urls()->resolveRedirect()); a relative one with a backslash or a control character is refused (invalid_url).
- A followed redirect is judged by its status and Location alone — the transfer stops before its body.
- Headers to the first origin only. A hop to another origin (scheme, host or port) carries only Accept, Accept-Encoding, Accept-Language, Content-Type, Content-Length, Content-Encoding, User-Agent and the policy’s forwardHeaders. The auth option, curl’s HTTPAUTH / USERPWD (digest, NTLM) and Guzzle’s digest state are dropped too (CVE-2022-31090), and stay dropped even if a later hop returns to the first origin.
- Cookies per hop: each hop sends the jar’s cookies for its own URL and each answer stores its Set-Cookie against the request that got it — a site the request was redirected to cannot set cookies for the first one. withOptions(['cookies' => false]) turns cookies off.
- Laravel’s recorder records every hop, so Http::assertSentCount() counts hops.
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 cryptoBy 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.