Skip to main content

tower_http_cache/codec/
mod.rs

1//! Serialization of cached entries, and the on-the-wire format used by the
2//! shared backends.
3//!
4//! Two layers are involved, and they are deliberately separate:
5//!
6//! * A [`CacheCodec`] turns a [`CacheEntry`] into bytes and back. The default
7//!   is [`PostcardCodec`]. A codec knows nothing about expiry.
8//! * The [`envelope`] wraps that payload in a 21-byte versioned header that
9//!   carries the magic, the format version, the codec id, and the entry's
10//!   expiry and stale-until timestamps.
11//!
12//! See [`envelope`] for the byte layout. Custom codecs only implement
13//! [`CacheCodec`]; the envelope is applied by the backend.
14
15pub mod envelope;
16#[cfg(feature = "legacy-bincode1-read")]
17pub mod legacy;
18
19use bytes::Bytes;
20use http::{StatusCode, Version};
21use serde::{Deserialize, Serialize};
22
23use crate::backend::CacheEntry;
24use crate::error::CacheError;
25
26/// Trait representing a serialization strategy for cached entries.
27///
28/// The payload produced by [`encode`](CacheCodec::encode) is stored inside the
29/// [`envelope`], which supplies the timing metadata. Implementations therefore
30/// only need to round-trip the entry itself.
31pub trait CacheCodec: Send + Sync + Clone + 'static {
32    /// Identifies this codec in byte 4 of the [`envelope`] header.
33    ///
34    /// Entries are only decoded by a codec whose `CODEC_ID` matches the byte
35    /// recorded when they were written; a mismatch is reported as a miss rather
36    /// than decoded with the wrong codec.
37    ///
38    /// `0x00..=0x7F` is reserved for this crate (`0x01` is
39    /// [`PostcardCodec`]). `0x80..=0xFF` is free for downstream codecs, and
40    /// the default value is [`envelope::CODEC_USER`] (`0x80`).
41    const CODEC_ID: u8 = envelope::CODEC_USER;
42
43    fn encode(&self, entry: &CacheEntry) -> Result<Vec<u8>, CacheError>;
44    fn decode(&self, bytes: &[u8]) -> Result<CacheEntry, CacheError>;
45}
46
47/// Default [`CacheCodec`] implementation, backed by [`postcard`].
48///
49/// Replaces the `bincode`-backed codec used up to 0.5.x. The payload carries
50/// `tags`, which the previous codec silently dropped.
51#[derive(Clone, Default)]
52pub struct PostcardCodec;
53
54/// Former name of [`PostcardCodec`].
55#[deprecated(
56    since = "0.6.0",
57    note = "renamed to PostcardCodec; the default wire format is now postcard inside a versioned envelope, not bare bincode"
58)]
59pub type BincodeCodec = PostcardCodec;
60
61#[derive(Serialize, Deserialize)]
62struct StoredEntry {
63    status: u16,
64    version: u8,
65    headers: Vec<(String, Vec<u8>)>,
66    body: Vec<u8>,
67    tags: Option<Vec<String>>,
68}
69
70impl CacheCodec for PostcardCodec {
71    const CODEC_ID: u8 = envelope::CODEC_POSTCARD;
72
73    fn encode(&self, entry: &CacheEntry) -> Result<Vec<u8>, CacheError> {
74        let stored = StoredEntry {
75            status: entry.status.as_u16(),
76            version: version_to_u8(entry.version),
77            headers: entry.headers.clone(),
78            body: entry.body.to_vec(),
79            tags: entry.tags.clone(),
80        };
81
82        postcard::to_allocvec(&stored).map_err(|err| CacheError::Backend(err.to_string()))
83    }
84
85    fn decode(&self, bytes: &[u8]) -> Result<CacheEntry, CacheError> {
86        let stored: StoredEntry =
87            postcard::from_bytes(bytes).map_err(|err| CacheError::Backend(err.to_string()))?;
88        // Deliberately a struct literal rather than `CacheEntry::new`, which
89        // hardcodes `tags: None`. Routing through it is what dropped tags in
90        // 0.5.x.
91        Ok(CacheEntry {
92            status: StatusCode::from_u16(stored.status)
93                .map_err(|err| CacheError::Backend(err.to_string()))?,
94            version: version_from_u8(stored.version)?,
95            headers: stored.headers,
96            body: Bytes::from(stored.body),
97            tags: stored.tags,
98        })
99    }
100}
101
102pub(crate) fn version_to_u8(version: Version) -> u8 {
103    match version {
104        Version::HTTP_09 => 0,
105        Version::HTTP_10 => 1,
106        Version::HTTP_11 => 2,
107        Version::HTTP_2 => 3,
108        Version::HTTP_3 => 4,
109        _ => 2,
110    }
111}
112
113pub(crate) fn version_from_u8(value: u8) -> Result<Version, CacheError> {
114    match value {
115        0 => Ok(Version::HTTP_09),
116        1 => Ok(Version::HTTP_10),
117        2 => Ok(Version::HTTP_11),
118        3 => Ok(Version::HTTP_2),
119        4 => Ok(Version::HTTP_3),
120        _ => Err(CacheError::Backend("unknown HTTP version".into())),
121    }
122}