Skip to content

Latest commit

 

History

History
422 lines (382 loc) · 19.5 KB

Middleware.md

File metadata and controls

422 lines (382 loc) · 19.5 KB
title layout category
Defining and Working With Middleware
default
Kettle

Defining and Working With Middleware

Working with middleware

The most crucial structuring device in the expressjs (or wider pillarjs) community is known as middleware. In its most basic form, a piece of middleware is simply a function with the following signature:

middleware(req, res, next)

The elements req and res have been described in the section on request components. The element next is a callback provided by the framework to be invoked when the middleware has completed its task. This could be seen as a form of continuation passing style with 0 arguments – although only in terms of control flow since in general middleware has its effect as a result of side-effects on the request and response. In express, middleware are typically accumulated in arrays or groups of arrays by directives such as app.use. If a piece of middleware completes without error, it will invoke the next callback with no argument, which will signal that control should pass to the next middleware in the current sequence, or back to the framework if the sequence is at an end. Providing an argument to the callback next is intended to signal an error and the framework will then abort the middleware chain and propagate the argument, conventionally named err, to an error handler. This creates an analogy with executing promise sequences which we will return to when we construct middleware components.

In Kettle, middleware can be scheduled more flexibly than by simply being accumulated in arrays – the priority of a piece of middleware can be freely adjusted by assigning it a Priority as seen in many places in the Infusion framework, and so integrators can easily arrange for middleware to be inserted in arbitrary positions in already existing applications.

Middleware is accumulated at two levels in a Kettle application – firstly, overall middleware is accumulated at the top level of a kettle.server in an option named rootMiddleware. This is analogous to express app-level middleware registered with app.use. Secondly, individual request middleware can be attached to an individual kettle.request in its options at requestMiddleware. This is analogous to express router-level middleware. The structure of these two options areas is the same, which we name middlewareSequence. When the request begins to be handled, the framework will execute the following in sequence:

  • The root middleware attached to the kettle.server
  • The request middleware attached to the resolved kettle.request component
  • The actual request handler designated by the request component's invoker handleRequest

If any of the middleware in this sequence signals an error, the entire sequence will be aborted and an error returned to the client.

Structure of entries in a middlewareSequence

A middlewareSequence is a free hash of keys, considered as namespaces for the purpose of resolving Priorities onto records of type middlewareEntry:

{
    <middlewareKey> : <middlewareEntry>,
    <middlewareKey> : <middlewareEntry>,
...
}
Members of a middlewareEntry entry within the middlewareSequence block of a component (rootMiddleware for kettle.server or requestMiddleware for kettle.request)
Member Type Description
middleware String (IoC Reference) An IoC reference to the middleware component which should be inserted into the handler sequence. Often this will be qualified by the context {middlewareHolder} e.g. {middlewareHolder}.session – to reference the core middleware collection attached to the kettle.server but middleware could be resolved from anywhere visible in the component tree. This should be a reference to a component descended from the grade kettle.middleware
priority (optional) String (Priority) An encoding of a priority relative to some other piece of middleware within the same group will typically be before:middlewareKey or after:middlewareKey for the middlewareKey of some other entry in the group

Defining and registering middleware components

A piece of Kettle middleware is derived from grade kettle.middleware. This is a very simple grade which defines a single invoker named handle which accepts one argument, a kettle.request, and returns a promise representing the completion of the middleware. Conveniently a fluid.promise implementation is available in the framework, but you can return any variety of thenable that you please. Here is a skeleton, manually implemented middleware component:

fluid.defaults("examples.customMiddleware", {
    gradeNames: "kettle.middleware",
    invokers: {
        handle: "examples.customMiddleware.handle"
    }
});

examples.customMiddleware.handle = function (request) {
    var togo = fluid.promise();
    if (request.req.params.id === 42) {
        togo.resolve();
    } else {
        togo.reject({
            isError: true,
            statusCode: 401,
            message: "Only the id 42 is authorised"
        });
    }
    return togo;
};

The framework makes it very easy to adapt any standard express middleware into a middleware component by means of the adaptor grade kettle.plainMiddleware. This accepts any standard express middleware as the option named middleware and from it fabricates a handle method with the semantic we just saw earlier. Any options that the middleware accepts can be forwarded to it from the component's options. Here is an example from the framework's own json middleware grade:

kettle.npm.bodyParser = require("body-parser");

fluid.defaults("kettle.middleware.json", {
    gradeNames: ["kettle.plainMiddleware"],
    middlewareOptions: {}, // see https://github.com/expressjs/body-parser#bodyparserjsonoptions
    middleware: "@expand:kettle.npm.bodyParser.json({that}.options.middlewareOptions)"
});

Consult the Infusion documentation on the compact format for expanders if you are unfamiliar with this syntax for designating elements in component options which arise from function calls.

If your middleware may act asynchronously by performing some raw I/O, you must use the grade kettle.plainAsyncMiddleware instead. This is to ensure that the Kettle request component is unmarked during the period that the system is not acting on behalf of the currently incoming request. If the code for the middleware is under your control, it is recommended that wherever possible you use [dataSources][DataSources.md] for I/O since their callbacks automatically perform the necessary request marking.

Built-in standard middleware bundled with Kettle

Here we describe the built-in middleware supplied with Kettle, which is mostly sourced from standard middleware in the express and pillarjs communities. You can consult the straightforward implementations in KettleMiddleware.js for suggestions on how to implement your own.

