Skip to content
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

Merged
merged 2 commits into from
Jul 10, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,10 @@ var config = {
env: {
browser: false,
node: true,
es6: false
es6: true
},
parserOptions: {
ecmaVersion: 2018
},
rules: {}
};
Expand Down Expand Up @@ -124,7 +127,7 @@ if (!process.env.NO_LINT) {
// stylistic.
if (!process.env.NO_STYLE) {
// Global
config.rules['max-len'] = [ERROR, { code: 80 }];
config.rules['max-len'] = [ERROR, { code: 80, ignoreComments: true }];

// Prettier
config.extends.push('prettier');
Expand Down
1 change: 0 additions & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
sudo: false
language: node_js
node_js:
- '8'
- '10'
- "lts/*" # Active LTS release
- "node" # Latest stable release
Expand Down
96 changes: 96 additions & 0 deletions docs/guides/8to9guide.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
---
title: restify 8.x to 9.x migration guide
permalink: /docs/8to9/
---

## Introduction

restify `9.x` comes with `async/await` support!

## Breaking Changes

### Drops support for Node.js `8.x`

Restify requires Node.js version `>=10.0.0`.

### Async/await support

`async/await` basic support for `.pre()`, `.use()` and route handlers.

#### Example

```js
const restify = require('restify');

const server = restify.createServer({});

server.use(async (req, res) => {
req.something = await doSomethingAsync();
});

server.get('/params', async (req, res) => {
const value = await asyncOperation(req.something);
res.send(value);
});
```

#### Middleware API (`.pre()` and `.use()`)

```js
server.use(async (req, res) => {
req.something = await doSomethingAsync();
});
```
- `fn.length === 2` (arity 2);
- `fn instanceof AsyncFunction`;
- if the async function resolves, it calls `next()`;
- any value returned by the async function will be discarded;
- if it rejects with an `Error` instance it calls `next(err)`;
- if it rejects with anything else it wraps in a `AsyncError` and calls `next(err)`;

#### Route handler API

```js
server.get('/something', async (req, res) => {
const someData = await fetchSomeDataAsync();
res.send({ data: someData });
});
```
- `fn.length === 2` (arity 2);
- `fn instanceof AsyncFunction`;
- if the async function resolves without a value, it calls `next()`;
- if the async function resolves with a string value, it calls `next(string)` (re-routes*);
- if the async function resolves with a value other than string, it calls `next(any)`;
- if it rejects with an `Error` instance it calls `next(err)`;
- if it rejects with anything else it wraps in a `AsyncError` and calls `next(err)` (error-handing**);

##### (*) Note about re-routing:
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 the given handler. The same is valid for resolving a async function. If the value returned by
the async function is a string, it will try to re-route to the given handler.

##### (**) Note about error handling:
Although it is recommended to always reject with an instance of Error, in a async function it is possible to
throw or reject without returning an `Error` instance or even anything at all. In such cases, the value rejected
will be wrapped on a `AsyncError`.

### Handler arity check
Handlers expecting 2 or fewer parameters added to a `.pre()`, `.use()` or route chain must be async functions, as:

```js
server.use(async (req, res) => {
req.something = await doSomethingAsync();
});
```

Handlers expecting more than 2 parameters shouldn't be async functions, as:

````js
// This middleware will be rejected and restify will throw
server.use(async (req, res, next) => {
ghermeto marked this conversation as resolved.
Show resolved Hide resolved
doSomethingAsync(function callback(val) {
req.something = val;
next();
});
});
````
45 changes: 41 additions & 4 deletions lib/chain.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

var assert = require('assert-plus');
var once = require('once');
var customErrorTypes = require('./errorTypes');

module.exports = Chain;

Expand Down Expand Up @@ -71,6 +72,15 @@ Chain.prototype.getHandlers = function getHandlers() {
* @returns {undefined} no return value
*/
Chain.prototype.add = function add(handler) {
assert.func(handler);
if (handler.length <= 2) {
// arity <= 2, must be AsyncFunction
assert.equal(handler.constructor.name, 'AsyncFunction');
} else {
// otherwise shouldn't be AsyncFunction
assert.notEqual(handler.constructor.name, 'AsyncFunction');
}

// _name is assigned in the server and router
handler._name = handler._name || handler.name;

Expand Down Expand Up @@ -144,7 +154,6 @@ Chain.prototype.run = function run(req, res, done) {
*/
function call(handler, err, req, res, _next) {
var arity = handler.length;
var error = err;
var hasError = err === false || Boolean(err);

// Meassure handler timings
Expand All @@ -157,19 +166,47 @@ function call(handler, err, req, res, _next) {
_next(nextErr, req, res);
}

function resolve(value) {
if (value && req.log) {
// logs resolved value
req.log.debug({ value }, 'Async handler resolved with a value');
}

return next(value);
}

function reject(error) {
if (!(error instanceof Error)) {
error = new customErrorTypes.AsyncError(
{
info: {
cause: error,
handler: handler._name,
method: req.method,
path: req.path ? req.path() : undefined
}
},
'Async middleware rejected without an error'
);
}
return next(error);
}

if (hasError && arity === 4) {
// error-handling middleware
handler(err, req, res, next);
return;
} else if (!hasError && arity < 4) {
// request-handling middleware
process.nextTick(function nextTick() {
handler(req, res, next);
const result = handler(req, res, next);
if (result && typeof result.then === 'function') {
result.then(resolve, reject);
}
});
return;
}

// continue
next(error, req, res);
return;
next(err);
}
3 changes: 2 additions & 1 deletion lib/errorTypes.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'),
AsyncError: errors.makeConstructor('AsyncError')
};
48 changes: 44 additions & 4 deletions lib/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -367,6 +367,13 @@ Server.prototype.close = function close(callback) {
* res.send({ hello: 'world' });
* next();
* });
* @example
* <caption>using with async/await</caption>
* server.get('/', function (req, res) {
* await somethingAsync();
* res.send({ hello: 'world' });
* next();
* }
*/
Server.prototype.get = serverMethodFactory('GET');

Expand Down Expand Up @@ -474,9 +481,16 @@ Server.prototype.opts = serverMethodFactory('OPTIONS');
* return next();
* });
* @example
* <caption>using with async/await</caption>
* server.pre(async function(req, res) {
* await somethingAsync();
* somethingSync();
* }
* @example
* <caption>For example, `pre()` can be used to deduplicate slashes in
* URLs</caption>
* server.pre(restify.pre.dedupeSlashes());
* @see {@link http://restify.com/docs/plugins-api/#serverpre-plugins|Restify pre() plugins}
*/
Server.prototype.pre = function pre() {
var self = this;
Expand Down Expand Up @@ -575,6 +589,22 @@ Server.prototype.first = function first() {
* * and/or a
* variable number of nested arrays of handler functions
* @returns {Object} returns self
* @example
* server.use(function(req, res, next) {
* // do something...
* return next();
* });
* @example
* <caption>using with async/await</caption>
* server.use(async function(req, res) {
* await somethingAsync();
* somethingSync();
* }
* @example
* <caption>For example, `use()` can be used to attach a request logger
* </caption>
* server.pre(restify.plugins.requestLogger());
* @see {@link http://restify.com/docs/plugins-api/#serveruse-plugins|Restify use() plugins}
*/
Server.prototype.use = function use() {
var self = this;
Expand All @@ -596,12 +626,22 @@ Server.prototype.use = function use() {
* new middleware function that only fires if the specified parameter exists
* in req.params
*
* Exposes an API:
* server.param("user", function (req, res, next) {
* // load the user's information here, always making sure to call next()
* @example
* server.param("user", function (req, res, next) {
* // load the user's information here, always making sure to call next()
* fetchUserInformation(req, function callback(user) {
* req.user = user;
* next();
* });
* });
ghermeto marked this conversation as resolved.
Show resolved Hide resolved
* @example
* <caption>using with async/await</caption>
* server.param("user", async function(req, res) {
* req.user = await fetchUserInformation(req);
* somethingSync();
* }
*
* @see http://expressjs.com/guide.html#route-param%20pre-conditions
* @see {@link http://expressjs.com/guide.html#route-param%20pre-conditions| Express route param pre-conditions}
* @public
* @memberof Server
* @instance
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@
"report-latency": "./bin/report-latency"
},
"engines": {
"node": ">=8.3.0"
"node": ">=10.0.0"
},
"dependencies": {
"assert-plus": "^1.0.0",
Expand Down
Loading