axum_conditional_requests/
lib.rs1use 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)]
35pub 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}