Skip to content

Commit 6e595d2

Browse files
authored
Define distinct ClientOptions in azure_core (#2737)
* Define distinct ClientOptions in azure_core This sets us up to have Azure-specific client options like UserAgentOptions nee TelemetryOptions. Also allows ability to disable sending the User-Agent like in the Go SDK. Fixes #1753 * Use deconstruction to efficiently split up ClientOptions * Fix lint
1 parent 10262bf commit 6e595d2

File tree

8 files changed

+225
-78
lines changed

8 files changed

+225
-78
lines changed

sdk/core/azure_core/CHANGELOG.md

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,24 @@
44

55
### Features Added
66

7-
- Added `get_async_runtime()` and `set_async_runtime()` to allow customers to replace
8-
the asynchronous runtime used by the Azure SDK.
7+
- Added `get_async_runtime()` and `set_async_runtime()` to allow customers to replace the asynchronous runtime used by the Azure SDK.
8+
- Added `UserAgentOptions::enabled` to allow disabling sending the `User-Agent` header.
99

1010
### Breaking Changes
1111

12+
- `azure_core::http::Pipeline::new` now takes an `azure_core::http::ClientOptions` which is defined in `azure_core`, but convertible to `typespec_client_core::http::ClientOptions`.
1213
- Moved `process::Executor` to `azure_identity`.
14+
- Removed `Pipeline::replace_policy`.
1315
- Renamed `azure_core::date` to `azure_core::time` and added `azure_core::time::Duration` as the standard "duration" type for the SDK.
16+
- Renamed `TelemetryOptions` to `UserAgentOptions`.
17+
- Renamed `TelemetryPolicy` to `UserAgentPolicy`.
1418

1519
### Bugs Fixed
1620

1721
### Other Changes
1822

23+
- The `CustomHeadersPolicy` is executed after the retry policy in the `Pipeline`.
24+
1925
## 0.25.0 (2025-06-06)
2026

2127
### Features Added
Lines changed: 42 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,48 @@
11
// Copyright (c) Microsoft Corporation. All rights reserved.
22
// Licensed under the MIT License.
33

4-
mod telemetry;
4+
mod user_agent;
55

6-
pub use telemetry::*;
6+
use std::sync::Arc;
7+
use typespec_client_core::http::policies::Policy;
78
pub use typespec_client_core::http::{
8-
ClientMethodOptions, ClientOptions, ExponentialRetryOptions, FixedRetryOptions, RetryOptions,
9-
TransportOptions,
9+
ClientMethodOptions, ExponentialRetryOptions, FixedRetryOptions, RetryOptions, TransportOptions,
1010
};
11+
pub use user_agent::*;
12+
13+
/// Client options allow customization of general client policies, retry options, and more.
14+
#[derive(Clone, Debug, Default)]
15+
pub struct ClientOptions {
16+
/// Policies called per call.
17+
pub per_call_policies: Vec<Arc<dyn Policy>>,
18+
19+
/// Policies called per try.
20+
pub per_try_policies: Vec<Arc<dyn Policy>>,
21+
22+
/// Retry options.
23+
pub retry: Option<RetryOptions>,
24+
25+
/// Transport options.
26+
pub transport: Option<TransportOptions>,
27+
28+
/// User-Agent telemetry options.
29+
pub user_agent: Option<UserAgentOptions>,
30+
}
31+
32+
impl ClientOptions {
33+
/// Efficiently deconstructs into owned [`typespec_client_core::http::ClientOptions`] as well as unwrapped or default Azure-specific options.
34+
///
35+
/// If instead we implemented [`Into`], we'd have to clone Azure-specific options instead of moving memory of [`Some`] values.
36+
pub(in crate::http) fn deconstruct(
37+
self,
38+
) -> (UserAgentOptions, typespec_client_core::http::ClientOptions) {
39+
let options = typespec_client_core::http::ClientOptions {
40+
per_call_policies: self.per_call_policies,
41+
per_try_policies: self.per_try_policies,
42+
retry: self.retry,
43+
transport: self.transport,
44+
};
45+
46+
(self.user_agent.unwrap_or_default(), options)
47+
}
48+
}
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
// Copyright (c) Microsoft Corporation. All rights reserved.
22
// Licensed under the MIT License.
33

4-
/// Telemetry options.
4+
/// Policy options to telemeter the `User-Agent` header.
55
#[derive(Clone, Debug, Default)]
6-
pub struct TelemetryOptions {
7-
/// Set the application ID in the `User-Agent` header that can be telemetered
6+
pub struct UserAgentOptions {
7+
/// Set the application ID in the `User-Agent` header that can be telemetered.
88
pub application_id: Option<String>,
9+
10+
/// Disable to prevent sending the `User-Agent` header in requests.
11+
pub disabled: bool,
912
}

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

Lines changed: 135 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -2,32 +2,31 @@
22
// Licensed under the MIT License.
33

44
use super::policies::ClientRequestIdPolicy;
5-
use crate::http::{options::TelemetryOptions, policies::TelemetryPolicy};
5+
use crate::http::{
6+
policies::{Policy, UserAgentPolicy},
7+
ClientOptions,
8+
};
69
use std::{
710
any::{Any, TypeId},
811
ops::Deref,
912
sync::Arc,
1013
};
11-
use typespec_client_core::http::{self, policies::Policy};
14+
use typespec_client_core::http;
1215

1316
/// Execution pipeline.
1417
///
1518
/// A pipeline follows a precise flow:
1619
///
1720
/// 1. Client library-specified per-call policies are executed. Per-call policies can fail and bail out of the pipeline
1821
/// immediately.
19-
/// 2. User-specified per-call policies are executed.
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.
23-
/// 5. Client library-specified per-retry policies. Per-retry polices are always executed at least once but are re-executed
24-
/// in case of retries.
25-
/// 6. User-specified per-retry policies are executed.
26-
/// 7. The authorization policy is executed. Authorization can depend on the HTTP headers and/or the request body so it
27-
/// must be executed right before sending the request to the transport. Also, the authorization
28-
/// can depend on the current time so it must be executed at every retry.
29-
/// 8. The transport policy is executed. Transport policy is always the last policy and is the policy that
30-
/// actually constructs the `Response` to be passed up the pipeline.
22+
/// 2. User-specified per-call policies in [`ClientOptions::per_call_policies`] are executed.
23+
/// 3. The retry policy is executed. It allows to re-execute the following policies.
24+
/// 4. The [`CustomHeadersPolicy`](crate::http::policies::CustomHeadersPolicy) is executed
25+
/// 5. Client library-specified per-retry policies. Per-retry polices are always executed at least once but are
26+
/// re-executed in case of retries.
27+
/// 6. User-specified per-retry policies in [`ClientOptions::per_try_policies`] are executed.
28+
/// 7. The transport policy is executed. Transport policy is always the last policy and is the policy that
29+
/// actually constructs the [`RawResponse`](http::RawResponse) to be passed up the pipeline.
3130
///
3231
/// A pipeline is immutable. In other words a policy can either succeed and call the following
3332
/// policy of fail and return to the calling policy. Arbitrary policy "skip" must be avoided (but
@@ -45,35 +44,34 @@ impl Pipeline {
4544
pub fn new(
4645
crate_name: Option<&'static str>,
4746
crate_version: Option<&'static str>,
48-
options: http::ClientOptions,
47+
options: ClientOptions,
4948
per_call_policies: Vec<Arc<dyn Policy>>,
50-
per_retry_policies: Vec<Arc<dyn Policy>>,
49+
per_try_policies: Vec<Arc<dyn Policy>>,
5150
) -> Self {
5251
let mut per_call_policies = per_call_policies.clone();
52+
push_unique(&mut per_call_policies, ClientRequestIdPolicy::default());
5353

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()));
54+
let (user_agent, options) = options.deconstruct();
55+
if !user_agent.disabled {
56+
let telemetry_policy = UserAgentPolicy::new(crate_name, crate_version, &user_agent);
57+
push_unique(&mut per_call_policies, telemetry_policy);
5958
}
6059

61-
let telemetry_policy = TelemetryPolicy::new(
62-
crate_name,
63-
crate_version,
64-
// TODO: &options.telemetry.unwrap_or_default(),
65-
&TelemetryOptions::default(),
66-
);
67-
per_call_policies.push(Arc::new(telemetry_policy));
68-
6960
Self(http::Pipeline::new(
7061
options,
7162
per_call_policies,
72-
per_retry_policies,
63+
per_try_policies,
7364
))
7465
}
7566
}
7667

