Skip to main content

rivet_envoy_client/
tunnel.rs

1use rivet_envoy_protocol as protocol;
2use std::collections::HashMap;
3use std::time::Duration;
4
5use crate::connection::ws_send;
6use crate::envoy::{BufferedActorMessage, EnvoyContext, HttpRequestRoute, WebSocketRoute};
7
8const HTTP_REQUEST_CANCELLATION_TTL: Duration = Duration::from_secs(60);
9
10#[derive(Clone, Debug, Eq, Hash, PartialEq)]
11pub struct HttpRequestCancellationKey {
12	gateway_id: protocol::GatewayId,
13	request_id: protocol::RequestId,
14	actor_id: String,
15	actor_generation: u32,
16}
17
18fn request_cancellation_key(
19	message_id: &protocol::MessageId,
20	actor_id: &str,
21	actor_generation: u32,
22) -> HttpRequestCancellationKey {
23	HttpRequestCancellationKey {
24		gateway_id: message_id.gateway_id,
25		request_id: message_id.request_id,
26		actor_id: actor_id.to_owned(),
27		actor_generation,
28	}
29}
30
31fn request_abort_cancellation_key(
32	message_id: &protocol::MessageId,
33	abort: &protocol::ToEnvoyRequestAbort,
34) -> Result<Option<HttpRequestCancellationKey>, &'static str> {
35	match (&abort.actor_id, abort.actor_generation) {
36		(Some(actor_id), Some(actor_generation)) => Ok(Some(request_cancellation_key(
37			message_id,
38			actor_id,
39			actor_generation,
40		))),
41		(None, None) => Ok(None),
42		_ => Err("HTTP request abort must include both actor id and generation"),
43	}
44}
45
46fn prune_http_request_cancellations(ctx: &mut EnvoyContext) {
47	let now = crate::time::Instant::now();
48	ctx.http_request_cancellations.retain(|_, cancelled_at| {
49		now.duration_since(*cancelled_at) < HTTP_REQUEST_CANCELLATION_TTL
50	});
51}
52
53pub(crate) fn make_ws_key(
54	gateway_id: &protocol::GatewayId,
55	request_id: &protocol::RequestId,
56) -> [u8; 8] {
57	let mut key = [0u8; 8];
58	key[..4].copy_from_slice(gateway_id);
59	key[4..].copy_from_slice(request_id);
60	key
61}
62
63fn advance_http_message_index(ctx: &mut EnvoyContext, message_id: &protocol::MessageId) -> bool {
64	let key: [&[u8]; 2] = [&message_id.gateway_id, &message_id.request_id];
65	let Some(expected) = ctx.http_message_indices.get_mut(&key) else {
66		tracing::warn!(
67			message_index = message_id.message_index,
68			"received HTTP tunnel message without request start"
69		);
70		return false;
71	};
72	if message_id.message_index != *expected {
73		tracing::warn!(
74			expected_message_index = *expected,
75			actual_message_index = message_id.message_index,
76			"received reordered HTTP tunnel message"
77		);
78		return false;
79	}
80	*expected = expected.wrapping_add(1);
81	true
82}
83
84pub struct HibernatingWebSocketMetadata {
85	pub gateway_id: protocol::GatewayId,
86	pub request_id: protocol::RequestId,
87	pub envoy_message_index: u16,
88	pub rivet_message_index: u16,
89	pub path: String,
90	pub headers: std::collections::HashMap<String, String>,
91}
92
93pub async fn handle_tunnel_message(
94	ctx: &mut EnvoyContext,
95	connection_session: u64,
96	msg: protocol::ToEnvoyTunnelMessage,
97) {
98	let message_id = msg.message_id;
99	let is_http_continuation = matches!(
100		&msg.message_kind,
101		protocol::ToEnvoyTunnelMessageKind::ToEnvoyRequestChunk(_)
102			| protocol::ToEnvoyTunnelMessageKind::ToEnvoyRequestAbort(_)
103			| protocol::ToEnvoyTunnelMessageKind::ToEnvoyRequestBodyCancel
104			| protocol::ToEnvoyTunnelMessageKind::ToEnvoyResponseBodyWindowUpdate(_)
105	);
106	if is_http_continuation
107		&& let Some(route) = ctx
108			.http_request_routes
109			.get(&[&message_id.gateway_id, &message_id.request_id])
110		&& route.session != connection_session
111	{
112		handle_http_protocol_violation(
113			ctx,
114			connection_session,
115			message_id,
116			"HTTP request identifier belongs to another connection",
117		)
118		.await;
119		return;
120	}
121	match msg.message_kind {
122		protocol::ToEnvoyTunnelMessageKind::ToEnvoyRequestStart(req) => {
123			handle_request_start(ctx, connection_session, message_id, req).await;
124		}
125		protocol::ToEnvoyTunnelMessageKind::ToEnvoyRequestChunk(chunk) => {
126			if advance_http_message_index(ctx, &message_id) {
127				handle_request_chunk(ctx, message_id, chunk).await;
128			} else {
129				handle_http_protocol_violation(
130					ctx,
131					connection_session,
132					message_id,
133					"invalid HTTP tunnel message sequence",
134				)
135				.await;
136			}
137		}
138		protocol::ToEnvoyTunnelMessageKind::ToEnvoyRequestAbort(abort) => {
139			let cancellation_key = match request_abort_cancellation_key(&message_id, &abort) {
140				Ok(key) => key,
141				Err(detail) => {
142					handle_http_protocol_violation(ctx, connection_session, message_id, detail)
143						.await;
144					return;
145				}
146			};
147			if let Some(cancellation_key) = cancellation_key {
148				let route = ctx
149					.http_request_routes
150					.get(&[&message_id.gateway_id, &message_id.request_id])
151					.map(|route| (route.actor_id.clone(), route.actor_generation));
152				if let Some((route_actor_id, route_actor_generation)) = route
153					&& (cancellation_key.actor_id != route_actor_id
154						|| Some(cancellation_key.actor_generation) != route_actor_generation)
155				{
156					handle_http_protocol_violation(
157						ctx,
158						connection_session,
159						message_id,
160						"HTTP request abort actor identity does not match request start",
161					)
162					.await;
163				} else {
164					// Exact-generation cancellation is intentionally out of band from the
165					// ordered body stream. It must work after an ambiguous message handoff,
166					// when Gateway cannot know which message index Envoy accepted.
167					handle_request_abort(ctx, message_id, abort, Some(cancellation_key));
168				}
169			} else if advance_http_message_index(ctx, &message_id) {
170				// V6 cancellation has no actor identity, so preserve its ordered behavior.
171				handle_request_abort(ctx, message_id, abort, None);
172			} else {
173				handle_http_protocol_violation(
174					ctx,
175					connection_session,
176					message_id,
177					"invalid HTTP request-abort sequence",
178				)
179				.await;
180			}
181		}
182		protocol::ToEnvoyTunnelMessageKind::ToEnvoyRequestBodyCancel => {
183			if advance_http_message_index(ctx, &message_id) {
184				handle_request_body_cancel(ctx, message_id);
185			} else {
186				handle_http_protocol_violation(
187					ctx,
188					connection_session,
189					message_id,
190					"invalid HTTP request-body-cancel sequence",
191				)
192				.await;
193			}
194		}
195		protocol::ToEnvoyTunnelMessageKind::ToEnvoyResponseBodyWindowUpdate(update) => {
196			if advance_http_message_index(ctx, &message_id) {
197				handle_response_body_window_update(ctx, message_id, update.consumed_bytes);
198			} else {
199				handle_http_protocol_violation(
200					ctx,
201					connection_session,
202					message_id,
203					"invalid HTTP response-window sequence",
204				)
205				.await;
206			}
207		}
208		protocol::ToEnvoyTunnelMessageKind::ToEnvoyWebSocketOpen(open) => {
209			handle_ws_open(ctx, message_id, open).await;
210		}
211		protocol::ToEnvoyTunnelMessageKind::ToEnvoyWebSocketMessage(msg) => {
212			handle_ws_message(ctx, message_id, msg);
213		}
214		protocol::ToEnvoyTunnelMessageKind::ToEnvoyWebSocketClose(close) => {
215			handle_ws_close(ctx, message_id, close);
216		}
217	}
218}
219
220async fn handle_http_protocol_violation(
221	ctx: &mut EnvoyContext,
222	connection_session: u64,
223	message_id: protocol::MessageId,
224	detail: &'static str,
225) {
226	let key: [&[u8]; 2] = [&message_id.gateway_id, &message_id.request_id];
227	if let Some(route) = ctx.http_request_routes.get(&key) {
228		if route.session != connection_session {
229			send_response_abort_for_session(ctx, connection_session, message_id, detail).await;
230			return;
231		}
232		let actor_id = route.actor_id.clone();
233		let actor_generation = route.actor_generation;
234		if route.actor_admitted
235			&& let Some(actor) = ctx.get_actor(&actor_id, actor_generation)
236		{
237			let _ = actor
238				.handle
239				.send(crate::actor::ToActor::ReqProtocolViolation {
240					message_id: message_id.clone(),
241					detail: detail.to_owned(),
242				});
243		}
244		ctx.http_request_routes.remove(&key);
245		ctx.http_message_indices.remove(&key);
246		return;
247	}
248
249	send_response_abort_for_session(ctx, connection_session, message_id, detail).await;
250}
251
252async fn send_response_abort_for_session(
253	ctx: &EnvoyContext,
254	connection_session: u64,
255	mut message_id: protocol::MessageId,
256	detail: &'static str,
257) {
258	message_id.message_index = 0;
259	let _ = crate::connection::ws_send_http_for_session(
260		&ctx.shared,
261		protocol::ToRivet::ToRivetTunnelMessage(protocol::ToRivetTunnelMessage {
262			message_id,
263			message_kind: protocol::ToRivetTunnelMessageKind::ToRivetResponseAbort(
264				protocol::ToRivetResponseAbort {
265					reason: protocol::HttpStreamAbortReason {
266						kind: protocol::HttpStreamAbortReasonKind::InternalError,
267						detail: Some(detail.to_owned()),
268					},
269				},
270			),
271		}),
272		connection_session,
273	)
274	.await;
275}
276
277fn handle_request_body_cancel(ctx: &mut EnvoyContext, message_id: protocol::MessageId) {
278	let route = ctx
279		.http_request_routes
280		.get(&[&message_id.gateway_id, &message_id.request_id])
281		.map(|route| {
282			(
283				route.actor_id.clone(),
284				route.actor_generation,
285				route.actor_admitted,
286			)
287		});
288	if let Some((actor_id, actor_generation, true)) = &route
289		&& let Some(actor) = ctx.get_actor(actor_id, *actor_generation)
290	{
291		let _ = actor.handle.send(crate::actor::ToActor::ReqBodyCancel {
292			message_id: message_id.clone(),
293		});
294	}
295	if matches!(route, Some((_, _, false))) {
296		ctx.http_request_routes
297			.remove(&[&message_id.gateway_id, &message_id.request_id]);
298		ctx.http_message_indices
299			.remove(&[&message_id.gateway_id, &message_id.request_id]);
300	}
301}
302
303fn handle_response_body_window_update(
304	ctx: &mut EnvoyContext,
305	message_id: protocol::MessageId,
306	consumed_bytes: u64,
307) {
308	let route = ctx
309		.http_request_routes
310		.get(&[&message_id.gateway_id, &message_id.request_id])
311		.map(|route| {
312			(
313				route.actor_id.clone(),
314				route.actor_generation,
315				route.actor_admitted,
316			)
317		});
318	if let Some((actor_id, actor_generation, true)) = &route
319		&& let Some(actor) = ctx.get_actor(actor_id, *actor_generation)
320	{
321		let _ = actor
322			.handle
323			.send(crate::actor::ToActor::ResponseBodyWindowUpdate {
324				message_id,
325				consumed_bytes,
326			});
327	}
328}
329
330async fn handle_request_start(
331	ctx: &mut EnvoyContext,
332	connection_session: u64,
333	message_id: protocol::MessageId,
334	req: protocol::ToEnvoyRequestStart,
335) {
336	let key: [&[u8]; 2] = [&message_id.gateway_id, &message_id.request_id];
337	let actor_id = req.actor_id.clone();
338	let actor_generation = req.actor_generation;
339	let response_stream = req.response_stream;
340	let request_stream = req.stream;
341	if message_id.message_index != 0 {
342		tracing::warn!(
343			message_index = message_id.message_index,
344			"received invalid HTTP request start sequence"
345		);
346		send_response_abort_for_session(
347			ctx,
348			connection_session,
349			message_id,
350			"HTTP request start must use message index zero",
351		)
352		.await;
353		return;
354	}
355	prune_http_request_cancellations(ctx);
356	if let Some(actor_generation) = actor_generation {
357		let cancellation_key = request_cancellation_key(&message_id, &actor_id, actor_generation);
358		if ctx
359			.http_request_cancellations
360			.contains_key(&cancellation_key)
361		{
362			tracing::debug!(
363				%actor_id,
364				actor_generation,
365				gateway_id = ?message_id.gateway_id,
366				request_id = ?message_id.request_id,
367				"discarding HTTP request start cancelled before delivery"
368			);
369			return;
370		}
371	}
372	if let Some(existing) = ctx.http_request_routes.get(&key) {
373		let same_session = existing.session == connection_session;
374		handle_http_protocol_violation(
375			ctx,
376			connection_session,
377			message_id,
378			if same_session {
379				"duplicate HTTP request start"
380			} else {
381				"HTTP request identifier belongs to another connection"
382			},
383		)
384		.await;
385		return;
386	}
387	let actor_handle = ctx
388		.get_actor_for_admission(&actor_id, actor_generation)
389		.map(|actor| actor.handle.clone());
390
391	let Some(actor_handle) = actor_handle else {
392		let generation_mismatch =
393			actor_generation.is_some() && ctx.get_actor(&actor_id, None).is_some();
394		let (error_code, message) = if generation_mismatch {
395			tracing::warn!(
396				actor_id = %actor_id,
397				?actor_generation,
398				"received request for stale actor generation"
399			);
400			(
401				"envoy.actor_generation_mismatch",
402				"Actor generation does not match",
403			)
404		} else {
405			tracing::warn!(actor_id = %actor_id, ?actor_generation, "received request for unknown actor");
406			("envoy.actor_not_found", "Actor not found")
407		};
408		// RequestStart is deliberately not application-acknowledged, so body frames may already be
409		// in flight when this rejection reaches Gateway. Keep a short-lived drain route only for a
410		// streamed upload; its finish, cancellation, abort, or connection close removes the state.
411		if request_stream {
412			ctx.http_message_indices.insert(&key, 1);
413			ctx.http_request_routes.insert(
414				&key,
415				HttpRequestRoute {
416					actor_id: actor_id.clone(),
417					actor_generation,
418					actor_admitted: false,
419					session: connection_session,
420					gateway_id: message_id.gateway_id,
421					request_id: message_id.request_id,
422				},
423			);
424		}
425		send_error_response(
426			ctx,
427			response_stream.then_some(connection_session),
428			message_id.gateway_id,
429			message_id.request_id,
430			error_code,
431			message,
432			actor_generation.map(|generation| (actor_id.as_str(), generation)),
433		)
434		.await;
435		return;
436	};
437	ctx.http_message_indices
438		.insert(&[&message_id.gateway_id, &message_id.request_id], 1);
439
440	ctx.http_request_routes.insert(
441		&[&message_id.gateway_id, &message_id.request_id],
442		HttpRequestRoute {
443			actor_id: actor_id.clone(),
444			actor_generation,
445			actor_admitted: true,
446			session: connection_session,
447			gateway_id: message_id.gateway_id,
448			request_id: message_id.request_id,
449		},
450	);
451
452	if actor_handle
453		.send(crate::actor::ToActor::ReqStart {
454			message_id: message_id.clone(),
455			req,
456			connection_session,
457		})
458		.is_err()
459	{
460		ctx.http_request_routes
461			.remove(&[&message_id.gateway_id, &message_id.request_id]);
462		ctx.http_message_indices
463			.remove(&[&message_id.gateway_id, &message_id.request_id]);
464		send_error_response(
465			ctx,
466			response_stream.then_some(connection_session),
467			message_id.gateway_id,
468			message_id.request_id,
469			"envoy.actor_not_found",
470			"Actor stopped before accepting request",
471			actor_generation.map(|generation| (actor_id.as_str(), generation)),
472		)
473		.await;
474	}
475}
476
477async fn handle_request_chunk(
478	ctx: &mut EnvoyContext,
479	message_id: protocol::MessageId,
480	chunk: protocol::ToEnvoyRequestChunk,
481) {
482	let route = ctx
483		.http_request_routes
484		.get(&[&message_id.gateway_id, &message_id.request_id])
485		.map(|route| {
486			(
487				route.actor_id.clone(),
488				route.actor_generation,
489				route.actor_admitted,
490			)
491		});
492
493	if let Some((actor_id, actor_generation, actor_admitted)) = &route {
494		if !actor_admitted {
495			if chunk.finish {
496				ctx.http_request_routes
497					.remove(&[&message_id.gateway_id, &message_id.request_id]);
498				ctx.http_message_indices
499					.remove(&[&message_id.gateway_id, &message_id.request_id]);
500			}
501			return;
502		}
503		if let Some(actor) = ctx.get_actor(actor_id, *actor_generation) {
504			let _ = actor.handle.send(crate::actor::ToActor::ReqChunk {
505				message_id: message_id.clone(),
506				chunk,
507			});
508		} else {
509			tracing::warn!(actor_id = %actor_id, "received request chunk for unknown actor");
510		}
511	} else {
512		tracing::warn!(
513			gateway_id = ?message_id.gateway_id,
514			request_id = ?message_id.request_id,
515			message_index = message_id.message_index,
516			"received request chunk without request start"
517		);
518		send_error_response(
519			ctx,
520			None,
521			message_id.gateway_id,
522			message_id.request_id,
523			"envoy.request_not_found",
524			"Request start was not delivered",
525			None,
526		)
527		.await;
528	}
529}
530
531fn handle_request_abort(
532	ctx: &mut EnvoyContext,
533	message_id: protocol::MessageId,
534	abort: protocol::ToEnvoyRequestAbort,
535	cancellation_key: Option<HttpRequestCancellationKey>,
536) {
537	let route = ctx
538		.http_request_routes
539		.get(&[&message_id.gateway_id, &message_id.request_id])
540		.map(|route| {
541			(
542				route.actor_id.clone(),
543				route.actor_generation,
544				route.actor_admitted,
545			)
546		});
547	if let Some((actor_id, actor_generation, true)) = &route {
548		if let Some(actor) = ctx.get_actor(actor_id, *actor_generation) {
549			let _ = actor.handle.send(crate::actor::ToActor::ReqAbort {
550				message_id: message_id.clone(),
551				reason: abort.reason,
552			});
553		}
554	}
555	if let Some(cancellation_key) = cancellation_key {
556		prune_http_request_cancellations(ctx);
557		ctx.http_request_cancellations
558			.insert(cancellation_key, crate::time::Instant::now());
559		tracing::debug!(
560			gateway_id = ?message_id.gateway_id,
561			request_id = ?message_id.request_id,
562			"recorded exact HTTP request cancellation"
563		);
564	}
565
566	ctx.http_request_routes
567		.remove(&[&message_id.gateway_id, &message_id.request_id]);
568	ctx.http_message_indices
569		.remove(&[&message_id.gateway_id, &message_id.request_id]);
570}
571
572async fn handle_ws_open(
573	ctx: &mut EnvoyContext,
574	message_id: protocol::MessageId,
575	open: protocol::ToEnvoyWebSocketOpen,
576) {
577	let actor_id = open.actor_id.clone();
578	let actor_generation = open.actor_generation;
579	let actor_handle = ctx
580		.get_actor_for_admission(&actor_id, actor_generation)
581		.map(|actor| actor.handle.clone());
582
583	let Some(actor_handle) = actor_handle else {
584		let generation_mismatch =
585			actor_generation.is_some() && ctx.get_actor(&actor_id, None).is_some();
586		let reason = if generation_mismatch {
587			tracing::warn!(
588				actor_id = %actor_id,
589				?actor_generation,
590				"received ws open for stale actor generation"
591			);
592			"envoy.actor_generation_mismatch"
593		} else {
594			tracing::warn!(actor_id = %actor_id, ?actor_generation, "received ws open for unknown actor");
595			"envoy.actor_not_found"
596		};
597
598		ws_send(
599			&ctx.shared,
600			protocol::ToRivet::ToRivetTunnelMessage(protocol::ToRivetTunnelMessage {
601				message_id,
602				message_kind: protocol::ToRivetTunnelMessageKind::ToRivetWebSocketClose(
603					protocol::ToRivetWebSocketClose {
604						code: Some(1011),
605						reason: Some(reason.to_string()),
606						hibernate: false,
607					},
608				),
609			}),
610		)
611		.await;
612		return;
613	};
614
615	ctx.request_to_actor.insert(
616		&[&message_id.gateway_id, &message_id.request_id],
617		WebSocketRoute {
618			actor_id: actor_id.clone(),
619			actor_generation,
620		},
621	);
622	ctx.shared
623		.live_tunnel_requests
624		.lock()
625		.expect("shared live tunnel request registry poisoned")
626		.insert(
627			make_ws_key(&message_id.gateway_id, &message_id.request_id),
628			actor_id.clone(),
629		);
630
631	// Convert HashMap headers to BTreeMap for the actor message
632	let headers = open
633		.headers
634		.iter()
635		.map(|(k, v)| (k.clone(), v.clone()))
636		.collect();
637
638	if actor_handle
639		.send(crate::actor::ToActor::WsOpen {
640			message_id: message_id.clone(),
641			path: open.path,
642			headers,
643		})
644		.is_err()
645	{
646		ctx.request_to_actor
647			.remove(&[&message_id.gateway_id, &message_id.request_id]);
648		ctx.shared
649			.live_tunnel_requests
650			.lock()
651			.expect("shared live tunnel request registry poisoned")
652			.remove(&make_ws_key(&message_id.gateway_id, &message_id.request_id));
653		ws_send(
654			&ctx.shared,
655			protocol::ToRivet::ToRivetTunnelMessage(protocol::ToRivetTunnelMessage {
656				message_id,
657				message_kind: protocol::ToRivetTunnelMessageKind::ToRivetWebSocketClose(
658					protocol::ToRivetWebSocketClose {
659						code: Some(1011),
660						reason: Some("envoy.actor_not_found".to_string()),
661						hibernate: false,
662					},
663				),
664			}),
665		)
666		.await;
667	}
668}
669
670fn handle_ws_message(
671	ctx: &mut EnvoyContext,
672	message_id: protocol::MessageId,
673	msg: protocol::ToEnvoyWebSocketMessage,
674) {
675	let route = ctx
676		.request_to_actor
677		.get(&[&message_id.gateway_id, &message_id.request_id])
678		.cloned();
679	if let Some(route) = &route {
680		if let Some(actor) = ctx.get_actor(&route.actor_id, route.actor_generation) {
681			let _ = actor
682				.handle
683				.send(crate::actor::ToActor::WsMsg { message_id, msg });
684		} else if route.actor_generation.is_none() {
685			ctx.buffered_actor_messages
686				.entry(route.actor_id.clone())
687				.or_default()
688				.push(BufferedActorMessage::WsMsg { message_id, msg });
689		}
690	}
691}
692
693fn handle_ws_close(
694	ctx: &mut EnvoyContext,
695	message_id: protocol::MessageId,
696	close: protocol::ToEnvoyWebSocketClose,
697) {
698	let route = ctx
699		.request_to_actor
700		.get(&[&message_id.gateway_id, &message_id.request_id])
701		.cloned();
702	if let Some(route) = &route {
703		if let Some(actor) = ctx.get_actor(&route.actor_id, route.actor_generation) {
704			let _ = actor.handle.send(crate::actor::ToActor::WsClose {
705				message_id: message_id.clone(),
706				close,
707			});
708		} else if route.actor_generation.is_none() {
709			ctx.buffered_actor_messages
710				.entry(route.actor_id.clone())
711				.or_default()
712				.push(BufferedActorMessage::WsClose {
713					message_id: message_id.clone(),
714					close,
715				});
716		}
717	}
718
719	ctx.request_to_actor
720		.remove(&[&message_id.gateway_id, &message_id.request_id]);
721	ctx.shared
722		.live_tunnel_requests
723		.lock()
724		.expect("shared live tunnel request registry poisoned")
725		.remove(&make_ws_key(&message_id.gateway_id, &message_id.request_id));
726}
727
728pub fn send_hibernatable_ws_message_ack(
729	ctx: &mut EnvoyContext,
730	gateway_id: protocol::GatewayId,
731	request_id: protocol::RequestId,
732	envoy_message_index: u16,
733) {
734	let route = ctx
735		.request_to_actor
736		.get(&[&gateway_id, &request_id])
737		.cloned();
738	if let Some(route) = &route {
739		if let Some(actor) = ctx.get_actor(&route.actor_id, route.actor_generation) {
740			let _ = actor.handle.send(crate::actor::ToActor::HwsAck {
741				gateway_id,
742				request_id,
743				envoy_message_index,
744			});
745		}
746	}
747}
748
749pub async fn resend_buffered_tunnel_messages(ctx: &mut EnvoyContext) {
750	if ctx.buffered_messages.is_empty() {
751		return;
752	}
753
754	tracing::info!(
755		count = ctx.buffered_messages.len(),
756		"resending buffered tunnel messages"
757	);
758
759	let messages = std::mem::take(&mut ctx.buffered_messages);
760	let mut messages = messages.into_iter();
761	while let Some(msg) = messages.next() {
762		let failed = ws_send(
763			&ctx.shared,
764			protocol::ToRivet::ToRivetTunnelMessage(msg.clone()),
765		)
766		.await;
767		if failed {
768			ctx.buffered_messages.push(msg);
769			ctx.buffered_messages.extend(messages);
770			break;
771		}
772	}
773}
774
775pub async fn send_or_buffer_tunnel_message(
776	ctx: &mut EnvoyContext,
777	msg: protocol::ToRivetTunnelMessage,
778) {
779	let failed = ws_send(
780		&ctx.shared,
781		protocol::ToRivet::ToRivetTunnelMessage(msg.clone()),
782	)
783	.await;
784	if failed {
785		ctx.buffered_messages.push(msg);
786	}
787}
788
789async fn send_error_response(
790	ctx: &EnvoyContext,
791	connection_session: Option<u64>,
792	gateway_id: protocol::GatewayId,
793	request_id: protocol::RequestId,
794	error_code: &str,
795	message: &str,
796	actor: Option<(&str, u32)>,
797) {
798	let code = error_code.strip_prefix("envoy.").unwrap_or(error_code);
799	let mut error = serde_json::json!({
800		"group": "envoy",
801		"code": code,
802		"message": message,
803	});
804	if let Some((actor_id, generation)) = actor {
805		error["actor"] = serde_json::json!({
806			"actorId": actor_id,
807			"generation": generation,
808		});
809	}
810	let body = serde_json::to_vec(&error).expect("serialize canonical HTTP error response");
811	let mut headers = HashMap::new();
812	headers.insert("x-rivet-error".to_string(), error_code.to_owned());
813	headers.insert("content-type".to_string(), "application/json".to_owned());
814	headers.insert("content-length".to_string(), body.len().to_string());
815
816	let response = protocol::ToRivet::ToRivetTunnelMessage(protocol::ToRivetTunnelMessage {
817		message_id: protocol::MessageId {
818			gateway_id,
819			request_id,
820			message_index: 0,
821		},
822		message_kind: protocol::ToRivetTunnelMessageKind::ToRivetResponseStart(
823			protocol::ToRivetResponseStart {
824				status: 503,
825				headers,
826				body: Some(body),
827				stream: false,
828			},
829		),
830	});
831	match connection_session {
832		Some(session) => {
833			let _ =
834				crate::connection::ws_send_http_for_session(&ctx.shared, response, session).await;
835		}
836		None => {
837			ws_send(&ctx.shared, response).await;
838		}
839	}
840}