Skip to main content

modo/template/
htmx.rs

1use axum::extract::FromRequestParts;
2use http::request::Parts;
3
4/// Axum extractor that detects HTMX requests.
5///
6/// Returns `HxRequest(true)` when the request contains `HX-Request: true`,
7/// and `HxRequest(false)` otherwise. The extraction is infallible.
8///
9/// # Example
10///
11/// ```rust,no_run
12/// use modo::template::HxRequest;
13///
14/// async fn handler(hx: HxRequest) {
15///     if hx.is_htmx() {
16///         // respond with a partial
17///     }
18/// }
19/// ```
20#[derive(Debug, Clone, Copy)]
21pub struct HxRequest(bool);
22
23impl HxRequest {
24    /// Returns `true` if the request was issued by HTMX (`HX-Request: true`).
25    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}