68+
#[inline]
69+
fn push_unique<T: Policy + 'static>(policies: &mut Vec<Arc<dyn Policy>>, policy: T) {
70+
if policies.iter().all(|p| TypeId::of::<T>() != p.type_id()) {
71+
policies.push(Arc::new(policy));
72+
}
73+
}
74+
7775
// TODO: Should we instead use the newtype pattern?
7876
impl Deref for Pipeline {
7977
type Target = http::Pipeline;
@@ -90,14 +88,14 @@ mod tests {
9088
headers::{self, HeaderName, Headers},
9189
policies::Policy,
9290
request::options::ClientRequestId,
93-
Context, Method, Request, StatusCode, TransportOptions,
91+
ClientOptions, Context, Method, RawResponse, Request, StatusCode, TransportOptions,
92+
UserAgentOptions,
9493
},
9594
Bytes,
9695
};
9796
use azure_core_test::http::MockHttpClient;
9897
use futures::FutureExt as _;
9998
use std::sync::Arc;
100-
use typespec_client_core::http::{ClientOptions, RawResponse};
10199

102100
#[tokio::test]
103101
async fn pipeline_with_custom_client_request_id_policy() {
@@ -210,4 +208,109 @@ mod tests {
210208
.await
211209
.expect("Pipeline execution failed");
212210
}
211+
212+
#[tokio::test]
213+
async fn pipeline_with_user_agent_enabled_default() {
214+
// Arrange
215+
let ctx = Context::new();
216+
217+
let transport = TransportOptions::new(Arc::new(MockHttpClient::new(|req| {
218+
async {
219+
// Assert
220+
let user_agent = req
221+
.headers()
222+
.get_optional_str(&headers::USER_AGENT)
223+
.expect("User-Agent header should be present by default");
224+
// The default user agent format is: azsdk-rust-<crate_name>/<crate_version> (<rustc_version>; <OS>; <ARCH>)
225+
// Since we can't know the rustc version at runtime, just check the prefix and crate/version
226+
assert!(
227+
user_agent.starts_with("azsdk-rust-test-crate/1.0.0 "),
228+
"User-Agent header should start with expected prefix, got: {}",
229+
user_agent
230+
);
231+
232+
Ok(RawResponse::from_bytes(
233+
StatusCode::Ok,
234+
Headers::new(),
235+
Bytes::new(),
236+
))
237+
}
238+
.boxed()
239+
})));
240+
let options = ClientOptions {
241+
transport: Some(transport),
242+
..Default::default()
243+
};
244+
245+
let per_call_policies = vec![];
246+
let per_retry_policies = vec![];
247+
248+
let pipeline = Pipeline::new(
249+
Some("test-crate"),
250+
Some("1.0.0"),
251+
options,
252+
per_call_policies,
253+
per_retry_policies,
254+
);
255+
256+
let mut request = Request::new("https://example.com".parse().unwrap(), Method::Get);
257+
258+
// Act
259+
pipeline
260+
.send(&ctx, &mut request)
261+
.await
262+
.expect("Pipeline execution failed");
263+
}
264+
265+
#[tokio::test]
266+
async fn pipeline_with_user_agent_disabled() {
267+
// Arrange
268+
let ctx = Context::new();
269+
270+
let transport = TransportOptions::new(Arc::new(MockHttpClient::new(|req| {
271+
async {
272+
// Assert
273+
let user_agent = req.headers().get_optional_str(&headers::USER_AGENT);
274+
assert!(
275+
user_agent.is_none(),
276+
"User-Agent header should not be present when disabled"
277+
);
278+
279+
Ok(RawResponse::from_bytes(
280+
StatusCode::Ok,
281+
Headers::new(),
282+
Bytes::new(),
283+
))
284+
}
285+
.boxed()
286+
})));
287+
let user_agent = UserAgentOptions {
288+
disabled: true,
289+
..Default::default()
290+
};
291+
let options = ClientOptions {
292+
transport: Some(transport),
293+
user_agent: Some(user_agent),
294+
..Default::default()
295+
};
296+
297+
let per_call_policies = vec![];
298+
let per_retry_policies = vec![];
299+
300+
let pipeline = Pipeline::new(
301+
Some("test-crate"),
302+
Some("1.0.0"),
303+
options,
304+
per_call_policies,
305+
per_retry_policies,
306+
);
307+
308+
let mut request = Request::new("https://example.com".parse().unwrap(), Method::Get);
309+
310+
// Act
311+
pipeline
312+
.send(&ctx, &mut request)
313+
.await
314+
.expect("Pipeline execution failed");
315+
}
213316
}

sdk/core/azure_core/src/http/policies/mod.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@
55
66
mod bearer_token_policy;
77
mod client_request_id;
8-
mod telemetry;
8+
mod user_agent;
99

1010
pub use bearer_token_policy::BearerTokenCredentialPolicy;
1111
pub use client_request_id::*;
12-
pub use telemetry::*;
1312
pub use typespec_client_core::http::policies::*;
13+
pub use user_agent::*;

0 commit comments

Comments
 (0)