Skip to main content

rivet_envoy_client/
http.rs

1use std::collections::{HashMap, VecDeque};
2use std::sync::Arc;
3
4use anyhow::{Context, Result as AnyhowResult, ensure};
5use parking_lot::Mutex as SyncMutex;
6use rivet_envoy_protocol as protocol;
7use tokio::sync::{Mutex, Notify, mpsc, watch};
8
9pub const HTTP_BODY_STREAM_CHANNEL_CAPACITY: usize = 16;
10pub const HTTP_BODY_MAX_CHUNK_SIZE: usize = 64 * 1024;
11
12#[derive(Clone, Debug)]
13pub struct HttpRequestBodyError {
14	pub reason: protocol::HttpStreamAbortReason,
15}
16
17impl std::fmt::Display for HttpRequestBodyError {
18	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
19		match &self.reason.detail {
20			Some(detail) => write!(f, "{:?}: {detail}", self.reason.kind),
21			None => write!(f, "{:?}", self.reason.kind),
22		}
23	}
24}
25
26impl std::error::Error for HttpRequestBodyError {}
27
28#[derive(Debug)]
29pub struct HttpRequestBodyStream {
30	source: HttpRequestBodySource,
31	event_tx: Option<mpsc::UnboundedSender<RequestBodyEvent>>,
32	terminal: bool,
33}
34
35#[derive(Debug)]
36enum HttpRequestBodySource {
37	Legacy {
38		rx: mpsc::Receiver<Vec<u8>>,
39		abort_rx: watch::Receiver<Option<HttpRequestBodyError>>,
40	},
41	FlowControlled(Arc<HttpRequestBodyQueue>),
42}
43
44#[derive(Debug)]
45struct HttpRequestBodyQueueState {
46	chunks: VecDeque<Vec<u8>>,
47	finished: bool,
48	abort: Option<HttpRequestBodyError>,
49	receiver_closed: bool,
50}
51
52/// Byte-windowed request bodies use a coalescing queue instead of a frame-count bounded channel.
53/// The producer is the synchronous actor inbox, so this is a deliberately short forced-sync lock;
54/// it is never held across an await. Byte credit separately caps the total queued payload.
55#[derive(Debug)]
56pub(crate) struct HttpRequestBodyQueue {
57	state: SyncMutex<HttpRequestBodyQueueState>,
58	ready: Notify,
59}
60
61impl HttpRequestBodyQueue {
62	pub(crate) fn new() -> Arc<Self> {
63		Arc::new(Self {
64			state: SyncMutex::new(HttpRequestBodyQueueState {
65				chunks: VecDeque::new(),
66				finished: false,
67				abort: None,
68				receiver_closed: false,
69			}),
70			ready: Notify::new(),
71		})
72	}
73
74	pub(crate) fn push(&self, mut data: Vec<u8>) -> bool {
75		let mut state = self.state.lock();
76		if state.finished || state.abort.is_some() || state.receiver_closed {
77			return false;
78		}
79
80		if let Some(last) = state.chunks.back_mut() {
81			let available = HTTP_BODY_MAX_CHUNK_SIZE.saturating_sub(last.len());
82			let append = available.min(data.len());
83			last.extend_from_slice(&data[..append]);
84			data.drain(..append);
85		}
86		for chunk in data.chunks(HTTP_BODY_MAX_CHUNK_SIZE) {
87			state.chunks.push_back(chunk.to_vec());
88		}
89		drop(state);
90		self.ready.notify_one();
91		true
92	}
93
94	pub(crate) fn finish(&self) {
95		let mut state = self.state.lock();
96		if state.abort.is_none() {
97			state.finished = true;
98		}
99		drop(state);
100		self.ready.notify_waiters();
101	}
102
103	pub(crate) fn abort(&self, error: HttpRequestBodyError) {
104		let mut state = self.state.lock();
105		if state.abort.is_none() && !state.finished {
106			state.chunks.clear();
107			state.abort = Some(error);
108		}
109		drop(state);
110		self.ready.notify_waiters();
111	}
112
113	fn close_receiver(&self) {
114		let mut state = self.state.lock();
115		state.receiver_closed = true;
116		state.chunks.clear();
117		drop(state);
118		self.ready.notify_waiters();
119	}
120
121	async fn recv(&self) -> Result<Option<Vec<u8>>, HttpRequestBodyError> {
122		loop {
123			let notified = self.ready.notified();
124			{
125				let mut state = self.state.lock();
126				if let Some(error) = &state.abort {
127					return Err(error.clone());
128				}
129				if let Some(chunk) = state.chunks.pop_front() {
130					return Ok(Some(chunk));
131				}
132				if state.finished || state.receiver_closed {
133					return Ok(None);
134				}
135			}
136			notified.await;
137		}
138	}
139}
140
141#[derive(Debug)]
142pub(crate) enum RequestBodyEvent {
143	Consumed(u64),
144	Cancelled,
145}
146
147impl HttpRequestBodyStream {
148	fn handle_chunk(
149		&mut self,
150		chunk: Option<Vec<u8>>,
151	) -> Result<Option<Vec<u8>>, HttpRequestBodyError> {
152		match chunk {
153			Some(chunk) => {
154				if let Some(event_tx) = &self.event_tx {
155					let _ = event_tx.send(RequestBodyEvent::Consumed(chunk.len() as u64));
156				}
157				Ok(Some(chunk))
158			}
159			None => {
160				self.terminal = true;
161				Ok(None)
162			}
163		}
164	}
165
166	pub fn new(
167		rx: mpsc::Receiver<Vec<u8>>,
168		abort_rx: watch::Receiver<Option<HttpRequestBodyError>>,
169	) -> Self {
170		Self {
171			source: HttpRequestBodySource::Legacy { rx, abort_rx },
172			event_tx: None,
173			terminal: false,
174		}
175	}
176
177	pub(crate) fn new_with_flow_control(
178		queue: Arc<HttpRequestBodyQueue>,
179		event_tx: mpsc::UnboundedSender<RequestBodyEvent>,
180	) -> Self {
181		Self {
182			source: HttpRequestBodySource::FlowControlled(queue),
183			event_tx: Some(event_tx),
184			terminal: false,
185		}
186	}
187
188	pub async fn recv(&mut self) -> Result<Option<Vec<u8>>, HttpRequestBodyError> {
189		let chunk = match &mut self.source {
190			HttpRequestBodySource::Legacy { rx, abort_rx } => loop {
191				if let Some(error) = abort_rx.borrow().clone() {
192					self.terminal = true;
193					return Err(error);
194				}
195
196				tokio::select! {
197					biased;
198					changed = abort_rx.changed() => {
199						if changed.is_ok() {
200							continue;
201						}
202						break rx.recv().await;
203					}
204					chunk = rx.recv() => break chunk,
205				}
206			},
207			HttpRequestBodySource::FlowControlled(queue) => match queue.recv().await {
208				Ok(chunk) => chunk,
209				Err(error) => {
210					self.terminal = true;
211					return Err(error);
212				}
213			},
214		};
215		self.handle_chunk(chunk)
216	}
217}
218
219impl Drop for HttpRequestBodyStream {
220	fn drop(&mut self) {
221		if let HttpRequestBodySource::FlowControlled(queue) = &self.source {
222			queue.close_receiver();
223		}
224		if !self.terminal
225			&& let Some(event_tx) = &self.event_tx
226		{
227			let _ = event_tx.send(RequestBodyEvent::Cancelled);
228		}
229	}
230}
231
232#[derive(Debug)]
233struct HttpBodySendWindowState {
234	sent_bytes: u64,
235	consumed_bytes: u64,
236}
237
238#[derive(Debug)]
239pub(crate) struct HttpBodySendWindow {
240	state: Mutex<HttpBodySendWindowState>,
241	credit_available: Notify,
242}
243
244impl HttpBodySendWindow {
245	pub(crate) fn new() -> Arc<Self> {
246		Arc::new(Self {
247			state: Mutex::new(HttpBodySendWindowState {
248				sent_bytes: 0,
249				consumed_bytes: 0,
250			}),
251			credit_available: Notify::new(),
252		})
253	}
254
255	pub(crate) async fn reserve(&self, bytes: u64) -> AnyhowResult<()> {
256		ensure!(
257			bytes <= protocol::HTTP_STREAM_INITIAL_WINDOW_BYTES,
258			"HTTP body frame exceeds the flow-control window"
259		);
260		if bytes == 0 {
261			return Ok(());
262		}
263
264		loop {
265			let notified = self.credit_available.notified();
266			{
267				let mut state = self.state.lock().await;
268				let outstanding = state
269					.sent_bytes
270					.checked_sub(state.consumed_bytes)
271					.context("HTTP body flow-control accounting underflow")?;
272				let available = protocol::HTTP_STREAM_INITIAL_WINDOW_BYTES
273					.checked_sub(outstanding)
274					.context("HTTP body flow-control window exceeded")?;
275				if bytes <= available {
276					state.sent_bytes = state
277						.sent_bytes
278						.checked_add(bytes)
279						.context("HTTP body sent-byte counter overflow")?;
280					return Ok(());
281				}
282			}
283			notified.await;
284		}
285	}
286
287	pub(crate) async fn update_consumed(&self, consumed_bytes: u64) -> AnyhowResult<()> {
288		let mut state = self.state.lock().await;
289		ensure!(
290			consumed_bytes >= state.consumed_bytes,
291			"HTTP body consumed-byte counter moved backwards"
292		);
293		ensure!(
294			consumed_bytes <= state.sent_bytes,
295			"HTTP body consumed-byte counter exceeds sent bytes"
296		);
297		if consumed_bytes == state.consumed_bytes {
298			return Ok(());
299		}
300		state.consumed_bytes = consumed_bytes;
301		drop(state);
302		self.credit_available.notify_waiters();
303		Ok(())
304	}
305}
306
307/// HTTP request/response types used by the envoy client.
308pub struct HttpRequest {
309	pub method: String,
310	pub path: String,
311	pub headers: HashMap<String, String>,
312	pub body: Option<Vec<u8>>,
313	/// If the request is streamed, body chunks arrive on this channel.
314	pub body_stream: Option<HttpRequestBodyStream>,
315}
316
317pub struct HttpResponse {
318	pub status: u16,
319	pub headers: HashMap<String, String>,
320	pub body: Option<Vec<u8>>,
321	/// If set, the response is streamed. The envoy client reads chunks and sends
322	/// `ToRivetResponseChunk` for each one.
323	pub body_stream: Option<HttpResponseBodyStream>,
324}
325
326/// A chunk in a streaming HTTP response.
327pub enum ResponseChunk {
328	Data { data: Vec<u8>, finish: bool },
329	Error(String),
330}
331
332pub struct HttpResponseBodyStream {
333	rx: mpsc::Receiver<ResponseChunk>,
334	on_drop: Option<Box<dyn FnOnce() + Send>>,
335}
336
337impl HttpResponseBodyStream {
338	pub fn set_on_drop(&mut self, on_drop: impl FnOnce() + Send + 'static) {
339		self.on_drop = Some(Box::new(on_drop));
340	}
341
342	pub async fn recv(&mut self) -> Option<ResponseChunk> {
343		self.rx.recv().await
344	}
345}
346
347impl From<mpsc::Receiver<ResponseChunk>> for HttpResponseBodyStream {
348	fn from(rx: mpsc::Receiver<ResponseChunk>) -> Self {
349		Self { rx, on_drop: None }
350	}
351}
352
353impl Drop for HttpResponseBodyStream {
354	fn drop(&mut self) {
355		if let Some(on_drop) = self.on_drop.take() {
356			on_drop();
357		}
358	}
359}
360
361#[cfg(test)]
362mod tests {
363	use std::time::Duration;
364
365	use super::*;
366
367	#[tokio::test]
368	async fn request_body_returns_credit_only_when_consumed() {
369		let queue = HttpRequestBodyQueue::new();
370		let (event_tx, mut event_rx) = mpsc::unbounded_channel();
371		let mut body = HttpRequestBodyStream::new_with_flow_control(queue.clone(), event_tx);
372		assert!(queue.push(vec![1, 2, 3]));
373
374		assert!(event_rx.try_recv().is_err());
375		assert_eq!(body.recv().await.expect("read body"), Some(vec![1, 2, 3]));
376		assert!(matches!(
377			event_rx.recv().await,
378			Some(RequestBodyEvent::Consumed(3))
379		));
380	}
381
382	#[tokio::test]
383	async fn dropping_unfinished_request_body_emits_upload_cancel() {
384		let queue = HttpRequestBodyQueue::new();
385		let (event_tx, mut event_rx) = mpsc::unbounded_channel();
386		let body = HttpRequestBodyStream::new_with_flow_control(queue, event_tx);
387
388		drop(body);
389
390		assert!(matches!(
391			event_rx.recv().await,
392			Some(RequestBodyEvent::Cancelled)
393		));
394	}
395
396	#[tokio::test]
397	async fn request_body_coalesces_tiny_frames_within_the_byte_window() {
398		let queue = HttpRequestBodyQueue::new();
399		let (event_tx, mut event_rx) = mpsc::unbounded_channel();
400		let mut body = HttpRequestBodyStream::new_with_flow_control(queue.clone(), event_tx);
401		for byte in 0..=255u8 {
402			assert!(queue.push(vec![byte]));
403		}
404		queue.finish();
405
406		let chunk = body.recv().await.expect("read coalesced body").unwrap();
407		assert_eq!(chunk, (0..=255u8).collect::<Vec<_>>());
408		assert!(matches!(
409			event_rx.recv().await,
410			Some(RequestBodyEvent::Consumed(256))
411		));
412		assert_eq!(body.recv().await.expect("read request eof"), None);
413	}
414
415	#[tokio::test]
416	async fn response_body_window_blocks_until_consumption_is_acknowledged() {
417		let window = HttpBodySendWindow::new();
418		window
419			.reserve(protocol::HTTP_STREAM_INITIAL_WINDOW_BYTES)
420			.await
421			.expect("reserve initial window");
422		let blocked = tokio::spawn({
423			let window = window.clone();
424			async move { window.reserve(1).await }
425		});
426		assert!(
427			tokio::time::timeout(Duration::from_millis(20), async {
428				while !blocked.is_finished() {
429					tokio::task::yield_now().await;
430				}
431			})
432			.await
433			.is_err()
434		);
435
436		window
437			.update_consumed(1)
438			.await
439			.expect("return one byte of credit");
440		blocked
441			.await
442			.expect("join blocked reservation")
443			.expect("reserve after credit");
444	}
445
446	#[tokio::test]
447	async fn response_body_window_rejects_regression_and_over_credit() {
448		let window = HttpBodySendWindow::new();
449		window.reserve(10).await.expect("reserve bytes");
450		window.update_consumed(5).await.expect("consume bytes");
451		assert!(window.update_consumed(4).await.is_err());
452		assert!(window.update_consumed(11).await.is_err());
453		window
454			.update_consumed(5)
455			.await
456			.expect("duplicate cumulative ack");
457	}
458}