Skip to main content

timvw_rust_runtime/
request.rs

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
14/// Builds a request, with the methods of reqwest's `RequestBuilder` that generated clients use.
15///
16/// An error in any step (an invalid URL, header or body) is kept and returned by [`RequestBuilder::build`].
17pub 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    /// The base the URL was built on ([`RequestBuilder::base`]).
28    base: Option<Url>,
29    /// What [`RequestBuilder::options`] set.
30    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    /// Adds a header.
58    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    /// Adds every header of a map.
74    pub fn headers(self, headers: HeaderMap) -> Self {
75        self.with(|parts| {
76            parts.headers.extend(headers);
77            Ok(())
78        })
79    }
80
81    /// Appends query parameters, serialized as `application/x-www-form-urlencoded`: a sequence of pairs, a map or a
82    /// struct.
83    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    /// Sets an `Authorization: Bearer` header.
99    pub fn bearer_auth<T: fmt::Display>(self, token: T) -> Self {
100        self.sensitive_header(AUTHORIZATION, format!("Bearer {token}"))
101    }
102
103    /// Sets an `Authorization: Basic` header.
104    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    /// Sets a header, replacing any earlier value, and marks it sensitive (see [`HeaderValue::set_sensitive`]): for
118    /// credentials such as an API key.
119    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    /// Sets the body: bytes, a [`Body`], or an [`Upload`](crate::Upload) (a file, say, streamed and sent again by a
136    /// retry).
137    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    /// Sets a JSON body, and `Content-Type: application/json` unless a content type is set.
145    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    /// Sets a form body (`application/x-www-form-urlencoded`).
158    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    /// Sets a `multipart/form-data` body, with a `Content-Length` when the length of every part is known.
171    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    /// Declares the base URL the request's URL was built on: a generated client's `Configuration.base_path`. A
187    /// [`RequestOptions::base_url`] replaces it. A base the URL is not under is ignored.
188    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    /// Applies [`RequestOptions`]: each of its headers replaces every value the request has under that name (call it
196    /// last to override what was set before, as generated clients do), its query parameters are appended, and its
197    /// [base URL](RequestOptions::base_url) replaces the request's [base](RequestBuilder::base), keeping the rest of
198    /// the path and the query. A [`ProviderLayer`](crate::ProviderLayer) leaves what the options set alone.
199    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    /// The request, or the first error of the steps that built it.
223    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    /// Builds the request and sends it with the client that started it.
238    pub async fn send(self) -> Result<Response, Error> {
239        let client = self.client.clone();
240        client.execute(self.build()?).await
241    }
242}
243
244/// Moves `url` from the base `from` (its origin when `from` is unknown or `url` is not under it) to `to`, keeping the
245/// rest of the path and the query.
246fn 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        // The base itself, and a root base.
428        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            // a base the URL is not under
460            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}