Skip to content

Commit

Permalink
ETag middleware draft
Browse files Browse the repository at this point in the history
  • Loading branch information
syphar committed Jun 17, 2023
1 parent 058ea55 commit f030bd1
Show file tree
Hide file tree
Showing 4 changed files with 71 additions and 2 deletions.
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,8 @@ uuid = "1.1.2"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
memmap2 = "0.5.0"
md-5 = "0.10.5"
hex = "0.4.3"

# axum dependencies
axum = { version = "0.6.1", features = ["headers"]}
Expand Down
68 changes: 66 additions & 2 deletions src/web/cache.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,17 @@
use crate::config::Config;
use crate::{config::Config, web::error::AxumResult};
use anyhow::Context as _;
use axum::{
http::Request as AxumHttpRequest, middleware::Next, response::Response as AxumResponse,
headers::{ETag, HeaderMapExt, IfNoneMatch},
http::{Request as AxumHttpRequest, StatusCode},
middleware::Next,
response::IntoResponse,
response::Response as AxumResponse,
TypedHeader,
};
use http::{header::CACHE_CONTROL, HeaderValue};
use md5::{Digest, Md5};
use std::sync::Arc;
use tracing::trace;

pub static NO_CACHING: HeaderValue = HeaderValue::from_static("max-age=0");

Expand Down Expand Up @@ -101,6 +109,62 @@ pub(crate) async fn cache_middleware<B>(req: AxumHttpRequest<B>, next: Next<B>)
response
}

pub(crate) async fn etag_middleware<B>(
if_none_match: Option<TypedHeader<IfNoneMatch>>,
req: AxumHttpRequest<B>,
next: Next<B>,
) -> AxumResult<AxumResponse> {
let if_none_match = if_none_match.map(|header| header.0);
let response = next.run(req).await;

let (mut parts, body) = response.into_parts();

if let Some(etag) = parts.headers.typed_get::<ETag>() {
// when the handler already set an ETag, use that one.
// can be used to generate an ETag not based on the content, but
// on other metadata.
// We also don't need to hash the response body then.
if let Some(if_none_match) = if_none_match {
if !if_none_match.precondition_passes(&etag) {
return Ok((StatusCode::NOT_MODIFIED, parts).into_response());
}
}
Ok((parts, body).into_response())
} else {
// when the response doesn't have an ETag, we want to generate one,
// which we need the body for.
let bytes = hyper::body::to_bytes(body)
.await
.context("could not read response body")?;

if bytes.is_empty() {
Ok(parts.into_response())
} else {
let mut hasher = Md5::new();
hasher.update(&bytes);
let result = hasher.finalize();

let etag: ETag = format!("\"{}\"", hex::encode(result)).parse().expect(
"this is safe since the format! & hex::encode always is a valid ETag header",
);
parts.headers.typed_insert(etag.clone());

if let Some(if_none_match) = if_none_match {
let etag_matches_if_none_match = !if_none_match.precondition_passes(&etag);

trace!(?etag, etag_matches_if_none_match, ?if_none_match);

if etag_matches_if_none_match {
return Ok((StatusCode::NOT_MODIFIED, parts).into_response());
}
}
trace!(?etag, "no if-none-match header or no match");

Ok((parts, bytes).into_response())
}
}
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down
1 change: 1 addition & 0 deletions src/web/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,7 @@ fn apply_middleware(
.layer(Extension(context.storage()?))
.layer(Extension(context.repository_stats_updater()?))
.layer(option_layer(template_data.map(Extension)))
.layer(middleware::from_fn(cache::etag_middleware))
.layer(middleware::from_fn(csp::csp_middleware))
.layer(option_layer(has_templates.then_some(middleware::from_fn(
page::web_page::render_templates_middleware,
Expand Down

0 comments on commit f030bd1

Please sign in to comment.