Redaction and stored data
Which parameters are masked, which keys you declare per API, which bodies are stored as they came, and how to keep successful bodies too.
A recorder for outbound calls sits exactly where your credentials travel: API keys in query strings, passwords in login bodies, tokens in headers. Requizon redacts by parameter name, which means anything it can parse into names it can clean, and anything it cannot it leaves alone. This page lists what a row holds and which rules reach it.
What a row holds#
| Column | Stored |
|---|---|
api_name, host | As described in Naming APIs |
method, status_code, duration_ms | As they were |
path | Normalised, without the query string. See Paths |
raw_path | The path as it was called, with segments that may be secrets stored as ***. See Secrets in URLs |
failure_type, failure_message | For failures only. See Failure detection |
query_params | The query string, redacted as below |
request_params | The request body, redacted as below |
request_headers, response_headers | By the rules on Headers, with sensitive names masked |
response_body | For failures, redacted where it parses, and for the successes success_bodies keeps |
Headers are recorded under rules of their own, which Headers
covers in full. Out of the box nothing from the request is stored and the response headers of calls recorded as a
failure are, with Authorization, Cookie and the rest of the sensitive names kept as
***.
Query strings and request bodies#
The query string and the body are recorded separately, whatever the method. The query goes into
query_params and the body into request_params, so a
POST /v1/charges?expand[]=customer keeps its expand, a HEAD or
OPTIONS keeps the only parameters it has, and a GET or DELETE that sends a
JSON body (search APIs often do) keeps the body. A name that appears in both is kept in both, since which of the two
it came from is usually what you opened the row to find out.
The Details panel shows them as Query and Body. Both go through the same redaction,
per-API rules and callback included. The query string is parsed into named parameters; a JSON body (a
Content-Type containing json) and a form body
(application/x-www-form-urlencoded) are parsed the same way, and anything else is
unparsed.
A query string has no length limit of its own, so recording.query_max_bytes (4 KB) caps what is stored.
The column holds JSON and JSON cannot be cut part of the way through, so whole parameters that do not fit are dropped
and counted under _truncated, together with any values parse_str() never reached because
they sat past PHP's max_input_vars:
{"page": "2", "sort": "name", "_truncated": "1 parameter omitted"}
Request bodies are read under fixed limits: recording.max_body_read_bytes (256 KB) is never read
past, only seekable streams are read at all, and a request with no body costs nothing to check.
Bodies Requizon cannot parse#
Redaction works by parameter name, so a body that cannot be parsed into names is a body whose credentials Requizon
cannot find: XML, SOAP, text/plain, multipart/form-data uploads, and anything with no usable
Content-Type. By default such a body is recorded by shape only:
{ "_unparsed": "application/xml, 1482 bytes" }
If you know your unparsed bodies carry nothing sensitive, store them. They are kept verbatim, cut to
recording.request_body_max_bytes (4 KB):
'recording' => [
// ...
'store_unparsed_bodies' => true, // stored as {"_raw": "<?xml ..."}
],
Two kinds of body are never read at all. A body larger than recording.max_body_read_bytes (256 KB) is
recorded by shape from its declared size, which keeps a file upload from being loaded into memory on every request.
A body that cannot be rewound after reading is skipped, and the row has no parameters.
Redaction by parameter name#
Before storage, every parameter's name is lowercased and checked against two lists. A match replaces the value with
***. Nested arrays are walked, so credentials.api_secret is caught as well. The same two
lists decide which header names are masked and which values in a failed response body are.
'recording' => [
// ...
// Redacted when the name contains one of these: user_password, api_secret_key, auth_token
'redact_patterns' => ['pass', 'secret', 'token', 'apikey', 'api_key', 'auth'],
// Redacted on an exact name match, for APIs that abbreviate: ?l=user&p=password
'redact_exact' => ['p', 'l', 'pwd'],
],
{
"username": "rest@example",
"password": "***",
"periodFrom": "12.10.2026",
"credentials": { "api_secret": "***", "region": "eu" }
}
Add the names your own APIs use. When you replace either list in your config, include the defaults you still want: the list you write replaces the default one.
Matching on part of a name is deliberately generous. pass also catches passenger_count
and auth catches author, and those values are stored as ***. If a parameter
you need to read is being hidden, rename the pattern to something more specific (password instead
of pass) rather than removing it.
Keys that belong to one API#
The names worth hiding are usually one provider's rather than everybody's. Declare them on that API's
apis entry and they are added to the global lists for that API alone:
'apis' => [
// Patterns left bare, with the keys beside them
'nausys' => ['ws.nausys.com', '*.nausys.com', 'redact' => ['l']],
'stripe' => [
'hosts' => ['api.stripe.com', 'files.stripe.com'],
'redact' => ['card_number', 'cvc'], // substrings, like redact_patterns
],
'sedna' => [
'hosts' => ['sedna.example.com'],
'redact_exact' => ['l', 'p'], // whole names, like redact_exact
],
],
Both entry shapes can sit in the same
list and both can carry the keys. redact matches by substring and redact_exact by the whole
name, the same split as the global lists, and that split is what makes a one-letter key usable: Sedna authenticates
with ?l=user&p=pass, and l as a global substring rule would take every parameter with an
l in its name.
Rules are looked up by the resolved API name, so they also cover a name minted by
Requizon::resolveApiUsing(). An
entry that carries rules and no hosts matches no host of its own; it exists to hold the rules for such a
name.
Redacting with a callback#
For a rule no key name can express, such as a value that looks like a card number, a field that is only sensitive on one endpoint, or a nested structure to drop whole, register a callback in a service provider:
use BoringO11y\Requizon\Requizon;
use Psr\Http\Message\RequestInterface;
Requizon::redactUsing(function (array $params, string $api, RequestInterface $request): array {
if ($api === 'stripe') {
unset($params['source']);
}
return $params;
});
It receives what the name rules have already redacted, so it adds to the redaction rather than replacing it. It runs
for request parameters and for the parsed body of a failed response, so switch on $api rather than
assuming a direction. Anything it throws takes the row down with it, which is the safe direction for a failure in
redaction. The path a call was made to has a
callback of its own, as a path has no names to go by.
The response body of a failed call#
The response body of a failed call is stored whole, because it is the thing you opened the row to read. It is
redacted by the rules above when it parses as JSON or as a form, and stored as it came when it does not: XML, SOAP,
or a body past recording.response_body_read_bytes (256 KB), which is cut short before it is read and
therefore no longer parses. A parsed body is only re-encoded when something was actually redacted, so a body with
nothing to hide keeps the formatting it arrived with. Redaction happens before the result is cut to
response_body_max_bytes, so a JSON body between the two limits is redacted and then truncated. A
connection_error that arrived with part of a response stores no body at all.
What that means in practice:
- An API that echoes your request back in its error response has those credentials caught, as long as the response is JSON or a form.
- A SOAP fault or an XML error carrying a password is stored with the password in it, in
requizon_http_requests. - A failed call to an API that returns personal data stores that personal data: redaction matches names that look like credentials, and a customer's address is not one of them.
- A message returned by your failure detector is stored as-is.
These are the settings that bound it:
-
retention.detail_daysdecides how long those bodies exist. The default is 14 days; a few days is often enough to debug with. The hourly rollup holds no bodies and is unaffected. -
recording.response_body_max_bytescaps how much of each body is kept (64 KB), andrecording.response_body_read_byteshow much is read to redact it (256 KB). -
connectioncan put Requizon's tables on a separate database with its own access controls and backup policy. -
ignore_hostskeeps an API whose traffic must not be stored anywhere out of the table entirely.
Response bodies of successful calls#
A body is kept for failures only, by default. That leaves the bug where the API returned 200 and the data was wrong (partial results, a changed schema, an empty list, a soft error your detector does not catch) with nothing to look at. Turn success bodies on for the integrations you are debugging:
// config/requizon.php
'recording' => [
// ...
'success_bodies' => ['stripe', '*.nausys.com'], // or true for every API
'success_body_sample_rate' => 0.1, // keep one success in ten
],
success_bodies takes true, or a list of API names and Str::is() host patterns.
From the environment, REQUIZON_SUCCESS_BODIES takes true or a comma separated list, and
REQUIZON_SUCCESS_BODY_SAMPLE_RATE the rate. A call that is not sampled has its body left unread.
A kept success body is read, redacted and cut exactly as a failure's is, with one difference: a body redaction
cannot see into (XML, text, no Content-Type, or JSON cut short at
response_body_read_bytes) is stored by shape only, as an unparsed request body is, unless
store_unparsed_bodies is on. A 200 is the response that carries the token. A success whose
Content-Type is an image, video, audio, PDF or application/octet-stream is stored as its
type and size without being read. Response headers set to failures are kept for a row whose success
body is kept, too.
Every kept body costs up to response_body_max_bytes (64 KB) a row, held for
retention.detail_days. An API making 100,000 calls a day with 4 KB responses, all of them kept, adds
around 400 MB a day, or 5.6 GB over the default 14 days. Scope it to the APIs you need, sample the busy ones, and
switch it off once the bug is found.
Give it the same care as your application logs: restrict who can query it, think about what your backups copy,
and keep the viewRequizon gate as narrow as the table deserves. The dashboard shows these bodies to
anyone the gate lets in.
Secrets in URLs#
The query string is never part of either stored path. It goes into query_params, redacted like the
body. A failure message that quotes the URL drops its query string and any credentials in it.
The normalised path stores a segment longer than 40 characters as :id. The raw path stores one as
***, along with any segment a rewrite covers with a * and the segment after one the rules
above name, so /api_key/abc123/users keeps api_key and loses abc123. What
neither catches is a short secret no rule names, such as /hooks/abc123/send, which is stored as written
in both. For that host:
- a rewrite such as
'/hooks/*/send' => '/hooks/:id/send'masks it in both paths Requizon::redactPathUsing()masks it in the raw path by valueREQUIZON_STORE_RAW_PATHS=falsestops raw paths being stored at all
A search by URL still finds such a call, as it matches a hash of the path as sent, keyed with your
APP_KEY. See Searching by path or URL.