Skip to main content

hyper/ext/
h1_reason_phrase.rs

1use bytes::Bytes;
2
3/// A reason phrase in an HTTP/1 response.
4///
5/// # Clients
6///
7/// For clients, a `ReasonPhrase` will be present in the extensions of the `http::Response` returned
8/// for a request if the reason phrase is different from the canonical reason phrase for the
9/// response's status code. For example, if a server returns `HTTP/1.1 200 Awesome`, the
10/// `ReasonPhrase` will be present and contain `Awesome`, but if a server returns `HTTP/1.1 200 OK`,
11/// the response will not contain a `ReasonPhrase`.
12///
13/// ```no_run
14/// # #[cfg(all(feature = "tcp", feature = "client", feature = "http1"))]
15/// # async fn fake_fetch() -> hyper::Result<()> {
16/// use hyper::{Client, Uri};
17/// use hyper::ext::ReasonPhrase;
18///
19/// let res = Client::new().get(Uri::from_static("http://example.com/non_canonical_reason")).await?;
20///
21/// // Print out the non-canonical reason phrase, if it has one...
22/// if let Some(reason) = res.extensions().get::<ReasonPhrase>() {
23///     println!("non-canonical reason: {}", std::str::from_utf8(reason.as_bytes()).unwrap());
24/// }
25/// # Ok(())
26/// # }
27/// ```
28///
29/// # Servers
30///
31/// When a `ReasonPhrase` is present in the extensions of the `http::Response` written by a server,
32/// its contents will be written in place of the canonical reason phrase when responding via HTTP/1.
33#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
34pub struct ReasonPhrase(Bytes);
35
36impl ReasonPhrase {
37    /// Gets the reason phrase as bytes.
38    pub fn as_bytes(&self) -> &[u8] {
39        &self.0
40    }
41
42    /// Converts a static byte slice to a reason phrase.
43    pub const fn from_static(reason: &'static [u8]) -> Self {
44        // TODO: this can be made const once MSRV is >= 1.57.0
45        assert!(
46            find_invalid_byte(reason).is_none(),
47            "invalid byte in static reason phrase"
48        );
49        Self(Bytes::from_static(reason))
50    }
51
52    // Not public on purpose.
53    /// Converts a `Bytes` directly into a `ReasonPhrase` without validating.
54    ///
55    /// Use with care; invalid bytes in a reason phrase can cause serious security problems if
56    /// emitted in a response.
57    #[cfg(feature = "client")]
58    pub(crate) fn from_bytes_unchecked(reason: Bytes) -> Self {
59        Self(reason)
60    }
61}
62
63impl TryFrom<&[u8]> for ReasonPhrase {
64    type Error = InvalidReasonPhrase;
65
66    fn try_from(reason: &[u8]) -> Result<Self, Self::Error> {
67        if let Some(bad_byte) = find_invalid_byte(reason) {
68            Err(InvalidReasonPhrase { bad_byte })
69        } else {
70            Ok(Self(Bytes::copy_from_slice(reason)))
71        }
72    }
73}
74
75impl TryFrom<Vec<u8>> for ReasonPhrase {
76    type Error = InvalidReasonPhrase;
77
78    fn try_from(reason: Vec<u8>) -> Result<Self, Self::Error> {
79        if let Some(bad_byte) = find_invalid_byte(&reason) {
80            Err(InvalidReasonPhrase { bad_byte })
81        } else {
82            Ok(Self(Bytes::from(reason)))
83        }
84    }
85}
86
87impl TryFrom<String> for ReasonPhrase {
88    type Error = InvalidReasonPhrase;
89
90    fn try_from(reason: String) -> Result<Self, Self::Error> {
91        if let Some(bad_byte) = find_invalid_byte(reason.as_bytes()) {
92            Err(InvalidReasonPhrase { bad_byte })
93        } else {
94            Ok(Self(Bytes::from(reason)))
95        }
96    }
97}
98
99impl TryFrom<Bytes> for ReasonPhrase {
100    type Error = InvalidReasonPhrase;
101
102    fn try_from(reason: Bytes) -> Result<Self, Self::Error> {
103        if let Some(bad_byte) = find_invalid_byte(&reason) {
104            Err(InvalidReasonPhrase { bad_byte })
105        } else {
106            Ok(Self(reason))
107        }
108    }
109}
110
111impl From<ReasonPhrase> for Bytes {
112    fn from(reason: ReasonPhrase) -> Self {
113        reason.0
114    }
115}
116
117impl AsRef<[u8]> for ReasonPhrase {
118    fn as_ref(&self) -> &[u8] {
119        &self.0
120    }
121}
122
123/// Error indicating an invalid byte when constructing a `ReasonPhrase`.
124///
125/// See [the spec][spec] for details on allowed bytes.
126///
127/// [spec]: https://httpwg.org/http-core/draft-ietf-httpbis-messaging-latest.html#rfc.section.4.p.7
128#[derive(Debug)]
129pub struct InvalidReasonPhrase {
130    bad_byte: u8,
131}
132
133impl std::fmt::Display for InvalidReasonPhrase {
134    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
135        write!(f, "Invalid byte in reason phrase: {}", self.bad_byte)
136    }
137}
138
139impl std::error::Error for InvalidReasonPhrase {}
140
141const fn is_valid_byte(b: u8) -> bool {
142    // See https://www.rfc-editor.org/rfc/rfc5234.html#appendix-B.1
143    const fn is_vchar(b: u8) -> bool {
144        0x21 <= b && b <= 0x7E
145    }
146
147    // See https://httpwg.org/http-core/draft-ietf-httpbis-semantics-latest.html#fields.values
148    //
149    // The 0xFF comparison is technically redundant, but it matches the text of the spec more
150    // clearly and will be optimized away.
151    #[allow(unused_comparisons, clippy::absurd_extreme_comparisons)]
152    const fn is_obs_text(b: u8) -> bool {
153        0x80 <= b && b <= 0xFF
154    }
155
156    // See https://httpwg.org/http-core/draft-ietf-httpbis-messaging-latest.html#rfc.section.4.p.7
157    b == b'\t' || b == b' ' || is_vchar(b) || is_obs_text(b)
158}
159
160const fn find_invalid_byte(bytes: &[u8]) -> Option<u8> {
161    let mut i = 0;
162    while i < bytes.len() {
163        let b = bytes[i];
164        if !is_valid_byte(b) {
165            return Some(b);
166        }
167        i += 1;
168    }
169    None
170}
171
172#[cfg(test)]
173mod tests {
174    use super::*;
175
176    #[test]
177    fn basic_valid() {
178        const PHRASE: &[u8] = b"OK";
179        assert_eq!(ReasonPhrase::from_static(PHRASE).as_bytes(), PHRASE);
180        assert_eq!(ReasonPhrase::try_from(PHRASE).unwrap().as_bytes(), PHRASE);
181    }
182
183    #[test]
184    fn empty_valid() {
185        const PHRASE: &[u8] = b"";
186        assert_eq!(ReasonPhrase::from_static(PHRASE).as_bytes(), PHRASE);
187        assert_eq!(ReasonPhrase::try_from(PHRASE).unwrap().as_bytes(), PHRASE);
188    }
189
190    #[test]
191    fn obs_text_valid() {
192        const PHRASE: &[u8] = b"hyp\xe9r";
193        assert_eq!(ReasonPhrase::from_static(PHRASE).as_bytes(), PHRASE);
194        assert_eq!(ReasonPhrase::try_from(PHRASE).unwrap().as_bytes(), PHRASE);
195    }
196
197    const NEWLINE_PHRASE: &[u8] = b"hyp\ner";
198
199    #[test]
200    #[should_panic]
201    fn newline_invalid_panic() {
202        ReasonPhrase::from_static(NEWLINE_PHRASE);
203    }
204
205    #[test]
206    fn newline_invalid_err() {
207        assert!(ReasonPhrase::try_from(NEWLINE_PHRASE).is_err());
208    }
209
210    const CR_PHRASE: &[u8] = b"hyp\rer";
211
212    #[test]
213    #[should_panic]
214    fn cr_invalid_panic() {
215        ReasonPhrase::from_static(CR_PHRASE);
216    }
217
218    #[test]
219    fn cr_invalid_err() {
220        assert!(ReasonPhrase::try_from(CR_PHRASE).is_err());
221    }
222}