Skip to content

Commit dd499e1

Browse files
committed
feat: add a "revalidation hook" to Cache to allow for recalculation of authentication.
This commit adds a `with_revalidation_hook` method to `Cache` that allows users to control the headers sent as part of a cache revalidation request. This is needed for Azure Storage where they inexplicably use conditional request headers like `If-None-Match` as part of the authentication signature, which breaks transparent caching middleware like this one. With the hook, a new `Authorization` header can be calculated and then overridden for the request to revalidate.
1 parent 235faef commit dd499e1

File tree

2 files changed

+74
-2
lines changed

2 files changed

+74
-2
lines changed

crates/reqwest/src/lib.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,13 +43,15 @@ pub use http_cache_stream::X_CACHE_LOOKUP;
4343
use http_cache_stream::http::Extensions;
4444
use http_cache_stream::http::Uri;
4545
use http_cache_stream::http_body::Frame;
46+
pub use http_cache_stream::semantics;
4647
pub use http_cache_stream::semantics::CacheOptions;
4748
pub use http_cache_stream::storage;
4849
pub use http_cache_stream::storage::CacheStorage;
4950
use reqwest::Body;
5051
use reqwest::Request;
5152
use reqwest::Response;
5253
use reqwest::ResponseBuilderExt;
54+
use reqwest::header::HeaderMap;
5355
use reqwest_middleware::Next;
5456

5557
pin_project_lite::pin_project! {
@@ -152,6 +154,24 @@ impl<S: CacheStorage> Cache<S> {
152154
Self(http_cache_stream::Cache::new_with_options(storage, options))
153155
}
154156

157+
/// Sets the revalidation hook to use.
158+
///
159+
/// The hook is provided the original request and a mutable header map
160+
/// containing headers explicitly set for the revalidation request.
161+
///
162+
/// For example, a hook may alter the revalidation headers to update an
163+
/// `Authorization` header based on the headers used for revalidation.
164+
///
165+
/// If the hook returns an error, the error is propagated out as the result
166+
/// of the original request.
167+
pub fn with_revalidation_hook(
168+
mut self,
169+
hook: impl Fn(&dyn semantics::RequestLike, &mut HeaderMap) -> Result<()> + Send + Sync + 'static,
170+
) -> Self {
171+
self.0 = self.0.with_revalidation_hook(hook);
172+
self
173+
}
174+
155175
/// Gets the underlying storage of the cache.
156176
pub fn storage(&self) -> &S {
157177
self.0.storage()

src/cache.rs

Lines changed: 54 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -291,12 +291,31 @@ impl http_cache_semantics::RequestLike for RequestLike {
291291
}
292292
}
293293

294+
/// Represents a revalidation hook.
295+
///
296+
/// The hook is provided the original request and a mutable header map
297+
/// containing headers explicitly set for the revalidation request.
298+
///
299+
/// For example, a hook may alter the revalidation headers to update an
300+
/// `Authorization` header based on the headers used for revalidation.
301+
///
302+
/// If the hook returns an error, the error is propagated out as the result of
303+
/// the original request.
304+
type RevalidationHook = dyn Fn(&dyn http_cache_semantics::RequestLike, &mut HeaderMap) -> Result<()>
305+
+ Send
306+
+ Sync
307+
+ 'static;
308+
294309
/// Implement a HTTP cache.
295310
pub struct Cache<S> {
296311
/// The cache storage.
297312
storage: S,
298313
/// The cache options to use.
299314
options: CacheOptions,
315+
/// Stores the revalidation hook.
316+
///
317+
/// This is `None` if no revalidation hook is used.
318+
hook: Option<Box<RevalidationHook>>,
300319
}
301320

302321
impl<S> Cache<S>
@@ -314,12 +333,38 @@ where
314333
shared: false,
315334
..Default::default()
316335
},
336+
hook: None,
317337
}
318338
}
319339

320340
/// Construct a new cache with the given storage and options.
321341
pub fn new_with_options(storage: S, options: CacheOptions) -> Self {
322-
Self { storage, options }
342+
Self {
343+
storage,
344+
options,
345+
hook: None,
346+
}
347+
}
348+
349+
/// Sets the revalidation hook to use.
350+
///
351+
/// The hook is provided the original request and a mutable header map
352+
/// containing headers explicitly set for the revalidation request.
353+
///
354+
/// For example, a hook may alter the revalidation headers to update an
355+
/// `Authorization` header based on the headers used for revalidation.
356+
///
357+
/// If the hook returns an error, the error is propagated out as the result
358+
/// of the original request.
359+
pub fn with_revalidation_hook(
360+
mut self,
361+
hook: impl Fn(&dyn http_cache_semantics::RequestLike, &mut HeaderMap) -> Result<()>
362+
+ Send
363+
+ Sync
364+
+ 'static,
365+
) -> Self {
366+
self.hook = Some(Box::new(hook));
367+
self
323368
}
324369

325370
/// Gets the storage used by the cache.
@@ -469,7 +514,7 @@ where
469514
) -> Result<Response<Body<B>>> {
470515
let request_like = RequestLike::new(&request);
471516

472-
let headers = match stored
517+
let mut headers = match stored
473518
.policy
474519
.before_request(&request_like, SystemTime::now())
475520
{
@@ -511,6 +556,13 @@ where
511556
"response is stale: sending request upstream for validation"
512557
);
513558

559+
// Invoke the revalidation hook if the request will be different headers
560+
if let Some(headers) = &mut headers
561+
&& let Some(hook) = &self.hook
562+
{
563+
hook(&request_like, headers)?;
564+
}
565+
514566
// Revalidate the request
515567
match request.send(headers).await {
516568
Ok(response) if response.status().is_success() => {

0 commit comments

Comments
 (0)