Skip to main content

axum_conditional_requests/
lib.rs

1use std::convert::Infallible;
2
3use axum_core::{
4    extract::OptionalFromRequestParts,
5    response::{AppendHeaders, IntoResponse, IntoResponseParts, ResponseParts},
6};
7use chrono::{DateTime, TimeZone, Timelike};
8use http::{
9    HeaderValue, Method, StatusCode,
10    header::{IF_MODIFIED_SINCE, IF_NONE_MATCH, LAST_MODIFIED},
11    request::Parts,
12};
13
14use crate::timezone::HttpGmt;
15mod timezone;
16
17#[derive(Debug)]
18pub struct LastModified(DateTime<HttpGmt>);
19impl IntoResponseParts for LastModified {
20    type Error = Infallible;
21
22    fn into_response_parts(self, mut res: ResponseParts) -> Result<ResponseParts, Self::Error> {
23        res.headers_mut()
24            .insert(LAST_MODIFIED, HeaderValue::from_str(&self.0.to_rfc2822()).unwrap());
25        Ok(res)
26    }
27}
28impl IntoResponse for LastModified {
29    fn into_response(self) -> axum_core::response::Response {
30        AppendHeaders([(LAST_MODIFIED, HeaderValue::from_str(&self.0.to_rfc2822()).unwrap())]).into_response()
31    }
32}
33
34#[derive(Clone, Copy, Debug)]
35/// Only implements OptionalFromRequestParts to force the user
36/// to handle the case of ignoring this header,
37/// as extracting this header is not supposed to error out, but MUST instead be ignored.
38pub struct IfModifiedSince(DateTime<HttpGmt>);
39impl<S: Send + Sync> OptionalFromRequestParts<S> for IfModifiedSince {
40    type Rejection = Infallible;
41    async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Option<Self>, Self::Rejection> {
42        if parts.headers.contains_key(IF_NONE_MATCH) {
43            return Ok(None);
44        }
45        if !matches!(parts.method, Method::GET | Method::HEAD) {
46            #[cfg(feature = "tracing")]
47            tracing::warn!("client sent if-modified-since on a non-GET/HEAD request");
48            return Ok(None);
49        }
50        let Some(header) = parts.headers.get(IF_MODIFIED_SINCE) else {
51            return Ok(None);
52        };
53        let Ok(header_str) = header.to_str() else {
54            #[cfg(feature = "tracing")]
55            tracing::warn!("Client sent non-utf8 if-modified-since header");
56            return Ok(None);
57        };
58        let Ok(time) = DateTime::parse_from_rfc2822(header_str) else {
59            #[cfg(feature = "tracing")]
60            tracing::warn!("Client sent invalid date in if-modified-since header");
61            return Ok(None);
62        };
63        Ok(Some(Self(HttpGmt.from_local_datetime(&time.naive_utc()).unwrap())))
64    }
65}
66
67#[derive(Debug)]
68pub struct MaybeUnmodified<T> {
69    last_modified: DateTime<HttpGmt>,
70    payload: MaybeModifiedPayload<T>,
71}
72impl<T> MaybeUnmodified<T> {
73    pub fn from_header<Tz: TimeZone>(header: Option<IfModifiedSince>, last_modified: DateTime<Tz>, payload: T) -> Self {
74        let last_modified: DateTime<HttpGmt> = HttpGmt.from_local_datetime(&last_modified.naive_utc()).unwrap();
75        let Some(IfModifiedSince(header_time)) = header else {
76            return MaybeUnmodified {
77                last_modified,
78                payload: MaybeModifiedPayload::New(payload),
79            };
80        };
81        let last_modified = last_modified.with_nanosecond(0).unwrap();
82        let header_time = header_time.with_nanosecond(0).unwrap();
83        let payload = if last_modified <= header_time {
84            MaybeModifiedPayload::NotModified
85        } else {
86            MaybeModifiedPayload::New(payload)
87        };
88        MaybeUnmodified { last_modified, payload }
89    }
90}
91impl<T: IntoResponse> IntoResponse for MaybeUnmodified<T> {
92    fn into_response(self) -> axum_core::response::Response {
93        let header = LastModified(self.last_modified);
94        match self.payload {
95            MaybeModifiedPayload::NotModified => (StatusCode::NOT_MODIFIED, header).into_response(),
96            MaybeModifiedPayload::New(p) => (header, p).into_response(),
97        }
98    }
99}
100
101#[derive(Debug)]
102enum MaybeModifiedPayload<T> {
103    NotModified,
104    New(T),
105}