# Logging incoming HTTP requests in Laravel with a middleware

> Spatie's laravel-http-logger package adds a middleware that writes incoming requests to the Laravel log so that a failed submission can still be inspected afterwards. It is small, configurable and deliberately dumb, which means the filtering of what gets logged stays your problem.

**spatie/laravel-http-logger** — Log HTTP requests in Laravel applications

- Repository: https://github.com/spatie/laravel-http-logger
- Website: https://freek.dev/882-a-laravel-package-to-log-http-requests
- Stars: 673 · Forks: 57
- Language: PHP
- License: MIT
- Published: 2026-10-21 · Updated: 2026-10-21 · Language: en
- Canonical page: https://hysenlabs.com/projects/spatie-laravel-http-logger

## A safety net for requests that already failed

The problem this package addresses is narrow and real. When a user submits a form and something downstream throws an exception, the request that caused it is often gone: the validation error is on screen, the stack trace is in the log, and the payload the user typed is nowhere to be found. Spatie describes the middleware as a way to log incoming requests to the default log so that if anything goes wrong during a user's request, the original request data sent by that user is still accessible.

The framing in the project documentation is that this log acts as an extra safety net for critical user submissions, such as forms that generate leads. That is the right way to size the expectation. This is not an access log and it is not an APM product. It is a small middleware that records what came in, so that a support ticket about a lost lead can be answered.

The audience is therefore a Laravel developer or small team, not an operations department. There is no dashboard, no retention policy, no search. What you get is structured text in the log channel you already have, which you then query with whatever your log aggregator does. If your team already runs Laravel with a Spatie-style log setup, this fits into that world without adding infrastructure.

## The LogProfile and LogWriter split is the whole architecture

Two interfaces carry the logic, and separating them is the one design decision that explains everything else. A `LogProfile` class decides whether a request should be logged, and a `LogWriter` class does the writing. Both are configurable in the published config file.

The default profile, `LogNonGetRequests`, only logs `POST`, `PUT`, `PATCH`, `DELETE` and `QUERY` requests, and its implementation is a single method that checks the lowercased request method against that list.

```php
public function shouldLogRequest(Request $request): bool
{
   return in_array(strtolower($request->method()), ['post', 'put', 'patch', 'delete', 'query']);
}
```

The default writer, `DefaultLogWriter`, writes to the default Laravel logger, and it composes a single message from the method, the path and the request body encoded as JSON.

```php
public function logRequest(Request $request): void
{
    $method = strtoupper($request->getMethod());
    
    $uri = $request->getPathInfo();
    
    $bodyAsJson = json_encode($request->except(config('http-logger.except')));
    
    $message = "{$method} {$uri} - {$bodyAsJson}";
    
    Log::channel(config('http-logger.log_channel'))->info($message);
}
```

Three things fall out of that code. The log line contains no status code and no response body, because the writer only ever sees the request. The body is filtered through `$request->except(...)`, which is where the excluded fields list matters. And the log message is a hand-built string, not an array, so anything downstream that expects structured log payloads will need to parse it or replace the writer.

Both interfaces are yours to implement. The documentation states that a custom log profile must implement `\Spatie\HttpLogger\LogProfile`, which requires the `shouldLogRequest` method, and a custom log writer must implement `\Spatie\HttpLogger\LogWriter`, which requires `logRequest`. That is the extension point most teams will use.

## Installing the package and publishing the config

Installation is a single Composer command.

```bash
composer require spatie/laravel-http-logger
```

The config file is optional, and publishing it is a separate step with the service provider and the config tag named explicitly.

```bash
php artisan vendor:publish --provider="Spatie\HttpLogger\HttpLoggerServiceProvider" --tag="config" 
```

That command produces `config/http-logger.php`, and the published file is the place where every behaviour of the package is decided. It sets `enabled` to the result of `HTTP_LOGGER_ENABLED` with a default of true, so the middleware can be switched off from the `.env` file without touching code. It points `log_profile` at `LogNonGetRequests` and `log_writer` at `DefaultLogWriter`, and it sets `log_channel` from the `LOG_CHANNEL` environment variable with `stack` as the default and `log_level` to `info`.

