Skip to content

Commit b0abca9

Browse files
committed
custom impl
1 parent 304ec51 commit b0abca9

File tree

7 files changed

+89
-19
lines changed

7 files changed

+89
-19
lines changed

clippy.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,8 @@ reason = """
1414
[[disallowed-types]]
1515
path = "semver::Version"
1616
reason = "use our own custom db::types::version::Version so you can use it with sqlx"
17+
18+
[[disallowed-types]]
19+
path = "axum_extra::headers::IfNoneMatch"
20+
reason = "use our own custom web::headers::IfNoneMatch for sane behaviour with missing headers"
21+

src/web/file.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
//! Database based file handler
22
3-
use super::cache::CachePolicy;
3+
use super::{cache::CachePolicy, headers::IfNoneMatch};
44
use crate::{
55
Config,
66
error::Result,
@@ -14,7 +14,7 @@ use axum::{
1414
};
1515
use axum_extra::{
1616
TypedHeader,
17-
headers::{ContentType, IfNoneMatch, LastModified},
17+
headers::{ContentType, LastModified},
1818
};
1919
use std::time::SystemTime;
2020
use tokio_util::io::ReaderStream;

src/web/headers/if_none_match.rs

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
//! Adapted version of `headers::IfNoneMatch`.
2+
//!
3+
//! The combination of `TypedHeader` and `IfNoneMatch` works in odd ways.
4+
//! They are built in a way that a _missing_ `If-None-Match` header will lead to:
5+
//!
6+
//! 1. extractor with `TypedHeader<IfNoneMatch>` returning `IfNoneMatch("")`
7+
//! 2. extractor with `Option<TypedHeader<IfNoneMatch>>` returning `Some(IfNoneMatch(""))`
8+
//!
9+
//! Where I would expect:
10+
//! 1. a failure because of the missing header
11+
//! 2. `None` for the missing header
12+
//!
13+
//! This could be solved by either adapting `TypedHeader` or `IfNoneMatch`, I'm not sure which is
14+
//! right.
15+
//!
16+
//! Some reading material for those interested:
17+
//! * https://github.com/hyperium/headers/issues/204
18+
//! * https://github.com/hyperium/headers/pull/165
19+
//! * https://github.com/tokio-rs/axum/issues/1781
20+
//! * https://github.com/tokio-rs/axum/pull/1810
21+
//! * https://github.com/tokio-rs/axum/pull/2475
22+
//!
23+
//! Right now I feel like adapting `IfNoneMatch` is the "most correct-ish" option.
24+
25+
#[allow(clippy::disallowed_types)]
26+
mod header_impl {
27+
use axum_extra::headers::{self, ETag, Header, IfNoneMatch as OriginalIfNoneMatch};
28+
use derive_more::Deref;
29+
30+
#[derive(Debug, Clone, PartialEq, Deref)]
31+
pub(crate) struct IfNoneMatch(pub axum_extra::headers::IfNoneMatch);
32+
33+
impl Header for IfNoneMatch {
34+
fn name() -> &'static http::HeaderName {
35+
OriginalIfNoneMatch::name()
36+
}
37+
38+
fn decode<'i, I>(values: &mut I) -> Result<Self, headers::Error>
39+
where
40+
Self: Sized,
41+
I: Iterator<Item = &'i http::HeaderValue>,
42+
{
43+
let mut values = values.peekable();
44+
// NOTE: this is the difference to the original implementation.
45+
// When there is no header set, I want the decoding to fail.
46+
// This makes Option<TypedHeader<H>> return `None`, and also matches
47+
// most other header implementations.
48+
if values.peek().is_none() {
49+
Err(headers::Error::invalid())
50+
} else {
51+
OriginalIfNoneMatch::decode(&mut values).map(IfNoneMatch)
52+
}
53+
}
54+
55+
fn encode<E: Extend<http::HeaderValue>>(&self, values: &mut E) {
56+
self.0.encode(values)
57+
}
58+
}
59+
60+
impl From<ETag> for IfNoneMatch {
61+
fn from(value: ETag) -> Self {
62+
Self(value.into())
63+
}
64+
}
65+
}
66+
67+
pub(crate) use header_impl::IfNoneMatch;

src/web/headers/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
mod canonical_url;
2+
mod if_none_match;
23
mod surrogate_key;
34

45
use axum_extra::headers::ETag;
56
pub use canonical_url::CanonicalUrl;
67
use http::HeaderName;
8+
pub(crate) use if_none_match::IfNoneMatch;
79
pub use surrogate_key::{SURROGATE_KEY, SurrogateKey, SurrogateKeys};
810

911
/// Fastly's Surrogate-Control header

src/web/rustdoc.rs

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ use crate::{
1919
rustdoc::{PageKind, RustdocParams},
2020
},
2121
file::StreamingFile,
22+
headers::IfNoneMatch,
2223
match_version,
2324
metrics::WebMetrics,
2425
page::{
@@ -35,10 +36,7 @@ use axum::{
3536
http::StatusCode,
3637
response::{IntoResponse, Response as AxumResponse},
3738
};
38-
use axum_extra::{
39-
headers::{ContentType, IfNoneMatch},
40-
typed_header::TypedHeader,
41-
};
39+
use axum_extra::{headers::ContentType, typed_header::TypedHeader};
4240
use http::{Uri, uri::Authority};
4341
use serde::Deserialize;
4442
use std::{

src/web/source.rs

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,17 +13,15 @@ use crate::{
1313
},
1414
file::File as DbFile,
1515
headers::CanonicalUrl,
16+
headers::IfNoneMatch,
1617
match_version,
1718
page::templates::{RenderBrands, RenderRegular, RenderSolid, filters},
1819
},
1920
};
2021
use anyhow::{Context as _, Result};
2122
use askama::Template;
2223
use axum::{Extension, response::IntoResponse};
23-
use axum_extra::{
24-
TypedHeader,
25-
headers::{HeaderMapExt, IfNoneMatch},
26-
};
24+
use axum_extra::{TypedHeader, headers::HeaderMapExt};
2725
use mime::Mime;
2826
use std::{cmp::Ordering, sync::Arc};
2927
use tracing::instrument;

src/web/statics.rs

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
use super::{cache::CachePolicy, metrics::request_recorder, routes::get_static};
1+
use super::{
2+
cache::CachePolicy, headers::IfNoneMatch, metrics::request_recorder, routes::get_static,
3+
};
24
use axum::{
35
Router as AxumRouter,
46
extract::{Extension, Request},
@@ -8,7 +10,7 @@ use axum::{
810
routing::get_service,
911
};
1012
use axum_extra::{
11-
headers::{ETag, HeaderMapExt as _, HeaderValue, IfNoneMatch},
13+
headers::{ETag, HeaderMapExt as _, HeaderValue},
1214
typed_header::TypedHeader,
1315
};
1416
use http::{Method, StatusCode, Uri};
@@ -137,25 +139,23 @@ pub(crate) fn build_static_router() -> AxumRouter {
137139

138140
#[cfg(test)]
139141
mod tests {
140-
use super::{STYLE_CSS, VENDORED_CSS};
142+
use super::*;
141143
use crate::{
142144
test::{AxumResponseTestExt, AxumRouterTestExt, async_wrapper},
143-
web::{cache::CachePolicy, headers::compute_etag},
145+
web::headers::compute_etag,
144146
};
145-
use axum::{Router, body::Body, extract::Request, response::Response as AxumResponse};
146-
use axum_extra::headers::{ETag, HeaderMapExt as _, IfNoneMatch};
147+
use axum::{Router, body::Body};
147148
use http::{
148149
HeaderMap,
149150
header::{CONTENT_LENGTH, CONTENT_TYPE, ETAG},
150151
};
151-
use reqwest::StatusCode;
152152
use std::fs;
153153
use test_case::test_case;
154154
use tower::ServiceExt as _;
155155

156156
const STATIC_SEARCH_PATHS: &[&str] = &["static", "vendor"];
157157

158-
fn content_length(resp: &AxumResponse) -> u64 {
158+
fn content_length(resp: &Response) -> u64 {
159159
resp.headers()
160160
.get(CONTENT_LENGTH)
161161
.expect("content-length header")
@@ -165,7 +165,7 @@ mod tests {
165165
.unwrap()
166166
}
167167

168-
fn etag(resp: &AxumResponse) -> ETag {
168+
fn etag(resp: &Response) -> ETag {
169169
let etag = resp.headers().get(ETAG).unwrap();
170170
etag.to_str().unwrap().parse().unwrap()
171171
}

0 commit comments

Comments
 (0)