AccessDenied from a policy that refused the call and an AccessDenied from a backing store that refused
it are the same error. Reading one, an operator either goes and audits policies that are fine, or opens a
ticket with a cloud provider that did nothing.
The x-t9-request-error header
A failed response can carry a header naming the layer that produced the failure:
The header rides beside the AWS error rather than inside it. The status, the code, the message and the
envelope your SDK reads are unchanged, so retry and error handling that keys on the error code behaves
exactly as it did.
An absent header means the layer is not stated
It does not mean the layer is unknown, and it is not a hint about which one it was. An error that has not been taught its layer says nothing rather than guessing, because a wrong layer costs more than no layer: it sends you to read the wrong system. Treat a response without the header the way you would have before it existed.What states a layer today
Three of the four values have a producer today.fabric is defined and reserved: it is what the header will
carry once an adapter states it, and none does. Do not branch a runbook on a value that is not in this list.
The
authorization row is short on purpose. Telling your own policy denial apart from a refusal by the
service behind the adapter is the question this header exists for, and only the adapters listed there answer
it. Everywhere else, a policy denial arrives with no header at all.
DynamoDB is the one to reach for if you want to see both halves. A refusal from your policies says
authorization, a refusal from Spanner or Cosmos says backend, and the two are otherwise byte-identical
to your SDK.
That distinction matters more than it looks. On an adapter listed under backend but not under
authorization, a refusal from the backing service says backend while a refusal from your policies says
nothing, so it is tempting to read an absent header as “not the backend”. Do not. Absence means unstated,
and you get it from every adapter that has not been taught, from every refusal that spans layers, and from
S3’s own proxy tiers. The header tells you something when it is present and nothing when it is absent.
S3 needs one more sentence. Where the adapter translates a store failure into an S3 error it states
backend. Where it runs against an S3-compatible store and relays that store’s own response unchanged,
there is no translation step and no header.
Not every refusal states a layer, and that is deliberate. The S3 adapter answers the same AccessDenied for
a signature that did not verify and for a bucket owned by another account, so that a refusal tells an
unauthenticated caller nothing about which of their guesses was closest. Neither may claim a layer, so
neither does: a response with no header is one where naming a layer would have been a guess.
Related topics
- Cross-Cloud IAM: What the
authorizationlayer evaluates, and which policies survive adaptation - Service Adapters: The adaptation tiers, and which services run through an adapter at all
- Observability: Routing appliance telemetry to your own sinks