Skip to content

Commit 12be9e2

Browse files
committedJul 10, 2020
feat: async/await support
BREAKING CHANGE: adds async/await support to pre, use and handler chains
1 parent bd34988 commit 12be9e2

11 files changed

+483
-13
lines changed
 

‎.eslintrc.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ if (!process.env.NO_LINT) {
127127
// stylistic.
128128
if (!process.env.NO_STYLE) {
129129
// Global
130-
config.rules['max-len'] = [ERROR, { code: 80 }];
130+
config.rules['max-len'] = [ERROR, { code: 80, ignoreComments: true }];
131131

132132
// Prettier
133133
config.extends.push('prettier');

‎.travis.yml

-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
sudo: false
22
language: node_js
33
node_js:
4-
- '8'
54
- '10'
65
- "lts/*" # Active LTS release
76
- "node" # Latest stable release

‎docs/guides/8to9guide.md

+96
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
---
2+
title: restify 8.x to 9.x migration guide
3+
permalink: /docs/8to9/
4+
---
5+
6+
## Introduction
7+
8+
restify `9.x` comes with `async/await` support!
9+
10+
## Breaking Changes
11+
12+
### Drops support for Node.js `8.x`
13+
14+
Restify requires Node.js version `>=10.0.0`.
15+
16+
### Async/await support
17+
18+
`async/await` basic support for `.pre()`, `.use()` and route handlers.
19+
20+
#### Example
21+
22+
```js
23+
const restify = require('restify');
24+
25+
const server = restify.createServer({});
26+
27+
server.use(async (req, res) => {
28+
req.something = await doSomethingAsync();
29+
});
30+
31+
server.get('/params', async (req, res) => {
32+
const value = await asyncOperation(req.something);
33+
res.send(value);
34+
});
35+
```
36+
37+
#### Middleware API (`.pre()` and `.use()`)
38+
39+
```js
40+
server.use(async (req, res) => {
41+
req.something = await doSomethingAsync();
42+
});
43+
```
44+
- `fn.length === 2` (arity 2);
45+
- `fn instanceof AsyncFunction`;
46+
- if the async function resolves, it calls `next()`;
47+
- any value returned by the async function will be discarded;
48+
- if it rejects with an `Error` instance it calls `next(err)`;
49+
- if it rejects with anything else it wraps in a `AsyncError` and calls `next(err)`;
50+
51+
#### Route handler API
52+
53+
```js
54+
server.get('/something', async (req, res) => {
55+
const someData = await fetchSomeDataAsync();
56+
res.send({ data: someData });
57+
});
58+
```
59+
- `fn.length === 2` (arity 2);
60+
- `fn instanceof AsyncFunction`;
61+
- if the async function resolves without a value, it calls `next()`;
62+
- if the async function resolves with a string value, it calls `next(string)` (re-routes*);
63+
- if the async function resolves with a value other than string, it calls `next(any)`;
64+
- if it rejects with an `Error` instance it calls `next(err)`;
65+
- if it rejects with anything else it wraps in a `AsyncError` and calls `next(err)` (error-handing**);
66+
67+
##### (*) Note about re-routing:
68+
The `8.x` API allows re-routing when calling `next()` with a string value. If the string matches a valid route,
69+
it will re-route to the given handler. The same is valid for resolving a async function. If the value returned by
70+
the async function is a string, it will try to re-route to the given handler.
71+
72+
##### (**) Note about error handling:
73+
Although it is recommended to always reject with an instance of Error, in a async function it is possible to
74+
throw or reject without returning an `Error` instance or even anything at all. In such cases, the value rejected
75+
will be wrapped on a `AsyncError`.
76+
77+
### Handler arity check
78+
Handlers expecting 2 or fewer parameters added to a `.pre()`, `.use()` or route chain must be async functions, as:
79+
80+
```js
81+
server.use(async (req, res) => {
82+
req.something = await doSomethingAsync();
83+
});
84+
```
85+
86+
Handlers expecting more than 2 parameters shouldn't be async functions, as:
87+
88+
````js
89+
// This middleware will be rejected and restify will throw
90+
server.use(async (req, res, next) => {
91+
doSomethingAsync(function callback(val) {
92+
req.something = val;
93+
next();
94+
});
95+
});
96+
````

‎lib/chain.js

+41-4
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
var assert = require('assert-plus');
44
var once = require('once');
5+
var customErrorTypes = require('./errorTypes');
56

67
module.exports = Chain;
78

@@ -71,6 +72,15 @@ Chain.prototype.getHandlers = function getHandlers() {
7172
* @returns {undefined} no return value
7273
*/
7374
Chain.prototype.add = function add(handler) {
75+
assert.func(handler);
76+
if (handler.length <= 2) {
77+
// arity <= 2, must be AsyncFunction
78+
assert.equal(handler.constructor.name, 'AsyncFunction');
79+
} else {
80+
// otherwise shouldn't be AsyncFunction
81+
assert.notEqual(handler.constructor.name, 'AsyncFunction');
82+
}
83+
7484
// _name is assigned in the server and router
7585
handler._name = handler._name || handler.name;
7686

@@ -144,7 +154,6 @@ Chain.prototype.run = function run(req, res, done) {
144154
*/
145155
function call(handler, err, req, res, _next) {
146156
var arity = handler.length;
147-
var error = err;
148157
var hasError = err === false || Boolean(err);
149158

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

169+
function resolve(value) {
170+
if (value && req.log) {
171+
// logs resolved value
172+
req.log.debug({ value }, 'Async handler resolved with a value');
173+
}
174+
175+
return next(value);
176+
}
177+
178+
function reject(error) {
179+
if (!(error instanceof Error)) {
180+
error = new customErrorTypes.AsyncError(
181+
{
182+
info: {
183+
cause: error,
184+
handler: handler._name,
185+
method: req.method,
186+
path: req.path ? req.path() : undefined
187+
}
188+
},
189+
'Async middleware rejected without an error'
190+
);
191+
}
192+
return next(error);
193+
}
194+
160195
if (hasError && arity === 4) {
161196
// error-handling middleware
162197
handler(err, req, res, next);
163198
return;
164199
} else if (!hasError && arity < 4) {
165200
// request-handling middleware
166201
process.nextTick(function nextTick() {
167-
handler(req, res, next);
202+
const result = handler(req, res, next);
203+
if (result && typeof result.then === 'function') {
204+
result.then(resolve, reject);
205+
}
168206
});
169207
return;
170208
}
171209

172210
// continue
173-
next(error, req, res);
174-
return;
211+
next(err);
175212
}

‎lib/errorTypes.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,6 @@ var errors = require('restify-errors');
55
// This allows Restify to work with restify-errors v6+
66
module.exports = {
77
RequestCloseError: errors.makeConstructor('RequestCloseError'),
8-
RouteMissingError: errors.makeConstructor('RouteMissingError')
8+
RouteMissingError: errors.makeConstructor('RouteMissingError'),
9+
AsyncError: errors.makeConstructor('AsyncError')
910
};

‎lib/server.js

+44-4
Original file line numberDiff line numberDiff line change
@@ -367,6 +367,13 @@ Server.prototype.close = function close(callback) {
367367
* res.send({ hello: 'world' });
368368
* next();
369369
* });
370+
* @example
371+
* <caption>using with async/await</caption>
372+
* server.get('/', function (req, res) {
373+
* await somethingAsync();
374+
* res.send({ hello: 'world' });
375+
* next();
376+
* }
370377
*/
371378
Server.prototype.get = serverMethodFactory('GET');
372379

@@ -474,9 +481,16 @@ Server.prototype.opts = serverMethodFactory('OPTIONS');
474481
* return next();
475482
* });
476483
* @example
484+
* <caption>using with async/await</caption>
485+
* server.pre(async function(req, res) {
486+
* await somethingAsync();
487+
* somethingSync();
488+
* }
489+
* @example
477490
* <caption>For example, `pre()` can be used to deduplicate slashes in
478491
* URLs</caption>
479492
* server.pre(restify.pre.dedupeSlashes());
493+
* @see {@link http://restify.com/docs/plugins-api/#serverpre-plugins|Restify pre() plugins}
480494
*/
481495
Server.prototype.pre = function pre() {
482496
var self = this;
@@ -575,6 +589,22 @@ Server.prototype.first = function first() {
575589
* * and/or a
576590
* variable number of nested arrays of handler functions
577591
* @returns {Object} returns self
592+
* @example
593+
* server.use(function(req, res, next) {
594+
* // do something...
595+
* return next();
596+
* });
597+
* @example
598+
* <caption>using with async/await</caption>
599+
* server.use(async function(req, res) {
600+
* await somethingAsync();
601+
* somethingSync();
602+
* }
603+
* @example
604+
* <caption>For example, `use()` can be used to attach a request logger
605+
* </caption>
606+
* server.pre(restify.plugins.requestLogger());
607+
* @see {@link http://restify.com/docs/plugins-api/#serveruse-plugins|Restify use() plugins}
578608
*/
579609
Server.prototype.use = function use() {
580610
var self = this;
@@ -596,12 +626,22 @@ Server.prototype.use = function use() {
596626
* new middleware function that only fires if the specified parameter exists
597627
* in req.params
598628
*
599-
* Exposes an API:
600-
* server.param("user", function (req, res, next) {
601-
* // load the user's information here, always making sure to call next()
629+
* @example
630+
* server.param("user", function (req, res, next) {
631+
* // load the user's information here, always making sure to call next()
632+
* fetchUserInformation(req, function callback(user) {
633+
* req.user = user;
634+
* next();
602635
* });
636+
* });
637+
* @example
638+
* <caption>using with async/await</caption>
639+
* server.param("user", async function(req, res) {
640+
* req.user = await fetchUserInformation(req);
641+
* somethingSync();
642+
* }
603643
*
604-
* @see http://expressjs.com/guide.html#route-param%20pre-conditions
644+
* @see {@link http://expressjs.com/guide.html#route-param%20pre-conditions| Express route param pre-conditions}
605645
* @public
606646
* @memberof Server
607647
* @instance

‎package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@
9090
"report-latency": "./bin/report-latency"
9191
},
9292
"engines": {
93-
"node": ">=10.21.0"
93+
"node": ">=10.0.0"
9494
},
9595
"dependencies": {
9696
"assert-plus": "^1.0.0",

‎test/chain.test.js

+147
Original file line numberDiff line numberDiff line change
@@ -282,3 +282,150 @@ test('getHandlers returns with the array of handlers', function(t) {
282282
t.deepEqual(chain.getHandlers(), handlers);
283283
t.end();
284284
});
285+
286+
test('waits async handlers', function(t) {
287+
const chain = new Chain();
288+
let counter = 0;
289+
290+
chain.add(async function(req, res) {
291+
await helper.sleep(50);
292+
counter++;
293+
});
294+
chain.add(function(req, res, next) {
295+
counter++;
296+
next();
297+
});
298+
chain.run(
299+
{
300+
startHandlerTimer: function() {},
301+
endHandlerTimer: function() {},
302+
closed: function() {
303+
return false;
304+
}
305+
},
306+
{},
307+
function() {
308+
t.equal(counter, 2);
309+
t.done();
310+
}
311+
);
312+
});
313+
314+
test('abort with rejected promise', function(t) {
315+
const myError = new Error('Foo');
316+
const chain = new Chain();
317+
let counter = 0;
318+
319+
chain.add(async function(req, res) {
320+
counter++;
321+
await helper.sleep(10);
322+
return Promise.reject(myError);
323+
});
324+
chain.add(function(req, res, next) {
325+
counter++;
326+
next();
327+
});
328+
chain.run(
329+
{
330+
startHandlerTimer: function() {},
331+
endHandlerTimer: function() {},
332+
closed: function() {
333+
return false;
334+
}
335+
},
336+
{},
337+
function(err) {
338+
t.deepEqual(err, myError);
339+
t.equal(counter, 1);
340+
t.done();
341+
}
342+
);
343+
});
344+
345+
test('abort with rejected promise without error', function(t) {
346+
const chain = new Chain();
347+
let counter = 0;
348+
349+
chain.add(async function(req, res) {
350+
counter++;
351+
await helper.sleep(10);
352+
return Promise.reject();
353+
});
354+
chain.add(function(req, res, next) {
355+
counter++;
356+
next();
357+
});
358+
chain.run(
359+
{
360+
startHandlerTimer: function() {},
361+
endHandlerTimer: function() {},
362+
closed: function() {
363+
return false;
364+
},
365+
path: function() {
366+
return '/';
367+
}
368+
},
369+
{},
370+
function(err) {
371+
t.ok(typeof err === 'object');
372+
t.equal(err.name, 'AsyncError');
373+
t.equal(err.jse_info.cause, undefined);
374+
t.equal(counter, 1);
375+
t.done();
376+
}
377+
);
378+
});
379+
380+
test('abort with throw inside async function', function(t) {
381+
const myError = new Error('Foo');
382+
const chain = new Chain();
383+
let counter = 0;
384+
385+
chain.add(async function(req, res) {
386+
counter++;
387+
await helper.sleep(10);
388+
throw myError;
389+
});
390+
chain.add(function(req, res, next) {
391+
counter++;
392+
next();
393+
});
394+
chain.run(
395+
{
396+
startHandlerTimer: function() {},
397+
endHandlerTimer: function() {},
398+
closed: function() {
399+
return false;
400+
}
401+
},
402+
{},
403+
function(err) {
404+
t.deepEqual(err, myError);
405+
t.equal(counter, 1);
406+
t.done();
407+
}
408+
);
409+
});
410+
411+
test('fails to add non async function with arity 2', function(t) {
412+
var chain = new Chain();
413+
414+
t.throws(function() {
415+
chain.add(function(req, res) {
416+
res.send('ok');
417+
});
418+
}, Error);
419+
t.end();
420+
});
421+
422+
test('fails to add async function with arity 3', function(t) {
423+
var chain = new Chain();
424+
425+
t.throws(function() {
426+
chain.add(async function(req, res, next) {
427+
res.send('ok');
428+
});
429+
}, Error);
430+
t.end();
431+
});

‎test/lib/helper.js

+8
Original file line numberDiff line numberDiff line change
@@ -85,5 +85,13 @@ module.exports = {
8585

8686
get dtrace() {
8787
return true;
88+
},
89+
90+
sleep: function sleep(timeInMs) {
91+
return new Promise(function sleepPromise(resolve) {
92+
setTimeout(function timeout() {
93+
resolve();
94+
}, timeInMs);
95+
});
8896
}
8997
};

‎test/plugins/inflightRequestThrottle.test.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ describe('inlfightRequestThrottle', function() {
9090
var opts = { server: server, limit: 1, err: err };
9191
server.pre(inflightRequestThrottle(opts));
9292
var RES;
93-
server.get('/foo', function(req, res) {
93+
server.get('/foo', function(req, res, next) {
9494
if (RES) {
9595
res.send(999);
9696
} else {

‎test/server.test.js

+142
Original file line numberDiff line numberDiff line change
@@ -2926,3 +2926,145 @@ test('inflightRequest accounting stable with firstChain', function(t) {
29262926
CLIENT.get('/foobar', getDone);
29272927
CLIENT.get('/foobar', getDone);
29282928
});
2929+
2930+
test('async prerouting chain with error', function(t) {
2931+
SERVER.pre(async function(req, res) {
2932+
await helper.sleep(10);
2933+
throw new RestError({ statusCode: 400, restCode: 'BadRequest' }, 'bum');
2934+
});
2935+
2936+
SERVER.get('/hello/:name', function tester(req, res, next) {
2937+
res.send(req.params.name);
2938+
next();
2939+
});
2940+
2941+
CLIENT.get('/hello/mark', function(err, _, res) {
2942+
t.ok(err);
2943+
t.equal(res.statusCode, 400);
2944+
t.end();
2945+
});
2946+
});
2947+
2948+
test('async prerouting chain with empty rejection', function(t) {
2949+
SERVER.pre(async function(req, res) {
2950+
await helper.sleep(10);
2951+
return Promise.reject();
2952+
});
2953+
2954+
SERVER.get('/hello/:name', function tester(req, res, next) {
2955+
res.send(req.params.name);
2956+
next();
2957+
});
2958+
2959+
SERVER.on('Async', function(req, res, err, callback) {
2960+
t.equal(err.jse_info.cause, undefined);
2961+
t.equal(err.jse_info.method, 'GET');
2962+
t.equal(err.jse_info.path, '/hello/mark');
2963+
callback();
2964+
});
2965+
2966+
CLIENT.get('/hello/mark', function(err, _, res) {
2967+
t.ok(err);
2968+
t.equal(res.statusCode, 500);
2969+
t.end();
2970+
});
2971+
});
2972+
2973+
test('async use chain with error', function(t) {
2974+
SERVER.use(async function(req, res) {
2975+
await helper.sleep(10);
2976+
throw new RestError({ statusCode: 400, restCode: 'BadRequest' }, 'bum');
2977+
});
2978+
2979+
SERVER.get('/hello/:name', function tester(req, res, next) {
2980+
res.send(req.params.name);
2981+
next();
2982+
});
2983+
2984+
CLIENT.get('/hello/mark', function(err, _, res) {
2985+
t.ok(err);
2986+
t.equal(res.statusCode, 400);
2987+
t.end();
2988+
});
2989+
});
2990+
2991+
test('async handler with error', function(t) {
2992+
SERVER.get('/hello/:name', async function tester(req, res) {
2993+
await helper.sleep(10);
2994+
throw new RestError({ statusCode: 400, restCode: 'BadRequest' }, 'bum');
2995+
});
2996+
2997+
CLIENT.get('/hello/mark', function(err, _, res) {
2998+
t.ok(err);
2999+
t.equal(res.statusCode, 400);
3000+
t.end();
3001+
});
3002+
});
3003+
3004+
test('async handler with error after send succeeds', function(t) {
3005+
SERVER.get('/hello/:name', async function tester(req, res) {
3006+
await helper.sleep(10);
3007+
res.send(req.params.name);
3008+
throw new RestError({ statusCode: 400, restCode: 'BadRequest' }, 'bum');
3009+
});
3010+
3011+
CLIENT.get('/hello/mark', function(err, _, res) {
3012+
t.ok(!err);
3013+
t.equal(res.statusCode, 200);
3014+
t.end();
3015+
});
3016+
});
3017+
3018+
test('async handler with error after send succeeds', function(t) {
3019+
SERVER.get('/hello/:name', async function tester(req, res) {
3020+
res.send(req.params.name);
3021+
await helper.sleep(20);
3022+
throw new RestError({ statusCode: 400, restCode: 'BadRequest' }, 'bum');
3023+
});
3024+
3025+
SERVER.on('after', function(req, res, route, error) {
3026+
t.ok(error);
3027+
t.end();
3028+
});
3029+
3030+
CLIENT.get('/hello/mark', function(err, _, res) {
3031+
t.ok(!err);
3032+
t.equal(res.statusCode, 200);
3033+
});
3034+
});
3035+
3036+
test('async handler without next', function(t) {
3037+
SERVER.get('/hello/:name', async function tester(req, res) {
3038+
await helper.sleep(10);
3039+
res.send(req.params.name);
3040+
});
3041+
3042+
SERVER.on('after', function(req, res, route, error) {
3043+
t.ok(!error);
3044+
t.equal(res.statusCode, 200);
3045+
t.end();
3046+
});
3047+
3048+
CLIENT.get('/hello/mark', function(err, _, res) {
3049+
t.ok(!err);
3050+
t.equal(res.statusCode, 200);
3051+
});
3052+
});
3053+
3054+
test('async handler resolved with string should re-route', function(t) {
3055+
SERVER.get('/hello/:name', async function tester(req, res) {
3056+
await helper.sleep(10);
3057+
return 'getredirected';
3058+
});
3059+
3060+
SERVER.get('/redirected', async function tester(req, res) {
3061+
res.send(req.params.name);
3062+
});
3063+
3064+
CLIENT.get('/hello/mark', function(err, _, res) {
3065+
t.ok(!err);
3066+
t.equal(res.statusCode, 200);
3067+
t.equal(res.body, '"mark"');
3068+
t.end();
3069+
});
3070+
});

0 commit comments

Comments
 (0)
Please sign in to comment.