C3: add a read timeout and message-size limits to the Binance stream

connect() used connect_async with no WebSocketConfig and next_event
awaited the socket with no deadline, so a stalled server hung the feed
forever and an oversized message could force an unbounded allocation.
connect() now passes a WebSocketConfig capping message/frame size, and
next_event wraps the read in a 300s tokio timeout (well above Binance's
~3-minute ping), surfacing a stall as the new Error::Timeout.
This commit is contained in:
kingchenc
2026-05-22 04:22:46 +02:00
parent 23e5890265
commit 62d0fb623e
2 changed files with 32 additions and 5 deletions
+4
View File
@@ -20,6 +20,10 @@ pub enum Error {
#[error("malformed payload: {0}")]
Malformed(String),
/// A live-feed read exceeded its deadline.
#[error("read timed out")]
Timeout,
#[cfg(feature = "live-binance")]
#[error("websocket error: {0}")]
WebSocket(#[from] tokio_tungstenite::tungstenite::Error),
+28 -5
View File
@@ -18,10 +18,13 @@
//! # Ok(()) }
//! ```
use std::time::Duration;
use futures_util::SinkExt;
use futures_util::StreamExt;
use serde::Deserialize;
use tokio::net::TcpStream;
use tokio_tungstenite::tungstenite::protocol::WebSocketConfig;
use tokio_tungstenite::tungstenite::Message;
use tokio_tungstenite::MaybeTlsStream;
use tokio_tungstenite::WebSocketStream;
@@ -29,6 +32,18 @@ use tokio_tungstenite::WebSocketStream;
use crate::error::{Error, Result};
use wickra_core::Candle;
/// Maximum time to wait for the next WebSocket frame before treating the
/// connection as stalled. Binance pings roughly every 3 minutes, so a healthy
/// but quiet stream stays comfortably inside this window.
const READ_TIMEOUT: Duration = Duration::from_secs(300);
/// Upper bound on an inbound WebSocket message. Kline frames are tiny; this
/// only caps a pathological or hostile server from forcing an unbounded alloc.
const MAX_MESSAGE_SIZE: usize = 8 << 20;
/// Upper bound on a single inbound WebSocket frame.
const MAX_FRAME_SIZE: usize = 2 << 20;
/// Supported Binance kline intervals. The `as_str` value matches Binance's
/// wire-format strings (`"1m"`, `"5m"`, `"1h"`, etc.).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
@@ -161,7 +176,14 @@ impl BinanceKlineStream {
streams.join("/")
);
let url = url::Url::parse(&url).map_err(|e| Error::Malformed(e.to_string()))?;
let (socket, _) = tokio_tungstenite::connect_async(url.as_str()).await?;
let ws_config = WebSocketConfig {
max_message_size: Some(MAX_MESSAGE_SIZE),
max_frame_size: Some(MAX_FRAME_SIZE),
..WebSocketConfig::default()
};
let (socket, _) =
tokio_tungstenite::connect_async_with_config(url.as_str(), Some(ws_config), false)
.await?;
Ok(Self {
socket,
interval,
@@ -182,13 +204,14 @@ impl BinanceKlineStream {
return Ok(None);
}
loop {
let msg = match self.socket.next().await {
Some(Ok(m)) => m,
Some(Err(e)) => return Err(Error::from(e)),
None => {
let msg = match tokio::time::timeout(READ_TIMEOUT, self.socket.next()).await {
Ok(Some(Ok(m))) => m,
Ok(Some(Err(e))) => return Err(Error::from(e)),
Ok(None) => {
self.closed = true;
return Ok(None);
}
Err(_elapsed) => return Err(Error::Timeout),
};
match msg {
Message::Text(text) => {