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 {
130 self.with(|parts| {
131 parts.body = Some(body.into());
132 Ok(())
133 })
134 }
135
136 pub fn json<T: Serialize + ?Sized>(self, json: &T) -> Self {
138 self.with(|parts| {
139 let bytes = serde_json::to_vec(json).map_err(Error::builder)?;
140 parts
141 .headers
142 .entry(CONTENT_TYPE)
143 .or_insert(HeaderValue::from_static("application/json"));
144 parts.body = Some(bytes.into());
145 Ok(())
146 })
147 }
148
149 pub fn form<T: Serialize + ?Sized>(self, form: &T) -> Self {
151 self.with(|parts| {
152 let text = serde_urlencoded::to_string(form).map_err(Error::builder)?;
153 parts.headers.insert(
154 CONTENT_TYPE,
155 HeaderValue::from_static("application/x-www-form-urlencoded"),
156 );
157 parts.body = Some(text.into());
158 Ok(())
159 })
160 }
161
162 pub fn multipart(self, form: Form) -> Self {
164 self.with(|parts| {
165 let content_type =
166 HeaderValue::try_from(format!("multipart/form-data; boundary={}", form.boundary()))
167 .map_err(Error::builder)?;
168 parts.headers.insert(CONTENT_TYPE, content_type);
169 let body = form.into_body();
170 if let Some(len) = http_body::Body::size_hint(&body).exact() {
171 parts.headers.insert(CONTENT_LENGTH, HeaderValue::from(len));
172 }
173 parts.body = Some(body);
174 Ok(())
175 })
176 }
177
178 pub fn options(self, options: &RequestOptions) -> Self {
182 let query = options.query_pairs();
183 let this = if query.is_empty() {
184 self
185 } else {
186 self.query(query)
187 };
188 this.with(|parts| {
189 parts.headers.extend(options.header_map().clone());
190 Ok(())
191 })
192 }
193
194 pub fn build(self) -> Result<http::Request<Body>, Error> {
196 let parts = self.request?;
197 let mut request = http::Request::builder()
198 .method(parts.method)
199 .uri(parts.url.as_str())
200 .body(parts.body.unwrap_or_default())
201 .map_err(Error::builder)?;
202 *request.headers_mut() = parts.headers;
203 Ok(request)
204 }
205
206 pub async fn send(self) -> Result<Response, Error> {
208 let client = self.client.clone();
209 client.execute(self.build()?).await
210 }
211}
212
213impl fmt::Debug for RequestBuilder {
214 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
215 match &self.request {
216 Ok(parts) => f
217 .debug_struct("RequestBuilder")
218 .field("method", &parts.method)
219 .field("url", &parts.url.as_str())
220 .field("headers", &parts.headers)
221 .finish_non_exhaustive(),
222 Err(error) => f
223 .debug_struct("RequestBuilder")
224 .field("error", error)
225 .finish(),
226 }
227 }
228}
229
230#[cfg(test)]
231mod tests {
232 use super::*;
233
234 fn builder(url: &str) -> RequestBuilder {
235 Client::default().request(Method::GET, url)
236 }
237
238 #[test]
239 fn query_pairs_are_appended_and_encoded() {
240 let request = builder("https://example.com/items?a=1")
241 .query(&[("b", "x y"), ("c", "&")])
242 .query(&[("d", 2)])
243 .build()
244 .unwrap();
245 assert_eq!(
246 request.uri(),
247 "https://example.com/items?a=1&b=x+y&c=%26&d=2"
248 );
249 }
250
251 #[test]
252 fn an_empty_query_leaves_no_question_mark() {
253 let empty: [(&str, &str); 0] = [];
254 let request = builder("https://example.com/items")
255 .query(&empty)
256 .build()
257 .unwrap();
258 assert_eq!(request.uri(), "https://example.com/items");
259 }
260
261 #[test]
262 fn json_sets_content_type_and_body() {
263 let request = builder("https://example.com")
264 .json(&serde_json::json!({"a": 1}))
265 .build()
266 .unwrap();
267 assert_eq!(request.headers()[CONTENT_TYPE], "application/json");
268 assert_eq!(request.body().as_bytes(), Some(&br#"{"a":1}"#[..]));
269 }
270
271 #[test]
272 fn form_sets_content_type_and_body() {
273 let request = builder("https://example.com")
274 .form(&[("a", "1"), ("b", "x y")])
275 .build()
276 .unwrap();
277 assert_eq!(
278 request.headers()[CONTENT_TYPE],
279 "application/x-www-form-urlencoded"
280 );
281 assert_eq!(request.body().as_bytes(), Some(&b"a=1&b=x+y"[..]));
282 }
283
284 #[test]
285 fn auth_headers_are_sensitive() {
286 let bearer = builder("https://example.com")
287 .bearer_auth("secret")
288 .build()
289 .unwrap();
290 assert_eq!(bearer.headers()[AUTHORIZATION], "Bearer secret");
291 assert!(bearer.headers()[AUTHORIZATION].is_sensitive());
292 let basic = builder("https://example.com")
293 .basic_auth("user", Some("pass"))
294 .build()
295 .unwrap();
296 assert_eq!(basic.headers()[AUTHORIZATION], "Basic dXNlcjpwYXNz");
297 assert!(basic.headers()[AUTHORIZATION].is_sensitive());
298 }
299
300 #[test]
301 fn secrets_are_exposed_only_into_sensitive_headers() {
302 use crate::{ExposeSecret, SecretString};
303 let key = SecretString::from("sk-123");
304 assert!(!format!("{key:?}").contains("sk-123"));
305 let request = builder("https://example.com")
306 .header("x-api-key", "old")
307 .sensitive_header("x-api-key", key.expose_secret())
308 .bearer_auth(key.expose_secret())
309 .build()
310 .unwrap();
311 let values: Vec<_> = request.headers().get_all("x-api-key").iter().collect();
312 assert_eq!(values, ["sk-123"]);
313 assert!(values[0].is_sensitive());
314 assert!(!format!("{:?}", request.headers()).contains("sk-123"));
315 assert_eq!(request.headers()[AUTHORIZATION], "Bearer sk-123");
316 }
317
318 #[test]
319 fn options_override_headers_and_append_query() {
320 let mut tags = HeaderMap::new();
321 tags.append("x-tag", HeaderValue::from_static("a"));
322 tags.append("x-tag", HeaderValue::from_static("b"));
323 let options = RequestOptions::new()
324 .header("x-generated", "from-options")
325 .unwrap()
326 .header(AUTHORIZATION, "Bearer other")
327 .unwrap()
328 .headers(tags)
329 .query(&[("b", "2"), ("a", "3")])
330 .unwrap();
331 let request = builder("https://example.com/items?a=1")
332 .header("x-generated", "one")
333 .header("x-generated", "two")
334 .header("x-kept", "kept")
335 .header("x-tag", "generated")
336 .bearer_auth("secret")
337 .options(&options)
338 .build()
339 .unwrap();
340 let headers = request.headers();
341 assert_eq!(
342 headers.get_all("x-generated").iter().collect::<Vec<_>>(),
343 ["from-options"]
344 );
345 assert_eq!(headers["x-kept"], "kept");
346 assert_eq!(
347 headers.get_all("x-tag").iter().collect::<Vec<_>>(),
348 ["a", "b"]
349 );
350 assert_eq!(headers[AUTHORIZATION], "Bearer other");
351 assert_eq!(request.uri(), "https://example.com/items?a=1&b=2&a=3");
352 }
353
354 #[test]
355 fn empty_options_change_nothing() {
356 let request = builder("https://example.com/items")
357 .header("x-kept", "kept")
358 .options(&RequestOptions::new())
359 .build()
360 .unwrap();
361 assert_eq!(request.uri(), "https://example.com/items");
362 assert_eq!(request.headers().len(), 1);
363 }
364
365 #[test]
366 fn errors_surface_at_build() {
367 assert!(builder("not a url").build().unwrap_err().is_builder());
368 let invalid = builder("https://example.com")
369 .header("x-bad", "line\nbreak")
370 .build();
371 assert!(invalid.unwrap_err().is_builder());
372 }
373}