Spry v8.1.0 Route-Level WebSocket Support: Building Real-Time Applications
Spry v8.1.0 Route-Level WebSocket Support: Building Real-Time Applications
With the release of Spry v8.1.0, the framework now includes first-class route-level WebSocket support, making it easier than ever to build real-time applications without leaving the familiar filesystem routing model. In this tutorial, we'll explore the new event.ws API, compare it with previous approaches, and walk through practical examples you can use in your own projects.
What's New: Route-Level WebSocket Handlers
Before Spry v8.1.0, adding WebSocket support required working directly with the underlying runtime's WebSocket APIs. While this was possible through event.context.webSocket, it lacked the convenience and safety of a framework-integrated solution. Developers had to manually check runtime capabilities, validate upgrade requests, and handle errors—all while maintaining consistency with the rest of their route handlers.
The new event.ws API changes this by providing a unified interface that:
- Checks runtime support automatically via
event.ws.isSupported - Detects upgrade requests with
event.ws.isUpgradeRequest - Accesses requested protocols through
event.ws.requestedProtocols - Performs safe upgrades with
event.ws.upgrade(handler, {protocol})
Most importantly, this API integrates seamlessly with Spry's existing request lifecycle. WebSocket upgrades are still initiated from normal route files, meaning you don't need to learn a separate routing system or file naming convention.
Defining Your First WebSocket Route
Let's start with a basic example. Create a new route file routes/chat.get.dart:
import 'package:spry/spry.dart';
import 'package:spry/websocket.dart';
Response handler(Event event) {
// Check if WebSocket is supported and this is an upgrade request
if (!event.ws.isSupported || !event.ws.isUpgradeRequest) {
return Response.json({
'message': 'This endpoint supports WebSocket connections',
'hint': 'Use a WebSocket client to connect to ws://localhost:4000/chat'
});
}
// Upgrade to WebSocket connection
return event.ws.upgrade((ws) async {
// Send welcome message
ws.sendText('Connected to Spry WebSocket server!');
// Listen for incoming messages
await for (final message in ws.events) {
switch (message) {
case TextDataReceived(text: final text):
// Echo the text back
ws.sendText('Echo: $text');
case BinaryDataReceived():
ws.sendText('[Binary data received]');
case CloseReceived():
// Connection closed
break;
}
}
});
}
This simple echo server demonstrates the core pattern:
- Check support and upgrade request – Provide a helpful HTTP fallback for non-WebSocket clients
- Call
event.ws.upgrade– Pass a handler that will manage the WebSocket session - Handle WebSocket events – Use Dart's pattern matching to respond to different message types
Practical Use Cases
Real-Time Chat Application
Let's build a more practical example: a simple chat room that broadcasts messages to all connected clients. We'll need to track connected WebSockets and manage message distribution.
import 'package:spry/spry.dart';
import 'package:spry/websocket.dart';
// In-memory store for connected clients
final _connections = <WebSocket>[];
Response handler(Event event) {
if (!event.ws.isSupported || !event.ws.isUpgradeRequest) {
return Response.json({
'name': 'Spry Chat Room',
'connections': _connections.length,
'instructions': 'Connect with a WebSocket client to join the chat'
});
}
return event.ws.upgrade((ws) async {
// Add to connections list
_connections.add(ws);
print('New connection (total: ${_connections.length})');
try {
// Notify all clients about new connection
_broadcast('User joined (${_connections.length} online)');
// Listen for messages
await for (final message in ws.events) {
switch (message) {
case TextDataReceived(text: final text):
// Broadcast to all connected clients
_broadcast('Message: $text');
case CloseReceived():
// Remove from connections list
_connections.remove(ws);
_broadcast('User left (${_connections.length} online)');
break;
case BinaryDataReceived():
// Ignore binary data for this example
break;
}
}
} finally {
// Ensure cleanup on unexpected disconnects
_connections.remove(ws);
}
});
}
void _broadcast(String message) {
for (final connection in _connections) {
try {
connection.sendText(message);
} catch (_) {
// Ignore failed sends (disconnected clients)
}
}
}
Real-Time Notifications
WebSockets are perfect for sending server-initiated notifications to clients. Here's an example that simulates a notification feed:
import 'package:spry/spry.dart';
import 'package:spry/websocket.dart';
import 'dart:async';
Response handler(Event event) {
if (!event.ws.isSupported || !event.ws.isUpgradeRequest) {
return Response.json({
'service': 'Notification Feed',
'mode': 'HTTP',
'connect': 'Use WebSocket for real-time notifications'
});
}
return event.ws.upgrade((ws) async {
// Send a welcome notification
ws.sendText('Connected to notification service');
// Simulate periodic notifications
final timer = Timer.periodic(const Duration(seconds: 10), (_) {
final timestamp = DateTime.now().toIso8601String();
ws.sendText('Server update at $timestamp');
});
try {
// Keep connection alive until client disconnects
await ws.events.drain<void>();
} finally {
timer.cancel();
}
});
}
Integrating with Existing HTTP Routes
One of Spry's strengths is that WebSocket routes coexist with regular HTTP routes. The same routes/ directory can contain both types, and you can even combine them in a single handler.
Hybrid Endpoint Example
import 'package:spry/spry.dart';
import 'package:spry/websocket.dart';
Response handler(Event event) {
// Handle WebSocket upgrades
if (event.ws.isSupported && event.ws.isUpgradeRequest) {
return event.ws.upgrade((ws) async {
ws.sendText('WebSocket connection established');
await ws.events.drain<void>();
});
}
// Handle regular HTTP GET requests
if (event.method == 'GET') {
return Response.html('''
<!DOCTYPE html>
<html>
<head>
<title>Hybrid Endpoint</title>
<script>
// Simple WebSocket client for demonstration
const ws = new WebSocket('ws://' + window.location.host + '/hybrid');
ws.onmessage = (event) => console.log('Received:', event.data);
ws.onopen = () => ws.send('Hello from browser');
</script>
</head>
<body>
<h1>Hybrid HTTP/WebSocket Endpoint</h1>
<p>This page automatically connects via WebSocket. Check the console.</p>
</body>
</html>
''');
}
// Handle other HTTP methods
return Response.json({'error': 'Method not allowed'},
ResponseInit(status: 405));
}
Middleware Compatibility
WebSocket upgrades still pass through middleware, making it easy to add authentication, logging, or other cross-cutting concerns:
// routes/_middleware.dart
import 'package:spry/spry.dart';
Future<Response> handler(Event event, Next next) async {
// Log all requests (including WebSocket upgrade attempts)
print('${DateTime.now()} ${event.method} ${event.request.uri}');
// Check authentication for specific routes
if (event.request.uri.path.startsWith('/admin')) {
final auth = event.request.headers.get('authorization');
if (auth != 'Bearer secret-token') {
return Response.json({'error': 'Unauthorized'},
ResponseInit(status: 401));
}
}
return next();
}
Testing and Debugging Tips
Testing WebSocket Routes
Spry's WebSocket support is designed to be testable. Here's an example using the test package:
import 'package:spry/spry.dart';
import 'package:spry/websocket.dart';
import 'package:test/test.dart';
import 'package:osrv/websocket.dart' as osrv;
void main() {
test('WebSocket upgrade works', () async {
// Create a mock WebSocket request context
final context = RequestContext(
request: Request(Uri.parse('http://localhost/chat')),
webSocket: osrv.WebSocketRequest(
isUpgradeRequest: true,
requestedProtocols: [],
accept: (handler, {protocol}) => Response.empty(),
),
);
final event = Event(context);
// Verify upgrade detection
expect(event.ws.isUpgradeRequest, isTrue);
expect(event.ws.isSupported, isTrue);
});
}
Debugging Common Issues
"426 Upgrade Required" – This error occurs when
event.ws.upgrade()is called on a non-WebSocket request. Always checkevent.ws.isUpgradeRequestfirst."501 Not Implemented" – The runtime doesn't support WebSockets. Check
event.ws.isSupportedand verify your deployment target.Middleware not firing for WebSocket messages – Remember that middleware only runs during the handshake phase. Once upgraded, WebSocket messages bypass middleware.
Memory leaks with connection tracking – Always ensure connections are removed from tracking lists in
finallyblocks to handle unexpected disconnects.
Using Browser Developer Tools
Modern browsers provide excellent WebSocket inspection tools:
- Chrome/Edge: Open DevTools → Network → WS tab to see frames
- Firefox: Network panel → WS filter to inspect messages
- curl: Test upgrades with
curl -i -H 'Connection: Upgrade' -H 'Upgrade: websocket' -H 'Sec-WebSocket-Version: 13' http://localhost:4000/chat
Conclusion
Spry's new route-level WebSocket support brings real-time capabilities to the framework without compromising its elegant filesystem routing model. By integrating WebSockets into the normal request lifecycle, Spry ensures that developers can apply existing knowledge and patterns to build real-time applications.
The event.ws API provides a safe, consistent interface that works across all supported runtimes, while maintaining the flexibility to handle both HTTP and WebSocket traffic from the same endpoints.
To learn more:
Ready to build something real-time? Update to Spry v8.1.0 and start using event.ws in your routes today!