actix_web/http/header/
accept.rs

1use std::cmp::Ordering;
2
3use mime::Mime;
4
5use super::{common_header, QualityItem};
6use crate::http::header;
7
8common_header! {
9    /// `Accept` header, defined in [RFC 7231 §5.3.2].
10    ///
11    /// The `Accept` header field can be used by user agents to specify
12    /// response media types that are acceptable. Accept header fields can
13    /// be used to indicate that the request is specifically limited to a
14    /// small set of desired types, as in the case of a request for an
15    /// in-line image
16    ///
17    /// # ABNF
18    /// ```plain
19    /// Accept = #( media-range [ accept-params ] )
20    ///
21    /// media-range    = ( "*/*"
22    ///                  / ( type "/" "*" )
23    ///                  / ( type "/" subtype )
24    ///                  ) *( OWS ";" OWS parameter )
25    /// accept-params  = weight *( accept-ext )
26    /// accept-ext = OWS ";" OWS token [ "=" ( token / quoted-string ) ]
27    /// ```
28    ///
29    /// # Example Values
30    /// * `audio/*; q=0.2, audio/basic`
31    /// * `text/plain; q=0.5, text/html, text/x-dvi; q=0.8, text/x-c`
32    ///
33    /// # Examples
34    /// ```
35    /// use actix_web::HttpResponse;
36    /// use actix_web::http::header::{Accept, QualityItem};
37    ///
38    /// let mut builder = HttpResponse::Ok();
39    /// builder.insert_header(
40    ///     Accept(vec![
41    ///         QualityItem::max(mime::TEXT_HTML),
42    ///     ])
43    /// );
44    /// ```
45    ///
46    /// ```
47    /// use actix_web::HttpResponse;
48    /// use actix_web::http::header::{Accept, QualityItem};
49    ///
50    /// let mut builder = HttpResponse::Ok();
51    /// builder.insert_header(
52    ///     Accept(vec![
53    ///         QualityItem::max(mime::APPLICATION_JSON),
54    ///     ])
55    /// );
56    /// ```
57    ///
58    /// ```
59    /// use actix_web::HttpResponse;
60    /// use actix_web::http::header::{Accept, QualityItem, q};
61    ///
62    /// let mut builder = HttpResponse::Ok();
63    /// builder.insert_header(
64    ///     Accept(vec![
65    ///         QualityItem::max(mime::TEXT_HTML),
66    ///         QualityItem::max("application/xhtml+xml".parse().unwrap()),
67    ///         QualityItem::new(mime::TEXT_XML, q(0.9)),
68    ///         QualityItem::max("image/webp".parse().unwrap()),
69    ///         QualityItem::new(mime::STAR_STAR, q(0.8)),
70    ///     ])
71    /// );
72    /// ```
73    ///
74    /// [RFC 7231 §5.3.2]: https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.2
75    (Accept, header::ACCEPT) => (QualityItem<Mime>)*
76
77    test_parse_and_format {
78        // Tests from the RFC
79         crate::http::header::common_header_test!(
80            test1,
81            [b"audio/*; q=0.2, audio/basic"],
82            Some(Accept(vec![
83                QualityItem::new("audio/*".parse().unwrap(), q(0.2)),
84                QualityItem::max("audio/basic".parse().unwrap()),
85                ])));
86
87        crate::http::header::common_header_test!(
88            test2,
89            [b"text/plain; q=0.5, text/html, text/x-dvi; q=0.8, text/x-c"],
90            Some(Accept(vec![
91                QualityItem::new(mime::TEXT_PLAIN, q(0.5)),
92                QualityItem::max(mime::TEXT_HTML),
93                QualityItem::new(
94                    "text/x-dvi".parse().unwrap(),
95                    q(0.8)),
96                QualityItem::max("text/x-c".parse().unwrap()),
97                ])));
98
99        // Custom tests
100        crate::http::header::common_header_test!(
101            test3,
102            [b"text/plain; charset=utf-8"],
103            Some(Accept(vec![
104                QualityItem::max(mime::TEXT_PLAIN_UTF_8),
105            ])));
106        crate::http::header::common_header_test!(
107            test4,
108            [b"text/plain; charset=utf-8; q=0.5"],
109            Some(Accept(vec![
110                QualityItem::new(mime::TEXT_PLAIN_UTF_8, q(0.5)),
111            ])));
112
113        #[test]
114        fn test_fuzzing1() {
115            let req = test::TestRequest::default()
116                .insert_header((header::ACCEPT, "chunk#;e"))
117                .finish();
118            let header = Accept::parse(&req);
119            assert!(header.is_ok());
120        }
121    }
122}
123
124impl Accept {
125    /// Construct `Accept: */*`.
126    pub fn star() -> Accept {
127        Accept(vec![QualityItem::max(mime::STAR_STAR)])
128    }
129
130    /// Construct `Accept: application/json`.
131    pub fn json() -> Accept {
132        Accept(vec![QualityItem::max(mime::APPLICATION_JSON)])
133    }
134
135    /// Construct `Accept: text/*`.
136    pub fn text() -> Accept {
137        Accept(vec![QualityItem::max(mime::TEXT_STAR)])
138    }
139
140    /// Construct `Accept: image/*`.
141    pub fn image() -> Accept {
142        Accept(vec![QualityItem::max(mime::IMAGE_STAR)])
143    }
144
145    /// Construct `Accept: text/html`.
146    pub fn html() -> Accept {
147        Accept(vec![QualityItem::max(mime::TEXT_HTML)])
148    }
149
150    // TODO: method for getting best content encoding based on q-factors, available from server side
151    // and if none are acceptable return None
152
153    /// Extracts the most preferable mime type, accounting for [q-factor weighting].
154    ///
155    /// If no q-factors are provided, the first mime type is chosen. Note that items without
156    /// q-factors are given the maximum preference value.
157    ///
158    /// As per the spec, will return [`mime::STAR_STAR`] (indicating no preference) if the contained
159    /// list is empty.
160    ///
161    /// [q-factor weighting]: https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.2
162    pub fn preference(&self) -> Mime {
163        use actix_http::header::Quality;
164
165        let mut max_item = None;
166        let mut max_pref = Quality::ZERO;
167
168        // uses manual max lookup loop since we want the first occurrence in the case of same
169        // preference but `Iterator::max_by_key` would give us the last occurrence
170
171        for pref in &self.0 {
172            // only change if strictly greater
173            // equal items, even while unsorted, still have higher preference if they appear first
174            if pref.quality > max_pref {
175                max_pref = pref.quality;
176                max_item = Some(pref.item.clone());
177            }
178        }
179
180        max_item.unwrap_or(mime::STAR_STAR)
181    }
182
183    /// Returns a sorted list of mime types from highest to lowest preference, accounting for
184    /// [q-factor weighting] and specificity.
185    ///
186    /// [q-factor weighting]: https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.2
187    pub fn ranked(&self) -> Vec<Mime> {
188        if self.is_empty() {
189            return vec![];
190        }
191
192        let mut types = self.0.clone();
193
194        // use stable sort so items with equal q-factor and specificity retain listed order
195        types.sort_by(|a, b| {
196            // sort by q-factor descending
197            b.quality.cmp(&a.quality).then_with(|| {
198                // use specificity rules on mime types with
199                // same q-factor (eg. text/html > text/* > */*)
200
201                // subtypes are not comparable if main type is star, so return
202                match (a.item.type_(), b.item.type_()) {
203                    (mime::STAR, mime::STAR) => return Ordering::Equal,
204
205                    // a is sorted after b
206                    (mime::STAR, _) => return Ordering::Greater,
207
208                    // a is sorted before b
209                    (_, mime::STAR) => return Ordering::Less,
210
211                    _ => {}
212                }
213
214                // in both these match expressions, the returned ordering appears
215                // inverted because sort is high-to-low ("descending") precedence
216                match (a.item.subtype(), b.item.subtype()) {
217                    (mime::STAR, mime::STAR) => Ordering::Equal,
218
219                    // a is sorted after b
220                    (mime::STAR, _) => Ordering::Greater,
221
222                    // a is sorted before b
223                    (_, mime::STAR) => Ordering::Less,
224
225                    _ => Ordering::Equal,
226                }
227            })
228        });
229
230        types.into_iter().map(|qitem| qitem.item).collect()
231    }
232}
233
234#[cfg(test)]
235mod tests {
236    use super::*;
237    use crate::http::header::q;
238
239    #[test]
240    fn ranking_precedence() {
241        let test = Accept(vec![]);
242        assert!(test.ranked().is_empty());
243
244        let test = Accept(vec![QualityItem::max(mime::APPLICATION_JSON)]);
245        assert_eq!(test.ranked(), vec![mime::APPLICATION_JSON]);
246
247        let test = Accept(vec![
248            QualityItem::max(mime::TEXT_HTML),
249            "application/xhtml+xml".parse().unwrap(),
250            QualityItem::new("application/xml".parse().unwrap(), q(0.9)),
251            QualityItem::new(mime::STAR_STAR, q(0.8)),
252        ]);
253        assert_eq!(
254            test.ranked(),
255            vec![
256                mime::TEXT_HTML,
257                "application/xhtml+xml".parse().unwrap(),
258                "application/xml".parse().unwrap(),
259                mime::STAR_STAR,
260            ]
261        );
262
263        let test = Accept(vec![
264            QualityItem::max(mime::STAR_STAR),
265            QualityItem::max(mime::IMAGE_STAR),
266            QualityItem::max(mime::IMAGE_PNG),
267        ]);
268        assert_eq!(
269            test.ranked(),
270            vec![mime::IMAGE_PNG, mime::IMAGE_STAR, mime::STAR_STAR]
271        );
272    }
273
274    #[test]
275    fn preference_selection() {
276        let test = Accept(vec![
277            QualityItem::max(mime::TEXT_HTML),
278            "application/xhtml+xml".parse().unwrap(),
279            QualityItem::new("application/xml".parse().unwrap(), q(0.9)),
280            QualityItem::new(mime::STAR_STAR, q(0.8)),
281        ]);
282        assert_eq!(test.preference(), mime::TEXT_HTML);
283
284        let test = Accept(vec![
285            QualityItem::new("video/*".parse().unwrap(), q(0.8)),
286            QualityItem::max(mime::IMAGE_PNG),
287            QualityItem::new(mime::STAR_STAR, q(0.5)),
288            QualityItem::max(mime::IMAGE_SVG),
289            QualityItem::new(mime::IMAGE_STAR, q(0.8)),
290        ]);
291        assert_eq!(test.preference(), mime::IMAGE_PNG);
292    }
293}