Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
133 changes: 133 additions & 0 deletions docs/src/cache-modes.md
Original file line number Diff line number Diff line change
Expand Up @@ -384,3 +384,136 @@ let cache = HttpCache {
5. **Global Settings**: Options like `max_ttl` and `cache_status_headers` provide global configuration

All of these functions are called on a per-request basis, giving you complete control over caching behavior for each individual request.

## Response Metadata

The cache allows storing custom metadata alongside cached responses using the `metadata_provider` callback. This is useful for storing computed information that should be associated with cached responses, avoiding recomputation on cache hits.

### Basic Usage

```rust
use http_cache::{HttpCacheOptions, CACacheManager, HttpCache, CacheMode};
use std::sync::Arc;

let manager = CACacheManager::new("./cache".into(), true);

let options = HttpCacheOptions {
metadata_provider: Some(Arc::new(|request_parts, response_parts| {
// Generate metadata based on request and response
let content_type = response_parts
.headers
.get("content-type")
.and_then(|v| v.to_str().ok())
.unwrap_or("unknown");

// Serialize metadata as bytes (users handle serialization)
Some(format!(
"path={};content-type={};status={}",
request_parts.uri.path(),
content_type,
response_parts.status.as_u16()
).into_bytes())
})),
..Default::default()
};

let cache = HttpCache {
mode: CacheMode::Default,
manager,
options,
};
```

### Use Cases

The `metadata_provider` is particularly useful for:

1. **Computed Information**: Store computed data based on request/response pairs that would be expensive to recompute
2. **Logging Context**: Store information for logging that should be associated with cached responses
3. **Custom Headers**: Store additional headers or information that should be returned with cached responses
4. **Analytics Data**: Store request timing, transformation information, or other analytics data

### Accessing Metadata

When retrieving cached responses through the `HttpCacheInterface`, the `HttpResponse` struct contains the metadata field:

```rust
// When looking up cached responses
if let Some((cached_response, policy)) = cache.lookup_cached_response(&cache_key).await? {
// Access the metadata
if let Some(metadata) = &cached_response.metadata {
// Deserialize and use the metadata
let metadata_str = String::from_utf8_lossy(metadata);
println!("Cached with metadata: {}", metadata_str);
}

// Use the cached response body
let body = &cached_response.body;
}
```

### Conditional Metadata Generation

The metadata provider can return `None` to skip metadata generation for certain responses:

```rust
let options = HttpCacheOptions {
metadata_provider: Some(Arc::new(|request_parts, response_parts| {
// Only generate metadata for API responses
if request_parts.uri.path().starts_with("/api/") {
let computed_info = format!(
"api_version={};response_time={}",
response_parts.headers
.get("x-api-version")
.and_then(|v| v.to_str().ok())
.unwrap_or("unknown"),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_millis()
);
Some(computed_info.into_bytes())
} else {
None // No metadata for non-API responses
}
})),
..Default::default()
};
```

### Integration with Middleware

For middleware implementations (like reqwest-middleware), the types `HttpCacheMetadata` and `MetadataProvider` are re-exported:

```rust
use http_cache_reqwest::{
HttpCacheMetadata, MetadataProvider, HttpCacheOptions,
CacheMode, CACacheManager, HttpCache, Cache
};
use reqwest::Client;
use reqwest_middleware::ClientBuilder;
use std::sync::Arc;

let options = HttpCacheOptions {
metadata_provider: Some(Arc::new(|req, res| {
// Store request path and response status as metadata
Some(format!("{}:{}", req.uri.path(), res.status.as_u16()).into_bytes())
})),
..Default::default()
};

let client = ClientBuilder::new(Client::new())
.with(Cache(HttpCache {
mode: CacheMode::Default,
manager: CACacheManager::new("./cache".into(), true),
options,
}))
.build();
```

### Notes

- Users are responsible for serialization/deserialization of metadata
- Metadata is stored as `Vec<u8>` bytes
- When both explicit metadata is passed to `process_response` and a `metadata_provider` is configured, the explicit metadata takes precedence
- Metadata persists with the cached response and is available on cache hits
2 changes: 2 additions & 0 deletions http-cache-quickcache/src/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ async fn quickcache() -> Result<()> {
status: 200,
url: url.clone(),
version: HttpVersion::Http11,
metadata: None,
};
let req = http::Request::get("http://example.com").body(())?;
let res = http::Response::builder().status(200).body(TEST_BODY.to_vec())?;
Expand Down Expand Up @@ -126,6 +127,7 @@ async fn default_mode_with_options() -> Result<()> {
cache_bust: None,
cache_status_headers: true,
max_ttl: None,
metadata_provider: None,
},
}))
.build();
Expand Down
14 changes: 10 additions & 4 deletions http-cache-reqwest/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -305,8 +305,8 @@ fn to_middleware_error<E: std::error::Error + Send + Sync + 'static>(
use url::Url;

pub use http_cache::{
CacheManager, CacheMode, CacheOptions, HttpCache, HttpCacheOptions,
HttpResponse, ResponseCacheModeFn,
CacheManager, CacheMode, CacheOptions, HttpCache, HttpCacheMetadata,
HttpCacheOptions, HttpResponse, MetadataProvider, ResponseCacheModeFn,
};

