Compose middleware specifically for Koa.
$ npm install koa-compose
- Lead: @jonathanong @jongleberry
Compose the given middleware and return middleware.
MIT
Middleware composition utility
License: MIT License
Compose middleware specifically for Koa.
$ npm install koa-compose
Compose the given middleware and return middleware.
MIT
/**
* @param {Object} context
* @return {Promise}
* @api public
*/
return function (context, next) {
// last called middleware #
let index = -1
return dispatch(0)
function dispatch (i) {
if (i <= index) return Promise.reject(new Error('next() called multiple times'))
index = i
let fn = middleware[i]
if (i === middleware.length) fn = next
if (!fn) return Promise.resolve()
try {
return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
} catch (err) {
return Promise.reject(err)
}
}
}
return function (context, next) {
// last called middleware #
let index = -1
return disctxtch(0)
function disctxtch(i) {
if (i <= index) throw 'next() called multiple times';
index = i
let fn = middleware[i]
if (i === middleware.length) fn = next
if (!fn) return ;
try {
return fn(context, disctxtch.bind(null, i + 1));
} catch (err) {
throw err
}
}
}
For what purpose? Can you explain?
it's faster: tj/co#43
at least it'll work with or without it. it's an easy win. testing koa's benchmark, here's loa without yield*
:
with yield*
:
only problem is that next()
should always return a generator. that's an easy fix.
koa-compose@3 should have dist-tag
of next
on npm and @2 latest
since v3 is for koa v2 which is currently in alpha on npm
I happened to run an example with --strict-mode
and noticed a syntax error, well I'm not sure if compose is even bothered about strict mode, the issue is with those console.log statements which is highlighting strings.
PR #96 introduced an operation reduce
on each call of compose
, which shallow copies the middleware array. Previously middlewares can be added after koa.listen
, with the PR landed middlewares added by koa.use
after koa.listen
are no longer working. Furthermore, koa.middleware
property no longer reflects the correct middleware array since compose
fn holds its own copy of it.
Should this be considered as breaking changes/bugs?
app.js
const Koa = require('koa');
const app = new Koa();
const views = require('koa-views')(__dirname + '/views', {
extension: 'pug'
});
const json = require('koa-json');
const onerror = require('koa-onerror');
const bodyparser = require('koa-bodyparser')({
enableTypes:['json', 'form', 'text']
});
const logger = require('koa-logger');
const koa_static = require('koa-static')(__dirname + '/public')
const compose = require('koa-compose');
const index = require('./routes/index');
const users = require('./routes/users');
// error handler
onerror(app);
// composed middleware
const all = compose([
bodyparser,
json,
logger,
koa_static,
views
]);
// routes
app.use(all);
// routes
app.use(index.routes(), index.allowedMethods());
app.use(users.routes(), users.allowedMethods());
module.exports = app;
As is, a middleware which acts on the return path will consistently redefine the handler for the call to next
:
module.exports = (ctx, next) => {
// down
return next().then(() => {
// up
});
};
By resolving next
with ctx
, the handler could be hoisted:
const myMiddlewareUp = (ctx) => {
// up
};
module.exports = (ctx, next) => {
// down
return next().then(myMiddlewareUp);
};
https://gist.github.com/jonathanong/dbef759c675258b44c86
gonna just remove that test. major or minor bump?
I write three generator ,a
, b
and c
:
use compose:
var all = compose([a, b, c]);
app.use(all);
run, then throw a error:
app.use require a generator function
Then I try:
app.use(a);
app.use(b);
app.use(c);
It work well.
Hi, i try write a api service for my project using a middlewares.
I write this middleware
export const globaHandler = async ({ response }, next) => {
await next();
const { errors } = response; // reference error response is null;
};
if i write like that
export const globaHandler = async (ctx, next) => {
await next();
const { errors } = ctx.response; // { object }
};
Why?
Part of my api service code
const doRequest = async (ctx, next) => {
const response = await axios(ctx.options);
ctx.response = response;
await next();
};
const api = async (context, middleware) => {
const middlewares = [
apiMiddleware.globaHandler,
];
if (middleware && Array.isArray(middleware)) {
middlewares.push(...middleware);
} else if (middleware) {
middlewares.push(middleware);
}
middlewares.push(doRequest);
const composition = compose(middlewares);
const handleResponse = async (context) => {
try {
await composition(context);
} catch (err) {
console.error('Error at middleware composition', err);
}
return context.response;
};
return handleResponse(context);
};
/**
* @param {makeRequestObject} { resource, params, query, data, middleware, cancel }
* @returns {Promise}
*/
const makeRequest = function ({ resource, params, query, data, middleware }) {
const { url, method, auth } = getResource(resource, params);
const options = {
url,
method,
headers: {},
params: query,
meta: { auth },
};
const context = {
options,
response: null,
};
return api(context, middleware);
};
want to handle them specially? throw if length 0, return the first element if length 1.
Since MIT license explicitly requires the inclusion of the license text, it is insufficient to merely state the license used as "MIT" in package.json
.
Quote from the MIT license from https://mit-license.org/
... The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
Solution: create license.md
or license.txt
to include the content of the MIT license.
I'd find this super handy personally, but the duration would compound so we'll have to diff or something
When trying to run hello world with
node --harmony --use_strict app.js
I get
/home/user/node_modules/koa/node_modules/koa-compose/index.js:98
console.log(' \033[1m%d | \033[0m<< \033[36m%s\033[0m', this._level, name
^^
SyntaxError: Octal literals are not allowed in strict mode.
In the following code, why use Promise.resolve()
to wrap the execution results of each middleware?
try {
return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
}
I think it is enough to use Promise.resolve()
to wrap dispatch(0)
and return the rest of middleware execution results directly. Just like the following code:
return function (context, next) {
// last called middleware #
let index = -1
- return dispatch(0)
+ return Promise.resolve(dispatch(0))
function dispatch (i) {
if (i <= index) return Promise.reject(new Error('next() called multiple times'))
index = i
let fn = middleware[i]
if (i === middleware.length) fn = next
if (!fn) return Promise.resolve()
try {
- return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
+ return fn(context, dispatch.bind(null, i + 1));
} catch (err) {
return Promise.reject(err)
}
}
}
Could you help me to answer this question, please!
Hi, I am learning source code about koa.I'm sorry, my English is not good.
Line 36 in 25568a3
I donโt understand how the error is thrown when I find that next is called repeatedly.
Below is my test code
const Koa = require('koa');
const app = new Koa();
app.use(
async function (ctx, next) {
try {
await next();
} catch (e) {
console.log('catch error:', e.toString())
}
}
)
app.use(
function (ctx, next) {
console.log('m1');
next();
next();
}
)
app.use(
function (ctx, next) {
console.log('m2');
next();
next();
}
)
app.listen(3000);
The console will print a log when I trigger it
m1
m2
(node:15374) UnhandledPromiseRejectionWarning: Error: next() called multiple times
at dispatch (/Users/helloWorld/code/learn-koa/playground/node_modules/koa-compose/index.js:36:45)
at /Users/helloWorld/code/learn-koa/playground/main.js:27:9
at dispatch (/Users/helloWorld/code/learn-koa/playground/node_modules/koa-compose/index.js:42:32)
at /Users/helloWorld/code/learn-koa/playground/main.js:18:9
at dispatch (/Users/helloWorld/code/learn-koa/playground/node_modules/koa-compose/index.js:42:32)
at /Users/helloWorld/code/learn-koa/playground/main.js:8:19
at dispatch (/Users/helloWorld/code/learn-koa/playground/node_modules/koa-compose/index.js:42:32)
at /Users/helloWorld/code/learn-koa/playground/node_modules/koa-compose/index.js:34:12
at Application.handleRequest (/Users/helloWorld/code/learn-koa/playground/node_modules/koa/lib/application.js:168:12)
at Server.handleRequest (/Users/helloWorld/code/learn-koa/playground/node_modules/koa/lib/application.js:150:19)
(Use `node --trace-warnings ...` to show where the warning was created)
(node:15374) UnhandledPromiseRejectionWarning: Unhandled promise rejection. This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). To terminate the node process on unhandled promise rejection, use the CLI flag `--unhandled-rejections=strict` (see https://nodejs.org/api/cli.html#cli_unhandled_rejections_mode). (rejection id: 1)
(node:15374) [DEP0018] DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.
(node:15374) UnhandledPromiseRejectionWarning: Error: next() called multiple times
at dispatch (/Users/helloWorld/code/learn-koa/playground/node_modules/koa-compose/index.js:36:45)
at /Users/helloWorld/code/learn-koa/playground/main.js:19:9
at dispatch (/Users/helloWorld/code/learn-koa/playground/node_modules/koa-compose/index.js:42:32)
at /Users/helloWorld/code/learn-koa/playground/main.js:8:19
at dispatch (/Users/helloWorld/code/learn-koa/playground/node_modules/koa-compose/index.js:42:32)
at /Users/helloWorld/code/learn-koa/playground/node_modules/koa-compose/index.js:34:12
at Application.handleRequest (/Users/helloWorld/code/learn-koa/playground/node_modules/koa/lib/application.js:168:12)
at Server.handleRequest (/Users/helloWorld/code/learn-koa/playground/node_modules/koa/lib/application.js:150:19)
at Server.emit (events.js:315:20)
at parserOnIncoming (_http_server.js:874:12)
(node:15374) UnhandledPromiseRejectionWarning: Unhandled promise rejection. This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). To terminate the node process on unhandled promise rejection, use the CLI flag `--unhandled-rejections=strict` (see https://nodejs.org/api/cli.html#cli_unhandled_rejections_mode). (rejection id: 2)
m1
m2
(node:15374) UnhandledPromiseRejectionWarning: Error: next() called multiple times
at dispatch (/Users/helloWorld/code/learn-koa/playground/node_modules/koa-compose/index.js:36:45)
at /Users/helloWorld/code/learn-koa/playground/main.js:27:9
at dispatch (/Users/helloWorld/code/learn-koa/playground/node_modules/koa-compose/index.js:42:32)
at /Users/helloWorld/code/learn-koa/playground/main.js:18:9
at dispatch (/Users/helloWorld/code/learn-koa/playground/node_modules/koa-compose/index.js:42:32)
at /Users/helloWorld/code/learn-koa/playground/main.js:8:19
at dispatch (/Users/helloWorld/code/learn-koa/playground/node_modules/koa-compose/index.js:42:32)
at /Users/helloWorld/code/learn-koa/playground/node_modules/koa-compose/index.js:34:12
at Application.handleRequest (/Users/helloWorld/code/learn-koa/playground/node_modules/koa/lib/application.js:168:12)
at Server.handleRequest (/Users/helloWorld/code/learn-koa/playground/node_modules/koa/lib/application.js:150:19)
(node:15374) UnhandledPromiseRejectionWarning: Unhandled promise rejection. This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). To terminate the node process on unhandled promise rejection, use the CLI flag `--unhandled-rejections=strict` (see https://nodejs.org/api/cli.html#cli_unhandled_rejections_mode). (rejection id: 3)
(node:15374) UnhandledPromiseRejectionWarning: Error: next() called multiple times
at dispatch (/Users/helloWorld/code/learn-koa/playground/node_modules/koa-compose/index.js:36:45)
at /Users/helloWorld/code/learn-koa/playground/main.js:19:9
at dispatch (/Users/helloWorld/code/learn-koa/playground/node_modules/koa-compose/index.js:42:32)
at /Users/helloWorld/code/learn-koa/playground/main.js:8:19
at dispatch (/Users/helloWorld/code/learn-koa/playground/node_modules/koa-compose/index.js:42:32)
at /Users/helloWorld/code/learn-koa/playground/node_modules/koa-compose/index.js:34:12
at Application.handleRequest (/Users/helloWorld/code/learn-koa/playground/node_modules/koa/lib/application.js:168:12)
at Server.handleRequest (/Users/helloWorld/code/learn-koa/playground/node_modules/koa/lib/application.js:150:19)
at Server.emit (events.js:315:20)
at parserOnIncoming (_http_server.js:874:12)
(node:15374) UnhandledPromiseRejectionWarning: Unhandled promise rejection. This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). To terminate the node process on unhandled promise rejection, use the CLI flag `--unhandled-rejections=strict` (see https://nodejs.org/api/cli.html#cli_unhandled_rejections_mode). (rejection id: 4)
If I change the function to asynchronous call, the error can be caught
const Koa = require('koa');
const app = new Koa();
app.use(
async function (ctx, next) {
try {
await next();
} catch (e) {
console.log('catch error:', e.toString())
}
}
)
app.use(
async function (ctx, next) {
console.log('m1');
await next();
await next();
}
)
app.use(
async function (ctx, next) {
console.log('m2');
await next();
await next();
}
)
app.listen(3000);
Is it necessary to use asynchronous function๏ผ
It would be extremely useful for debugging purposes if koa-compose
logged the name of each middleware before and after it executed.
I propose adding the debug
packaging and configuration a namespace of koa:compose
to match Koa
's convention and using it implement the aforementioned logs.
this is just my ocd, but imo the latter looks much nicer:
app.use(route.get('/foo', compose([ middleware1, middleware2, routeHandler ])))
vs
app.use(route.get('/foo', compose(middleware1, middleware2, routeHandler)))
would you take a pr implementing this in a backward compatible way?
/cc @jonathanong
Hi!
Thank you for koa โ๏ธ
I use [email protected] and koa@2.
Often my middleware do things like this:
app.use(function(ctx, next) {
ctx.state.foo = 'bar' // <-- sync operation
next()
})
I mean they perform synchronous operations.
At the moment koa-compose reduce operations of all types to asynchronous operations.
We could perform more (non-blocking) work per one event cycle (v8) than we do now.
Let me explain what I mean.
I took the example from official site and I tried realized my vision. I replaced Koa, Context & Compose so that they consider synchronous/asynchronous behavior.
It looks like after these modifications up/down stream behavior works as expected.
See code bellow
// node v7.6.0
// const Koa = require('koa')
function isAsyncFunction(any) {
return Object.prototype.toString.call(any) === '[object AsyncFunction]'
}
class Koa {
constructor() {
this.middleware = []
}
use(fn) {
this.middleware.push(fn)
}
dispatch() {
let ctx = new Context()
let iterator = this.middleware[Symbol.iterator]()
let next = function () {
let {done,value} = iterator.next()
if (done) return
if (isAsyncFunction(value)) {
return value(ctx, next).catch(console.error) // TODO: handle errors
} else {
return value(ctx, next) // TODO: try {} ... catch(e) {}
}
}
next()
}
}
class Context {
constructor() {
this.method = 'EMULATION'
this.url = '/'
this.headers = {}
this.body = null
}
set(key, value) {
this.headers[key] = value
}
}
let app = new Koa()
// ----------- EXAMPLE ---------------
// x-response-time
app.use(async function (ctx, next) {
const start = new Date()
console.log(1)
await next()
console.log(6)
const ms = new Date() - start
ctx.set('X-Response-Time', `${ms}ms`)
})
// logger
app.use(async function (ctx, next) {
const start = new Date()
console.log(2)
await next()
console.log(5)
const ms = new Date() - start
console.log(`${ctx.method} ${ctx.url} - ${ms}`)
})
// a sync function
app.use(function (ctx, next) {
// a sync function
console.log(3)
next()
})
// response
app.use(ctx => {
console.log(4)
ctx.body = 'Hello World'
})
// ----------- \EXAMPLE ---------------
app.dispatch() // emulation
// app.listen(3000)
// curl localhost:3000
app.use(function(ctx, next) {
next().then() // <-- may not work. I did not tested this case.
})
I don't force use my implementation. It's only for example.
Key message is: if we would care about sync/async functions it probably may increase performance.
What do you think about it?
Test case:
const compose = require("koa-compose");
compose([
function A(ctx, next) {
console.log("A called");
return next();
},
])({}, function B(ctx, next) {
console.log("B called");
return next(); // if call next function here, it will enter an infinite loop
}).then(() => console.log("done"), err => console.error(err));
displays several times in our case
would be nice :)
This makes Node v4 a requirement which is not mentioned anywhere.
Shame to break Node 0.12.x support for such a minor syntax change, seeing as Koa@2 supports 0.12.x.
Hi,
I wrote test like below, it throw an error but it come like this
UnhandledPromiseRejectionWarning: Error: next() called multiple times
and the test failed. Is this expected?
it(`should throw if next() is called multiple times in non async function`, async () => {
const stack = [];
stack.push((ctx, next) => {
next();
next();
});
expect.hasAssertions();
try {
await compose(stack)({});
} catch (error) {
expect(error).toBeInstanceOf(Error);
}
});
see:
Line 234 in e754ca3
the then
function throws new Error
which is the same type as the stack
function. The returned promise's then
function should fail the test rather than throw a new Error
.
I am learning the source code of compose, and I found that for each callback it return a promise object.
I can not understand why should do that return a promise object. Could you please tell me the reason?
Thank you very mush. (^_^)v
const compose = require('koa-compose');
const middleware = [
function(ctx,next){
ctx.step1 = true;
next();
},
function(ctx,next){
ctx.step2 = true;
next();
},
function(ctx,next){
ctx.step3 = true;
a = b;
next();
}
];
const ctx = {};
compose(middleware)(ctx).then(console.log).catch(console.error);
I can't catch the error ~
Whilst fn.name
will work across node environments, it appears to be non-standard.
However, the real problem is that anonymous middleware will not get a name in the compose stack, making debugging a little more difficult. This situation is difficult to avoid in middleware returned by koa-route for example (the name field is read only).
Example with koa-route:
app.use(route.get('/id/:id', middleware.getById));
becomes:
app.use(function* getById() {
yield route.get('/id/:id', middleware.getById).apply(this, arguments);
});
Perhaps a better way would be to allow explicit naming of composed routes. i.e.:
compose([{ name: 'a', middleware: a }, ...]);
although that is a little bulky. I'd be happy to submit a PR if you have a preferred way of adding the name in.
When I first request the page I get the correct response, however the second request I get a 500 response.
app.use(function* (next) {
// Array with middleware functions,
// generated elsewhere, but on every request
var mws = [];
yield compose(mws);
yield next;
});
Error stack:
TypeError: Cannot call method 'call' of undefined
at Object. (/Users/thomas/CloudStation/Dropbox/www/koa-router/node_modules/koa-compose/index.js:36:19)
at GeneratorFunctionPrototype.next (native)
at next (/Users/thomas/CloudStation/Dropbox/www/koa-router/node_modules/koa/node_modules/co/index.js:76:21)
at Object. (/Users/thomas/CloudStation/Dropbox/www/koa-router/node_modules/koa/node_modules/co/index.js:56:5)
at next (/Users/thomas/CloudStation/Dropbox/www/koa-router/node_modules/koa/node_modules/co/index.js:92:21)
at Object. (/Users/thomas/CloudStation/Dropbox/www/koa-router/node_modules/koa/node_modules/co/index.js:56:5)
at Server. (/Users/thomas/CloudStation/Dropbox/www/koa-router/node_modules/koa/lib/application.js:105:8)
at Server.EventEmitter.emit (events.js:101:17)
at HTTPParser.parserOnIncoming as onIncoming
at HTTPParser.parserOnHeadersComplete (_http_common.js:111:23)
function compose (middleware) {
if(!Array.isArray(middleware)) throw TypeError('Middleware stack must be an array!')
for (const fn of middleware) {
if(typeof fn !== 'function') throw new TypeError('Middleware must be composed of functions!')
}
let index = -1 //
/**
@param {Object} context
@return {Promise}
@api compose
*/
return function (context, next) {
return new Promise((resolve, reject) => {
function dispatch (i) { //
if (i <= index) {
console.warn('[warn]next() called multiple times')
return //reject(new Error('next() called multiple times'))
}
index=i
let fn=middleware[i]
if(i===middleware.length) fn=next //
if(!fn) return resolve()
try {
return resolve(fn(context,dispatch.bind(null,i+1)))
} catch (error) {
return reject(error)
}
}
dispatch(0)
})
}
}
Because of #65, specifically lodash.flatten
, compose()
returns a generator based on a copy of the provided array from that point in time. Any changes made to this array subsequently will no longer be reflected in the returned middleware.
Previously, one could mutate the array after calling compose
.
some API for extending externally, for example the new debug stuff I just added could live in a different module. I'd like to provide a nicer alternative that outputs HTML documents since the terminal quickly becomes a clusterfuck, especially with parallel requests
History.md is outdated
Hello,
could you please update koa-compose on npm + update the dependency in koa ? I need it for the _name / name order split before I can package my audit middleware.
Thanks
I found 2 outdated tests . They are on line 111 and line 139 respectively.
Should their names changed to awaiting something?
Also, one of the test ('should work when yielding at the end of the stack with yield*', line 139) does not make sense anymore. next
is a function that returns a promise, not a promise. Await next will simply resolve the function itself without calling it. Should this test be removed?
Hey, folks --
I feel like I must be doing something silly here, but I'm experiencing an issue where the middleware chain is not completely resolved before my code passes control to the final handler. Let me know if this is something I'm doing or an actual bug, thanks!
Setup:
Expected Output:
Middleware #1: Triggered
Middleware #1: Next
Middleware #2: Triggered
Middleware #2: Next
Middleware #3: Triggered
Middleware #3: Next
Finished
Actual Output:
Middleware #1: Triggered
Middleware #1: Next
Middleware #2: Triggered
Middleware #2: Next
Middleware #3: Triggered
Finished
Middleware #3: Next
Code:
const compose = require('koa-compose');
/**
* Simple App Structure.
*/
class App {
constructor () {
this.middleware = [];
}
use (fn) {
if (typeof fn !== 'function') {
throw new TypeError('middleware must be a function!');
}
this.middleware.push(fn);
return this;
}
route (handler) {
let fn = compose(this.middleware);
return fn(this).then( () => this.handleRequest(handler) );
}
handleRequest (handler) {
return handler();
}
}
let app = new App();
/**
* Setup Middleware
*/
app.use( (ctx, next) => {
console.log("Middleware #1: Triggered");
console.log("Middleware #1: Next");
next();
});
app.use( (ctx, next) => {
console.log("Middleware #2: Triggered");
console.log("Middleware #2: Next");
next();
});
app.use( (ctx, next) => {
console.log("Middleware #3: Triggered");
setTimeout( () => {
console.log("Middleware #3: Next");
next();
}, 2000);
});
app.route(() => console.log("Finished") );
Given that koa's master branch now holds koa v2, I think we should do the same or set the default branch to next. We should also set the default npm release to v3 (the next branch).
Right now all of the tests are under one describe
. I'm not sure we need to split them into more files, but multiple describe
sections would certainly be nice.
A declarative, efficient, and flexible JavaScript library for building user interfaces.
๐ Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. ๐๐๐
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google โค๏ธ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.