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:

Minimal filter timing the subsequent request
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:

cms/site.yaml
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 as req.myFlag = true, is not an error, but the field is dropped, so nothing downstream sees it. To pass your own data along the pipeline, use XPXP8.1.08.1.0 setCustomLocalAttribute(): it stores the value in the current execution, where implementations, response processors and the code after next() in this very filter can all read it back.

  • A filter can re-route rendering. Assigning contentPath before next() 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:

A webapp handler wrapped in a filter
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.

Filter logging the request and the response
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;
}
Filter intercepting the request
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);
}
Filter changing request params
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);
}
Filter adding a response header
export function filter(req, next) {
    const response = next(req);
    response.headers = {...response.headers, 'X-Served-By': app.name};
    return response;
}

Contents

Contents