1pub mod breaker;
18pub mod fake;
19pub mod stream;
20pub mod url;
21
22use rustlavel_core::events::Event;
23use rustlavel_core::{Error, Json, Result};
24use rustlavel_http::{Headers, Method, Status};
25use std::sync::Arc;
26use std::time::{Duration, Instant};
27use tokio::io::{AsyncReadExt, AsyncWriteExt};
28use tokio::net::TcpStream;
29use url::Url;
30
31pub use fake::{Fake, FakeResponse};
32pub use breaker::{CircuitBreaker, Permit, State as CircuitState};
33pub use stream::{Body, ServerSentEvent, SseReader};
34
35#[derive(Debug, Clone)]
37pub struct ClientResponse {
38 pub status: Status,
39 pub headers: Headers,
40 pub body: Vec<u8>,
41}
42
43impl ClientResponse {
44 pub fn text(&self) -> String {
45 String::from_utf8_lossy(&self.body).into_owned()
46 }
47
48 pub fn json(&self) -> Result<Json> {
49 Json::parse(&self.text())
50 }
51
52 pub fn is_success(&self) -> bool {
53 self.status.is_success()
54 }
55
56 pub fn error_for_status(self) -> Result<ClientResponse> {
59 if self.is_success() {
60 return Ok(self);
61 }
62 let body = self.text();
63 let excerpt = if body.len() > 500 { format!("{}…", &body[..500]) } else { body };
64 Err(Error::msg(format!("HTTP {}: {excerpt}", self.status)))
65 }
66}
67
68#[derive(Clone)]
70pub struct Client {
71 timeout: Duration,
72 retries: u32,
74 default_headers: Headers,
75 max_body_bytes: usize,
76 breaker: Option<crate::breaker::CircuitBreaker>,
77 fake: Option<Arc<Fake>>,
78}
79
80impl Default for Client {
81 fn default() -> Self {
82 let mut default_headers = Headers::new();
83 default_headers.set("user-agent", concat!("rustlavel/", env!("CARGO_PKG_VERSION")));
84 default_headers.set("accept", "*/*");
85 default_headers.set("accept-encoding", "gzip, deflate");
89
90 Client {
91 timeout: Duration::from_secs(30),
92 retries: 0,
93 default_headers,
94 max_body_bytes: 32 * 1024 * 1024,
95 breaker: None,
96 fake: None,
97 }
98 }
99}
100
101impl Client {
102 pub fn new() -> Self {
103 Client::default()
104 }
105
106 pub fn timeout(mut self, timeout: Duration) -> Self {
107 self.timeout = timeout;
108 self
109 }
110
111 pub fn retries(mut self, retries: u32) -> Self {
116 self.retries = retries;
117 self
118 }
119
120 pub fn breaker(mut self, breaker: crate::breaker::CircuitBreaker) -> Self {
125 self.breaker = Some(breaker);
126 self
127 }
128
129 pub fn circuit(&self) -> Option<&crate::breaker::CircuitBreaker> {
131 self.breaker.as_ref()
132 }
133
134 pub fn default_header(mut self, name: &str, value: impl Into<String>) -> Self {
135 self.default_headers.set(name, value);
136 self
137 }
138
139 pub fn faking(mut self, fake: Fake) -> Self {
144 self.fake = Some(Arc::new(fake));
145 self
146 }
147
148 pub fn fake(&self) -> Option<&Arc<Fake>> {
149 self.fake.as_ref()
150 }
151
152 pub fn request(&self, method: Method, url: impl Into<String>) -> RequestBuilder {
153 RequestBuilder {
154 client: self.clone(),
155 method,
156 url: url.into(),
157 headers: self.default_headers.clone(),
158 body: Vec::new(),
159 }
160 }
161
162 pub fn get(&self, url: impl Into<String>) -> RequestBuilder {
163 self.request(Method::Get, url)
164 }
165
166 pub fn post(&self, url: impl Into<String>) -> RequestBuilder {
167 self.request(Method::Post, url)
168 }
169
170 pub fn put(&self, url: impl Into<String>) -> RequestBuilder {
171 self.request(Method::Put, url)
172 }
173
174 pub fn patch(&self, url: impl Into<String>) -> RequestBuilder {
175 self.request(Method::Patch, url)
176 }
177
178 pub fn delete(&self, url: impl Into<String>) -> RequestBuilder {
179 self.request(Method::Delete, url)
180 }
181}
182
183pub struct RequestBuilder {
185 client: Client,
186 method: Method,
187 url: String,
188 headers: Headers,
189 body: Vec<u8>,
190}
191
192impl RequestBuilder {
193 pub fn header(mut self, name: &str, value: impl Into<String>) -> Self {
194 self.headers.set(name, value);
195 self
196 }
197
198 pub fn bearer(self, token: &str) -> Self {
199 self.header("authorization", format!("Bearer {token}"))
200 }
201
202 pub fn body(mut self, body: impl Into<Vec<u8>>) -> Self {
203 self.body = body.into();
204 self
205 }
206
207 pub fn json(self, value: Json) -> Self {
208 self.header("content-type", "application/json").body(value.to_string())
209 }
210
211 pub fn accept_events(self) -> Self {
213 self.header("accept", "text/event-stream")
214 }
215
216 pub fn method(&self) -> Method {
217 self.method
218 }
219
220 pub fn url(&self) -> &str {
221 &self.url
222 }
223
224 pub fn headers(&self) -> &Headers {
225 &self.headers
226 }
227
228 pub fn body_bytes(&self) -> &[u8] {
229 &self.body
230 }
231
232 pub async fn send(self) -> Result<ClientResponse> {
234 let started = Instant::now();
235 let method = self.method;
236 let url = self.url.clone();
237
238 if let Some(fake) = self.client.fake.clone() {
241 let response = fake.respond(&self)?;
242 record(method, &url, Some(response.status), started);
243 return Ok(response);
244 }
245
246 let permit = match (&self.client.breaker, Url::parse(&self.url)) {
252 (Some(breaker), Ok(parsed)) => Some(breaker.acquire(&parsed.authority())?),
253 _ => None,
254 };
255
256 let mut attempt = 0;
257 loop {
258 match self.send_once().await {
259 Ok(response) => {
260 if let Some(permit) = permit {
264 permit.record_status(response.status);
265 }
266 record(method, &url, Some(response.status), started);
267 return Ok(response);
268 }
269 Err(error) if attempt < self.client.retries && is_retryable(&error) => {
270 let backoff = Duration::from_millis(100 * 2u64.pow(attempt));
271 rustlavel_core::debug!("retrying {method} {url} after {error}");
272 tokio::time::sleep(backoff).await;
273 attempt += 1;
274 }
275 Err(error) => {
276 if let Some(permit) = permit {
279 permit.failure();
280 }
281 record(method, &url, None, started);
282 return Err(error);
283 }
284 }
285 }
286 }
287
288 pub async fn stream(self) -> Result<Body> {
290 if let Some(fake) = self.client.fake.clone() {
291 let response = fake.respond(&self)?;
292 return Ok(Body::from_bytes(response.status, response.headers, response.body));
293 }
294
295 let url = Url::parse(&self.url)?;
296 let stream = connect(&url, self.client.timeout).await?;
297 let request = self.wire(&url);
298
299 stream::open(stream, request, self.client.timeout).await
300 }
301
302 async fn send_once(&self) -> Result<ClientResponse> {
303 let url = Url::parse(&self.url)?;
304 let mut stream = connect(&url, self.client.timeout).await?;
305 let request = self.wire(&url);
306
307 let exchange = async {
308 stream.write_all(&request).await.map_err(Error::Io)?;
309 stream.flush().await.map_err(Error::Io)?;
310 let response = read_response(&mut stream, self.method, self.client.max_body_bytes).await?;
311 decode_body(response, self.client.max_body_bytes)
312 };
313
314 tokio::time::timeout(self.client.timeout, exchange)
315 .await
316 .map_err(|_| Error::msg(format!("{} {} timed out", self.method, self.url)))?
317 }
318
319 fn wire(&self, url: &Url) -> Vec<u8> {
321 let mut head = format!("{} {} HTTP/1.1\r\n", self.method, url.target);
322 head.push_str(&format!("host: {}\r\n", url.authority()));
323
324 for (name, value) in self.headers.iter() {
325 if name == "host" || name == "content-length" || name == "connection" {
326 continue;
327 }
328 head.push_str(&format!("{name}: {value}\r\n"));
329 }
330
331 head.push_str("connection: close\r\n");
334 if !self.body.is_empty() || self.method.takes_body() {
335 head.push_str(&format!("content-length: {}\r\n", self.body.len()));
336 }
337 head.push_str("\r\n");
338
339 let mut out = head.into_bytes();
340 out.extend_from_slice(&self.body);
341 out
342 }
343}
344
345fn record(method: Method, url: &str, status: Option<Status>, started: Instant) {
346 if !rustlavel_core::events::has_subscribers() {
347 return;
348 }
349 let mut event = Event::new("http.client")
350 .with("method", method.as_str())
351 .with("url", url)
352 .took(started.elapsed());
353 if let Some(status) = status {
354 event = event.with("status", status.code());
355 }
356 event.dispatch();
357}
358
359fn is_retryable(error: &Error) -> bool {
361 let text = error.to_string();
362 text.contains("timed out")
363 || text.contains("Connection refused")
364 || text.contains("connection reset")
365 || text.contains("Temporary failure")
366}
367
368pub enum Connection {
373 Plain(TcpStream),
374 Tls(Box<tokio_rustls::client::TlsStream<TcpStream>>),
375}
376
377impl Connection {
378 pub async fn write_all(&mut self, bytes: &[u8]) -> std::io::Result<()> {
379 match self {
380 Connection::Plain(stream) => stream.write_all(bytes).await,
381 Connection::Tls(stream) => stream.write_all(bytes).await,
382 }
383 }
384
385 pub async fn flush(&mut self) -> std::io::Result<()> {
386 match self {
387 Connection::Plain(stream) => stream.flush().await,
388 Connection::Tls(stream) => stream.flush().await,
389 }
390 }
391
392 pub async fn read(&mut self, buffer: &mut [u8]) -> std::io::Result<usize> {
393 match self {
394 Connection::Plain(stream) => stream.read(buffer).await,
395 Connection::Tls(stream) => stream.read(buffer).await,
396 }
397 }
398}
399
400pub async fn connect(url: &Url, timeout: Duration) -> Result<Connection> {
402 let address = url.socket_address();
403
404 let tcp = tokio::time::timeout(timeout, TcpStream::connect(&address))
405 .await
406 .map_err(|_| Error::msg(format!("connecting to {address} timed out")))?
407 .map_err(|e| Error::msg(format!("cannot connect to {address}: {e}")))?;
408
409 let _ = tcp.set_nodelay(true);
410
411 if !url.secure {
412 return Ok(Connection::Plain(tcp));
413 }
414
415 let connector = tls_connector();
416 let server_name = rustls::pki_types::ServerName::try_from(url.host.clone())
417 .map_err(|_| Error::msg(format!("`{}` is not a valid TLS server name", url.host)))?;
418
419 let tls = connector
420 .connect(server_name, tcp)
421 .await
422 .map_err(|e| Error::msg(format!("TLS handshake with {} failed: {e}", url.host)))?;
423
424 Ok(Connection::Tls(Box::new(tls)))
425}
426
427fn tls_connector() -> tokio_rustls::TlsConnector {
439 use std::sync::OnceLock;
440 static CONNECTOR: OnceLock<tokio_rustls::TlsConnector> = OnceLock::new();
441
442 CONNECTOR
443 .get_or_init(|| {
444 let roots = rustls::RootCertStore {
445 roots: webpki_roots::TLS_SERVER_ROOTS.to_vec(),
446 };
447 let config = rustls::ClientConfig::builder()
448 .with_root_certificates(roots)
449 .with_no_client_auth();
450 tokio_rustls::TlsConnector::from(Arc::new(config))
451 })
452 .clone()
453}
454
455fn body_is_possible(method: Method, status: Status) -> bool {
468 method != Method::Head
471 && status != Status::NO_CONTENT
472 && status != Status::NOT_MODIFIED
473 && status.code() >= 200
474}
475
476async fn read_response(
477 connection: &mut Connection,
478 method: Method,
479 max_body: usize,
480) -> Result<ClientResponse> {
481 let mut buffer = Vec::with_capacity(8 * 1024);
482
483 let head_end = loop {
484 if let Some(at) = find_head_end(&buffer) {
485 break at;
486 }
487 if !fill(connection, &mut buffer).await? {
488 return Err(Error::Protocol("the server closed before sending headers".into()));
489 }
490 if buffer.len() > 256 * 1024 {
491 return Err(Error::Protocol("response headers are too large".into()));
492 }
493 };
494
495 let (status, headers) = parse_head(&buffer[..head_end])?;
496 let mut body = buffer.split_off(head_end);
497
498 if !body_is_possible(method, status) {
499 return Ok(ClientResponse { status, headers, body: Vec::new() });
502 }
503
504 if headers.get("transfer-encoding").is_some_and(|te| te.contains("chunked")) {
505 body = read_chunked(connection, body, max_body).await?;
506 } else if let Some(length) = headers.content_length() {
507 if length > max_body {
508 return Err(Error::Protocol("response body is too large".into()));
509 }
510 while body.len() < length {
511 if !fill_into(connection, &mut body).await? {
512 return Err(Error::Protocol("response body ended early".into()));
513 }
514 }
515 body.truncate(length);
516 } else {
517 while fill_into(connection, &mut body).await? {
520 if body.len() > max_body {
521 return Err(Error::Protocol("response body is too large".into()));
522 }
523 }
524 }
525
526 Ok(ClientResponse { status, headers, body })
527}
528
529fn decode_body(mut response: ClientResponse, max_body: usize) -> Result<ClientResponse> {
536 use rustlavel_http::compression::gzip;
537
538 let encoding = response.headers.get("content-encoding").map(|e| e.trim().to_ascii_lowercase());
539 let decoded = match encoding.as_deref() {
540 Some("gzip" | "x-gzip") => gzip::decompress_with_limit(&response.body, max_body),
541 Some("deflate") => gzip::zlib_decompress_with_limit(&response.body, max_body),
542 _ => return Ok(response),
543 };
544
545 response.body = decoded.map_err(|e| {
546 Error::Protocol(format!("the response body could not be decompressed: {e}"))
547 })?;
548 response.headers.remove("content-encoding");
549 response.headers.remove("content-length");
550 Ok(response)
551}
552
553pub(crate) fn parse_head(head: &[u8]) -> Result<(Status, Headers)> {
554 let text = std::str::from_utf8(head)
555 .map_err(|_| Error::Protocol("response headers are not UTF-8".into()))?;
556 let mut lines = text.split("\r\n");
557
558 let status_line = lines.next().ok_or_else(|| Error::Protocol("empty response".into()))?;
559 let code = status_line
560 .split(' ')
561 .nth(1)
562 .and_then(|code| code.parse::<u16>().ok())
563 .ok_or_else(|| Error::Protocol(format!("malformed status line: {status_line}")))?;
564
565 let mut headers = Headers::new();
566 for line in lines {
567 if line.is_empty() {
568 continue;
569 }
570 if let Some((name, value)) = line.split_once(':') {
571 headers.append(name.trim(), value.trim());
572 }
573 }
574
575 Ok((Status(code), headers))
576}
577
578async fn read_chunked(
579 connection: &mut Connection,
580 mut buffer: Vec<u8>,
581 max_body: usize,
582) -> Result<Vec<u8>> {
583 let mut body = Vec::new();
584
585 loop {
586 let line_end = loop {
587 if let Some(at) = find_crlf(&buffer) {
588 break at;
589 }
590 if !fill_into(connection, &mut buffer).await? {
591 return Err(Error::Protocol("chunked body ended early".into()));
592 }
593 };
594
595 let header: Vec<u8> = buffer.drain(..line_end + 2).collect();
596 let size_text = String::from_utf8_lossy(&header[..line_end]);
597 let size = usize::from_str_radix(size_text.split(';').next().unwrap_or("").trim(), 16)
598 .map_err(|_| Error::Protocol("invalid chunk size".into()))?;
599
600 if size == 0 {
601 return Ok(body);
602 }
603 if body.len() + size > max_body {
604 return Err(Error::Protocol("response body is too large".into()));
605 }
606
607 while buffer.len() < size + 2 {
608 if !fill_into(connection, &mut buffer).await? {
609 return Err(Error::Protocol("chunked body ended early".into()));
610 }
611 }
612 body.extend(buffer.drain(..size));
613 buffer.drain(..2);
614 }
615}
616
617async fn fill(connection: &mut Connection, buffer: &mut Vec<u8>) -> Result<bool> {
618 fill_into(connection, buffer).await
619}
620
621async fn fill_into(connection: &mut Connection, buffer: &mut Vec<u8>) -> Result<bool> {
622 let mut chunk = [0u8; 8192];
623 let read = connection.read(&mut chunk).await.map_err(Error::Io)?;
624 buffer.extend_from_slice(&chunk[..read]);
625 Ok(read > 0)
626}
627
628pub(crate) fn find_head_end(buffer: &[u8]) -> Option<usize> {
629 buffer.windows(4).position(|w| w == b"\r\n\r\n").map(|at| at + 4)
630}
631
632fn find_crlf(buffer: &[u8]) -> Option<usize> {
633 buffer.windows(2).position(|w| w == b"\r\n")
634}
635
636#[cfg(test)]
637mod tests {
638 #[test]
639 fn a_head_response_never_has_a_body_whatever_its_headers_claim() {
640 use super::body_is_possible;
641 use rustlavel_http::{Method, Status};
642
643 assert!(!body_is_possible(Method::Head, Status::OK));
648 assert!(!body_is_possible(Method::Head, Status::NOT_FOUND));
649
650 assert!(body_is_possible(Method::Get, Status::OK));
651 assert!(body_is_possible(Method::Post, Status::CREATED));
652 }
653
654 #[test]
655 fn the_two_statuses_that_forbid_a_body_are_honoured() {
656 use super::body_is_possible;
657 use rustlavel_http::{Method, Status};
658
659 assert!(!body_is_possible(Method::Get, Status::NO_CONTENT));
660 assert!(!body_is_possible(Method::Get, Status::NOT_MODIFIED));
661 }
664
665 use super::*;
666
667 #[test]
668 fn builds_a_request_line_and_headers() {
669 let client = Client::new();
670 let builder = client
671 .post("https://example.com/v1/things?x=1")
672 .bearer("secret")
673 .json(Json::object([("name", "widget".into())]));
674
675 let wire = String::from_utf8(builder.wire(&Url::parse(builder.url()).unwrap())).unwrap();
676
677 assert!(wire.starts_with("POST /v1/things?x=1 HTTP/1.1\r\n"));
678 assert!(wire.contains("host: example.com\r\n"));
679 assert!(wire.contains("authorization: Bearer secret\r\n"));
680 assert!(wire.contains("content-type: application/json\r\n"));
681 assert!(wire.contains("content-length: 17\r\n"));
682 assert!(wire.ends_with("\r\n\r\n{\"name\":\"widget\"}"));
683 }
684
685 #[test]
686 fn parses_a_response_head() {
687 let head = b"HTTP/1.1 201 Created\r\nContent-Type: application/json\r\nContent-Length: 2\r\n\r\n";
688 let (status, headers) = parse_head(head).unwrap();
689
690 assert_eq!(status, Status::CREATED);
691 assert_eq!(headers.content_type(), Some("application/json"));
692 assert_eq!(headers.content_length(), Some(2));
693 }
694
695 #[test]
696 fn a_failed_status_becomes_an_error_carrying_the_body() {
697 let response = ClientResponse {
698 status: Status(429),
699 headers: Headers::new(),
700 body: b"{\"error\":\"rate limited\"}".to_vec(),
701 };
702
703 let error = response.error_for_status().unwrap_err().to_string();
704 assert!(error.contains("429"));
705 assert!(error.contains("rate limited"));
706 }
707
708 #[test]
709 fn only_transport_failures_are_retried() {
710 assert!(is_retryable(&Error::msg("connecting to x timed out")));
711 assert!(is_retryable(&Error::msg("cannot connect to x: Connection refused (os error 61)")));
712 assert!(!is_retryable(&Error::msg("HTTP 500 Internal Server Error: boom")));
713 }
714
715 #[tokio::test]
716 async fn talks_to_a_real_server_over_plain_http() {
717 use rustlavel_http::{Request, Response, Router, Server};
720 use rustlavel_core::Context;
721
722 let mut router = Router::new();
723 router.post("/echo", |mut req: Request| async move {
724 Response::json(Json::object([
725 ("saw", Json::from(req.input("name").unwrap_or_default())),
726 ("agent", Json::from(req.header("user-agent").unwrap_or("").to_string())),
727 ]))
728 });
729
730 let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
731 let address = listener.local_addr().unwrap();
732 drop(listener);
733
734 let server = Server::new(router, Context::default());
735 tokio::spawn(async move {
736 let _ = server.listen(address.to_string()).await;
737 });
738 tokio::time::sleep(Duration::from_millis(150)).await;
740
741 let response = Client::new()
742 .post(format!("http://{address}/echo"))
743 .json(Json::object([("name", "ada".into())]))
744 .send()
745 .await
746 .unwrap()
747 .error_for_status()
748 .unwrap();
749
750 let body = response.json().unwrap();
751 assert_eq!(body.get("saw").unwrap().as_str(), Some("ada"));
752 assert!(body.get("agent").unwrap().as_str().unwrap().starts_with("rustlavel/"));
753 }
754
755 #[tokio::test]
756 async fn a_connection_failure_is_reported_clearly() {
757 let error = Client::new()
758 .timeout(Duration::from_millis(500))
759 .get("http://127.0.0.1:1/nope")
760 .send()
761 .await
762 .unwrap_err()
763 .to_string();
764
765 assert!(error.contains("127.0.0.1:1"), "{error}");
766 }
767
768 #[test]
783 fn the_key_exchange_leads_with_a_post_quantum_hybrid() {
784 let config = rustls::ClientConfig::builder()
788 .with_root_certificates(rustls::RootCertStore::empty())
789 .with_no_client_auth();
790
791 let offered: Vec<String> = config
792 .crypto_provider()
793 .kx_groups
794 .iter()
795 .map(|group| format!("{:?}", group.name()))
796 .collect();
797
798 assert_eq!(
799 offered.first().map(String::as_str),
800 Some("X25519MLKEM768"),
801 "the post-quantum hybrid must lead the ClientHello; offered: {offered:?}"
802 );
803 assert!(
804 offered.iter().any(|name| name == "X25519"),
805 "a classical group must remain, for servers that do not know the hybrid: {offered:?}"
806 );
807 }
808}
809
810#[cfg(test)]
811mod compression_tests {
812 use super::*;
813 use rustlavel_http::compression::gzip;
814 use tokio::net::TcpListener;
815
816 async fn one_shot(head: &'static str, body: Vec<u8>) -> String {
818 let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
819 let address = listener.local_addr().unwrap();
820 tokio::spawn(async move {
821 let (mut socket, _) = listener.accept().await.unwrap();
822 let mut request = vec![0u8; 8192];
823 let _ = socket.read(&mut request).await;
824 let mut wire = format!("HTTP/1.1 200 OK\r\ncontent-length: {}\r\n{head}\r\n", body.len()).into_bytes();
825 wire.extend_from_slice(&body);
826 socket.write_all(&wire).await.unwrap();
827 let _ = socket.shutdown().await;
828 });
829 format!("http://{address}/")
830 }
831
832 #[tokio::test]
833 async fn gzip_and_deflate_bodies_are_decoded_before_the_caller_sees_them() {
834 let text = "{\"users\":[".to_string() + &"{\"name\":\"same\"},".repeat(200) + "{}]}";
835
836 let url = one_shot("content-encoding: gzip\r\ncontent-type: application/json\r\n", gzip::compress(text.as_bytes())).await;
837 let response = Client::new().get(url).send().await.unwrap();
838 assert_eq!(response.text(), text);
839 assert_eq!(response.headers.get("content-encoding"), None, "the encoding is gone with the bytes it described");
840 assert_eq!(response.headers.get("content-type"), Some("application/json"));
841
842 let url = one_shot("content-encoding: deflate\r\n", gzip::zlib_compress(text.as_bytes())).await;
843 assert_eq!(Client::new().get(url).send().await.unwrap().text(), text);
844 }
845
846 #[tokio::test]
847 async fn an_unknown_encoding_is_left_as_it_came() {
848 let url = one_shot("content-encoding: br\r\n", b"not really brotli".to_vec()).await;
849 let response = Client::new().get(url).send().await.unwrap();
850 assert_eq!(response.headers.get("content-encoding"), Some("br"));
851 assert_eq!(response.body, b"not really brotli");
852 }
853
854 #[tokio::test]
855 async fn a_corrupt_gzip_body_is_an_error_not_garbage() {
856 let url = one_shot("content-encoding: gzip\r\n", b"\x1f\x8b\x08definitely not deflate".to_vec()).await;
857 let error = Client::new().get(url).send().await.expect_err("an error").to_string();
858 assert!(error.contains("decompressed"), "{error}");
859 }
860
861 #[tokio::test]
862 async fn every_request_asks_for_compression_by_default() {
863 let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
864 let address = listener.local_addr().unwrap();
865 let seen = tokio::spawn(async move {
866 let (mut socket, _) = listener.accept().await.unwrap();
867 let mut request = vec![0u8; 8192];
868 let n = socket.read(&mut request).await.unwrap();
869 socket.write_all(b"HTTP/1.1 204 No Content\r\n\r\n").await.unwrap();
870 String::from_utf8_lossy(&request[..n]).to_ascii_lowercase()
871 });
872 Client::new().get(format!("http://{address}/")).send().await.unwrap();
873 assert!(seen.await.unwrap().contains("accept-encoding: gzip, deflate"));
874 }
875}
876
877#[cfg(test)]
878mod breaker_integration_tests {
879 use super::*;
880 use crate::breaker::{CircuitBreaker, State};
881 use tokio::net::TcpListener;
882
883 async fn counting_server(status: &'static str) -> (String, Arc<std::sync::atomic::AtomicUsize>) {
886 let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
887 let address = listener.local_addr().unwrap();
888 let served = Arc::new(std::sync::atomic::AtomicUsize::new(0));
889 let counter = served.clone();
890
891 tokio::spawn(async move {
892 loop {
893 let Ok((mut socket, _)) = listener.accept().await else { return };
894 counter.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
895 tokio::spawn(async move {
896 let mut request = vec![0u8; 4096];
897 let _ = socket.read(&mut request).await;
898 let _ = socket
899 .write_all(format!("HTTP/1.1 {status}\r\ncontent-length: 0\r\n\r\n").as_bytes())
900 .await;
901 let _ = socket.shutdown().await;
902 });
903 }
904 });
905 (format!("http://{address}/"), served)
906 }
907
908 #[tokio::test]
909 async fn a_failing_upstream_stops_being_called_at_all() {
910 let (url, served) = counting_server("500 Internal Server Error").await;
911 let breaker = CircuitBreaker::new().minimum_calls(4).failure_rate(0.5);
912 let http = Client::new().breaker(breaker.clone());
913
914 for _ in 0..4 {
916 let response = http.get(&url).send().await.expect("the exchange succeeded");
917 assert_eq!(response.status.code(), 500);
918 }
919 assert_eq!(served.load(std::sync::atomic::Ordering::SeqCst), 4);
920
921 for _ in 0..20 {
923 let error = http.get(&url).send().await.expect_err("refused by the breaker");
924 assert!(matches!(error, Error::Unavailable(_)), "{error:?}");
925 }
926 assert_eq!(
927 served.load(std::sync::atomic::Ordering::SeqCst),
928 4,
929 "the server was not touched again"
930 );
931 }
932
933 #[tokio::test]
934 async fn a_healthy_upstream_is_never_interrupted() {
935 let (url, served) = counting_server("200 OK").await;
936 let http = Client::new().breaker(CircuitBreaker::new().minimum_calls(4));
937
938 for _ in 0..30 {
939 http.get(&url).send().await.expect("fine").status.code();
940 }
941 assert_eq!(served.load(std::sync::atomic::Ordering::SeqCst), 30);
942 }
943
944 #[tokio::test]
945 async fn a_4xx_never_opens_the_circuit() {
946 let (url, _) = counting_server("404 Not Found").await;
947 let http = Client::new().breaker(CircuitBreaker::new().minimum_calls(4));
948
949 for _ in 0..30 {
950 assert_eq!(http.get(&url).send().await.unwrap().status.code(), 404);
951 }
952 let host = Url::parse(&url).unwrap().authority();
953 assert_eq!(http.circuit().unwrap().state(&host), State::Closed);
954 }
955
956 #[tokio::test]
957 async fn an_unreachable_host_opens_the_circuit_and_retries_do_not_multiply_the_verdict() {
958 let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
960 let address = listener.local_addr().unwrap();
961 drop(listener);
962 let url = format!("http://{address}/");
963
964 let breaker = CircuitBreaker::new().minimum_calls(3).failure_rate(0.5);
965 let http = Client::new().retries(2).breaker(breaker.clone());
966
967 for _ in 0..3 {
970 http.get(&url).send().await.expect_err("nothing is listening");
971 }
972 assert_eq!(breaker.state(&address.to_string()), State::Open);
973 }
974
975 #[tokio::test]
976 async fn one_host_failing_does_not_stop_calls_to_another() {
977 let (broken, _) = counting_server("503 Service Unavailable").await;
978 let (healthy, served) = counting_server("200 OK").await;
979 let breaker = CircuitBreaker::new().minimum_calls(4).failure_rate(0.5);
980 let http = Client::new().breaker(breaker);
981
982 for _ in 0..6 {
983 let _ = http.get(&broken).send().await;
984 }
985 http.get(&broken).send().await.expect_err("that one is out");
986
987 for _ in 0..5 {
988 http.get(&healthy).send().await.expect("this one is fine");
989 }
990 assert_eq!(served.load(std::sync::atomic::Ordering::SeqCst), 5);
991 }
992
993 #[tokio::test]
994 async fn it_recovers_once_the_upstream_does() {
995 let (url, _) = counting_server("500 Internal Server Error").await;
996 let breaker = CircuitBreaker::new()
997 .minimum_calls(4)
998 .failure_rate(0.5)
999 .reset_after(Duration::from_millis(60))
1000 .probes(1);
1001 let http = Client::new().breaker(breaker.clone());
1002 let host = Url::parse(&url).unwrap().authority();
1003
1004 for _ in 0..4 {
1005 let _ = http.get(&url).send().await;
1006 }
1007 assert_eq!(breaker.state(&host), State::Open);
1008
1009 tokio::time::sleep(Duration::from_millis(80)).await;
1011 let (healthy, _) = counting_server("200 OK").await;
1012 let healthy_host = Url::parse(&healthy).unwrap().authority();
1013 http.get(&healthy).send().await.expect("healthy");
1015 assert_eq!(breaker.state(&healthy_host), State::Closed);
1016 assert_eq!(breaker.state(&host), State::HalfOpen, "the broken one is still probing");
1017 }
1018
1019 #[tokio::test]
1020 async fn without_a_breaker_nothing_changes() {
1021 let (url, served) = counting_server("500 Internal Server Error").await;
1022 let http = Client::new();
1023 for _ in 0..25 {
1024 assert_eq!(http.get(&url).send().await.unwrap().status.code(), 500);
1025 }
1026 assert_eq!(served.load(std::sync::atomic::Ordering::SeqCst), 25, "every one was sent");
1027 }
1028}