1#[cfg(test)]
56mod tests;
57
58use std::io::{Read, Write};
59use std::net::TcpStream;
60use std::time::Duration;
61
62#[cfg(any(feature = "http-client", feature = "http2"))]
63use std::sync::Arc;
64
65#[derive(Debug)]
69pub struct HttpClientError(pub String);
70
71impl std::fmt::Display for HttpClientError {
72 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
73 f.write_str(&self.0)
74 }
75}
76
77impl std::error::Error for HttpClientError {}
78
79struct ParsedUrl {
82 scheme: String,
83 host: String,
84 port: u16,
85 path_and_query: String,
86}
87
88impl ParsedUrl {
89 fn parse(url: &str) -> Result<Self, HttpClientError> {
90 let rest = if let Some(r) = url.strip_prefix("https://") {
92 ("https", r)
93 } else if let Some(r) = url.strip_prefix("http://") {
94 ("http", r)
95 } else {
96 return Err(HttpClientError(format!(
97 "unsupported or missing URL scheme in '{url}'"
98 )));
99 };
100
101 let (scheme, authority_and_path) = rest;
102 let default_port: u16 = if scheme == "https" { 443 } else { 80 };
103
104 let (authority, path_and_query) = match authority_and_path.find('/') {
106 Some(idx) => {
107 let (a, p) = authority_and_path.split_at(idx);
108 (a, p.to_string())
109 }
110 None => (authority_and_path, "/".to_string()),
111 };
112
113 let (host, port) = if let Some(bracket_end) = authority.find(']') {
115 let host = &authority[..=bracket_end];
117 let port_part = &authority[bracket_end + 1..];
118 let port = if let Some(p) = port_part.strip_prefix(':') {
119 p.parse::<u16>().map_err(|_| {
120 HttpClientError(format!("invalid port in URL '{url}'"))
121 })?
122 } else {
123 default_port
124 };
125 (host.to_string(), port)
126 } else {
127 match authority.rfind(':') {
128 Some(idx) => {
129 let port_str = &authority[idx + 1..];
130 let port = port_str.parse::<u16>().map_err(|_| {
131 HttpClientError(format!("invalid port in URL '{url}'"))
132 })?;
133 (authority[..idx].to_string(), port)
134 }
135 None => (authority.to_string(), default_port),
136 }
137 };
138
139 if host.is_empty() {
140 return Err(HttpClientError(format!("missing host in URL '{url}'")));
141 }
142
143 Ok(ParsedUrl {
144 scheme: scheme.to_string(),
145 host,
146 port,
147 path_and_query,
148 })
149 }
150}
151
152fn resolve_url(base_url: &str, location: &str) -> String {
156 if location.starts_with("http://") || location.starts_with("https://") {
157 return location.to_string();
158 }
159 if let Ok(base) = ParsedUrl::parse(base_url) {
161 let default_port = if base.scheme == "https" { 443 } else { 80 };
162 let port_str = if base.port == default_port {
163 String::new()
164 } else {
165 format!(":{}", base.port)
166 };
167 if location.starts_with('/') {
168 return format!("{}://{}{}{}", base.scheme, base.host, port_str, location);
169 }
170 let base_path = base.path_and_query;
172 let dir = match base_path.rfind('/') {
173 Some(i) => &base_path[..=i],
174 None => "/",
175 };
176 return format!(
177 "{}://{}{}{}{}",
178 base.scheme, base.host, port_str, dir, location
179 );
180 }
181 location.to_string()
182}
183
184#[derive(Debug)]
188pub struct Response {
189 status: u16,
190 headers: Vec<(String, String)>,
191 body: Vec<u8>,
192}
193
194impl Response {
195 pub fn status(&self) -> u16 {
197 self.status
198 }
199
200 pub fn is_success(&self) -> bool {
202 (200..300).contains(&self.status)
203 }
204
205 pub fn is_redirect(&self) -> bool {
207 matches!(self.status, 301 | 302 | 303 | 307 | 308)
208 }
209
210 pub fn header(&self, name: &str) -> Option<&str> {
212 let lower = name.to_lowercase();
213 self.headers
214 .iter()
215 .find(|(k, _)| k.to_lowercase() == lower)
216 .map(|(_, v)| v.as_str())
217 }
218
219 pub fn headers(&self) -> &[(String, String)] {
223 &self.headers
224 }
225
226 pub fn bytes(&self) -> &[u8] {
228 &self.body
229 }
230
231 pub fn text(&self) -> Result<String, HttpClientError> {
233 String::from_utf8(self.body.clone())
234 .map_err(|e| HttpClientError(format!("body is not valid UTF-8: {e}")))
235 }
236
237 #[cfg(feature = "serde")]
239 pub fn json<T: serde::de::DeserializeOwned>(&self) -> Result<T, HttpClientError> {
240 serde_json::from_slice(&self.body)
241 .map_err(|e| HttpClientError(format!("JSON parse error: {e}")))
242 }
243}
244
245fn build_request_bytes(
249 method: &str,
250 path_and_query: &str,
251 host: &str,
252 headers: &[(String, String)],
253 body: &Option<Vec<u8>>,
254) -> Vec<u8> {
255 let mut out: Vec<u8> = Vec::new();
256
257 let _ = write!(
259 out,
260 "{method} {path_and_query} HTTP/1.1\r\nHost: {host}\r\nConnection: close\r\nUser-Agent: rust-web-server/{}\r\n",
261 env!("CARGO_PKG_VERSION"),
262 );
263
264 if let Some(b) = body {
266 if !b.is_empty() {
267 let _ = write!(out, "Content-Length: {}\r\n", b.len());
268 }
269 }
270
271 for (k, v) in headers {
273 let _ = write!(out, "{k}: {v}\r\n");
274 }
275
276 out.extend_from_slice(b"\r\n");
277
278 if let Some(b) = body {
279 out.extend_from_slice(b);
280 }
281
282 out
283}
284
285fn read_response(stream: &mut dyn Read, is_head: bool) -> Result<Response, HttpClientError> {
287 let mut buf: Vec<u8> = Vec::with_capacity(8192);
288 let mut tmp = [0u8; 4096];
289
290 let header_end = loop {
292 let n = stream
293 .read(&mut tmp)
294 .map_err(|e| HttpClientError(format!("read error: {e}")))?;
295 if n == 0 {
296 if buf.is_empty() {
297 return Err(HttpClientError(
298 "server closed connection without sending a response".into(),
299 ));
300 }
301 break buf.len();
303 }
304 buf.extend_from_slice(&tmp[..n]);
305 if let Some(pos) = buf.windows(4).position(|w| w == b"\r\n\r\n") {
306 break pos + 4;
307 }
308 };
309
310 let header_block = std::str::from_utf8(&buf[..header_end])
312 .map_err(|_| HttpClientError("response headers are not valid UTF-8".into()))?;
313
314 let mut lines = header_block.lines();
315
316 let status_line = lines
318 .next()
319 .ok_or_else(|| HttpClientError("empty response".into()))?;
320 let status = parse_status(status_line)?;
321
322 let response_headers: Vec<(String, String)> = lines
324 .filter_map(|line| {
325 let mut parts = line.splitn(2, ':');
326 let name = parts.next()?.trim().to_string();
327 let value = parts.next()?.trim().to_string();
328 if name.is_empty() {
329 None
330 } else {
331 Some((name, value))
332 }
333 })
334 .collect();
335
336 let mut body = buf[header_end..].to_vec();
338
339 if !is_head {
340 let transfer_encoding = response_headers
342 .iter()
343 .find(|(k, _)| k.to_lowercase() == "transfer-encoding")
344 .map(|(_, v)| v.to_lowercase());
345
346 let content_length: Option<usize> = response_headers
347 .iter()
348 .find(|(k, _)| k.to_lowercase() == "content-length")
349 .and_then(|(_, v)| v.trim().parse().ok());
350
351 if transfer_encoding
352 .as_deref()
353 .map(|te| te.contains("chunked"))
354 .unwrap_or(false)
355 {
356 loop {
358 let n = stream
359 .read(&mut tmp)
360 .map_err(|e| HttpClientError(format!("read error: {e}")))?;
361 if n == 0 {
362 break;
363 }
364 body.extend_from_slice(&tmp[..n]);
365 }
366 body = decode_chunked(&body)?;
367 } else if let Some(len) = content_length {
368 while body.len() < len {
369 let n = stream
370 .read(&mut tmp)
371 .map_err(|e| HttpClientError(format!("read error: {e}")))?;
372 if n == 0 {
373 break;
374 }
375 body.extend_from_slice(&tmp[..n]);
376 }
377 body.truncate(len);
378 } else {
379 loop {
381 let n = stream
382 .read(&mut tmp)
383 .map_err(|e| HttpClientError(format!("read error: {e}")))?;
384 if n == 0 {
385 break;
386 }
387 body.extend_from_slice(&tmp[..n]);
388 }
389 }
390 } else {
391 body.clear();
392 }
393
394 Ok(Response {
395 status,
396 headers: response_headers,
397 body,
398 })
399}
400
401fn parse_status(line: &str) -> Result<u16, HttpClientError> {
402 let mut parts = line.splitn(3, ' ');
404 let _version = parts
405 .next()
406 .ok_or_else(|| HttpClientError("malformed status line".into()))?;
407 let code_str = parts
408 .next()
409 .ok_or_else(|| HttpClientError("missing status code".into()))?;
410 code_str
411 .parse::<u16>()
412 .map_err(|_| HttpClientError(format!("invalid status code '{code_str}'")))
413}
414
415fn decode_chunked(data: &[u8]) -> Result<Vec<u8>, HttpClientError> {
417 let mut out = Vec::new();
418 let mut pos = 0;
419
420 while pos < data.len() {
421 let line_end = data[pos..]
423 .windows(2)
424 .position(|w| w == b"\r\n")
425 .ok_or_else(|| HttpClientError("invalid chunked encoding: missing CRLF".into()))?;
426 let size_line = std::str::from_utf8(&data[pos..pos + line_end])
427 .map_err(|_| HttpClientError("chunked size is not ASCII".into()))?
428 .trim();
429 let size_str = size_line.split(';').next().unwrap_or("").trim();
431 let chunk_size = usize::from_str_radix(size_str, 16)
432 .map_err(|_| HttpClientError(format!("invalid chunk size '{size_str}'")))?;
433 pos += line_end + 2; if chunk_size == 0 {
436 break; }
438
439 let end = pos + chunk_size;
440 if end > data.len() {
441 return Err(HttpClientError("chunked body truncated".into()));
442 }
443 out.extend_from_slice(&data[pos..end]);
444 pos = end + 2; }
446
447 Ok(out)
448}
449
450#[cfg(any(feature = "http-client", feature = "http2"))]
453fn tls_connect(
454 host: &str,
455 tcp: TcpStream,
456) -> Result<rustls::StreamOwned<rustls::ClientConnection, TcpStream>, HttpClientError> {
457 use rustls::pki_types::ServerName;
458 use rustls::ClientConfig;
459
460 let root_store =
461 rustls::RootCertStore::from_iter(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());
462 let config = Arc::new(
463 ClientConfig::builder()
464 .with_root_certificates(root_store)
465 .with_no_client_auth(),
466 );
467 let server_name = ServerName::try_from(host.to_string())
468 .map_err(|e| HttpClientError(format!("invalid hostname '{host}': {e}")))?;
469 let conn = rustls::ClientConnection::new(config, server_name)
470 .map_err(|e| HttpClientError(e.to_string()))?;
471 Ok(rustls::StreamOwned::new(conn, tcp))
472}
473
474fn send_once(
477 method: &str,
478 parsed: &ParsedUrl,
479 headers: &[(String, String)],
480 body: &Option<Vec<u8>>,
481 timeout_ms: u64,
482) -> Result<Response, HttpClientError> {
483 let addr = format!("{}:{}", parsed.host, parsed.port);
484 let timeout = Duration::from_millis(timeout_ms);
485
486 let sock_addr = addr
488 .parse::<std::net::SocketAddr>()
489 .or_else(|_| {
490 use std::net::ToSocketAddrs;
491 addr.to_socket_addrs()
492 .map_err(|e| HttpClientError(format!("DNS lookup for '{addr}' failed: {e}")))?
493 .next()
494 .ok_or_else(|| HttpClientError(format!("no address for '{addr}'")))
495 })
496 .map_err(|e: HttpClientError| e)?;
497
498 let tcp = TcpStream::connect_timeout(&sock_addr, timeout)
499 .map_err(|e| HttpClientError(format!("connect to '{addr}' failed: {e}")))?;
500 tcp.set_read_timeout(Some(timeout))
501 .map_err(|e| HttpClientError(e.to_string()))?;
502 tcp.set_write_timeout(Some(timeout))
503 .map_err(|e| HttpClientError(e.to_string()))?;
504
505 let request_bytes =
506 build_request_bytes(method, &parsed.path_and_query, &parsed.host, headers, body);
507
508 let is_head = method.eq_ignore_ascii_case("HEAD");
509
510 #[cfg(any(feature = "http-client", feature = "http2"))]
512 if parsed.scheme == "https" {
513 let mut tls_stream = tls_connect(&parsed.host, tcp)?;
514 tls_stream
515 .write_all(&request_bytes)
516 .map_err(|e| HttpClientError(format!("write error: {e}")))?;
517 return read_response(&mut tls_stream, is_head);
518 }
519
520 let mut stream = tcp;
522 stream
523 .write_all(&request_bytes)
524 .map_err(|e| HttpClientError(format!("write error: {e}")))?;
525 read_response(&mut stream, is_head)
526}
527
528pub struct Client {
536 timeout_ms: u64,
537 max_redirects: u8,
538}
539
540impl Client {
541 pub fn new() -> Self {
545 Self {
546 timeout_ms: 30_000,
547 max_redirects: 10,
548 }
549 }
550
551 pub fn timeout_ms(mut self, ms: u64) -> Self {
553 self.timeout_ms = ms;
554 self
555 }
556
557 pub fn max_redirects(mut self, n: u8) -> Self {
559 self.max_redirects = n;
560 self
561 }
562
563 pub fn get(&self, url: &str) -> RequestBuilder<'_> {
565 self.request("GET", url)
566 }
567
568 pub fn post(&self, url: &str) -> RequestBuilder<'_> {
570 self.request("POST", url)
571 }
572
573 pub fn put(&self, url: &str) -> RequestBuilder<'_> {
575 self.request("PUT", url)
576 }
577
578 pub fn patch(&self, url: &str) -> RequestBuilder<'_> {
580 self.request("PATCH", url)
581 }
582
583 pub fn delete(&self, url: &str) -> RequestBuilder<'_> {
585 self.request("DELETE", url)
586 }
587
588 pub fn head(&self, url: &str) -> RequestBuilder<'_> {
590 self.request("HEAD", url)
591 }
592
593 pub fn request(&self, method: &str, url: &str) -> RequestBuilder<'_> {
595 RequestBuilder {
596 client: self,
597 method: method.to_uppercase(),
598 url: url.to_string(),
599 headers: Vec::new(),
600 body: None,
601 timeout_ms: None,
602 }
603 }
604}
605
606impl Default for Client {
607 fn default() -> Self {
608 Self::new()
609 }
610}
611
612pub struct RequestBuilder<'a> {
616 client: &'a Client,
617 method: String,
618 url: String,
619 headers: Vec<(String, String)>,
620 body: Option<Vec<u8>>,
621 timeout_ms: Option<u64>,
622}
623
624impl<'a> RequestBuilder<'a> {
625 pub fn header(mut self, name: &str, value: &str) -> Self {
627 self.headers.push((name.to_string(), value.to_string()));
628 self
629 }
630
631 pub fn body(mut self, bytes: Vec<u8>) -> Self {
633 self.body = Some(bytes);
634 self
635 }
636
637 pub fn body_text(mut self, s: &str) -> Self {
639 self.headers
640 .push(("Content-Type".to_string(), "text/plain".to_string()));
641 self.body = Some(s.as_bytes().to_vec());
642 self
643 }
644
645 pub fn body_json(mut self, s: &str) -> Self {
647 self.headers.push((
648 "Content-Type".to_string(),
649 "application/json".to_string(),
650 ));
651 self.body = Some(s.as_bytes().to_vec());
652 self
653 }
654
655 pub fn timeout_ms(mut self, ms: u64) -> Self {
657 self.timeout_ms = Some(ms);
658 self
659 }
660
661 pub fn send(self) -> Result<Response, HttpClientError> {
666 let timeout = self.timeout_ms.unwrap_or(self.client.timeout_ms);
667 let max_redirects = self.client.max_redirects;
668
669 let mut method = self.method;
670 let mut url = self.url;
671 let headers = self.headers;
672 let mut body = self.body;
673 let mut redirects = 0u8;
674
675 loop {
676 let parsed = ParsedUrl::parse(&url)?;
677 let resp = send_once(&method, &parsed, &headers, &body, timeout)?;
678
679 if resp.is_redirect() && redirects < max_redirects {
680 let location = resp
681 .header("location")
682 .ok_or_else(|| HttpClientError("redirect with no Location header".into()))?
683 .to_string();
684 url = resolve_url(&url, &location);
685 redirects += 1;
686 if matches!(resp.status(), 301 | 302 | 303) {
687 method = "GET".to_string();
688 body = None;
689 }
690 continue;
691 }
692
693 return Ok(resp);
694 }
695 }
696}
697
698#[cfg(feature = "http2")]
701pub use async_impl::{AsyncClient, AsyncRequestBuilder};
702
703#[cfg(feature = "http2")]
704mod async_impl {
705 use super::{
706 build_request_bytes, decode_chunked, parse_status, resolve_url, HttpClientError,
707 ParsedUrl, Response,
708 };
709 use std::sync::Arc;
710 use tokio::io::{AsyncReadExt, AsyncWriteExt};
711
712 async fn async_tls_connect(
713 host: &str,
714 stream: tokio::net::TcpStream,
715 ) -> Result<tokio_rustls::client::TlsStream<tokio::net::TcpStream>, HttpClientError> {
716 use rustls::pki_types::ServerName;
717 use rustls::ClientConfig;
718 use tokio_rustls::TlsConnector;
719
720 let root_store = rustls::RootCertStore::from_iter(
721 webpki_roots::TLS_SERVER_ROOTS.iter().cloned(),
722 );
723 let config = Arc::new(
724 ClientConfig::builder()
725 .with_root_certificates(root_store)
726 .with_no_client_auth(),
727 );
728 let connector = TlsConnector::from(config);
729 let server_name = ServerName::try_from(host.to_string())
730 .map_err(|e| HttpClientError(format!("invalid hostname '{host}': {e}")))?;
731 connector
732 .connect(server_name, stream)
733 .await
734 .map_err(|e| HttpClientError(format!("TLS handshake failed: {e}")))
735 }
736
737 async fn async_read_response(
738 stream: &mut (impl AsyncReadExt + Unpin),
739 is_head: bool,
740 ) -> Result<Response, HttpClientError> {
741 let mut buf: Vec<u8> = Vec::with_capacity(8192);
742 let mut tmp = vec![0u8; 4096];
743
744 let header_end = loop {
745 let n = stream
746 .read(&mut tmp)
747 .await
748 .map_err(|e| HttpClientError(format!("read error: {e}")))?;
749 if n == 0 {
750 if buf.is_empty() {
751 return Err(HttpClientError(
752 "server closed connection without a response".into(),
753 ));
754 }
755 break buf.len();
756 }
757 buf.extend_from_slice(&tmp[..n]);
758 if let Some(pos) = buf.windows(4).position(|w| w == b"\r\n\r\n") {
759 break pos + 4;
760 }
761 };
762
763 let header_block = std::str::from_utf8(&buf[..header_end])
764 .map_err(|_| HttpClientError("response headers not UTF-8".into()))?;
765
766 let mut lines = header_block.lines();
767 let status_line = lines
768 .next()
769 .ok_or_else(|| HttpClientError("empty response".into()))?;
770 let status = parse_status(status_line)?;
771
772 let response_headers: Vec<(String, String)> = lines
773 .filter_map(|line| {
774 let mut parts = line.splitn(2, ':');
775 let name = parts.next()?.trim().to_string();
776 let value = parts.next()?.trim().to_string();
777 if name.is_empty() { None } else { Some((name, value)) }
778 })
779 .collect();
780
781 let mut body = buf[header_end..].to_vec();
782
783 if !is_head {
784 let transfer_encoding = response_headers
785 .iter()
786 .find(|(k, _)| k.to_lowercase() == "transfer-encoding")
787 .map(|(_, v)| v.to_lowercase());
788
789 let content_length: Option<usize> = response_headers
790 .iter()
791 .find(|(k, _)| k.to_lowercase() == "content-length")
792 .and_then(|(_, v)| v.trim().parse().ok());
793
794 if transfer_encoding
795 .as_deref()
796 .map(|te| te.contains("chunked"))
797 .unwrap_or(false)
798 {
799 loop {
800 let n = stream.read(&mut tmp).await
801 .map_err(|e| HttpClientError(format!("read error: {e}")))?;
802 if n == 0 { break; }
803 body.extend_from_slice(&tmp[..n]);
804 }
805 body = decode_chunked(&body)?;
806 } else if let Some(len) = content_length {
807 while body.len() < len {
808 let n = stream.read(&mut tmp).await
809 .map_err(|e| HttpClientError(format!("read error: {e}")))?;
810 if n == 0 { break; }
811 body.extend_from_slice(&tmp[..n]);
812 }
813 body.truncate(len);
814 } else {
815 loop {
816 let n = stream.read(&mut tmp).await
817 .map_err(|e| HttpClientError(format!("read error: {e}")))?;
818 if n == 0 { break; }
819 body.extend_from_slice(&tmp[..n]);
820 }
821 }
822 } else {
823 body.clear();
824 }
825
826 Ok(Response { status, headers: response_headers, body })
827 }
828
829 async fn async_send_once(
830 method: &str,
831 parsed: &ParsedUrl,
832 headers: &[(String, String)],
833 body: &Option<Vec<u8>>,
834 timeout_ms: u64,
835 ) -> Result<Response, HttpClientError> {
836 use std::time::Duration;
837 use tokio::net::TcpStream;
838 use tokio::time::timeout;
839
840 let addr = format!("{}:{}", parsed.host, parsed.port);
841 let dur = Duration::from_millis(timeout_ms);
842 let request_bytes =
843 build_request_bytes(method, &parsed.path_and_query, &parsed.host, headers, body);
844 let is_head = method.eq_ignore_ascii_case("HEAD");
845
846 let tcp = timeout(dur, TcpStream::connect(&addr))
847 .await
848 .map_err(|_| HttpClientError(format!("connect to '{addr}' timed out")))?
849 .map_err(|e| HttpClientError(format!("connect to '{addr}' failed: {e}")))?;
850
851 if parsed.scheme == "https" {
852 let tls_stream = timeout(dur, async_tls_connect(&parsed.host, tcp))
853 .await
854 .map_err(|_| HttpClientError("TLS handshake timed out".into()))??;
855 let mut stream = tls_stream;
856 timeout(dur, stream.write_all(&request_bytes))
857 .await
858 .map_err(|_| HttpClientError("write timed out".into()))?
859 .map_err(|e| HttpClientError(format!("write error: {e}")))?;
860 return timeout(dur, async_read_response(&mut stream, is_head))
861 .await
862 .map_err(|_| HttpClientError("read timed out".into()))?;
863 }
864
865 let mut stream = tcp;
866 timeout(dur, stream.write_all(&request_bytes))
867 .await
868 .map_err(|_| HttpClientError("write timed out".into()))?
869 .map_err(|e| HttpClientError(format!("write error: {e}")))?;
870 timeout(dur, async_read_response(&mut stream, is_head))
871 .await
872 .map_err(|_| HttpClientError("read timed out".into()))?
873 }
874
875 pub struct AsyncClient {
877 timeout_ms: u64,
878 max_redirects: u8,
879 }
880
881 impl AsyncClient {
882 pub fn new() -> Self {
884 Self {
885 timeout_ms: 30_000,
886 max_redirects: 10,
887 }
888 }
889
890 pub fn timeout_ms(mut self, ms: u64) -> Self {
892 self.timeout_ms = ms;
893 self
894 }
895
896 pub fn max_redirects(mut self, n: u8) -> Self {
898 self.max_redirects = n;
899 self
900 }
901
902 pub fn get(&self, url: &str) -> AsyncRequestBuilder<'_> {
904 self.request("GET", url)
905 }
906
907 pub fn post(&self, url: &str) -> AsyncRequestBuilder<'_> {
909 self.request("POST", url)
910 }
911
912 pub fn put(&self, url: &str) -> AsyncRequestBuilder<'_> {
914 self.request("PUT", url)
915 }
916
917 pub fn patch(&self, url: &str) -> AsyncRequestBuilder<'_> {
919 self.request("PATCH", url)
920 }
921
922 pub fn delete(&self, url: &str) -> AsyncRequestBuilder<'_> {
924 self.request("DELETE", url)
925 }
926
927 pub fn request(&self, method: &str, url: &str) -> AsyncRequestBuilder<'_> {
929 AsyncRequestBuilder {
930 client: self,
931 method: method.to_uppercase(),
932 url: url.to_string(),
933 headers: Vec::new(),
934 body: None,
935 timeout_ms: None,
936 }
937 }
938 }
939
940 impl Default for AsyncClient {
941 fn default() -> Self {
942 Self::new()
943 }
944 }
945
946 pub struct AsyncRequestBuilder<'a> {
948 client: &'a AsyncClient,
949 method: String,
950 url: String,
951 headers: Vec<(String, String)>,
952 body: Option<Vec<u8>>,
953 timeout_ms: Option<u64>,
954 }
955
956 impl<'a> AsyncRequestBuilder<'a> {
957 pub fn header(mut self, name: &str, value: &str) -> Self {
959 self.headers.push((name.to_string(), value.to_string()));
960 self
961 }
962
963 pub fn body(mut self, bytes: Vec<u8>) -> Self {
965 self.body = Some(bytes);
966 self
967 }
968
969 pub fn body_text(mut self, s: &str) -> Self {
971 self.headers
972 .push(("Content-Type".to_string(), "text/plain".to_string()));
973 self.body = Some(s.as_bytes().to_vec());
974 self
975 }
976
977 pub fn body_json(mut self, s: &str) -> Self {
979 self.headers.push((
980 "Content-Type".to_string(),
981 "application/json".to_string(),
982 ));
983 self.body = Some(s.as_bytes().to_vec());
984 self
985 }
986
987 pub fn timeout_ms(mut self, ms: u64) -> Self {
989 self.timeout_ms = Some(ms);
990 self
991 }
992
993 pub async fn send(self) -> Result<Response, HttpClientError> {
995 let timeout = self.timeout_ms.unwrap_or(self.client.timeout_ms);
996 let max_redirects = self.client.max_redirects;
997
998 let mut method = self.method;
999 let mut url = self.url;
1000 let headers = self.headers;
1001 let mut body = self.body;
1002 let mut redirects = 0u8;
1003
1004 loop {
1005 let parsed = ParsedUrl::parse(&url)?;
1006 let resp = async_send_once(&method, &parsed, &headers, &body, timeout).await?;
1007
1008 if resp.is_redirect() && redirects < max_redirects {
1009 let location = resp
1010 .header("location")
1011 .ok_or_else(|| {
1012 HttpClientError("redirect with no Location header".into())
1013 })?
1014 .to_string();
1015 url = resolve_url(&url, &location);
1016 redirects += 1;
1017 if matches!(resp.status(), 301 | 302 | 303) {
1018 method = "GET".to_string();
1019 body = None;
1020 }
1021 continue;
1022 }
1023
1024 return Ok(resp);
1025 }
1026 }
1027 }
1028}