# Recording headers

> Which headers are kept in each direction, how sensitive ones are masked, and how to narrow the list.

Source: https://requizon.boring-observability.dev/docs/headers
Section: Configuration — Requizon documentation (version 0.3)
Updated: 2026-09-20

---

When an integration goes wrong, the answer is often in a header rather than in the body. `Retry-After` and the `X-RateLimit-*` family say when you may call again and how much of your budget is left. The provider's `X-Request-Id` or `CF-Ray` is what their support desk asks for. `Cache-Control` and `ETag` explain a response you thought you had already fetched. Requizon stores headers beside the parameters and the response body, and shows them in the request row's Details panel.

## Choosing which messages are captured

Capture is configured once, for every API, under `recording.headers`:

```php
// config/requizon.php
'recording' => [
    // ...
    'headers' => [
        'request' => env('REQUIZON_REQUEST_HEADERS', 'none'),       // none | failures | all
        'response' => env('REQUIZON_RESPONSE_HEADERS', 'failures'), // none | failures | all
        'only' => [],              // e.g. ['x-ratelimit-*', 'retry-after', 'x-request-id']
        'except' => [],
        'redact' => ['authorization', 'proxy-authorization', 'cookie', 'set-cookie', '*api-key*', '*subscription-key*', 'x-functions-key'],
        'max_bytes' => 16 * 1024,
    ],
],
```

The two directions are set independently and take the same three values. `none` stores nothing, `all` stores every header of every recorded call, and `failures` stores them only for calls recorded as a failure, which means the same thing in either direction: the same rows that keep a response body. Out of the box the request's headers are not stored at all and the response's are stored for failures, so a fresh install gives you the headers of the calls you are most likely to open and nothing for the rest. `REQUIZON_REQUEST_HEADERS` and `REQUIZON_RESPONSE_HEADERS` set the two from the environment.

## What the Details panel shows

Expanding a request row prints one line per value, in the shape they had on the wire, so a response that sets two cookies gets two `Set-Cookie` lines:

```text
Date: Fri, 18 Sep 2026 09:14:02 GMT
Content-Type: application/json
X-Request-Id: req_8fJk2Lm0Qd
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1758186900
Retry-After: 42
Set-Cookie: ***
Set-Cookie: ***
```

Request headers appear above the Query and Body blocks, response headers below them and above the response body, so the panel reads in the order the call happened.

## Masking sensitive headers

A header whose name is sensitive keeps its name and its number of values, and every value becomes `***`. That way you can still see that the header was sent, and how many times, without the credential reaching the table. Names are matched case-insensitively, so `X-Api-Key` and `x-api-key` are the same header.

Masked by default:

- `Authorization` and `Proxy-Authorization`
- `Cookie` and `Set-Cookie`
- Anything containing `api-key`: `X-Api-Key`, Azure's `api-key`, `X-Goog-Api-Key`
- Anything containing `subscription-key`, such as `Ocp-Apim-Subscription-Key`
- `X-Functions-Key`

On top of that list, a header is masked when its name matches the parameter rules in force for its API: the global `redact_patterns` and `redact_exact`, plus whatever that API's `apis` entry adds. See [Redaction](https://requizon.boring-observability.dev/docs/redaction#per-api) for both. One provider's `X-Partner-Token` is caught by the global `token` pattern without you naming it here, and a header only that provider sends can be named on its entry and masked nowhere else.

`redact` takes `Str::is()` patterns, and the list you write replaces the default one, so copy across the entries you still want. A published config that has no `redact` key at all keeps the defaults, which is what an application that published its config before header capture existed will have.

## Keeping a shortlist with only and except

`only` and `except` also take `Str::is()` patterns matched against the lowercased name. `only` keeps just the headers it matches; `except` then removes some of what is left. A header that `only` lets through is still subject to redaction, so a shortlist cannot accidentally expose an `Authorization` header.

```php
'headers' => [
    'response' => 'all',
    'only' => ['x-ratelimit-*', 'retry-after', 'x-request-id', 'cf-ray', 'location'],
    // ...
],
```

A shortlist is what makes `all` affordable. Every recorded call then stores five short headers instead of the twenty a CDN adds to each response.

## The size cap

`max_bytes` (16 KB) caps the stored header JSON of each message. JSON cannot be cut part of the way through, so a header that does not fit is dropped whole rather than truncated, and the dropped ones are counted under `_truncated`:

```json
{"Date": ["Fri, 18 Sep 2026 09:14:02 GMT"], "_truncated": ["2 headers omitted"]}
```

Headers are considered in the order they arrived, and one that does not fit is skipped rather than ending the list, so a single enormous `Content-Security-Policy` does not cost you every header after it.

## Following a redirect

A redirect is recorded hop by hop, but a `3xx` is not a failure, so `failures` never stores the hops. To read each hop's `Location`, set `response` to `all`, preferably with an `only` list so you are not storing every header of every successful call as well.

## What capture costs

Headers are already parsed by the time Requizon sees them, so capturing them reads nothing extra off the wire. The cost is table size, which is why the default keeps the response headers of failures alone. If you turn `all` on for both directions, keep [retention.detail_days](https://requizon.boring-observability.dev/docs/retention) short and expect the detail table to grow noticeably faster than it did.


## Common questions

### Does Requizon store request and response headers?

By default it keeps the response headers of calls recorded as a failure, and no request headers at all. Both directions are set independently to none, failures or all, under recording.headers in config/requizon.php or through REQUIZON_REQUEST_HEADERS and REQUIZON_RESPONSE_HEADERS.

### Are Authorization headers stored by Requizon?

The name is stored, the value is not. Authorization, Proxy-Authorization, Cookie, Set-Cookie, X-Functions-Key, anything containing api-key or subscription-key, and any header matching the parameter redaction rules in force for that API are kept with every value replaced by ***, so you can still see the header was sent. Names match case-insensitively.

### How do I record only rate-limit headers?

Set recording.headers.only to the patterns you want, such as x-ratelimit-*, retry-after and x-request-id. They are Str::is() patterns matched against the lowercased name, except removes some of what only let through, and a header only keeps is still subject to redaction.
