1use axum::extract::FromRequestParts;
8use axum::http::request::Parts;
9use axum::http::{HeaderValue, StatusCode, header};
10use axum::response::{IntoResponse, Response};
11
12pub struct IfNoneMatch(pub Option<String>);
16
17impl<S> FromRequestParts<S> for IfNoneMatch
18where
19 S: Send + Sync,
20{
21 type Rejection = std::convert::Infallible;
22
23 async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
24 let value = parts
25 .headers
26 .get(header::IF_NONE_MATCH)
27 .and_then(|v| v.to_str().ok())
28 .map(ToOwned::to_owned);
29 Ok(Self(value))
30 }
31}
32
33#[expect(
37 clippy::expect_used,
38 reason = "ETag value is from trusted internal source"
39)]
40pub fn conditional(etag: &str, if_none_match: &IfNoneMatch) -> Option<Response> {
41 let client_etag = if_none_match.0.as_deref()?;
42 if client_etag == etag {
43 let mut response = StatusCode::NOT_MODIFIED.into_response();
44 response.headers_mut().insert(
45 header::ETAG,
46 HeaderValue::from_str(etag).expect("valid ETag header value"),
47 );
48 Some(response)
49 } else {
50 None
51 }
52}
53
54#[expect(
56 clippy::expect_used,
57 reason = "ETag value is from trusted internal source"
58)]
59pub fn with_etag<T: IntoResponse>(response: T, etag: &str) -> Response {
60 let mut response = response.into_response();
61 response.headers_mut().insert(
62 header::ETAG,
63 HeaderValue::from_str(etag).expect("valid ETag header value"),
64 );
65 response
66}
67
68#[cfg(test)]
69mod tests {
70 use super::*;
71
72 #[test]
73 fn conditional_returns_304_on_match() {
74 let inm = IfNoneMatch(Some("\"abc123\"".to_owned()));
75 let resp = conditional("\"abc123\"", &inm);
76 assert!(resp.is_some());
77 let resp = resp.unwrap();
78 assert_eq!(resp.status(), StatusCode::NOT_MODIFIED);
79 assert_eq!(
80 resp.headers().get(header::ETAG).unwrap().to_str().unwrap(),
81 "\"abc123\""
82 );
83 }
84
85 #[test]
86 fn conditional_returns_none_on_mismatch() {
87 let inm = IfNoneMatch(Some("\"old\"".to_owned()));
88 assert!(conditional("\"new\"", &inm).is_none());
89 }
90
91 #[test]
92 fn conditional_returns_none_when_header_absent() {
93 let inm = IfNoneMatch(None);
94 assert!(conditional("\"abc\"", &inm).is_none());
95 }
96
97 #[test]
98 fn with_etag_sets_header() {
99 let resp = with_etag("hello", "\"tag1\"");
100 assert_eq!(
101 resp.headers().get(header::ETAG).unwrap().to_str().unwrap(),
102 "\"tag1\""
103 );
104 }
105}