Two entries in that file are the ones to read twice. The `except` array lists body fields which will never be logged, and it ships with only `password` and `password_confirmation` in it. The `sanitize_headers` array lists headers to be sanitized, and it ships empty, with the documentation suggesting Authorization, Cookie and Set-Cookie as examples of what belongs there. In other words, the defaults are deliberately minimal, and a project that accepts payments or handles tokens has work to do before it enables this in production.

## Registering the middleware globally or per route

The middleware can be attached as a global middleware or to a single route, and the project documentation gives the two registration shapes, one per Laravel generation.

For Laravel 11 and later, the middleware is appended in the `withMiddleware` closure.

```php
->withMiddleware(function (Middleware $middleware) {
    $middleware->append(\Spatie\HttpLogger\Middlewares\HttpLogger::class);
})
```

For Laravel 10 and earlier it goes into the `$middleware` array in `app/Http/Kernel.php`.

```php
// in `app/Http/Kernel.php`

protected $middleware = [
    // ...
    
    \Spatie\HttpLogger\Middlewares\HttpLogger::class
];
```

And a route can carry it directly, which is the more selective option.

```php
// in a routes file

Route::post('/submit-form', function () {
    //
})->middleware(\Spatie\HttpLogger\Middlewares\HttpLogger::class);
```

The per-route form is the better starting point for most teams, because it limits the logged surface to the endpoints you actually care about and leaves the rest of the application untouched. The global form logs every request that the default profile accepts, which on a busy application means a large volume of entries flowing into whatever channel `stack` points at. Since the package's own stated use case is critical submissions such as lead forms, applying the middleware globally goes well past that brief.

One thing the documentation does not spell out is the interaction between the global `enabled` flag and per-route registration. The flag is described as toggling logging on and off via `HTTP_LOGGER_ENABLED`, but how that interacts with a route that has the middleware attached explicitly is not described, so treat the flag as the global kill switch and verify its behavior in your own environment rather than assuming it is a no-op for selectively registered routes.

## Sanitizing headers so tokens do not land in your log

Bearer tokens in a log file are a real problem, and the package has a specific mechanism for it. You list header names in `sanitize_headers` in the config file, and the documentation gives the Authorization header as the most common example, with the note that this is how you avoid logging a JWT.

```php
// in config/http-logger.php

return [
    // ...
    
    'sanitize_headers' => [
        'Authorization'
    ],
];
```

The documented effect is precise. A header that is not on the list is written as it arrived, and a header that is on the list is written as `Authorization: "****"` instead of `Authorization: "Bearer {token}"`.

That mechanism has a boundary worth understanding. It replaces the header value, it does not exclude the header, so the set of headers you send is still visible in the log line. And the list is a fixed set of names, so a token carried in a custom header, or in the body, or in a query string, is not covered by it. The body side has its own filter, the `except` array, which removes named fields from the body entirely rather than masking them, and the defaults cover only `password` and `password_confirmation`.

So the two filters work differently and you need both. Fields you never want stored go in `except`. Headers you want visible but not readable go in `sanitize_headers`. Anything sensitive that arrives under a name you did not anticipate ends up in the log verbatim, which makes reviewing the actual field names of a form part of the integration work rather than an afterthought.

## What the package does not record

It is easier to describe the limits of this middleware by looking at the default writer's three variables. There is a method, a path and a JSON body. That is the entire log line.

There is no status code, so you cannot tell from the log whether the request succeeded or returned a 500. There is no response body, so you cannot see what the application answered. There is no query string, because the writer uses `getPathInfo()` and no other request accessor, so a token passed as a query parameter would not appear in the path but also would not be logged. There is no user identifier, no session data, no duration and no IP address, which means the log cannot be joined to a user without adding your own custom writer.

That is not a criticism so much as a design boundary. The package is small enough to read in one sitting, and the extension mechanism is explicit: implement `LogWriter`, add whatever you need, and point `log_writer` at your class. A team that wants status codes and timings can write a writer that records them, but then the package is providing little beyond the middleware plumbing and the request lifecycle hook.

