Skip to main content

winhttp/
client.rs

1//! High-level HTTP client with ergonomic helpers.
2//!
3//! [`Client`] wraps a WinHTTP [`Session`] and provides one-liner methods for
4//! common HTTP verbs (`get`, `post`, `put`, `delete`, `patch`, `head`).
5//!
6//! # Builder pattern
7//!
8//! ```no_run
9//! use winhttp::Client;
10//!
11//! let client = Client::builder()
12//!     .base_url("https://httpbin.org")
13//!     .user_agent("my-app/1.0")
14//!     .build()?;
15//!
16//! // Paths are joined to the base URL:
17//! let resp = client.get("/get")?;
18//!
19//! // Absolute URLs bypass the base URL:
20//! let resp = client.get("https://other.com/foo")?;
21//! # Ok::<(), windows::core::Error>(())
22//! ```
23//!
24//! # Quick start (no base URL)
25//!
26//! ```no_run
27//! use winhttp::Client;
28//!
29//! let client = Client::new()?;
30//! let resp = client.get("https://httpbin.org/get")?;
31//! println!("{} {}", resp.status, String::from_utf8_lossy(&resp.body));
32//! # Ok::<(), windows::core::Error>(())
33//! ```
34//!
35//! # One-shot helpers
36//!
37//! For truly minimal usage, module-level functions create an ephemeral client:
38//!
39//! ```no_run
40//! let resp = winhttp::get("https://httpbin.org/get")?;
41//! # Ok::<(), windows::core::Error>(())
42//! ```
43
44use crate::session::{Session, SessionConfig};
45use crate::url::{UrlComponents, crack_url};
46use windows::core::Result;
47
48// ---------------------------------------------------------------------------
49// Body
50// ---------------------------------------------------------------------------
51
52/// A request body that can be raw bytes or serialized JSON.
53///
54/// Use the [`From`] impls to pass raw bytes, or [`Body::json`] to serialize a
55/// value and automatically set the `Content-Type: application/json` header.
56///
57/// # Examples
58///
59/// ```
60/// use winhttp::Body;
61///
62/// // Raw bytes — no Content-Type is set.
63/// let body: Body = b"hello".as_slice().into();
64/// let body: Body = vec![1, 2, 3].into();
65/// let body: Body = "plain text".into();
66/// ```
67///
68/// ```
69/// # #[cfg(feature = "json")]
70/// # fn demo() -> windows::core::Result<()> {
71/// use winhttp::Body;
72///
73/// // JSON — Content-Type is set to application/json.
74/// let body = Body::json(&serde_json::json!({"key": "value"}))?;
75/// # Ok(())
76/// # }
77/// ```
78#[derive(Debug, Clone)]
79pub struct Body {
80    bytes: Vec<u8>,
81    content_type: Option<&'static str>,
82}
83
84impl Body {
85    /// Serialize `value` as JSON and set `Content-Type: application/json`.
86    ///
87    /// Returns a `windows::core::Error` if serialization fails.
88    ///
89    /// Requires the `json` feature.
90    ///
91    /// # Example
92    ///
93    /// ```no_run
94    /// # #[cfg(feature = "json")]
95    /// # fn demo() -> windows::core::Result<()> {
96    /// use winhttp::{Body, Client};
97    /// use serde::Serialize;
98    ///
99    /// #[derive(Serialize)]
100    /// struct Payload { name: String }
101    ///
102    /// let client = Client::new()?;
103    /// let resp = client.post("/users", Body::json(&Payload { name: "Ada".into() })?)?;
104    /// # Ok(())
105    /// # }
106    /// ```
107    #[cfg(feature = "json")]
108    pub fn json<T: serde::Serialize>(value: &T) -> Result<Self> {
109        let bytes = serde_json::to_vec(value).map_err(|err| {
110            windows::core::Error::new(
111                windows::Win32::Foundation::E_FAIL,
112                format!("JSON serialization failed: {err}"),
113            )
114        })?;
115        Ok(Self {
116            bytes,
117            content_type: Some("application/json"),
118        })
119    }
120
121    /// Returns the raw bytes of this body.
122    #[must_use]
123    pub fn as_bytes(&self) -> &[u8] {
124        &self.bytes
125    }
126
127    /// Returns the content type that should be set for this body, if any.
128    #[must_use]
129    pub fn content_type(&self) -> Option<&'static str> {
130        self.content_type
131    }
132}
133
134impl From<&[u8]> for Body {
135    fn from(bytes: &[u8]) -> Self {
136        Self {
137            bytes: bytes.to_vec(),
138            content_type: None,
139        }
140    }
141}
142
143impl<const N: usize> From<&[u8; N]> for Body {
144    fn from(bytes: &[u8; N]) -> Self {
145        Self {
146            bytes: bytes.to_vec(),
147            content_type: None,
148        }
149    }
150}
151
152impl From<Vec<u8>> for Body {
153    fn from(bytes: Vec<u8>) -> Self {
154        Self {
155            bytes,
156            content_type: None,
157        }
158    }
159}
160
161impl From<&str> for Body {
162    fn from(s: &str) -> Self {
163        Self {
164            bytes: s.as_bytes().to_vec(),
165            content_type: None,
166        }
167    }
168}
169
170impl From<String> for Body {
171    fn from(s: String) -> Self {
172        Self {
173            bytes: s.into_bytes(),
174            content_type: None,
175        }
176    }
177}
178
179/// Extract the implicit headers (e.g. `Content-Type`) from a [`Body`].
180fn body_headers(body: &Body) -> Vec<(String, String)> {
181    body.content_type
182        .map(|ct| vec![("Content-Type".to_string(), ct.to_string())])
183        .unwrap_or_default()
184}
185
186// ---------------------------------------------------------------------------
187// Response
188// ---------------------------------------------------------------------------
189
190/// A high-level response returned by the [`Client`] helper methods.
191#[derive(Debug, Clone)]
192pub struct Response {
193    /// HTTP status code (e.g. 200, 404, 500).
194    pub status: u16,
195    /// HTTP status text (e.g. "OK", "Not Found").
196    pub status_text: String,
197    /// All response headers as a single CRLF-delimited string.
198    pub headers: String,
199    /// The full response body bytes.
200    pub body: Vec<u8>,
201}
202
203impl Response {
204    /// Interpret the body as UTF-8 (lossy).
205    #[must_use]
206    pub fn text(&self) -> String {
207        String::from_utf8_lossy(&self.body).into_owned()
208    }
209
210    /// Returns `true` if the status code is 2xx.
211    #[must_use]
212    pub fn is_success(&self) -> bool {
213        (200..300).contains(&self.status)
214    }
215
216    /// Returns `true` if the status code is 3xx.
217    #[must_use]
218    pub fn is_redirect(&self) -> bool {
219        (300..400).contains(&self.status)
220    }
221
222    /// Returns `true` if the status code is 4xx.
223    #[must_use]
224    pub fn is_client_error(&self) -> bool {
225        (400..500).contains(&self.status)
226    }
227
228    /// Returns `true` if the status code is 5xx.
229    #[must_use]
230    pub fn is_server_error(&self) -> bool {
231        (500..600).contains(&self.status)
232    }
233
234    /// Deserialize the response body as JSON.
235    ///
236    /// Requires the `json` feature.
237    ///
238    /// # Example
239    ///
240    /// ```no_run
241    /// # #[derive(serde::Deserialize)]
242    /// # struct ApiResponse { url: String }
243    /// let resp = winhttp::get("https://httpbin.org/get")?;
244    /// let data: ApiResponse = resp.json()?;
245    /// # Ok::<(), Box<dyn std::error::Error>>(())
246    /// ```
247    #[cfg(feature = "json")]
248    pub fn json<T: serde::de::DeserializeOwned>(
249        &self,
250    ) -> std::result::Result<T, serde_json::Error> {
251        serde_json::from_slice(&self.body)
252    }
253}
254
255// ---------------------------------------------------------------------------
256// ClientBuilder
257// ---------------------------------------------------------------------------
258
259/// Builder for configuring and constructing a [`Client`].
260///
261/// Obtained via [`Client::builder`]. All fields have sensible defaults;
262/// call [`build`](ClientBuilder::build) to create the client.
263///
264/// # Example
265///
266/// ```no_run
267/// # use winhttp::Client;
268/// let client = Client::builder()
269///     .base_url("https://httpbin.org")
270///     .user_agent("my-app/1.0")
271///     .connect_timeout_ms(10_000)
272///     .build()?;
273/// # Ok::<(), windows::core::Error>(())
274/// ```
275pub struct ClientBuilder {
276    base_url: Option<String>,
277    user_agent: String,
278    connect_timeout_ms: u32,
279    send_timeout_ms: u32,
280    receive_timeout_ms: u32,
281}
282
283impl ClientBuilder {
284    /// Set a base URL that will be prepended to relative paths.
285    ///
286    /// When set, calls like `client.get("/users")` resolve to
287    /// `{base_url}/users`. Absolute URLs (starting with `http://` or
288    /// `https://`) bypass the base URL entirely.
289    ///
290    /// Trailing slashes on the base URL are trimmed automatically.
291    #[must_use]
292    pub fn base_url(mut self, url: impl Into<String>) -> Self {
293        let mut url = url.into();
294        // Trim trailing slashes so join logic is consistent.
295        while url.ends_with('/') {
296            url.pop();
297        }
298        self.base_url = Some(url);
299        self
300    }
301
302    /// Set the `User-Agent` header string (default: `"winhttp-rs/0.1.0"`).
303    #[must_use]
304    pub fn user_agent(mut self, agent: impl Into<String>) -> Self {
305        self.user_agent = agent.into();
306        self
307    }
308
309    /// Set the connection timeout in milliseconds (default: 60 000).
310    #[must_use]
311    pub fn connect_timeout_ms(mut self, ms: u32) -> Self {
312        self.connect_timeout_ms = ms;
313        self
314    }
315
316    /// Set the send timeout in milliseconds (default: 30 000).
317    #[must_use]
318    pub fn send_timeout_ms(mut self, ms: u32) -> Self {
319        self.send_timeout_ms = ms;
320        self
321    }
322
323    /// Set the receive timeout in milliseconds (default: 30 000).
324    #[must_use]
325    pub fn receive_timeout_ms(mut self, ms: u32) -> Self {
326        self.receive_timeout_ms = ms;
327        self
328    }
329
330    /// Build the [`Client`].
331    ///
332    /// This creates the underlying WinHTTP session(s) and may fail if the
333    /// platform does not support WinHTTP or if the base URL is invalid.
334    pub fn build(self) -> Result<Client> {
335        let base_components = match &self.base_url {
336            Some(url) => Some(crack_url(url)?),
337            None => None,
338        };
339
340        let config = SessionConfig {
341            user_agent: self.user_agent,
342            connect_timeout_ms: self.connect_timeout_ms,
343            send_timeout_ms: self.send_timeout_ms,
344            receive_timeout_ms: self.receive_timeout_ms,
345        };
346
347        let session = Session::with_config(config.clone())?;
348
349        #[cfg(feature = "async")]
350        let async_session = Session::with_config_async(config)?;
351
352        Ok(Client {
353            base_url: self.base_url,
354            base_components,
355            session,
356            #[cfg(feature = "async")]
357            async_session,
358        })
359    }
360}
361
362// ---------------------------------------------------------------------------
363// RequestHelper
364// ---------------------------------------------------------------------------
365
366/// Builder for constructing a single request with custom headers and body.
367///
368/// Obtained via [`Client::request`]. Call [`send`](RequestHelper::send) (or
369/// [`send_async`](RequestHelper::send_async) with the `async` feature) to
370/// execute it.
371///
372/// # Example
373///
374/// ```no_run
375/// # use winhttp::Client;
376/// let client = Client::new()?;
377/// let resp = client
378///     .request("POST", "https://httpbin.org/post")
379///     .header("Content-Type", "application/json")
380///     .body(b"{\"key\":\"value\"}")
381///     .send()?;
382/// # Ok::<(), windows::core::Error>(())
383/// ```
384pub struct RequestHelper<'c> {
385    client: &'c Client,
386    method: String,
387    url: String,
388    headers: Vec<(String, String)>,
389    body: Option<Body>,
390}
391
392impl RequestHelper<'_> {
393    /// Add a header to this request.
394    #[must_use]
395    pub fn header(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
396        self.headers.push((name.into(), value.into()));
397        self
398    }
399
400    /// Set the request body.
401    ///
402    /// Accepts anything that converts into a [`Body`]: `&[u8]`, `Vec<u8>`,
403    /// `&str`, `String`, or a pre-built `Body` (e.g. from [`Body::json`]).
404    ///
405    /// If the body carries an implicit content type (such as `Body::json`),
406    /// the corresponding `Content-Type` header is added automatically.
407    ///
408    /// # Examples
409    ///
410    /// ```no_run
411    /// # use winhttp::Client;
412    /// let client = Client::new()?;
413    ///
414    /// // Raw bytes
415    /// let resp = client.request("POST", "https://httpbin.org/post")
416    ///     .body(b"raw bytes")
417    ///     .send()?;
418    /// # Ok::<(), windows::core::Error>(())
419    /// ```
420    ///
421    /// ```no_run
422    /// # #[cfg(feature = "json")]
423    /// # fn demo() -> windows::core::Result<()> {
424    /// # use winhttp::{Body, Client};
425    /// # let client = Client::new()?;
426    /// // JSON body (sets Content-Type automatically)
427    /// let resp = client.request("POST", "https://httpbin.org/post")
428    ///     .body(Body::json(&serde_json::json!({"key": "value"}))?)
429    ///     .send()?;
430    /// # Ok(())
431    /// # }
432    /// ```
433    #[must_use]
434    pub fn body(mut self, data: impl Into<Body>) -> Self {
435        self.body = Some(data.into());
436        self
437    }
438
439    /// Execute the request synchronously and return the full [`Response`].
440    pub fn send(self) -> Result<Response> {
441        let (body_headers, body_bytes) = split_body(self.body);
442        let mut all_headers = body_headers;
443        all_headers.extend(self.headers);
444        self.client
445            .execute(&self.method, &self.url, &all_headers, body_bytes.as_deref())
446    }
447
448    /// Execute the request asynchronously and return the full [`Response`].
449    ///
450    /// This future is **runtime-agnostic** — it works with any executor
451    /// (tokio, smol, pollster, etc.).
452    #[cfg(feature = "async")]
453    pub async fn send_async(self) -> Result<Response> {
454        let (body_headers, body_bytes) = split_body(self.body);
455        let mut all_headers = body_headers;
456        all_headers.extend(self.headers);
457        self.client
458            .execute_async(&self.method, &self.url, &all_headers, body_bytes)
459            .await
460    }
461}
462
463/// Split a `Body` into its implicit headers and raw bytes.
464fn split_body(body: Option<Body>) -> (Vec<(String, String)>, Option<Vec<u8>>) {
465    match body {
466        Some(body) => {
467            let headers = body_headers(&body);
468            (headers, Some(body.bytes))
469        }
470        None => (Vec::new(), None),
471    }
472}
473
474// ---------------------------------------------------------------------------
475// Client
476// ---------------------------------------------------------------------------
477
478/// A reusable HTTP client backed by a WinHTTP [`Session`].
479///
480/// Create one `Client` and reuse it across many requests to benefit from
481/// connection pooling and shared configuration.
482///
483/// # Base URL
484///
485/// Use [`Client::builder`] to set a base URL. Relative paths passed to
486/// request methods are joined to the base URL, while absolute URLs
487/// (starting with `http://` or `https://`) are used as-is.
488///
489/// ```no_run
490/// # use winhttp::Client;
491/// let client = Client::builder()
492///     .base_url("https://httpbin.org")
493///     .build()?;
494///
495/// let resp = client.get("/get")?;          // → https://httpbin.org/get
496/// let resp = client.get("https://x.com")?; // → https://x.com (absolute)
497/// # Ok::<(), windows::core::Error>(())
498/// ```
499pub struct Client {
500    base_url: Option<String>,
501    base_components: Option<UrlComponents>,
502    session: Session,
503    #[cfg(feature = "async")]
504    async_session: Session,
505}
506
507impl Client {
508    /// Create a new `Client` with default settings and no base URL.
509    ///
510    /// Shorthand for `Client::builder().build()`.
511    pub fn new() -> Result<Self> {
512        Self::builder().build()
513    }
514
515    /// Return a [`ClientBuilder`] for full configuration.
516    ///
517    /// # Example
518    ///
519    /// ```no_run
520    /// # use winhttp::Client;
521    /// let client = Client::builder()
522    ///     .base_url("https://api.example.com")
523    ///     .user_agent("my-app/2.0")
524    ///     .connect_timeout_ms(10_000)
525    ///     .build()?;
526    /// # Ok::<(), windows::core::Error>(())
527    /// ```
528    #[must_use]
529    pub fn builder() -> ClientBuilder {
530        ClientBuilder {
531            base_url: None,
532            user_agent: "winhttp-rs/0.1.0".to_string(),
533            connect_timeout_ms: 60_000,
534            send_timeout_ms: 30_000,
535            receive_timeout_ms: 30_000,
536        }
537    }
538
539    /// Access the underlying sync [`Session`].
540    #[must_use]
541    pub fn session(&self) -> &Session {
542        &self.session
543    }
544
545    /// Access the underlying async [`Session`].
546    #[cfg(feature = "async")]
547    #[must_use]
548    pub fn async_session(&self) -> &Session {
549        &self.async_session
550    }
551
552    /// Return the base URL, if one was configured.
553    #[must_use]
554    pub fn base_url(&self) -> Option<&str> {
555        self.base_url.as_deref()
556    }
557
558    /// Resolve a URL against the configured base URL, returning cracked
559    /// [`UrlComponents`] ready for use by the execute methods.
560    ///
561    /// - Absolute URLs are cracked directly via [`crack_url`].
562    /// - Relative paths are merged with the stored base components.
563    /// - If no base URL is configured, the input is cracked as-is (will
564    ///   fail if it is not a valid absolute URL).
565    fn resolve_url(&self, url: &str) -> Result<UrlComponents> {
566        // If crack_url succeeds the URL is already absolute — use it.
567        if let Ok(components) = crack_url(url) {
568            return Ok(components);
569        }
570
571        // Relative path — we need stored base components.
572        let Some(base) = &self.base_components else {
573            // No base URL configured; crack again to surface the original
574            // WinHTTP error for the caller.
575            return crack_url(url);
576        };
577
578        let mut components = base.clone();
579
580        // Split the relative URL into path and query/fragment.
581        let (path_part, extra_part) = match url.find(['?', '#']) {
582            Some(i) => (&url[..i], &url[i..]),
583            None => (url, ""),
584        };
585
586        if path_part.starts_with('/') {
587            components.path = path_part.to_string();
588        } else {
589            let base_path = components.path.trim_end_matches('/');
590            components.path = format!("{base_path}/{path_part}");
591        }
592        components.extra_info = extra_part.to_string();
593
594        Ok(components)
595    }
596
597    /// Start building a request with a custom method and URL.
598    ///
599    /// Use this when you need to set headers or a body before sending.
600    /// The `url` is resolved against the base URL (see [`Client`] docs).
601    ///
602    /// # Example
603    ///
604    /// ```no_run
605    /// # use winhttp::Client;
606    /// let client = Client::builder()
607    ///     .base_url("https://httpbin.org")
608    ///     .build()?;
609    /// let resp = client
610    ///     .request("PATCH", "/patch")
611    ///     .header("Content-Type", "application/json")
612    ///     .body(b"{\"patched\":true}")
613    ///     .send()?;
614    /// # Ok::<(), windows::core::Error>(())
615    /// ```
616    #[must_use]
617    pub fn request(&self, method: &str, url: &str) -> RequestHelper<'_> {
618        RequestHelper {
619            client: self,
620            method: method.to_string(),
621            url: url.to_string(),
622            headers: Vec::new(),
623            body: None,
624        }
625    }
626
627    // -- Sync helpers -------------------------------------------------------
628
629    /// Perform a synchronous `GET` request.
630    ///
631    /// The `url` is resolved against the base URL (see [`Client`] docs).
632    pub fn get(&self, url: &str) -> Result<Response> {
633        self.execute("GET", url, &[], None)
634    }
635
636    /// Perform a synchronous `POST` request with the given body.
637    ///
638    /// The body can be raw bytes (`&[u8]`, `Vec<u8>`, `&str`, `String`) or a
639    /// [`Body::json`] value. See [`Body`] for details.
640    ///
641    /// The `url` is resolved against the base URL (see [`Client`] docs).
642    pub fn post(&self, url: &str, body: impl Into<Body>) -> Result<Response> {
643        let body = body.into();
644        let headers = body_headers(&body);
645        self.execute("POST", url, &headers, Some(&body.bytes))
646    }
647
648    /// Perform a synchronous `PUT` request with the given body.
649    ///
650    /// The body can be raw bytes or a [`Body::json`] value. See [`Body`].
651    ///
652    /// The `url` is resolved against the base URL (see [`Client`] docs).
653    pub fn put(&self, url: &str, body: impl Into<Body>) -> Result<Response> {
654        let body = body.into();
655        let headers = body_headers(&body);
656        self.execute("PUT", url, &headers, Some(&body.bytes))
657    }
658
659    /// Perform a synchronous `DELETE` request.
660    ///
661    /// The `url` is resolved against the base URL (see [`Client`] docs).
662    pub fn delete(&self, url: &str) -> Result<Response> {
663        self.execute("DELETE", url, &[], None)
664    }
665
666    /// Perform a synchronous `PATCH` request with the given body.
667    ///
668    /// The body can be raw bytes or a [`Body::json`] value. See [`Body`].
669    ///
670    /// The `url` is resolved against the base URL (see [`Client`] docs).
671    pub fn patch(&self, url: &str, body: impl Into<Body>) -> Result<Response> {
672        let body = body.into();
673        let headers = body_headers(&body);
674        self.execute("PATCH", url, &headers, Some(&body.bytes))
675    }
676
677    /// Perform a synchronous `HEAD` request.
678    ///
679    /// The returned `Response` will have an empty body since HEAD responses
680    /// contain no body by definition.
681    ///
682    /// The `url` is resolved against the base URL (see [`Client`] docs).
683    pub fn head(&self, url: &str) -> Result<Response> {
684        self.execute_head(url)
685    }
686
687    // -- Async helpers ------------------------------------------------------
688
689    /// Perform an async `GET` request.
690    ///
691    /// The `url` is resolved against the base URL (see [`Client`] docs).
692    #[cfg(feature = "async")]
693    pub async fn async_get(&self, url: &str) -> Result<Response> {
694        self.execute_async("GET", url, &[], None).await
695    }
696
697    /// Perform an async `POST` request with the given body.
698    ///
699    /// The body can be raw bytes (`&[u8]`, `Vec<u8>`, `&str`, `String`) or a
700    /// [`Body::json`] value. See [`Body`] for details.
701    ///
702    /// The `url` is resolved against the base URL (see [`Client`] docs).
703    #[cfg(feature = "async")]
704    pub async fn async_post(&self, url: &str, body: impl Into<Body>) -> Result<Response> {
705        let body = body.into();
706        let headers = body_headers(&body);
707        self.execute_async("POST", url, &headers, Some(body.bytes))
708            .await
709    }
710
711    /// Perform an async `PUT` request with the given body.
712    ///
713    /// The body can be raw bytes or a [`Body::json`] value. See [`Body`].
714    ///
715    /// The `url` is resolved against the base URL (see [`Client`] docs).
716    #[cfg(feature = "async")]
717    pub async fn async_put(&self, url: &str, body: impl Into<Body>) -> Result<Response> {
718        let body = body.into();
719        let headers = body_headers(&body);
720        self.execute_async("PUT", url, &headers, Some(body.bytes))
721            .await
722    }
723
724    /// Perform an async `DELETE` request.
725    ///
726    /// The `url` is resolved against the base URL (see [`Client`] docs).
727    #[cfg(feature = "async")]
728    pub async fn async_delete(&self, url: &str) -> Result<Response> {
729        self.execute_async("DELETE", url, &[], None).await
730    }
731
732    /// Perform an async `PATCH` request with the given body.
733    ///
734    /// The body can be raw bytes or a [`Body::json`] value. See [`Body`].
735    ///
736    /// The `url` is resolved against the base URL (see [`Client`] docs).
737    #[cfg(feature = "async")]
738    pub async fn async_patch(&self, url: &str, body: impl Into<Body>) -> Result<Response> {
739        let body = body.into();
740        let headers = body_headers(&body);
741        self.execute_async("PATCH", url, &headers, Some(body.bytes))
742            .await
743    }
744
745    /// Perform an async `HEAD` request.
746    ///
747    /// The `url` is resolved against the base URL (see [`Client`] docs).
748    #[cfg(feature = "async")]
749    pub async fn async_head(&self, url: &str) -> Result<Response> {
750        self.execute_async_head(url).await
751    }
752
753    // -- Internal -----------------------------------------------------------
754
755    fn execute(
756        &self,
757        method: &str,
758        url: &str,
759        headers: &[(String, String)],
760        body: Option<&[u8]>,
761    ) -> Result<Response> {
762        let components = self.resolve_url(url)?;
763        let secure = components.scheme.eq_ignore_ascii_case("https");
764        let path_and_query = if components.extra_info.is_empty() {
765            components.path.clone()
766        } else {
767            format!("{}{}", components.path, components.extra_info)
768        };
769
770        let connection = self.session.connect(&components.host, components.port)?;
771
772        let mut builder = connection.request(method, &path_and_query);
773        if secure {
774            builder = builder.secure();
775        }
776        for (name, value) in headers {
777            builder = builder.header(name, value);
778        }
779        let request = builder.build()?;
780
781        match body {
782            Some(data) if !data.is_empty() => request.send_with_body(data)?,
783            _ => request.send()?,
784        }
785        request.receive_response()?;
786
787        let status = request.status_code()?;
788        let status_text = request.status_text().unwrap_or_default();
789        let resp_headers = request.raw_headers().unwrap_or_default();
790        let resp_body = request.read_all()?;
791
792        Ok(Response {
793            status,
794            status_text,
795            headers: resp_headers,
796            body: resp_body,
797        })
798    }
799
800    fn execute_head(&self, url: &str) -> Result<Response> {
801        let components = self.resolve_url(url)?;
802        let secure = components.scheme.eq_ignore_ascii_case("https");
803        let path_and_query = if components.extra_info.is_empty() {
804            components.path.clone()
805        } else {
806            format!("{}{}", components.path, components.extra_info)
807        };
808
809        let connection = self.session.connect(&components.host, components.port)?;
810        let mut builder = connection.request("HEAD", &path_and_query);
811        if secure {
812            builder = builder.secure();
813        }
814        let request = builder.build()?;
815        request.send()?;
816        request.receive_response()?;
817
818        let status = request.status_code()?;
819        let status_text = request.status_text().unwrap_or_default();
820        let resp_headers = request.raw_headers().unwrap_or_default();
821
822        Ok(Response {
823            status,
824            status_text,
825            headers: resp_headers,
826            body: Vec::new(),
827        })
828    }
829
830    #[cfg(feature = "async")]
831    async fn execute_async(
832        &self,
833        method: &str,
834        url: &str,
835        headers: &[(String, String)],
836        body: Option<Vec<u8>>,
837    ) -> Result<Response> {
838        let components = self.resolve_url(url)?;
839        let secure = components.scheme.eq_ignore_ascii_case("https");
840        let path_and_query = if components.extra_info.is_empty() {
841            components.path.clone()
842        } else {
843            format!("{}{}", components.path, components.extra_info)
844        };
845
846        let connection = self
847            .async_session
848            .connect(&components.host, components.port)?;
849
850        let mut builder = connection.request(method, &path_and_query);
851        if secure {
852            builder = builder.secure();
853        }
854        for (name, value) in headers {
855            builder = builder.header(name, value);
856        }
857        let request = builder.build()?;
858        let async_request = request.into_async()?;
859
860        let response = match body {
861            Some(data) if !data.is_empty() => async_request.send_with_body(data).await?,
862            _ => async_request.send().await?,
863        };
864
865        let status = response.status_code()?;
866        let status_text = response.status_text().unwrap_or_default();
867        let resp_headers = response.raw_headers().unwrap_or_default();
868        let resp_body = response.read_all().await?;
869
870        Ok(Response {
871            status,
872            status_text,
873            headers: resp_headers,
874            body: resp_body,
875        })
876    }
877
878    #[cfg(feature = "async")]
879    async fn execute_async_head(&self, url: &str) -> Result<Response> {
880        let components = self.resolve_url(url)?;
881        let secure = components.scheme.eq_ignore_ascii_case("https");
882        let path_and_query = if components.extra_info.is_empty() {
883            components.path.clone()
884        } else {
885            format!("{}{}", components.path, components.extra_info)
886        };
887
888        let connection = self
889            .async_session
890            .connect(&components.host, components.port)?;
891        let mut builder = connection.request("HEAD", &path_and_query);
892        if secure {
893            builder = builder.secure();
894        }
895        let request = builder.build()?;
896        let async_request = request.into_async()?;
897        let response = async_request.send().await?;
898
899        let status = response.status_code()?;
900        let status_text = response.status_text().unwrap_or_default();
901        let resp_headers = response.raw_headers().unwrap_or_default();
902
903        Ok(Response {
904            status,
905            status_text,
906            headers: resp_headers,
907            body: Vec::new(),
908        })
909    }
910}
911
912// ---------------------------------------------------------------------------
913// Module-level one-shot helpers
914// ---------------------------------------------------------------------------
915
916/// Perform a one-shot synchronous `GET` request.
917///
918/// Creates an ephemeral [`Client`] internally. For multiple requests, prefer
919/// creating a [`Client`] and reusing it.
920///
921/// # Example
922///
923/// ```no_run
924/// let resp = winhttp::get("https://httpbin.org/get")?;
925/// assert!(resp.is_success());
926/// println!("{}", resp.text());
927/// # Ok::<(), windows::core::Error>(())
928/// ```
929pub fn get(url: &str) -> Result<Response> {
930    Client::new()?.get(url)
931}
932
933/// Perform a one-shot synchronous `POST` request.
934///
935/// The body can be raw bytes (`&[u8]`, `Vec<u8>`, `&str`, `String`) or a
936/// [`Body::json`] value.
937///
938/// # Example
939///
940/// ```no_run
941/// let resp = winhttp::post("https://httpbin.org/post", b"hello".as_slice())?;
942/// # Ok::<(), windows::core::Error>(())
943/// ```
944pub fn post(url: &str, body: impl Into<Body>) -> Result<Response> {
945    Client::new()?.post(url, body)
946}
947
948/// Perform a one-shot synchronous `PUT` request.
949pub fn put(url: &str, body: impl Into<Body>) -> Result<Response> {
950    Client::new()?.put(url, body)
951}
952
953/// Perform a one-shot synchronous `DELETE` request.
954pub fn delete(url: &str) -> Result<Response> {
955    Client::new()?.delete(url)
956}
957
958/// Perform a one-shot synchronous `PATCH` request.
959pub fn patch(url: &str, body: impl Into<Body>) -> Result<Response> {
960    Client::new()?.patch(url, body)
961}
962
963/// Perform a one-shot synchronous `HEAD` request.
964pub fn head(url: &str) -> Result<Response> {
965    Client::new()?.head(url)
966}
967
968// ---------------------------------------------------------------------------
969// Tests
970// ---------------------------------------------------------------------------
971
972#[cfg(test)]
973mod tests {
974    use super::*;
975
976    #[test]
977    fn test_response_status_checks() {
978        let resp = Response {
979            status: 200,
980            status_text: "OK".to_string(),
981            headers: String::new(),
982            body: b"hello".to_vec(),
983        };
984        assert!(resp.is_success());
985        assert!(!resp.is_redirect());
986        assert!(!resp.is_client_error());
987        assert!(!resp.is_server_error());
988        assert_eq!(resp.text(), "hello");
989    }
990
991    #[test]
992    fn test_response_redirect() {
993        let resp = Response {
994            status: 301,
995            status_text: "Moved Permanently".to_string(),
996            headers: String::new(),
997            body: Vec::new(),
998        };
999        assert!(!resp.is_success());
1000        assert!(resp.is_redirect());
1001    }
1002
1003    #[test]
1004    fn test_response_client_error() {
1005        let resp = Response {
1006            status: 404,
1007            status_text: "Not Found".to_string(),
1008            headers: String::new(),
1009            body: Vec::new(),
1010        };
1011        assert!(resp.is_client_error());
1012        assert!(!resp.is_server_error());
1013    }
1014
1015    #[test]
1016    fn test_response_server_error() {
1017        let resp = Response {
1018            status: 500,
1019            status_text: "Internal Server Error".to_string(),
1020            headers: String::new(),
1021            body: Vec::new(),
1022        };
1023        assert!(resp.is_server_error());
1024        assert!(!resp.is_client_error());
1025    }
1026
1027    #[test]
1028    fn test_client_creation() {
1029        let client = Client::new();
1030        assert!(client.is_ok());
1031    }
1032
1033    #[test]
1034    fn test_client_with_builder() {
1035        let client = Client::builder()
1036            .user_agent("test-client/1.0")
1037            .connect_timeout_ms(5000)
1038            .send_timeout_ms(3000)
1039            .receive_timeout_ms(10_000)
1040            .build();
1041        assert!(client.is_ok());
1042    }
1043
1044    #[test]
1045    fn test_client_with_base_url() {
1046        let client = Client::builder().base_url("https://httpbin.org").build();
1047        assert!(client.is_ok());
1048        let client = client.unwrap();
1049        assert_eq!(client.base_url(), Some("https://httpbin.org"));
1050    }
1051
1052    #[test]
1053    fn test_client_base_url_trailing_slash_trimmed() {
1054        let client = Client::builder()
1055            .base_url("https://httpbin.org///")
1056            .build()
1057            .unwrap();
1058        assert_eq!(client.base_url(), Some("https://httpbin.org"));
1059    }
1060
1061    #[test]
1062    fn test_resolve_url_absolute_bypasses_base() {
1063        let client = Client::builder()
1064            .base_url("https://base.example.com")
1065            .build()
1066            .unwrap();
1067        let resolved = client.resolve_url("https://other.com/foo").unwrap();
1068        assert_eq!(resolved.host, "other.com");
1069        assert_eq!(resolved.path, "/foo");
1070    }
1071
1072    #[test]
1073    fn test_resolve_url_relative_path() {
1074        let client = Client::builder()
1075            .base_url("https://base.example.com")
1076            .build()
1077            .unwrap();
1078        let resolved = client.resolve_url("/api/users").unwrap();
1079        assert_eq!(resolved.host, "base.example.com");
1080        assert_eq!(resolved.path, "/api/users");
1081    }
1082
1083    #[test]
1084    fn test_resolve_url_relative_no_slash() {
1085        let client = Client::builder()
1086            .base_url("https://base.example.com")
1087            .build()
1088            .unwrap();
1089        let resolved = client.resolve_url("api/users").unwrap();
1090        assert_eq!(resolved.host, "base.example.com");
1091        assert_eq!(resolved.path, "/api/users");
1092    }
1093
1094    #[test]
1095    fn test_resolve_url_no_base() {
1096        let client = Client::new().unwrap();
1097        let resolved = client.resolve_url("https://example.com/test").unwrap();
1098        assert_eq!(resolved.host, "example.com");
1099        assert_eq!(resolved.path, "/test");
1100    }
1101
1102    #[test]
1103    fn test_resolve_url_relative_with_query() {
1104        let client = Client::builder()
1105            .base_url("https://base.example.com")
1106            .build()
1107            .unwrap();
1108        let resolved = client.resolve_url("/search?q=hello&page=2").unwrap();
1109        assert_eq!(resolved.host, "base.example.com");
1110        assert_eq!(resolved.path, "/search");
1111        assert_eq!(resolved.extra_info, "?q=hello&page=2");
1112    }
1113
1114    #[test]
1115    fn test_resolve_url_relative_fails_without_base() {
1116        let client = Client::new().unwrap();
1117        let result = client.resolve_url("/relative/path");
1118        assert!(
1119            result.is_err(),
1120            "relative path without base URL should fail"
1121        );
1122    }
1123
1124    // -- Body tests ---------------------------------------------------------
1125
1126    #[test]
1127    fn test_body_from_byte_slice() {
1128        let body: Body = b"hello".as_slice().into();
1129        assert_eq!(body.as_bytes(), b"hello");
1130        assert!(body.content_type().is_none());
1131    }
1132
1133    #[test]
1134    fn test_body_from_vec() {
1135        let body: Body = vec![1, 2, 3].into();
1136        assert_eq!(body.as_bytes(), &[1, 2, 3]);
1137        assert!(body.content_type().is_none());
1138    }
1139
1140    #[test]
1141    fn test_body_from_str() {
1142        let body: Body = "hello".into();
1143        assert_eq!(body.as_bytes(), b"hello");
1144        assert!(body.content_type().is_none());
1145    }
1146
1147    #[test]
1148    fn test_body_from_string() {
1149        let body: Body = String::from("hello").into();
1150        assert_eq!(body.as_bytes(), b"hello");
1151        assert!(body.content_type().is_none());
1152    }
1153
1154    #[test]
1155    fn test_body_headers_raw() {
1156        let body: Body = b"raw".as_slice().into();
1157        let headers = body_headers(&body);
1158        assert!(headers.is_empty());
1159    }
1160
1161    #[cfg(feature = "json")]
1162    #[test]
1163    fn test_body_json() {
1164        use serde::Serialize;
1165
1166        #[derive(Serialize)]
1167        struct Payload {
1168            key: String,
1169            count: u32,
1170        }
1171
1172        let body = Body::json(&Payload {
1173            key: "hello".into(),
1174            count: 42,
1175        })
1176        .expect("serialization should succeed");
1177
1178        assert_eq!(body.content_type(), Some("application/json"));
1179
1180        let parsed: serde_json::Value =
1181            serde_json::from_slice(body.as_bytes()).expect("should be valid JSON");
1182        assert_eq!(parsed["key"], "hello");
1183        assert_eq!(parsed["count"], 42);
1184    }
1185
1186    #[cfg(feature = "json")]
1187    #[test]
1188    fn test_body_json_headers() {
1189        let body = Body::json(&serde_json::json!({"a": 1})).expect("serialization should succeed");
1190        let headers = body_headers(&body);
1191        assert_eq!(headers.len(), 1);
1192        assert_eq!(headers[0].0, "Content-Type");
1193        assert_eq!(headers[0].1, "application/json");
1194    }
1195
1196    #[cfg(feature = "json")]
1197    #[test]
1198    fn test_response_json() {
1199        use serde::Deserialize;
1200
1201        #[derive(Deserialize)]
1202        struct Data {
1203            key: String,
1204            count: u32,
1205        }
1206
1207        let resp = Response {
1208            status: 200,
1209            status_text: "OK".to_string(),
1210            headers: String::new(),
1211            body: br#"{"key":"hello","count":42}"#.to_vec(),
1212        };
1213
1214        let data: Data = resp.json().expect("Failed to parse JSON");
1215        assert_eq!(data.key, "hello");
1216        assert_eq!(data.count, 42);
1217    }
1218
1219    #[cfg(feature = "json")]
1220    #[test]
1221    fn test_response_json_error() {
1222        let resp = Response {
1223            status: 200,
1224            status_text: "OK".to_string(),
1225            headers: String::new(),
1226            body: b"not json".to_vec(),
1227        };
1228
1229        let result: std::result::Result<serde_json::Value, _> = resp.json();
1230        assert!(result.is_err());
1231    }
1232}