|
| 1 | +# Idempotency |
| 2 | + |
| 3 | +It is sometimes useful for an API to have a unique, customer-provided |
| 4 | +identifier for particular requests. This can be useful for several purposes, |
| 5 | +such as: |
| 6 | + |
| 7 | +- De-duplicating requests from parallel processes |
| 8 | +- Ensuring the safety of retries |
| 9 | +- Auditing |
| 10 | + |
| 11 | +The purpose of idempotency keys is to provide idempotency guarantees: allowing |
| 12 | +the same request to be issued more than once without subsequent calls having |
| 13 | +any effect. In the event of a network failure, the client can retry the |
| 14 | +request, and the server can detect duplication and ensure that the request is |
| 15 | +only processed once. |
| 16 | + |
| 17 | +## Guidance |
| 18 | + |
| 19 | +APIs **may** add a `aep.api.IdempotencyKey idempotency_key` parameter to |
| 20 | +request messages (including those of standard methods) in order to uniquely |
| 21 | +identify particular requests. API servers **must not** execute requests with |
| 22 | +the same `idempotency_key` more than once. |
| 23 | + |
| 24 | +```proto |
| 25 | +message CreateBookRequest { |
| 26 | + // The parent resource where this book will be created. |
| 27 | + // Format: publishers/{publisher} |
| 28 | + string parent = 1 [ |
| 29 | + (google.api.field_behavior) = REQUIRED, |
| 30 | + (google.api.resource_reference) = { |
| 31 | + child_type: "library.example.com/Book" |
| 32 | + }]; |
| 33 | +
|
| 34 | + // The book to create. |
| 35 | + Book book = 2 [(google.api.field_behavior) = REQUIRED]; |
| 36 | +
|
| 37 | + // This request is only idempotent if `idempotency_key` is provided. |
| 38 | + // |
| 39 | + // This key will be honored for at least one hour after the first time it is |
| 40 | + // seen by the server. |
| 41 | + // |
| 42 | + // The key is restricted to 36 ASCII characters. A random UUID is recommended. |
| 43 | + aep.api.IdempotencyKey idempotency_key = 3 [ |
| 44 | + (aep.api.field_info).minimum_lifetime = { seconds: 3600 } |
| 45 | + ]; |
| 46 | +} |
| 47 | +``` |
| 48 | + |
| 49 | +- [`aep.api.IdempotencyKey`][] has a `key` and a `first_sent` timestamp. |
| 50 | + |
| 51 | + - `key` is simply a unique identifier. |
| 52 | + |
| 53 | +- Providing an idempotency key **must** guarantee idempotency. |
| 54 | + |
| 55 | + - If a duplicate request is detected, the server **must** return one of: |
| 56 | + |
| 57 | + - A response equivalent to the response for the previously successful |
| 58 | + request, because the client most likely did not receive the previous |
| 59 | + response. |
| 60 | + - An error indicating that the `first_sent` field of the idempotency key is |
| 61 | + invalid or cannot be honored (expired, in the future, or differs from a |
| 62 | + previous `first_sent` value with the same `key`). |
| 63 | + - An error, if returning an equivalent response is not possible. |
| 64 | + |
| 65 | + For example, if a resource was created, then deleted, and then a |
| 66 | + duplicate request to create the resource is received, the server **may** |
| 67 | + return an error if returning the previously created resource is not |
| 68 | + possible. |
| 69 | + |
| 70 | + - APIs **should** honor idempotency keys for at least an hour. |
| 71 | + - When using protocol buffers, idempotency keys that are UUIDs **must** be |
| 72 | + annotated with a minimum lifetime using the extension |
| 73 | + [`(aep.api.field_info).minimum_lifetime`][]. |
| 74 | + |
| 75 | +- The `idempotency_key` field **must** be provided on the request message to |
| 76 | + which it applies (and it **must not** be a field on resources themselves). |
| 77 | + |
| 78 | + - The `first_sent` field can be used by API servers to determine if a key is |
| 79 | + expired. API servers **must** reject requests with expired keys, and |
| 80 | + **must** reject requests with keys that are in the future. When feasible, |
| 81 | + API servers **should** reject requests that use the same `key` but have a |
| 82 | + different `first_sent` timestamp. |
| 83 | + - The `key` field **must** be able to be a UUID, and **may** allow UUIDs to |
| 84 | + be the only valid format. The format restrictions for idempotency keys |
| 85 | + **must** be documented. |
| 86 | + |
| 87 | +- Idempotency keys **should** be optional. |
| 88 | + |
| 89 | +## Further reading |
| 90 | + |
| 91 | +- For which codes to retry, see [AEP-194](https://aep.dev/194). |
| 92 | +- For how to retry errors in client libraries, see |
| 93 | + [AEP-4221](https://aep.dev/4221). |
| 94 | + |
| 95 | +## Rationale |
| 96 | + |
| 97 | +### Naming the field `idempotency_key` |
| 98 | + |
| 99 | +The original content from which this AEP is derived defines a `request_id` |
| 100 | +field; we define `idempotency_key` instead for two reasons: |
| 101 | + |
| 102 | +1. There is an [active Internet-Draft][idempotency-key-draft] to standardize an |
| 103 | + HTTP header named `Idempotency-Key`. |
| 104 | +1. There may be edge cases in which separately identifying idempotent requests |
| 105 | + is useful; `request_id` would be more appropriate for such use cases. For |
| 106 | + example, an API producer might be testing the idempotency behavior of the |
| 107 | + API server, and might want to issue multiple requests with the same |
| 108 | + `idempotency_key` and trace the behavior of each request separately. |
| 109 | + |
| 110 | +<!-- prettier-ignore-start --> |
| 111 | +[idempotency-key-draft]: https://datatracker.ietf.org/doc/draft-ietf-httpapi-idempotency-key-header/ |
| 112 | +[`aep.api.IdempotencyKey`]: https://buf.build/aep/api/file/main:aep/api/idempotency_key.proto#L21 |
| 113 | +[`(aep.api.field_info).minimum_lifetime`]: https://buf.build/aep/api/file/main:aep/api/field_info.proto#L35 |
| 114 | +<!-- prettier-ignore-end --> |
| 115 | + |
| 116 | +### Using UUIDs for request identification |
| 117 | + |
| 118 | +When a value is required to be unique, leaving the format open-ended can lead |
| 119 | +to API consumers incorrectly providing a duplicate identifier. As such, |
| 120 | +standardizing on a universally unique identifier drastically reduces the chance |
| 121 | +for collisions when done correctly. |
| 122 | + |
| 123 | +## Changelog |
| 124 | + |
| 125 | +- **2023-23-20**: Adopt AEP from from Google's AIP with the following changes: |
| 126 | + - Rename field from `request_id` to `idempotency_key` (plus some minor |
| 127 | + releated rewording). |
| 128 | + - Add a common component [`aep.api.IdempotencyKey`][] and use this rather |
| 129 | + than `string` for the `idempotency_key` field; add related guidance about |
| 130 | + `IdempotencyKey.first_seen`. |
| 131 | + - Remove guidance about annotating `idempotency_key` with |
| 132 | + `(google.api.field_info).format`. |
| 133 | + - Add guidance about annotating `idempotency_key` with |
| 134 | + [`(aep.api.field_info).minimum_lifetime`]. |
| 135 | + - Update guidance about responses to be more explicit about success and error |
| 136 | + cases, while allowing "equivalent" rather than identical responses for |
| 137 | + subsequent requests. |
| 138 | + - Temporarily removed the section about stale success responses, pending |
| 139 | + further discussion. |
| 140 | +- **2023-10-02**: Add UUID format extension guidance. |
| 141 | +- **2019-08-01**: Changed the examples from "shelves" to "publishers", to |
| 142 | + present a better example of resource ownership. |
0 commit comments