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}