Crates.io | http-client-vcr |
lib.rs | http-client-vcr |
version | 1.1.0 |
created_at | 2025-08-05 19:36:04.755732+00 |
updated_at | 2025-08-05 21:27:08.126481+00 |
description | Record http request and responses for testing |
homepage | https://github.com/colonelpanic8/http-client-vcr |
repository | https://github.com/colonelpanic8/http-client-vcr |
max_upload_size | |
id | 1782741 |
size | 5,614,727 |
A Rust library for recording and replaying HTTP requests, inspired by VCR libraries in other languages. This library works with the http-client
crate to provide a simple way to test HTTP interactions.
Add this to your Cargo.toml
:
[dependencies]
http-client-vcr = "0.1.0"
use http_client_vcr::{VcrClient, VcrMode};
use http_client::Request;
use http_types::{Method, Url};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Create your HTTP client (h1, hyper, isahc, etc.)
let inner_client = Box::new(h1::H1Client::new());
// Create VCR client
let vcr_client = VcrClient::builder()
.inner_client(inner_client)
.cassette_path("fixtures/my_test.yaml")
.mode(VcrMode::Once)
.build()
.await?;
// Use it like any HttpClient
let request = Request::new(Method::Get, Url::parse("https://httpbin.org/get")?);
let response = vcr_client.send(request).await?;
println!("Status: {}", response.status());
// Save cassette
vcr_client.save_cassette().await?;
Ok(())
}
VcrMode::Record
: Always make real HTTP requests and record them. If an interaction already exists in the cassette, replay it instead.VcrMode::Replay
: Only replay interactions from the cassette. Fail if no matching interaction is found.VcrMode::Once
: Record interactions only if the cassette is empty, otherwise replay existing interactions.VcrMode::None
: Pass through to the inner HTTP client without any recording or replaying.By default, requests are matched by HTTP method and URL. You can customize matching behavior:
use http_client_vcr::{DefaultMatcher, ExactMatcher};
// Custom matching (method, URL, specific headers)
let matcher = DefaultMatcher::new()
.with_method(true)
.with_url(true)
.with_headers(vec!["Authorization".to_string(), "Content-Type".to_string()]);
let vcr_client = VcrClient::builder()
.inner_client(inner_client)
.cassette_path("fixtures/my_test.yaml")
.matcher(Box::new(matcher))
.build()
.await?;
VCR supports filtering sensitive data from requests and responses before they are stored in cassettes:
use http_client_vcr::{HeaderFilter, BodyFilter, UrlFilter, FilterChain};
// Remove sensitive headers
let header_filter = HeaderFilter::new()
.remove_auth_headers() // Removes Authorization, Cookie, X-API-Key, etc.
.remove_header("X-Custom-Secret")
.replace_header("User-Id", "FILTERED");
// Filter JSON body content
let body_filter = BodyFilter::new()
.remove_common_sensitive_keys() // Removes password, token, api_key, etc.
.remove_json_key("credit_card")
.replace_regex(r"\d{4}-\d{4}-\d{4}-\d{4}", "XXXX-XXXX-XXXX-XXXX")
.unwrap();
// Filter URL query parameters
let url_filter = UrlFilter::new()
.remove_common_sensitive_params() // Removes api_key, token, etc.
.remove_query_param("secret")
.replace_query_param("user_id", "FILTERED");
// Chain filters together
let filter_chain = FilterChain::new()
.add_filter(Box::new(header_filter))
.add_filter(Box::new(body_filter))
.add_filter(Box::new(url_filter));
let vcr_client = VcrClient::builder()
.inner_client(inner_client)
.cassette_path("fixtures/filtered_test.yaml")
.filter_chain(filter_chain)
.build()
.await?;
You can create custom filters for more complex scenarios:
use http_client_vcr::CustomFilter;
let custom_filter = CustomFilter::new(|req, resp| {
// Remove any header containing "secret"
req.headers.retain(|key, _| !key.to_lowercase().contains("secret"));
// Replace response body if it contains errors
if let Some(body) = &mut resp.body {
if body.contains("error") {
*body = r#"{"error": "FILTERED"}"#.to_string();
}
}
});
let vcr_client = VcrClient::builder()
.inner_client(inner_client)
.add_filter(Box::new(custom_filter))
.build()
.await?;
Important: Filters are applied only to the data stored in cassette files, not to the actual HTTP interactions. During recording:
This ensures your code gets the real data it needs while keeping cassettes safe for version control.
For ultimate safety during testing, VCR provides a NoOpClient
that ensures no real HTTP requests are ever made:
use http_client_vcr::{VcrClient, VcrMode, NoOpClient};
// Guarantee no real HTTP requests can be made
let vcr_client = VcrClient::builder()
.inner_client(Box::new(NoOpClient::new()))
.cassette_path("tests/fixtures/api_test.yaml")
.mode(VcrMode::Replay) // Only replay from cassette
.build()
.await?;
// This works if the request exists in the cassette
let response = vcr_client.send(request).await?;
// If not in cassette, you get a clear error (not a real HTTP request)
Two variants are available:
NoOpClient::new()
- Returns an error if a request is attemptedNoOpClient::panicking()
- Panics with a stack trace (useful for development)This is particularly useful in CI/CD environments or when you want to be absolutely certain your tests are deterministic.
Cassettes are stored as YAML files with the following structure:
interactions:
- request:
method: GET
url: https://httpbin.org/get
headers:
User-Agent: ["http-client/1.0"]
body: null
version: Http1_1
response:
status: 200
headers:
Content-Type: ["application/json"]
body: '{"origin": "127.0.0.1"}'
version: Http1_1
VCR is particularly useful for testing:
#[tokio::test]
async fn test_api_call() {
let vcr_client = VcrClient::builder()
.inner_client(Box::new(h1::H1Client::new()))
.cassette_path("tests/fixtures/api_call.yaml")
.mode(VcrMode::Once)
.build()
.await
.unwrap();
let request = Request::new(Method::Get, Url::parse("https://api.example.com/data").unwrap());
let response = vcr_client.send(request).await.unwrap();
assert_eq!(response.status(), 200);
// First run records the interaction
// Subsequent runs replay from cassette
}
MIT