#[cfg(any(target_arch = "wasm32", test))]
use std::time::Duration;
#[cfg(any(target_arch = "wasm32", test))]
use crate::TransportKind;
#[cfg(any(target_arch = "wasm32", test))]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Stage {
Fetch,
Body,
}
#[cfg(any(target_arch = "wasm32", test))]
fn clamped(timeout: Duration) -> Duration {
timeout.min(Duration::from_millis(i32::MAX as u64))
}
#[cfg(any(target_arch = "wasm32", test))]
fn timed_out(reqwest_says: bool, elapsed: Duration, timeout: Duration) -> bool {
reqwest_says || elapsed + CLOCK_SLACK >= timeout
}
#[cfg(any(target_arch = "wasm32", test))]
const CLOCK_SLACK: Duration = Duration::from_millis(2);
#[cfg(any(target_arch = "wasm32", test))]
fn kind_of(stage: Stage, timed_out: bool, malformed: bool) -> TransportKind {
if timed_out {
TransportKind::Timeout
} else if malformed {
TransportKind::Request
} else {
match stage {
Stage::Fetch => TransportKind::Connect,
Stage::Body => TransportKind::Body,
}
}
}
#[cfg(target_arch = "wasm32")]
mod web {
use std::time::Duration;
use bytes::Bytes;
use polyoxide_rt::{default_runtime, BoxFuture, Instant};
use send_wrapper::SendWrapper;
use super::{clamped, kind_of, timed_out, Stage};
use crate::{HttpRequest, HttpTransport, Response, TransportError};
#[derive(Debug, Clone, Default)]
#[non_exhaustive]
pub struct FetchTransport {}
impl FetchTransport {
pub fn new() -> Self {
Self {}
}
}
impl HttpTransport for FetchTransport {
fn send(&self, request: HttpRequest) -> BoxFuture<Result<Response, TransportError>> {
Box::pin(SendWrapper::new(fetch(request)))
}
}
async fn fetch(request: HttpRequest) -> Result<Response, TransportError> {
let timeout = clamped(request.timeout);
let runtime = default_runtime();
let started = runtime.now();
let mut builder = reqwest::Client::new()
.request(request.method, request.url)
.headers(request.headers)
.timeout(timeout);
if let Some(body) = request.body {
builder = builder.body(body);
}
let response = builder
.send()
.await
.map_err(|error| failure(Stage::Fetch, error, runtime.now(), started, timeout))?;
let status = response.status();
let headers = response.headers().clone();
let url = response.url().clone();
let body: BoxFuture<Result<Bytes, TransportError>> =
Box::pin(SendWrapper::new(async move {
response
.bytes()
.await
.map_err(|error| failure(Stage::Body, error, runtime.now(), started, timeout))
}));
Ok(Response::new(status, headers, url, body))
}
fn failure(
stage: Stage,
error: reqwest::Error,
now: Instant,
started: Instant,
timeout: Duration,
) -> TransportError {
let elapsed = now.saturating_duration_since(started);
let timed_out = timed_out(error.is_timeout(), elapsed, timeout);
TransportError::new(kind_of(stage, timed_out, error.is_builder()), error)
}
}
#[cfg(target_arch = "wasm32")]
pub use web::FetchTransport;
#[cfg(test)]
mod tests {
use polyoxide_venue::Classify;
use std::time::Duration;
use super::{clamped, kind_of, timed_out, Stage, CLOCK_SLACK};
use crate::{TransportError, TransportKind};
const TIMEOUT: Duration = Duration::from_secs(30);
#[test]
fn a_timeout_is_a_timeout_in_either_stage() {
for stage in [Stage::Fetch, Stage::Body] {
assert_eq!(kind_of(stage, true, false), TransportKind::Timeout);
}
}
#[test]
fn a_timeout_outranks_a_request_the_browser_refused() {
assert_eq!(kind_of(Stage::Fetch, true, true), TransportKind::Timeout);
}
#[test]
fn a_failed_fetch_is_a_connect_failure() {
assert_eq!(kind_of(Stage::Fetch, false, false), TransportKind::Connect);
}
#[test]
fn a_failed_body_read_is_a_body_failure() {
assert_eq!(kind_of(Stage::Body, false, false), TransportKind::Body);
}
#[test]
fn a_request_the_browser_refused_to_build_is_a_request_failure() {
for stage in [Stage::Fetch, Stage::Body] {
assert_eq!(kind_of(stage, false, true), TransportKind::Request);
}
}
#[test]
fn a_body_failure_after_the_deadline_is_a_timeout() {
let late = timed_out(false, TIMEOUT + Duration::from_millis(1), TIMEOUT);
assert_eq!(kind_of(Stage::Body, late, false), TransportKind::Timeout);
let at = timed_out(false, TIMEOUT, TIMEOUT);
assert_eq!(kind_of(Stage::Body, at, false), TransportKind::Timeout);
}
#[test]
fn a_body_failure_before_the_deadline_is_a_body_failure() {
let early = timed_out(
false,
TIMEOUT - CLOCK_SLACK - Duration::from_millis(1),
TIMEOUT,
);
assert_eq!(kind_of(Stage::Body, early, false), TransportKind::Body);
}
#[test]
fn a_failure_one_clock_tick_short_of_the_deadline_is_still_a_timeout() {
assert!(timed_out(false, TIMEOUT - CLOCK_SLACK, TIMEOUT));
assert!(!timed_out(
false,
TIMEOUT - CLOCK_SLACK - Duration::from_nanos(1),
TIMEOUT
));
}
#[test]
fn reqwests_own_timeout_flag_is_a_timeout_whatever_the_clock_says() {
assert!(timed_out(true, Duration::ZERO, TIMEOUT));
assert!(timed_out(true, TIMEOUT * 2, TIMEOUT));
}
#[test]
fn a_timeout_above_the_bound_is_held_to_it() {
let bound = Duration::from_millis(i32::MAX as u64);
assert_eq!(clamped(bound + Duration::from_millis(1)), bound);
assert_eq!(clamped(Duration::MAX), bound);
assert_eq!(clamped(bound), bound);
assert_eq!(clamped(TIMEOUT), TIMEOUT);
assert_eq!(clamped(Duration::ZERO), Duration::ZERO);
}
#[test]
fn a_failed_fetch_is_retried_and_a_refused_request_is_not() {
let lost = TransportError::new(kind_of(Stage::Fetch, false, false), "network error");
assert!(lost.is_retriable());
let refused = TransportError::new(kind_of(Stage::Fetch, false, true), "bad header value");
assert!(!refused.is_retriable());
}
}