1use std::fmt;
2
3use base64::Engine;
4use http::header::{HeaderName, HeaderValue, AUTHORIZATION, CONTENT_LENGTH, CONTENT_TYPE};
5use http::HeaderMap;
6use serde::Serialize;
7use url::Url;
8
9use crate::multipart::Form;
10use crate::{Body, Client, Error, Method, RequestOptions, Response};
11
12pub struct RequestBuilder {
16 client: Client,
17 request: Result<Parts, Error>,
18}
19
20struct Parts {
21 method: Method,
22 url: Url,
23 headers: HeaderMap,
24 body: Option<Body>,
25}
26
27impl RequestBuilder {
28 pub(crate) fn new(client: Client, method: Method, url: &str) -> Self {
29 let request = Url::parse(url)
30 .map(|url| Parts {
31 method,
32 url,
33 headers: HeaderMap::new(),
34 body: None,
35 })
36 .map_err(Error::builder);
37 Self { client, request }
38 }
39
40 fn with(mut self, f: impl FnOnce(&mut Parts) -> Result<(), Error>) -> Self {
41 if let Ok(parts) = &mut self.request {
42 if let Err(error) = f(parts) {
43 self.request = Err(error);
44 }
45 }
46 self
47 }
48
49 pub fn header<K, V>(self, name: K, value: V) -> Self
51 where
52 HeaderName: TryFrom<K>,
53 <HeaderName as TryFrom<K>>::Error: Into<http::Error>,
54 HeaderValue: TryFrom<V>,
55 <HeaderValue as TryFrom<V>>::Error: Into<http::Error>,
56 {
57 self.with(|parts| {
58 let name = HeaderName::try_from(name).map_err(|e| Error::builder(e.into()))?;
59 let value = HeaderValue::try_from(value).map_err(|e| Error::builder(e.into()))?;
60 parts.headers.append(name, value);
61 Ok(())
62 })
63 }
64
65 pub fn headers(self, headers: HeaderMap) -> Self {
67 self.with(|parts| {
68 parts.headers.extend(headers);
69 Ok(())
70 })
71 }
72
73 pub fn query<T: Serialize + ?Sized>(self, query: &T) -> Self {
76 self.with(|parts| {
77 {
78 let mut pairs = parts.url.query_pairs_mut();
79 query
80 .serialize(serde_urlencoded::Serializer::new(&mut pairs))
81 .map_err(Error::builder)?;
82 }
83 if parts.url.query() == Some("") {
84 parts.url.set_query(None);
85 }
86 Ok(())
87 })
88 }
89
90 pub fn bearer_auth<T: fmt::Display>(self, token: T) -> Self {
92 self.sensitive_header(AUTHORIZATION, format!("Bearer {token}"))
93 }
94
95 pub fn basic_auth<U: fmt::Display, P: fmt::Display>(
97 self,
98 username: U,
99 password: Option<P>,
100 ) -> Self {
101 let credentials = match password {
102 Some(password) => format!("{username}:{password}"),
103 None => format!("{username}:"),
104 };
105 let encoded = base64::engine::general_purpose::STANDARD.encode(credentials);
106 self.sensitive_header(AUTHORIZATION, format!("Basic {encoded}"))
107 }
108
109 pub fn sensitive_header<K, V>(self, name: K, value: V) -> Self
112 where
113 HeaderName: TryFrom<K>,
114 <HeaderName as TryFrom<K>>::Error: Into<http::Error>,
115 HeaderValue: TryFrom<V>,
116 <HeaderValue as TryFrom<V>>::Error: Into<http::Error>,
117 {
118 self.with(|parts| {
119 let name = HeaderName::try_from(name).map_err(|e| Error::builder(e.into()))?;
120 let mut value = HeaderValue::try_from(value).map_err(|e| Error::builder(e.into()))?;
121 value.set_sensitive(true);
122 parts.headers.insert(name, value);
123 Ok(())
124 })
125 }
126
127 pub fn body<T: Into<Body>>(self, body: T) -> Self {
129 self.with(|parts| {
130 parts.body = Some(body.into());
131 Ok(())
132 })
133 }
134
135 pub fn json<T: Serialize + ?Sized>(self, json: &T) -> Self {
137 self.with(|parts| {
138 let bytes = serde_json::to_vec(json).map_err(Error::builder)?;
139 parts
140 .headers
141 .entry(CONTENT_TYPE)
142 .or_insert(HeaderValue::from_static("application/json"));
143 parts.body = Some(bytes.into());
144 Ok(())
145 })
146 }
147
148 pub fn form<T: Serialize + ?Sized>(self, form: &T) -> Self {
150 self.with(|parts| {
151 let text = serde_urlencoded::to_string(form).map_err(Error::builder)?;
152 parts.headers.insert(
153 CONTENT_TYPE,
154 HeaderValue::from_static("application/x-www-form-urlencoded"),
155 );
156 parts.body = Some(text.into());
157 Ok(())
158 })
159 }
160
161 pub fn multipart(self, form: Form) -> Self {
163 self.with(|parts| {
164 let content_type =
165 HeaderValue::try_from(format!("multipart/form-data; boundary={}", form.boundary()))
166 .map_err(Error::builder)?;
167 parts.headers.insert(CONTENT_TYPE, content_type);
168 let body = form.into_body();
169 if let Some(bytes) = body.as_bytes() {
170 parts
171 .headers
172 .insert(CONTENT_LENGTH, HeaderValue::from(bytes.len()));
173 }
174 parts.body = Some(body);
175 Ok(())
176 })
177 }
178
179 pub fn options(self, options: &RequestOptions) -> Self {
183 let query = options.query_pairs();
184 let this = if query.is_empty() {
185 self
186 } else {
187 self.query(query)
188 };
189 this.with(|parts| {
190 parts.headers.extend(options.header_map().clone());
191 Ok(())
192 })
193 }
194
195 pub fn build(self) -> Result<http::Request<Body>, Error> {
197 let parts = self.request?;
198 let mut request = http::Request::builder()
199 .method(parts.method)
200 .uri(parts.url.as_str())
201 .body(parts.body.unwrap_or_default())
202 .map_err(Error::builder)?;
203 *request.headers_mut() = parts.headers;
204 Ok(request)
205 }
206
207 pub async fn send(self) -> Result<Response, Error> {
209 let client = self.client.clone();
210 client.execute(self.build()?).await
211 }
212}
213
214impl fmt::Debug for RequestBuilder {
215 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
216 match &self.request {
217 Ok(parts) => f
218 .debug_struct("RequestBuilder")
219 .field("method", &parts.method)
220 .field("url", &parts.url.as_str())
221 .field("headers", &parts.headers)
222 .finish_non_exhaustive(),
223 Err(error) => f
224 .debug_struct("RequestBuilder")
225 .field("error", error)
226 .finish(),
227 }
228 }
229}
230
231#[cfg(test)]
232mod tests {
233 use super::*;
234
235 fn builder(url: &str) -> RequestBuilder {
236 Client::default().request(Method::GET, url)
237 }
238
239 #[test]
240 fn query_pairs_are_appended_and_encoded() {
241 let request = builder("https://example.com/items?a=1")
242 .query(&[("b", "x y"), ("c", "&")])
243 .query(&[("d", 2)])
244 .build()
245 .unwrap();
246 assert_eq!(
247 request.uri(),
248 "https://example.com/items?a=1&b=x+y&c=%26&d=2"
249 );
250 }
251
252 #[test]
253 fn an_empty_query_leaves_no_question_mark() {
254 let empty: [(&str, &str); 0] = [];
255 let request = builder("https://example.com/items")
256 .query(&empty)
257 .build()
258 .unwrap();
259 assert_eq!(request.uri(), "https://example.com/items");
260 }
261
262 #[test]
263 fn json_sets_content_type_and_body() {
264 let request = builder("https://example.com")
265 .json(&serde_json::json!({"a": 1}))
266 .build()
267 .unwrap();
268 assert_eq!(request.headers()[CONTENT_TYPE], "application/json");
269 assert_eq!(request.body().as_bytes(), Some(&br#"{"a":1}"#[..]));
270 }
271
272 #[test]
273 fn form_sets_content_type_and_body() {
274 let request = builder("https://example.com")
275 .form(&[("a", "1"), ("b", "x y")])
276 .build()
277 .unwrap();
278 assert_eq!(
279 request.headers()[CONTENT_TYPE],
280 "application/x-www-form-urlencoded"
281 );
282 assert_eq!(request.body().as_bytes(), Some(&b"a=1&b=x+y"[..]));
283 }
284
285 #[test]
286 fn auth_headers_are_sensitive() {
287 let bearer = builder("https://example.com")
288 .bearer_auth("secret")
289 .build()
290 .unwrap();
291 assert_eq!(bearer.headers()[AUTHORIZATION], "Bearer secret");
292 assert!(bearer.headers()[AUTHORIZATION].is_sensitive());
293 let basic = builder("https://example.com")
294 .basic_auth("user", Some("pass"))
295 .build()
296 .unwrap();
297 assert_eq!(basic.headers()[AUTHORIZATION], "Basic dXNlcjpwYXNz");
298 assert!(basic.headers()[AUTHORIZATION].is_sensitive());
299 }
300
301 #[test]
302 fn secrets_are_exposed_only_into_sensitive_headers() {
303 use crate::{ExposeSecret, SecretString};
304 let key = SecretString::from("sk-123");
305 assert!(!format!("{key:?}").contains("sk-123"));
306 let request = builder("https://example.com")
307 .header("x-api-key", "old")
308 .sensitive_header("x-api-key", key.expose_secret())
309 .bearer_auth(key.expose_secret())
310 .build()
311 .unwrap();
312 let values: Vec<_> = request.headers().get_all("x-api-key").iter().collect();
313 assert_eq!(values, ["sk-123"]);
314 assert!(values[0].is_sensitive());
315 assert!(!format!("{:?}", request.headers()).contains("sk-123"));
316 assert_eq!(request.headers()[AUTHORIZATION], "Bearer sk-123");
317 }
318
319 #[test]
320 fn options_override_headers_and_append_query() {
321 let mut tags = HeaderMap::new();
322 tags.append("x-tag", HeaderValue::from_static("a"));
323 tags.append("x-tag", HeaderValue::from_static("b"));
324 let options = RequestOptions::new()
325 .header("x-generated", "from-options")
326 .unwrap()
327 .header(AUTHORIZATION, "Bearer other")
328 .unwrap()
329 .headers(tags)
330 .query(&[("b", "2"), ("a", "3")])
331 .unwrap();
332 let request = builder("https://example.com/items?a=1")
333 .header("x-generated", "one")
334 .header("x-generated", "two")
335 .header("x-kept", "kept")
336 .header("x-tag", "generated")
337 .bearer_auth("secret")
338 .options(&options)
339 .build()
340 .unwrap();
341 let headers = request.headers();
342 assert_eq!(
343 headers.get_all("x-generated").iter().collect::<Vec<_>>(),
344 ["from-options"]
345 );
346 assert_eq!(headers["x-kept"], "kept");
347 assert_eq!(
348 headers.get_all("x-tag").iter().collect::<Vec<_>>(),
349 ["a", "b"]
350 );
351 assert_eq!(headers[AUTHORIZATION], "Bearer other");
352 assert_eq!(request.uri(), "https://example.com/items?a=1&b=2&a=3");
353 }
354
355 #[test]
356 fn empty_options_change_nothing() {
357 let request = builder("https://example.com/items")
358 .header("x-kept", "kept")
359 .options(&RequestOptions::new())
360 .build()
361 .unwrap();
362 assert_eq!(request.uri(), "https://example.com/items");
363 assert_eq!(request.headers().len(), 1);
364 }
365
366 #[test]
367 fn errors_surface_at_build() {
368 assert!(builder("not a url").build().unwrap_err().is_builder());
369 let invalid = builder("https://example.com")
370 .header("x-bad", "line\nbreak")
371 .build();
372 assert!(invalid.unwrap_err().is_builder());
373 }
374}