Skip to main content

http_extract/
content_type.rs

1//! `Content-Type` extraction.
2//!
3//! This module parses the representation media type described by
4//! [RFC 9110, Section 8.3]. It does not inspect a message body or infer a media
5//! type when the field is absent.
6//!
7//! [RFC 9110, Section 8.3]: https://www.rfc-editor.org/rfc/rfc9110.html#section-8.3
8
9use http::{HeaderMap, Request, header::CONTENT_TYPE};
10
11use crate::{Error, header::extract_single_header_text};
12
13/// Extract and parse a singular `Content-Type` field as a media type.
14///
15/// A missing field returns `None`. Duplicate field lines, non-text values, and
16/// invalid or empty media types return an error. The result is parsed as
17/// [`mime::Mime`]; this function does not inspect the body, sniff content, or
18/// determine whether the declared type is truthful.
19pub fn extract_header_content_type(headers: &HeaderMap) -> Result<Option<mime::Mime>, Error> {
20    extract_single_header_text(headers, &CONTENT_TYPE)?
21        .map(|value| {
22            value
23                .parse()
24                .map_err(|_| Error::invalid_header(CONTENT_TYPE))
25        })
26        .transpose()
27}
28
29/// Extract and parse `Content-Type` from a complete request.
30///
31/// This reads `request.headers()` and delegates to
32/// [`extract_header_content_type`], preserving its missing, duplicate,
33/// encoding, and media-type validation behavior.
34pub fn extract_request_content_type<B>(request: &Request<B>) -> Result<Option<mime::Mime>, Error> {
35    extract_header_content_type(request.headers())
36}
37
38#[cfg(test)]
39mod tests {
40    use super::*;
41
42    #[test]
43    fn extracts_a_singular_media_type() {
44        let mut headers = HeaderMap::new();
45        headers.insert(
46            CONTENT_TYPE,
47            "application/json; charset=utf-8".parse().unwrap(),
48        );
49
50        let value = extract_header_content_type(&headers).unwrap().unwrap();
51        assert_eq!(value.type_(), "application");
52        assert_eq!(value.subtype(), "json");
53    }
54
55    #[test]
56    fn rejects_duplicate_content_type() {
57        let mut headers = HeaderMap::new();
58        headers.append(CONTENT_TYPE, "application/json".parse().unwrap());
59        headers.append(CONTENT_TYPE, "text/plain".parse().unwrap());
60
61        assert!(matches!(
62            extract_header_content_type(&headers),
63            Err(Error::DuplicateHeader { .. })
64        ));
65    }
66
67    #[test]
68    fn request_entry_point_delegates_to_headers() {
69        let request = Request::builder()
70            .header(CONTENT_TYPE, "application/json")
71            .body(())
72            .unwrap();
73        assert_eq!(
74            extract_request_content_type(&request).unwrap(),
75            Some(mime::APPLICATION_JSON)
76        );
77    }
78}