Skip to main content

actix_web/http/header/
accept_encoding.rs

1use std::collections::HashSet;
2
3use super::{common_header, ContentEncoding, Encoding, Preference, Quality, QualityItem};
4use crate::http::header;
5
6common_header! {
7    /// `Accept-Encoding` header, defined
8    /// in [RFC 7231](https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.4)
9    ///
10    /// The `Accept-Encoding` header field can be used by user agents to indicate what response
11    /// content-codings are acceptable in the response. An `identity` token is used as a synonym
12    /// for "no encoding" in order to communicate when no encoding is preferred.
13    ///
14    /// # Note
15    /// This is a request header. Servers should not send `Accept-Encoding` in responses; use the
16    /// `Content-Encoding` header (or middleware like compression) to describe any content-coding
17    /// applied to the response body.
18    ///
19    /// # ABNF
20    /// ```plain
21    /// Accept-Encoding  = #( codings [ weight ] )
22    /// codings          = content-coding / "identity" / "*"
23    /// ```
24    ///
25    /// # Example Values
26    /// * `compress, gzip`
27    /// * ``
28    /// * `*`
29    /// * `compress;q=0.5, gzip;q=1`
30    /// * `gzip;q=1.0, identity; q=0.5, *;q=0`
31    ///
32    /// # Examples
33    /// ```
34    /// use actix_web::{http::header::{AcceptEncoding, Encoding, Preference, QualityItem}, test};
35    ///
36    /// let req = test::TestRequest::default()
37    ///     .insert_header(AcceptEncoding(vec![
38    ///         QualityItem::max(Preference::Specific(Encoding::gzip())),
39    ///     ]))
40    ///     .to_http_request();
41    /// # let _ = req;
42    /// ```
43    ///
44    /// ```
45    /// use actix_web::{http::header::{AcceptEncoding, QualityItem}, test};
46    ///
47    /// let req = test::TestRequest::default()
48    ///     .insert_header(AcceptEncoding(vec![
49    ///         "gzip".parse().unwrap(),
50    ///         "br".parse().unwrap(),
51    ///     ]))
52    ///     .to_http_request();
53    /// # let _ = req;
54    /// ```
55    (AcceptEncoding, header::ACCEPT_ENCODING) => (QualityItem<Preference<Encoding>>)*
56
57    test_parse_and_format {
58        common_header_test!(no_headers, [b""; 0], Some(AcceptEncoding(vec![])));
59        common_header_test!(empty_header, [b""; 1], Some(AcceptEncoding(vec![])));
60
61        common_header_test!(
62            order_of_appearance,
63            [b"br, gzip"],
64            Some(AcceptEncoding(vec![
65                QualityItem::max(Preference::Specific(Encoding::brotli())),
66                QualityItem::max(Preference::Specific(Encoding::gzip())),
67            ]))
68        );
69
70        common_header_test!(any, [b"*"], Some(AcceptEncoding(vec![
71            QualityItem::max(Preference::Any),
72        ])));
73
74        // Note: Removed quality 1 from gzip
75        common_header_test!(implicit_quality, [b"gzip, identity; q=0.5, *;q=0"]);
76
77        // Note: Removed quality 1 from gzip
78        common_header_test!(implicit_quality_out_of_order, [b"compress;q=0.5, gzip"]);
79
80        common_header_test!(
81            only_gzip_no_identity,
82            [b"gzip, *; q=0"],
83            Some(AcceptEncoding(vec![
84                QualityItem::max(Preference::Specific(Encoding::gzip())),
85                QualityItem::zero(Preference::Any),
86            ]))
87        );
88    }
89}
90
91impl AcceptEncoding {
92    /// Selects the most acceptable encoding according to client preference and supported types.
93    ///
94    /// The "identity" encoding is not assumed and should be included in the `supported` iterator
95    /// if a non-encoded representation can be selected.
96    ///
97    /// If `None` is returned, this indicates that none of the supported encodings are acceptable to
98    /// the client. The caller should generate a 406 Not Acceptable response (unencoded) that
99    /// includes the server's supported encodings in the body plus a [`Vary`] header.
100    ///
101    /// [`Vary`]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Vary
102    pub fn negotiate<'a>(&self, supported: impl Iterator<Item = &'a Encoding>) -> Option<Encoding> {
103        // 1. If no Accept-Encoding field is in the request, any content-coding is considered
104        // acceptable by the user agent.
105
106        let supported_set = supported.collect::<HashSet<_>>();
107
108        if supported_set.is_empty() {
109            return None;
110        }
111
112        if self.0.is_empty() {
113            // though it is not recommended to encode in this case, return identity encoding
114            return Some(Encoding::identity());
115        }
116
117        // 2. If the representation has no content-coding, then it is acceptable by default unless
118        // specifically excluded by the Accept-Encoding field stating either "identity;q=0" or
119        // "*;q=0" without a more specific entry for "identity".
120
121        let acceptable_items = self.ranked_items().collect::<Vec<_>>();
122
123        let identity_acceptable = is_identity_acceptable(&acceptable_items);
124        let identity_supported = supported_set.contains(&Encoding::identity());
125
126        if identity_acceptable && identity_supported && supported_set.len() == 1 {
127            return Some(Encoding::identity());
128        }
129
130        // 3. If the representation's content-coding is one of the content-codings listed in the
131        // Accept-Encoding field, then it is acceptable unless it is accompanied by a qvalue of 0.
132
133        // 4. If multiple content-codings are acceptable, then the acceptable content-coding with
134        // the highest non-zero qvalue is preferred.
135
136        let matched = acceptable_items
137            .into_iter()
138            .filter(|q| q.quality > Quality::ZERO)
139            // search relies on item list being in descending order of quality
140            .find(|q| {
141                let enc = &q.item;
142                matches!(enc, Preference::Specific(enc) if supported_set.contains(enc))
143            })
144            .map(|q| q.item);
145
146        match matched {
147            Some(Preference::Specific(enc)) => Some(enc),
148
149            _ if identity_acceptable => Some(Encoding::identity()),
150
151            _ => None,
152        }
153    }
154
155    /// Extracts the most preferable encoding, accounting for [q-factor weighting].
156    ///
157    /// If no q-factors are provided, we prefer brotli > zstd > gzip. Note that items without
158    /// q-factors are given the maximum preference value.
159    ///
160    /// As per the spec, returns [`Preference::Any`] if acceptable list is empty. Though, if this is
161    /// returned, it is recommended to use an un-encoded representation.
162    ///
163    /// If `None` is returned, it means that the client has signalled that no representations
164    /// are acceptable. This should never occur for a well behaved user-agent.
165    ///
166    /// [q-factor weighting]: https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.2
167    pub fn preference(&self) -> Option<Preference<Encoding>> {
168        // empty header indicates no preference
169        if self.0.is_empty() {
170            return Some(Preference::Any);
171        }
172
173        let mut max_item = None;
174        let mut max_pref = Quality::ZERO;
175        let mut max_rank = 0;
176
177        // uses manual max lookup loop since we want the first occurrence in the case of same
178        // preference but `Iterator::max_by_key` would give us the last occurrence
179
180        for pref in &self.0 {
181            // only change if strictly greater
182            // equal items, even while unsorted, still have higher preference if they appear first
183
184            let rank = encoding_rank(pref);
185
186            if (pref.quality, rank) > (max_pref, max_rank) {
187                max_pref = pref.quality;
188                max_item = Some(pref.item.clone());
189                max_rank = rank;
190            }
191        }
192
193        // Return max_item if any items were above 0 quality...
194        max_item.or_else(|| {
195            // ...or else check for "*" or "identity". We can elide quality checks since
196            // entering this block means all items had "q=0".
197            match self.0.iter().find(|pref| {
198                matches!(
199                    pref.item,
200                    Preference::Any
201                        | Preference::Specific(Encoding::Known(ContentEncoding::Identity))
202                )
203            }) {
204                // "identity" or "*" found so no representation is acceptable
205                Some(_) => None,
206
207                // implicit "identity" is acceptable
208                None => Some(Preference::Specific(Encoding::identity())),
209            }
210        })
211    }
212
213    /// Returns a sorted list of encodings from highest to lowest precedence, accounting
214    /// for [q-factor weighting].
215    ///
216    /// If no q-factors are provided, we prefer brotli > zstd > gzip.
217    ///
218    /// [q-factor weighting]: https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.2
219    pub fn ranked(&self) -> Vec<Preference<Encoding>> {
220        self.ranked_items().map(|q| q.item).collect()
221    }
222
223    fn ranked_items(&self) -> impl Iterator<Item = QualityItem<Preference<Encoding>>> {
224        if self.0.is_empty() {
225            return Vec::new().into_iter();
226        }
227
228        let mut types = self.0.clone();
229
230        // use stable sort so items with equal q-factor retain listed order
231        types.sort_by(|a, b| {
232            // sort by q-factor descending then server ranking descending
233
234            b.quality
235                .cmp(&a.quality)
236                .then(encoding_rank(b).cmp(&encoding_rank(a)))
237        });
238
239        types.into_iter()
240    }
241}
242
243/// Returns server-defined encoding ranking.
244fn encoding_rank(qv: &QualityItem<Preference<Encoding>>) -> u8 {
245    // ensure that q=0 items are never sorted above identity encoding
246    // invariant: sorting methods calling this fn use first-on-equal approach
247    if qv.quality == Quality::ZERO {
248        return 0;
249    }
250
251    match qv.item {
252        Preference::Specific(Encoding::Known(ContentEncoding::Brotli)) => 5,
253        Preference::Specific(Encoding::Known(ContentEncoding::Zstd)) => 4,
254        Preference::Specific(Encoding::Known(ContentEncoding::Gzip)) => 3,
255        Preference::Specific(Encoding::Known(ContentEncoding::Deflate)) => 2,
256        Preference::Any => 0,
257        Preference::Specific(Encoding::Known(ContentEncoding::Identity)) => 0,
258        Preference::Specific(Encoding::Known(_)) => 1,
259        Preference::Specific(Encoding::Unknown(_)) => 1,
260    }
261}
262
263/// Returns true if "identity" is an acceptable encoding.
264///
265/// Internal algorithm relies on item list being in descending order of quality.
266fn is_identity_acceptable(items: &'_ [QualityItem<Preference<Encoding>>]) -> bool {
267    if items.is_empty() {
268        return true;
269    }
270
271    // Loop algorithm depends on items being sorted in descending order of quality. As such, it
272    // is sufficient to return (q > 0) when reaching either an "identity" or "*" item.
273    for q in items {
274        match (q.quality, &q.item) {
275            // occurrence of "identity;q=n"; return true if quality is non-zero
276            (q, Preference::Specific(Encoding::Known(ContentEncoding::Identity))) => {
277                return q > Quality::ZERO
278            }
279
280            // occurrence of "*;q=n"; return true if quality is non-zero
281            (q, Preference::Any) => return q > Quality::ZERO,
282
283            _ => {}
284        }
285    }
286
287    // implicit acceptable identity
288    true
289}
290
291#[cfg(test)]
292mod tests {
293    use super::*;
294    use crate::http::header::*;
295
296    macro_rules! accept_encoding {
297        () => { AcceptEncoding(vec![]) };
298        ($($q:expr),+ $(,)?) => { AcceptEncoding(vec![$($q.parse().unwrap()),+]) };
299    }
300
301    /// Parses an encoding string.
302    fn enc(enc: &str) -> Preference<Encoding> {
303        enc.parse().unwrap()
304    }
305
306    #[test]
307    fn detect_identity_acceptable() {
308        macro_rules! accept_encoding_ranked {
309            () => { accept_encoding!().ranked_items().collect::<Vec<_>>() };
310            ($($q:expr),+ $(,)?) => { accept_encoding!($($q),+).ranked_items().collect::<Vec<_>>() };
311        }
312
313        let test = accept_encoding_ranked!();
314        assert!(is_identity_acceptable(&test));
315        let test = accept_encoding_ranked!("gzip");
316        assert!(is_identity_acceptable(&test));
317        let test = accept_encoding_ranked!("gzip", "br");
318        assert!(is_identity_acceptable(&test));
319        let test = accept_encoding_ranked!("gzip", "*;q=0.1");
320        assert!(is_identity_acceptable(&test));
321        let test = accept_encoding_ranked!("gzip", "identity;q=0.1");
322        assert!(is_identity_acceptable(&test));
323        let test = accept_encoding_ranked!("gzip", "identity;q=0.1", "*;q=0");
324        assert!(is_identity_acceptable(&test));
325        let test = accept_encoding_ranked!("gzip", "*;q=0", "identity;q=0.1");
326        assert!(is_identity_acceptable(&test));
327
328        let test = accept_encoding_ranked!("gzip", "*;q=0");
329        assert!(!is_identity_acceptable(&test));
330        let test = accept_encoding_ranked!("gzip", "identity;q=0");
331        assert!(!is_identity_acceptable(&test));
332        let test = accept_encoding_ranked!("gzip", "identity;q=0", "*;q=0");
333        assert!(!is_identity_acceptable(&test));
334        let test = accept_encoding_ranked!("gzip", "*;q=0", "identity;q=0");
335        assert!(!is_identity_acceptable(&test));
336    }
337
338    #[test]
339    fn encoding_negotiation() {
340        // no preference
341        let test = accept_encoding!();
342        assert_eq!(test.negotiate([].iter()), None);
343
344        let test = accept_encoding!();
345        assert_eq!(
346            test.negotiate([Encoding::identity()].iter()),
347            Some(Encoding::identity()),
348        );
349
350        let test = accept_encoding!("identity;q=0");
351        assert_eq!(test.negotiate([Encoding::identity()].iter()), None);
352
353        let test = accept_encoding!("*;q=0");
354        assert_eq!(test.negotiate([Encoding::identity()].iter()), None);
355
356        let test = accept_encoding!();
357        assert_eq!(
358            test.negotiate([Encoding::gzip(), Encoding::identity()].iter()),
359            Some(Encoding::identity()),
360        );
361
362        let test = accept_encoding!("gzip");
363        assert_eq!(
364            test.negotiate([Encoding::gzip(), Encoding::identity()].iter()),
365            Some(Encoding::gzip()),
366        );
367        assert_eq!(
368            test.negotiate([Encoding::brotli(), Encoding::identity()].iter()),
369            Some(Encoding::identity()),
370        );
371        assert_eq!(
372            test.negotiate([Encoding::brotli(), Encoding::gzip(), Encoding::identity()].iter()),
373            Some(Encoding::gzip()),
374        );
375
376        let test = accept_encoding!("gzip", "identity;q=0");
377        assert_eq!(
378            test.negotiate([Encoding::gzip(), Encoding::identity()].iter()),
379            Some(Encoding::gzip()),
380        );
381        assert_eq!(
382            test.negotiate([Encoding::brotli(), Encoding::identity()].iter()),
383            None
384        );
385
386        let test = accept_encoding!("gzip", "*;q=0");
387        assert_eq!(
388            test.negotiate([Encoding::gzip(), Encoding::identity()].iter()),
389            Some(Encoding::gzip()),
390        );
391        assert_eq!(
392            test.negotiate([Encoding::brotli(), Encoding::identity()].iter()),
393            None
394        );
395
396        let test = accept_encoding!("gzip", "deflate", "br");
397        assert_eq!(
398            test.negotiate([Encoding::gzip(), Encoding::identity()].iter()),
399            Some(Encoding::gzip()),
400        );
401        assert_eq!(
402            test.negotiate([Encoding::brotli(), Encoding::identity()].iter()),
403            Some(Encoding::brotli())
404        );
405        assert_eq!(
406            test.negotiate([Encoding::deflate(), Encoding::identity()].iter()),
407            Some(Encoding::deflate())
408        );
409        assert_eq!(
410            test.negotiate([Encoding::gzip(), Encoding::deflate(), Encoding::identity()].iter()),
411            Some(Encoding::gzip())
412        );
413        assert_eq!(
414            test.negotiate([Encoding::gzip(), Encoding::brotli(), Encoding::identity()].iter()),
415            Some(Encoding::brotli())
416        );
417        assert_eq!(
418            test.negotiate([Encoding::brotli(), Encoding::gzip(), Encoding::identity()].iter()),
419            Some(Encoding::brotli())
420        );
421    }
422
423    #[test]
424    fn ranking_precedence() {
425        let test = accept_encoding!();
426        assert!(test.ranked().is_empty());
427
428        let test = accept_encoding!("gzip");
429        assert_eq!(test.ranked(), vec![enc("gzip")]);
430
431        let test = accept_encoding!("gzip;q=0.900", "*;q=0.700", "br;q=1.0");
432        assert_eq!(test.ranked(), vec![enc("br"), enc("gzip"), enc("*")]);
433
434        let test = accept_encoding!("br", "gzip", "*");
435        assert_eq!(test.ranked(), vec![enc("br"), enc("gzip"), enc("*")]);
436
437        let test = accept_encoding!("gzip", "br", "*");
438        assert_eq!(test.ranked(), vec![enc("br"), enc("gzip"), enc("*")]);
439    }
440
441    #[test]
442    fn preference_selection() {
443        assert_eq!(accept_encoding!().preference(), Some(Preference::Any));
444
445        assert_eq!(accept_encoding!("identity;q=0").preference(), None);
446        assert_eq!(accept_encoding!("*;q=0").preference(), None);
447        assert_eq!(accept_encoding!("compress;q=0", "*;q=0").preference(), None);
448        assert_eq!(accept_encoding!("identity;q=0", "*;q=0").preference(), None);
449
450        let test = accept_encoding!("*;q=0.5");
451        assert_eq!(test.preference().unwrap(), enc("*"));
452
453        let test = accept_encoding!("br;q=0");
454        assert_eq!(test.preference().unwrap(), enc("identity"));
455
456        let test = accept_encoding!("br;q=0.900", "gzip;q=1.0", "*;q=0.500");
457        assert_eq!(test.preference().unwrap(), enc("gzip"));
458
459        let test = accept_encoding!("br", "gzip", "*");
460        assert_eq!(test.preference().unwrap(), enc("br"));
461
462        let test = accept_encoding!("gzip", "br", "*");
463        assert_eq!(test.preference().unwrap(), enc("br"));
464    }
465}