Grade nameMiddleware nameDescriptionAccepted optionsStandard IoC Path
kettle.middleware.json expressjs/body-parser Parses JSON request bodies, possibly with compression. middlewareOptions, forwarded to bodyParser.json(options) {middlewareHolder}.json
kettle.middleware.urlencoded expressjs/body-parser Applies URL decoding to a submitted request body middlewareOptions, forwarded to bodyParser.urlencoded(options) {middlewareHolder}.urlencoded
kettle.middleware.cookieParser expressjs/cookie-parser Parses the Cookie header as well as signed cookies via req.secret. secret and middlewareOptions, forwarded to the two arguments of cookieParser(secret, options) none
kettle.middleware.multer expressjs/multer Handles multipart/form-data, primarily for file uploading. middlewareOptions, forwarded to multer(options), and formFieldOptions, used to configure the field parameters for uploaded files as described in multer's documentation. Note: Multer's storage and fileFilter multer options require functions as their values, and are implemented in Kettle using subcomponents; see the documentation below on using kettle.middleware.multer for more details. none – user must configure on each use
kettle.middleware.session expressjs/session Stores and retrieves req.session from various backends middlewareOptions, forwarded to session(options) {middlewareHolder}.session when using kettle.server.sessionAware server
kettle.middleware.static expressjs/serve-static Serves static content from the filesystem root and middlewareOptions, forwarded to the two arguments of serveStatic(root, options) none – user must configure on each use
kettle.middleware.CORS Kettle built-in Adds CORS headers to outgoing HTTP request to enable cross-domain access allowMethods {String} (default "GET"),
origin {String} (default *),
credentials {Boolean} (default true)
- see CORS response headers
{middlewareHolder}.CORS
kettle.middleware.null Kettle built-in No-op middleware, useful for overriding and inactivating undesired middleware none {middlewareHolder}.null

Middleware which it makes sense to share configuration application-wide is stored in a standard holder of grade kettle.standardMiddleware which is descended from the grade kettle.middlewareHolder – the context reference {middlewareHolder} is recommended for referring to this if required – e.g. {middlewareHolder}.session.

Using the static middleware

Here is an example of mounting a section of a module's filesystem path at a particular URL. In this case, we want to mount the src directory of our Infusion module at the global path /infusion/, a common enough requirement. Note that this is done by registering a handler just as with any other Kettle request handler, even though in this case the useful request handling function is actually achieved by the middleware. The only function of the request handler is to serve the 404 message in case the referenced file is not found in the mounted image – in this case, it can refer to the standard builtin handler named kettle.request.notFoundHandler. Note that the request handler must declare explicitly that it will handle all URLs under its prefix path by declaring a route of /* – this is different to the express model of routing and middleware handling. Kettle will not dispatch a request to a handler unless its route matches all of the incoming URL.

Note that our static middleware can refer symbolically to the path of any module loaded using Infusion's module system fluid.module.register by means of interpolated terms such as %infusion.

Our config:

{
    "type": "examples.static.config",
    "options": {
        "gradeNames": ["fluid.component"],
        "components": {
            "server": {
                "type": "kettle.server",
                "options": {
                    "port": 8081,
                    "components": {
                        "infusionStatic": {
                            "type": "kettle.middleware.static",
                            "options": {
                                "root": "%infusion/src/"
                            }
                        },
                        "app": {
                            "type": "kettle.app",
                            "options": {
                                "requestHandlers": {
                                    "staticHandler": {
                                        "type": "examples.static.handler",
                                        "prefix": "/infusion",
                                        "route": "/*",
                                        "method": "get"
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
    }
}

Our handler:

fluid.defaults("examples.static.handler", {
    gradeNames: "kettle.request.http",
    requestMiddleware: {
        "static": {
            middleware: "{server}.infusionStatic"
        }
    },
    invokers: {
        handleRequest: {
            funcName: "kettle.request.notFoundHandler"
        }
    }
});

Using the multer middleware

This shows a single-file upload that allows image files only and saves them to disk storage; for more examples of possible usage, refer to the kettle.tests.multer.config.json5 configuration file in tests/configs, the middleware source code, and the Multer documentation.

Code for this example can be found in /examples/multipartForm.

fluid.defaults("examples.uploadConfig", {
    "gradeNames": ["fluid.component"],
    "components": {
        "server": {
            "type": "kettle.server",
            "options": {
                "port": 8081,
                "components": {
                    "imageUpload": {
                        "type": "kettle.middleware.multer",
                        "options": {
                            "formFieldOptions": {
                                "method": "single",
                                "fieldName": "image"
                            },
                            "components": {
                                "storage": {
                                    "type": "kettle.middleware.multer.storage.disk",
                                    "options": {
                                        "destination": "./examples/multipartForm/uploads"
                                    }
                                },
                                "fileFilter": {
                                    "type": "kettle.middleware.multer.filter.mimeType",
                                    "options": {
                                        "acceptedMimeTypes": ["image/png", "image/jpg", "image/gif"]
                                    }
                                }
                            }
                        }
                    },
                    "app": {
                        "type": "kettle.app",
                        "options": {
                            "requestHandlers": {
                                "imageUploadHandler": {
                                    "type": "examples.uploadConfig.handler",
                                    "route": "/upload",
                                    "method": "post"
                                }
                            }
                        }
                    }
                }
            }
        }
    }
});

And our corresponding handlers:

fluid.defaults("examples.uploadConfig.handler", {
    gradeNames: "kettle.request.http",
    requestMiddleware: {
        imageUpload: {
            middleware: "{server}.imageUpload"
        }
    },
    invokers: {
        handleRequest: "examples.uploadConfig.handleRequest"
    }
});

examples.uploadConfig.handleRequest = function (request) {
    var uploadedFileDetails = request.req.file;
    request.events.onSuccess.fire({
        message: fluid.stringTemplate("POST request received on path /upload; "
        + "file %originalName uploaded to %uploadedPath",
        {originalName: uploadedFileDetails.originalname, uploadedPath: uploadedFileDetails.path})
    });
};