handleRequest method
Dispatches and processes an incoming BloomRequest through global middlewares and matching routes.
Matches routes based on specificity order. If a matching route is found, executes the
route's scoped middleware chain followed by its handler. Handles HEAD requests by executing
the matching GET handler and safely canceling any resulting body stream without sending body bytes.
Path parameters never overwrite the reserved auth_* namespace
(auth_user_id, auth_roles, …) populated by verified auth middleware:
global middlewares run before route matching, so a route like
/users/:auth_user_id must not clobber verified identity for
downstream params readers. Never name a route parameter auth_*;
prefer the verified req.authUserId/req.authRoles getters (Expando)
over raw params where available.
Returns a 404 Not Found BloomResponse if no registered route matches request.
Example
final req = BloomRequest(method: 'GET', uri: Uri.parse('http://localhost/api/health'));
final res = await router.handleRequest(req);
expect(res.statusCode, equals(200));
Implementation
Future<BloomResponse> handleRequest(BloomRequest request) async {
return _executePipeline(_globalMiddlewares, request, () async {
final method = request.method.toUpperCase();
final path = request.path;
// 1. Check for an exact matching route for method and path.
for (final route in _routes) {
final matchesMethod = route.method == '*' ||
route.method == method ||
(method == 'HEAD' && route.method == 'GET');
if (!matchesMethod) continue;
final match = route.regex.firstMatch(path);
if (match != null) {
for (var i = 0; i < route.paramNames.length; i++) {
final name = route.paramNames[i];
// Reserve the auth_* namespace for verified auth middleware.
// Global middlewares run before matching and store verified
// identity in params; an attacker-controlled path segment must
// never replace it for downstream params readers.
if (name.startsWith('auth_')) continue;
request.params[name] = Uri.decodeComponent(match.group(i + 1)!);
}
return _executePipeline(route.middlewares, request, () async {
final res = await route.handler(request);
if (method == 'HEAD') {
// A HEAD response carries no body. Cancel any stream the handler
// produced, or its subscription is never listened to and the
// producer is left running for the life of the process.
if (res.isStreaming) {
unawaited(res.takeBodyStream().listen(null).cancel());
}
return BloomResponse(
statusCode: res.statusCode,
headers: res.headers,
body: null,
);
}
return res;
});
}
}
// 2. No matching route for (method, path). Check if path matches any registered routes.
final matchingRoutes = <_RouteEntry>[];
for (final route in _routes) {
if (route.regex.firstMatch(path) != null) {
matchingRoutes.add(route);
}
}
// If no route matches the path at all, return 404 Not Found.
if (matchingRoutes.isEmpty) {
return BloomResponse.notFound(
'Cannot ${request.method} ${request.path}');
}
// 3. Path matches one or more routes, but not the requested HTTP method.
final allowedMethods = <String>{};
bool hasExplicitOptions = false;
for (final route in matchingRoutes) {
if (route.method == '*') {
allowedMethods.add(method);
} else if (route.method == 'GET') {
allowedMethods.add('GET');
allowedMethods.add('HEAD');
} else if (route.method == 'OPTIONS') {
hasExplicitOptions = true;
allowedMethods.add('OPTIONS');
} else {
allowedMethods.add(route.method);
}
}
if (!hasExplicitOptions) {
allowedMethods.add('OPTIONS');
}
final allowHeader = _buildAllowHeader(allowedMethods);
// If the request is OPTIONS with no explicit OPTIONS route, respond 204 with Allow header.
if (method == 'OPTIONS') {
return BloomResponse.noContent(headers: {
'allow': allowHeader,
});
}
// Otherwise respond 405 Method Not Allowed with Allow header.
return BloomResponse.methodNotAllowed(
'Method Not Allowed',
{
'allow': allowHeader,
},
);
});
}