-
Notifications
You must be signed in to change notification settings - Fork 147
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
guard against unhandled next()s in development #74
Comments
I'm working on this now. |
There are two ways to approach this: should I try to resolve the problem while printing out an error message, or should I just throw an error? Right now I'm leaning towards the latter, the former isn't always possible anyways. |
This is proving to be more complicated then expected. Consider the following examples (where compose([(ctx, next) => next(), (ctx, next) => { ctx.body = 'hello'; return longAction(); }]) and compose([(ctx, next) => longAction(), (ctx, next) => { ctx.body = 'hello'; }]) From compose's point of view, we can't tell those apart. We could try to "tag" the promise (and modify I think the best we can do is simply check if the middleware resolves before next. However, this can create a race condition, which could be very dangerous if e.g. a user doesn't set their node environment in prod. |
I have a working test for this, I haven't gotten around to updating the PR yet. You can see here This jest test passes: it('should throw if next() or skipNext() not called', async () => {
const middleware = [
async (ctx, next, skipNext) => {
}
];
expect.assertions(1);
try {
await compose(middleware)({}, terminate, terminate);
} catch (e) {
expect(e).toBeInstanceOf(Error);
}
}); If you don't have Adding the promise chain check cuts performance significantly. (125k req/s vs 330 req/s) I've also experimented with using the promise chain check to find out of order promise resolution. It really doesn't buy you much, though. I think the technique of injecting a known value to the bottom of the chain and then testing for it at the top (enabled by skipNext) is fast and verifies the entire chain is intact. The benchmark shows why tagging and anything that doesn't instrument the promise chain will fail: const fn = (ctx, next) => {
return logic().then(next).then(logic)
} |
This issue is about forgetting to return next, not forgetting to call next. |
That's why I say:
However, I agree that it is still really unreliable. |
Sorry for missing that point. I'm a little rushed today. I spent a lot of time on this previously and mostly just wanted to quickly dump what I had so you didn't duplicate effort or might find something in what I did. |
This is where I had gotten to in detecting out of order resolves, against this branch Initialize a bottom up counter by let minResolved = middleware.length + 1; After we know we have a promise: return result.then ((value) => {
if (i > minResolved) {
throw new Error('Middleware Promise chain resolved out of order');
}
minResolved = i;
return value;
}) No tests to share yet, sorry. |
I already have code to detect out of order resolves, but that can easily create a race condition. That's very dangerous if a user e.g. doesn't set their node env in production. |
@PlasmaPower I'm not understanding the race condition possibility. Can you share an example? |
compose([(ctx, next) => doStuff().then(doStuff), (ctx, next) => doStuff()]); Where the promise returned by doStuff takes a somewhat variable amount of time to resolve (for instance updating a row in a database). Depending on the timing, it may occur e.g. 1% of the time, noticeable in production but not dev. |
compose([(ctx, next) => doStuff().then(doStuff), (ctx, next) => doStuff()]); I'm confused. The first middleware doesn't call next. |
Oops. Sorry about that. I meant: compose([(ctx, next) => { next(); return doStuff().then(doStuff) }, (ctx, next) => doStuff()]); |
@PlasmaPower Thanks for the update. There's a clear break in the promise chain. I believe the If the promises all resolve in the right order and no error occurs to trigger an unhandled rejection, the code is still wrong because if an error did occur in the detached promise chain it would result in an unhandled rejection. Detecting out of order promises does not necessarily give early warning of the error, unless the promises consistently resolve out of order (or often enough to trigger errors). I've tried constructing some test cases around these scenarios. A lot of times the promises just happen resolve in order even when the chain is broken. It would be nice to have confidence in tests without having to noodle over exotic async scenarios. Injecting a known value at the end of the promise chain and testing for it at the top can tell you instantly that the chain is broken. Its fast and easy and you don't have to think much about async, just get good conditional test coverage and use the framing code in your tests (inject sentinel, test return result at the end). I like That's why I gave up on out of order detection. It's much slower in the benchmark, creates extra promises and still doesn't necessarily catch errors in test cases. The catch being that the case of intentionally breaking the chain requires some additional support and a perfect elegant solution to that case doesn't seem to have emerged. I lean toward supporting catching errors. Even the best programmers have bad days. |
Speaking of bad days, I just realized the out of order resolve detection code above doesn't cover cases where errors occur. return result.then ((value) => {
if (i > minResolved) {
throw new Error('Middleware Promise chain resolved out of order');
}
minResolved = i;
return value;
}, (error) => {
if (i > minResolved) {
throw new Error('Middleware Promise chain resolved out of order');
}
minResolved = i;
throw error;
})
} |
koajs/koa#882
The text was updated successfully, but these errors were encountered: