Middleware and Errors
Spry keeps request behavior in two explicit places:
- middleware for cross-cutting flow control
- error handlers for translating failures into responses
Global middleware
Files in top-level middleware/ are loaded in filename order.
// ignore_for_file: file_names
import 'package:spry/spry.dart';
Future<Response> middleware(Event event, Next next) async {
final startedAt = DateTime.now();
final response = await next();
final duration = DateTime.now().difference(startedAt).inMilliseconds;
print(
'${event.method} ${event.url.path} -> ${response.status} (${duration}ms)',
);
return response;
}If a middleware should apply only to one HTTP method, add the method suffix before .dart, for example:
middleware/02_auth.get.dartmiddleware/03_audit.post.dart
This is the right place for:
- request logging
- tracing
- auth shells
- response timing
For first-party middleware helpers such as requestId(...), see Middleware Overview.
Scoped middleware
Use _middleware.dart inside routes/ when behavior should apply only to that branch of the route tree.
import 'package:spry/spry.dart';
Future<Response> middleware(Event event, Next next) async {
event.locals.set(
#requestId,
DateTime.now().microsecondsSinceEpoch.toString(),
);
return next();
}Scoped middleware supports the same method suffix convention:
routes/admin/_middleware.get.dartroutes/admin/_middleware.delete.dartroutes/admin/_error.get.dartroutes/admin/_error.delete.dart
This is useful when a subset of routes needs shared locals, auth checks, or response wrapping.
Error handling
Use _error.dart to catch errors inside the current route scope and convert them into a stable response shape.
import 'package:spry/spry.dart';
Response onError(Object error, StackTrace stackTrace, Event event) {
if (error case NotFoundError()) {
return Response.json({
'error': 'not_found',
'path': event.url.path,
}, ResponseInit(status: 404));
}
if (error case HTTPError()) {
return error.toResponse();
}
return Response.json({
'error': 'internal_server_error',
'path': event.url.path,
}, ResponseInit(status: 500));
}Scoped error handlers support the same method suffix convention as scoped middleware, for example _error.get.dart and _error.delete.dart.
This is the clean path for:
- handling
NotFoundError - returning structured JSON errors
- avoiding repeated
try/catchblocks in handlers
One handler only
If middleware or error shaping belongs to one handler only, use defineHandler(...) instead of creating _middleware.dart or _error.dart files for that one-off case.
import 'package:spry/spry.dart';
final handler = defineHandler(
(event) async {
return Response.json({'ok': true});
},
middleware: [
(event, next) async {
if (event.headers.get('x-demo') == null) {
throw const HTTPError(400, body: 'missing x-demo');
}
return next();
},
],
onError: (error, stackTrace, event) {
if (error case HTTPError()) {
return error.toResponse();
}
rethrow;
},
);defineHandler(...) keeps the normal Spry ordering:
- global and scoped filesystem middleware still run outside the handler
- local middleware wraps that handler
- local
onErrorcatches that local chain - rethrowing still bubbles into scoped
_error.dart
Practical rule
- if it changes request flow across multiple routes, use middleware
- if it converts thrown errors into responses, use
_error.dart - if it belongs to one handler only, use
defineHandler(...)or keep it inline
For first-party middleware documentation and helper-specific behavior, use the dedicated Middleware section.
For websocket routes, middleware and _error.dart still apply during the handshake phase, but not after the upgrade is committed. Use that phase for auth, validation, and fallback decisions before calling event.ws.upgrade(...). See WebSockets.