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