Skip to content
This repository was archived by the owner on Nov 9, 2023. It is now read-only.

Commit 378a820

Browse files
committed
Remove createAsyncMiddleware
* Handle errors fro masync middleware function rejections * Update readme * Update JsonRpcMiddleware type * Prevent promise resolution function from being called twice
1 parent cb6714c commit 378a820

File tree

7 files changed

+128
-365
lines changed

7 files changed

+128
-365
lines changed

README.md

Lines changed: 39 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -39,11 +39,21 @@ They can let processing continue down the stack with `next()`, or complete the r
3939
engine.push(function (req, res, next, end) {
4040
if (req.skipCache) return next();
4141
res.result = getResultFromCache(req);
42-
end();
42+
return end();
4343
});
4444
```
4545

46-
By passing a _return handler_ to the `next` function, you can get a peek at the result before it returns.
46+
Middleware functions can be `async`:
47+
48+
```js
49+
engine.push(async function (req, res, next, end) {
50+
if (req.method !== targetMethod) return next();
51+
res.result = await processTargetMethodRequest(req);
52+
return end();
53+
});
54+
```
55+
56+
By passing a _return handler_ to the `next` function, you can get a peek at the response before it is returned to the requester.
4757

4858
```js
4959
engine.push(function (req, res, next, end) {
@@ -73,69 +83,45 @@ const subengine = new JsonRpcEngine();
7383
engine.push(subengine.asMiddleware());
7484
```
7585

76-
### `async` Middleware
86+
### Error Handling
7787

78-
If you require your middleware function to be `async`, use `createAsyncMiddleware`:
88+
Errors should be handled by throwing inside middleware functions.
7989

80-
```js
81-
const { createAsyncMiddleware } = require('@metamask/json-rpc-engine');
82-
83-
let engine = new RpcEngine();
84-
engine.push(
85-
createAsyncMiddleware(async (req, res, next) => {
86-
res.result = 42;
87-
next();
88-
}),
89-
);
90-
```
91-
92-
`async` middleware do not take an `end` callback.
93-
Instead, the request ends if the middleware returns without calling `next()`:
90+
For backwards compatibility, you can also pass an error to the `end` callback,
91+
or set the error on the response object, and then call `end` or `next`.
92+
However, errors must **not** be passed to the `next` callback.
9493

95-
```js
96-
engine.push(
97-
createAsyncMiddleware(async (req, res, next) => {
98-
res.result = 42;
99-
/* The request will end when this returns */
100-
}),
101-
);
102-
```
94+
Errors always take precedent over results.
95+
If an error is detected, the response's `result` property will be deleted.
10396

104-
The `next` callback of `async` middleware also don't take return handlers.
105-
Instead, you can `await next()`.
106-
When the execution of the middleware resumes, you can work with the response again.
97+
All of the following examples are equivalent.
98+
It does not matter of the middleware function is synchronous or asynchronous.
10799

108100
```js
109-
engine.push(
110-
createAsyncMiddleware(async (req, res, next) => {
111-
res.result = 42;
112-
await next();
113-
/* Your return handler logic goes here */
114-
addToMetrics(res);
115-
}),
116-
);
117-
```
101+
// Throwing is preferred.
102+
engine.push(function (req, res, next, end) {
103+
throw new Error();
104+
});
118105

119-
You can freely mix callback-based and `async` middleware:
106+
// For backwards compatibility, you can also do this:
107+
engine.push(function (req, res, next, end) {
108+
end(new Error());
109+
});
120110

121-
```js
122111
engine.push(function (req, res, next, end) {
123-
if (!isCached(req)) {
124-
return next((cb) => {
125-
insertIntoCache(res, cb);
126-
});
127-
}
128-
res.result = getResultFromCache(req);
112+
res.error = new Error();
129113
end();
130114
});
131115

132-
engine.push(
133-
createAsyncMiddleware(async (req, res, next) => {
134-
res.result = 42;
135-
await next();
136-
addToMetrics(res);
137-
}),
138-
);
116+
engine.push(function (req, res, next, end) {
117+
res.error = new Error();
118+
next();
119+
});
120+
121+
// INCORRECT. Do not do this:
122+
engine.push(function (req, res, next, end) {
123+
next(new Error());
124+
});
139125
```
140126

141127
### Teardown
@@ -164,41 +150,3 @@ engine.destroy();
164150
// will throw an error.
165151
engine.handle(req);
166152
```
167-
168-
### Gotchas
169-
170-
Handle errors via `end(err)`, _NOT_ `next(err)`.
171-
172-
```js
173-
/* INCORRECT */
174-
engine.push(function (req, res, next, end) {
175-
next(new Error());
176-
});
177-
178-
/* CORRECT */
179-
engine.push(function (req, res, next, end) {
180-
end(new Error());
181-
});
182-
```
183-
184-
However, `next()` will detect errors on the response object, and cause
185-
`end(res.error)` to be called.
186-
187-
```js
188-
engine.push(function (req, res, next, end) {
189-
res.error = new Error();
190-
next(); /* This will cause end(res.error) to be called. */
191-
});
192-
```
193-
194-
## Running tests
195-
196-
Build the project if not already built:
197-
198-
```bash
199-
yarn build
200-
```
201-
202-
```bash
203-
yarn test
204-
```

src/JsonRpcEngine.test.ts

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -499,8 +499,10 @@ describe('JsonRpcEngine', () => {
499499
});
500500
});
501501

