Skip to main content

actix_web/guard/
acceptable.rs

1use super::{Guard, GuardContext};
2use crate::http::header::Accept;
3
4/// A guard that verifies that an `Accept` header is present and it contains a compatible MIME type.
5///
6/// An exception is that matching `*/*` must be explicitly enabled because most browsers send this
7/// as part of their `Accept` header for almost every request.
8///
9/// # Examples
10/// ```
11/// use actix_web::{guard::Acceptable, web, HttpResponse};
12///
13/// web::resource("/images")
14///     .guard(Acceptable::new(mime::IMAGE_STAR))
15///     .default_service(web::to(|| async {
16///         HttpResponse::Ok().body("only called when images responses are acceptable")
17///     }));
18/// ```
19#[derive(Debug, Clone)]
20pub struct Acceptable {
21    mime: mime::Mime,
22
23    /// Whether to match `*/*` mime type.
24    ///
25    /// Defaults to false because it's not very useful otherwise.
26    match_star_star: bool,
27}
28
29impl Acceptable {
30    /// Constructs new `Acceptable` guard with the given `mime` type/pattern.
31    pub fn new(mime: mime::Mime) -> Self {
32        Self {
33            mime,
34            match_star_star: false,
35        }
36    }
37
38    /// Allows `*/*` in the `Accept` header to pass the guard check.
39    pub fn match_star_star(mut self) -> Self {
40        self.match_star_star = true;
41        self
42    }
43}
44
45impl Guard for Acceptable {
46    fn check(&self, ctx: &GuardContext<'_>) -> bool {
47        let accept = match ctx.header::<Accept>() {
48            Some(hdr) => hdr,
49            None => return false,
50        };
51
52        let target_type = self.mime.type_();
53        let target_subtype = self.mime.subtype();
54
55        for mime in accept.0.into_iter().map(|q| q.item) {
56            return match (mime.type_(), mime.subtype()) {
57                (typ, subtype) if typ == target_type && subtype == target_subtype => true,
58                (typ, mime::STAR) if typ == target_type => true,
59                (mime::STAR, mime::STAR) if self.match_star_star => true,
60                _ => continue,
61            };
62        }
63
64        false
65    }
66
67    #[cfg(feature = "experimental-introspection")]
68    fn name(&self) -> String {
69        if self.match_star_star {
70            format!("Acceptable({}, match_star_star=true)", self.mime)
71        } else {
72            format!("Acceptable({})", self.mime)
73        }
74    }
75
76    #[cfg(feature = "experimental-introspection")]
77    fn details(&self) -> Option<Vec<super::GuardDetail>> {
78        let mut details = Vec::new();
79        details.push(super::GuardDetail::Generic(format!("mime={}", self.mime)));
80        if self.match_star_star {
81            details.push(super::GuardDetail::Generic(
82                "match_star_star=true".to_string(),
83            ));
84        }
85        Some(details)
86    }
87}
88
89#[cfg(test)]
90mod tests {
91    use super::*;
92    use crate::{http::header, test::TestRequest};
93
94    #[test]
95    fn test_acceptable() {
96        let req = TestRequest::default().to_srv_request();
97        assert!(!Acceptable::new(mime::APPLICATION_JSON).check(&req.guard_ctx()));
98
99        let req = TestRequest::default()
100            .insert_header((header::ACCEPT, "application/json"))
101            .to_srv_request();
102        assert!(Acceptable::new(mime::APPLICATION_JSON).check(&req.guard_ctx()));
103
104        let req = TestRequest::default()
105            .insert_header((header::ACCEPT, "text/html, application/json"))
106            .to_srv_request();
107        assert!(Acceptable::new(mime::APPLICATION_JSON).check(&req.guard_ctx()));
108    }
109
110    #[test]
111    fn test_acceptable_star() {
112        let req = TestRequest::default()
113            .insert_header((header::ACCEPT, "text/html, */*;q=0.8"))
114            .to_srv_request();
115
116        assert!(Acceptable::new(mime::APPLICATION_JSON)
117            .match_star_star()
118            .check(&req.guard_ctx()));
119    }
120
121    #[cfg(feature = "experimental-introspection")]
122    #[test]
123    fn acceptable_guard_details_include_mime() {
124        let guard = Acceptable::new(mime::APPLICATION_JSON).match_star_star();
125        let details = guard.details().expect("missing guard details");
126
127        assert!(details.iter().any(|detail| match detail {
128            crate::guard::GuardDetail::Generic(value) => value == "match_star_star=true",
129            _ => false,
130        }));
131        let expected = format!("mime={}", mime::APPLICATION_JSON);
132        assert!(details.iter().any(|detail| match detail {
133            crate::guard::GuardDetail::Generic(value) => value == &expected,
134            _ => false,
135        }));
136        assert_eq!(
137            guard.name(),
138            format!(
139                "Acceptable({}, match_star_star=true)",
140                mime::APPLICATION_JSON
141            )
142        );
143    }
144}