Skip to content

Commit 299d9c9

Browse files
authored
Add x-ms-client-request-id pipeline policy (Azure#2406)
* Add `x-ms-client-request-id` pipeline policy Resolves Azure#2240. This also supports setting a custom header name and, thus, must be added by each client to the per-call pipeline policies instead of being added automatically. * Resolve meeting feedback * Always add the policy, but support custom policies. * For now, caller can add the existing `ClientRequestId` header to `ClientMethodOptions::context`. * Resolve PR feedback
1 parent bb6b6f9 commit 299d9c9

File tree

11 files changed

+419
-28
lines changed

11 files changed

+419
-28
lines changed

.vscode/cspell.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@
6464
"seekable",
6565
"servicebus",
6666
"stylesheet",
67+
"telemetered",
6768
"typespec",
6869
"undelete",
6970
"upvote",

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

sdk/core/azure_core/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
- Added `azure_core::process::Executor` to run commands asynchronously.
88
The `tokio` feature is disabled by default so `std::process::Command` is used; otherwise, if enabled, `tokio::process::Command` is used.
99
- Added `http` module containing all functions, modules, and types from `typespec_client_core::http`.
10+
- Added `azure_core::http::policies::ClientRequestIdPolicy` to every pipeline. Client libraries can add with custom header name instead.
1011
- Moved `Pager` from `typespec_client_core::http` to `azure_core::http` module since it is Azure-specific.
1112
- Re-exported `Body`, `Request`, and `RequestContent` from `http::request` module.
1213
- Re-exported `create_enum`, `create_extensible_enum` macros from `typespec_client_core`.

sdk/core/azure_core/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ typespec_client_core = { workspace = true, features = ["http", "json"] }
3333
rustc_version.workspace = true
3434

3535
[dev-dependencies]
36+
azure_core_test.workspace = true
3637
azure_identity.workspace = true
3738
azure_security_keyvault_secrets.path = "../../keyvault/azure_security_keyvault_secrets"
3839
thiserror.workspace = true
Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,9 @@
11
// Copyright (c) Microsoft Corporation. All rights reserved.
22
// Licensed under the MIT License.
33

4-
use typespec_client_core::setters;
5-
64
/// Telemetry options.
75
#[derive(Clone, Debug, Default)]
86
pub struct TelemetryOptions {
9-
/// Optional application ID to telemetry.
10-
pub(crate) application_id: Option<String>,
11-
}
12-
13-
impl TelemetryOptions {
14-
setters! {
15-
#[doc = "Set the application ID to telemetry."]
16-
application_id: String => Some(application_id),
17-
}
7+
/// Set the application ID in the `User-Agent` header that can be telemetered
8+
pub application_id: Option<String>,
189
}

sdk/core/azure_core/src/http/pipeline.rs

Lines changed: 149 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,13 @@
11
// Copyright (c) Microsoft Corporation. All rights reserved.
22
// Licensed under the MIT License.
33

4+
use super::policies::ClientRequestIdPolicy;
45
use crate::http::{options::TelemetryOptions, policies::TelemetryPolicy};
5-
use std::{ops::Deref, sync::Arc};
6+
use std::{
7+
any::{Any, TypeId},
8+
ops::Deref,
9+
sync::Arc,
10+
};
611
use typespec_client_core::http::{self, policies::Policy};
712

813
/// Execution pipeline.
@@ -12,15 +17,16 @@ use typespec_client_core::http::{self, policies::Policy};
1217
/// 1. Client library-specified per-call policies are executed. Per-call policies can fail and bail out of the pipeline
1318
/// immediately.
1419
/// 2. User-specified per-call policies are executed.
15-
/// 3. Telemetry policy.
16-
/// 4. Retry policy. It allows to re-execute the following policies.
20+
/// 3. Built-in per-call policies are executed. If a [`ClientRequestIdPolicy`] was not added to `per_call_policies`,
21+
/// the default will be added automatically.
22+
/// 4. The retry policy is executed. It allows to re-execute the following policies.
1723
/// 5. Client library-specified per-retry policies. Per-retry polices are always executed at least once but are re-executed
1824
/// in case of retries.
1925
/// 6. User-specified per-retry policies are executed.
20-
/// 7. Authorization policy. Authorization can depend on the HTTP headers and/or the request body so it
26+
/// 7. The authorization policy is executed. Authorization can depend on the HTTP headers and/or the request body so it
2127
/// must be executed right before sending the request to the transport. Also, the authorization
2228
/// can depend on the current time so it must be executed at every retry.
23-
/// 8. Transport policy. Transport policy is always the last policy and is the policy that
29+
/// 8. The transport policy is executed. Transport policy is always the last policy and is the policy that
2430
/// actually constructs the `Response` to be passed up the pipeline.
2531
///
2632
/// A pipeline is immutable. In other words a policy can either succeed and call the following
@@ -45,13 +51,20 @@ impl Pipeline {
4551
) -> Self {
4652
let mut per_call_policies = per_call_policies.clone();
4753

54+
if per_call_policies
55+
.iter()
56+
.all(|policy| TypeId::of::<ClientRequestIdPolicy>() != policy.type_id())
57+
{
58+
per_call_policies.push(Arc::new(ClientRequestIdPolicy::default()));
59+
}
60+
4861
let telemetry_policy = TelemetryPolicy::new(
4962
crate_name,
5063
crate_version,
5164
// TODO: &options.telemetry.unwrap_or_default(),
5265
&TelemetryOptions::default(),
5366
);
54-
per_call_policies.insert(0, Arc::new(telemetry_policy));
67+
per_call_policies.push(Arc::new(telemetry_policy));
5568

5669
Self(http::Pipeline::new(
5770
options,
@@ -68,3 +81,133 @@ impl Deref for Pipeline {
6881
&self.0
6982
}
7083
}
84+
85+
#[cfg(test)]
86+
mod tests {
87+
use super::*;
88+
use crate::{
89+
http::{
90+
headers::{self, HeaderName, Headers},
91+
policies::Policy,
92+
request::options::ClientRequestId,
93+
Context, Method, Request, Response, StatusCode, TransportOptions,
94+
},
95+
Bytes,
96+
};
97+
use azure_core_test::http::MockHttpClient;
98+
use futures::FutureExt as _;
99+
use std::sync::Arc;
100+
use typespec_client_core::http::ClientOptions;
101+
102+
#[tokio::test]
103+
async fn pipeline_with_custom_client_request_id_policy() {
104+
// Arrange
105+
const CUSTOM_HEADER_NAME: &str = "x-custom-request-id";
106+
const CUSTOM_HEADER: HeaderName = HeaderName::from_static(CUSTOM_HEADER_NAME);
107+
const CLIENT_REQUEST_ID: &str = "custom-request-id";
108+
109+
let mut ctx = Context::new();
110+
ctx.insert(ClientRequestId::new(CLIENT_REQUEST_ID.to_string()));
111+
112+
let transport = TransportOptions::new(Arc::new(MockHttpClient::new(|req| {
113+
async {
114+
// Assert
115+
let header_value = req
116+
.headers()
117+
.get_optional_str(&CUSTOM_HEADER)
118+
.expect("Custom header should be present");
119+
assert_eq!(
120+
header_value, CLIENT_REQUEST_ID,
121+
"Custom header value should match the client request ID"
122+
);
123+
124+
Ok(Response::from_bytes(
125+
StatusCode::Ok,
126+
Headers::new(),
127+
Bytes::new(),
128+
))
129+
}
130+
.boxed()
131+
})));
132+
let options = ClientOptions {
133+
transport: Some(transport),
134+
..Default::default()
135+
};
136+
137+
let per_call_policies: Vec<Arc<dyn Policy>> =
138+
vec![
139+
Arc::new(ClientRequestIdPolicy::with_header_name(CUSTOM_HEADER_NAME))
140+
as Arc<dyn Policy>,
141+
];
142+
let per_retry_policies = vec![];
143+
144+
let pipeline = Pipeline::new(
145+
Some("test-crate"),
146+
Some("1.0.0"),
147+
options,
148+
per_call_policies,
149+
per_retry_policies,
150+
);
151+
152+
let mut request = Request::new("https://example.com".parse().unwrap(), Method::Get);
153+
154+
// Act
155+
pipeline
156+
.send::<()>(&ctx, &mut request)
157+
.await
158+
.expect("Pipeline execution failed");
159+
}
160+
161+
#[tokio::test]
162+
async fn pipeline_without_client_request_id_policy() {
163+
// Arrange
164+
const CLIENT_REQUEST_ID: &str = "default-request-id";
165+
166+
let mut ctx = Context::new();
167+
ctx.insert(ClientRequestId::new(CLIENT_REQUEST_ID.to_string()));
168+
169+
let transport = TransportOptions::new(Arc::new(MockHttpClient::new(|req| {
170+
async {
171+
// Assert
172+
let header_value = req
173+
.headers()
174+
.get_optional_str(&headers::CLIENT_REQUEST_ID)
175+
.expect("Default header should be present");
176+
assert_eq!(
177+
header_value, CLIENT_REQUEST_ID,
178+
"Default header value should match the client request ID"
179+
);
180+
181+
Ok(Response::from_bytes(
182+
StatusCode::Ok,
183+
Headers::new(),
184+
Bytes::new(),
185+
))
186+
}
187+
.boxed()
188+
})));
189+
let options = ClientOptions {
190+
transport: Some(transport),
191+
..Default::default()
192+
};
193+
194+
let per_call_policies = vec![]; // No ClientRequestIdPolicy added
195+
let per_retry_policies = vec![];
196+
197+
let pipeline = Pipeline::new(
198+
Some("test-crate"),
199+
Some("1.0.0"),
200+
options,
201+
per_call_policies,
202+
per_retry_policies,
203+
);
204+
205+
let mut request = Request::new("https://example.com".parse().unwrap(), Method::Get);
206+
207+
// Act
208+
pipeline
209+
.send::<()>(&ctx, &mut request)
210+
.await
211+
.expect("Pipeline execution failed");
212+
}
213+
}

0 commit comments

Comments
 (0)