WebSockets¶
Diana's WebSocket support mirrors Spring's WebSocketHandler /
WebSocketSession model: implement a handler interface, annotate it with a
path, and the framework wires the HTTP upgrade, the lifecycle callbacks and
dependency injection.
import 'package:diana/diana.dart';
@WebSocketController('/ws/echo')
class EchoHandler extends BaseWebSocketHandler {
@override
void onOpen(WebSocketSession session) => session.sendText('welcome');
@override
void onMessage(WebSocketSession session, WebSocketMessage message) {
if (message is TextMessage) session.sendText(message.text);
}
}
Clients connect with any WebSocket client:
The handler lifecycle¶
WebSocketHandler declares four callbacks (Spring's
afterConnectionEstablished / handleMessage / handleTransportError /
afterConnectionClosed, with shorter names). Extend
BaseWebSocketHandler to override only what you need; all callbacks may be
async.
| Callback | When |
|---|---|
onOpen(session) |
Connection established. |
onMessage(session, message) |
A frame arrived — TextMessage(text) or BinaryMessage(data) (pattern-match the sealed type). |
onError(session, error, stackTrace) |
A transport error or an exception escaped a callback; the connection is closed with code 1011 afterwards. |
onClose(session, code, reason) |
Connection closed by either side; always called last. |
Messages within one connection are dispatched sequentially — awaiting
in onMessage back-pressures that connection's frames.
The session¶
WebSocketSession is your channel to one connected peer:
| Member | Description |
|---|---|
id |
Unique per connection (ws-1, ws-2, …). |
uri |
The URI the client connected to. |
pathParameters |
Route parameters — @WebSocketController('/ws/chat/:room') ⇒ session.pathParameters['room']. |
queryParameters |
Handshake query string. |
attributes |
Per-connection Map<String, Object?> bag, alive for the whole session. |
isOpen |
Whether the connection is (believed) open. |
sendText(text) / sendBytes(data) |
Send a frame; return false instead of throwing when already closed. |
close([code, reason]) |
Close the connection (code: 1000 or 3000–4999). |
Keep a Set<WebSocketSession> in your handler to broadcast — see the
chat example.
Handler instances and scope¶
@WebSocketController(path, [scope]) — the class joins the DI graph
(constructor injection, @Value, everything from the
DI guide applies):
Scope.singleton(default): one shared handler for all connections — Spring's default. Keep per-connection state insession.attributes, and connection registries as handler fields.Scope.transient: a fresh handler per connection — instance fields become per-connection state, no bookkeeping needed.Scope.requestdoesn't apply to WebSockets and is a build error.
@WebSocketController('/ws/game', Scope.transient)
class GameSession extends BaseWebSocketHandler {
GameSession(this.engine); // injected per connection
final GameEngine engine;
int score = 0; // this connection's state
}
Handshake interceptors¶
@UseMiddleware on a @WebSocketController runs the listed
middleware on the handshake request:
@WebSocketController('/ws/chat/:room')
@UseMiddleware([ApiKeyMiddleware])
class ChatHandler extends BaseWebSocketHandler {
@override
void onOpen(WebSocketSession session) {
final user = session.attributes['user']; // set by the middleware
...
}
}
- A middleware that short-circuits (e.g. returns a 401) blocks the upgrade with that response.
request.attributesset during the handshake are copied intosession.attributes.- Exceptions in handshake middleware go through the normal exception
pipeline (advice,
HttpExceptionmapping). - Global middleware do not apply to handshakes — interceptors are
explicit opt-in (mirroring Spring's
HandshakeInterceptorregistration).
What does not apply¶
- After the upgrade, middleware and
@ControllerAdviceplay no role in socket traffic; errors reach the handler'sonError. - The validation machinery targets request bodies; decode
and validate WebSocket payloads yourself (e.g.
$MyDtoFromJson(jsonDecode(message.text))plus a generated$MyDtoValidator).
Build-time checks¶
- The annotated class must implement
WebSocketHandler(or extendBaseWebSocketHandler). - Endpoint paths must be unique across the application.
Scope.requestis rejected.
See build diagnostics.
What gets generated¶
One registration per endpoint in buildDianaServer():
server.registerWebSocket('/ws/chat/:room', () => ctx.chatHandler,
middleware: [ctx.apiKeyMiddleware]); // singleton
server.registerWebSocket('/ws/game', () => ctx.createGameSession()); // transient
Testing¶
Connect a real client from dart:io against an ephemeral port:
final server = await buildDianaServer(config: const DianaConfig.empty());
await server.serve(port: 0, quiet: true);
final ws = await WebSocket.connect(
'ws://127.0.0.1:${server.port}/ws/chat/dart');
ws.add('hello');
expect(await ws.first, 'joined dart');
The example's WebSocket suite shows multi-client broadcast assertions.