azure_core/http/policies/
user_agent.rs1use crate::http::{
5 headers::{HeaderValue, USER_AGENT},
6 options::UserAgentOptions,
7};
8use std::{
9 env::consts::{ARCH, OS},
10 sync::Arc,
11};
12use typespec_client_core::http::{
13 policies::{Policy, PolicyResult},
14 Context, Request,
15};
16
17#[derive(Clone, Debug)]
19pub struct UserAgentPolicy {
20 header: HeaderValue,
21}
22
23impl<'a> UserAgentPolicy {
24 pub fn new(
34 crate_name: Option<&'a str>,
35 crate_version: Option<&'a str>,
36 options: &UserAgentOptions,
37 ) -> Self {
38 Self::new_with_rustc_version(
39 crate_name,
40 crate_version,
41 option_env!("AZSDK_RUSTC_VERSION"),
42 options,
43 )
44 }
45
46 fn new_with_rustc_version(
47 crate_name: Option<&'a str>,
48 crate_version: Option<&'a str>,
49 rustc_version: Option<&'a str>,
50 options: &UserAgentOptions,
51 ) -> Self {
52 const UNKNOWN: &str = "unknown";
53 let mut crate_name = crate_name.unwrap_or(UNKNOWN);
54 let crate_version = crate_version.unwrap_or(UNKNOWN);
55 let rustc_version = rustc_version.unwrap_or(UNKNOWN);
56 let platform_info = format!("({rustc_version}; {OS}; {ARCH})",);
57
58 if let Some(name) = crate_name.strip_prefix("azure_") {
59 crate_name = name;
60 }
61
62 const MAX_APPLICATION_ID_LEN: usize = 24;
63 let header_str = match &options.application_id {
64 Some(application_id) => {
65 if application_id.len() > MAX_APPLICATION_ID_LEN {
66 panic!(
67 "application_id must be shorter than {} characters",
68 MAX_APPLICATION_ID_LEN + 1
69 );
70 }
71
72 if !application_id.chars().all(|c| {
77 c.is_ascii_alphanumeric()
78 || matches!(
79 c,
80 '!' | '#'
81 | '$'
82 | '%'
83 | '&'
84 | '\''
85 | '*'
86 | '+'
87 | '-'
88 | '.'
89 | '^'
90 | '_'
91 | '`'
92 | '|'
93 | '~'
94 )
95 }) {
96 panic!("application_id contains invalid characters. Only RFC 9110 tokens are allowed.");
97 }
98
99 format!("{application_id} azsdk-rust-{crate_name}/{crate_version} {platform_info}")
100 }
101 None => format!("azsdk-rust-{crate_name}/{crate_version} {platform_info}"),
102 };
103
104 UserAgentPolicy {
105 header: HeaderValue::from(header_str),
106 }
107 }
108}
109
110#[async_trait::async_trait]
111impl Policy for UserAgentPolicy {
112 async fn send(
113 &self,
114 ctx: &Context,
115 request: &mut Request,
116 next: &[Arc<dyn Policy>],
117 ) -> PolicyResult {
118 request.insert_header(USER_AGENT, self.header.clone());
119 next[0].send(ctx, request, &next[1..]).await
120 }
121}
122
123#[cfg(test)]
124mod tests {
125 use super::*;
126
127 #[test]
128 fn without_application_id() {
129 let policy = UserAgentPolicy::new_with_rustc_version(
130 Some("azure_test"), Some("1.2.3"),
132 Some("4.5.6"),
133 &UserAgentOptions::default(),
134 );
135 assert_eq!(
136 policy.header.as_str(),
137 format!("azsdk-rust-test/1.2.3 (4.5.6; {OS}; {ARCH})")
138 );
139 }
140
141 #[test]
142 fn with_application_id() {
143 let options = UserAgentOptions {
144 application_id: Some("my_app".to_string()),
145 };
146 let policy = UserAgentPolicy::new_with_rustc_version(
147 Some("test"),
148 Some("1.2.3"),
149 Some("4.5.6"),
150 &options,
151 );
152 assert_eq!(
153 policy.header.as_str(),
154 format!("my_app azsdk-rust-test/1.2.3 (4.5.6; {OS}; {ARCH})")
155 );
156 }
157
158 #[test]
159 fn missing_env() {
160 let policy =
162 UserAgentPolicy::new_with_rustc_version(None, None, None, &UserAgentOptions::default());
163 assert_eq!(
164 policy.header.as_str(),
165 format!("azsdk-rust-unknown/unknown (unknown; {OS}; {ARCH})")
166 );
167 }
168
169 #[test]
170 #[should_panic(expected = "application_id must be shorter than 25 characters")]
171 fn panics_when_application_id_too_long() {
172 let options = UserAgentOptions {
173 application_id: Some(
174 "this_application_id_is_way_too_long_and_exceeds_limit".to_string(),
175 ), };
177 let _policy = UserAgentPolicy::new_with_rustc_version(
178 Some("test"),
179 Some("1.2.3"),
180 Some("4.5.6"),
181 &options,
182 );
183 }
184
185 #[test]
186 fn works_with_application_id_at_limit() {
187 let options = UserAgentOptions {
188 application_id: Some("exactly_24_characters!!!".to_string()), };
190 let policy = UserAgentPolicy::new_with_rustc_version(
191 Some("test"),
192 Some("1.2.3"),
193 Some("4.5.6"),
194 &options,
195 );
196 assert_eq!(
197 policy.header.as_str(),
198 format!("exactly_24_characters!!! azsdk-rust-test/1.2.3 (4.5.6; {OS}; {ARCH})")
199 );
200 }
201
202 #[test]
203 #[should_panic(
204 expected = "application_id contains invalid characters. Only RFC 9110 tokens are allowed."
205 )]
206 fn test_user_agent_invalid_chars() {
207 let options = UserAgentOptions {
209 application_id: Some("invalid application id".to_string()),
210 };
211 let _policy = UserAgentPolicy::new_with_rustc_version(
212 Some("test"),
213 Some("1.2.3"),
214 Some("4.5.6"),
215 &options,
216 );
217 }
218}