-
Notifications
You must be signed in to change notification settings - Fork 983
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
9.X - async/await #1833
9.X - async/await #1833
Conversation
ops... forgot to add examples on the docs (and build the docs)... just added. |
doh! the 3rd commit with async/await docs made this PR diff looks huge... if you can go into each individual commit to add your comments, I appreciate :) |
cc /@cprussin |
Nit: For future, would recommend separate, smaller PRs for easier reviewing/accounting. There's a lot going on here (style changes, docs, Node 8.x EOL, etc) and its a bit hard to parse it all at once. A few questions that might influence the direction of the PR:
|
Yes, I broke them into different commits, but you are right. After I added the documentation commit I realized it should have waited. I won't send multiple commits next time.
I followed the ecosystem. I guess we could check
yes, this allows mixed mode. Like the other frameworks that support both models.
I used the same approach as fastify. You are right, we can't enforce it and it deviates from the current behavior. I don't think changing the error handling behavior should be coupled with this PR. Right now, an What I can do is check for if
The biggest problem with fastify happens because they allow the user to both return the function with the body or use We should recommend using |
@DonutEspresso Started issue #1837 to discuss this further. |
docs/_api/formatters.md
Outdated
@@ -50,7 +50,7 @@ Format a response for being sent over the wire | |||
|
|||
Type: [Function][9] | |||
|
|||
**Parameters** | |||
#### Parameters |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you please generate docs as a separate commit outside of this PR?
lib/chain.js
Outdated
name: 'AsyncHandlerRejection', | ||
info: { handler: handler._name } | ||
}, | ||
`Async middleware ${handler._name} rejected without an error` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please avoid dynamic input in messages.
lib/chain.js
Outdated
`Async middleware ${handler._name} rejected without an error` | ||
); | ||
} | ||
next(error); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: add return to be consistent and to highlight that this fn ends here
lib/chain.js
Outdated
`Async middleware ${handler._name} rejected without an error` | ||
); | ||
} | ||
next(error); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it's expected with async-await but this will catch syntax errors etc.
Maybe explain in the 9.x migration guide.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should the migration guide be part of a separate PR, with the docs?
lib/chain.js
Outdated
error = new errors.VError( | ||
{ | ||
name: 'AsyncHandlerRejection', | ||
info: { handler: handler._name } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please add cause
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what should add here as a cause
? This error is created whenever the user rejects an async function without passing it anything:
server.get('/params', async (req, res) => {
return Promise.reject();
});
Do you have suggestions for cause
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You know... just got a mistake in my code... I wasn't wrapping synthetic errors because I thought Restify would throw anything, but actually, if the value passed to next()
is a string, it will try to route it, for instance:
const restify = require('restify');
const server = restify.createServer({});
server.use(restify.plugins.queryParser({mapParams:true}));
server.get('/route', (req, res, next) => {
next('getresult');
});
server.get('/string', (req, res, next) => {
next('invalid');
});
server.get('/number', (req, res, next) => {
next(1);
});
server.get('/error', (req, res, next) => {
next(new Error(''));
});
server.get('/result', (req, res, next) => {
res.send(req.query);
next();
});
server.listen(8081, 'localhost', function () {
console.info('server started');
});
The result of curl http://localhost:8081/error?id=1
is:
{"code":"Internal","message":"Error"}
The result of curl http://localhost:8081/number?id=1
is:
{"code":"Internal","message":"1"}
The result of curl http://localhost:8081/string?id=1
is:
{"code":"Internal","message":"RouteMissingError: Route by name doesn't exist"}
The result of curl http://localhost:8081/route?id=1
is:
{"id":"1"}
I will update the code to always wrap rejections and update #1837 .
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
FWIW trying to route on next with a string is IMHO an awful barnacle of Restify, it's worth considering deprecating it--it's essentially a GOTO with all the pain that comes along.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
wrapping all non-errors in AsyncHandlerError
actually turned out pretty nice, because if nothing is specified, the cause is undefined
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
and I just discovered cause
must be an instance of Error
, so I'm moving the cause
to inside the info
.
package.json
Outdated
@@ -90,7 +90,7 @@ | |||
"report-latency": "./bin/report-latency" | |||
}, | |||
"engines": { | |||
"node": ">=8.3.0" | |||
"node": ">=10.21.0" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please add a migration guide like 6to7 and highlight Node version bump.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should the migration guide be a separate PR?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yes, please. or you can add related pieces in this PR
package.json
Outdated
@@ -90,7 +90,7 @@ | |||
"report-latency": "./bin/report-latency" | |||
}, | |||
"engines": { | |||
"node": ">=8.3.0" | |||
"node": ">=10.21.0" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we need to change Travis CI minimum node test version as well
lib/chain.js
Outdated
if (!error) { | ||
error = new errors.VError( | ||
{ | ||
name: 'AsyncHandlerRejection', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please define new error types in: https://github.com/restify/node-restify/blob/master/lib/errorTypes.js
@ghermeto if you want some help I can break this PR in three and force-push only one commit here. It might be easier for folks to review. |
thank you @mmarchini. I force pushed removing the generated docs and added some changes Peter requested. |
Yeah, that makes sense. My concern is that this opens up a new footgun with promise usage. The following handler will just hang forever:
I haven't used promises enough to know what's the best way to handle this. Is it common to do runtime assertions on thenables? I'd much rather the server blew up than fail silently. One way to enforce this is by saying async functions should only have an arity of 2, and we don't pass next to those functions.
Sorry! I wasn't very clear, I was suggesting that we formalize the next() contract, and the promise pathways that lead up to that. Essentially, there's three ways to cede control back to restify through next today, and this PR adds a variation for each of those contracts:
I think there's a good argument to be made that anything that is not one of those three should cause a runtime exception. But I feel less strongly than I do about at least formalizing and documenting it. |
What do you mean by |
Oops, sorry, maybe I should have left my comments in #1837! |
I will reply to your comments on #1837 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎉 looks good!
docs/guides/8to9guide.md
Outdated
|
||
### Drops support for Node.js `8.x` | ||
|
||
Restify requires Node.js version `>=10.21.0`. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there anything in particular for 10.21.0 instead of just 10.x?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess not... I will update it
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Overall LGTM (I didn't review tests yet), although I'm still processing the need to call req.send
and a few other things. I'll leave design-related comments in the issue.
What I can do is check for if err instanceof Error or err instanceof VError and, if not, I can force wrap it under an AsyncHandlerRejection (or AsyncHandlerError), but that will differ from the callback behavior that allows user to throw anything.
Async functions and Promises don't have an equivalent to "callback throw behavior". A rejection is the equivalent of calling a callback passing an Error object to it (so in our case, calling next(err)
).
docs/guides/8to9guide.md
Outdated
- `fn.length === 2` (arity 2); | ||
- `fn instanceof AsyncFunction`; | ||
- if the async function resolves, it calls `next()`; | ||
- any value resolved will be discarded; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you elaborate on "any value resolved will be discarded"?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
do you think it would read better "any value returned by the async function?". I will make the change.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
docs/guides/8to9guide.md
Outdated
```js | ||
server.use(async (req, res) => { | ||
req.something = await doSomethingAsync(); | ||
}); | ||
``` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks like a middleware example?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OMG...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fixed
docs/guides/8to9guide.md
Outdated
The `8.x` API allows re-routing when calling `next()` with a string value. If the string matches a valid route, | ||
it will re-route to that handler. The same is valid for resolving a async function. Whatever value it returns, | ||
will be sent to `next()` and the behavior will be exactly the same. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How do users call next()
if it is not passed as an argument to async functions?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, I see, if the async function returns a string it will reroute. I guess it makes sense for rerouting, I was thinking of cross-route calls instead.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
rephrased
lib/errorTypes.js
Outdated
@@ -5,5 +5,6 @@ var errors = require('restify-errors'); | |||
// This allows Restify to work with restify-errors v6+ | |||
module.exports = { | |||
RequestCloseError: errors.makeConstructor('RequestCloseError'), | |||
RouteMissingError: errors.makeConstructor('RouteMissingError') | |||
RouteMissingError: errors.makeConstructor('RouteMissingError'), | |||
AsyncHandlerError: errors.makeConstructor('AsyncHandlerError') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The name is AsyncHandlerError
but we also use it on middlewares. Wouldn't it cause confusion to users? Maybe a more generic AsyncError
would fit better?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
renamed
I spoke with @hekike and he said I can merge this. Just want to make sure @mmarchini (who had comments) and others can take a look at it before I do. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice! Might be worth dogfooding this a bit and getting a feel for the ergonomics and possible gotchas before wide adoption.
ok, since no new objection was raised, and all comments were addressed, I'm merging this code. We can always submit fixes before we make a new release. |
BREAKING CHANGE: drops suppoprt to node 8 and updates linting rules
BREAKING CHANGE: adds async/await support to pre, use and handler chains
What happens if the handler calls server.get('/async', async function (req, res, next) {
const body = await thatThing();
res.send(body);
next();
}); Is this valid? |
Pre-Submission Checklist
make prepush
Changes
This PR has two commits:
pre
,use
and route handlers;Example of async handling:
Basically, the
Chain
call will check if the handler return is aPromise
(by checking the existence of.then
, as Falcor Router does it) and resolves by callingnext
or rejects by callingnext with error
..