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, aMap, aresponse(...)). 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.
Write your code in handle. For example:
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:
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.
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.
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.
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:
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.statusis the status that was sent, also for an error answer.terminatecan beasync.- An error in
terminategoes toExceptionHandler.report. It never changes the answer. - The
handlepart is not needed. A class can implement onlyIDoxTerminableMiddleware.
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.
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 ofA-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.LogMiddlewareaddsrequest_idto each log line. PutRequestIdbefore it.- The default
ExceptionHandler.reportprints the id with the error. In your ownreport, readExceptionHandler.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.
- It works on
200answers with a body in memory: aString, aMap, aListor a model. Files and streams are not touched. - An answer that already has an
ETagheader orCache-Control: no-storeis 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
304saves 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:
- For every route, add
RequestTimeout(...)toglobalMiddleware. - 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, not504.504 Gateway Timeoutis what a proxy sends when the app is slow.503says 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:
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:
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
LogMiddlewarefromdox_corewrites one JSON log line per request. Headers are logged only withwithHeader: true. Body fields and headers whose name has a word likepassword,secret,token,authorization,cookieorapi_keyin it (upper or lower case) are logged as[REDACTED]. Change the list withredact:. The new project turns it off whenAPP_ENVisproduction.AuthMiddlewarefromdox_authchecks the JWT token.AuthMiddleware(optional: true)lets guests through too.GuestMiddlewarelets only guests through, for login pages. See Authentication.RateLimitfromdox_coreanswers429when a client sends too many requests. See Rate limiting.SecurityHeadersfromdox_coreadds safe headers likeX-Frame-Optionsto every answer. See Security headers.SessionMiddlewarefromdox_coregives each request a session. See Session.CsrfMiddlewarefromdox_corestops cross site request forgery. See CSRF protection.RequestId,ETagandRequestTimeoutfromdox_core. See above.ValidateSignaturefromdox_corechecks signed urls. See Signed URLs.