Shelf

repository·master·Indexed 21 days ago

https://github.com/dart-lang/shelf

A modular web server middleware framework for Dart, inspired by NodeJS's Connect and Ruby's Rack. Shelf provides a lightweight, composable model for building web servers using Handlers, Middleware, and Pipelines. The ecosystem includes specialized packages such as shelf_router for request routing, shelf_static for serving local files, shelf_proxy for forwarding requests, and shelf_web_socket for WebSocket connections.

Tokens
12.1K
Snippets
36
Records
52
Agent score
75%

What's inside shelf

  1. What is Shelf?

    master

    Shelf is a web server middleware model for Dart that focuses on composition and reuse. It simplifies web server development by mapping server logic into a single function: a handler takes a request as an argument and returns a response.

    Key features include:

    • A small set of simple, composable types.
    • Seamless mixing of synchronous and asynchronous processing.
    • Flexible response types, supporting both simple strings and byte streams.
  2. What is an Adapter and how to implement one

    master

    An Adapter is the bridge between an external source (like an HTTP server or a browser) and the Shelf Handler. It creates Request objects, passes them to a handler, and processes the resulting Response.

    Implementation Requirements

    Error Handling:

    • Must handle all errors from the handler (including null responses).
    • Should print errors to the console and return a 500 response.
    • The 500 response body must not include internal error details to prevent information leakage.
    • Must ensure asynchronous errors don't crash the application. Use catchTopLevelErrors to capture errors that would otherwise be top-leveled.

    Request Requirements:

    • Do not pass url or handlerPath to Request; only pass requestedUri.
    • If using context, all keys must start with `<package_name>.
    • Collapse multiple headers with the same name into a single comma-separated header (per RFC 2616).
    • Decode chunked transfer-encoded bodies before passing them to Request and remove the Transfer-Encoding header.

    Response Requirements:

    • Do not add or modify entity headers.
    • Apply chunked transfer coding (and set Transfer-Encoding: chunked) unless:
      • Status is < 200, 204, or 304.
      • Content-Length is provided.
      • Content-Type is multipart/byteranges.
      • Transfer-Encoding is already set to something other than identity.
    • For HEAD requests, do not emit an entity body.
    • Include the Server and Date headers, allowing the handler's own headers to take precedence.
    /// Run [callback] and capture any errors that would otherwise be top-leveled.
    ///
    /// If `this` is called in a non-root error zone, it will just run `callback`
    /// and return the result. Otherwise, it will capture any errors using
    /// `runZoned` and pass them to `onError`.
    void catchTopLevelErrors(
      void Function() callback,
      void Function(Object error, StackTrace stackTrace) onError,
    ) {
      if (Zone.current.inSameErrorZone(Zone.root)) {
        return runZonedGuarded(callback, onError);
      } else {
        return callback();
      }
    }
  3. Automate router generation with shelf_router_generator

    master
    If you prefer to use annotations to define your routes, you can use the shelf_router_generator package. This allows you to use the @Route annotation (provided by shelf_router) to automatically generate a Router instance via code generation.
  4. How Handlers and Middleware work together

    master

    Shelf applications are built using a layered model:

    1. Handler: A function that handles a Request and returns a Response. It is the core logic of your application (e.g., serving a file or returning JSON).
    2. Middleware: A function that takes a Handler and wraps it in another Handler. This allows you to add functionality like logging, authentication, or compression to the request/response lifecycle.
    3. Pipeline: A class used to compose multiple layers of middleware and a final handler into a single Handler.

    Some middleware (like routers) may call multiple handlers. When routing, middleware should update the request's handlerPath and url using Request.change() so that inner handlers can correctly identify their position in the application.

    // In an imaginary routing middleware...
    var component = request.url.pathSegments.first;
    var handler = _handlers[component];
    if (handler == null) return Response.notFound(null);
    
    // Create a new request just like this one but with whatever URL comes after
    // [component] instead.
    return handler(request.change(path: component));
  5. Understand the compliance test architecture

    master

    The compliance testing process follows a specific lifecycle for each test category (such as Compliance, Smuggling, or MalformedInput):

    1. Server Startup: An echo server (located in bin/) is started on a dynamic port.
    2. Probing: The .NET Http11Probe CLI tool is executed against the running server.
    3. Reporting: Raw JSON results are saved to reports/[name]/[category].json.
    4. Validation: The results are compared against existing "golden" files.
    5. Summarization: A combined summary (shelf_summary.md) is generated after all categories complete.

    Project Layout

    • bin/: Contains the echo server implementations (the Device Under Test).
    • reports/: Stores the golden JSON reports for each category.
    • test/: Contains the test runner harness and the main compliance_test.dart file.
    • tool/: Contains scripts used to convert JSON reports into human-readable Markdown.
  6. Quickstart: Create a basic web server with Shelf

    master

    To create a web server, you define a Handler (a function that takes a Request and returns a Response), wrap it in a Pipeline with any desired Middleware (like logRequests()), and then use an adapter like shelf_io.serve to start the server.

    This example demonstrates a simple echo server that logs requests and serves a response containing the requested URL.

    import 'package:shelf/shelf.dart';
    import 'package:shelf/shelf_io.dart' as shelf_io;
    
    void main() async {
      var handler =
          const Pipeline().addMiddleware(logRequests()).addHandler(_echoRequest);
    
      var server = await shelf_io.serve(handler, 'localhost', 8080);
    
      // Enable content compression
      server.autoCompress = true;
    
      print('Serving at http://${server.address.host}:${server.port}');
    }
    
    Response _echoRequest(Request request) =>
        Response.ok('Request for "${request.url}"');
  7. Generate a Router using annotations

    master

    The shelf_router_generator package allows you to generate a shelf_router.Router automatically by annotating your service methods with @Route annotations.

    1. Annotate your handler methods with @Route.get, @Route.post, etc., specifying the path and any path parameters (e.g., <userId>).
    2. Include a part directive pointing to the generated file (e.g., part 'filename.g.dart';).
    3. Implement a getter that calls the generated constructor (e.g., _$ClassNameRouter(this)).
    4. Run the build command to generate the code.

    Build Command:

    pub run build_runner build
    import 'package:shelf/shelf.dart';
    import 'package:shelf_router/shelf_router.dart';
    
    part 'userservice.g.dart'; // generated with 'pub run build_runner build'
    
    class UserService {
      final DatabaseConnection connection;
      UserService(this.connection);
    
      @Route.get('/users/')
      Future<Response> listUsers(Request request) async {
        return Response.ok('["user1"]');
      }
    
      @Route.get('/users/<userId>')
      Future<Response> fetchUser(Request request, String userId) async {
        if (userId == 'user1') {
          return Response.ok('user1');
        }
        return Response.notFound('no such user');
      }
    
      // Create router using the generate function defined in 'userservice.g.dart'.
      Router get router => _$UserServiceRouter(this);
    }
    
    void main() async {
      var connection = await DatabaseConnection.connect('localhost:1234');
      var service = UserService(connection);
      var router = service.router;
      var server = await io.serve(router.handler, 'localhost', 8080);
    }
  8. Use shelf_proxy to proxy requests to an external server

    master

    The shelf_proxy package provides a Shelf handler that forwards incoming requests to an external server. You can use it in two ways:

    1. As a standalone proxy server: Serve the proxyHandler directly using shelf_io.serve.
    2. As a mounted handler: Mount the proxyHandler within a larger Shelf application to proxy only specific URL paths.

    To use it, import package:shelf_proxy/shelf_proxy.dart and call proxyHandler(url) with the target destination URL.

    import 'package:shelf/shelf_io.dart' as shelf_io;
    import 'package:shelf_proxy/shelf_proxy.dart';
    
    void main() async {
      var server = await shelf_io.serve(
        proxyHandler("https://dart.dev"),
        'localhost',
        8080,
      );
    
      print('Proxying at http://${server.address.host}:${server.port}');
    }
  9. Run HTTP/1.1 compliance and hardening tests

    master

    To validate package:shelf or a new implementation against HTTP/1.1 RFC requirements and edge cases, use the compliance test suite. This suite uses the Http11Probe tool to probe an echo server and compares results against established golden files.

    Prerequisites

    • .NET 10 SDK must be installed on your system.

    Execution Run the following command from the package root:

    dart test
  10. Use shelf_web_socket to handle WebSocket connections

    master

    The shelf_web_socket package provides a Shelf handler for establishing WebSocket connections. You can use the webSocketHandler function to create a Handler that triggers an onConnection callback whenever a new connection is established.

    The callback receives two arguments:

    1. A WebSocketChannel object representing the connection.
    2. An HttpRequest object representing the initial handshake request.

    You can interact with the connection using the stream property to listen for incoming messages and the sink property to send messages back to the client.

    import 'package:shelf/shelf_io.dart' as shelf_io;
    import 'package:shelf_web_socket/shelf_web_socket.dart';
    
    void main() {
      // Create a handler that echoes received messages
      var handler = webSocketHandler((webSocket, _) {
        webSocket.stream.listen((message) {
          webSocket.sink.add('echo $message');
        });
      });
    
      // Serve the handler using shelf_io
      shelf_io.serve(handler, 'localhost', 8080).then((server) {
        print('Serving at ws://${server.address.host}:${server.port}');
      });
    }
  11. Install shelf_router_generator

    master

    To use shelf_router_generator, you must add it as a development dependency alongside build_runner. You also need shelf and shelf_router as regular dependencies in your pubspec.yaml.

    dependencies:
      shelf: ^0.7.5
      shelf_router: ^0.7.0+1
    dev_dependencies:
      shelf_router_generator: ^0.7.0+1
      build_runner: ^1.3.1
  12. Use shelf_router to route web requests

    master

    The shelf_router package provides a Router class for shelf applications. It allows you to match incoming HTTP requests to specific handlers using route patterns, including support for path parameters.

    To use it:

    1. Instantiate a Router.
    2. Define routes using methods like .get(), .post(), etc., specifying the path pattern and a handler function.
    3. For paths with parameters (e.g., /user/<user>), the handler function must accept the parameter values as additional arguments after the Request object.
    4. Add the Router instance as a handler to a shelf Pipeline or use it directly as a Handler.
    import 'package:shelf_router/shelf_router.dart';
    import 'package:shelf/shelf.dart';
    import 'package:shelf/shelf_io.dart' as io;
    
    // 1. Instantiate a router
    var router = Router();
    
    // 2. Configure routes
    // Static path
    router.get('/hello', (Request request) {
      return Response.ok('hello-world');
    });
    
    // Path with a parameter <user>
    router.get('/user/<user>', (Request request, String user) {
      return Response.ok('hello $user');
    });
    
    // 3. Use the router in a Pipeline and serve
    final app = const Pipeline()
      .addMiddleware(logRequests())
      .addHandler(router);
    
    var server = await io.serve(app, 'localhost', 8080);