1use eyre::OptionExt;
38pub use http::{header, Method, StatusCode, Version};
39use std::time::{Duration, Instant};
40use tracing::*;
41
42#[derive(Debug, thiserror::Error)]
43pub enum Error {
44 #[error("HttpError: {0}")]
45 Http(#[from] reqwest::Error),
46 #[error("failed to deserialize http response into the specified type: {0}")]
47 Deserialize(#[from] serde_json::Error),
48 #[error("{0:#}")]
49 Unexpected(#[from] eyre::Error),
50}
51
52#[derive(Debug, Clone)]
53pub struct LogRequest {
54 pub url: url::Url,
55 pub method: Method,
56 pub headers: header::HeaderMap,
57}
58
59#[derive(Debug, Clone, Default)]
60pub struct LogResponse {
61 pub headers: header::HeaderMap,
62 pub body: String,
63 pub status: StatusCode,
64 pub duration_req: Duration,
65}
66
67#[derive(Debug, Clone)]
68pub struct Log {
69 pub request: LogRequest,
70 pub response: LogResponse,
71}
72
73#[derive(Debug, Clone)]
102pub struct Response {
103 pub headers: header::HeaderMap,
104 pub status: StatusCode,
105 pub text: String,
106 pub url: url::Url,
107 #[cfg(feature = "cookies")]
108 cookies: Vec<cookie::Cookie<'static>>,
109}
110
111impl Response {
112 pub fn status(&self) -> StatusCode {
122 self.status
123 }
124
125 pub fn headers(&self) -> &header::HeaderMap {
135 &self.headers
136 }
137
138 pub fn url(&self) -> &url::Url {
147 &self.url
148 }
149
150 pub async fn text(self) -> Result<String, Error> {
159 Ok(self.text)
160 }
161
162 pub async fn json<T: serde::de::DeserializeOwned>(self) -> Result<T, Error> {
178 Ok(serde_json::from_str(&self.text)?)
179 }
180
181 #[cfg(feature = "cookies")]
182 pub fn cookies(&self) -> impl Iterator<Item = &cookie::Cookie<'static>> + '_ {
183 self.cookies.iter()
184 }
185
186 async fn from(res: reqwest::Response) -> Self {
187 let headers = res.headers().clone();
188 let status = res.status();
189 let url = res.url().clone();
190
191 #[cfg(feature = "cookies")]
192 let cookies: Vec<cookie::Cookie<'static>> = res
193 .cookies()
194 .map(|cookie| {
195 cookie::Cookie::build((cookie.name().to_string(), cookie.value().to_string()))
196 .build()
197 })
198 .collect();
199
200 let text = res.text().await.unwrap_or_default();
201
202 Response {
203 headers,
204 status,
205 url,
206 text,
207 #[cfg(feature = "cookies")]
208 cookies,
209 }
210 }
211}
212
213#[derive(Clone, Default)]
245pub struct Client {
246 pub(crate) inner: reqwest::Client,
247}
248
249impl Client {
250 pub fn new() -> Client {
264 #[cfg(feature = "cookies")]
265 let inner = reqwest::Client::builder()
266 .cookie_store(true)
267 .build()
268 .unwrap_or_default();
269
270 #[cfg(not(feature = "cookies"))]
271 let inner = reqwest::Client::default();
272
273 Client { inner }
274 }
275
276 pub fn get(&self, url: impl reqwest::IntoUrl) -> RequestBuilder {
277 let url = url.into_url().unwrap();
278 debug!("Requesting {url}");
279 RequestBuilder {
280 inner: Some(self.inner.get(url)),
281 client: self.inner.clone(),
282 }
283 }
284
285 pub fn post(&self, url: impl reqwest::IntoUrl) -> RequestBuilder {
286 let url = url.into_url().unwrap();
287 debug!("Requesting {url}");
288 RequestBuilder {
289 inner: Some(self.inner.post(url)),
290 client: self.inner.clone(),
291 }
292 }
293
294 pub fn put(&self, url: impl reqwest::IntoUrl) -> RequestBuilder {
295 let url = url.into_url().unwrap();
296 debug!("Requesting {url}");
297 RequestBuilder {
298 inner: Some(self.inner.put(url)),
299 client: self.inner.clone(),
300 }
301 }
302
303 pub fn patch(&self, url: impl reqwest::IntoUrl) -> RequestBuilder {
304 let url = url.into_url().unwrap();
305 debug!("Requesting {url}");
306 RequestBuilder {
307 inner: Some(self.inner.patch(url)),
308 client: self.inner.clone(),
309 }
310 }
311
312 pub fn delete(&self, url: impl reqwest::IntoUrl) -> RequestBuilder {
313 let url = url.into_url().unwrap();
314 debug!("Requesting {url}");
315 RequestBuilder {
316 inner: Some(self.inner.delete(url)),
317 client: self.inner.clone(),
318 }
319 }
320
321 pub fn head(&self, url: impl reqwest::IntoUrl) -> RequestBuilder {
322 let url = url.into_url().unwrap();
323 debug!("Requesting {url}");
324 RequestBuilder {
325 inner: Some(self.inner.head(url)),
326 client: self.inner.clone(),
327 }
328 }
329}
330
331pub struct RequestBuilder {
332 pub(crate) inner: Option<reqwest::RequestBuilder>,
333 pub(crate) client: reqwest::Client,
334}
335
336impl RequestBuilder {
337 pub fn header<K, V>(mut self, key: K, value: V) -> RequestBuilder
338 where
339 header::HeaderName: TryFrom<K>,
340 <header::HeaderName as TryFrom<K>>::Error: Into<http::Error>,
341 header::HeaderValue: TryFrom<V>,
342 <header::HeaderValue as TryFrom<V>>::Error: Into<http::Error>,
343 {
344 let inner = self.inner.take().expect("inner missing");
345 self.inner = Some(inner.header(key, value));
346 self
347 }
348
349 pub fn headers(mut self, headers: header::HeaderMap) -> RequestBuilder {
350 let inner = self.inner.take().expect("inner missing");
351 self.inner = Some(inner.headers(headers));
352 self
353 }
354
355 pub fn basic_auth<U, P>(mut self, username: U, password: Option<P>) -> RequestBuilder
356 where
357 U: std::fmt::Display,
358 P: std::fmt::Display,
359 {
360 let inner = self.inner.take().expect("inner missing");
361 self.inner = Some(inner.basic_auth(username, password));
362 self
363 }
364
365 pub fn bearer_auth<T>(mut self, token: T) -> RequestBuilder
366 where
367 T: std::fmt::Display,
368 {
369 let inner = self.inner.take().expect("inner missing");
370 self.inner = Some(inner.bearer_auth(token));
371 self
372 }
373
374 pub fn body<T: Into<reqwest::Body>>(mut self, body: T) -> RequestBuilder {
375 let inner = self.inner.take().expect("inner missing");
376 self.inner = Some(inner.body(body));
377 self
378 }
379
380 pub fn query<T: serde::Serialize + ?Sized>(mut self, query: &T) -> RequestBuilder {
381 let inner = self.inner.take().expect("inner missing");
382 self.inner = Some(inner.query(query));
383 self
384 }
385
386 pub fn form<T: serde::Serialize + ?Sized>(mut self, form: &T) -> RequestBuilder {
387 let inner = self.inner.take().expect("inner missing");
388 self.inner = Some(inner.form(form));
389 self
390 }
391
392 #[cfg(feature = "json")]
393 pub fn json<T: serde::Serialize + ?Sized>(mut self, json: &T) -> RequestBuilder {
394 self.inner = self.inner.take().map(|inner| inner.json(json));
395 self
396 }
397
398 #[cfg(feature = "multipart")]
399 pub fn multipart(mut self, multipart: reqwest::multipart::Form) -> RequestBuilder {
400 let inner = self.inner.take().expect("inner missing");
401 self.inner = Some(inner.multipart(multipart));
402 self
403 }
404
405 pub async fn send(mut self) -> Result<Response, Error> {
406 let req = self.inner.take().ok_or_eyre("inner missing")?.build()?;
407
408 let log_request = LogRequest {
409 url: req.url().clone(),
410 method: req.method().clone(),
411 headers: req.headers().clone(),
412 };
413
414 let time_req = Instant::now();
415 let res = self.client.execute(req).await;
416
417 match res {
418 Ok(res) => {
419 let res = Response::from(res).await;
420 let duration_req = time_req.elapsed();
421
422 let log_response = LogResponse {
423 headers: res.headers.clone(),
424 body: res.text.clone(),
425 status: res.status(),
426 duration_req,
427 };
428
429 crate::runner::publish(crate::runner::EventBody::Http(Box::new(Log {
430 request: log_request.clone(),
431 response: log_response,
432 })))?;
433 Ok(res)
434 }
435 Err(e) => {
436 crate::runner::publish(crate::runner::EventBody::Http(Box::new(Log {
437 request: log_request,
438 response: Default::default(),
439 })))?;
440 Err(e.into())
441 }
442 }
443 }
444
445 pub fn timeout(mut self, timeout: std::time::Duration) -> RequestBuilder {
446 let inner = self.inner.take().expect("inner missing");
447 self.inner = Some(inner.timeout(timeout));
448 self
449 }
450
451 pub fn try_clone(&self) -> Option<RequestBuilder> {
452 let inner = self.inner.as_ref()?;
453 Some(RequestBuilder {
454 inner: Some(inner.try_clone()?),
455 client: self.client.clone(),
456 })
457 }
458
459 pub fn version(mut self, version: Version) -> RequestBuilder {
460 let inner = self.inner.take().expect("inner missing");
461 self.inner = Some(inner.version(version));
462 self
463 }
464}