use std::future::Future;
use std::pin::Pin;
use std::task::{Context, Poll};
use bytes::Bytes;
use http::HeaderMap;
use parlov_core::{Error, ProbeDefinition, ProbeExchange, ResponseSurface};
use worker::{Fetch, Headers, Method, Request, RequestInit, Response};
use crate::Probe;
const NANOS_PER_MS: u64 = 1_000_000;
fn ms_to_ns(elapsed_ms: f64) -> u64 {
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
let ms = elapsed_ms.max(0.0) as u64;
ms.saturating_mul(NANOS_PER_MS)
}
fn parse_status(code: u16) -> Result<http::StatusCode, Error> {
http::StatusCode::from_u16(code)
.map_err(|e| Error::Http(format!("unrecognised status code: {e}")))
}
#[derive(Clone)]
pub struct HttpProbe;
impl HttpProbe {
#[must_use]
pub fn new() -> Self {
Self
}
}
impl Default for HttpProbe {
fn default() -> Self {
Self::new()
}
}
struct SendFuture<F>(F);
unsafe impl<F: Future> Send for SendFuture<F> {}
impl<F: Future + Unpin> Future for SendFuture<F> {
type Output = F::Output;
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
Pin::new(&mut self.0).poll(cx)
}
}
impl Probe for HttpProbe {
fn execute(
&self,
def: &ProbeDefinition,
) -> impl Future<Output = Result<ProbeExchange, Error>> + Send {
let def = def.clone();
SendFuture(Box::pin(execute_fetch(def)))
}
}
async fn execute_fetch(def: ProbeDefinition) -> Result<ProbeExchange, Error> {
let request = build_request(&def)?;
let start_ms = worker::js_sys::Date::now();
let mut response = Fetch::Request(request)
.send()
.await
.map_err(|e| Error::Http(format!("fetch failed: {e}")))?;
let elapsed_ms = worker::js_sys::Date::now() - start_ms;
let surface = extract_surface(&mut response, elapsed_ms).await?;
Ok(ProbeExchange {
request: def,
response: surface,
})
}
fn build_request(def: &ProbeDefinition) -> Result<Request, Error> {
let mut init = RequestInit::new();
init.with_method(Method::from(def.method.as_str().to_owned()));
init.with_headers(build_headers(&def.headers)?);
if let Some(body) = &def.body {
let arr = worker::js_sys::Uint8Array::from(body.as_ref());
init.with_body(Some(arr.into()));
}
Request::new_with_init(&def.url, &init)
.map_err(|e| Error::Http(format!("failed to build Request: {e}")))
}
fn build_headers(map: &HeaderMap) -> Result<Headers, Error> {
let out = Headers::new();
for (name, value) in map {
let val_str = value
.to_str()
.map_err(|e| Error::Http(format!("non-ASCII header value: {e}")))?;
out.append(name.as_str(), val_str)
.map_err(|e| Error::Http(format!("failed to append header: {e}")))?;
}
Ok(out)
}
async fn extract_surface(resp: &mut Response, elapsed_ms: f64) -> Result<ResponseSurface, Error> {
let status = parse_status(resp.status_code())?;
let headers = parse_response_headers(resp.headers());
let body_bytes = resp
.bytes()
.await
.map(Bytes::from)
.map_err(|e| Error::Http(format!("failed to read response body: {e}")))?;
Ok(ResponseSurface {
status,
headers,
body: body_bytes,
timing_ns: ms_to_ns(elapsed_ms),
})
}
fn parse_response_headers(worker_headers: &Headers) -> HeaderMap {
let mut out = HeaderMap::new();
for (name, value) in worker_headers.entries() {
if let (Ok(n), Ok(v)) = (
http::header::HeaderName::from_bytes(name.as_bytes()),
http::header::HeaderValue::from_str(&value),
) {
out.append(n, v);
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn ms_to_ns_converts_correctly() {
assert_eq!(ms_to_ns(1.0), 1_000_000);
assert_eq!(ms_to_ns(0.0), 0);
assert_eq!(ms_to_ns(500.0), 500_000_000);
}
#[test]
fn ms_to_ns_clamps_negative_to_zero() {
assert_eq!(ms_to_ns(-5.0), 0);
assert_eq!(ms_to_ns(-0.001), 0);
}
#[test]
fn ms_to_ns_handles_fractional_truncation() {
assert_eq!(ms_to_ns(1.9), 1_000_000);
}
#[test]
fn parse_status_200_ok() {
let status = parse_status(200).expect("valid status");
assert_eq!(status, http::StatusCode::OK);
}
#[test]
fn parse_status_404_not_found() {
let status = parse_status(404).expect("valid status");
assert_eq!(status, http::StatusCode::NOT_FOUND);
}
#[test]
fn parse_status_403_forbidden() {
let status = parse_status(403).expect("valid status");
assert_eq!(status, http::StatusCode::FORBIDDEN);
}
#[test]
fn parse_status_rejects_zero() {
assert!(parse_status(0).is_err());
}
#[test]
fn parse_status_rejects_1000() {
assert!(parse_status(1000).is_err());
}
}