no404 / laravel
no404 for Laravel: real server-side 301/302 redirects for your application's 404s, matched against your live catalogue.
Requires
- php: ^8.2
- composer-runtime-api: ^2.2
- ext-json: *
- guzzlehttp/guzzle: ^7.8 || ^8.0
- illuminate/cache: ^12.0 || ^13.0
- illuminate/console: ^12.0 || ^13.0
- illuminate/contracts: ^12.0 || ^13.0
- illuminate/http: ^12.0 || ^13.0
- illuminate/support: ^12.0 || ^13.0
Requires (Dev)
- larastan/larastan: ^3.0
- laravel/pint: ^1.18
- orchestra/testbench: ^10.0 || ^11.0
- phpunit/phpunit: ^11.5 || ^12.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
Turns your application's 404s — deleted products, renamed categories, old campaign links — into real server-side 301/302 redirects to the closest live page on your site.
What no404 is
no404 watches the 404 traffic of your site and matches every dead address against your live catalogue (read from your sitemap). You see which addresses break, where their visitors come from, and where they were sent.
What this package does
When your Laravel application answers 404, the package asks no404 for the closest live page and sends the visitor there with a real HTTP redirect — the thing a JavaScript snippet on the 404 page cannot do (search engines still see a 404 there).
- Every kind of 404: no matching route,
abort(404), a missing model (findOrFail, route-model binding),Route::fallback(), a custom 404 renderer. - 301 or 302 is decided per site in the no404 dashboard (how sure a match must be before it becomes permanent). Your own redirects defined in the dashboard are always 301.
- Fail-open: if no404 is slow or unreachable, the visitor gets your normal 404 page after at most 1.5 seconds, and lookups pause for a minute.
- Quota-friendly: answers are cached per path — misses too — and static files,
/storage,/livewire,/api, admin panels and similar paths are never sent. - Normal pages cost nothing: the package only looks at responses that are already 404.
Requirements
| Laravel | 12 or 13 |
| PHP | 8.2+ (Laravel 13 needs 8.3+) |
| Cache | any store that survives between requests (redis, database, file, memcached…) |
| no404 | an account and a site with its API key |
Laravel 11 reached end of life on 12 March 2026 and is not supported: Composer will refuse to install the package there. Upgrade to Laravel 12 or 13 first.
Installation
composer require no404/laravel
Add the API key from the no404 dashboard (Site → Integration) to .env:
NO404_API_KEY=your-api-key
Check the connection:
php artisan no404:test
That is all — the middleware registers itself. Then press Test in the dashboard's Integration tab: it opens a random missing address on your site and confirms that the 404 reached no404 through this package.
Make sure APP_URL is your site's real address: redirect targets are only accepted on
that host (and its www. twin).
Configuration
Everything works with the defaults. To change them, publish the config file:
php artisan vendor:publish --tag=no404-config
.env |
Default | |
|---|---|---|
NO404_API_KEY |
— | Required. |
NO404_ENABLED |
true |
Keep the package installed but inert. |
NO404_CACHE_STORE |
default store | Use a persistent store; with array every 404 uses quota. |
NO404_CACHE_TTL |
3600 |
Seconds an answer is remembered (60 – 604800). |
NO404_FORCE_301 |
false |
Send every match as 301, ignoring the dashboard threshold. |
NO404_LOG_CHANNEL |
default channel | Where configuration problems are logged (at most once an hour). |
NO404_DEBUG |
false |
Adds X-No404-Source, X-No404-Score, X-No404-Skip headers. |
In config/no404.php you can also add allowed redirect hosts, extra path prefixes and
extensions to skip, and turn visitor forwarding off.
Registering the middleware yourself
Set 'middleware' => ['register' => false] in config/no404.php and append it in
bootstrap/app.php:
->withMiddleware(function (Middleware $middleware) { $middleware->append(\No404\Laravel\Http\Middleware\RedirectNotFound::class); })
Keep it global (not in a route group): when no route matches, route middleware never runs.
Commands
| Command | |
|---|---|
php artisan no404:test |
Asks no404 once and explains the answer (key, plan, network). --path=/old-address resolves a real address (uses one lookup of your quota). |
php artisan no404:status |
The local configuration and anything that needs attention. No network request. |
php artisan no404:flush |
Resumes lookups paused after an error (once you have fixed the key or the plan). |
php artisan about |
Includes a no404 section. |
Hooks and events
Register hooks in a service provider's boot() method:
use Illuminate\Http\Request; use No404\Laravel\Facades\No404; use No404\Laravel\RedirectDecision; // Never ask about some requests. No404::skipWhen(fn (Request $request, string $path) => str_starts_with($path, '/members')); // Answer from your own data first — not cached, no quota used. No404::resolveUsing(fn (string $path) => LegacyUrl::where('path', $path)->first()?->toRedirect()); // returns ['redirect' => '/new-path', 'status' => 301] or null // Change or cancel a redirect (return null to keep the 404). No404::beforeRedirect(fn (RedirectDecision $decision) => $decision->withStatus(302)); // Other domains of the same site that targets may point to. No404::allowedHosts(['shop.example.org']); // Replace the visitor data (return null to send none). No404::visitorUsing(fn (Request $request, array $visitor) => $visitor);
Events for your own telemetry: No404\Laravel\Events\RedirectResolved,
LookupFailed, LookupSkipped.
What is sent to no404
For each 404 that is not answered from the cache:
- the path, without the query string;
- the
Refererheader, if any; - the ad network category (
google,microsoft,meta,other) when the URL carries a paid-traffic marker — never the click ID itself; - the API key, in the
Authorizationheader (never in the URL); User-Agent: no404-laravel/<version>; <APP_URL>;- about the visitor: the IP truncated to its network (
203.0.113.0,/48for IPv6), a hash of the IP keyed with a secret derived from yourAPP_KEY(no404 cannot turn it back into an address or match it across sites), the user agent, and the country fromCF-IPCountry. Private addresses are never sent.
Never sent: the full IP, cookies, session data, form data, user IDs, APP_KEY.
The visitor IP comes from $request->ip(), so it honours your
trusted proxies setup.
Known limits
- A page cache in front of Laravel (a CDN caching 404s, full-page cache packages) can answer before the application runs — then the package never sees the 404.
- A 404 turned into a 200 ("soft 404") is not seen either. Keep the 404 status.
- A global middleware that redirects on its own (locale prefixes) runs first, so a missing address can take two hops: its redirect, then no404's.
- Requests that expect JSON, Livewire updates and JSON 404 responses are left alone.
License
MIT — see LICENSE.