1#![deny(clippy::all)]
8#![warn(clippy::pedantic)]
9
10use std::time::Instant;
11
12use bytes::Bytes;
13use http::HeaderMap;
14use parlov_core::{Error, ProbeDefinition, ResponseSurface};
15
16use crate::Probe;
17
18pub struct HttpProbe {
23 client: reqwest::Client,
24}
25
26impl HttpProbe {
27 #[must_use]
29 pub fn new() -> Self {
30 Self {
31 client: reqwest::Client::new(),
32 }
33 }
34
35 #[must_use]
40 pub fn with_client(client: reqwest::Client) -> Self {
41 Self { client }
42 }
43}
44
45impl Default for HttpProbe {
46 fn default() -> Self {
47 Self::new()
48 }
49}
50
51impl Probe for HttpProbe {
52 async fn execute(&self, def: &ProbeDefinition) -> Result<ResponseSurface, Error> {
53 let method = build_method(&def.method)?;
54 let mut builder = self.client.request(method, &def.url);
55 builder = apply_headers(builder, &def.headers)?;
56
57 if let Some(body) = &def.body {
58 builder = builder.body(body.clone());
59 }
60
61 let request = builder
62 .build()
63 .map_err(|e| Error::Http(e.to_string()))?;
64
65 let start = Instant::now();
66 let response = self
67 .client
68 .execute(request)
69 .await
70 .map_err(|e| Error::Http(e.to_string()))?;
71 let status = convert_status(response.status())?;
72 let headers = convert_headers(response.headers());
73 let body_bytes: Bytes = response
74 .bytes()
75 .await
76 .map_err(|e| Error::Http(e.to_string()))?;
77 let timing_ns = u64::try_from(start.elapsed().as_nanos()).unwrap_or(u64::MAX);
78
79 Ok(ResponseSurface {
80 status,
81 headers,
82 body: body_bytes,
83 timing_ns,
84 })
85 }
86}
87
88fn build_method(method: &http::Method) -> Result<reqwest::Method, Error> {
89 reqwest::Method::from_bytes(method.as_str().as_bytes())
90 .map_err(|e| Error::Http(format!("invalid HTTP method: {e}")))
91}
92
93fn apply_headers(
94 mut builder: reqwest::RequestBuilder,
95 headers: &HeaderMap,
96) -> Result<reqwest::RequestBuilder, Error> {
97 for (name, value) in headers {
98 let rname = reqwest::header::HeaderName::from_bytes(name.as_str().as_bytes())
99 .map_err(|e| Error::Http(format!("invalid header name: {e}")))?;
100 let rvalue = reqwest::header::HeaderValue::from_bytes(value.as_bytes())
101 .map_err(|e| Error::Http(format!("invalid header value: {e}")))?;
102 builder = builder.header(rname, rvalue);
103 }
104 Ok(builder)
105}
106
107fn convert_status(status: reqwest::StatusCode) -> Result<http::StatusCode, Error> {
108 http::StatusCode::from_u16(status.as_u16())
109 .map_err(|e| Error::Http(format!("unrecognised status code: {e}")))
110}
111
112fn convert_headers(headers: &reqwest::header::HeaderMap) -> HeaderMap {
113 let mut out = HeaderMap::new();
114 for (name, value) in headers {
115 if let (Ok(n), Ok(v)) = (
116 http::header::HeaderName::from_bytes(name.as_str().as_bytes()),
117 http::header::HeaderValue::from_bytes(value.as_bytes()),
118 ) {
119 out.insert(n, v);
120 }
121 }
122 out
123}