Skip to main content

rama_http_headers/common/
connection.rs

1use rama_core::error::BoxError;
2use rama_core::{combinators::Either, telemetry::tracing};
3use rama_http_types::{
4    HeaderName, HeaderValue,
5    header::{KEEP_ALIVE, UPGRADE},
6};
7use rama_utils::collections::NonEmptyVec;
8
9use crate::util::{
10    FlatCsvSeparator, TryFromValues, try_decode_flat_csv_header_values_as_non_empty_vec,
11    try_encode_non_empty_vec_as_flat_csv_header_value,
12};
13
14/// `Connection` header, defined in
15/// [RFC7230](https://datatracker.ietf.org/doc/html/rfc7230#section-6.1)
16///
17/// The `Connection` header field allows the sender to indicate desired
18/// control options for the current connection.  In order to avoid
19/// confusing downstream recipients, a proxy or gateway MUST remove or
20/// replace any received connection options before forwarding the
21/// message.
22///
23/// # ABNF
24///
25/// ```text
26/// Connection        = 1#connection-option
27/// connection-option = token
28///
29/// # Example values
30/// * `close`
31/// * `keep-alive`
32/// * `upgrade`
33/// * `keep-alive, upgrade`
34/// ```
35///
36/// # Examples
37///
38/// ```
39/// use rama_http_headers::Connection;
40///
41/// let keep_alive = Connection::keep_alive();
42/// ```
43#[derive(Clone, Debug)]
44pub struct Connection(Directive);
45
46impl Connection {
47    pub fn iter_headers(&self) -> impl Iterator<Item = &HeaderName> {
48        match &self.0 {
49            Directive::Close => Either::A(std::iter::empty()),
50            Directive::Open(non_empty_vec) => Either::B(non_empty_vec.iter()),
51        }
52    }
53}
54
55#[derive(Clone, Debug)]
56enum Directive {
57    Close,
58    Open(NonEmptyVec<HeaderName>),
59}
60
61impl TryFrom<&Directive> for HeaderValue {
62    type Error = BoxError;
63
64    fn try_from(value: &Directive) -> Result<Self, Self::Error> {
65        match value {
66            Directive::Close => Ok(DIRECTIVE_HEADER_VALUE_CLOSE),
67            Directive::Open(values) => {
68                try_encode_non_empty_vec_as_flat_csv_header_value(values, FlatCsvSeparator::Comma)
69            }
70        }
71    }
72}
73
74impl TryFromValues for Directive {
75    fn try_from_values<'i, I>(values: &mut I) -> Result<Self, crate::Error>
76    where
77        Self: Sized,
78        I: Iterator<Item = &'i HeaderValue>,
79    {
80        match try_decode_flat_csv_header_values_as_non_empty_vec(values, FlatCsvSeparator::Comma) {
81            Ok(values) => {
82                if values.len() == 1 && values.first() == "close" {
83                    Ok(Self::Close)
84                } else {
85                    Ok(Self::Open(values))
86                }
87            }
88            Err(err) => {
89                tracing::trace!("invalid connection directive: {err}");
90                Err(crate::Error::invalid())
91            }
92        }
93    }
94}
95
96const DIRECTIVE_HEADER_VALUE_CLOSE: HeaderValue = HeaderValue::from_static("close");
97
98impl crate::TypedHeader for Connection {
99    fn name() -> &'static ::rama_http_types::header::HeaderName {
100        &::rama_http_types::header::CONNECTION
101    }
102}
103
104impl crate::HeaderDecode for Connection {
105    fn decode<'i, I>(values: &mut I) -> Result<Self, crate::Error>
106    where
107        I: Iterator<Item = &'i ::rama_http_types::header::HeaderValue>,
108    {
109        Directive::try_from_values(values).map(Self)
110    }
111}
112
113impl crate::HeaderEncode for Connection {
114    fn encode<E: Extend<::rama_http_types::HeaderValue>>(&self, values: &mut E) {
115        match HeaderValue::try_from(&self.0) {
116            Ok(value) => values.extend(::std::iter::once(value)),
117            Err(err) => {
118                rama_core::telemetry::tracing::debug!(
119                    "failed to encode connection directive {:?} as flat csv header: {err}",
120                    self.0,
121                );
122            }
123        }
124    }
125}
126
127impl Connection {
128    /// A constructor to easily create a `Connection` header,
129    /// for the given header names.
130    #[inline]
131    #[must_use]
132    pub fn open(headers: NonEmptyVec<HeaderName>) -> Self {
133        Self(Directive::Open(headers))
134    }
135
136    /// A constructor to easily create a `Connection: close` header.
137    #[inline]
138    #[must_use]
139    pub fn close() -> Self {
140        Self(Directive::Close)
141    }
142
143    /// Returns true if this [`Connection`] header contains `close`.
144    #[inline]
145    pub fn is_close(&self) -> bool {
146        matches!(self.0, Directive::Close)
147    }
148
149    /// A constructor to easily create a `Connection: keep-alive` header.
150    #[inline]
151    #[must_use]
152    pub fn keep_alive() -> Self {
153        Self(Directive::Open(NonEmptyVec::new(KEEP_ALIVE.clone())))
154    }
155
156    /// A constructor to easily create a `Connection: Upgrade` header.
157    #[inline]
158    #[must_use]
159    pub fn upgrade() -> Self {
160        Self(Directive::Open(NonEmptyVec::new(UPGRADE.clone())))
161    }
162
163    /// Returns true if this [`Connection`] header contains the given header.
164    ///
165    /// # Example
166    ///
167    /// ```
168    /// use rama_http_types::header::UPGRADE;
169    /// use rama_http_headers::Connection;
170    ///
171    /// let conn = Connection::keep_alive();
172    ///
173    /// assert!(!conn.contains_header(UPGRADE));
174    /// assert!(conn.contains_header("keep-alive"));
175    /// assert!(conn.contains_header("Keep-Alive"));
176    /// ```
177    #[inline]
178    #[expect(clippy::needless_pass_by_value)]
179    pub fn contains_header(&self, name: impl PartialEq<HeaderName>) -> bool {
180        match &self.0 {
181            Directive::Close => false,
182            Directive::Open(values) => values.iter().any(|candidate| name.eq(candidate)),
183        }
184    }
185
186    /// Returns true if this [`Connection`] header contains `Upgrade`.
187    ///
188    /// # Example
189    ///
190    /// ```
191    /// use rama_http_headers::Connection;
192    ///
193    /// assert!(!Connection::keep_alive().contains_upgrade());
194    /// assert!(Connection::upgrade().contains_upgrade());
195    /// ```
196    #[inline]
197    pub fn contains_upgrade(&self) -> bool {
198        self.contains_header(&UPGRADE)
199    }
200
201    /// Returns true if this [`Connection`] header contains `Keep-Alive`.
202    ///
203    /// # Example
204    ///
205    /// ```
206    /// use rama_http_headers::Connection;
207    ///
208    /// assert!(Connection::keep_alive().contains_keep_alive());
209    /// assert!(!Connection::upgrade().contains_keep_alive());
210    /// ```
211    #[inline]
212    pub fn contains_keep_alive(&self) -> bool {
213        self.contains_header(&KEEP_ALIVE)
214    }
215}
216
217#[cfg(test)]
218mod tests {
219    use super::super::{test_decode, test_encode};
220    use super::*;
221    use rama_utils::collections::non_empty_vec;
222
223    #[test]
224    fn decode_header_single_open() {
225        let Connection(directive) = test_decode(&["foo, bar"]).unwrap();
226        match directive {
227            Directive::Close => panic!("unexpecte close directive"),
228            Directive::Open(non_empty_vec) => {
229                assert_eq!(2, non_empty_vec.len());
230                assert_eq!(non_empty_vec[0], "foo");
231                assert_eq!(non_empty_vec[1], "bar");
232            }
233        }
234    }
235
236    #[test]
237    fn decode_header_single_close() {
238        let Connection(directive) = test_decode(&["close"]).unwrap();
239        match directive {
240            Directive::Close => (),
241            Directive::Open(non_empty_vec) => {
242                panic!("unexpected open directive, headers: {non_empty_vec:?}")
243            }
244        }
245    }
246
247    #[test]
248    fn encode_open() {
249        let allow = Connection::open(non_empty_vec![
250            ::rama_http_types::header::KEEP_ALIVE.clone(),
251            ::rama_http_types::header::TRAILER,
252        ]);
253
254        let headers = test_encode(allow);
255        assert_eq!(headers["connection"], "keep-alive, trailer");
256    }
257
258    #[test]
259    fn decode_with_empty_header_value() {
260        assert!(test_decode::<Connection>(&[""]).is_none());
261    }
262
263    #[test]
264    fn decode_with_no_headers() {
265        assert!(test_decode::<Connection>(&[]).is_none());
266    }
267
268    #[test]
269    fn decode_with_invalid_value_str() {
270        assert!(test_decode::<Connection>(&["foo foo, bar"]).is_none());
271    }
272}