HTTP filters
Contents
A filter is a function that runs around a request handler. It receives the request and a next function, may inspect or change the request before calling next(req), and may inspect or change the response next() returns before handing it back. A filter that does not call next() answers the request itself.
The filter contract
A filter module exports a single function named filter:
export function filter(req, next) {
const before = Date.now();
const response = next(req); // next(req) hands over the request to the pipeline and returns the response
log.info(`${Date.now() - before}ms`);
return response;
}
req is the request object, and next() returns a response object. The same module works wherever a filter is wired, and it can be wired in two ways.
In the site service
The site service invokes filters for you. A site mapping with a filter: property names the module, and a pattern: or match: decides which requests it applies to:
kind: "Site"
mappings:
- filter: "/cms/filters/timing.ts"
pattern: '/.*'
order: 10
For a request, the site service collects the matching filter mappings from every app on the site, sorts them by order, and calls the first one. Its next() runs the second, and so on down to the page or controller mapping that produces the response. The response then passes back up through the same filters in reverse: code after next() in the first filter runs last. A response with applyFilters: false stops this: no further filters or response processors run on it.
Two things follow from the site service being in charge of next():
-
Only the documented request fields cross
next(). The site service rebuilds the request from the object you pass, reading the documented fields and nothing else. Assigning a property of your own, such asreq.myFlag = true, is not an error, but the field is dropped, so nothing downstream sees it. To pass your own data along the pipeline, usesetCustomLocalAttribute(): it stores the value in the current execution, where implementations, response processors and the code afternext()in this very filter can all read it back. -
A filter can re-route rendering. Assigning
contentPathbeforenext()makes the site service render another content at the same address. See Re-routing to another content.
In webapps, APIs and admin tools
The other services have no filter mappings. Nothing invokes a filter for you there, and the same module is wired by calling it from the handler:
import {filter as requireToken} from '/filters/require-token';
function handle(req) {
return {body: 'hello'};
}
export function GET(req) {
return requireToken(req, handle);
}
To run several filters, or to combine them with routing, use lib-router: router.filter(fn) adds a filter to the chain in front of every route, with the same (req, next) signature.
Because next() is now a plain function, the request is the same object all the way through. Properties you assign survive into the handler, and contentPath has no effect: re-routing is a site service feature.
Examples
The examples below work in both modes.
export function filter(req, next) {
log.info('Request:' + JSON.stringify(req, null, 2));
const response = next(req); // Continue request pipeline
log.info('Response:' + JSON.stringify(response, null, 2));
return response;
}
export function filter(req, next) {
if (req.getHeader('X-Auth-Token') !== 'letMeIn') {
// intercept request pipeline
return {
status: 403
};
}
req.headers['Authenticated'] = true;
return next(req);
}
export function filter(req, next) {
req.params = {
param1: 'val', // if param1 was not in the original request it will be added, otherwise the original value will be replaced
param2: null, // remove param2 from the original request
param3: [] // another way to remove a parameter
};
return next(req);
}
export function filter(req, next) {
const response = next(req);
response.headers = {...response.headers, 'X-Served-By': app.name};
return response;
}