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    ///
44    /// # Panics
45    ///
46    /// This method will panic if 'reason' contains an invalid byte. In a const context,
47    /// this is a compile-time error instead.
48    pub const fn from_static(reason: &'static [u8]) -> Self {
49        assert!(
50            find_invalid_byte(reason).is_none(),
51            "invalid byte in static reason phrase"
52        );
53        Self(Bytes::from_static(reason))
54    }
55
56    // Not public on purpose.
57    /// Converts a `Bytes` directly into a `ReasonPhrase` without validating.
58    ///
59    /// Use with care; invalid bytes in a reason phrase can cause serious security problems if
60    /// emitted in a response.
61    #[cfg(feature = "client")]
62    pub(crate) fn from_bytes_unchecked(reason: Bytes) -> Self {
63        Self(reason)
64    }
65}
66
67impl TryFrom<&[u8]> for ReasonPhrase {
68    type Error = InvalidReasonPhrase;
69
70    fn try_from(reason: &[u8]) -> Result<Self, Self::Error> {
71        if let Some(bad_byte) = find_invalid_byte(reason) {
72            Err(InvalidReasonPhrase { bad_byte })
73        } else {
74            Ok(Self(Bytes::copy_from_slice(reason)))
75        }
76    }
77}
78
79impl TryFrom<Vec<u8>> for ReasonPhrase {
80    type Error = InvalidReasonPhrase;
81
82    fn try_from(reason: Vec<u8>) -> Result<Self, Self::Error> {
83        if let Some(bad_byte) = find_invalid_byte(&reason) {
84            Err(InvalidReasonPhrase { bad_byte })
85        } else {
86            Ok(Self(Bytes::from(reason)))
87        }
88    }
89}
90
91impl TryFrom<String> for ReasonPhrase {
92    type Error = InvalidReasonPhrase;
93
94    fn try_from(reason: String) -> Result<Self, Self::Error> {
95        if let Some(bad_byte) = find_invalid_byte(reason.as_bytes()) {
96            Err(InvalidReasonPhrase { bad_byte })
97        } else {
98            Ok(Self(Bytes::from(reason)))
99        }
100    }
101}
102
103impl TryFrom<Bytes> for ReasonPhrase {
104    type Error = InvalidReasonPhrase;
105
106    fn try_from(reason: Bytes) -> Result<Self, Self::Error> {
107        if let Some(bad_byte) = find_invalid_byte(&reason) {
108            Err(InvalidReasonPhrase { bad_byte })
109        } else {
110            Ok(Self(reason))
111        }
112    }
113}
114
115impl From<ReasonPhrase> for Bytes {
116    fn from(reason: ReasonPhrase) -> Self {
117        reason.0
118    }
119}
120
121impl AsRef<[u8]> for ReasonPhrase {
122    fn as_ref(&self) -> &[u8] {
123        &self.0
124    }
125}
126
127/// Error indicating an invalid byte when constructing a `ReasonPhrase`.
128///
129/// See [the spec][spec] for details on allowed bytes.
130///
131/// [spec]: https://httpwg.org/http-core/draft-ietf-httpbis-messaging-latest.html#rfc.section.4.p.7
132#[derive(Debug)]
133pub struct InvalidReasonPhrase {
134    bad_byte: u8,
135}
136
137impl std::fmt::Display for InvalidReasonPhrase {
138    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
139        write!(f, "Invalid byte in reason phrase: {}", self.bad_byte)
140    }
141}
142
143impl std::error::Error for InvalidReasonPhrase {}
144
145const fn is_valid_byte(b: u8) -> bool {
146    // See https://www.rfc-editor.org/rfc/rfc5234.html#appendix-B.1
147    const fn is_vchar(b: u8) -> bool {
148        0x21 <= b && b <= 0x7E
149    }
150
151    // See https://httpwg.org/http-core/draft-ietf-httpbis-semantics-latest.html#fields.values
152    //
153    // The 0xFF comparison is technically redundant, but it matches the text of the spec more
154    // clearly and will be optimized away.
155    #[allow(unused_comparisons, clippy::absurd_extreme_comparisons)]
156    const fn is_obs_text(b: u8) -> bool {
157        0x80 <= b && b <= 0xFF
158    }
159
160    // See https://httpwg.org/http-core/draft-ietf-httpbis-messaging-latest.html#rfc.section.4.p.7
161    b == b'\t' || b == b' ' || is_vchar(b) || is_obs_text(b)
162}
163
164const fn find_invalid_byte(bytes: &[u8]) -> Option<u8> {
165    let mut i = 0;
166    while i < bytes.len() {
167        let b = bytes[i];
168        if !is_valid_byte(b) {
169            return Some(b);
170        }
171        i += 1;
172    }
173    None
174}
175
176#[cfg(test)]
177mod tests {
178    use super::*;
179
180    #[test]
181    fn basic_valid() {
182        const PHRASE: &[u8] = b"OK";
183        assert_eq!(ReasonPhrase::from_static(PHRASE).as_bytes(), PHRASE);
184        assert_eq!(ReasonPhrase::try_from(PHRASE).unwrap().as_bytes(), PHRASE);
185    }
186
187    #[test]
188    fn empty_valid() {
189        const PHRASE: &[u8] = b"";
190        assert_eq!(ReasonPhrase::from_static(PHRASE).as_bytes(), PHRASE);
191        assert_eq!(ReasonPhrase::try_from(PHRASE).unwrap().as_bytes(), PHRASE);
192    }
193
194    #[test]
195    fn obs_text_valid() {
196        const PHRASE: &[u8] = b"hyp\xe9r";
197        assert_eq!(ReasonPhrase::from_static(PHRASE).as_bytes(), PHRASE);
198        assert_eq!(ReasonPhrase::try_from(PHRASE).unwrap().as_bytes(), PHRASE);
199    }
200
201    const NEWLINE_PHRASE: &[u8] = b"hyp\ner";
202
203    #[test]
204    #[should_panic]
205    fn newline_invalid_panic() {
206        ReasonPhrase::from_static(NEWLINE_PHRASE);
207    }
208
209    #[test]
210    fn newline_invalid_err() {
211        assert!(ReasonPhrase::try_from(NEWLINE_PHRASE).is_err());
212    }
213
214    const CR_PHRASE: &[u8] = b"hyp\rer";
215
216    #[test]
217    #[should_panic]
218    fn cr_invalid_panic() {
219        ReasonPhrase::from_static(CR_PHRASE);
220    }
221
222    #[test]
223    fn cr_invalid_err() {
224        assert!(ReasonPhrase::try_from(CR_PHRASE).is_err());
225    }
226}