#[cfg(feature = "streaming")]
Expand Down Expand Up @@ -460,12 +460,14 @@ impl Middleware for ReqwestMiddleware<'_> {
status,
url,
version: version.try_into()?,
metadata: None,
})
}
}

// Converts an [`HttpResponse`] to a reqwest [`Response`]
fn convert_response(response: HttpResponse) -> Result<Response> {
let metadata = response.metadata.clone();
let mut ret_res = http::Response::builder()
.status(response.status)
.url(response.url)
Expand All @@ -477,6 +479,10 @@ fn convert_response(response: HttpResponse) -> Result<Response> {
HeaderValue::from_str(&header.1)?,
);
}
// Insert metadata into response extensions if present
if let Some(metadata) = metadata {
ret_res.extensions_mut().insert(HttpCacheMetadata::from(metadata));
}
Ok(Response::from(ret_res))
}

Expand Down Expand Up @@ -789,7 +795,7 @@ where
})?;
let cached_response = self
.cache
.process_response(analysis, http_response)
.process_response(analysis, http_response, None)
.await
.map_err(|e| {
to_middleware_error(HttpCacheError::Cache(
Expand Down Expand Up @@ -843,7 +849,7 @@ where
// Process and potentially cache the response
let cached_response = self
.cache
.process_response(analysis, http_response)
.process_response(analysis, http_response, None)
.await
.map_err(|e| {
to_middleware_error(HttpCacheError::Cache(e.to_string()))
Expand Down
78 changes: 74 additions & 4 deletions http-cache-reqwest/src/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1142,6 +1142,7 @@ async fn options_request_not_cached() -> Result<()> {
Ok(())
}

#[cfg(feature = "streaming")]
#[tokio::test]
async fn test_multipart_form_cloning_issue() -> Result<()> {
// This test reproduces the exact issue reported by the user
Expand Down Expand Up @@ -1274,7 +1275,7 @@ mod streaming_tests {

// Process and cache the response
let cached_response =
cache.process_response(analysis.clone(), response).await?;
cache.process_response(analysis.clone(), response, None).await?;
assert_eq!(cached_response.status(), 200);

// Verify the response body
Expand Down Expand Up @@ -1323,7 +1324,7 @@ mod streaming_tests {

// Process the large response
let cached_response =
cache.process_response(analysis.clone(), response).await?;
cache.process_response(analysis.clone(), response, None).await?;
assert_eq!(cached_response.status(), 200);

// Verify the large response body
Expand Down Expand Up @@ -1365,7 +1366,7 @@ mod streaming_tests {

// Process the empty response
let cached_response =
cache.process_response(analysis.clone(), response).await?;
cache.process_response(analysis.clone(), response, None).await?;
assert_eq!(cached_response.status(), 204);

// Verify empty body
Expand Down Expand Up @@ -1734,7 +1735,7 @@ mod streaming_tests {

// Process the response
let cached_response =
cache.process_response(analysis.clone(), response).await?;
cache.process_response(analysis.clone(), response, None).await?;
assert_eq!(cached_response.status(), 200);

// Verify the body
Expand Down Expand Up @@ -2239,3 +2240,72 @@ mod rate_limiting_tests {
Ok(())
}
}

#[tokio::test]
async fn test_metadata_retrieval_through_extensions() -> Result<()> {
let mock_server = MockServer::start().await;

Mock::given(method(GET))
.respond_with(
ResponseTemplate::new(200)
.insert_header("cache-control", CACHEABLE_PUBLIC)
.set_body_bytes(TEST_BODY),
)
.expect(1) // Only called once, second request is cached
.mount(&mock_server)
.await;

let url = format!("{}/metadata-test", mock_server.uri());

// Create cache options with a metadata provider
let options = HttpCacheOptions {
metadata_provider: Some(Arc::new(|_request_parts, _response_parts| {
Some(b"test-metadata-value".to_vec())
})),
..Default::default()
};

let client = ClientBuilder::new(Client::new())
.with(Cache(HttpCache {
mode: CacheMode::Default,
manager: create_cache_manager(),
options,
}))
.build();

// First request - stores the response with metadata
let response1 = client.get(&url).send().await?;
assert_eq!(response1.status(), 200);

// First response should have metadata (generated by metadata provider when caching)
let metadata1 = response1.extensions().get::<HttpCacheMetadata>();
assert!(
metadata1.is_some(),
"Metadata should be present when response is cached"
);
assert_eq!(
metadata1.unwrap().as_slice(),
b"test-metadata-value",
"Metadata value should match what was stored"
);

// Second request - should retrieve from cache with metadata in extensions
let response2 = client.get(&url).send().await?;
assert_eq!(response2.status(), 200);

// Check that metadata is also present on cache hit
let metadata2 = response2.extensions().get::<HttpCacheMetadata>();
assert!(
metadata2.is_some(),
"Metadata should be present in response extensions on cache hit"
);

let metadata_value = metadata2.unwrap();
assert_eq!(
metadata_value.as_slice(),
b"test-metadata-value",
"Metadata value should match what was stored"
);

Ok(())
}
1 change: 1 addition & 0 deletions http-cache-surf/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,7 @@ impl Middleware for SurfMiddleware<'_> {
status,
url,
version: version.try_into()?,
metadata: None,
})
}
}
Expand Down
Loading