1use axum::extract::FromRequestParts;
2use http::request::Parts;
3
4#[derive(Debug, Clone, Copy)]
21pub struct HxRequest(bool);
22
23impl HxRequest {
24 pub fn is_htmx(&self) -> bool {
26 self.0
27 }
28}
29
30impl<S: Send + Sync> FromRequestParts<S> for HxRequest {
31 type Rejection = std::convert::Infallible;
32
33 async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
34 let is_htmx = parts
35 .headers
36 .get("hx-request")
37 .and_then(|v| v.to_str().ok())
38 .is_some_and(|v| v == "true");
39 Ok(HxRequest(is_htmx))
40 }
41}
42
43#[cfg(test)]
44mod tests {
45 use super::*;
46 use axum::extract::FromRequestParts;
47 use http::Request;
48
49 #[tokio::test]
50 async fn detects_htmx_request() {
51 let req = Request::builder()
52 .header("hx-request", "true")
53 .body(())
54 .unwrap();
55 let (mut parts, _) = req.into_parts();
56 let hx = HxRequest::from_request_parts(&mut parts, &())
57 .await
58 .unwrap();
59 assert!(hx.is_htmx());
60 }
61
62 #[tokio::test]
63 async fn detects_non_htmx_request() {
64 let req = Request::builder().body(()).unwrap();
65 let (mut parts, _) = req.into_parts();
66 let hx = HxRequest::from_request_parts(&mut parts, &())
67 .await
68 .unwrap();
69 assert!(!hx.is_htmx());
70 }
71
72 #[tokio::test]
73 async fn hx_request_false_header_is_not_htmx() {
74 let req = Request::builder()
75 .header("hx-request", "false")
76 .body(())
77 .unwrap();
78 let (mut parts, _) = req.into_parts();
79 let hx = HxRequest::from_request_parts(&mut parts, &())
80 .await
81 .unwrap();
82 assert!(!hx.is_htmx());
83 }
84
85 #[tokio::test]
86 async fn hx_request_header_case_insensitive() {
87 let req = Request::builder()
88 .header("HX-Request", "true")
89 .body(())
90 .unwrap();
91 let (mut parts, _) = req.into_parts();
92 let hx = HxRequest::from_request_parts(&mut parts, &())
93 .await
94 .unwrap();
95 assert!(hx.is_htmx());
96 }
97}