Skip to main content

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