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::{Body, Client, Error, Method, RequestOptions, Response};
11
12/// Builds a request, with the methods of reqwest's `RequestBuilder` that generated clients use.
13///
14/// An error in any step (an invalid URL, header or body) is kept and returned by [`RequestBuilder::build`].
15pub 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    /// Adds a header.
50    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    /// Adds every header of a map.
66    pub fn headers(self, headers: HeaderMap) -> Self {
67        self.with(|parts| {
68            parts.headers.extend(headers);
69            Ok(())
70        })
71    }
72
73    /// Appends query parameters, serialized as `application/x-www-form-urlencoded`: a sequence of pairs, a map or a
74    /// struct.
75    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    /// Sets an `Authorization: Bearer` header.
91    pub fn bearer_auth<T: fmt::Display>(self, token: T) -> Self {
92        self.sensitive_header(AUTHORIZATION, format!("Bearer {token}"))
93    }
94
95    /// Sets an `Authorization: Basic` header.
96    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    /// Sets a header, replacing any earlier value, and marks it sensitive (see [`HeaderValue::set_sensitive`]): for
110    /// credentials such as an API key.
111    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    /// Sets the body: bytes, a [`Body`], or an [`Upload`](crate::Upload) (a file, say, streamed and sent again by a
128    /// retry).
129    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    /// Sets a JSON body, and `Content-Type: application/json` unless a content type is set.
137    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    /// Sets a form body (`application/x-www-form-urlencoded`).
150    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    /// Sets a `multipart/form-data` body, with a `Content-Length` when the length of every part is known.
163    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    /// Applies the extra headers and query parameters of [`RequestOptions`]: each of its headers replaces every value
179    /// the request has under that name (call it last to override what was set before, as generated clients do), and
180    /// its query parameters are appended.
181    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    /// The request, or the first error of the steps that built it.
195    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    /// Builds the request and sends it with the client that started it.
207    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}