The other practical limit is volume. The default profile accepts every POST, PUT, PATCH, DELETE and QUERY request that reaches the middleware, including ones you did not intend to record. On a public application with a JSON API, that is a lot of entries, each containing a body, written at the `info` level into the `stack` channel by default. There is no sampling, no rate limit and no size cap in the documented configuration, so the filtering has to happen either in a custom `LogProfile` or by not registering the middleware globally.

## Version 1.12.0 and the upgrade path

The release record shows 1.12.0 tagged on 2026-07-14, 1.11.2 in February 2026 and 1.11.1 in February 2025. The last push to the repository was on 2026-07-14 as well, the same day as that release, so the project is not archived, although it is worth being precise that no change has landed since 2026-07-14.

The numbering is the thing to notice. The package is on a 1.x line with a long tail of point releases, and there is no 2.0 in sight, so the upgrade path is a routine `composer update` rather than a migration project. The project documentation points at `CHANGELOG.md` for what changed recently, which is where you look before pulling a new version, and the testing command is `composer test`.

The repository layout is small and tells you the shape of the thing: a `config/` directory, a `src/` directory, a `tests/` directory, plus `composer.json`, `phpunit.xml.dist` and a `.php_cs.dist.php` file for code style. There is no database layer, no migration, no model and no job, which is exactly why the upgrade cost is low. Nothing in the package touches your database, so a version bump cannot require a schema change.

The licence is MIT, and it is the permissive kind: you can use the package in commercial applications, modify it, and redistribute it, provided you keep the copyright notice and the licence text with it. There is no copyleft obligation that would reach your own codebase through the dependency, and no separate commercial tier.

One maintenance note that is not about versions. The project documentation asks that security problems be reported by mail to the address given rather than through the issue tracker. For a package that writes request payloads, including bodies and headers, into your logs, the security contact route matters more than it would for a package that only formats a string.

## Conclusion

This package suits a Laravel team that wants a quick safety net around lead-generating or otherwise critical POST submissions, especially one that has already standardized on Spatie's other logging packages and wants log output in the same format. It does not suit an application that needs full request and response bodies, status codes, or a searchable HTTP log, because the default writer records method, path and body only, and the filtering rules are yours to write. Verify first that your own sensitive fields are covered by the except list and your own sensitive headers by sanitize_headers, since the shipped defaults strip only password and password_confirmation and no headers at all, then check the log_channel and log_level settings against the channel your Laravel application already writes to.

## FAQ

### How do I install the spatie/laravel-http-logger package?

Run composer require spatie/laravel-http-logger. You can then publish the config file with php artisan vendor:publish using the Spatie\HttpLogger\HttpLoggerServiceProvider provider and the config tag.

### How do I stop Authorization headers and JWT tokens from being logged?

Add the header name to the sanitize_headers array in config/http-logger.php. The documentation shows Authorization on that list, and states that a sanitized header is written as `Authorization: "****"` rather than its original value.

### Which requests does the laravel-http-logger log by default?

The default log profile, LogNonGetRequests, only logs POST, PUT, PATCH, DELETE and QUERY requests, and it writes to the default Laravel logger through the channel and level set in the config file.

### Can I log only specific routes in Laravel?

Yes. The middleware can be added to a single route with ->middleware(\Spatie\HttpLogger\Middlewares\HttpLogger::class), as the documentation shows for a submit-form route, instead of being registered as global middleware.

### How do I log a custom field or add fields to the log line?

Implement the \Spatie\HttpLogger\LogWriter interface, which requires the logRequest method, and point the log_writer key in config/http-logger.php at your class. The same applies to filtering, where a custom \Spatie\HttpLogger\LogProfile implementation controls which requests are logged.

## Sources

- [License: MIT](https://github.com/spatie/laravel-http-logger/blob/main/LICENSE)
- [Project website](https://freek.dev/882-a-laravel-package-to-log-http-requests)
- [README](https://github.com/spatie/laravel-http-logger/blob/main/README.md)
- [Releases](https://github.com/spatie/laravel-http-logger/releases)
- [spatie/laravel-http-logger on GitHub](https://github.com/spatie/laravel-http-logger)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/spatie-laravel-http-logger
