1#[cfg(test)]
56mod tests;
57
58use std::io::{Read, Write};
59#[cfg(not(target_arch = "wasm32"))]
60use std::net::TcpStream;
61#[cfg(not(target_arch = "wasm32"))]
62use std::time::Duration;
63
64#[cfg(any(feature = "http-client", feature = "http2"))]
65use std::sync::Arc;
66
67fn form_encode(s: &str) -> String {
71 let mut out = String::with_capacity(s.len());
72 for b in s.bytes() {
73 match b {
74 b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => {
75 out.push(b as char)
76 }
77 _ => out.push_str(&format!("%{:02X}", b)),
78 }
79 }
80 out
81}
82
83fn form_urlencode(pairs: &[(&str, &str)]) -> String {
85 pairs
86 .iter()
87 .map(|(k, v)| format!("{}={}", form_encode(k), form_encode(v)))
88 .collect::<Vec<_>>()
89 .join("&")
90}
91
92#[derive(Debug)]
96pub struct HttpClientError(pub String);
97
98impl std::fmt::Display for HttpClientError {
99 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
100 f.write_str(&self.0)
101 }
102}
103
104impl std::error::Error for HttpClientError {}
105
106struct ParsedUrl {
109 scheme: String,
110 host: String,
111 port: u16,
112 path_and_query: String,
113}
114
115impl ParsedUrl {
116 fn parse(url: &str) -> Result<Self, HttpClientError> {
117 let rest = if let Some(r) = url.strip_prefix("https://") {
119 ("https", r)
120 } else if let Some(r) = url.strip_prefix("http://") {
121 ("http", r)
122 } else {
123 return Err(HttpClientError(format!(
124 "unsupported or missing URL scheme in '{url}'"
125 )));
126 };
127
128 let (scheme, authority_and_path) = rest;
129 let default_port: u16 = if scheme == "https" { 443 } else { 80 };
130
131 let (authority, path_and_query) = match authority_and_path.find('/') {
133 Some(idx) => {
134 let (a, p) = authority_and_path.split_at(idx);
135 (a, p.to_string())
136 }
137 None => (authority_and_path, "/".to_string()),
138 };
139
140 let (host, port) = if let Some(bracket_end) = authority.find(']') {
142 let host = &authority[..=bracket_end];
144 let port_part = &authority[bracket_end + 1..];
145 let port = if let Some(p) = port_part.strip_prefix(':') {
146 p.parse::<u16>().map_err(|_| {
147 HttpClientError(format!("invalid port in URL '{url}'"))
148 })?
149 } else {
150 default_port
151 };
152 (host.to_string(), port)
153 } else {
154 match authority.rfind(':') {
155 Some(idx) => {
156 let port_str = &authority[idx + 1..];
157 let port = port_str.parse::<u16>().map_err(|_| {
158 HttpClientError(format!("invalid port in URL '{url}'"))
159 })?;
160 (authority[..idx].to_string(), port)
161 }
162 None => (authority.to_string(), default_port),
163 }
164 };
165
166 if host.is_empty() {
167 return Err(HttpClientError(format!("missing host in URL '{url}'")));
168 }
169
170 Ok(ParsedUrl {
171 scheme: scheme.to_string(),
172 host,
173 port,
174 path_and_query,
175 })
176 }
177}
178
179fn resolve_url(base_url: &str, location: &str) -> String {
183 if location.starts_with("http://") || location.starts_with("https://") {
184 return location.to_string();
185 }
186 if let Ok(base) = ParsedUrl::parse(base_url) {
188 let default_port = if base.scheme == "https" { 443 } else { 80 };
189 let port_str = if base.port == default_port {
190 String::new()
191 } else {
192 format!(":{}", base.port)
193 };
194 if location.starts_with('/') {
195 return format!("{}://{}{}{}", base.scheme, base.host, port_str, location);
196 }
197 let base_path = base.path_and_query;
199 let dir = match base_path.rfind('/') {
200 Some(i) => &base_path[..=i],
201 None => "/",
202 };
203 return format!(
204 "{}://{}{}{}{}",
205 base.scheme, base.host, port_str, dir, location
206 );
207 }
208 location.to_string()
209}
210
211#[derive(Debug)]
215pub struct Response {
216 status: u16,
217 headers: Vec<(String, String)>,
218 body: Vec<u8>,
219}
220
221impl Response {
222 pub fn status(&self) -> u16 {
224 self.status
225 }
226
227 pub fn is_success(&self) -> bool {
229 (200..300).contains(&self.status)
230 }
231
232 pub fn is_redirect(&self) -> bool {
234 matches!(self.status, 301 | 302 | 303 | 307 | 308)
235 }
236
237 pub fn header(&self, name: &str) -> Option<&str> {
239 let lower = name.to_lowercase();
240 self.headers
241 .iter()
242 .find(|(k, _)| k.to_lowercase() == lower)
243 .map(|(_, v)| v.as_str())
244 }
245
246 pub fn headers(&self) -> &[(String, String)] {
250 &self.headers
251 }
252
253 pub fn bytes(&self) -> &[u8] {
255 &self.body
256 }
257
258 pub fn text(&self) -> Result<String, HttpClientError> {
260 String::from_utf8(self.body.clone())
261 .map_err(|e| HttpClientError(format!("body is not valid UTF-8: {e}")))
262 }
263
264 #[cfg(feature = "serde")]
266 pub fn json<T: serde::de::DeserializeOwned>(&self) -> Result<T, HttpClientError> {
267 serde_json::from_slice(&self.body)
268 .map_err(|e| HttpClientError(format!("JSON parse error: {e}")))
269 }
270}
271
272fn build_request_bytes(
276 method: &str,
277 path_and_query: &str,
278 host: &str,
279 headers: &[(String, String)],
280 body: &Option<Vec<u8>>,
281) -> Vec<u8> {
282 let mut out: Vec<u8> = Vec::new();
283
284 let _ = write!(
286 out,
287 "{method} {path_and_query} HTTP/1.1\r\nHost: {host}\r\nConnection: close\r\nUser-Agent: rust-web-server/{}\r\n",
288 env!("CARGO_PKG_VERSION"),
289 );
290
291 if let Some(b) = body {
293 if !b.is_empty() {
294 let _ = write!(out, "Content-Length: {}\r\n", b.len());
295 }
296 }
297
298 for (k, v) in headers {
300 let _ = write!(out, "{k}: {v}\r\n");
301 }
302
303 out.extend_from_slice(b"\r\n");
304
305 if let Some(b) = body {
306 out.extend_from_slice(b);
307 }
308
309 out
310}
311
312fn read_response(stream: &mut dyn Read, is_head: bool) -> Result<Response, HttpClientError> {
314 let mut buf: Vec<u8> = Vec::with_capacity(8192);
315 let mut tmp = [0u8; 4096];
316
317 let header_end = loop {
319 let n = stream
320 .read(&mut tmp)
321 .map_err(|e| HttpClientError(format!("read error: {e}")))?;
322 if n == 0 {
323 if buf.is_empty() {
324 return Err(HttpClientError(
325 "server closed connection without sending a response".into(),
326 ));
327 }
328 break buf.len();
330 }
331 buf.extend_from_slice(&tmp[..n]);
332 if let Some(pos) = buf.windows(4).position(|w| w == b"\r\n\r\n") {
333 break pos + 4;
334 }
335 };
336
337 let header_block = std::str::from_utf8(&buf[..header_end])
339 .map_err(|_| HttpClientError("response headers are not valid UTF-8".into()))?;
340
341 let mut lines = header_block.lines();
342
343 let status_line = lines
345 .next()
346 .ok_or_else(|| HttpClientError("empty response".into()))?;
347 let status = parse_status(status_line)?;
348
349 let response_headers: Vec<(String, String)> = lines
351 .filter_map(|line| {
352 let mut parts = line.splitn(2, ':');
353 let name = parts.next()?.trim().to_string();
354 let value = parts.next()?.trim().to_string();
355 if name.is_empty() {
356 None
357 } else {
358 Some((name, value))
359 }
360 })
361 .collect();
362
363 let mut body = buf[header_end..].to_vec();
365
366 if !is_head {
367 let transfer_encoding = response_headers
369 .iter()
370 .find(|(k, _)| k.to_lowercase() == "transfer-encoding")
371 .map(|(_, v)| v.to_lowercase());
372
373 let content_length: Option<usize> = response_headers
374 .iter()
375 .find(|(k, _)| k.to_lowercase() == "content-length")
376 .and_then(|(_, v)| v.trim().parse().ok());
377
378 if transfer_encoding
379 .as_deref()
380 .map(|te| te.contains("chunked"))
381 .unwrap_or(false)
382 {
383 loop {
385 let n = stream
386 .read(&mut tmp)
387 .map_err(|e| HttpClientError(format!("read error: {e}")))?;
388 if n == 0 {
389 break;
390 }
391 body.extend_from_slice(&tmp[..n]);
392 }
393 body = decode_chunked(&body)?;
394 } else if let Some(len) = content_length {
395 while body.len() < len {
396 let n = stream
397 .read(&mut tmp)
398 .map_err(|e| HttpClientError(format!("read error: {e}")))?;
399 if n == 0 {
400 break;
401 }
402 body.extend_from_slice(&tmp[..n]);
403 }
404 body.truncate(len);
405 } else {
406 loop {
408 let n = stream
409 .read(&mut tmp)
410 .map_err(|e| HttpClientError(format!("read error: {e}")))?;
411 if n == 0 {
412 break;
413 }
414 body.extend_from_slice(&tmp[..n]);
415 }
416 }
417 } else {
418 body.clear();
419 }
420
421 Ok(Response {
422 status,
423 headers: response_headers,
424 body,
425 })
426}
427
428fn parse_status(line: &str) -> Result<u16, HttpClientError> {
429 let mut parts = line.splitn(3, ' ');
431 let _version = parts
432 .next()
433 .ok_or_else(|| HttpClientError("malformed status line".into()))?;
434 let code_str = parts
435 .next()
436 .ok_or_else(|| HttpClientError("missing status code".into()))?;
437 code_str
438 .parse::<u16>()
439 .map_err(|_| HttpClientError(format!("invalid status code '{code_str}'")))
440}
441
442fn decode_chunked(data: &[u8]) -> Result<Vec<u8>, HttpClientError> {
444 let mut out = Vec::new();
445 let mut pos = 0;
446
447 while pos < data.len() {
448 let line_end = data[pos..]
450 .windows(2)
451 .position(|w| w == b"\r\n")
452 .ok_or_else(|| HttpClientError("invalid chunked encoding: missing CRLF".into()))?;
453 let size_line = std::str::from_utf8(&data[pos..pos + line_end])
454 .map_err(|_| HttpClientError("chunked size is not ASCII".into()))?
455 .trim();
456 let size_str = size_line.split(';').next().unwrap_or("").trim();
458 let chunk_size = usize::from_str_radix(size_str, 16)
459 .map_err(|_| HttpClientError(format!("invalid chunk size '{size_str}'")))?;
460 pos += line_end + 2; if chunk_size == 0 {
463 break; }
465
466 let end = pos + chunk_size;
467 if end > data.len() {
468 return Err(HttpClientError("chunked body truncated".into()));
469 }
470 out.extend_from_slice(&data[pos..end]);
471 pos = end + 2; }
473
474 Ok(out)
475}
476
477#[cfg(all(any(feature = "http-client", feature = "http2"), not(target_arch = "wasm32")))]
480fn tls_connect(
481 host: &str,
482 tcp: TcpStream,
483) -> Result<rustls::StreamOwned<rustls::ClientConnection, TcpStream>, HttpClientError> {
484 use rustls::pki_types::ServerName;
485 use rustls::ClientConfig;
486
487 let root_store =
488 rustls::RootCertStore::from_iter(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());
489 let config = Arc::new(
490 ClientConfig::builder()
491 .with_root_certificates(root_store)
492 .with_no_client_auth(),
493 );
494 let server_name = ServerName::try_from(host.to_string())
495 .map_err(|e| HttpClientError(format!("invalid hostname '{host}': {e}")))?;
496 let conn = rustls::ClientConnection::new(config, server_name)
497 .map_err(|e| HttpClientError(e.to_string()))?;
498 Ok(rustls::StreamOwned::new(conn, tcp))
499}
500
501#[cfg(not(target_arch = "wasm32"))]
504fn send_once(
505 method: &str,
506 parsed: &ParsedUrl,
507 headers: &[(String, String)],
508 body: &Option<Vec<u8>>,
509 timeout_ms: u64,
510) -> Result<Response, HttpClientError> {
511 let addr = format!("{}:{}", parsed.host, parsed.port);
512 let timeout = Duration::from_millis(timeout_ms);
513
514 let sock_addr = addr
516 .parse::<std::net::SocketAddr>()
517 .or_else(|_| {
518 use std::net::ToSocketAddrs;
519 addr.to_socket_addrs()
520 .map_err(|e| HttpClientError(format!("DNS lookup for '{addr}' failed: {e}")))?
521 .next()
522 .ok_or_else(|| HttpClientError(format!("no address for '{addr}'")))
523 })
524 .map_err(|e: HttpClientError| e)?;
525
526 let tcp = TcpStream::connect_timeout(&sock_addr, timeout)
527 .map_err(|e| HttpClientError(format!("connect to '{addr}' failed: {e}")))?;
528 tcp.set_read_timeout(Some(timeout))
529 .map_err(|e| HttpClientError(e.to_string()))?;
530 tcp.set_write_timeout(Some(timeout))
531 .map_err(|e| HttpClientError(e.to_string()))?;
532
533 let request_bytes =
534 build_request_bytes(method, &parsed.path_and_query, &parsed.host, headers, body);
535
536 let is_head = method.eq_ignore_ascii_case("HEAD");
537
538 #[cfg(any(feature = "http-client", feature = "http2"))]
540 if parsed.scheme == "https" {
541 let mut tls_stream = tls_connect(&parsed.host, tcp)?;
542 tls_stream
543 .write_all(&request_bytes)
544 .map_err(|e| HttpClientError(format!("write error: {e}")))?;
545 return read_response(&mut tls_stream, is_head);
546 }
547
548 let mut stream = tcp;
550 stream
551 .write_all(&request_bytes)
552 .map_err(|e| HttpClientError(format!("write error: {e}")))?;
553 read_response(&mut stream, is_head)
554}
555
556#[cfg(target_arch = "wasm32")]
571fn send_once(
572 method: &str,
573 parsed: &ParsedUrl,
574 headers: &[(String, String)],
575 body: &Option<Vec<u8>>,
576 timeout_ms: u64,
577) -> Result<Response, HttpClientError> {
578 use wasip2::http::outgoing_handler;
579 use wasip2::http::types::{Fields, OutgoingBody, OutgoingRequest, RequestOptions, Scheme};
580
581 let wasi_headers = Fields::new();
582 for (name, value) in headers {
583 let _ = wasi_headers.append(name, value.as_bytes());
584 }
585
586 let request = OutgoingRequest::new(wasi_headers);
587 request
588 .set_method(&to_wasi_method(method))
589 .map_err(|_| HttpClientError(format!("invalid method '{method}'")))?;
590 let scheme = if parsed.scheme == "https" { Scheme::Https } else { Scheme::Http };
591 request
592 .set_scheme(Some(&scheme))
593 .map_err(|_| HttpClientError(format!("invalid scheme '{}'", parsed.scheme)))?;
594 let authority = format!("{}:{}", parsed.host, parsed.port);
595 request
596 .set_authority(Some(&authority))
597 .map_err(|_| HttpClientError(format!("invalid authority '{authority}'")))?;
598 request
599 .set_path_with_query(Some(&parsed.path_and_query))
600 .map_err(|_| HttpClientError(format!("invalid path '{}'", parsed.path_and_query)))?;
601
602 let outgoing_body = request
603 .body()
604 .map_err(|_| HttpClientError("outgoing-request body handle taken twice".to_string()))?;
605 if let Some(bytes) = body {
606 let mut out = outgoing_body
607 .write()
608 .map_err(|_| HttpClientError("outgoing-body write stream taken twice".to_string()))?;
609 out.write_all(bytes)
610 .map_err(|e| HttpClientError(format!("write error: {e}")))?;
611 }
612 OutgoingBody::finish(outgoing_body, None)
613 .map_err(|e| HttpClientError(format!("failed to finish request body: {e:?}")))?;
614
615 let options = RequestOptions::new();
619 let timeout_ns = timeout_ms.saturating_mul(1_000_000);
620 let _ = options.set_connect_timeout(Some(timeout_ns));
621 let _ = options.set_first_byte_timeout(Some(timeout_ns));
622 let _ = options.set_between_bytes_timeout(Some(timeout_ns));
623
624 let future_response = outgoing_handler::handle(request, Some(options))
625 .map_err(|e| HttpClientError(format!("failed to dispatch request: {e:?}")))?;
626
627 future_response.subscribe().block();
628
629 let incoming_response = match future_response.get() {
630 Some(Ok(Ok(response))) => response,
631 Some(Ok(Err(code))) => return Err(HttpClientError(format!("{code:?}"))),
632 Some(Err(())) => {
633 return Err(HttpClientError("response already retrieved".to_string()))
634 }
635 None => {
636 return Err(HttpClientError(
637 "response not ready after blocking on it".to_string(),
638 ))
639 }
640 };
641
642 let status = incoming_response.status();
643 let resp_headers = incoming_response
644 .headers()
645 .entries()
646 .into_iter()
647 .map(|(name, value)| (name, String::from_utf8_lossy(&value).to_string()))
648 .collect();
649
650 let mut resp_body = Vec::new();
651 if !method.eq_ignore_ascii_case("HEAD") {
652 if let Ok(incoming_body) = incoming_response.consume() {
653 if let Ok(mut stream) = incoming_body.stream() {
654 let _ = stream.read_to_end(&mut resp_body);
655 }
656 }
657 }
658
659 Ok(Response { status, headers: resp_headers, body: resp_body })
660}
661
662#[cfg(target_arch = "wasm32")]
663fn to_wasi_method(method: &str) -> wasip2::http::types::Method {
664 use wasip2::http::types::Method;
665 match method.to_ascii_uppercase().as_str() {
666 "GET" => Method::Get,
667 "HEAD" => Method::Head,
668 "POST" => Method::Post,
669 "PUT" => Method::Put,
670 "DELETE" => Method::Delete,
671 "CONNECT" => Method::Connect,
672 "OPTIONS" => Method::Options,
673 "TRACE" => Method::Trace,
674 "PATCH" => Method::Patch,
675 other => Method::Other(other.to_string()),
676 }
677}
678
679pub struct Client {
687 timeout_ms: u64,
688 max_redirects: u8,
689}
690
691impl Client {
692 pub fn new() -> Self {
696 Self {
697 timeout_ms: 30_000,
698 max_redirects: 10,
699 }
700 }
701
702 pub fn timeout_ms(mut self, ms: u64) -> Self {
704 self.timeout_ms = ms;
705 self
706 }
707
708 pub fn max_redirects(mut self, n: u8) -> Self {
710 self.max_redirects = n;
711 self
712 }
713
714 pub fn get(&self, url: &str) -> RequestBuilder<'_> {
716 self.request("GET", url)
717 }
718
719 pub fn post(&self, url: &str) -> RequestBuilder<'_> {
721 self.request("POST", url)
722 }
723
724 pub fn put(&self, url: &str) -> RequestBuilder<'_> {
726 self.request("PUT", url)
727 }
728
729 pub fn patch(&self, url: &str) -> RequestBuilder<'_> {
731 self.request("PATCH", url)
732 }
733
734 pub fn delete(&self, url: &str) -> RequestBuilder<'_> {
736 self.request("DELETE", url)
737 }
738
739 pub fn head(&self, url: &str) -> RequestBuilder<'_> {
741 self.request("HEAD", url)
742 }
743
744 pub fn request(&self, method: &str, url: &str) -> RequestBuilder<'_> {
746 RequestBuilder {
747 client: self,
748 method: method.to_uppercase(),
749 url: url.to_string(),
750 headers: Vec::new(),
751 body: None,
752 timeout_ms: None,
753 }
754 }
755}
756
757impl Default for Client {
758 fn default() -> Self {
759 Self::new()
760 }
761}
762
763pub struct RequestBuilder<'a> {
767 client: &'a Client,
768 method: String,
769 url: String,
770 headers: Vec<(String, String)>,
771 body: Option<Vec<u8>>,
772 timeout_ms: Option<u64>,
773}
774
775impl<'a> RequestBuilder<'a> {
776 pub fn header(mut self, name: &str, value: &str) -> Self {
778 self.headers.push((name.to_string(), value.to_string()));
779 self
780 }
781
782 pub fn body(mut self, bytes: Vec<u8>) -> Self {
784 self.body = Some(bytes);
785 self
786 }
787
788 pub fn body_text(mut self, s: &str) -> Self {
790 self.headers
791 .push(("Content-Type".to_string(), "text/plain".to_string()));
792 self.body = Some(s.as_bytes().to_vec());
793 self
794 }
795
796 pub fn body_json(mut self, s: &str) -> Self {
798 self.headers.push((
799 "Content-Type".to_string(),
800 "application/json".to_string(),
801 ));
802 self.body = Some(s.as_bytes().to_vec());
803 self
804 }
805
806 pub fn form(mut self, pairs: &[(&str, &str)]) -> Self {
821 self.headers.push((
822 "Content-Type".to_string(),
823 "application/x-www-form-urlencoded".to_string(),
824 ));
825 self.body = Some(form_urlencode(pairs).into_bytes());
826 self
827 }
828
829 pub fn timeout_ms(mut self, ms: u64) -> Self {
831 self.timeout_ms = Some(ms);
832 self
833 }
834
835 pub fn send(self) -> Result<Response, HttpClientError> {
840 let timeout = self.timeout_ms.unwrap_or(self.client.timeout_ms);
841 let max_redirects = self.client.max_redirects;
842
843 let mut method = self.method;
844 let mut url = self.url;
845 let headers = self.headers;
846 let mut body = self.body;
847 let mut redirects = 0u8;
848
849 loop {
850 let parsed = ParsedUrl::parse(&url)?;
851 let resp = send_once(&method, &parsed, &headers, &body, timeout)?;
852
853 if resp.is_redirect() && redirects < max_redirects {
854 let location = resp
855 .header("location")
856 .ok_or_else(|| HttpClientError("redirect with no Location header".into()))?
857 .to_string();
858 url = resolve_url(&url, &location);
859 redirects += 1;
860 if matches!(resp.status(), 301 | 302 | 303) {
861 method = "GET".to_string();
862 body = None;
863 }
864 continue;
865 }
866
867 return Ok(resp);
868 }
869 }
870}
871
872#[cfg(feature = "http2")]
875pub use async_impl::{AsyncClient, AsyncRequestBuilder};
876
877#[cfg(feature = "http2")]
878mod async_impl {
879 use super::{
880 build_request_bytes, decode_chunked, parse_status, resolve_url, HttpClientError,
881 ParsedUrl, Response,
882 };
883 use std::sync::Arc;
884 use tokio::io::{AsyncReadExt, AsyncWriteExt};
885
886 async fn async_tls_connect(
887 host: &str,
888 stream: tokio::net::TcpStream,
889 ) -> Result<tokio_rustls::client::TlsStream<tokio::net::TcpStream>, HttpClientError> {
890 use rustls::pki_types::ServerName;
891 use rustls::ClientConfig;
892 use tokio_rustls::TlsConnector;
893
894 let root_store = rustls::RootCertStore::from_iter(
895 webpki_roots::TLS_SERVER_ROOTS.iter().cloned(),
896 );
897 let config = Arc::new(
898 ClientConfig::builder()
899 .with_root_certificates(root_store)
900 .with_no_client_auth(),
901 );
902 let connector = TlsConnector::from(config);
903 let server_name = ServerName::try_from(host.to_string())
904 .map_err(|e| HttpClientError(format!("invalid hostname '{host}': {e}")))?;
905 connector
906 .connect(server_name, stream)
907 .await
908 .map_err(|e| HttpClientError(format!("TLS handshake failed: {e}")))
909 }
910
911 async fn async_read_response(
912 stream: &mut (impl AsyncReadExt + Unpin),
913 is_head: bool,
914 ) -> Result<Response, HttpClientError> {
915 let mut buf: Vec<u8> = Vec::with_capacity(8192);
916 let mut tmp = vec![0u8; 4096];
917
918 let header_end = loop {
919 let n = stream
920 .read(&mut tmp)
921 .await
922 .map_err(|e| HttpClientError(format!("read error: {e}")))?;
923 if n == 0 {
924 if buf.is_empty() {
925 return Err(HttpClientError(
926 "server closed connection without a response".into(),
927 ));
928 }
929 break buf.len();
930 }
931 buf.extend_from_slice(&tmp[..n]);
932 if let Some(pos) = buf.windows(4).position(|w| w == b"\r\n\r\n") {
933 break pos + 4;
934 }
935 };
936
937 let header_block = std::str::from_utf8(&buf[..header_end])
938 .map_err(|_| HttpClientError("response headers not UTF-8".into()))?;
939
940 let mut lines = header_block.lines();
941 let status_line = lines
942 .next()
943 .ok_or_else(|| HttpClientError("empty response".into()))?;
944 let status = parse_status(status_line)?;
945
946 let response_headers: Vec<(String, String)> = lines
947 .filter_map(|line| {
948 let mut parts = line.splitn(2, ':');
949 let name = parts.next()?.trim().to_string();
950 let value = parts.next()?.trim().to_string();
951 if name.is_empty() { None } else { Some((name, value)) }
952 })
953 .collect();
954
955 let mut body = buf[header_end..].to_vec();
956
957 if !is_head {
958 let transfer_encoding = response_headers
959 .iter()
960 .find(|(k, _)| k.to_lowercase() == "transfer-encoding")
961 .map(|(_, v)| v.to_lowercase());
962
963 let content_length: Option<usize> = response_headers
964 .iter()
965 .find(|(k, _)| k.to_lowercase() == "content-length")
966 .and_then(|(_, v)| v.trim().parse().ok());
967
968 if transfer_encoding
969 .as_deref()
970 .map(|te| te.contains("chunked"))
971 .unwrap_or(false)
972 {
973 loop {
974 let n = stream.read(&mut tmp).await
975 .map_err(|e| HttpClientError(format!("read error: {e}")))?;
976 if n == 0 { break; }
977 body.extend_from_slice(&tmp[..n]);
978 }
979 body = decode_chunked(&body)?;
980 } else if let Some(len) = content_length {
981 while body.len() < len {
982 let n = stream.read(&mut tmp).await
983 .map_err(|e| HttpClientError(format!("read error: {e}")))?;
984 if n == 0 { break; }
985 body.extend_from_slice(&tmp[..n]);
986 }
987 body.truncate(len);
988 } else {
989 loop {
990 let n = stream.read(&mut tmp).await
991 .map_err(|e| HttpClientError(format!("read error: {e}")))?;
992 if n == 0 { break; }
993 body.extend_from_slice(&tmp[..n]);
994 }
995 }
996 } else {
997 body.clear();
998 }
999
1000 Ok(Response { status, headers: response_headers, body })
1001 }
1002
1003 async fn async_send_once(
1004 method: &str,
1005 parsed: &ParsedUrl,
1006 headers: &[(String, String)],
1007 body: &Option<Vec<u8>>,
1008 timeout_ms: u64,
1009 ) -> Result<Response, HttpClientError> {
1010 use std::time::Duration;
1011 use tokio::net::TcpStream;
1012 use tokio::time::timeout;
1013
1014 let addr = format!("{}:{}", parsed.host, parsed.port);
1015 let dur = Duration::from_millis(timeout_ms);
1016 let request_bytes =
1017 build_request_bytes(method, &parsed.path_and_query, &parsed.host, headers, body);
1018 let is_head = method.eq_ignore_ascii_case("HEAD");
1019
1020 let tcp = timeout(dur, TcpStream::connect(&addr))
1021 .await
1022 .map_err(|_| HttpClientError(format!("connect to '{addr}' timed out")))?
1023 .map_err(|e| HttpClientError(format!("connect to '{addr}' failed: {e}")))?;
1024
1025 if parsed.scheme == "https" {
1026 let tls_stream = timeout(dur, async_tls_connect(&parsed.host, tcp))
1027 .await
1028 .map_err(|_| HttpClientError("TLS handshake timed out".into()))??;
1029 let mut stream = tls_stream;
1030 timeout(dur, stream.write_all(&request_bytes))
1031 .await
1032 .map_err(|_| HttpClientError("write timed out".into()))?
1033 .map_err(|e| HttpClientError(format!("write error: {e}")))?;
1034 return timeout(dur, async_read_response(&mut stream, is_head))
1035 .await
1036 .map_err(|_| HttpClientError("read timed out".into()))?;
1037 }
1038
1039 let mut stream = tcp;
1040 timeout(dur, stream.write_all(&request_bytes))
1041 .await
1042 .map_err(|_| HttpClientError("write timed out".into()))?
1043 .map_err(|e| HttpClientError(format!("write error: {e}")))?;
1044 timeout(dur, async_read_response(&mut stream, is_head))
1045 .await
1046 .map_err(|_| HttpClientError("read timed out".into()))?
1047 }
1048
1049 pub struct AsyncClient {
1051 timeout_ms: u64,
1052 max_redirects: u8,
1053 }
1054
1055 impl AsyncClient {
1056 pub fn new() -> Self {
1058 Self {
1059 timeout_ms: 30_000,
1060 max_redirects: 10,
1061 }
1062 }
1063
1064 pub fn timeout_ms(mut self, ms: u64) -> Self {
1066 self.timeout_ms = ms;
1067 self
1068 }
1069
1070 pub fn max_redirects(mut self, n: u8) -> Self {
1072 self.max_redirects = n;
1073 self
1074 }
1075
1076 pub fn get(&self, url: &str) -> AsyncRequestBuilder<'_> {
1078 self.request("GET", url)
1079 }
1080
1081 pub fn post(&self, url: &str) -> AsyncRequestBuilder<'_> {
1083 self.request("POST", url)
1084 }
1085
1086 pub fn put(&self, url: &str) -> AsyncRequestBuilder<'_> {
1088 self.request("PUT", url)
1089 }
1090
1091 pub fn patch(&self, url: &str) -> AsyncRequestBuilder<'_> {
1093 self.request("PATCH", url)
1094 }
1095
1096 pub fn delete(&self, url: &str) -> AsyncRequestBuilder<'_> {
1098 self.request("DELETE", url)
1099 }
1100
1101 pub fn request(&self, method: &str, url: &str) -> AsyncRequestBuilder<'_> {
1103 AsyncRequestBuilder {
1104 client: self,
1105 method: method.to_uppercase(),
1106 url: url.to_string(),
1107 headers: Vec::new(),
1108 body: None,
1109 timeout_ms: None,
1110 }
1111 }
1112 }
1113
1114 impl Default for AsyncClient {
1115 fn default() -> Self {
1116 Self::new()
1117 }
1118 }
1119
1120 pub struct AsyncRequestBuilder<'a> {
1122 client: &'a AsyncClient,
1123 method: String,
1124 url: String,
1125 headers: Vec<(String, String)>,
1126 body: Option<Vec<u8>>,
1127 timeout_ms: Option<u64>,
1128 }
1129
1130 impl<'a> AsyncRequestBuilder<'a> {
1131 pub fn header(mut self, name: &str, value: &str) -> Self {
1133 self.headers.push((name.to_string(), value.to_string()));
1134 self
1135 }
1136
1137 pub fn body(mut self, bytes: Vec<u8>) -> Self {
1139 self.body = Some(bytes);
1140 self
1141 }
1142
1143 pub fn body_text(mut self, s: &str) -> Self {
1145 self.headers
1146 .push(("Content-Type".to_string(), "text/plain".to_string()));
1147 self.body = Some(s.as_bytes().to_vec());
1148 self
1149 }
1150
1151 pub fn body_json(mut self, s: &str) -> Self {
1153 self.headers.push((
1154 "Content-Type".to_string(),
1155 "application/json".to_string(),
1156 ));
1157 self.body = Some(s.as_bytes().to_vec());
1158 self
1159 }
1160
1161 pub fn form(mut self, pairs: &[(&str, &str)]) -> Self {
1164 self.headers.push((
1165 "Content-Type".to_string(),
1166 "application/x-www-form-urlencoded".to_string(),
1167 ));
1168 self.body = Some(super::form_urlencode(pairs).into_bytes());
1169 self
1170 }
1171
1172 pub fn timeout_ms(mut self, ms: u64) -> Self {
1174 self.timeout_ms = Some(ms);
1175 self
1176 }
1177
1178 pub async fn send(self) -> Result<Response, HttpClientError> {
1180 let timeout = self.timeout_ms.unwrap_or(self.client.timeout_ms);
1181 let max_redirects = self.client.max_redirects;
1182
1183 let mut method = self.method;
1184 let mut url = self.url;
1185 let headers = self.headers;
1186 let mut body = self.body;
1187 let mut redirects = 0u8;
1188
1189 loop {
1190 let parsed = ParsedUrl::parse(&url)?;
1191 let resp = async_send_once(&method, &parsed, &headers, &body, timeout).await?;
1192
1193 if resp.is_redirect() && redirects < max_redirects {
1194 let location = resp
1195 .header("location")
1196 .ok_or_else(|| {
1197 HttpClientError("redirect with no Location header".into())
1198 })?
1199 .to_string();
1200 url = resolve_url(&url, &location);
1201 redirects += 1;
1202 if matches!(resp.status(), 301 | 302 | 303) {
1203 method = "GET".to_string();
1204 body = None;
1205 }
1206 continue;
1207 }
1208
1209 return Ok(resp);
1210 }
1211 }
1212 }
1213}