Skip to content

Commit bef6088

Browse files
authored
fix engine websocket frame limit (#5357)
[SLOP(gpt-5)] fix(engine): raise websocket frame limit to documented cap
1 parent 5c6430e commit bef6088

5 files changed

Lines changed: 98 additions & 6 deletions

File tree

engine/artifacts/config-schema.json

Lines changed: 18 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

engine/packages/config/src/config/guard.rs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@ use schemars::JsonSchema;
22
use serde::{Deserialize, Serialize};
33
use std::{net::IpAddr, path::PathBuf};
44

5+
pub const DEFAULT_WEBSOCKET_MAX_MESSAGE_SIZE: usize = 32 * 1024 * 1024;
6+
pub const DEFAULT_WEBSOCKET_MAX_FRAME_SIZE: usize = 32 * 1024 * 1024;
7+
58
#[derive(Debug, Serialize, Deserialize, Clone, Default, JsonSchema)]
69
#[serde(deny_unknown_fields)]
710
pub struct Guard {
@@ -45,6 +48,10 @@ pub struct Guard {
4548
pub https: Option<Https>,
4649
/// Max HTTP request body size in bytes (first line of defense).
4750
pub http_max_request_body_size: Option<usize>,
51+
/// Max WebSocket message size in bytes.
52+
pub websocket_max_message_size: Option<usize>,
53+
/// Max WebSocket frame size in bytes.
54+
pub websocket_max_frame_size: Option<usize>,
4855

4956
/// Enables W3C trace context propagation (extract from incoming requests, inject into
5057
/// upstream requests/websockets).
@@ -134,6 +141,16 @@ impl Guard {
134141
self.http_max_request_body_size.unwrap_or(20 * 1024 * 1024) // 20 MiB
135142
}
136143

144+
pub fn websocket_max_message_size(&self) -> usize {
145+
self.websocket_max_message_size
146+
.unwrap_or(DEFAULT_WEBSOCKET_MAX_MESSAGE_SIZE)
147+
}
148+
149+
pub fn websocket_max_frame_size(&self) -> usize {
150+
self.websocket_max_frame_size
151+
.unwrap_or(DEFAULT_WEBSOCKET_MAX_FRAME_SIZE)
152+
}
153+
137154
pub fn trace_propagation(&self) -> bool {
138155
self.trace_propagation.unwrap_or(false)
139156
}

engine/packages/guard-core/src/proxy_service.rs

Lines changed: 53 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ use hyper::{
88
header::{HeaderName, HeaderValue},
99
};
1010
use hyper_tungstenite;
11+
use hyper_tungstenite::tungstenite::protocol::WebSocketConfig;
1112
use hyper_util::{client::legacy::Client, rt::TokioExecutor};
1213
use moka::future::Cache;
1314
use opentelemetry_http::{HeaderExtractor, HeaderInjector};
@@ -43,6 +44,12 @@ pub const X_RIVET_ERROR: HeaderName = HeaderName::from_static("x-rivet-error");
4344
const PROXY_STATE_CACHE_TTL: Duration = Duration::from_secs(60 * 60); // 1 hour
4445
const WEBSOCKET_CLOSE_LINGER: Duration = Duration::from_millis(5); // Keep TCP connection open briefly after WebSocket close
4546

47+
fn websocket_config(guard_config: &rivet_config::config::guard::Guard) -> WebSocketConfig {
48+
WebSocketConfig::default()
49+
.max_message_size(Some(guard_config.websocket_max_message_size()))
50+
.max_frame_size(Some(guard_config.websocket_max_frame_size()))
51+
}
52+
4653
// State shared across all request handlers
4754
pub struct ProxyState {
4855
config: rivet_config::Config,
@@ -483,7 +490,10 @@ impl ProxyService {
483490
// HTTP errors in a meaningful way resulting in unhelpful errors for the user
484491
if is_websocket {
485492
tracing::debug!("Upgrading client connection to WebSocket for error proxy");
486-
match hyper_tungstenite::upgrade(mock_req, None) {
493+
match hyper_tungstenite::upgrade(
494+
mock_req,
495+
Some(websocket_config(self.state.config.guard())),
496+
) {
487497
Ok((client_response, client_ws)) => {
488498
tracing::debug!("Client WebSocket upgrade for error proxy successful");
489499

@@ -1032,7 +1042,10 @@ impl ProxyService {
10321042

10331043
// Handle WebSocket upgrade properly with hyper_tungstenite
10341044
tracing::debug!(path=%req_ctx.path, "Upgrading client connection to WebSocket");
1035-
let (client_response, client_ws) = match hyper_tungstenite::upgrade(req, None) {
1045+
let (client_response, client_ws) = match hyper_tungstenite::upgrade(
1046+
req,
1047+
Some(websocket_config(self.state.config.guard())),
1048+
) {
10361049
Ok(x) => {
10371050
tracing::debug!("Client WebSocket upgrade successful");
10381051
x
@@ -1180,7 +1193,11 @@ impl ProxyService {
11801193

11811194
match tokio::time::timeout(
11821195
Duration::from_secs(5), // 5 second timeout per connection attempt
1183-
tokio_tungstenite::connect_async(ws_request),
1196+
tokio_tungstenite::connect_async_with_config(
1197+
ws_request,
1198+
Some(websocket_config(state.config.guard())),
1199+
false,
1200+
),
11841201
)
11851202
.instrument(tracing::info_span!("connect_upstream_ws"))
11861203
.await
@@ -1913,3 +1930,36 @@ impl ProxyServiceFactory {
19131930
self.state.tasks.remaining_tasks()
19141931
}
19151932
}
1933+
1934+
#[cfg(test)]
1935+
mod tests {
1936+
use super::*;
1937+
1938+
#[test]
1939+
fn websocket_config_uses_documented_limit() {
1940+
let guard_config = rivet_config::config::guard::Guard::default();
1941+
let config = websocket_config(&guard_config);
1942+
1943+
assert_eq!(
1944+
config.max_message_size,
1945+
Some(rivet_config::config::guard::DEFAULT_WEBSOCKET_MAX_MESSAGE_SIZE)
1946+
);
1947+
assert_eq!(
1948+
config.max_frame_size,
1949+
Some(rivet_config::config::guard::DEFAULT_WEBSOCKET_MAX_FRAME_SIZE)
1950+
);
1951+
}
1952+
1953+
#[test]
1954+
fn websocket_config_uses_guard_overrides() {
1955+
let guard_config = rivet_config::config::guard::Guard {
1956+
websocket_max_message_size: Some(8 * 1024 * 1024),
1957+
websocket_max_frame_size: Some(4 * 1024 * 1024),
1958+
..Default::default()
1959+
};
1960+
let config = websocket_config(&guard_config);
1961+
1962+
assert_eq!(config.max_message_size, Some(8 * 1024 * 1024));
1963+
assert_eq!(config.max_frame_size, Some(4 * 1024 * 1024));
1964+
}
1965+
}

engine/packages/guard-core/tests/common/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -467,6 +467,7 @@ pub fn create_test_config(
467467
host: None, // Use default host
468468
port: Some(0), // Use 0 to let the OS choose a port
469469
https: None, // No HTTPS by default in tests
470+
..Default::default()
470471
};
471472
mutate(&mut guard);
472473
root.guard = Some(guard);

website/src/content/docs/actors/limits.mdx

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,9 @@ These limits affect actions that use `.connect()` and [low-level WebSockets](/do
2727

2828
| Name | Soft Limit | Hard Limit | Description |
2929
|------|------------|------------|-------------|
30-
| Max incoming message size | 64 KiB | 32 MiB | Maximum size of incoming WebSocket messages. Soft limit configurable via `maxIncomingMessageSize`. |
31-
| Max outgoing message size | 1 MiB | 32 MiB | Maximum size of outgoing WebSocket messages. Soft limit configurable via `maxOutgoingMessageSize`. |
30+
| Max incoming message size | 64 KiB | 32 MiB | Maximum size of incoming WebSocket messages. Soft limit configurable via `maxIncomingMessageSize`; hard limit configurable in self-hosted engine config via `guard.websocket_max_message_size`. |
31+
| Max outgoing message size | 1 MiB | 32 MiB | Maximum size of outgoing WebSocket messages. Soft limit configurable via `maxOutgoingMessageSize`; hard limit configurable in self-hosted engine config via `guard.websocket_max_message_size`. |
32+
| Max WebSocket frame size || 32 MiB | Maximum size of a single WebSocket frame accepted by Rivet's transport edge. Configurable in self-hosted engine config via `guard.websocket_max_frame_size`. This is not configurable by browser clients and is not negotiated during the WebSocket handshake. |
3233
| WebSocket open timeout || 15 seconds | Time allowed for WebSocket connection to be established, including `onBeforeConnect` and `createConnState` hooks. Connection is closed if exceeded. |
3334
| Message ack timeout || 30 seconds | Time allowed for message acknowledgment before connection is closed. Only relevant in the case of a network issue and does not affect your application. |
3435

@@ -50,7 +51,7 @@ These limits affect actions that do not use `.connect()` and [low-level HTTP req
5051
|------|------------|------------|-------------|
5152
| Max request body size || 20 MiB | Maximum size of HTTP request bodies. |
5253
| Max response body size || 20 MiB | Maximum size of HTTP response bodies. |
53-
| Request timeout | | 5 minutes | Maximum time for a request to complete. |
54+
| Request timeout | 60 seconds || Maximum time for an `onRequest` handler to complete. Defaults to `actionTimeout`; configure with `actionTimeout`. |
5455

5556
### Networking
5657

@@ -123,12 +124,17 @@ See [Actor Input](/docs/actors/input) for details.
123124
| Name | Soft Limit | Hard Limit | Description |
124125
|------|------------|------------|-------------|
125126
| Action timeout | 60 seconds || Timeout for RPC actions. Configurable via `actionTimeout`. |
127+
| On request timeout | 60 seconds || Timeout for raw `onRequest` handlers. Defaults to `actionTimeout`; configure with `actionTimeout`. |
128+
| On before connect timeout | 5 seconds || Timeout for the `onBeforeConnect` hook. Configurable via `onBeforeConnectTimeout`. |
126129
| Create vars timeout | 5 seconds || Timeout for `createVars` hook. Configurable via `createVarsTimeout`. |
127130
| Create conn state timeout | 5 seconds || Timeout for `createConnState` hook. Configurable via `createConnStateTimeout`. |
128131
| On connect timeout | 5 seconds || Timeout for `onConnect` hook. Configurable via `onConnectTimeout`. |
132+
| On migrate timeout | 30 seconds || Timeout for `onMigrate` hook. Configurable via `onMigrateTimeout`. |
129133
| Sleep grace period | 15 seconds || Total graceful shutdown budget for both sleep and destroy. Covers `onSleep`/`onDestroy`, run handler shutdown, `waitUntil`, `keepAwake`, async raw WebSocket handlers, and connection cleanup. Configurable via `sleepGracePeriod`. |
130134
| Sleep timeout | 30 seconds || Time of inactivity before actor hibernates. Configurable via `sleepTimeout`. |
131135
| State save interval | 1 second || Interval between automatic state saves. Configurable via `stateSaveInterval`. |
136+
| Connection liveness timeout | 2.5 seconds || Time a stateful connection can miss liveness checks before RivetKit treats it as unhealthy. Configurable via `connectionLivenessTimeout`. |
137+
| Connection liveness interval | 5 seconds || Interval between stateful connection liveness checks. Configurable via `connectionLivenessInterval`. |
132138

133139
### Serverless Shutdown
134140

0 commit comments

Comments
 (0)