Skip to content

Middleware

Middleware is code that runs before the controller, and if you want, after it too.

Use it for checks like login, logging or rate limits, or to change the response.

How it works

A middleware gets the request. It then does one of these:

  • Return the request. Dox goes on to the next middleware, then to the controller.
  • Return something else (a String, a Map, a response(...)). Dox sends it as the response. The controller does not run.
  • Throw an exception, like NotFoundHttpException(). Dox sends the error response. See Response: errors.

A middleware can be async. Dox waits for it.

Class based middleware

Create one with the CLI. It writes lib/app/http/middleware/logger.middleware.dart.

dox make:middleware Logger

Write your code in handle. For example:

lib/app/http/middleware/logger.middleware.dart
import 'package:dox_core/dox_core.dart';

class LoggerMiddleware implements IDoxMiddleware {
  @override
  dynamic handle(IDoxRequest req) {
    print('${req.method} ${req.uri.path}');
    return req; // go on to the next middleware
  }
}

Then put it on a route:

lib/routes/api.dart
Route.get('/ping', <dynamic>[LoggerMiddleware(), (WebController c) => c.pong]);

Note

handle takes an IDoxRequest, not a DoxRequest.

Warning

Do not forget return req. A middleware that returns null ends the request with an empty 200, and the controller does not run. When APP_ENV is development or local, Dox prints a warning to stderr the first time a middleware does this.

Function based middleware

A function that takes a DoxRequest also works as middleware.

lib/app/http/middleware/api_key.middleware.dart
import 'package:dox_core/dox_core.dart';

dynamic apiKeyMiddleware(DoxRequest req) {
  String apiKey = Env.get<String>('API_KEY');
  String given = req.header('X-Api-Key') ?? '';
  if (apiKey.isEmpty || !Hash.equals(given, apiKey)) {
    return response(<String, String>{'message': 'Unauthorized'}).statusCode(401);
  }
  return req;
}

The key comes from the API_KEY env variable, not from the code. Hash.equals takes the same time for every wrong key, so the time does not tell how much of the key was right. A normal == stops at the first wrong character.

lib/routes/api.dart
Route.get('/ping', <dynamic>[apiKeyMiddleware, (WebController c) => c.pong]);

Run code after the controller

A middleware can also get a next function. await next() runs the rest of the middleware and the controller, and gives the response. Code after that line runs after the controller. Change the response, then return it.

lib/app/http/middleware/see_other.middleware.dart
import 'package:dox_core/dox_core.dart';

/// a redirect after a form post becomes 303, so the browser does a GET
class SeeOtherMiddleware implements IDoxNextMiddleware {
  @override
  Future<DoxResponse> handle(DoxRequest req, Next next) async {
    // before the controller
    DoxResponse res = await next();
    // after the controller
    if (res.status == 302 && req.httpRequest.method != 'GET') {
      res.statusCode(303);
    }
    return res;
  }
}

The response from next() is always a DoxResponse, so you can use all its methods: statusCode(...), header(...), cookie(...), content(...). Read the status with res.status (null means 200) and the headers with res.headers.

When the controller (or a later middleware) throws an error, next() does not throw. It gives the error response from the ExceptionHandler, like a 404 or a 422. So the code after next() always runs.

A function works too. It takes the request and next:

Future<DoxResponse> timerMiddleware(DoxRequest req, Next next) async {
  Stopwatch watch = Stopwatch()..start();
  DoxResponse res = await next();
  return res.header('X-Time', '${watch.elapsedMilliseconds}ms');
}

To stop early, return a response and do not call next().

Speed

A route with no next middleware runs exactly as before. Only routes that have one pay for the extra step.

Run code after the answer is sent

A middleware can run code after the answer has gone to the client, like terminate in Laravel. Use it for work the client should not wait for: logs, metrics, cleanup. Implement IDoxTerminableMiddleware:

lib/app/http/middleware/metrics.middleware.dart
import 'package:dox_core/dox_core.dart';

class MetricsMiddleware implements IDoxMiddleware, IDoxTerminableMiddleware {
  @override
  dynamic handle(IDoxRequest req) => req; // before the controller

  @override
  void terminate(DoxRequest req, DoxResponse res) {
    // the answer is already sent
    print('${req.method} ${req.uri.path} ${res.status}');
  }
}
  • res.status is the status that was sent, also for an error answer.
  • terminate can be async.
  • An error in terminate goes to ExceptionHandler.report. It never changes the answer.
  • The handle part is not needed. A class can implement only IDoxTerminableMiddleware.

Speed

Dox checks each route once for such a middleware. Other routes do not pay for it.