502-
engine.push(function (_request, _response, next, _end) {
502+
// Async middleware function
503+
engine.push(async function (_request, _response, next, _end) {
503504
events.push('2-next');
505+
await delay();
504506
next(function (callback) {
505507
events.push('2-return');
506508
callback();
@@ -555,6 +557,33 @@ describe('JsonRpcEngine', () => {
555557
});
556558
});
557559

560+
it('calls back next handler even if async middleware rejects', async () => {
561+
const engine = new JsonRpcEngine();
562+
563+
let sawNextReturnHandlerCalled = false;
564+
565+
engine.push(function (_req, _res, next, _end) {
566+
next(function (callback) {
567+
sawNextReturnHandlerCalled = true;
568+
callback();
569+
});
570+
});
571+
572+
engine.push(async function (_req, _res, _next, _end) {
573+
throw new Error('boom');
574+
});
575+
576+
const payload = { id: 1, jsonrpc, method: 'hello' };
577+
578+
await new Promise<void>((resolve) => {
579+
engine.handle(payload, (error, _response) => {
580+
expect(error).toBeDefined();
581+
expect(sawNextReturnHandlerCalled).toBe(true);
582+
resolve();
583+
});
584+
});
585+
});
586+
558587
it('handles error in next handler', async () => {
559588
const engine = new JsonRpcEngine();
560589

@@ -673,3 +702,14 @@ describe('JsonRpcEngine', () => {
673702
});
674703
});
675704
});
705+
706+
/**
707+
* Delay for a number of milliseconds.
708+
*
709+
* @param ms - The number of milliseconds to delay.
710+
*/
711+
async function delay(ms = 1) {
712+
return new Promise<void>((resolve) => {
713+
setTimeout(() => resolve(), ms);
714+
});
715+
}

src/JsonRpcEngine.ts

Lines changed: 47 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ export type JsonRpcMiddleware<
3535
res: PendingJsonRpcResponse<Result>,
3636
next: JsonRpcEngineNextCallback,
3737
end: JsonRpcEngineEndCallback,
38-
): void;
38+
): void | Promise<void>;
3939
destroy?: () => void | Promise<void>;
4040
};
4141

@@ -517,49 +517,55 @@ export class JsonRpcEngine extends SafeEventEmitter {
517517
middleware: JsonRpcMiddleware<JsonRpcParams, Json>,
518518
returnHandlers: JsonRpcEngineReturnHandler[],
519519
): Promise<[unknown, boolean]> {
520-
return new Promise((resolve) => {
521-
const end: JsonRpcEngineEndCallback = (error?: unknown) => {
522-
const parsedError = error || response.error;
523-
if (parsedError) {
524-
response.error = serializeError(parsedError);
525-
}
526-
// True indicates that the request should end
527-
resolve([parsedError, true]);
528-
};
529-
530-
const next: JsonRpcEngineNextCallback = (
531-
returnHandler?: JsonRpcEngineReturnHandler,
532-
) => {
533-
if (response.error) {
534-
end(response.error);
535-
} else {
536-
if (returnHandler) {
537-
if (typeof returnHandler !== 'function') {
538-
end(
539-
new JsonRpcError(
540-
errorCodes.rpc.internal,
541-
`JsonRpcEngine: "next" return handlers must be functions. ` +
542-
`Received "${typeof returnHandler}" for request:\n${jsonify(
543-
request,
544-
)}`,
545-
{ request: request as Json },
546-
),
547-
);
548-
}
549-
returnHandlers.push(returnHandler);
550-
}
520+
let resolve: (value: [unknown, boolean]) => void;
521+
const middlewareCallbackPromise = new Promise<[unknown, boolean]>(
522+
(_resolve) => {
523+
resolve = _resolve;
524+
},
525+
);
551526

552-
// False indicates that the request should not end
553-
resolve([null, false]);
554-
}
555-
};
527+
const end: JsonRpcEngineEndCallback = (error?: unknown) => {
528+
const parsedError = error || response.error;
529+
if (parsedError) {
530+
response.error = serializeError(parsedError);
531+
}
532+
// True indicates that the request should end
533+
resolve([parsedError, true]);
534+
};
556535

557-
try {
558-
middleware(request, response, next, end);
559-
} catch (error: any) {
560-
end(error);
536+
const next: JsonRpcEngineNextCallback = (
537+
returnHandler?: JsonRpcEngineReturnHandler,
538+
) => {
539+
if (response.error) {
540+
return end(response.error);
561541
}
562-
});
542+
543+
if (returnHandler) {
544+
if (typeof returnHandler !== 'function') {
545+
return end(
546+
new JsonRpcError(
547+
errorCodes.rpc.internal,
548+
`JsonRpcEngine: "next" return handlers must be functions. ` +
549+
`Received "${typeof returnHandler}" for request:\n${jsonify(
550+
request,
551+
)}`,
552+
{ request: request as Json },
553+
),
554+
);
555+
}
556+
returnHandlers.push(returnHandler);
557+
}
558+
559+
// False indicates that the request should not end
560+
return resolve([null, false]);
561+
};
562+
563+
try {
564+
await middleware(request, response, next, end);
565+
} catch (error: any) {
566+
end(error);
567+
}
568+
return middlewareCallbackPromise;
563569
}
564570

565571
/**

0 commit comments

Comments
 (0)