static_http_cache/lib.rs
1#![doc(html_root_url = "https://docs.rs/static_http_cache/0.3.0")]
2//! Introduction
3//! ============
4//!
5//! `static_http_cache` is a local cache for static HTTP resources.
6//!
7//! This library maintains a cache of HTTP resources
8//! in a local directory you specify.
9//! Whenever you ask it for the contents of a URL,
10//! it will re-use a previously-downloaded copy
11//! if the resource has not changed on the server.
12//! Otherwise,
13//! it will download the new version and use that instead.
14//!
15//! Because it only supports static resources,
16//! `static_http_cache` only sends HTTP `GET` requests.
17//!
18//! `static_http_cache` uses the [Reqwest][rq] crate for HTTP operations,
19//! so it should properly handle HTTPS negotiation
20//! and use the operating-system's certificate store.
21//!
22//! Currently,
23//! `static_http_cache` only uses the `Last-Modified` and `ETag` HTTP headers
24//! to determine when its cached data is out of date.
25//! Therefore,
26//! it's not suitable for general-purpose HTTP caching;
27//! it's best suited for static content like Amazon S3 data,
28//! or Apache or nginx serving up a filesystem directory.
29//!
30//! [rq]: https://crates.io/crates/reqwest
31//!
32//! First Example
33//! =============
34//!
35//! To use this crate, you need to construct a [`Cache`]
36//! then call its [`get`] method:
37//!
38//! extern crate reqwest;
39//! extern crate static_http_cache;
40//!
41//! use std::error::Error;
42//! use std::fs::File;
43//! use std::path::PathBuf;
44//!
45//! fn get_my_resource() -> Result<File, Box<dyn Error>> {
46//! let mut cache = static_http_cache::Cache::new(
47//! PathBuf::from("my_cache_directory"),
48//! reqwest::blocking::Client::new(),
49//! )?;
50//!
51//! cache.get(reqwest::Url::parse("http://example.com/some-resource")?)
52//! }
53//!
54//! For repeated queries in the same program,
55//! you'd probably want to create the `Cache` once
56//! and call `get` repeatedly,
57//! of course.
58//!
59//! [`Cache`]: struct.Cache.html
60//! [`get`]: struct.Cache.html#method.get
61//!
62//! For a complete, minimal example of how to use `static_http_cache`,
63//! see the included [simple example][ex].
64//!
65//! [ex]: https://gitlab.com/Screwtapello/static_http_cache/blob/master/examples/simple.rs
66//!
67//! Capabilities
68//! ============
69//!
70//! Alternative HTTP backends
71//! -------------------------
72//!
73//! Although `static_http_cache` is designed to work with the `reqwest` library,
74//! it will accept any type that implements
75//! the traits in the [`reqwest_mock`] module.
76//! If you want to use it with an alternative HTTP backend,
77//! or if you need to stub out network access for testing purposes,
78//! you can do that.
79//!
80//! [`reqwest_mock`]: reqwest_mock/index.html
81//!
82//! Concurrent cache sharing
83//! ------------------------
84//!
85//! Cache metadata is stored in a SQLite database,
86//! so it's safe to give different threads
87//! (or even different processes)
88//! their own [`Cache`] instance
89//! backed by the same filesystem path.
90//!
91//! Note that while it's *safe* to have multiple things
92//! managing the same cache,
93//! it's not necessarily performant:
94//! a [`Cache`] instance that's downloading a new or updated file
95//! is likely to stall other cache reads or writes
96//! until it's complete.
97
98use std::error;
99use std::fs;
100use std::io;
101use std::path;
102
103use log::{debug, info, warn};
104use rand::distributions::Alphanumeric;
105use rand::{thread_rng, Rng};
106use reqwest::header as rh;
107
108pub mod reqwest_mock;
109
110mod db;
111
112fn make_random_file<P: AsRef<path::Path>>(
113 parent: P,
114) -> Result<(fs::File, path::PathBuf), Box<dyn error::Error>> {
115 let mut rng = thread_rng();
116
117 loop {
118 let new_path = parent.as_ref().join(
119 (0..20)
120 .map(|_| rng.sample(Alphanumeric) as char)
121 .collect::<String>(),
122 );
123
124 match fs::OpenOptions::new()
125 .create_new(true)
126 .write(true)
127 .open(&new_path)
128 {
129 Ok(handle) => return Ok((handle, new_path)),
130 Err(e) => {
131 if e.kind() != io::ErrorKind::AlreadyExists {
132 // An actual error, we'd better report it!
133 return Err(e.into());
134 }
135
136 // Otherwise, we just picked a bad name. Let's go back
137 // around the loop and try again.
138 }
139 };
140 }
141}
142
143fn header_as_string(
144 headers: &rh::HeaderMap,
145 key: &rh::HeaderName,
146) -> Option<String> {
147 headers.get(key).and_then(|value| match value.to_str() {
148 Ok(s) => Some(s.into()),
149 Err(err) => {
150 warn!("Header {} contained weird value: {}", key, err);
151 None
152 }
153 })
154}
155
156/// Represents a local cache of HTTP resources.
157///
158/// Whenever you ask it for the contents of a URL,
159/// it will re-use a previously-downloaded copy
160/// if the resource has not changed on the server.
161/// Otherwise,
162/// it will download the new version and use that instead.
163///
164/// See [an example](index.html#first-example).
165///
166/// [`reqwest_mock::Client`]: reqwest_mock/trait.Client.html
167/// [`Cache`]: struct.Cache.html
168#[derive(Debug, PartialEq, Eq)]
169pub struct Cache<C: reqwest_mock::Client> {
170 root: path::PathBuf,
171 db: db::CacheDB,
172 client: C,
173}
174
175impl<C: reqwest_mock::Client> Cache<C> {
176 /// Returns a Cache that wraps `client` and caches data in `root`.
177 ///
178 /// If the directory `root` does not exist, it will be created.
179 /// If multiple instances share the same `root`
180 /// (concurrently or in series),
181 /// each instance will be able to re-use resources downloaded by
182 /// the others.
183 ///
184 /// For best results,
185 /// choose a `root` that is directly attached to
186 /// the computer running your program,
187 /// such as somewhere inside the `%LOCALAPPDATA%` directory on Windows,
188 /// or the `$XDG_CACHE_HOME` directory on POSIX systems.
189 ///
190 /// `client` should almost certainly be a `reqwest::Client`,
191 /// but you can use any type that implements [`reqwest_mock::Client`]
192 /// if you want to use a different HTTP client library
193 /// or a test double of some kind.
194 ///
195 /// # extern crate reqwest;
196 /// # extern crate static_http_cache;
197 /// # use std::error::Error;
198 /// # use std::fs::File;
199 /// # use std::path::PathBuf;
200 /// # fn get_my_resource() -> Result<(), Box<dyn Error>> {
201 /// let mut cache = static_http_cache::Cache::new(
202 /// PathBuf::from("my_cache_directory"),
203 /// reqwest::blocking::Client::new(),
204 /// )?;
205 /// # Ok(())
206 /// # }
207 ///
208 /// [`reqwest_mock::Client`]: reqwest_mock/trait.Client.html
209 ///
210 /// Errors
211 /// ======
212 ///
213 /// This method may return an error:
214 ///
215 /// - if `root` cannot be created, or cannot be written to
216 /// - if the metadata database cannot be created or cannot be written to
217 /// - if the metadata database is corrupt
218 ///
219 /// In all cases, it should be safe to blow away the entire directory
220 /// and start from scratch.
221 /// It's only cached data, after all.
222 pub fn new(
223 root: path::PathBuf,
224 client: C,
225 ) -> Result<Cache<C>, Box<dyn error::Error>> {
226 fs::DirBuilder::new().recursive(true).create(&root)?;
227
228 let db = db::CacheDB::new(root.join("cache.db"))?;
229
230 Ok(Cache { root, db, client })
231 }
232
233 fn record_response(
234 &mut self,
235 url: reqwest::Url,
236 response: &C::Response,
237 ) -> Result<(fs::File, path::PathBuf, db::Transaction), Box<dyn error::Error>>
238 {
239 use reqwest_mock::HttpResponse;
240
241 let content_dir = self.root.join("content");
242 fs::DirBuilder::new().recursive(true).create(&content_dir)?;
243
244 let (handle, path) = make_random_file(&content_dir)?;
245 let trans = {
246 // We can be sure the relative path is valid UTF-8, because
247 // make_random_file() just generated it from ASCII.
248 let path = path.strip_prefix(&self.root)?.to_str().unwrap().into();
249
250 let last_modified =
251 header_as_string(response.headers(), &rh::LAST_MODIFIED);
252
253 let etag = header_as_string(response.headers(), &rh::ETAG);
254
255 self.db.set(
256 url,
257 db::CacheRecord {
258 path,
259 last_modified,
260 etag,
261 },
262 )?
263 };
264
265 Ok((handle, path, trans))
266 }
267
268 /// Retrieve the content of the given URL.
269 ///
270 /// If we've never seen this URL before,
271 /// we will try to retrieve it
272 /// (with a `GET` request)
273 /// and store its data locally.
274 ///
275 /// If we have seen this URL before, we will ask the server
276 /// whether our cached data is stale.
277 /// If our data is stale,
278 /// we'll download the new version
279 /// and store it locally.
280 /// If our data is fresh,
281 /// we'll re-use the local copy we already have.
282 ///
283 /// If we can't talk to the server to see if our cached data is stale,
284 /// we'll silently re-use the data we have.
285 ///
286 /// Returns a file-handle to the local copy of the data, open for
287 /// reading.
288 ///
289 /// # extern crate reqwest;
290 /// # extern crate static_http_cache;
291 /// # use std::error::Error;
292 /// # use std::fs::File;
293 /// # use std::path::PathBuf;
294 /// # fn get_my_resource() -> Result<(), Box<dyn Error>> {
295 /// # let mut cache = static_http_cache::Cache::new(
296 /// # PathBuf::from("my_cache_directory"),
297 /// # reqwest::blocking::Client::new(),
298 /// # )?;
299 /// let file = cache.get(reqwest::Url::parse("http://example.com/some-resource")?)?;
300 /// # Ok(())
301 /// # }
302 ///
303 /// Errors
304 /// ======
305 ///
306 /// This method may return an error:
307 ///
308 /// - if the cache metadata is corrupt
309 /// - if the requested resource is not cached,
310 /// and we can't connect to/download it
311 /// - if we can't update the cache metadata
312 /// - if the cache metadata points to a local file that no longer exists
313 ///
314 /// After returning a network-related or disk I/O-related error,
315 /// this `Cache` instance should be OK and you may keep using it.
316 /// If it returns a database-related error,
317 /// the on-disk storage *should* be OK,
318 /// so you might want to destroy this `Cache` instance
319 /// and create a new one pointing at the same location.
320 pub fn get(
321 &mut self,
322 mut url: reqwest::Url,
323 ) -> Result<fs::File, Box<dyn error::Error>> {
324 use reqwest::StatusCode;
325 use reqwest_mock::HttpResponse;
326
327 url.set_fragment(None);
328
329 let mut response = match self.db.get(url.clone()) {
330 Ok(db::CacheRecord {
331 path: p,
332 last_modified: lm,
333 etag: et,
334 }) => {
335 // We have a locally-cached copy, let's check whether the
336 // copy on the server has changed.
337 let mut request = reqwest::blocking::Request::new(
338 reqwest::Method::GET,
339 url.clone(),
340 );
341 if let Some(timestamp) = lm {
342 request.headers_mut().append(
343 rh::IF_MODIFIED_SINCE,
344 rh::HeaderValue::from_str(×tamp)?,
345 );
346 }
347 if let Some(etag) = et {
348 request.headers_mut().append(
349 rh::IF_NONE_MATCH,
350 rh::HeaderValue::from_str(&etag)?,
351 );
352 }
353
354 info!("Sending HTTP request: {:?}", request);
355
356 let maybe_validation = self
357 .client
358 .execute(request)
359 .and_then(|resp| resp.error_for_status());
360
361 match maybe_validation {
362 Ok(new_response) => {
363 info!("Got HTTP response: {:?}", new_response);
364
365 // If our existing cached data is still fresh...
366 if new_response.status() == StatusCode::NOT_MODIFIED {
367 // ... let's use it as is.
368 return Ok(fs::File::open(self.root.join(p))?);
369 }
370
371 // Otherwise, we got a new response we need to cache.
372 new_response
373 }
374 Err(e) => {
375 warn!("Could not validate cached response: {}", e);
376
377 // Let's just use the existing data we have.
378 return Ok(fs::File::open(self.root.join(p))?);
379 }
380 }
381 }
382 Err(_) => {
383 // This URL isn't in the cache, or we otherwise can't find it.
384 self.client
385 .execute(reqwest::blocking::Request::new(
386 reqwest::Method::GET,
387 url.clone(),
388 ))?
389 .error_for_status()?
390 }
391 };
392
393 let (mut handle, path, trans) =
394 self.record_response(url.clone(), &response)?;
395
396 let count = io::copy(&mut response, &mut handle)?;
397
398 debug!("Downloaded {} bytes", count);
399
400 trans.commit()?;
401
402 Ok(fs::File::open(path)?)
403 }
404}
405
406#[cfg(test)]
407mod tests {
408 extern crate env_logger;
409 extern crate tempdir;
410
411
412 use reqwest::header as rh;
413
414 use std::io;
415
416 use std::io::Read;
417
418 use super::reqwest_mock::tests as rmt;
419
420 const DATE_ZERO: &str = "Thu, 01 Jan 1970 00:00:00 GMT";
421 const DATE_ONE: &str = "Thu, 01 Jan 1970 00:00:00 GMT";
422
423 fn make_test_cache(
424 client: rmt::FakeClient,
425 ) -> super::Cache<rmt::FakeClient> {
426 super::Cache::new(
427 tempdir::TempDir::new("http-cache-test")
428 .unwrap()
429 .into_path(),
430 client,
431 )
432 .unwrap()
433 }
434
435 #[test]
436 fn initial_request_success() {
437 let _ = env_logger::try_init();
438
439 let url_text = "http://example.com/";
440 let url: reqwest::Url = url_text.parse().unwrap();
441
442 let body = b"hello world";
443
444 let mut c = make_test_cache(rmt::FakeClient::new(
445 url.clone(),
446 rh::HeaderMap::new(),
447 rmt::FakeResponse {
448 status: reqwest::StatusCode::OK,
449 headers: rh::HeaderMap::new(),
450 body: io::Cursor::new(body.as_ref().into()),
451 },
452 ));
453
454 // We should get a file-handle containing the body bytes.
455 let mut res = c.get(url).unwrap();
456 let mut buf = vec![];
457 res.read_to_end(&mut buf).unwrap();
458 assert_eq!(&buf, body);
459 c.client.assert_called();
460 }
461
462 #[test]
463 fn initial_request_failure() {
464 let _ = env_logger::try_init();
465
466 let url: reqwest::Url = "http://example.com/".parse().unwrap();
467 let mut c = make_test_cache(rmt::FakeClient::new(
468 url.clone(),
469 rh::HeaderMap::new(),
470 rmt::FakeResponse {
471 status: reqwest::StatusCode::INTERNAL_SERVER_ERROR,
472 headers: rh::HeaderMap::new(),
473 body: io::Cursor::new(vec![]),
474 },
475 ));
476
477 let err = c.get(url).expect_err("Got a response??");
478 assert_eq!(format!("{}", err), "FakeError");
479 c.client.assert_called();
480 }
481
482 #[test]
483 fn ignore_fragment_in_url() {
484 let _ = env_logger::try_init();
485
486 let url_fragment: reqwest::Url =
487 "http://example.com/#frag".parse().unwrap();
488
489 let mut network_url = url_fragment.clone();
490 network_url.set_fragment(None);
491
492 let mut c = make_test_cache(rmt::FakeClient::new(
493 // We expect the cache to request the URL without the fragment.
494 network_url,
495 rh::HeaderMap::new(),
496 rmt::FakeResponse {
497 status: reqwest::StatusCode::OK,
498 headers: rh::HeaderMap::new(),
499 body: io::Cursor::new(b"hello world"[..].into()),
500 },
501 ));
502
503 // Ask for the URL with the fragment.
504 c.get(url_fragment).unwrap();
505 }
506
507 #[test]
508 fn use_cache_data_if_not_modified_since() {
509 let _ = env_logger::try_init();
510
511 let url: reqwest::Url = "http://example.com/".parse().unwrap();
512 let body = b"hello world";
513
514 // We send a request, and the server responds with the data,
515 // and a "Last-Modified" header.
516 let mut response_headers = rh::HeaderMap::new();
517 response_headers
518 .append(rh::LAST_MODIFIED, rh::HeaderValue::from_static(DATE_ZERO));
519
520 let mut c = make_test_cache(rmt::FakeClient::new(
521 url.clone(),
522 rh::HeaderMap::new(),
523 rmt::FakeResponse {
524 status: reqwest::StatusCode::OK,
525 headers: response_headers.clone(),
526 body: io::Cursor::new(body.as_ref().into()),
527 },
528 ));
529
530 // The response and its last-modified date should now be recorded
531 // in the cache.
532 c.get(url.clone()).unwrap();
533 c.client.assert_called();
534
535 // For the next request, we expect the request to include the
536 // modified date in the "if modified since" header, and we'll give
537 // the "no, it hasn't been modified" response.
538 let mut second_request = rh::HeaderMap::new();
539 second_request.append(
540 rh::IF_MODIFIED_SINCE,
541 rh::HeaderValue::from_static(DATE_ZERO),
542 );
543
544 c.client = rmt::FakeClient::new(
545 url.clone(),
546 second_request,
547 rmt::FakeResponse {
548 status: reqwest::StatusCode::NOT_MODIFIED,
549 headers: response_headers,
550 body: io::Cursor::new(b""[..].into()),
551 },
552 );
553
554 // Now when we make the request, even though the actual response
555 // did not include a body, we should get the complete body from
556 // the local cache.
557 let mut res = c.get(url).unwrap();
558 let mut buf = vec![];
559 res.read_to_end(&mut buf).unwrap();
560 assert_eq!(&buf, body);
561 c.client.assert_called();
562 }
563
564 #[test]
565 fn update_cache_if_modified_since() {
566 let _ = env_logger::try_init();
567
568 let url: reqwest::Url = "http://example.com/".parse().unwrap();
569
570 // We send a request, and the server responds with the data,
571 // and a "Last-Modified" header.
572 let request_1_headers = rh::HeaderMap::new();
573 let mut response_1_headers = rh::HeaderMap::new();
574 response_1_headers
575 .append(rh::LAST_MODIFIED, rh::HeaderValue::from_static(DATE_ZERO));
576
577 let mut c = make_test_cache(rmt::FakeClient::new(
578 url.clone(),
579 request_1_headers,
580 rmt::FakeResponse {
581 status: reqwest::StatusCode::OK,
582 headers: response_1_headers,
583 body: io::Cursor::new(b"hello".as_ref().into()),
584 },
585 ));
586
587 // The response and its last-modified date should now be recorded
588 // in the cache.
589 c.get(url.clone()).unwrap();
590 c.client.assert_called();
591
592 // For the next request, we expect the request to include the
593 // modified date in the "if modified since" header, and we'll give
594 // the "yes, it has been modified" response with a new Last-Modified.
595 let mut request_2_headers = rh::HeaderMap::new();
596 request_2_headers.append(
597 rh::IF_MODIFIED_SINCE,
598 rh::HeaderValue::from_static(DATE_ZERO),
599 );
600 let mut response_2_headers = rh::HeaderMap::new();
601 response_2_headers
602 .append(rh::LAST_MODIFIED, rh::HeaderValue::from_static(DATE_ONE));
603
604 c.client = rmt::FakeClient::new(
605 url.clone(),
606 request_2_headers,
607 rmt::FakeResponse {
608 status: reqwest::StatusCode::OK,
609 headers: response_2_headers,
610 body: io::Cursor::new(b"world".as_ref().into()),
611 },
612 );
613
614 // Now when we make the request, we should get the new body and
615 // ignore what's in the cache.
616 let mut res = c.get(url.clone()).unwrap();
617 let mut buf = vec![];
618 res.read_to_end(&mut buf).unwrap();
619 assert_eq!(&buf, b"world");
620 c.client.assert_called();
621
622 // If we make another request, we should set If-Modified-Since
623 // to match the second response, and be able to return the data from
624 // the second response.
625 let mut request_3_headers = rh::HeaderMap::new();
626 request_3_headers.append(
627 rh::IF_MODIFIED_SINCE,
628 rh::HeaderValue::from_static(DATE_ONE),
629 );
630 let response_3_headers = rh::HeaderMap::new();
631
632 c.client = rmt::FakeClient::new(
633 url.clone(),
634 request_3_headers,
635 rmt::FakeResponse {
636 status: reqwest::StatusCode::NOT_MODIFIED,
637 headers: response_3_headers,
638 body: io::Cursor::new(b"".as_ref().into()),
639 },
640 );
641
642 // Now when we make the request, we should get updated info from the
643 // cache.
644 let mut res = c.get(url).unwrap();
645 let mut buf = vec![];
646 res.read_to_end(&mut buf).unwrap();
647 assert_eq!(&buf, b"world");
648 c.client.assert_called();
649 }
650
651 #[test]
652 fn return_existing_data_on_connection_refused() {
653 let _ = env_logger::try_init();
654
655 let temp_path = tempdir::TempDir::new("http-cache-test")
656 .unwrap()
657 .into_path();
658
659 let url: reqwest::Url = "http://example.com/".parse().unwrap();
660
661 // We send a request, and the server responds with the data,
662 // and a "Last-Modified" header.
663 let request_1_headers = rh::HeaderMap::new();
664 let mut response_1_headers = rh::HeaderMap::new();
665 response_1_headers
666 .append(rh::LAST_MODIFIED, rh::HeaderValue::from_static(DATE_ZERO));
667
668 let mut c = super::Cache::new(
669 temp_path.clone(),
670 rmt::FakeClient::new(
671 url.clone(),
672 request_1_headers,
673 rmt::FakeResponse {
674 status: reqwest::StatusCode::OK,
675 headers: response_1_headers,
676 body: io::Cursor::new(b"hello".as_ref().into()),
677 },
678 ),
679 )
680 .unwrap();
681
682 // The response and its last-modified date should now be recorded
683 // in the cache.
684 c.get(url.clone()).unwrap();
685 c.client.assert_called();
686
687 // If we make second request, we should set If-Modified-Since
688 // to match the first response's Last-Modified.
689 let mut request_2_headers = rh::HeaderMap::new();
690 request_2_headers.append(
691 rh::IF_MODIFIED_SINCE,
692 rh::HeaderValue::from_static(DATE_ZERO),
693 );
694
695 // This time, however, the request will return an error.
696 let mut c = super::Cache::new(
697 temp_path,
698 rmt::BrokenClient::new(url.clone(), request_2_headers, || {
699 rmt::FakeError.into()
700 }),
701 )
702 .unwrap();
703
704 // Now when we request a URL, we should get the cached result.
705 let mut res = c.get(url).unwrap();
706 let mut buf = vec![];
707 res.read_to_end(&mut buf).unwrap();
708 assert_eq!(&buf, b"hello");
709 c.client.assert_called();
710 }
711
712 #[test]
713 fn use_cache_data_if_some_match() {
714 let _ = env_logger::try_init();
715
716 let url: reqwest::Url = "http://example.com/".parse().unwrap();
717 let body = b"hello world";
718
719 // We send a request, and the server responds with the data,
720 // and an "Etag" header.
721 let mut response_headers = rh::HeaderMap::new();
722 response_headers.append(rh::ETAG, rh::HeaderValue::from_static("abcd"));
723
724 let mut c = make_test_cache(rmt::FakeClient::new(
725 url.clone(),
726 rh::HeaderMap::new(),
727 rmt::FakeResponse {
728 status: reqwest::StatusCode::OK,
729 headers: response_headers.clone(),
730 body: io::Cursor::new(body.as_ref().into()),
731 },
732 ));
733
734 // The response and its etag should now be recorded
735 // in the cache.
736 c.get(url.clone()).unwrap();
737 c.client.assert_called();
738
739 // For the next request, we expect the request to include the
740 // etag in the "if none match" header, and we'll give
741 // the "no, it hasn't been modified" response.
742 let mut second_request = rh::HeaderMap::new();
743 second_request
744 .append(rh::IF_NONE_MATCH, rh::HeaderValue::from_static("abcd"));
745
746 c.client = rmt::FakeClient::new(
747 url.clone(),
748 second_request,
749 rmt::FakeResponse {
750 status: reqwest::StatusCode::NOT_MODIFIED,
751 headers: response_headers,
752 body: io::Cursor::new(b""[..].into()),
753 },
754 );
755
756 // Now when we make the request, even though the actual response
757 // did not include a body, we should get the complete body from
758 // the local cache.
759 let mut res = c.get(url).unwrap();
760 let mut buf = vec![];
761 res.read_to_end(&mut buf).unwrap();
762 assert_eq!(&buf, body);
763 c.client.assert_called();
764 }
765
766 #[test]
767 fn update_cache_if_none_match() {
768 let _ = env_logger::try_init();
769
770 let url: reqwest::Url = "http://example.com/".parse().unwrap();
771
772 // We send a request, and the server responds with the data,
773 // and an "ETag" header.
774 let request_1_headers = rh::HeaderMap::new();
775 let mut response_1_headers = rh::HeaderMap::new();
776 response_1_headers
777 .append(rh::ETAG, rh::HeaderValue::from_static("abcd"));
778
779 let mut c = make_test_cache(rmt::FakeClient::new(
780 url.clone(),
781 request_1_headers,
782 rmt::FakeResponse {
783 status: reqwest::StatusCode::OK,
784 headers: response_1_headers,
785 body: io::Cursor::new(b"hello".as_ref().into()),
786 },
787 ));
788
789 // The response and its etag should now be recorded in the cache.
790 c.get(url.clone()).unwrap();
791 c.client.assert_called();
792
793 // For the next request, we expect the request to include the
794 // etag in the "if none match" header, and we'll give
795 // the "yes, it has been modified" response with a new etag.
796 let mut request_2_headers = rh::HeaderMap::new();
797 request_2_headers
798 .append(rh::IF_NONE_MATCH, rh::HeaderValue::from_static("abcd"));
799 let mut response_2_headers = rh::HeaderMap::new();
800 response_2_headers
801 .append(rh::ETAG, rh::HeaderValue::from_static("efgh"));
802
803 c.client = rmt::FakeClient::new(
804 url.clone(),
805 request_2_headers,
806 rmt::FakeResponse {
807 status: reqwest::StatusCode::OK,
808 headers: response_2_headers,
809 body: io::Cursor::new(b"world".as_ref().into()),
810 },
811 );
812
813 // Now when we make the request, we should get the new body and
814 // ignore what's in the cache.
815 let mut res = c.get(url.clone()).unwrap();
816 let mut buf = vec![];
817 res.read_to_end(&mut buf).unwrap();
818 assert_eq!(&buf, b"world");
819 c.client.assert_called();
820
821 // If we make another request, we should set If-None-Match
822 // to match the second response, and be able to return the data from
823 // the second response.
824 let mut request_3_headers = rh::HeaderMap::new();
825 request_3_headers
826 .append(rh::IF_NONE_MATCH, rh::HeaderValue::from_static("efgh"));
827 let response_3_headers = rh::HeaderMap::new();
828
829 c.client = rmt::FakeClient::new(
830 url.clone(),
831 request_3_headers,
832 rmt::FakeResponse {
833 status: reqwest::StatusCode::NOT_MODIFIED,
834 headers: response_3_headers,
835 body: io::Cursor::new(b"".as_ref().into()),
836 },
837 );
838
839 // Now when we make the request, we should get updated info from the
840 // cache.
841 let mut res = c.get(url).unwrap();
842 let mut buf = vec![];
843 res.read_to_end(&mut buf).unwrap();
844 assert_eq!(&buf, b"world");
845 c.client.assert_called();
846 }
847
848 // See also: https://developer.mozilla.org/en-US/docs/Web/HTTP/Caching
849}