trillium/upgrade.rs
1use crate::{Headers, HttpContext, Method, Transport, TypeSet, Version};
2use futures_lite::{AsyncRead, AsyncWrite};
3use std::{
4 mem,
5 net::IpAddr,
6 sync::{
7 Arc, OnceLock,
8 atomic::{AtomicUsize, Ordering::Relaxed},
9 },
10};
11use trillium_http::Swansong;
12use trillium_macros::{AsyncRead, AsyncWrite};
13
14/// # A HTTP protocol upgrade
15#[derive(Debug, AsyncWrite, AsyncRead)]
16pub struct Upgrade {
17 #[async_write]
18 #[async_read]
19 inner: trillium_http::Upgrade<Box<dyn Transport>>,
20 path_frames: PathFrames,
21}
22
23/// A path-frame stack mutable through `&self`, because [`Handler::has_upgrade`] takes
24/// `&Upgrade`.
25///
26/// [`Upgrade::path`] lends `&str`s out of this structure for the lifetime of `&self`, so a
27/// frame may never be freed or moved while the `Upgrade` is alive — even after `pop`. Storage
28/// is therefore an append-only chain of `OnceLock` links (`OnceLock::set` takes `&self`), and
29/// each node records its parent's position, making the chain a persistent stack: `top` is the
30/// 1-based chain position of the current top frame (0 = empty, full path), push appends a node
31/// whose parent is the current top, and pop moves `top` to the parent — popped frames stay
32/// allocated until the `Upgrade` drops.
33///
34/// [`Handler::has_upgrade`]: crate::Handler::has_upgrade
35#[derive(Debug, Default)]
36struct PathFrames {
37 head: OnceLock<Box<FrameNode>>,
38 top: AtomicUsize,
39}
40
41#[derive(Debug)]
42struct FrameNode {
43 frame: String,
44 /// 1-based chain position of the frame below this one on the stack; 0 = stack bottom
45 parent: usize,
46 next: OnceLock<Box<Self>>,
47}
48
49impl PathFrames {
50 fn get(&self, position: usize) -> Option<&FrameNode> {
51 let steps = position.checked_sub(1)?;
52 let mut node = self.head.get()?;
53 for _ in 0..steps {
54 node = node.next.get()?;
55 }
56 Some(node)
57 }
58
59 fn top_frame(&self) -> Option<&str> {
60 match self.top.load(Relaxed) {
61 0 => None,
62 top => self.get(top).map(|node| &*node.frame),
63 }
64 }
65
66 fn push(&self, frame: String) {
67 let parent = self.top.load(Relaxed);
68 let mut node = Box::new(FrameNode {
69 frame,
70 parent,
71 next: OnceLock::new(),
72 });
73 let mut position = 1;
74 let mut lock = &self.head;
75 loop {
76 while let Some(occupied) = lock.get() {
77 lock = &occupied.next;
78 position += 1;
79 }
80 match lock.set(node) {
81 Ok(()) => break,
82 // a concurrent push claimed this link between the get and the set; the cell is
83 // now (or is about to finish being) initialized, so resume walking from it
84 Err(rejected) => node = rejected,
85 }
86 }
87 self.top.store(position, Relaxed);
88 }
89
90 fn pop(&self) {
91 let top = self.top.load(Relaxed);
92 if let Some(node) = self.get(top) {
93 self.top.store(node.parent, Relaxed);
94 }
95 }
96}
97
98impl<T: Transport + 'static> From<trillium_http::Upgrade<T>> for Upgrade {
99 fn from(value: trillium_http::Upgrade<T>) -> Self {
100 Self {
101 inner: value.map_transport(|t| Box::new(t) as Box<dyn Transport>),
102 path_frames: PathFrames::default(),
103 }
104 }
105}
106
107impl<T: Transport + 'static> From<trillium_http::Conn<T>> for Upgrade {
108 fn from(value: trillium_http::Conn<T>) -> Self {
109 trillium_http::Upgrade::from(value).into()
110 }
111}
112
113impl From<crate::Conn> for Upgrade {
114 fn from(value: crate::Conn) -> Self {
115 Self {
116 inner: value.inner.into(),
117 path_frames: PathFrames::default(),
118 }
119 }
120}
121
122impl AsRef<trillium_http::Upgrade<Box<dyn Transport>>> for Upgrade {
123 fn as_ref(&self) -> &trillium_http::Upgrade<Box<dyn Transport>> {
124 &self.inner
125 }
126}
127
128impl AsMut<trillium_http::Upgrade<Box<dyn Transport>>> for Upgrade {
129 fn as_mut(&mut self) -> &mut trillium_http::Upgrade<Box<dyn Transport>> {
130 &mut self.inner
131 }
132}
133
134impl Upgrade {
135 /// Borrows the HTTP request headers
136 pub fn request_headers(&self) -> &Headers {
137 self.inner.received_headers()
138 }
139
140 /// Take the HTTP request headers
141 pub fn take_request_headers(&mut self) -> Headers {
142 mem::take(self.inner.received_headers_mut())
143 }
144
145 /// Returns a copy of the HTTP request method
146 pub fn method(&self) -> Method {
147 self.inner.method()
148 }
149
150 /// Borrows the state accumulated on the Conn before negotiating the upgrade
151 pub fn state(&self) -> &TypeSet {
152 self.inner.state()
153 }
154
155 /// Takes the [`TypeSet`] accumulated on the Conn before negotiating the upgrade
156 pub fn take_state(&mut self) -> TypeSet {
157 mem::take(self.inner.state_mut())
158 }
159
160 /// Mutably borrow the [`TypeSet`] accumulated on the Conn before negotiating the upgrade
161 pub fn state_mut(&mut self) -> &mut TypeSet {
162 self.inner.state_mut()
163 }
164
165 /// Borrows the underlying transport
166 pub fn transport(&self) -> &dyn Transport {
167 self.inner.transport().as_ref()
168 }
169
170 /// Mutably borrow the underlying transport
171 ///
172 /// This returns a tuple of (buffered bytes, transport) in order to make salient the requirement
173 /// to handle any buffered bytes before using the transport directly.
174 pub fn transport_mut(&mut self) -> (&[u8], &mut dyn Transport) {
175 let (buffer, transport) = self.inner.buffer_and_transport_mut();
176 (&*buffer, &mut **transport)
177 }
178
179 /// Consumes self, returning the underlying transport
180 ///
181 /// This returns a tuple of (buffered bytes, transport) in order to make salient the requirement
182 /// to handle any buffered bytes before using the transport directly.
183 pub fn into_transport(mut self) -> (Vec<u8>, Box<dyn Transport>) {
184 let buffer = self.inner.take_buffer();
185 (buffer, self.inner.into_transport())
186 }
187
188 /// Returns a copy of the peer IP address of the connection, if available
189 pub fn peer_ip(&self) -> Option<IpAddr> {
190 self.inner.peer_ip()
191 }
192
193 /// Borrows the :authority HTTP/3 pseudo-header
194 pub fn authority(&self) -> Option<&str> {
195 self.inner.authority()
196 }
197
198 /// Borrows the :scheme HTTP/3 pseudo-header
199 pub fn scheme(&self) -> Option<&str> {
200 self.inner.scheme()
201 }
202
203 /// Borrows the :protocol HTTP/3 pseudo-header
204 pub fn protocol(&self) -> Option<&str> {
205 self.inner.protocol()
206 }
207
208 /// Borrows the HTTP version
209 pub fn http_version(&self) -> &Version {
210 self.inner.http_version()
211 }
212
213 /// Returns a copy of whether this connection was deemed secure by the handler stack
214 pub fn is_secure(&self) -> bool {
215 self.inner.is_secure()
216 }
217
218 /// Borrows the shared state [`TypeSet`] for this application
219 pub fn shared_state(&self) -> &TypeSet {
220 self.inner.shared_state()
221 }
222
223 /// Returns the HTTP request path up to but excluding any query component
224 ///
225 /// As with [`Conn::path`][crate::Conn::path], this may not represent the entire http request
226 /// path if this upgrade is being dispatched through nested routers: after
227 /// [`push_path`][Upgrade::push_path], it returns the pushed path remainder relative to the
228 /// enclosing router mount.
229 pub fn path(&self) -> &str {
230 self.path_frames
231 .top_frame()
232 .unwrap_or_else(|| self.inner.path())
233 }
234
235 /// for router implementations. pushes a route segment onto the path, the upgrade-dispatch
236 /// analog of [`Conn::push_path`][crate::Conn::push_path] — see its documentation for the
237 /// contract shared by all of a handler's hooks.
238 ///
239 /// Takes a shared reference because [`Handler::has_upgrade`][crate::Handler::has_upgrade]
240 /// does. To make that possible while [`path`][Upgrade::path] lends out plain `&str`s, frames
241 /// removed by [`pop_path`][Upgrade::pop_path] remain allocated until the `Upgrade` drops.
242 pub fn push_path(&self, path: String) {
243 self.path_frames.push(path);
244 }
245
246 /// for router implementations. removes a route segment pushed by
247 /// [`push_path`][Upgrade::push_path], the upgrade-dispatch analog of
248 /// [`Conn::pop_path`][crate::Conn::pop_path]
249 pub fn pop_path(&self) {
250 self.path_frames.pop();
251 }
252
253 /// Retrieves the query component of the path
254 pub fn querystring(&self) -> &str {
255 self.inner.querystring()
256 }
257
258 /// Retrieves a cloned [`Swansong`] graceful shutdown controller
259 pub fn swansong(&self) -> Swansong {
260 self.inner.context().swansong().clone()
261 }
262
263 /// Retrieves a clone of the [`HttpContext`] for this upgrade
264 pub fn context(&self) -> Arc<HttpContext> {
265 self.inner.context().clone()
266 }
267
268 /// Returns a clone of the H3 connection, if any
269 pub fn h3_connection(&self) -> Option<Arc<trillium_http::h3::H3Connection>> {
270 self.inner.h3_connection().cloned()
271 }
272
273 /// Inbound trailers, populated conditionally when we have read this upgrade to completion
274 pub fn request_trailers(&self) -> Option<&Headers> {
275 self.inner.received_trailers()
276 }
277
278 /// Emit trailing headers and finish the outbound stream. Consumes `self`; further
279 /// writes are statically prevented.
280 ///
281 /// Per-protocol behavior:
282 /// - HTTP/1.1 with `Transfer-Encoding: chunked`: writes the last-chunk marker (`0\r\n`), the
283 /// trailer section, and a final CRLF, then closes the transport.
284 /// - HTTP/2: enqueues a trailing `HEADERS` frame with `END_STREAM` via the connection driver
285 /// and returns. The driver finishes the stream after draining any pending DATA frames.
286 /// - HTTP/3: encodes a trailing `HEADERS` frame via QPACK, writes it to the stream, then closes
287 /// the stream (QUIC `FIN`).
288 /// - HTTP/1.1 without chunked encoding (raw upgrade, CONNECT tunnel, websocket-over-h1):
289 /// trailers can't be expressed on the wire; dropped with a `log::warn!` and `Ok(())`
290 /// returned.
291 ///
292 /// # Errors
293 ///
294 /// Returns the underlying [`std::io::Error`] when the wire write fails, `BrokenPipe` if
295 /// the stream has already been closed, and `NotConnected` if the carried
296 /// `ProtocolSession` is missing the expected driver for h2/h3.
297 pub async fn send_trailers(self, trailers: Headers) -> std::io::Result<()> {
298 self.inner.send_trailers(trailers).await
299 }
300}
301
302#[cfg(test)]
303mod tests {
304 use super::PathFrames;
305
306 #[test]
307 fn path_frames_push_pop_and_divergence() {
308 let frames = PathFrames::default();
309 assert_eq!(frames.top_frame(), None);
310
311 frames.pop(); // empty pop is a no-op
312 assert_eq!(frames.top_frame(), None);
313
314 frames.push("a".into());
315 frames.push("b".into());
316 assert_eq!(frames.top_frame(), Some("b"));
317
318 let borrowed_before_pop = frames.top_frame().unwrap();
319 frames.pop();
320 assert_eq!(frames.top_frame(), Some("a"));
321 assert_eq!(borrowed_before_pop, "b"); // still valid after pop
322
323 frames.push("c".into()); // diverge from the popped "b"
324 assert_eq!(frames.top_frame(), Some("c"));
325
326 frames.pop();
327 frames.pop();
328 assert_eq!(frames.top_frame(), None);
329
330 frames.push("d".into());
331 assert_eq!(frames.top_frame(), Some("d"));
332 }
333}