pub struct WebSocketDispatcher { /* private fields */ }Expand description
WebSocket-based A2A dispatcher.
Accepts WebSocket connections and processes JSON-RPC 2.0 messages over the WebSocket channel. Streaming responses are sent as individual text frames.
Incoming messages are capped at 4 MiB at the WebSocket protocol level; a connection sending a larger message or frame is terminated.
§Authentication, tenancy, and headers
The HTTP headers of the upgrade request that establishes the connection
(lowercased, plus the request path under ":path") are captured during the
handshake and passed to the handler for every request on the
connection. Tenant resolvers and interceptors therefore see the same header
context they would on the HTTP bindings — credentials are presented once,
at connect time, and apply to the whole connection.
An upgrade request carrying an A2A-Version header with a major version
other than 1 is rejected during the handshake with HTTP 400.
Implementations§
Source§impl WebSocketDispatcher
impl WebSocketDispatcher
Sourcepub async fn serve_with_shutdown(
self: Arc<WebSocketDispatcher>,
listener: TcpListener,
shutdown: impl Future<Output = ()> + Send,
) -> ServeReport
pub async fn serve_with_shutdown( self: Arc<WebSocketDispatcher>, listener: TcpListener, shutdown: impl Future<Output = ()> + Send, ) -> ServeReport
Serves until shutdown resolves, then ends the work in flight and
closes the connections, in the order
Server::serve_with_shutdown
uses for the HTTP bindings:
- Stop accepting. The listener is closed. Open connections keep being read, and requests on them admitted, through step 2 — the same as the HTTP server.
- End in-flight tasks with
RequestHandler::finish_in_flight: up towith_completion_gracefor them to finish on their own, then their cancellation tokens fire and executors getwith_task_graceto act on it. A subscription whose task ends gets its terminal event. - Close the connections. Each stops reading, lets the requests it
already read finish, and sends a Close frame — for up to
with_drain_timeoutin all.
A WebSocket is one long-lived connection carrying many requests, so unlike HTTP it has no natural end between requests: without step 3 a client that stays connected would hold the drain open until its deadline.
It takes a bound listener so the caller knows the address — port 0
included — before serving starts. Call
RequestHandler::shutdown after it
returns.
Source§impl WebSocketDispatcher
impl WebSocketDispatcher
Sourcepub const fn new(handler: Arc<RequestHandler>) -> WebSocketDispatcher
pub const fn new(handler: Arc<RequestHandler>) -> WebSocketDispatcher
Creates a new WebSocket dispatcher.
Sourcepub const fn accept_missing_version_header(self) -> WebSocketDispatcher
pub const fn accept_missing_version_header(self) -> WebSocketDispatcher
Accepts upgrade requests without an A2A-Version header.
Spec §3.6.2 interprets a missing/empty header as protocol 0.3, which this server does not implement, so the strict default rejects such handshakes (parity with the HTTP dispatchers). This opt-out restores the tolerant pre-0.7 behavior.
Sourcepub const fn with_handshake_timeout(
self,
timeout: Duration,
) -> WebSocketDispatcher
pub const fn with_handshake_timeout( self, timeout: Duration, ) -> WebSocketDispatcher
Overrides the handshake timeout (default: 10 seconds).
A peer that does not complete the WebSocket handshake within this bound is disconnected.
Sourcepub const fn with_max_connections(self, max: usize) -> WebSocketDispatcher
pub const fn with_max_connections(self, max: usize) -> WebSocketDispatcher
Caps the connections served at once. Default: unbounded.
Without this the accept loop spawns a task per accepted socket with no
ceiling, exactly as
serve did before ServeConfig existed.
Measured on 2026-08-19: 400 idle handshaken connections were accepted
and held, none refused, the ceiling being the process’s file-descriptor
table.
The permit is taken before accept(), so load past the ceiling
waits in the kernel’s listen backlog and is refused by the kernel when
that fills — a far better failure than an unbounded task spawn that
turns a traffic spike into an OOM.
Unbounded stays the default for the same reason it does on
ServeConfig::max_connections: a
deployment may genuinely want no ceiling, and picking one for it is
picking its capacity.
Sourcepub const fn with_idle_timeout(self, timeout: Duration) -> WebSocketDispatcher
pub const fn with_idle_timeout(self, timeout: Duration) -> WebSocketDispatcher
Closes a connection that carries no traffic in either direction for
timeout. Default: off.
with_handshake_timeout bounds a peer
that connects and never upgrades. Nothing bounded the peer that
completes the handshake and then goes silent, so one that did held a
task, a socket and a file descriptor for the life of the process —
measured, a connection idle for 12 seconds was still being served, and
the read loop has no bound at all.
§Why this is off by default when the HTTP one is on
On HTTP, silence means nothing is happening. On a WebSocket it may mean a subscription is waiting for its next event, which is a legitimate thing to do for hours. A timeout defaulted on would close healthy subscriptions, and a knob that breaks correct programs is a knob nobody turns on.
What makes it safe to turn on: at the halfway point of the budget the server sends a WebSocket Ping. Every conformant client library — this SDK’s included, via tungstenite — answers automatically, and that Pong is traffic. So the timeout closes peers that are unresponsive, not peers that are merely quiet. Only a client that has stopped reading its socket, or gone away without a close frame, fails to answer.
Outbound frames count too, so a stream pushing events to a silent consumer keeps its own connection alive.
DEFAULT_WS_IDLE_TIMEOUT (75s, matching the HTTP default) is a
reasonable starting point.
Sourcepub const fn with_completion_grace(self, grace: Duration) -> WebSocketDispatcher
pub const fn with_completion_grace(self, grace: Duration) -> WebSocketDispatcher
How long in-flight tasks get to finish on their own once
serve_with_shutdown’s signal fires,
before they are cancelled. Default
DEFAULT_COMPLETION_GRACE;
the HTTP server’s
ServeConfig::with_completion_grace
is the same setting.
Sourcepub const fn with_task_grace(self, grace: Duration) -> WebSocketDispatcher
pub const fn with_task_grace(self, grace: Duration) -> WebSocketDispatcher
How long cancelled tasks get to act on their cancellation before the
connections are closed. Default
DEFAULT_TASK_GRACE.
Sourcepub const fn with_drain_timeout(self, timeout: Duration) -> WebSocketDispatcher
pub const fn with_drain_timeout(self, timeout: Duration) -> WebSocketDispatcher
How long serve_with_shutdown waits for
connections to close once they have been told to. Default
DEFAULT_DRAIN_TIMEOUT.
Sourcepub async fn serve(
self: Arc<WebSocketDispatcher>,
addr: impl ToSocketAddrs,
) -> Result<(), Error>
pub async fn serve( self: Arc<WebSocketDispatcher>, addr: impl ToSocketAddrs, ) -> Result<(), Error>
Starts a WebSocket server on the given address.
The accept loop never terminates on transient accept() errors
(per-connection aborts, fd-table exhaustion) — it logs, backs off when
the fd table is full, and keeps accepting.
It takes no shutdown signal; use
serve_with_shutdown for a graceful
stop.
§Errors
Returns std::io::Error if the TCP listener fails to bind.
Sourcepub async fn serve_with_addr(
self: Arc<WebSocketDispatcher>,
addr: impl ToSocketAddrs,
) -> Result<SocketAddr, Error>
pub async fn serve_with_addr( self: Arc<WebSocketDispatcher>, addr: impl ToSocketAddrs, ) -> Result<SocketAddr, Error>
Starts a WebSocket server and returns the bound address.
Like serve, but useful for tests (bind to port 0).
§Errors
Returns std::io::Error if the TCP listener fails to bind.
Auto Trait Implementations§
impl !RefUnwindSafe for WebSocketDispatcher
impl !UnwindSafe for WebSocketDispatcher
impl Freeze for WebSocketDispatcher
impl Send for WebSocketDispatcher
impl Sync for WebSocketDispatcher
impl Unpin for WebSocketDispatcher
impl UnsafeUnpin for WebSocketDispatcher
Blanket Implementations§
Source§impl<T> BorrowMut<T> for Twhere
T: ?Sized,
impl<T> BorrowMut<T> for Twhere
T: ?Sized,
Source§fn borrow_mut(&mut self) -> &mut T
fn borrow_mut(&mut self) -> &mut T
impl<ST, DT> CastableFrom<ST, Initialized, Initialized> for DT
impl<ST, DT> CastableFrom<ST, Uninit, Uninit> for DT
Source§impl<T> FutureExt for T
impl<T> FutureExt for T
Source§fn with_context(self, otel_cx: Context) -> WithContext<Self> ⓘ
fn with_context(self, otel_cx: Context) -> WithContext<Self> ⓘ
Source§fn with_current_context(self) -> WithContext<Self> ⓘ
fn with_current_context(self) -> WithContext<Self> ⓘ
Source§impl<T> Instrument for T
impl<T> Instrument for T
Source§fn instrument(self, span: Span) -> Instrumented<Self> ⓘ
fn instrument(self, span: Span) -> Instrumented<Self> ⓘ
Source§fn in_current_span(self) -> Instrumented<Self> ⓘ
fn in_current_span(self) -> Instrumented<Self> ⓘ
Source§impl<T> IntoEither for T
impl<T> IntoEither for T
Source§fn into_either(self, into_left: bool) -> Either<Self, Self> ⓘ
fn into_either(self, into_left: bool) -> Either<Self, Self> ⓘ
self into a Left variant of Either<Self, Self>
if into_left is true.
Converts self into a Right variant of Either<Self, Self>
otherwise. Read moreSource§fn into_either_with<F>(self, into_left: F) -> Either<Self, Self> ⓘ
fn into_either_with<F>(self, into_left: F) -> Either<Self, Self> ⓘ
self into a Left variant of Either<Self, Self>
if into_left(&self) returns true.
Converts self into a Right variant of Either<Self, Self>
otherwise. Read moreSource§impl<T> IntoRequest<T> for T
impl<T> IntoRequest<T> for T
Source§fn into_request(self) -> Request<T>
fn into_request(self) -> Request<T>
T in a tonic::Request