1use std::cell::RefCell;
4use std::collections::BTreeMap;
5use std::sync::{Arc, RwLock};
6use std::time::Duration;
7
8use rainier_support::{Error, Result};
9use serde::Serialize;
10
11use crate::fake::FakeTransport;
12use crate::response::HttpResponse;
13use crate::retry::{is_retryable, Backoff};
14use crate::transport::{OutboundRequest, Transport};
15
16static TRANSPORT: RwLock<Option<Arc<dyn Transport>>> = RwLock::new(None);
18
19static FAKE: RwLock<Option<Arc<FakeTransport>>> = RwLock::new(None);
21
22thread_local! {
23 static SCOPED_FAKE: RefCell<Option<Arc<FakeTransport>>> = const { RefCell::new(None) };
35}
36
37pub struct Http;
43
44impl Http {
45 pub fn get(url: impl Into<String>) -> PendingRequest {
47 PendingRequest::new("GET", url)
48 }
49
50 pub fn post(url: impl Into<String>) -> PendingRequest {
52 PendingRequest::new("POST", url)
53 }
54
55 pub fn put(url: impl Into<String>) -> PendingRequest {
57 PendingRequest::new("PUT", url)
58 }
59
60 pub fn patch(url: impl Into<String>) -> PendingRequest {
62 PendingRequest::new("PATCH", url)
63 }
64
65 pub fn delete(url: impl Into<String>) -> PendingRequest {
67 PendingRequest::new("DELETE", url)
68 }
69
70 pub fn request(method: impl Into<String>, url: impl Into<String>) -> PendingRequest {
72 PendingRequest::new(method, url)
73 }
74
75 pub fn install(transport: Arc<dyn Transport>) {
80 *TRANSPORT.write().expect("transport lock poisoned") = Some(transport);
81 *FAKE.write().expect("fake lock poisoned") = None;
82 }
83
84 pub fn fake() -> Arc<FakeTransport> {
96 let fake = Arc::new(FakeTransport::new());
97
98 SCOPED_FAKE.with(|scoped| *scoped.borrow_mut() = Some(Arc::clone(&fake)));
102
103 fake
104 }
105
106 pub fn fake_globally() -> Arc<FakeTransport> {
114 let fake = Arc::new(FakeTransport::new());
115
116 *TRANSPORT.write().expect("transport lock poisoned") =
117 Some(Arc::clone(&fake) as Arc<dyn Transport>);
118 *FAKE.write().expect("fake lock poisoned") = Some(Arc::clone(&fake));
119
120 fake
121 }
122
123 pub fn faking() -> Arc<FakeTransport> {
132 Self::scoped_fake()
133 .or_else(|| FAKE.read().expect("fake lock poisoned").clone())
134 .expect("`Http` is not faking — call `Http::fake()` before asserting on outbound calls")
135 }
136
137 fn scoped_fake() -> Option<Arc<FakeTransport>> {
139 SCOPED_FAKE.with(|scoped| scoped.borrow().clone())
140 }
141
142 pub fn assert_sent(matches: impl Fn(&crate::RecordedRequest) -> bool) {
149 Self::faking().assert_sent(matches);
150 }
151
152 pub fn assert_not_sent(matches: impl Fn(&crate::RecordedRequest) -> bool) {
158 Self::faking().assert_not_sent(matches);
159 }
160
161 pub fn assert_nothing_sent() {
167 Self::faking().assert_nothing_sent();
168 }
169
170 pub fn clear() {
174 SCOPED_FAKE.with(|scoped| *scoped.borrow_mut() = None);
175 *TRANSPORT.write().expect("transport lock poisoned") = None;
176 *FAKE.write().expect("fake lock poisoned") = None;
177 }
178
179 fn transport() -> Result<Arc<dyn Transport>> {
181 if let Some(fake) = Self::scoped_fake() {
184 return Ok(fake as Arc<dyn Transport>);
185 }
186 if let Some(transport) = TRANSPORT.read().expect("transport lock poisoned").clone() {
187 return Ok(transport);
188 }
189
190 #[cfg(feature = "reqwest-transport")]
191 {
192 let transport: Arc<dyn Transport> = Arc::new(crate::ReqwestTransport::new());
195 *TRANSPORT.write().expect("transport lock poisoned") = Some(Arc::clone(&transport));
196 Ok(transport)
197 }
198
199 #[cfg(not(feature = "reqwest-transport"))]
200 Err(Error::internal(
201 "no HTTP transport is installed — enable the `reqwest-transport` feature, or call \
202 `Http::install(..)` with your own",
203 ))
204 }
205}
206
207pub struct PendingRequest {
209 method: String,
210 url: String,
211 headers: BTreeMap<String, String>,
212 body: Option<Vec<u8>>,
213 timeout: Option<Duration>,
214 attempts: u32,
215 backoff: Backoff,
216 transport: Option<Arc<dyn Transport>>,
217}
218
219impl PendingRequest {
220 fn new(method: impl Into<String>, url: impl Into<String>) -> Self {
221 Self {
222 method: method.into().to_ascii_uppercase(),
223 url: url.into(),
224 headers: BTreeMap::new(),
225 body: None,
226 timeout: Some(Duration::from_secs(30)),
227 attempts: 1,
228 backoff: Backoff::default(),
229 transport: None,
230 }
231 }
232
233 #[must_use = "this returns the request rather than sending it"]
235 pub fn header(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
236 self.headers.insert(name.into().to_ascii_lowercase(), value.into());
237 self
238 }
239
240 #[must_use = "this returns the request rather than sending it"]
242 pub fn bearer(self, token: impl std::fmt::Display) -> Self {
243 self.header("authorization", format!("Bearer {token}"))
244 }
245
246 #[must_use = "this returns the request rather than sending it"]
248 pub fn accept_json(self) -> Self {
249 self.header("accept", "application/json")
250 }
251
252 pub fn json<T: Serialize>(mut self, value: &T) -> Result<Self> {
259 self.body = Some(serde_json::to_vec(value)?);
260 Ok(self.header("content-type", "application/json"))
261 }
262
263 #[must_use = "this returns the request rather than sending it"]
265 pub fn body(mut self, body: impl Into<Vec<u8>>) -> Self {
266 self.body = Some(body.into());
267 self
268 }
269
270 #[must_use = "this returns the request rather than sending it"]
272 pub fn form(mut self, fields: &[(&str, &str)]) -> Self {
273 let encoded = fields
274 .iter()
275 .map(|(name, value)| format!("{}={}", encode(name), encode(value)))
276 .collect::<Vec<_>>()
277 .join("&");
278
279 self.body = Some(encoded.into_bytes());
280 self.header("content-type", "application/x-www-form-urlencoded")
281 }
282
283 #[must_use = "this returns the request rather than sending it"]
289 pub fn timeout(mut self, timeout: Duration) -> Self {
290 self.timeout = Some(timeout);
291 self
292 }
293
294 #[must_use = "this returns the request rather than sending it"]
298 pub fn without_timeout(mut self) -> Self {
299 self.timeout = None;
300 self
301 }
302
303 #[must_use = "this returns the request rather than sending it"]
309 pub fn retry(mut self, attempts: u32, backoff: Backoff) -> Self {
310 self.attempts = attempts.max(1);
311 self.backoff = backoff;
312 self
313 }
314
315 #[must_use = "this returns the request rather than sending it"]
320 pub fn through(mut self, transport: Arc<dyn Transport>) -> Self {
321 self.transport = Some(transport);
322 self
323 }
324
325 pub async fn send(self) -> Result<HttpResponse> {
331 let transport = match &self.transport {
332 Some(transport) => Arc::clone(transport),
333 None => Http::transport()?,
334 };
335
336 let request = OutboundRequest {
337 method: self.method.clone(),
338 url: self.url.clone(),
339 headers: self.headers.clone(),
340 body: self.body.clone(),
341 timeout: self.timeout,
342 };
343
344 let mut last: Option<Error> = None;
345
346 for attempt in 1..=self.attempts {
347 let wait = self.backoff.wait_before(attempt);
348 if !wait.is_zero() {
349 tokio::time::sleep(wait).await;
350 }
351
352 match transport.send(request.clone()).await {
353 Ok(raw) => {
354 let response = HttpResponse::new(raw);
355
356 if attempt < self.attempts && is_retryable(Some(response.status())) {
357 tracing::debug!(
358 url = %self.url,
359 status = response.status(),
360 attempt,
361 "retrying an outbound request"
362 );
363 continue;
364 }
365
366 return Ok(response);
370 }
371 Err(e) => {
372 if attempt < self.attempts && is_retryable(None) {
373 tracing::debug!(url = %self.url, attempt, error = %e.message(), "retrying");
374 last = Some(e);
375 continue;
376 }
377 return Err(e);
378 }
379 }
380 }
381
382 Err(last.unwrap_or_else(|| Error::internal("the request was never attempted")))
383 }
384}
385
386impl std::fmt::Debug for PendingRequest {
387 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
388 f.debug_struct("PendingRequest")
389 .field("method", &self.method)
390 .field("url", &self.url)
391 .field("attempts", &self.attempts)
392 .finish()
393 }
394}
395
396fn encode(value: &str) -> String {
398 let mut out = String::with_capacity(value.len());
399
400 for byte in value.bytes() {
401 match byte {
402 b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'.' | b'_' | b'~' => {
403 out.push(byte as char);
404 }
405 _ => out.push_str(&format!("%{byte:02X}")),
406 }
407 }
408 out
409}
410
411#[cfg(test)]
412mod tests {
413 use super::*;
414 use serde_json::json;
415
416 fn fake() -> Arc<FakeTransport> {
418 Arc::new(FakeTransport::new())
419 }
420
421 #[tokio::test]
422 async fn a_json_post_carries_its_body_and_content_type() {
423 let fake = fake();
424
425 Http::post("https://hooks.example.com/x")
426 .json(&json!({ "id": 42 }))
427 .unwrap()
428 .through(Arc::clone(&fake) as Arc<dyn Transport>)
429 .send()
430 .await
431 .unwrap();
432
433 let sent = &fake.recorded()[0];
434 assert_eq!(sent.method(), "POST");
435 assert_eq!(sent.header("content-type"), Some("application/json"));
436 assert_eq!(sent.json().unwrap()["id"], 42);
437 }
438
439 #[tokio::test]
440 async fn a_bearer_token_becomes_a_header() {
441 let fake = fake();
442
443 Http::get("https://api.example.com/me")
444 .bearer("secret-token")
445 .accept_json()
446 .through(Arc::clone(&fake) as Arc<dyn Transport>)
447 .send()
448 .await
449 .unwrap();
450
451 let sent = &fake.recorded()[0];
452 assert_eq!(sent.header("authorization"), Some("Bearer secret-token"));
453 assert_eq!(sent.header("accept"), Some("application/json"));
454 }
455
456 #[tokio::test]
457 async fn a_form_body_is_encoded() {
458 let fake = fake();
459
460 Http::post("https://oauth.example.com/token")
461 .form(&[("grant_type", "client_credentials"), ("scope", "read write")])
462 .through(Arc::clone(&fake) as Arc<dyn Transport>)
463 .send()
464 .await
465 .unwrap();
466
467 let sent = &fake.recorded()[0];
468 assert_eq!(sent.header("content-type"), Some("application/x-www-form-urlencoded"));
469 assert_eq!(sent.body(), "grant_type=client_credentials&scope=read%20write");
470 }
471
472 #[tokio::test]
473 async fn a_five_hundred_comes_back_as_ok_and_the_caller_decides() {
474 let fake = fake();
475 fake.responding(500, "not my day");
476
477 let response = Http::get("https://x")
478 .through(Arc::clone(&fake) as Arc<dyn Transport>)
479 .send()
480 .await
481 .unwrap();
482
483 assert_eq!(response.status(), 500);
484 assert!(response.error_for_status().is_err());
485 }
486
487 #[tokio::test]
488 async fn a_retryable_status_is_retried_and_then_succeeds() {
489 let fake = fake();
490 fake.responding(503, "later").responding(503, "later").responding(200, "yes");
491
492 let response = Http::get("https://x")
493 .retry(3, Backoff::None)
494 .through(Arc::clone(&fake) as Arc<dyn Transport>)
495 .send()
496 .await
497 .unwrap();
498
499 assert_eq!(response.status(), 200);
500 assert_eq!(fake.count(), 3);
501 }
502
503 #[tokio::test]
504 async fn a_client_mistake_is_not_retried() {
505 let fake = fake();
508 fake.responding(422, "bad payload");
509
510 let response = Http::post("https://x")
511 .retry(4, Backoff::None)
512 .through(Arc::clone(&fake) as Arc<dyn Transport>)
513 .send()
514 .await
515 .unwrap();
516
517 assert_eq!(response.status(), 422);
518 assert_eq!(fake.count(), 1, "it should not have tried again");
519 }
520
521 #[tokio::test]
522 async fn retrying_gives_up_and_returns_the_last_answer() {
523 let fake = fake();
524 fake.responding(503, "still no");
525
526 let response = Http::get("https://x")
527 .retry(3, Backoff::None)
528 .through(Arc::clone(&fake) as Arc<dyn Transport>)
529 .send()
530 .await
531 .unwrap();
532
533 assert_eq!(response.status(), 503);
534 assert_eq!(fake.count(), 3);
535 }
536
537 #[tokio::test]
538 async fn there_is_a_timeout_by_default() {
539 let fake = fake();
542
543 Http::get("https://x")
544 .through(Arc::clone(&fake) as Arc<dyn Transport>)
545 .send()
546 .await
547 .unwrap();
548
549 let request = OutboundRequest {
551 method: "GET".into(),
552 url: "https://x".into(),
553 headers: BTreeMap::new(),
554 body: None,
555 timeout: Some(Duration::from_secs(30)),
556 };
557 assert_eq!(request.timeout, Some(Duration::from_secs(30)));
558 }
559
560 #[test]
561 fn form_values_are_percent_encoded() {
562 assert_eq!(encode("read write"), "read%20write");
563 assert_eq!(encode("a&b=c"), "a%26b%3Dc");
564 assert_eq!(encode("ada@example.com"), "ada%40example.com");
565 assert_eq!(encode("plain-value_1.0~"), "plain-value_1.0~");
566 }
567
568 #[test]
569 fn the_method_is_normalised() {
570 assert_eq!(Http::request("post", "https://x").method, "POST");
571 }
572}