|
| 1 | +RFC: Improving access to request IDs in SDK clients |
| 2 | +=================================================== |
| 3 | + |
| 4 | +> Status: Implemented in [#2129](https://github.com/awslabs/smithy-rs/pull/2129) |
| 5 | +> |
| 6 | +> Applies to: AWS SDK clients |
| 7 | +
|
| 8 | +At time of writing, customers can retrieve a request ID in one of four ways in the Rust SDK: |
| 9 | + |
| 10 | +1. For error cases where the response parsed successfully, the request ID can be retrieved |
| 11 | + via accessor method on operation error. This also works for unmodeled errors so long as |
| 12 | + the response parsing succeeds. |
| 13 | +2. For error cases where a response was received but parsing fails, the response headers |
| 14 | + can be retrieved from the raw response on the error, but customers have to manually extract |
| 15 | + the request ID from those headers (there's no convenient accessor method). |
| 16 | +3. For all error cases where the request ID header was sent in the response, customers can |
| 17 | + call `SdkError::into_service_error` to transform the `SdkError` into an operation error, |
| 18 | + which has a `request_id` accessor on it. |
| 19 | +4. For success cases, the customer can't retrieve the request ID at all if they use the fluent |
| 20 | + client. Instead, they must manually make the operation and call the underlying Smithy client |
| 21 | + so that they have access to `SdkSuccess`, which provides the raw response where the request ID |
| 22 | + can be manually extracted from headers. |
| 23 | + |
| 24 | +Only one of these mechanisms is convenient and ergonomic. The rest need considerable improvements. |
| 25 | +Additionally, the request ID should be attached to tracing events where possible so that enabling |
| 26 | +debug logging reveals the request IDs without any code changes being necessary. |
| 27 | + |
| 28 | +This RFC proposes changes to make the request ID easier to access. |
| 29 | + |
| 30 | +Terminology |
| 31 | +----------- |
| 32 | + |
| 33 | +- **Request ID:** A unique identifier assigned to and associated with a request to AWS that is |
| 34 | + sent back in the response headers. This identifier is useful to customers when requesting support. |
| 35 | +- **Operation Error:** Operation errors are code generated for each operation in a Smithy model. |
| 36 | + They are an enum of every possible modeled error that that operation can respond with, as well |
| 37 | + as an `Unhandled` variant for any unmodeled or unrecognized errors. |
| 38 | +- **Modeled Errors:** Any error that is represented in a Smithy model with the `@error` trait. |
| 39 | +- **Unmodeled Errors:** Errors that a service responds with that do not appear in the Smithy model. |
| 40 | +- **SDK Clients:** Clients generated for the AWS SDK, including "adhoc" or "one-off" clients. |
| 41 | +- **Smithy Clients:** Any clients not generated for the AWS SDK, excluding "adhoc" or "one-off" clients. |
| 42 | + |
| 43 | +SDK/Smithy Purity |
| 44 | +----------------- |
| 45 | + |
| 46 | +Before proposing any changes, the topic of purity needs to be covered. Request IDs are not |
| 47 | +currently a Smithy concept. However, at time of writing, the request ID concept is leaked into |
| 48 | +the non-SDK rust runtime crates and generated code via the [generic error] struct and the |
| 49 | +`request_id` functions on generated operation errors (e.g., [`GetObjectError` example in S3]). |
| 50 | + |
| 51 | +This RFC attempts to remove these leaks from Smithy clients. |
| 52 | + |
| 53 | +Proposed Changes |
| 54 | +---------------- |
| 55 | + |
| 56 | +First, we'll explore making it easier to retrieve a request ID from errors, |
| 57 | +and then look at making it possible to retrieve them from successful responses. |
| 58 | +To see the customer experience of these changes, see the **Example Interactions** |
| 59 | +section below. |
| 60 | + |
| 61 | +### Make request ID retrieval on errors consistent |
| 62 | + |
| 63 | +One could argue that customers being able to convert a `SdkError` into an operation error |
| 64 | +that has a request ID on it is sufficient. However, there's no way to write a function |
| 65 | +that takes an error from any operation and logs a request ID, so it's still not ideal. |
| 66 | + |
| 67 | +The `aws-http` crate needs to have a `RequestId` trait on it to facilitate generic |
| 68 | +request ID retrieval: |
| 69 | + |
| 70 | +```rust |
| 71 | +pub trait RequestId { |
| 72 | + /// Returns the request ID if it's available. |
| 73 | + fn request_id(&self) -> Option<&str>; |
| 74 | +} |
| 75 | +``` |
| 76 | + |
| 77 | +This trait will be implemented for `SdkError` in `aws-http` where it is declared, |
| 78 | +complete with logic to pull the request ID header out of the raw HTTP responses |
| 79 | +(it will always return `None` for event stream `Message` responses; an additional |
| 80 | +trait may need to be added to `aws-smithy-http` to facilitate access to the headers). |
| 81 | +This logic will try different request ID header names in order of probability |
| 82 | +since AWS services have a couple of header name variations. `x-amzn-requestid` is |
| 83 | +the most common, with `x-amzn-request-id` being the second most common. |
| 84 | + |
| 85 | +`aws-http` will also implement `RequestId` for `aws_smithy_types::error::Error`, |
| 86 | +and the `request_id` method will be removed from `aws_smithy_types::error::Error`. |
| 87 | +Places that construct `Error` will place the request ID into its `extras` field, |
| 88 | +where the `RequestId` trait implementation can retrieve it. |
| 89 | + |
| 90 | +A codegen decorator will be added to `sdk-codegen` to implement `RequestId` for |
| 91 | +operation errors, and the existing `request_id` accessors will be removed from |
| 92 | +`CombinedErrorGenerator` in `codegen-core`. |
| 93 | + |
| 94 | +With these changes, customers can directly access request IDs from `SdkError` and |
| 95 | +operations errors by importing the `RequestId` trait. Additionally, the Smithy/SDK |
| 96 | +purity is improved since both places where request IDs are leaked to Smithy clients |
| 97 | +will be resolved. |
| 98 | + |
| 99 | +### Implement `RequestId` for outputs |
| 100 | + |
| 101 | +To make it possible to retrieve request IDs when using the fluent client, the new |
| 102 | +`RequestId` trait can be implemented for outputs. |
| 103 | + |
| 104 | +Some services (e.g., Transcribe Streaming) model the request ID header in their |
| 105 | +outputs, while other services (e.g., Directory Service) model a request ID |
| 106 | +field on errors. In some cases, services take `RequestId` as a modeled input |
| 107 | +(e.g., IoT Event Data). It follows that it is possible, but unlikely, that |
| 108 | +a service could have a field named `RequestId` that is not the same concept |
| 109 | +in the future. |
| 110 | + |
| 111 | +Thus, name collisions are going to be a concern for putting a request ID accessor |
| 112 | +on output. However, if it is implemented as a trait, then this concern is partially |
| 113 | +resolved. In the vast majority of cases, importing `RequestId` will provide the |
| 114 | +accessor without any confusion. In cases where it is already modeled and is the |
| 115 | +same concept, customers will likely just use it and not even realize they didn't |
| 116 | +import the trait. The only concern is future cases where it is modeled as a |
| 117 | +separate concept, and as long as customers don't import `RequestId` for something |
| 118 | +else in the same file, that confusion can be avoided. |
| 119 | + |
| 120 | +In order to implement `RequestId` for outputs, either the original response needs |
| 121 | +to be stored on the output, or the request ID needs to be extracted earlier and |
| 122 | +stored on the output. The latter will lead to a small amount of header lookup |
| 123 | +code duplication. |
| 124 | + |
| 125 | +In either case, the `StructureGenerator` needs to be customized in `sdk-codegen` |
| 126 | +(Appendix B outlines an alternative approach to this and why it was dismissed). |
| 127 | +This will be done by adding customization hooks to `StructureGenerator` similar |
| 128 | +to the ones for `ServiceConfigGenerator` so that a `sdk-codegen` decorator can |
| 129 | +conditionally add fields and functions to any generated structs. A hook will |
| 130 | +also be needed to additional trait impl blocks. |
| 131 | + |
| 132 | +Once the hooks are in place, a decorator will be added to store either the original |
| 133 | +response or the request ID on outputs, and then the `RequestId` trait will be |
| 134 | +implemented for them. The `ParseResponse` trait implementation will be customized |
| 135 | +to populate this new field. |
| 136 | + |
| 137 | +Note: To avoid name collisions of the request ID or response on the output struct, |
| 138 | +these fields can be prefixed with an underscore. It shouldn't be possible for SDK |
| 139 | +fields to code generate with this prefix given the model validation rules in place. |
| 140 | + |
| 141 | +### Implement `RequestId` for `Operation` and `operation::Response` |
| 142 | + |
| 143 | +In the case that a customer wants to ditch the fluent client, it should still |
| 144 | +be easy to retrieve a request ID. To do this, `aws-http` will provide `RequestId` |
| 145 | +implementations for `Operation` and `operation::Response`. These implementations |
| 146 | +will likely make the other `RequestId` implementations easier to implement as well. |
| 147 | + |
| 148 | +### Implement `RequestId` for `Result` |
| 149 | + |
| 150 | +The `Result` returned by the SDK should directly implement `RequestId` when both |
| 151 | +its `Ok` and `Err` variants implement `RequestId`. This will make it possible |
| 152 | +for a customer to feed the return value from `send()` directly to a request ID logger. |
| 153 | + |
| 154 | +Example Interactions |
| 155 | +-------------------- |
| 156 | + |
| 157 | +### Generic Handling Case |
| 158 | + |
| 159 | +```rust |
| 160 | +// A re-export of the RequestId trait |
| 161 | +use aws_sdk_service::primitives::RequestId; |
| 162 | + |
| 163 | +fn my_request_id_logging_fn(request_id: &dyn RequestId) { |
| 164 | + println!("request ID: {:?}", request_id.request_id()); |
| 165 | +} |
| 166 | + |
| 167 | +let result = client.some_operation().send().await?; |
| 168 | +my_request_id_logging_fn(&result); |
| 169 | +``` |
| 170 | + |
| 171 | +### Success Case |
| 172 | + |
| 173 | +```rust |
| 174 | +use aws_sdk_service::primitives::RequestId; |
| 175 | + |
| 176 | +let output = client.some_operation().send().await?; |
| 177 | +println!("request ID: {:?}", output.request_id()); |
| 178 | +``` |
| 179 | + |
| 180 | +### Error Case with `SdkError` |
| 181 | + |
| 182 | +```rust |
| 183 | +use aws_sdk_service::primitives::RequestId; |
| 184 | + |
| 185 | +match client.some_operation().send().await { |
| 186 | + Ok(_) => { /* handle OK */ } |
| 187 | + Err(err) => { |
| 188 | + println!("request ID: {:?}", output.request_id()); |
| 189 | + } |
| 190 | +} |
| 191 | +``` |
| 192 | + |
| 193 | +### Error Case with operation error |
| 194 | + |
| 195 | +```rust |
| 196 | +use aws_sdk_service::primitives::RequestId; |
| 197 | + |
| 198 | +match client.some_operation().send().await { |
| 199 | + Ok(_) => { /* handle OK */ } |
| 200 | + Err(err) => match err.into_service_err() { |
| 201 | + err @ SomeOperationError::SomeError(_) => { println!("request ID: {:?}", err.request_id()); } |
| 202 | + _ => { /* don't care */ } |
| 203 | + } |
| 204 | +} |
| 205 | +``` |
| 206 | + |
| 207 | +Changes Checklist |
| 208 | +----------------- |
| 209 | + |
| 210 | +- [x] Create the `RequestId` trait in `aws-http` |
| 211 | +- [x] Implement for errors |
| 212 | + - [x] Implement `RequestId` for `SdkError` in `aws-http` |
| 213 | + - [x] Remove `request_id` from `aws_smithy_types::error::Error`, and store request IDs in its `extras` instead |
| 214 | + - [x] Implement `RequestId` for `aws_smithy_types::error::Error` in `aws-http` |
| 215 | + - [x] Remove generation of `request_id` accessors from `CombinedErrorGenerator` in `codegen-core` |
| 216 | +- [x] Implement for outputs |
| 217 | + - [x] Add customization hooks to `StructureGenerator` |
| 218 | + - [x] Add customization hook to `ParseResponse` |
| 219 | + - [x] Add customization hook to `HttpBoundProtocolGenerator` |
| 220 | + - [x] Customize output structure code gen in `sdk-codegen` to add either a request ID or a response field |
| 221 | + - [x] Customize `ParseResponse` in `sdk-codegen` to populate the outputs |
| 222 | +- [x] Implement `RequestId` for `Operation` and `operation::Response` |
| 223 | +- [x] Implement `RequestId` for `Result<O, E>` where `O` and `E` both implement `RequestId` |
| 224 | +- [x] Re-export `RequestId` in generated crates |
| 225 | +- [x] Add integration tests for each request ID access point |
| 226 | + |
| 227 | +Appendix A: Alternate solution for access on successful responses |
| 228 | +----------------------------------------------------------------- |
| 229 | + |
| 230 | +Alternatively, for successful responses, a second `send` method (that is difficult to name)w |
| 231 | +be added to the fluent client that has a return value that includes both the output and |
| 232 | +the request ID (or entire response). |
| 233 | + |
| 234 | +This solution was dismissed due to difficulty naming, and the risk of name collision. |
| 235 | + |
| 236 | +Appendix B: Adding `RequestId` as a string to outputs via model transform |
| 237 | +------------------------------------------------------------------------- |
| 238 | + |
| 239 | +The request ID could be stored on outputs by doing a model transform in `sdk-codegen` to add a |
| 240 | +`RequestId` member field. However, this causes problems when an output already has a `RequestId` field, |
| 241 | +and requires the addition of a synthetic trait to skip binding the field in the generated |
| 242 | +serializers/deserializers. |
| 243 | + |
| 244 | +[generic error]: https://docs.rs/aws-smithy-types/0.51.0/aws_smithy_types/error/struct.Error.html |
| 245 | +[`GetObjectError` example in S3]: https://docs.rs/aws-sdk-s3/0.21.0/aws_sdk_s3/error/struct.GetObjectError.html#method.request_id |
0 commit comments