ureq/
request.rs

1use std::convert::TryFrom;
2use std::fmt;
3use std::marker::PhantomData;
4
5use http::{Extensions, HeaderMap, HeaderName, HeaderValue};
6use http::{Method, Request, Response, Uri, Version};
7
8use crate::body::Body;
9use crate::config::typestate::RequestScope;
10use crate::config::{Config, ConfigBuilder, RequestLevelConfig};
11use crate::http;
12use crate::query::form_url_enc;
13use crate::query::{parse_query_params, QueryParam};
14use crate::send_body::AsSendBody;
15use crate::util::private::Private;
16use crate::util::HeaderMapExt;
17use crate::util::UriExt;
18use crate::{Agent, Error, SendBody};
19
20/// Transparent wrapper around [`http::request::Builder`].
21///
22/// The purpose is to provide the [`.call()`][RequestBuilder::call] and [`.send()`][RequestBuilder::send]
23/// and additional helpers for query parameters like [`.query()`][RequestBuilder::query] functions to
24/// make an API for sending requests.
25pub struct RequestBuilder<B> {
26    agent: Agent,
27    builder: http::request::Builder,
28    query_extra: Vec<QueryParam<'static>>,
29
30    // This is only used in case http::request::Builder contains an error
31    // (such as URL parsing error), and the user wants a `.config()`.
32    dummy_config: Option<Box<Config>>,
33
34    _ph: PhantomData<B>,
35}
36
37/// Typestate when [`RequestBuilder`] has no send body.
38///
39/// `RequestBuilder<WithoutBody>`
40///
41/// Methods: GET, DELETE, HEAD, OPTIONS, CONNECT, TRACE
42#[derive(Debug)]
43pub struct WithoutBody(());
44impl Private for WithoutBody {}
45
46/// Typestate when [`RequestBuilder`] needs to a send body.
47///
48/// `RequestBuilder<WithBody>`
49///
50/// Methods: POST, PUT, PATCH
51#[derive(Debug)]
52pub struct WithBody(());
53impl Private for WithBody {}
54
55impl<Any> RequestBuilder<Any> {
56    /// Get the HTTP Method for this request.
57    ///
58    /// By default this is `GET`. If builder has error, returns None.
59    ///
60    /// # Examples
61    ///
62    /// ```
63    /// use ureq::http::Method;
64    ///
65    /// let req = ureq::get("http://httpbin.org/get");
66    /// assert_eq!(req.method_ref(),Some(&Method::GET));
67    ///
68    /// let req = ureq::post("http://httpbin.org/post");
69    /// assert_eq!(req.method_ref(),Some(&Method::POST));
70    /// ```
71    pub fn method_ref(&self) -> Option<&Method> {
72        self.builder.method_ref()
73    }
74
75    /// Appends a header to this request builder.
76    ///
77    /// This function will append the provided key/value as a header to the
78    /// set of headers. It does not replace headers.
79    ///
80    /// # Examples
81    ///
82    /// ```
83    /// let req = ureq::get("https://httpbin.org/get")
84    ///     .header("X-Custom-Foo", "bar");
85    /// ```
86    pub fn header<K, V>(mut self, key: K, value: V) -> Self
87    where
88        HeaderName: TryFrom<K>,
89        <HeaderName as TryFrom<K>>::Error: Into<http::Error>,
90        HeaderValue: TryFrom<V>,
91        <HeaderValue as TryFrom<V>>::Error: Into<http::Error>,
92    {
93        self.builder = self.builder.header(key, value);
94        self
95    }
96
97    /// Get header on this request builder.
98    ///
99    /// When builder has error returns `None`.
100    ///
101    /// # Example
102    ///
103    /// ```
104    /// let req = ureq::get("http://httpbin.org/get")
105    ///     .header("Accept", "text/html")
106    ///     .header("X-Custom-Foo", "bar");
107    /// let headers = req.headers_ref().unwrap();
108    /// assert_eq!( headers["Accept"], "text/html" );
109    /// assert_eq!( headers["X-Custom-Foo"], "bar" );
110    /// ```
111    pub fn headers_ref(&self) -> Option<&HeaderMap<HeaderValue>> {
112        self.builder.headers_ref()
113    }
114
115    /// Get headers on this request builder.
116    ///
117    /// When builder has error returns `None`.
118    ///
119    /// # Example
120    ///
121    /// ```
122    /// # use ureq::http::header::HeaderValue;
123    /// let mut req =  ureq::get("http://httpbin.org/get");
124    /// {
125    ///   let headers = req.headers_mut().unwrap();
126    ///   headers.insert("Accept", HeaderValue::from_static("text/html"));
127    ///   headers.insert("X-Custom-Foo", HeaderValue::from_static("bar"));
128    /// }
129    /// let headers = req.headers_ref().unwrap();
130    /// assert_eq!( headers["Accept"], "text/html" );
131    /// assert_eq!( headers["X-Custom-Foo"], "bar" );
132    /// ```
133    pub fn headers_mut(&mut self) -> Option<&mut HeaderMap<HeaderValue>> {
134        self.builder.headers_mut()
135    }
136
137    /// Add a query parameter to the URL.
138    ///
139    /// Always appends a new parameter, also when using the name of
140    /// an already existing one. Both key and value are percent-encoded
141    /// according to the URL specification.
142    ///
143    /// # Examples
144    ///
145    /// ```
146    /// // Creates a URL with an encoded query parameter:
147    /// // https://httpbin.org/get?my_query=with%20value
148    /// let req = ureq::get("https://httpbin.org/get")
149    ///     .query("my_query", "with value");
150    /// ```
151    pub fn query<K, V>(mut self, key: K, value: V) -> Self
152    where
153        K: AsRef<str>,
154        V: AsRef<str>,
155    {
156        self.query_extra
157            .push(QueryParam::new_key_value(key.as_ref(), value.as_ref()));
158        self
159    }
160
161    /// Add a query parameter to the URL without percent-encoding.
162    ///
163    /// Always appends a new parameter, also when using the name of
164    /// an already existing one. Neither key nor value are percent-encoded,
165    /// which allows you to use pre-encoded values or bypass encoding.
166    ///
167    /// **Important note**: When using this method, you must ensure that your
168    /// query parameters don't contain characters that would make the URI invalid,
169    /// such as spaces or control characters. You are responsible for any pre-encoding
170    /// needed for URI validity. If you're unsure, use the regular `query()` method instead.
171    ///
172    /// # Examples
173    ///
174    /// ```
175    /// // Creates a URL with a raw query parameter:
176    /// // https://httpbin.org/get?my_query=pre-encoded%20value
177    /// let req = ureq::get("https://httpbin.org/get")
178    ///     .query_raw("my_query", "pre-encoded%20value");
179    /// ```
180    pub fn query_raw<K, V>(mut self, key: K, value: V) -> Self
181    where
182        K: AsRef<str>,
183        V: AsRef<str>,
184    {
185        self.query_extra
186            .push(QueryParam::new_key_value_raw(key.as_ref(), value.as_ref()));
187        self
188    }
189
190    /// Set multi query parameters.
191    ///
192    /// Both keys and values are percent-encoded according to the URL specification.
193    ///
194    /// For example, to set `?format=json&dest=%2Flogin`
195    ///
196    /// ```
197    /// let query = vec![
198    ///     ("format", "json"),
199    ///     ("dest", "/login"),
200    /// ];
201    ///
202    /// let response = ureq::get("http://httpbin.org/get")
203    ///    .query_pairs(query)
204    ///    .call()?;
205    /// # Ok::<_, ureq::Error>(())
206    /// ```
207    pub fn query_pairs<I, K, V>(mut self, iter: I) -> Self
208    where
209        I: IntoIterator<Item = (K, V)>,
210        K: AsRef<str>,
211        V: AsRef<str>,
212    {
213        self.query_extra.extend(
214            iter.into_iter()
215                .map(|(k, v)| QueryParam::new_key_value(k.as_ref(), v.as_ref())),
216        );
217        self
218    }
219    /// Set multi query parameters without percent-encoding.
220    ///
221    /// Neither keys nor values are percent-encoded, which allows you to use
222    /// pre-encoded values or bypass encoding.
223    ///
224    /// **Important note**: When using this method, you must ensure that your
225    /// query parameters don't contain characters that would make the URI invalid,
226    /// such as spaces or control characters. You are responsible for any pre-encoding
227    /// needed for URI validity. If you're unsure, use the regular `query_pairs()` method instead.
228    ///
229    /// For example, to set `?format=json&dest=/login` without encoding:
230    ///
231    /// ```
232    /// let query = vec![
233    ///     ("format", "json"),
234    ///     ("dest", "/login"),
235    /// ];
236    ///
237    /// let response = ureq::get("http://httpbin.org/get")
238    ///    .query_pairs_raw(query)
239    ///    .call()?;
240    /// # Ok::<_, ureq::Error>(())
241    /// ```
242    pub fn query_pairs_raw<I, K, V>(mut self, iter: I) -> Self
243    where
244        I: IntoIterator<Item = (K, V)>,
245        K: AsRef<str>,
246        V: AsRef<str>,
247    {
248        self.query_extra.extend(
249            iter.into_iter()
250                .map(|(k, v)| QueryParam::new_key_value_raw(k.as_ref(), v.as_ref())),
251        );
252        self
253    }
254    /// Overrides the URI for this request.
255    ///
256    /// Typically this is set via `ureq::get(<uri>)` or `Agent::get(<uri>)`. This
257    /// lets us change it.
258    ///
259    /// # Examples
260    ///
261    /// ```
262    /// let req = ureq::get("https://www.google.com/")
263    ///     .uri("https://httpbin.org/get");
264    /// ```
265    pub fn uri<T>(mut self, uri: T) -> Self
266    where
267        Uri: TryFrom<T>,
268        <Uri as TryFrom<T>>::Error: Into<http::Error>,
269    {
270        self.builder = self.builder.uri(uri);
271        self
272    }
273
274    /// Get the URI for this request
275    ///
276    /// By default this is `/`.
277    ///
278    /// # Examples
279    ///
280    /// ```
281    /// let req = ureq::get("http://httpbin.org/get");
282    /// assert_eq!(req.uri_ref().unwrap(), "http://httpbin.org/get");
283    /// ```
284    pub fn uri_ref(&self) -> Option<&Uri> {
285        self.builder.uri_ref()
286    }
287
288    /// Set the HTTP version for this request.
289    ///
290    /// By default this is HTTP/1.1.
291    /// ureq only handles HTTP/1.1 and HTTP/1.0.
292    ///
293    /// # Examples
294    ///
295    /// ```
296    /// use ureq::http::Version;
297    ///
298    /// let req = ureq::get("https://www.google.com/")
299    ///     .version(Version::HTTP_10);
300    /// ```
301    pub fn version(mut self, version: Version) -> Self {
302        self.builder = self.builder.version(version);
303        self
304    }
305
306    /// Get the HTTP version for this request
307    ///
308    /// By default this is HTTP/1.1.
309    ///
310    /// # Examples
311    ///
312    /// ```
313    /// use ureq::http::Version;
314    ///
315    /// let req = ureq::get("http://httpbin.org/get");
316    /// assert_eq!(req.version_ref().unwrap(), &Version::HTTP_11);
317    /// ```
318    pub fn version_ref(&self) -> Option<&Version> {
319        self.builder.version_ref()
320    }
321
322    /// Override agent level config on the request level.
323    ///
324    /// The agent config is copied and modified on request level.
325    ///
326    /// # Example
327    ///
328    /// ```
329    /// use ureq::Agent;
330    ///
331    /// let agent: Agent = Agent::config_builder()
332    ///     .https_only(false)
333    ///     .build()
334    ///     .into();
335    ///
336    /// let request = agent.get("http://httpbin.org/get")
337    ///     .config()
338    ///     // override agent default for this request
339    ///     .https_only(true)
340    ///     .build();
341    ///
342    /// // Make the request
343    /// let result = request.call();
344    ///
345    /// // The https_only was set on request level
346    /// assert!(matches!(result.unwrap_err(), ureq::Error::RequireHttpsOnly(_)));
347    /// # Ok::<_, ureq::Error>(())
348    /// ```
349    pub fn config(self) -> ConfigBuilder<RequestScope<Any>> {
350        ConfigBuilder(RequestScope(self))
351    }
352
353    /// Adds an extension to this builder
354    ///
355    /// # Examples
356    ///
357    /// ```
358    /// let req = ureq::get("http://httpbin.org/get")
359    ///     .extension("My Extension");
360    ///
361    /// assert_eq!(req.extensions_ref().unwrap().get::<&'static str>(),
362    ///            Some(&"My Extension"));
363    /// ```
364    pub fn extension<T>(mut self, extension: T) -> Self
365    where
366        T: Clone + std::any::Any + Send + Sync + 'static,
367    {
368        self.builder = self.builder.extension(extension);
369        self
370    }
371
372    /// Get a reference to the extensions for this request builder.
373    ///
374    /// If the builder has an error, this returns `None`.
375    ///
376    /// # Example
377    ///
378    /// ```
379    /// let req = ureq::get("http://httpbin.org/get")
380    ///     .extension("My Extension").extension(5u32);
381    /// let extensions = req.extensions_ref().unwrap();
382    /// assert_eq!(extensions.get::<&'static str>(), Some(&"My Extension"));
383    /// assert_eq!(extensions.get::<u32>(), Some(&5u32));
384    /// ```
385    pub fn extensions_ref(&self) -> Option<&Extensions> {
386        self.builder.extensions_ref()
387    }
388
389    /// Get a mutable reference to the extensions for this request builder.
390    ///
391    /// If the builder has an error, this returns `None`.
392    ///
393    /// # Example
394    ///
395    /// ```
396    /// let mut req = ureq::get("http://httpbin.org/get");
397    /// let mut extensions = req.extensions_mut().unwrap();
398    /// extensions.insert(5u32);
399    /// assert_eq!(extensions.get::<u32>(), Some(&5u32));
400    /// ```
401    pub fn extensions_mut(&mut self) -> Option<&mut Extensions> {
402        self.builder.extensions_mut()
403    }
404
405    pub(crate) fn request_level_config(&mut self) -> &mut Config {
406        let Some(exts) = self.builder.extensions_mut() else {
407            // This means self.builder has an error such as URL parsing error.
408            // The error will surface on .call() (or .send()) and we fill in
409            // a dummy Config meanwhile.
410            return self
411                .dummy_config
412                .get_or_insert_with(|| Box::new(Config::default()));
413        };
414
415        if exts.get::<RequestLevelConfig>().is_none() {
416            exts.insert(self.agent.new_request_level_config());
417        }
418
419        // Unwrap is OK because of above check
420        let req_level: &mut RequestLevelConfig = exts.get_mut().unwrap();
421
422        &mut req_level.0
423    }
424}
425
426impl RequestBuilder<WithoutBody> {
427    pub(crate) fn new<T>(agent: Agent, method: Method, uri: T) -> Self
428    where
429        Uri: TryFrom<T>,
430        <Uri as TryFrom<T>>::Error: Into<http::Error>,
431    {
432        Self {
433            agent,
434            builder: Request::builder().method(method).uri(uri),
435            query_extra: vec![],
436            dummy_config: None,
437            _ph: PhantomData,
438        }
439    }
440
441    /// Sends the request and blocks the caller until we receive a response.
442    ///
443    /// It sends neither `Content-Length` nor `Transfer-Encoding`.
444    ///
445    /// ```
446    /// let res = ureq::get("http://httpbin.org/get")
447    ///     .call()?;
448    /// # Ok::<_, ureq::Error>(())
449    /// ```
450    pub fn call(self) -> Result<Response<Body>, Error> {
451        let request = self.builder.body(())?;
452        do_call(self.agent, request, self.query_extra, SendBody::none())
453    }
454
455    /// Force sending a body.
456    ///
457    /// This is an escape hatch to interact with broken services.
458    ///
459    /// According to the spec, methods such as GET, DELETE and TRACE should
460    /// not have a body. Despite that there are broken API services and
461    /// servers that use it.
462    ///
463    /// Example using DELETE while sending a body.
464    ///
465    /// ```
466    /// let res = ureq::delete("http://httpbin.org/delete")
467    ///     // this "unlocks" send() below
468    ///     .force_send_body()
469    ///     .send("DELETE with body is not correct")?;
470    /// # Ok::<_, ureq::Error>(())
471    /// ```
472    pub fn force_send_body(mut self) -> RequestBuilder<WithBody> {
473        if let Some(exts) = self.extensions_mut() {
474            exts.insert(ForceSendBody);
475        }
476
477        RequestBuilder {
478            agent: self.agent,
479            builder: self.builder,
480            query_extra: self.query_extra,
481            dummy_config: None,
482            _ph: PhantomData,
483        }
484    }
485}
486
487#[derive(Debug, Clone)]
488pub(crate) struct ForceSendBody;
489
490impl RequestBuilder<WithBody> {
491    pub(crate) fn new<T>(agent: Agent, method: Method, uri: T) -> Self
492    where
493        Uri: TryFrom<T>,
494        <Uri as TryFrom<T>>::Error: Into<http::Error>,
495    {
496        Self {
497            agent,
498            builder: Request::builder().method(method).uri(uri),
499            query_extra: vec![],
500            dummy_config: None,
501            _ph: PhantomData,
502        }
503    }
504
505    /// Set the content-type header.
506    ///
507    /// ```
508    /// let res = ureq::post("http://httpbin.org/post")
509    ///     .content_type("text/html; charset=utf-8")
510    ///     .send("<html><body>åäö</body></html>")?;
511    /// # Ok::<_, ureq::Error>(())
512    /// ```
513    pub fn content_type<V>(mut self, content_type: V) -> Self
514    where
515        HeaderValue: TryFrom<V>,
516        <HeaderValue as TryFrom<V>>::Error: Into<http::Error>,
517    {
518        self.builder = self.builder.header("content-type", content_type);
519        self
520    }
521
522    /// Send body data and blocks the caller until we receive response.
523    ///
524    /// ```
525    /// let res = ureq::post("http://httpbin.org/post")
526    ///     .send(&[0_u8; 1000])?;
527    /// # Ok::<_, ureq::Error>(())
528    /// ```
529    pub fn send(self, data: impl AsSendBody) -> Result<Response<Body>, Error> {
530        let request = self.builder.body(())?;
531        let mut data_ref = data;
532        do_call(self.agent, request, self.query_extra, data_ref.as_body())
533    }
534
535    /// Send an empty body.
536    ///
537    /// The method is POST, PUT or PATCH, which normally has a body. Using
538    /// this function makes it explicit you want to send an empty body despite
539    /// the method.
540    ///
541    /// This is equivalent to `.send(&[])`.
542    ///
543    /// ```
544    /// let res = ureq::post("http://httpbin.org/post")
545    ///     .send_empty()?;
546    /// # Ok::<_, ureq::Error>(())
547    /// ```
548    pub fn send_empty(self) -> Result<Response<Body>, Error> {
549        self.send(&[])
550    }
551
552    /// Send form encoded data.
553    ///
554    /// Constructs a [form submission] with the content-type header
555    /// `application/x-www-form-urlencoded`. Keys and values will be URL encoded.
556    ///
557    /// ```
558    /// let form = [
559    ///     ("name", "martin"),
560    ///     ("favorite_bird", "blue-footed booby"),
561    /// ];
562    ///
563    /// let response = ureq::post("http://httpbin.org/post")
564    ///    .send_form(form)?;
565    /// # Ok::<_, ureq::Error>(())
566    /// ```
567    ///
568    /// [form submission]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/POST#url-encoded_form_submission
569    pub fn send_form<I, K, V>(self, iter: I) -> Result<Response<Body>, Error>
570    where
571        I: IntoIterator<Item = (K, V)>,
572        K: AsRef<str>,
573        V: AsRef<str>,
574    {
575        let iter = iter.into_iter();
576
577        // TODO(martin): can we calculate a size hint for capacity here?
578        let mut body = String::new();
579
580        for (k, v) in iter {
581            if !body.is_empty() {
582                body.push('&');
583            }
584            body.push_str(&form_url_enc(k.as_ref()));
585            body.push('=');
586            body.push_str(&form_url_enc(v.as_ref()));
587        }
588
589        let mut request = self.builder.body(())?;
590
591        if !request.headers().has_content_type() {
592            request.headers_mut().append(
593                http::header::CONTENT_TYPE,
594                HeaderValue::from_static("application/x-www-form-urlencoded"),
595            );
596        }
597
598        do_call(self.agent, request, self.query_extra, body.as_body())
599    }
600
601    /// Send body data as JSON.
602    ///
603    /// Requires the **json** feature.
604    ///
605    /// The data typically derives [`Serialize`](serde::Serialize) and is converted
606    /// to a string before sending (does allocate). Will set the content-type header
607    /// `application/json`.
608    ///
609    /// ```
610    /// use serde::Serialize;
611    ///
612    /// #[derive(Serialize)]
613    /// struct MyData {
614    ///     thing: String,
615    /// }
616    ///
617    /// let body = MyData {
618    ///     thing: "yo".to_string(),
619    /// };
620    ///
621    /// let res = ureq::post("http://httpbin.org/post")
622    ///     .send_json(&body)?;
623    /// # Ok::<_, ureq::Error>(())
624    /// ```
625    #[cfg(feature = "json")]
626    pub fn send_json(self, data: impl serde::ser::Serialize) -> Result<Response<Body>, Error> {
627        let mut request = self.builder.body(())?;
628        let body = SendBody::from_json(&data)?;
629
630        if !request.headers().has_content_type() {
631            request.headers_mut().append(
632                http::header::CONTENT_TYPE,
633                HeaderValue::from_static("application/json; charset=utf-8"),
634            );
635        }
636
637        do_call(self.agent, request, self.query_extra, body)
638    }
639}
640
641fn do_call(
642    agent: Agent,
643    mut request: Request<()>,
644    query_extra: Vec<QueryParam<'static>>,
645    body: SendBody,
646) -> Result<Response<Body>, Error> {
647    if !query_extra.is_empty() {
648        request.uri().ensure_valid_url()?;
649        request = amend_request_query(request, query_extra.into_iter());
650    }
651    let response = agent.run_via_middleware(request, body)?;
652    Ok(response)
653}
654
655fn amend_request_query(
656    request: Request<()>,
657    query_extra: impl Iterator<Item = QueryParam<'static>>,
658) -> Request<()> {
659    let (mut parts, body) = request.into_parts();
660    let uri = parts.uri;
661    let mut path = uri.path().to_string();
662    let query_existing = parse_query_params(uri.query().unwrap_or(""));
663
664    let mut do_first = true;
665
666    fn append<'a>(
667        path: &mut String,
668        do_first: &mut bool,
669        iter: impl Iterator<Item = QueryParam<'a>>,
670    ) {
671        for q in iter {
672            if *do_first {
673                *do_first = false;
674                path.push('?');
675            } else {
676                path.push('&');
677            }
678            path.push_str(&q);
679        }
680    }
681
682    append(&mut path, &mut do_first, query_existing);
683    append(&mut path, &mut do_first, query_extra);
684
685    // Unwraps are OK, because we had a correct URI to begin with
686    let rebuild = Uri::builder()
687        .scheme(uri.scheme().unwrap().clone())
688        .authority(uri.authority().unwrap().clone())
689        .path_and_query(path)
690        .build()
691        .unwrap();
692
693    parts.uri = rebuild;
694
695    Request::from_parts(parts, body)
696}
697
698impl fmt::Debug for RequestBuilder<WithoutBody> {
699    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
700        f.debug_struct("RequestBuilder<WithoutBody>")
701            // unwraps are OK because we can't be in this state without having method+uri
702            .field("method", &self.builder.method_ref().unwrap())
703            .field("uri", &self.builder.uri_ref().unwrap())
704            .finish()
705    }
706}
707
708impl fmt::Debug for RequestBuilder<WithBody> {
709    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
710        f.debug_struct("RequestBuilder<WithBody>")
711            // unwraps are OK because we can't be in this state without having method+uri
712            .field("method", &self.builder.method_ref().unwrap())
713            .field("uri", &self.builder.uri_ref().unwrap())
714            .finish()
715    }
716}
717
718#[cfg(test)]
719mod test {
720    use std::time::Duration;
721
722    use crate::get;
723    use crate::test::init_test_log;
724
725    use super::*;
726
727    #[test]
728    fn disallow_empty_host() {
729        let err = crate::get("file:///some/path").call().unwrap_err();
730        assert_eq!(err.to_string(), "http: invalid format");
731        assert!(matches!(err, Error::Http(_)));
732    }
733
734    #[test]
735    fn query_with_encoding() {
736        let request = Request::builder()
737            .uri("https://foo.bar/path")
738            .body(())
739            .unwrap();
740
741        // Test that single quotes and spaces are encoded
742        let amended = amend_request_query(
743            request,
744            vec![QueryParam::new_key_value("key", "value with 'quotes'")].into_iter(),
745        );
746
747        assert_eq!(
748            amended.uri(),
749            "https://foo.bar/path?key=value%20with%20%27quotes%27"
750        );
751    }
752
753    #[test]
754    fn query_raw_without_encoding() {
755        let request = Request::builder()
756            .uri("https://foo.bar/path")
757            .body(())
758            .unwrap();
759
760        // Test that raw values remain unencoded (using URI-valid characters)
761        let amended = amend_request_query(
762            request,
763            vec![QueryParam::new_key_value_raw("key", "value-with-'quotes'")].into_iter(),
764        );
765
766        assert_eq!(
767            amended.uri(),
768            "https://foo.bar/path?key=value-with-'quotes'"
769        );
770    }
771    #[test]
772    fn encoded_and_raw_combined() {
773        let request = Request::builder()
774            .uri("https://foo.bar/path")
775            .body(())
776            .unwrap();
777
778        // Test combination of encoded and unencoded parameters
779        let amended = amend_request_query(
780            request,
781            vec![
782                QueryParam::new_key_value("encoded", "value with spaces"),
783                QueryParam::new_key_value_raw("raw", "value-without-spaces"),
784            ]
785            .into_iter(),
786        );
787
788        assert_eq!(
789            amended.uri(),
790            "https://foo.bar/path?encoded=value%20with%20spaces&raw=value-without-spaces"
791        );
792    }
793    #[test]
794    fn debug_print_without_body() {
795        let call = crate::get("https://foo/bar");
796        assert_eq!(
797            format!("{:?}", call),
798            "RequestBuilder<WithoutBody> { method: GET, uri: https://foo/bar }"
799        );
800    }
801
802    #[test]
803    fn debug_print_with_body() {
804        let call = crate::post("https://foo/bar");
805        assert_eq!(
806            format!("{:?}", call),
807            "RequestBuilder<WithBody> { method: POST, uri: https://foo/bar }"
808        );
809    }
810
811    #[test]
812    fn config_after_broken_url() {
813        init_test_log();
814        get("http://x.y.z/ borked url")
815            .config()
816            .timeout_global(Some(Duration::from_millis(1)))
817            .build();
818    }
819
820    #[test]
821    fn add_params_to_request_without_query() {
822        let request = Request::builder()
823            .uri("https://foo.bar/path")
824            .body(())
825            .unwrap();
826
827        let amended = amend_request_query(
828            request,
829            vec![
830                QueryParam::new_key_value("x", "z"),
831                QueryParam::new_key_value("ab", "cde"),
832            ]
833            .into_iter(),
834        );
835
836        assert_eq!(amended.uri(), "https://foo.bar/path?x=z&ab=cde");
837    }
838
839    #[test]
840    fn add_params_to_request_with_query() {
841        let request = Request::builder()
842            .uri("https://foo.bar/path?x=z")
843            .body(())
844            .unwrap();
845
846        let amended = amend_request_query(
847            request,
848            vec![QueryParam::new_key_value("ab", "cde")].into_iter(),
849        );
850
851        assert_eq!(amended.uri(), "https://foo.bar/path?x=z&ab=cde");
852    }
853
854    #[test]
855    fn add_params_that_need_percent_encoding() {
856        let request = Request::builder()
857            .uri("https://foo.bar/path")
858            .body(())
859            .unwrap();
860
861        let amended = amend_request_query(
862            request,
863            vec![QueryParam::new_key_value("å ", "i åa ä e ö")].into_iter(),
864        );
865
866        assert_eq!(
867            amended.uri(),
868            "https://foo.bar/path?%C3%A5%20=i%20%C3%A5a%20%C3%A4%20e%20%C3%B6"
869        );
870    }
871}