title | layout | category |
---|---|---|
Defining and Working With Middleware |
default |
Kettle |
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.
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
|
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.
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 name | Middleware name | Description | Accepted options | Standard 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
.
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"
}
}
});
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})
});
};