Middlewares
Middlewares are simple functions that run before/after route handlers. They can be used to restrict access to certain set of routes or act based on parsed route params.
There are two types of middleware global and local. As the names suggest the first one is defined globally on the router instance using use()
method and the second type is bound to specific route and is defined in the route options.middlewares
property.
// The imports are stripped for compactness.
export let init = (ns, oc, config) => {
const router = oc.get('$Router');
router
.use(async (params, locals) => {
console.log('Global middleware', params, locals, locals.route, locals.action);
locals.counter = 0;
});
.add('home', '/', HomeController, HomeView, {
middlewares: [
async (params, locals, next) => {
next({ counter: counter++ });
}
]
})
.add(RouteNames.ERROR, '/error', ErrorController, ErrorView)
.add(RouteNames.NOT_FOUND, '/not-found', NotFoundController, NotFoundView);
}
Since you have access to the object container (oc
), you can basically do anything you want in the middlewares.
You can easily define authentication middlewares or access-restricting middlewares since throwing an error from the middleware is handled the same way as any other error in the application.
Function arguments
Each middleware can be async and the functions can use up to three arguments: params
, locals
and next
. params
specifically allows you to modify route params, locals
is used to pass data between middlewares and next
callback provides additional
params
object = {}
Contains route params extracted by the currently matched route handler. Can be empty if there was no route match before execution of concrete middleware.
locals
RouteLocals = {}
Mutable object you can use to pass data between middlewares. It is passed across all middlewares, so anything you define here, is available in following middleware functions.
In addition to mutating the original object, you can also return object values from middlewares or pass them as an argument in the next()
function. These are then merged into the locals
upon it's execution.
async (params, locals) => {
locals.counter++;
}
// or
async (params, locals) => {
return { counter: counter++ };
}
// or
async (params, locals, next) => {
next({ counter: counter++ });
}
Additionally it always contains following keys:
route
AbstractRoute
Instance of currently matched route.
action
RouteAction = {}
An action object describing what triggered this routing (can be empty).
next
(result?: object) => void
When called, this function (as the name suggest) allows you to continue with execution of other route handlers. Apart from other frameworks that use similar feature, when you define next
argument in your middleware, you have to execute it in order to continue. Otherwise the router will not proceed any further even if the middleware function content finished it's execution.
This is intentional as it allows you to have more control over the middleware execution and gives you ability to stop the routing process completely.
This is can be usefull in situations when for example you want to do a redirect, which is synchronous but takes a while until the window is reloaded. Without stopping the middleware execution (by defining the next
callback and not calling it), you could get a glimpse of Error Page that is rendered before the redirect takes places, because the router continued it's processing.
async (params, locals, next) => {
if (await oc.get('User').isLoggedIn()) {
// Continue normally
return next();
}
// Stop execution by not calling `next()` and do a redirect
oc.get('$Router').redirect('/');
}
Execution order
Middleware functions are resolved from top to bottom sequentially. In case of the code above, when routing to home
route, following things would have happened:
- Global middlewares defined above currently matched route are executed (in this case we have only one global middleware, defined above all routes).
- Params extraction from currently matched route handler (home) is executed.
- Local route middlewares are executed (with newly extracted route params).
In case of an error or not found page, the execution order is still the same, meaning the global and route middlewares are executed as with any other route.
There's only one exception, since the locals
object is cleared to an empty object before route handling, if an error occurs during route handling and execution is internally passed to error handling (displaying error page), the locals object may retain values that were there for the previous route matching. However the locals.route
object will still be up to date and equal to currently routed route (error in this case).
Execution timeout
To prevent middlewares from freezing the application, for example when the middlewares takes too long to execute, we've implemented execution timeout, which prevents them from running indefinitely.
You can customize the timeout value in app settings:
export default (ns, oc, config) => {
return {
prod: {
$Router: {
middlewareTimeout: 30000, // ms
},
},
};
};