handleRequest method

Future<BloomResponse> handleRequest(
  1. BloomRequest request
)

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.

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++) {
          request.params[route.paramNames[i]] =
              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,
      },
    );
  });
}