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 bytes(&self) -> &[u8] {
221 &self.body
222 }
223
224 pub fn text(&self) -> Result<String, HttpClientError> {
226 String::from_utf8(self.body.clone())
227 .map_err(|e| HttpClientError(format!("body is not valid UTF-8: {e}")))
228 }
229
230 #[cfg(feature = "serde")]
232 pub fn json<T: serde::de::DeserializeOwned>(&self) -> Result<T, HttpClientError> {
233 serde_json::from_slice(&self.body)
234 .map_err(|e| HttpClientError(format!("JSON parse error: {e}")))
235 }
236}
237
238fn build_request_bytes(
242 method: &str,
243 path_and_query: &str,
244 host: &str,
245 headers: &[(String, String)],
246 body: &Option<Vec<u8>>,
247) -> Vec<u8> {
248 let mut out: Vec<u8> = Vec::new();
249
250 let _ = write!(
252 out,
253 "{method} {path_and_query} HTTP/1.1\r\nHost: {host}\r\nConnection: close\r\nUser-Agent: rust-web-server/{}\r\n",
254 env!("CARGO_PKG_VERSION"),
255 );
256
257 if let Some(b) = body {
259 if !b.is_empty() {
260 let _ = write!(out, "Content-Length: {}\r\n", b.len());
261 }
262 }
263
264 for (k, v) in headers {
266 let _ = write!(out, "{k}: {v}\r\n");
267 }
268
269 out.extend_from_slice(b"\r\n");
270
271 if let Some(b) = body {
272 out.extend_from_slice(b);
273 }
274
275 out
276}
277
278fn read_response(stream: &mut dyn Read, is_head: bool) -> Result<Response, HttpClientError> {
280 let mut buf: Vec<u8> = Vec::with_capacity(8192);
281 let mut tmp = [0u8; 4096];
282
283 let header_end = loop {
285 let n = stream
286 .read(&mut tmp)
287 .map_err(|e| HttpClientError(format!("read error: {e}")))?;
288 if n == 0 {
289 if buf.is_empty() {
290 return Err(HttpClientError(
291 "server closed connection without sending a response".into(),
292 ));
293 }
294 break buf.len();
296 }
297 buf.extend_from_slice(&tmp[..n]);
298 if let Some(pos) = buf.windows(4).position(|w| w == b"\r\n\r\n") {
299 break pos + 4;
300 }
301 };
302
303 let header_block = std::str::from_utf8(&buf[..header_end])
305 .map_err(|_| HttpClientError("response headers are not valid UTF-8".into()))?;
306
307 let mut lines = header_block.lines();
308
309 let status_line = lines
311 .next()
312 .ok_or_else(|| HttpClientError("empty response".into()))?;
313 let status = parse_status(status_line)?;
314
315 let response_headers: Vec<(String, String)> = lines
317 .filter_map(|line| {
318 let mut parts = line.splitn(2, ':');
319 let name = parts.next()?.trim().to_string();
320 let value = parts.next()?.trim().to_string();
321 if name.is_empty() {
322 None
323 } else {
324 Some((name, value))
325 }
326 })
327 .collect();
328
329 let mut body = buf[header_end..].to_vec();
331
332 if !is_head {
333 let transfer_encoding = response_headers
335 .iter()
336 .find(|(k, _)| k.to_lowercase() == "transfer-encoding")
337 .map(|(_, v)| v.to_lowercase());
338
339 let content_length: Option<usize> = response_headers
340 .iter()
341 .find(|(k, _)| k.to_lowercase() == "content-length")
342 .and_then(|(_, v)| v.trim().parse().ok());
343
344 if transfer_encoding
345 .as_deref()
346 .map(|te| te.contains("chunked"))
347 .unwrap_or(false)
348 {
349 loop {
351 let n = stream
352 .read(&mut tmp)
353 .map_err(|e| HttpClientError(format!("read error: {e}")))?;
354 if n == 0 {
355 break;
356 }
357 body.extend_from_slice(&tmp[..n]);
358 }
359 body = decode_chunked(&body)?;
360 } else if let Some(len) = content_length {
361 while body.len() < len {
362 let n = stream
363 .read(&mut tmp)
364 .map_err(|e| HttpClientError(format!("read error: {e}")))?;
365 if n == 0 {
366 break;
367 }
368 body.extend_from_slice(&tmp[..n]);
369 }
370 body.truncate(len);
371 } else {
372 loop {
374 let n = stream
375 .read(&mut tmp)
376 .map_err(|e| HttpClientError(format!("read error: {e}")))?;
377 if n == 0 {
378 break;
379 }
380 body.extend_from_slice(&tmp[..n]);
381 }
382 }
383 } else {
384 body.clear();
385 }
386
387 Ok(Response {
388 status,
389 headers: response_headers,
390 body,
391 })
392}
393
394fn parse_status(line: &str) -> Result<u16, HttpClientError> {
395 let mut parts = line.splitn(3, ' ');
397 let _version = parts
398 .next()
399 .ok_or_else(|| HttpClientError("malformed status line".into()))?;
400 let code_str = parts
401 .next()
402 .ok_or_else(|| HttpClientError("missing status code".into()))?;
403 code_str
404 .parse::<u16>()
405 .map_err(|_| HttpClientError(format!("invalid status code '{code_str}'")))
406}
407
408fn decode_chunked(data: &[u8]) -> Result<Vec<u8>, HttpClientError> {
410 let mut out = Vec::new();
411 let mut pos = 0;
412
413 while pos < data.len() {
414 let line_end = data[pos..]
416 .windows(2)
417 .position(|w| w == b"\r\n")
418 .ok_or_else(|| HttpClientError("invalid chunked encoding: missing CRLF".into()))?;
419 let size_line = std::str::from_utf8(&data[pos..pos + line_end])
420 .map_err(|_| HttpClientError("chunked size is not ASCII".into()))?
421 .trim();
422 let size_str = size_line.split(';').next().unwrap_or("").trim();
424 let chunk_size = usize::from_str_radix(size_str, 16)
425 .map_err(|_| HttpClientError(format!("invalid chunk size '{size_str}'")))?;
426 pos += line_end + 2; if chunk_size == 0 {
429 break; }
431
432 let end = pos + chunk_size;
433 if end > data.len() {
434 return Err(HttpClientError("chunked body truncated".into()));
435 }
436 out.extend_from_slice(&data[pos..end]);
437 pos = end + 2; }
439
440 Ok(out)
441}
442
443#[cfg(any(feature = "http-client", feature = "http2"))]
446fn tls_connect(
447 host: &str,
448 tcp: TcpStream,
449) -> Result<rustls::StreamOwned<rustls::ClientConnection, TcpStream>, HttpClientError> {
450 use rustls::pki_types::ServerName;
451 use rustls::ClientConfig;
452
453 let root_store =
454 rustls::RootCertStore::from_iter(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());
455 let config = Arc::new(
456 ClientConfig::builder()
457 .with_root_certificates(root_store)
458 .with_no_client_auth(),
459 );
460 let server_name = ServerName::try_from(host.to_string())
461 .map_err(|e| HttpClientError(format!("invalid hostname '{host}': {e}")))?;
462 let conn = rustls::ClientConnection::new(config, server_name)
463 .map_err(|e| HttpClientError(e.to_string()))?;
464 Ok(rustls::StreamOwned::new(conn, tcp))
465}
466
467fn send_once(
470 method: &str,
471 parsed: &ParsedUrl,
472 headers: &[(String, String)],
473 body: &Option<Vec<u8>>,
474 timeout_ms: u64,
475) -> Result<Response, HttpClientError> {
476 let addr = format!("{}:{}", parsed.host, parsed.port);
477 let timeout = Duration::from_millis(timeout_ms);
478
479 let sock_addr = addr
481 .parse::<std::net::SocketAddr>()
482 .or_else(|_| {
483 use std::net::ToSocketAddrs;
484 addr.to_socket_addrs()
485 .map_err(|e| HttpClientError(format!("DNS lookup for '{addr}' failed: {e}")))?
486 .next()
487 .ok_or_else(|| HttpClientError(format!("no address for '{addr}'")))
488 })
489 .map_err(|e: HttpClientError| e)?;
490
491 let tcp = TcpStream::connect_timeout(&sock_addr, timeout)
492 .map_err(|e| HttpClientError(format!("connect to '{addr}' failed: {e}")))?;
493 tcp.set_read_timeout(Some(timeout))
494 .map_err(|e| HttpClientError(e.to_string()))?;
495 tcp.set_write_timeout(Some(timeout))
496 .map_err(|e| HttpClientError(e.to_string()))?;
497
498 let request_bytes =
499 build_request_bytes(method, &parsed.path_and_query, &parsed.host, headers, body);
500
501 let is_head = method.eq_ignore_ascii_case("HEAD");
502
503 #[cfg(any(feature = "http-client", feature = "http2"))]
505 if parsed.scheme == "https" {
506 let mut tls_stream = tls_connect(&parsed.host, tcp)?;
507 tls_stream
508 .write_all(&request_bytes)
509 .map_err(|e| HttpClientError(format!("write error: {e}")))?;
510 return read_response(&mut tls_stream, is_head);
511 }
512
513 let mut stream = tcp;
515 stream
516 .write_all(&request_bytes)
517 .map_err(|e| HttpClientError(format!("write error: {e}")))?;
518 read_response(&mut stream, is_head)
519}
520
521pub struct Client {
529 timeout_ms: u64,
530 max_redirects: u8,
531}
532
533impl Client {
534 pub fn new() -> Self {
538 Self {
539 timeout_ms: 30_000,
540 max_redirects: 10,
541 }
542 }
543
544 pub fn timeout_ms(mut self, ms: u64) -> Self {
546 self.timeout_ms = ms;
547 self
548 }
549
550 pub fn max_redirects(mut self, n: u8) -> Self {
552 self.max_redirects = n;
553 self
554 }
555
556 pub fn get(&self, url: &str) -> RequestBuilder<'_> {
558 self.request("GET", url)
559 }
560
561 pub fn post(&self, url: &str) -> RequestBuilder<'_> {
563 self.request("POST", url)
564 }
565
566 pub fn put(&self, url: &str) -> RequestBuilder<'_> {
568 self.request("PUT", url)
569 }
570
571 pub fn patch(&self, url: &str) -> RequestBuilder<'_> {
573 self.request("PATCH", url)
574 }
575
576 pub fn delete(&self, url: &str) -> RequestBuilder<'_> {
578 self.request("DELETE", url)
579 }
580
581 pub fn head(&self, url: &str) -> RequestBuilder<'_> {
583 self.request("HEAD", url)
584 }
585
586 pub fn request(&self, method: &str, url: &str) -> RequestBuilder<'_> {
588 RequestBuilder {
589 client: self,
590 method: method.to_uppercase(),
591 url: url.to_string(),
592 headers: Vec::new(),
593 body: None,
594 timeout_ms: None,
595 }
596 }
597}
598
599impl Default for Client {
600 fn default() -> Self {
601 Self::new()
602 }
603}
604
605pub struct RequestBuilder<'a> {
609 client: &'a Client,
610 method: String,
611 url: String,
612 headers: Vec<(String, String)>,
613 body: Option<Vec<u8>>,
614 timeout_ms: Option<u64>,
615}
616
617impl<'a> RequestBuilder<'a> {
618 pub fn header(mut self, name: &str, value: &str) -> Self {
620 self.headers.push((name.to_string(), value.to_string()));
621 self
622 }
623
624 pub fn body(mut self, bytes: Vec<u8>) -> Self {
626 self.body = Some(bytes);
627 self
628 }
629
630 pub fn body_text(mut self, s: &str) -> Self {
632 self.headers
633 .push(("Content-Type".to_string(), "text/plain".to_string()));
634 self.body = Some(s.as_bytes().to_vec());
635 self
636 }
637
638 pub fn body_json(mut self, s: &str) -> Self {
640 self.headers.push((
641 "Content-Type".to_string(),
642 "application/json".to_string(),
643 ));
644 self.body = Some(s.as_bytes().to_vec());
645 self
646 }
647
648 pub fn timeout_ms(mut self, ms: u64) -> Self {
650 self.timeout_ms = Some(ms);
651 self
652 }
653
654 pub fn send(self) -> Result<Response, HttpClientError> {
659 let timeout = self.timeout_ms.unwrap_or(self.client.timeout_ms);
660 let max_redirects = self.client.max_redirects;
661
662 let mut method = self.method;
663 let mut url = self.url;
664 let headers = self.headers;
665 let mut body = self.body;
666 let mut redirects = 0u8;
667
668 loop {
669 let parsed = ParsedUrl::parse(&url)?;
670 let resp = send_once(&method, &parsed, &headers, &body, timeout)?;
671
672 if resp.is_redirect() && redirects < max_redirects {
673 let location = resp
674 .header("location")
675 .ok_or_else(|| HttpClientError("redirect with no Location header".into()))?
676 .to_string();
677 url = resolve_url(&url, &location);
678 redirects += 1;
679 if matches!(resp.status(), 301 | 302 | 303) {
680 method = "GET".to_string();
681 body = None;
682 }
683 continue;
684 }
685
686 return Ok(resp);
687 }
688 }
689}
690
691#[cfg(feature = "http2")]
694pub use async_impl::{AsyncClient, AsyncRequestBuilder};
695
696#[cfg(feature = "http2")]
697mod async_impl {
698 use super::{
699 build_request_bytes, decode_chunked, parse_status, resolve_url, HttpClientError,
700 ParsedUrl, Response,
701 };
702 use std::sync::Arc;
703 use tokio::io::{AsyncReadExt, AsyncWriteExt};
704
705 async fn async_tls_connect(
706 host: &str,
707 stream: tokio::net::TcpStream,
708 ) -> Result<tokio_rustls::client::TlsStream<tokio::net::TcpStream>, HttpClientError> {
709 use rustls::pki_types::ServerName;
710 use rustls::ClientConfig;
711 use tokio_rustls::TlsConnector;
712
713 let root_store = rustls::RootCertStore::from_iter(
714 webpki_roots::TLS_SERVER_ROOTS.iter().cloned(),
715 );
716 let config = Arc::new(
717 ClientConfig::builder()
718 .with_root_certificates(root_store)
719 .with_no_client_auth(),
720 );
721 let connector = TlsConnector::from(config);
722 let server_name = ServerName::try_from(host.to_string())
723 .map_err(|e| HttpClientError(format!("invalid hostname '{host}': {e}")))?;
724 connector
725 .connect(server_name, stream)
726 .await
727 .map_err(|e| HttpClientError(format!("TLS handshake failed: {e}")))
728 }
729
730 async fn async_read_response(
731 stream: &mut (impl AsyncReadExt + Unpin),
732 is_head: bool,
733 ) -> Result<Response, HttpClientError> {
734 let mut buf: Vec<u8> = Vec::with_capacity(8192);
735 let mut tmp = vec![0u8; 4096];
736
737 let header_end = loop {
738 let n = stream
739 .read(&mut tmp)
740 .await
741 .map_err(|e| HttpClientError(format!("read error: {e}")))?;
742 if n == 0 {
743 if buf.is_empty() {
744 return Err(HttpClientError(
745 "server closed connection without a response".into(),
746 ));
747 }
748 break buf.len();
749 }
750 buf.extend_from_slice(&tmp[..n]);
751 if let Some(pos) = buf.windows(4).position(|w| w == b"\r\n\r\n") {
752 break pos + 4;
753 }
754 };
755
756 let header_block = std::str::from_utf8(&buf[..header_end])
757 .map_err(|_| HttpClientError("response headers not UTF-8".into()))?;
758
759 let mut lines = header_block.lines();
760 let status_line = lines
761 .next()
762 .ok_or_else(|| HttpClientError("empty response".into()))?;
763 let status = parse_status(status_line)?;
764
765 let response_headers: Vec<(String, String)> = lines
766 .filter_map(|line| {
767 let mut parts = line.splitn(2, ':');
768 let name = parts.next()?.trim().to_string();
769 let value = parts.next()?.trim().to_string();
770 if name.is_empty() { None } else { Some((name, value)) }
771 })
772 .collect();
773
774 let mut body = buf[header_end..].to_vec();
775
776 if !is_head {
777 let transfer_encoding = response_headers
778 .iter()
779 .find(|(k, _)| k.to_lowercase() == "transfer-encoding")
780 .map(|(_, v)| v.to_lowercase());
781
782 let content_length: Option<usize> = response_headers
783 .iter()
784 .find(|(k, _)| k.to_lowercase() == "content-length")
785 .and_then(|(_, v)| v.trim().parse().ok());
786
787 if transfer_encoding
788 .as_deref()
789 .map(|te| te.contains("chunked"))
790 .unwrap_or(false)
791 {
792 loop {
793 let n = stream.read(&mut tmp).await
794 .map_err(|e| HttpClientError(format!("read error: {e}")))?;
795 if n == 0 { break; }
796 body.extend_from_slice(&tmp[..n]);
797 }
798 body = decode_chunked(&body)?;
799 } else if let Some(len) = content_length {
800 while body.len() < len {
801 let n = stream.read(&mut tmp).await
802 .map_err(|e| HttpClientError(format!("read error: {e}")))?;
803 if n == 0 { break; }
804 body.extend_from_slice(&tmp[..n]);
805 }
806 body.truncate(len);
807 } else {
808 loop {
809 let n = stream.read(&mut tmp).await
810 .map_err(|e| HttpClientError(format!("read error: {e}")))?;
811 if n == 0 { break; }
812 body.extend_from_slice(&tmp[..n]);
813 }
814 }
815 } else {
816 body.clear();
817 }
818
819 Ok(Response { status, headers: response_headers, body })
820 }
821
822 async fn async_send_once(
823 method: &str,
824 parsed: &ParsedUrl,
825 headers: &[(String, String)],
826 body: &Option<Vec<u8>>,
827 timeout_ms: u64,
828 ) -> Result<Response, HttpClientError> {
829 use std::time::Duration;
830 use tokio::net::TcpStream;
831 use tokio::time::timeout;
832
833 let addr = format!("{}:{}", parsed.host, parsed.port);
834 let dur = Duration::from_millis(timeout_ms);
835 let request_bytes =
836 build_request_bytes(method, &parsed.path_and_query, &parsed.host, headers, body);
837 let is_head = method.eq_ignore_ascii_case("HEAD");
838
839 let tcp = timeout(dur, TcpStream::connect(&addr))
840 .await
841 .map_err(|_| HttpClientError(format!("connect to '{addr}' timed out")))?
842 .map_err(|e| HttpClientError(format!("connect to '{addr}' failed: {e}")))?;
843
844 if parsed.scheme == "https" {
845 let tls_stream = timeout(dur, async_tls_connect(&parsed.host, tcp))
846 .await
847 .map_err(|_| HttpClientError("TLS handshake timed out".into()))??;
848 let mut stream = tls_stream;
849 timeout(dur, stream.write_all(&request_bytes))
850 .await
851 .map_err(|_| HttpClientError("write timed out".into()))?
852 .map_err(|e| HttpClientError(format!("write error: {e}")))?;
853 return timeout(dur, async_read_response(&mut stream, is_head))
854 .await
855 .map_err(|_| HttpClientError("read timed out".into()))?;
856 }
857
858 let mut stream = tcp;
859 timeout(dur, stream.write_all(&request_bytes))
860 .await
861 .map_err(|_| HttpClientError("write timed out".into()))?
862 .map_err(|e| HttpClientError(format!("write error: {e}")))?;
863 timeout(dur, async_read_response(&mut stream, is_head))
864 .await
865 .map_err(|_| HttpClientError("read timed out".into()))?
866 }
867
868 pub struct AsyncClient {
870 timeout_ms: u64,
871 max_redirects: u8,
872 }
873
874 impl AsyncClient {
875 pub fn new() -> Self {
877 Self {
878 timeout_ms: 30_000,
879 max_redirects: 10,
880 }
881 }
882
883 pub fn timeout_ms(mut self, ms: u64) -> Self {
885 self.timeout_ms = ms;
886 self
887 }
888
889 pub fn max_redirects(mut self, n: u8) -> Self {
891 self.max_redirects = n;
892 self
893 }
894
895 pub fn get(&self, url: &str) -> AsyncRequestBuilder<'_> {
897 self.request("GET", url)
898 }
899
900 pub fn post(&self, url: &str) -> AsyncRequestBuilder<'_> {
902 self.request("POST", url)
903 }
904
905 pub fn put(&self, url: &str) -> AsyncRequestBuilder<'_> {
907 self.request("PUT", url)
908 }
909
910 pub fn patch(&self, url: &str) -> AsyncRequestBuilder<'_> {
912 self.request("PATCH", url)
913 }
914
915 pub fn delete(&self, url: &str) -> AsyncRequestBuilder<'_> {
917 self.request("DELETE", url)
918 }
919
920 pub fn request(&self, method: &str, url: &str) -> AsyncRequestBuilder<'_> {
922 AsyncRequestBuilder {
923 client: self,
924 method: method.to_uppercase(),
925 url: url.to_string(),
926 headers: Vec::new(),
927 body: None,
928 timeout_ms: None,
929 }
930 }
931 }
932
933 impl Default for AsyncClient {
934 fn default() -> Self {
935 Self::new()
936 }
937 }
938
939 pub struct AsyncRequestBuilder<'a> {
941 client: &'a AsyncClient,
942 method: String,
943 url: String,
944 headers: Vec<(String, String)>,
945 body: Option<Vec<u8>>,
946 timeout_ms: Option<u64>,
947 }
948
949 impl<'a> AsyncRequestBuilder<'a> {
950 pub fn header(mut self, name: &str, value: &str) -> Self {
952 self.headers.push((name.to_string(), value.to_string()));
953 self
954 }
955
956 pub fn body(mut self, bytes: Vec<u8>) -> Self {
958 self.body = Some(bytes);
959 self
960 }
961
962 pub fn body_text(mut self, s: &str) -> Self {
964 self.headers
965 .push(("Content-Type".to_string(), "text/plain".to_string()));
966 self.body = Some(s.as_bytes().to_vec());
967 self
968 }
969
970 pub fn body_json(mut self, s: &str) -> Self {
972 self.headers.push((
973 "Content-Type".to_string(),
974 "application/json".to_string(),
975 ));
976 self.body = Some(s.as_bytes().to_vec());
977 self
978 }
979
980 pub fn timeout_ms(mut self, ms: u64) -> Self {
982 self.timeout_ms = Some(ms);
983 self
984 }
985
986 pub async fn send(self) -> Result<Response, HttpClientError> {
988 let timeout = self.timeout_ms.unwrap_or(self.client.timeout_ms);
989 let max_redirects = self.client.max_redirects;
990
991 let mut method = self.method;
992 let mut url = self.url;
993 let headers = self.headers;
994 let mut body = self.body;
995 let mut redirects = 0u8;
996
997 loop {
998 let parsed = ParsedUrl::parse(&url)?;
999 let resp = async_send_once(&method, &parsed, &headers, &body, timeout).await?;
1000
1001 if resp.is_redirect() && redirects < max_redirects {
1002 let location = resp
1003 .header("location")
1004 .ok_or_else(|| {
1005 HttpClientError("redirect with no Location header".into())
1006 })?
1007 .to_string();
1008 url = resolve_url(&url, &location);
1009 redirects += 1;
1010 if matches!(resp.status(), 301 | 302 | 303) {
1011 method = "GET".to_string();
1012 body = None;
1013 }
1014 continue;
1015 }
1016
1017 return Ok(resp);
1018 }
1019 }
1020 }
1021}