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

Async middleware #148

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
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
28 changes: 26 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ The smol method routing and middleware for [Next.js](https://nextjs.org/) (also
- Compatible with Express.js middleware and router => Drop-in replacement for Express.js.
- Lightweight (~ 3KB) => Suitable for serverless environment.
- 5x faster than Express.js with no overhead
- Works with async handlers (with error catching)
- Works with async handlers and middleware (with error catching)
- TypeScript support

## Installation
Expand Down Expand Up @@ -57,6 +57,30 @@ For usage in pages with [`getServerSideProps`](https://nextjs.org/docs/basic-fea

See an example in [nextjs-mongodb-app](https://github.com/hoangvvo/nextjs-mongodb-app) (CRUD, Authentication with Passport, and more)

### Async middleware

Middleware may `await next()` to perform actions after the request handler has been resolved. Like other tools which support async middleware, however, **all** middleware in the handler **must** call `next()` with `await` or `return` to execute properly.

```javascript
import nc from "next-connect";

const handler = nc()
.use(async (req, res, next) => {
await next(); // Waits for .get() handler.

if (!res.writableEnded) {
res.end(res.body); // respond with "Hello world"!
}
})
.get(async (req, res) => {
res.body = "Hello world";
})

export default handler;
```



### TypeScript

By default, the base interfaces of `req` and `res` are `IncomingMessage` and `ServerResponse`. When using in API Routes, you would set them to `NextApiRequest` and `NextApiResponse` by providing the generics to the factory function like so:
Expand Down Expand Up @@ -244,7 +268,7 @@ export async function getServerSideProps({ req, res }) {
<details id="catch-all">
<summary>Match multiple routes</summary>

If you created the file `/api/<specific route>.js` folder, the handler will only run on that specific route.
If you created the file `/api/<specific route>.js` folder, the handler will only run on that specific route.

If you need to create all handlers for all routes in one file (similar to `Express.js`). You can use [Optional catch all API routes](https://nextjs.org/docs/api-routes/dynamic-api-routes#optional-catch-all-api-routes).

Expand Down
2 changes: 1 addition & 1 deletion src/index.d.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
declare module "next-connect" {
import { IncomingMessage, ServerResponse } from "http";

type NextHandler = (err?: any) => void;
type NextHandler = (err?: any) => void | Promise<void>;
type Middleware<Req, Res> = NextConnect<Req, Res> | RequestHandler<Req, Res>;

type RequestHandler<Req, Res> = (
Expand Down
12 changes: 5 additions & 7 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -67,13 +67,11 @@ export default function factory({
let i = 0;
const len = handlers.length;
const loop = async (next) => handlers[i++](req, res, next);
const next = (err) => {
i < len
? err
? onError(err, req, res, next)
: loop(next).catch(next)
: done && done(err);
};
const next = (err) => i < len
? err
? onError(err, req, res, next)
Copy link
Owner

@hoangvvo hoangvvo Jul 15, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What would be its behavior when an error is thrown. In this case the execution inside the handler still goes on after await next()?

Copy link
Author

@UncleClapton UncleClapton Jul 17, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah that's something I totally missed in my testing. my bad! Indeed errors aren't halting execution after await next().

It seems it's behaving this way because the error is handled inside of next() and never returned back. I found a solution that ensures that errors are propagated, but I can't find one that gives onError() an option to call next() and allow middleware to continue execution after doing so. Any ideas?

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We may need to decide on a behavior.

One thing we can do is to let next() throws and the error should propagate all the way up and only attach the error handler to the first handler:

handler1: await() -- onError()
     middleware1: await next() -- ⬆️
          middleware2: await next() - ⬆️
              middleware3: throw new Error()

But that is a breaking change since it will break if user have not been doing await next(), which is probably the case now.

Copy link
Author

@UncleClapton UncleClapton Jul 18, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My pending changes resolves error handling without having to unravel the stack. In the event that the error handler wishes to ignore the error and call next(), however, it leads to the following unexpected behaviour.

// ❌ = does not execute
// Middleware listed after .get() represents code to be executed after await next().

// Example: GET /
middleware1: await next()
    middleware2: await next()
        middleware3: throw new Error() -> onError() -> next()
            .get(handler): 
        middleware3: ❌ -- not expected to run, it gave up control after throwing
    middleware2: ❌ -- expected to run
middleare1: ❌ -- expected to run

This is where I'm running into problems with finding a decent solution. Do you think it's worth preserving this trait of error handling?

: loop(next).catch(next)
: done && done(err);
next();
};
return nc;
Expand Down
19 changes: 19 additions & 0 deletions test/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,25 @@ describe("nc()", () => {
return await request(app).get("/").expect("one", "1").expect("done");
});

it("supports async middleware", async () => {
const handler = nc()
.use(async (req, res, next) => {
await next();
res.end(res.body)
})
.get(
(req, res) =>
new Promise((resolve) => {
setTimeout(() => {
res.body = "done"
resolve();
}, 1);
})
);
const app = createServer(handler);
return await request(app).get("/").expect("done");
});

it("is a function with two argument", () => {
assert(typeof nc() === "function" && nc().length === 2);
});
Expand Down