Request id

RequestId gives each request an id. It is sent back in the X-Request-Id header, and you can read it as req.requestId.

lib/config/app.dart
AppConfig appConfig = AppConfig(
  globalMiddleware: <dynamic>[
    const RequestId(),
    LogMiddleware(enabled: true),
  ],
  // ...
);
  • When the client (or a proxy) sends a safe X-Request-Id (1 to 128 characters of A-Z a-z 0-9 . _ -), Dox keeps it. Else it makes a new one of 32 hex characters.
  • RequestId(trustClient: false) always makes a new one.
  • LogMiddleware adds request_id to each log line. Put RequestId before it.
  • The default ExceptionHandler.report prints the id with the error. In your own report, read ExceptionHandler.requestId.

ETag and 304 Not Modified

ETag adds an ETag header to GET and HEAD answers. When the client sends the same ETag back in If-None-Match, Dox answers 304 Not Modified with no body.

Route.get('/posts', <dynamic>[const ETag(), (PostController c) => c.index]);
  • It works on 200 answers with a body in memory: a String, a Map, a List or a model. Files and streams are not touched.
  • An answer that already has an ETag header or Cache-Control: no-store is not touched.
  • The ETag is weak (W/"..."), made from a fast 64-bit hash of the body. It is not a secure hash.
  • The controller still runs. A 304 saves bandwidth, not server time.

Request timeout

RequestTimeout answers 503 Service Unavailable when the middleware after it and the controller take too long.

Route.get('/report', (ReportController c) => c.show)
    .timeout(Duration(seconds: 30));

// the same as
Route.get('/report', <dynamic>[
  RequestTimeout(Duration(seconds: 30)),
  (ReportController c) => c.show,
]);

The answer is the normal error JSON:

{"code": "request_timeout", "message": "The request took too long"}
  • For every route, add RequestTimeout(...) to globalMiddleware.
  • Dart can not stop a running Future. The controller keeps running in the background. Its late result is dropped, so nothing is sent twice.
  • It is 503, not 504. 504 Gateway Timeout is what a proxy sends when the app is slow. 503 says the app itself gave up.

Many middleware on one route

Put them in the list before the controller. They run from left to right. You can mix class and function middleware, with or without next.

Route.get('/ping', <dynamic>[
  LoggerMiddleware(),
  apiKeyMiddleware,
  (WebController c) => c.pong, // the controller is always last
]);

Middleware for a group of routes

Route.middleware(<dynamic>[LoggerMiddleware()], () {
  Route.get('/me', (AuthController c) => c.me);
  Route.post('/logout', (AuthController c) => c.logout);
});

Middleware for a router or for all routes

For one router, set its middleware list:

lib/routes/api.dart
class ApiRouter extends Router {
  @override
  String get prefix => 'api';

  @override
  List<dynamic> get middleware => <dynamic>[LoggerMiddleware()];

  @override
  void register() {
    // every route here runs LoggerMiddleware
  }
}

For all routes, set globalMiddleware in the app config:

lib/config/app.dart
AppConfig appConfig = AppConfig(
  globalMiddleware: <dynamic>[
    LogMiddleware(enabled: env.appEnv != 'production'),
  ],
  // ...
);

The order is: global middleware, then router middleware, then group middleware, then route middleware.

flowchart LR
    Q["Request"] --> G["Global<br>globalMiddleware"]
    G --> R["Router<br>middleware getter"]
    R --> P["Group<br>Route.middleware"]
    P --> T["Route<br>list before the controller"]
    T --> C["Controller"]
    C --> S["Response"]
    G & R & P & T -.->|"returns a value or throws"| S

Built-in middleware

  • LogMiddleware from dox_core writes one JSON log line per request. Headers are logged only with withHeader: true. Body fields and headers whose name has a word like password, secret, token, authorization, cookie or api_key in it (upper or lower case) are logged as [REDACTED]. Change the list with redact:. The new project turns it off when APP_ENV is production.
  • AuthMiddleware from dox_auth checks the JWT token. AuthMiddleware(optional: true) lets guests through too. GuestMiddleware lets only guests through, for login pages. See Authentication.
  • RateLimit from dox_core answers 429 when a client sends too many requests. See Rate limiting.
  • SecurityHeaders from dox_core adds safe headers like X-Frame-Options to every answer. See Security headers.
  • SessionMiddleware from dox_core gives each request a session. See Session.
  • CsrfMiddleware from dox_core stops cross site request forgery. See CSRF protection.
  • RequestId, ETag and RequestTimeout from dox_core. See above.
  • ValidateSignature from dox_core checks signed urls. See Signed URLs.