Errors
Objective
Section titled “Objective”An error response is part of the contract. Callers branch on error codes, so a code whose meaning changes breaks working software as surely as a removed endpoint does.
Read this page before implementing any service, and before writing any client error handling.
Requirements
Section titled “Requirements”- None
Instructions
Section titled “Instructions”Two envelopes, not one
Section titled “Two envelopes, not one”Shelf Cloud services use one of two error envelopes, decided by the protocol the service speaks. They are not interchangeable, and a client written for one cannot parse the other.
EC2 query envelope — used by services mirroring Amazon EC2:
<?xml version="1.0" encoding="UTF-8"?><Response> <Errors> <Error> <Code>InvalidInstanceID.NotFound</Code> <Message>The instance ID 'i-0a1b2c3d4e5f60718' does not exist</Message> </Error> </Errors> <RequestID>b1e2c3d4-5678-90ab-cdef-1234567890ab</RequestID></Response>No <Type> element. The request id element is RequestID.
Standard query envelope — used by services mirroring IAM and STS:
<ErrorResponse xmlns="https://iam.amazonaws.com/doc/2010-05-08/"> <Error> <Type>Sender</Type> <Code>NoSuchEntity</Code> <Message>The user with name deploy cannot be found</Message> </Error> <RequestId>b1e2c3d4-5678-90ab-cdef-1234567890ab</RequestId></ErrorResponse>Has <Type>. The request id element is RequestId, with different casing from
the EC2 form.
JSON envelope — used by services with no AWS counterpart, such as Billing and Account:
{ "__type": "QuoteExpired", "message": "Quote quo-0a1b2c3d4e5f60718 expired at 2026-09-03T14:00:00Z"}with x-amzn-ErrorType also carrying the code as a response header.
| Service | Envelope |
|---|---|
| EC2 | EC2 query |
| IAM | Standard query |
| Account | JSON |
| Billing | JSON |
The casing differences between RequestID and RequestId are inherited and are
preserved exactly. A client that parses one and not the other must behave here
as it does against AWS.
Request ids in headers
Section titled “Request ids in headers”Every response, successful or not, carries the request id in the
x-amzn-RequestId header. This is the header every AWS SDK reads to populate
its response metadata; a custom header would be invisible to all of them, and
the customer told to “quote the request id” would have none to quote.
Successful EC2-protocol responses additionally carry a <requestId> element in
the body, lowercase initial, as Amazon EC2 does.
HTTP status codes
Section titled “HTTP status codes”| Status | Meaning |
|---|---|
400 | Malformed, invalid, expired, or a resource that does not exist |
403 | Credentials unknown, or policy denies the action |
404 | JSON-protocol services only, for a missing entity |
409 | The request conflicts with the state of the resource |
500 | An error on our side |
503 | Temporarily unavailable, or the request rate was exceeded |
There is no 401 anywhere in this platform. AWS uses none in this protocol,
and two things depend on that:
- The AWS SDKs correct their own clocks automatically when they see a 403 or 400
carrying a recognised skew-related code together with our
Dateheader. A 401 defeats that recovery entirely. - RFC 9110 requires a 401 response to carry a
WWW-Authenticateheader. We send none, so a 401 would be a protocol violation, and intermediaries that special-case 401 would behave unpredictably.
Shared codes
Section titled “Shared codes”Returned by every service.
| Code | Status | Meaning | Retry |
|---|---|---|---|
IncompleteSignature | 400 | The Authorization header is malformed | No |
InvalidAction | 400 | Unknown action, or an unimplemented version | No |
InvalidParameterValue | 400 | A parameter has an unacceptable value | No |
InvalidParameterCombination | 400 | Parameters cannot be used together | No |
MissingParameter | 400 | A required parameter is absent | No |
InvalidPaginationToken | 400 | The token is invalid or expired | No |
IdempotentParameterMismatch | 400 | Token reused with different parameters | No |
RequestExpired | 400 | The timestamp is outside the permitted skew | After fixing the clock |
AuthFailure | 403 | The credentials could not be validated | No |
SignatureDoesNotMatch | 403 | The signature does not match the request | No |
InvalidClientTokenId | 403 | The access key id is unknown or inactive | No |
MissingAuthenticationToken | 403 | The request was not signed | No |
UnauthorizedOperation | 403 | Policy denies this action on this resource | No |
AccessDenied | 403 | Policy denies this action | No |
InternalError | 500 | An unexpected failure on our side | Yes |
ServiceUnavailable | 503 | Temporarily unavailable | Yes |
Note the statuses carefully. InvalidClientTokenId is 403, not 401.
RequestExpired is 400, not 401 and not 403. IncompleteSignature is
400 while SignatureDoesNotMatch is 403. These are AWS’s own values,
and matching them is what makes existing client recovery logic work.
Throttling differs by service
Section titled “Throttling differs by service”| Service kind | Code | Status |
|---|---|---|
| EC2-mirroring | RequestLimitExceeded | 503 |
| IAM-mirroring | Throttling | 400 |
| JSON services | Throttling | 429 |
This is not an inconsistency we chose. Amazon EC2 returns
RequestLimitExceeded, and the Terraform AWS provider matches that exact string
for its backoff; a 429 there would produce no automatic retry at all for a large
installed base of clients. Services with no AWS counterpart are free to use the
modern 429 and do.
Each service’s error page states which applies to it.
Absence versus denial
Section titled “Absence versus denial”A caller must never learn that a resource exists by the shape of the error.
The rule is a decision procedure, not a preference:
- Another account’s resource, or no such resource → the service’s not-found code. Identical responses in both cases.
- This account, resource exists, policy denies →
UnauthorizedOperationorAccessDenied.
So UnauthorizedOperation confirms existence — and it may only be returned
within the caller’s own account, where existence is not a secret from them.
What an error never contains
Section titled “What an error never contains”No stack traces. No internal hostnames. No database or query text. No detail about another account. No hint that distinguishes a wrong password from an unknown user, or a wrong signature from a wrong key.
SignatureDoesNotMatch is deliberately identical whether the secret was wrong,
the region was wrong, or the clock was wrong. An error that distinguished them
would help an attacker as much as a customer, and the troubleshooting guides
carry the differential diagnosis instead.
Messages
Section titled “Messages”The Code is stable and is what callers branch on. The Message is written for
a human, may change wording between releases, and must never be parsed.
Deprecating a code
Section titled “Deprecating a code”A code is never given a new meaning. A behaviour needing a different meaning gets a new code, and the old code keeps returning what it always did until it is removed with notice.