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::provider::{rest_of_path, trimmed_path};
11use crate::request_options::Applied;
12use crate::{Body, Client, Error, Method, RequestOptions, Response};
13
14pub struct RequestBuilder {
18 client: Client,
19 request: Result<Parts, Error>,
20}
21
22struct Parts {
23 method: Method,
24 url: Url,
25 headers: HeaderMap,
26 body: Option<Body>,
27 base: Option<Url>,
29 applied: Option<Applied>,
31}
32
33impl RequestBuilder {
34 pub(crate) fn new(client: Client, method: Method, url: &str) -> Self {
35 let request = Url::parse(url)
36 .map(|url| Parts {
37 method,
38 url,
39 headers: HeaderMap::new(),
40 body: None,
41 base: None,
42 applied: None,
43 })
44 .map_err(Error::builder);
45 Self { client, request }
46 }
47
48 fn with(mut self, f: impl FnOnce(&mut Parts) -> Result<(), Error>) -> Self {
49 if let Ok(parts) = &mut self.request {
50 if let Err(error) = f(parts) {
51 self.request = Err(error);
52 }
53 }
54 self
55 }
56
57 pub fn header<K, V>(self, name: K, value: V) -> Self
59 where
60 HeaderName: TryFrom<K>,
61 <HeaderName as TryFrom<K>>::Error: Into<http::Error>,
62 HeaderValue: TryFrom<V>,
63 <HeaderValue as TryFrom<V>>::Error: Into<http::Error>,
64 {
65 self.with(|parts| {
66 let name = HeaderName::try_from(name).map_err(|e| Error::builder(e.into()))?;
67 let value = HeaderValue::try_from(value).map_err(|e| Error::builder(e.into()))?;
68 parts.headers.append(name, value);
69 Ok(())
70 })
71 }
72
73 pub fn headers(self, headers: HeaderMap) -> Self {
75 self.with(|parts| {
76 parts.headers.extend(headers);
77 Ok(())
78 })
79 }
80
81 pub fn query<T: Serialize + ?Sized>(self, query: &T) -> Self {
84 self.with(|parts| {
85 {
86 let mut pairs = parts.url.query_pairs_mut();
87 query
88 .serialize(serde_urlencoded::Serializer::new(&mut pairs))
89 .map_err(Error::builder)?;
90 }
91 if parts.url.query() == Some("") {
92 parts.url.set_query(None);
93 }
94 Ok(())
95 })
96 }
97
98 pub fn bearer_auth<T: fmt::Display>(self, token: T) -> Self {
100 self.sensitive_header(AUTHORIZATION, format!("Bearer {token}"))
101 }
102
103 pub fn basic_auth<U: fmt::Display, P: fmt::Display>(
105 self,
106 username: U,
107 password: Option<P>,
108 ) -> Self {
109 let credentials = match password {
110 Some(password) => format!("{username}:{password}"),
111 None => format!("{username}:"),
112 };
113 let encoded = base64::engine::general_purpose::STANDARD.encode(credentials);
114 self.sensitive_header(AUTHORIZATION, format!("Basic {encoded}"))
115 }
116
117 pub fn sensitive_header<K, V>(self, name: K, value: V) -> Self
120 where
121 HeaderName: TryFrom<K>,
122 <HeaderName as TryFrom<K>>::Error: Into<http::Error>,
123 HeaderValue: TryFrom<V>,
124 <HeaderValue as TryFrom<V>>::Error: Into<http::Error>,
125 {
126 self.with(|parts| {
127 let name = HeaderName::try_from(name).map_err(|e| Error::builder(e.into()))?;
128 let mut value = HeaderValue::try_from(value).map_err(|e| Error::builder(e.into()))?;
129 value.set_sensitive(true);
130 parts.headers.insert(name, value);
131 Ok(())
132 })
133 }
134
135 pub fn body<T: Into<Body>>(self, body: T) -> Self {
138 self.with(|parts| {
139 parts.body = Some(body.into());
140 Ok(())
141 })
142 }
143
144 pub fn json<T: Serialize + ?Sized>(self, json: &T) -> Self {
146 self.with(|parts| {
147 let bytes = serde_json::to_vec(json).map_err(Error::builder)?;
148 parts
149 .headers
150 .entry(CONTENT_TYPE)
151 .or_insert(HeaderValue::from_static("application/json"));
152 parts.body = Some(bytes.into());
153 Ok(())
154 })
155 }
156
157 pub fn form<T: Serialize + ?Sized>(self, form: &T) -> Self {
159 self.with(|parts| {
160 let text = serde_urlencoded::to_string(form).map_err(Error::builder)?;
161 parts.headers.insert(
162 CONTENT_TYPE,
163 HeaderValue::from_static("application/x-www-form-urlencoded"),
164 );
165 parts.body = Some(text.into());
166 Ok(())
167 })
168 }
169
170 pub fn multipart(self, form: Form) -> Self {
172 self.with(|parts| {
173 let content_type =
174 HeaderValue::try_from(format!("multipart/form-data; boundary={}", form.boundary()))
175 .map_err(Error::builder)?;
176 parts.headers.insert(CONTENT_TYPE, content_type);
177 let body = form.into_body();
178 if let Some(len) = http_body::Body::size_hint(&body).exact() {
179 parts.headers.insert(CONTENT_LENGTH, HeaderValue::from(len));
180 }
181 parts.body = Some(body);
182 Ok(())
183 })
184 }
185
186 pub fn base(self, base_url: &str) -> Self {
189 self.with(|parts| {
190 parts.base = Url::parse(base_url).ok();
191 Ok(())
192 })
193 }
194
195 pub fn options(self, options: &RequestOptions) -> Self {
200 let query = options.query_pairs();
201 let this = if query.is_empty() {
202 self
203 } else {
204 self.query(query)
205 };
206 this.with(|parts| {
207 if let Some(to) = options.base_url_parsed() {
208 rebase(&mut parts.url, parts.base.as_ref(), to)?;
209 parts.base = Some(to.clone());
210 }
211 parts.headers.extend(options.header_map().clone());
212 if !options.is_empty() {
213 parts
214 .applied
215 .get_or_insert_with(Applied::default)
216 .add(options);
217 }
218 Ok(())
219 })
220 }
221
222 pub fn build(self) -> Result<http::Request<Body>, Error> {
224 let parts = self.request?;
225 let mut request = http::Request::builder()
226 .method(parts.method)
227 .uri(parts.url.as_str())
228 .body(parts.body.unwrap_or_default())
229 .map_err(Error::builder)?;
230 *request.headers_mut() = parts.headers;
231 if let Some(applied) = parts.applied {
232 request.extensions_mut().insert(applied);
233 }
234 Ok(request)
235 }
236
237 pub async fn send(self) -> Result<Response, Error> {
239 let client = self.client.clone();
240 client.execute(self.build()?).await
241 }
242}
243
244fn rebase(url: &mut Url, from: Option<&Url>, to: &Url) -> Result<(), Error> {
247 let rest = from
248 .and_then(|from| rest_of_path(url, from))
249 .unwrap_or_else(|| url.path().to_owned());
250 let mut path = trimmed_path(to).to_owned();
251 path.push_str(&rest);
252 url.set_scheme(to.scheme())
253 .map_err(|()| Error::builder(format!("cannot move {url} to {to}")))?;
254 url.set_host(to.host_str()).map_err(Error::builder)?;
255 url.set_port(to.port())
256 .map_err(|()| Error::builder(format!("cannot move {url} to {to}")))?;
257 url.set_path(if path.is_empty() { "/" } else { &path });
258 Ok(())
259}
260
261impl fmt::Debug for RequestBuilder {
262 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
263 match &self.request {
264 Ok(parts) => f
265 .debug_struct("RequestBuilder")
266 .field("method", &parts.method)
267 .field("url", &parts.url.as_str())
268 .field("headers", &parts.headers)
269 .finish_non_exhaustive(),
270 Err(error) => f
271 .debug_struct("RequestBuilder")
272 .field("error", error)
273 .finish(),
274 }
275 }
276}
277
278#[cfg(test)]
279mod tests {
280 use super::*;
281
282 fn builder(url: &str) -> RequestBuilder {
283 Client::default().request(Method::GET, url)
284 }
285
286 #[test]
287 fn query_pairs_are_appended_and_encoded() {
288 let request = builder("https://example.com/items?a=1")
289 .query(&[("b", "x y"), ("c", "&")])
290 .query(&[("d", 2)])
291 .build()
292 .unwrap();
293 assert_eq!(
294 request.uri(),
295 "https://example.com/items?a=1&b=x+y&c=%26&d=2"
296 );
297 }
298
299 #[test]
300 fn an_empty_query_leaves_no_question_mark() {
301 let empty: [(&str, &str); 0] = [];
302 let request = builder("https://example.com/items")
303 .query(&empty)
304 .build()
305 .unwrap();
306 assert_eq!(request.uri(), "https://example.com/items");
307 }
308
309 #[test]
310 fn json_sets_content_type_and_body() {
311 let request = builder("https://example.com")
312 .json(&serde_json::json!({"a": 1}))
313 .build()
314 .unwrap();
315 assert_eq!(request.headers()[CONTENT_TYPE], "application/json");
316 assert_eq!(request.body().as_bytes(), Some(&br#"{"a":1}"#[..]));
317 }
318
319 #[test]
320 fn form_sets_content_type_and_body() {
321 let request = builder("https://example.com")
322 .form(&[("a", "1"), ("b", "x y")])
323 .build()
324 .unwrap();
325 assert_eq!(
326 request.headers()[CONTENT_TYPE],
327 "application/x-www-form-urlencoded"
328 );
329 assert_eq!(request.body().as_bytes(), Some(&b"a=1&b=x+y"[..]));
330 }
331
332 #[test]
333 fn auth_headers_are_sensitive() {
334 let bearer = builder("https://example.com")
335 .bearer_auth("secret")
336 .build()
337 .unwrap();
338 assert_eq!(bearer.headers()[AUTHORIZATION], "Bearer secret");
339 assert!(bearer.headers()[AUTHORIZATION].is_sensitive());
340 let basic = builder("https://example.com")
341 .basic_auth("user", Some("pass"))
342 .build()
343 .unwrap();
344 assert_eq!(basic.headers()[AUTHORIZATION], "Basic dXNlcjpwYXNz");
345 assert!(basic.headers()[AUTHORIZATION].is_sensitive());
346 }
347
348 #[test]
349 fn secrets_are_exposed_only_into_sensitive_headers() {
350 use crate::{ExposeSecret, SecretString};
351 let key = SecretString::from("sk-123");
352 assert!(!format!("{key:?}").contains("sk-123"));
353 let request = builder("https://example.com")
354 .header("x-api-key", "old")
355 .sensitive_header("x-api-key", key.expose_secret())
356 .bearer_auth(key.expose_secret())
357 .build()
358 .unwrap();
359 let values: Vec<_> = request.headers().get_all("x-api-key").iter().collect();
360 assert_eq!(values, ["sk-123"]);
361 assert!(values[0].is_sensitive());
362 assert!(!format!("{:?}", request.headers()).contains("sk-123"));
363 assert_eq!(request.headers()[AUTHORIZATION], "Bearer sk-123");
364 }
365
366 #[test]
367 fn options_override_headers_and_append_query() {
368 let mut tags = HeaderMap::new();
369 tags.append("x-tag", HeaderValue::from_static("a"));
370 tags.append("x-tag", HeaderValue::from_static("b"));
371 let options = RequestOptions::new()
372 .header("x-generated", "from-options")
373 .unwrap()
374 .header(AUTHORIZATION, "Bearer other")
375 .unwrap()
376 .headers(tags)
377 .query(&[("b", "2"), ("a", "3")])
378 .unwrap();
379 let request = builder("https://example.com/items?a=1")
380 .header("x-generated", "one")
381 .header("x-generated", "two")
382 .header("x-kept", "kept")
383 .header("x-tag", "generated")
384 .bearer_auth("secret")
385 .options(&options)
386 .build()
387 .unwrap();
388 let headers = request.headers();
389 assert_eq!(
390 headers.get_all("x-generated").iter().collect::<Vec<_>>(),
391 ["from-options"]
392 );
393 assert_eq!(headers["x-kept"], "kept");
394 assert_eq!(
395 headers.get_all("x-tag").iter().collect::<Vec<_>>(),
396 ["a", "b"]
397 );
398 assert_eq!(headers[AUTHORIZATION], "Bearer other");
399 assert_eq!(request.uri(), "https://example.com/items?a=1&b=2&a=3");
400 }
401
402 #[test]
403 fn empty_options_change_nothing() {
404 let request = builder("https://example.com/items")
405 .header("x-kept", "kept")
406 .options(&RequestOptions::new())
407 .build()
408 .unwrap();
409 assert_eq!(request.uri(), "https://example.com/items");
410 assert_eq!(request.headers().len(), 1);
411 }
412
413 #[test]
414 fn a_base_url_replaces_the_declared_base() {
415 let options = RequestOptions::new()
416 .base_url("http://localhost:8080/proxy/")
417 .unwrap();
418 let request = builder("https://api.example.com/v1/items/a%20b?limit=2")
419 .base("https://api.example.com/v1")
420 .options(&options)
421 .build()
422 .unwrap();
423 assert_eq!(
424 request.uri(),
425 "http://localhost:8080/proxy/items/a%20b?limit=2"
426 );
427 let request = builder("https://api.example.com/v1")
429 .base("https://api.example.com/v1/")
430 .options(
431 &RequestOptions::new()
432 .base_url("https://eu.example.com")
433 .unwrap(),
434 )
435 .build()
436 .unwrap();
437 assert_eq!(request.uri(), "https://eu.example.com/");
438 let request = builder("https://api.example.com/items")
439 .base("https://api.example.com")
440 .options(
441 &RequestOptions::new()
442 .base_url("https://eu.example.com/v2")
443 .unwrap(),
444 )
445 .build()
446 .unwrap();
447 assert_eq!(request.uri(), "https://eu.example.com/v2/items");
448 }
449
450 #[test]
451 fn without_a_declared_base_the_whole_path_is_kept() {
452 let options = RequestOptions::new()
453 .base_url("https://eu.example.com/v2")
454 .unwrap()
455 .query(&[("trace", "1")])
456 .unwrap();
457 for request in [
458 builder("https://api.example.com/v1/items?a=1"),
459 builder("https://api.example.com/v1/items?a=1").base("https://other.example/v1"),
461 ] {
462 let request = request.options(&options).build().unwrap();
463 assert_eq!(
464 request.uri(),
465 "https://eu.example.com/v2/v1/items?a=1&trace=1"
466 );
467 }
468 }
469
470 #[test]
471 fn options_are_recorded_for_the_provider_layer() {
472 let request = builder("https://example.com/items")
473 .options(&RequestOptions::new().header("x-a", "1").unwrap())
474 .options(
475 &RequestOptions::new()
476 .query(&[("q", "1")])
477 .unwrap()
478 .base_url("https://other.example")
479 .unwrap(),
480 )
481 .build()
482 .unwrap();
483 let applied = request.extensions().get::<Applied>().unwrap();
484 assert!(applied.has_header(&HeaderName::from_static("x-a")));
485 assert!(applied.has_query("q"));
486 assert!(applied.base);
487 let plain = builder("https://example.com/items")
488 .options(&RequestOptions::new())
489 .build()
490 .unwrap();
491 assert!(plain.extensions().get::<Applied>().is_none());
492 }
493
494 #[test]
495 fn errors_surface_at_build() {
496 assert!(builder("not a url").build().unwrap_err().is_builder());
497 let invalid = builder("https://example.com")
498 .header("x-bad", "line\nbreak")
499 .build();
500 assert!(invalid.unwrap_err().is_builder());
501 }
502}