Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
feat: async/await support
BREAKING CHANGE: adds async/await support to pre, use and handler chains
  • Loading branch information
ghermeto committed Jul 10, 2020
1 parent bd34988 commit 12be9e2
Show file tree
Hide file tree
Showing 11 changed files with 483 additions and 13 deletions.
2 changes: 1 addition & 1 deletion .eslintrc.js
Expand Up @@ -127,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
@@ -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
@@ -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) => {
doSomethingAsync(function callback(val) {
req.something = val;
next();
});
});
````
45 changes: 41 additions & 4 deletions lib/chain.js
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
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
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();
* });
* });
* @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
Expand Up @@ -90,7 +90,7 @@
"report-latency": "./bin/report-latency"
},
"engines": {
"node": ">=10.21.0"
"node": ">=10.0.0"
},
"dependencies": {
"assert-plus": "^1.0.0",
Expand Down

0 comments on commit 12be9e2

Please sign in to comment.