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}