Skip to content

Tweak CachePolicy construction #15

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
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
93 changes: 77 additions & 16 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,13 @@ impl Default for CacheOptions {
}
}

/// A [`CachePolicy`] that MUST NOT have either the request or response stored.
///
/// Policies returned as `NotStorable` are policies where [`CachePolicy::is_storable()`] returns
/// `true`.
#[derive(Debug, Clone)]
pub struct NotStorable(pub CachePolicy);

/// Identifies when responses can be reused from a cache, taking into account
/// HTTP RFC 7234 rules for user agents and shared caches. It's aware of many
/// tricky details such as the Vary header, proxy revalidation, and
Expand All @@ -173,40 +180,94 @@ pub struct CachePolicy {
impl CachePolicy {
/// Cacheability of an HTTP response depends on how it was requested, so
/// both request and response are required to create the policy.
///
/// `response_time` is a timestamp when the response has been received, usually `SystemTime::now()`.
///
/// # Errors
///
/// This constructor returns a [`Result`] to indicate if the cache policy is considered
/// storable. In the common case of disregarding these policies you can just ignore the
/// `Err(_)` case like so
///
/// ```
/// # use http_cache_semantics::CachePolicy;
/// # let req = http::Request::<()>::default();
/// # let res = http::Response::<()>::default();
/// # let now = std::time::SystemTime::now();
/// # let mut cache = std::collections::HashMap::new();
/// # let url = ();
/// if let Ok(policy) = CachePolicy::try_new(&req, &res, now) {
/// cache.insert(url, policy);
/// }
/// ```
///
/// and for the uncommon case of wanting to inspect cache policies regardless of whether or not
/// they should be stored then you can easily merge the `Ok(_)` and `Err(_)` cases together to
/// treat them the same
///
/// ```
/// # use http_cache_semantics::CachePolicy;
/// # let req = http::Request::<()>::default();
/// # let res = http::Response::<()>::default();
/// # let now = std::time::SystemTime::now();
/// # let mut cache = std::collections::HashMap::new();
/// # let url = ();
/// let policy = CachePolicy::try_new(&req, &res, now).unwrap_or_else(|err| err.0);
/// // ... do something with `policy`
/// if policy.is_storable() {
/// cache.insert(url, policy);
/// }
/// ```
#[inline]
pub fn new<Req: RequestLike, Res: ResponseLike>(req: &Req, res: &Res) -> Self {
pub fn try_new<Req: RequestLike, Res: ResponseLike>(
req: &Req,
res: &Res,
response_time: SystemTime,
) -> Result<Self, NotStorable> {
Self::try_new_with_options(req, res, response_time, Default::default())
}

/// Caching with customized behavior. See [`CacheOptions`] for details.
#[inline]
pub fn try_new_with_options<Req: RequestLike, Res: ResponseLike>(
req: &Req,
res: &Res,
response_time: SystemTime,
opts: CacheOptions,
) -> Result<Self, NotStorable> {
let uri = req.uri();
let status = res.status();
let method = req.method().clone();
let res = res.headers().clone();
let req = req.headers().clone();
Self::from_details(
uri,
method,
status,
req,
res,
SystemTime::now(),
Default::default(),
)
let policy = Self::from_details(uri, method, status, req, res, response_time, opts);
if policy.is_storable() {
Ok(policy)
} else {
Err(NotStorable(policy))
}
}

/// Cacheability of an HTTP response depends on how it was requested, so
/// both request and response are required to create the policy.
#[deprecated(note = "replaced with `CachePolicy::try_new`")]
#[inline]
pub fn new<Req: RequestLike, Res: ResponseLike>(req: &Req, res: &Res) -> Self {
Self::try_new(req, res, SystemTime::now()).unwrap_or_else(|n_s| n_s.0)
}

/// Caching with customized behavior. See `CacheOptions` for details.
///
/// `response_time` is a timestamp when the response has been received, usually `SystemTime::now()`.
#[deprecated(note = "replaced with `CachePolicy::try_new_with_options`")]
#[inline]
pub fn new_options<Req: RequestLike, Res: ResponseLike>(
req: &Req,
res: &Res,
response_time: SystemTime,
opts: CacheOptions,
) -> Self {
let uri = req.uri();
let status = res.status();
let method = req.method().clone();
let res = res.headers().clone();
let req = req.headers().clone();
Self::from_details(uri, method, status, req, res, response_time, opts)
Self::try_new_with_options(req, res, response_time, opts).unwrap_or_else(|n_s| n_s.0)
}

fn from_details(
Expand Down
66 changes: 37 additions & 29 deletions tests/okhttp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,8 @@ fn assert_cached(should_put: bool, response_code: u16) {

let request = request_parts(Request::get("/"));

let policy = CachePolicy::new_options(&request, &response, now, options);
let policy = CachePolicy::try_new_with_options(&request, &response, now, options)
.unwrap_or_else(|n_s| n_s.0);

assert_eq!(should_put, policy.is_storable());
}
Expand Down Expand Up @@ -106,7 +107,7 @@ fn test_default_expiration_date_fully_cached_for_less_than_24_hours() {
..Default::default()
};

let policy = CachePolicy::new_options(
let policy = CachePolicy::try_new_with_options(
&request_parts(Request::get("/")),
&response_parts(
Response::builder()
Expand All @@ -115,7 +116,7 @@ fn test_default_expiration_date_fully_cached_for_less_than_24_hours() {
),
now,
options,
);
).unwrap();

assert!(policy.time_to_live(now).as_millis() > 4000);
}
Expand All @@ -128,7 +129,7 @@ fn test_default_expiration_date_fully_cached_for_more_than_24_hours() {
..Default::default()
};

let policy = CachePolicy::new_options(
let policy = CachePolicy::try_new_with_options(
&request_parts(Request::get("/")),
&response_parts(
Response::builder()
Expand All @@ -137,7 +138,7 @@ fn test_default_expiration_date_fully_cached_for_more_than_24_hours() {
),
now,
options,
);
).unwrap();

assert!((policy.time_to_live(now) + policy.age(now)).as_secs() >= 10 * 3600 * 24);
assert!(policy.time_to_live(now).as_millis() + 1000 >= 5 * 3600 * 24);
Expand All @@ -159,7 +160,7 @@ fn test_max_age_in_the_past_with_date_header_but_no_last_modified_header() {
.header(header::AGE, 120)
.header(header::CACHE_CONTROL, "max-age=60"),
);
let policy = CachePolicy::new_options(&request, &response, now, options);
let policy = CachePolicy::try_new_with_options(&request, &response, now, options).unwrap();

assert!(policy.is_stale(now));
}
Expand All @@ -172,7 +173,7 @@ fn test_max_age_preferred_over_lower_shared_max_age() {
..Default::default()
};

let policy = CachePolicy::new_options(
let policy = CachePolicy::try_new_with_options(
&request_parts(Request::builder()),
&response_parts(
Response::builder()
Expand All @@ -181,7 +182,7 @@ fn test_max_age_preferred_over_lower_shared_max_age() {
),
now,
options,
);
).unwrap();

assert_eq!((policy.time_to_live(now) + policy.age(now)).as_secs(), 180);
}
Expand All @@ -200,7 +201,7 @@ fn test_max_age_preferred_over_higher_max_age() {
.header(header::AGE, 3 * 60)
.header(header::CACHE_CONTROL, "s-maxage=60, max-age=180"),
);
let policy = CachePolicy::new_options(&request, &response, now, options);
let policy = CachePolicy::try_new_with_options(&request, &response, now, options).unwrap();

assert!(policy.is_stale(now));
}
Expand All @@ -219,7 +220,8 @@ fn request_method_not_cached(method: &str) {
let response =
response_parts(Response::builder().header(header::EXPIRES, format_date(1, 3600)));

let policy = CachePolicy::new_options(&request, &response, now, options);
let policy =
CachePolicy::try_new_with_options(&request, &response, now, options).unwrap_err().0;

assert!(policy.is_stale(now));
}
Expand Down Expand Up @@ -252,7 +254,7 @@ fn test_etag_and_expiration_date_in_the_future() {
..Default::default()
};

let policy = CachePolicy::new_options(
let policy = CachePolicy::try_new_with_options(
&request_parts(Request::builder()),
&response_parts(
Response::builder()
Expand All @@ -262,7 +264,7 @@ fn test_etag_and_expiration_date_in_the_future() {
),
now,
options,
);
).unwrap();

assert!(policy.time_to_live(now).as_millis() > 0);
}
Expand All @@ -275,14 +277,12 @@ fn test_client_side_no_store() {
..Default::default()
};

let policy = CachePolicy::new_options(
CachePolicy::try_new_with_options(
&request_parts(Request::builder().header(header::CACHE_CONTROL, "no-store")),
&response_parts(Response::builder().header(header::CACHE_CONTROL, "max-age=60")),
now,
options,
);

assert!(!policy.is_storable());
).unwrap_err();
}

#[test]
Expand All @@ -297,15 +297,15 @@ fn test_request_max_age() {
.header(header::EXPIRES, format_date(1, 3600)),
);

let policy = CachePolicy::new_options(
let policy = CachePolicy::try_new_with_options(
&first_request,
&response,
now,
CacheOptions {
shared: false,
..Default::default()
},
);
).unwrap();

assert_eq!(policy.age(now).as_secs(), 60);
assert_eq!(policy.time_to_live(now).as_secs(), 3000);
Expand Down Expand Up @@ -335,7 +335,7 @@ fn test_request_min_fresh() {
let response = response_parts(Response::builder().header(header::CACHE_CONTROL, "max-age=60"));

let policy =
CachePolicy::new_options(&request_parts(Request::builder()), &response, now, options);
CachePolicy::try_new_with_options(&request_parts(Request::builder()), &response, now, options).unwrap();

assert!(!policy.is_stale(now));

Expand Down Expand Up @@ -368,8 +368,12 @@ fn test_request_max_stale() {
.header(header::AGE, 4 * 60),
);

let policy =
CachePolicy::new_options(&request_parts(Request::builder()), &response, now, options);
let policy = CachePolicy::try_new_with_options(
&request_parts(Request::builder()),
&response,
now,
options,
).unwrap();

assert!(policy.is_stale(now));

Expand Down Expand Up @@ -410,8 +414,12 @@ fn test_request_max_stale_not_honored_with_must_revalidate() {
.header(header::AGE, 4 * 60),
);

let policy =
CachePolicy::new_options(&request_parts(Request::builder()), &response, now, options);
let policy = CachePolicy::try_new_with_options(
&request_parts(Request::builder()),
&response,
now,
options,
).unwrap();

assert!(policy.is_stale(now));

Expand All @@ -433,14 +441,15 @@ fn test_request_max_stale_not_honored_with_must_revalidate() {
#[test]
fn test_get_headers_deletes_cached_100_level_warnings() {
let now = SystemTime::now();
let policy = CachePolicy::new(
let policy = CachePolicy::try_new(
&request_parts(Request::builder().header("cache-control", "max-stale")),
&response_parts(
Response::builder()
.header("cache-control", "immutable")
.header(header::WARNING, "199 test danger, 200 ok ok"),
),
);
now,
).unwrap();

assert_eq!(
"200 ok ok",
Expand All @@ -451,17 +460,16 @@ fn test_get_headers_deletes_cached_100_level_warnings() {

#[test]
fn test_do_not_cache_partial_response() {
let policy = CachePolicy::new(
CachePolicy::try_new(
&request_parts(Request::builder()),
&response_parts(
Response::builder()
.status(206)
.header(header::CONTENT_RANGE, "bytes 100-100/200")
.header(header::CACHE_CONTROL, "max-age=60"),
),
);

assert!(!policy.is_storable());
SystemTime::now(),
).unwrap_err();
}

fn format_date(delta: i64, unit: i64) -> String {
Expand Down
Loading