use async_trait::async_trait;
use http_client::{Error, HttpClient, Request, Response};
use http_client_vcr::{CassetteFormat, DefaultMatcher, NoOpClient, VcrClient, VcrMode};
use http_types::{Method, Url};
use std::env;
use std::path::PathBuf;
#[derive(Debug, Clone)]
struct ReqwestAdapter {
client: reqwest::Client,
}
impl ReqwestAdapter {
fn new() -> Self {
Self {
client: reqwest::Client::new(),
}
}
}
#[async_trait]
impl HttpClient for ReqwestAdapter {
async fn send(&self, mut req: Request) -> Result<Response, Error> {
let method = match req.method() {
Method::Get => reqwest::Method::GET,
Method::Post => reqwest::Method::POST,
Method::Put => reqwest::Method::PUT,
Method::Delete => reqwest::Method::DELETE,
Method::Head => reqwest::Method::HEAD,
Method::Options => reqwest::Method::OPTIONS,
Method::Patch => reqwest::Method::PATCH,
_ => reqwest::Method::GET,
};
let mut reqwest_req = self.client.request(method, req.url().as_str());
for (name, values) in req.iter() {
for value in values.iter() {
reqwest_req = reqwest_req.header(name.as_str(), value.as_str());
}
}
let body = req
.body_string()
.await
.map_err(|e| Error::from_str(500, e))?;
if !body.is_empty() {
reqwest_req = reqwest_req.body(body);
}
let reqwest_resp = reqwest_req
.send()
.await
.map_err(|e| Error::from_str(500, e))?;
let mut response = Response::new(reqwest_resp.status().as_u16());
for (name, value) in reqwest_resp.headers() {
let _ = response.insert_header(name.as_str(), value.to_str().unwrap_or(""));
}
let body_bytes = reqwest_resp
.bytes()
.await
.map_err(|e| Error::from_str(500, e))?;
response.set_body(body_bytes.to_vec());
Ok(response)
}
}
struct VcrTestSetup {
cassette_path: PathBuf,
mode: VcrMode,
}
impl VcrTestSetup {
fn new(test_name: &str) -> Result<Self, Box<dyn std::error::Error>> {
let cassette_path = PathBuf::from(format!("tests/fixtures/{test_name}"));
let vcr_record = env::var("VCR_RECORD").unwrap_or_default();
let cassette_exists = cassette_path.exists();
let mode = match vcr_record.as_str() {
"1" | "true" | "on" => VcrMode::Record,
_ => {
if !cassette_exists {
return Err(format!(
"No cassette found at '{}' and VCR_RECORD is not set. Either set VCR_RECORD=1 to record new interactions or ensure the cassette directory exists.",
cassette_path.display()
).into());
}
VcrMode::Replay
}
};
Ok(Self {
cassette_path,
mode,
})
}
async fn create_vcr_client(&self) -> Result<VcrClient, Box<dyn std::error::Error>> {
if let Some(parent_dir) = self.cassette_path.parent() {
std::fs::create_dir_all(parent_dir)?;
}
let inner_client: Box<dyn HttpClient + Send + Sync> = match self.mode {
VcrMode::Record => Box::new(ReqwestAdapter::new()),
_ => Box::new(NoOpClient::new()),
};
let matcher = DefaultMatcher::new().with_headers(vec![]);
let vcr_client = VcrClient::builder(&self.cassette_path)
.inner_client(inner_client)
.mode(self.mode.clone())
.format(CassetteFormat::Directory) .matcher(Box::new(matcher))
.build()
.await?;
Ok(vcr_client)
}
}
#[tokio::test]
async fn test_concurrent_requests_edge_case() -> Result<(), Box<dyn std::error::Error>> {
let setup = VcrTestSetup::new("concurrent_requests_test")?;
if matches!(setup.mode, VcrMode::Record) {
let vcr_client = setup.create_vcr_client().await?;
let url = "https://httpbin.org/uuid";
for i in 1..=3 {
let request = http_types::Request::new(Method::Get, Url::parse(url)?);
let response = vcr_client.send(request).await?;
println!("Recording request {}: status {}", i, response.status());
}
return Ok(());
}
let vcr_client = std::sync::Arc::new(setup.create_vcr_client().await?);
let url = "https://httpbin.org/uuid";
let mut handles = Vec::new();
for i in 1..=3 {
let client = vcr_client.clone();
let url = url.to_string();
let handle = tokio::spawn(async move {
let request = http_types::Request::new(Method::Get, Url::parse(&url)?);
let response = client.send(request).await?;
let mut response = response;
let body = response.body_string().await?;
println!("Concurrent request {i} got: {body}");
Ok::<String, Box<dyn std::error::Error + Send + Sync>>(body)
});
handles.push(handle);
}
let mut responses = Vec::new();
for handle in handles {
let response = handle.await.unwrap().unwrap();
responses.push(response);
}
assert_ne!(
responses[0], responses[1],
"Concurrent requests should get different responses"
);
assert_ne!(
responses[1], responses[2],
"Concurrent requests should get different responses"
);
assert_ne!(
responses[0], responses[2],
"Concurrent requests should get different responses"
);
println!("Concurrent requests handled correctly: {responses:?}");
Ok(())
}