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