---
title: "Redirects — Safe Fetch for Laravel | Roundly"
description: "Redirects are off by default; when on, every hop is vetted and pinned, with a hop limit, no downgrade, credentials and cookies kept per origin."
url: https://roundly-consulting.com/open-source/docs/safe-fetch-for-laravel/redirects
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)

# Redirects

Off by default: a redirect is returned to the caller unfollowed. Turn them on per call, or in the config (redirects.enabled):

```php
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. |

```php
$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);                        // true
```

## When 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 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)
