hoangvvo / next-connect

The TypeScript-ready, minimal router and middleware layer for Next.js, Micro, Vercel, or Node.js http/http2
https://www.npmjs.com/package/next-connect
MIT License
1.62k stars 65 forks source link

Complete handler chain if res is sent #166

Closed jakeorr closed 2 years ago

jakeorr commented 2 years ago

This change allows handler(res, req) to resolve for middlewares that don't call next, but end the response.

Example:

// index.page.js
const handler = nc()
  .get(async (req, res) => {
    res.status(200).send('hello!')
  });
export default handler;
import { createMocks } from 'node-mocks-http';
import handler from './index.page';

const { req, res } = createMocks({ method: 'GET', url: '/' });
await handler(req, res);
// With this change we can now get here.
changeset-bot[bot] commented 2 years ago

⚠️ No Changeset found

Latest commit: 0358b74facfa5d7d4f7cfcfeb0ea07690bd1372b

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

codecov[bot] commented 2 years ago

Codecov Report

Merging #166 (0358b74) into master (14aa547) will not change coverage. The diff coverage is 100.00%.

Impacted file tree graph

@@            Coverage Diff            @@
##            master      #166   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files            1         1           
  Lines           81        88    +7     
=========================================
+ Hits            81        88    +7     
Impacted Files Coverage Δ
src/index.js 100.00% <100.00%> (ø)

Continue to review full report at Codecov.

Legend - Click here to learn more Δ = absolute <relative> (impact), ø = not affected, ? = missing data Powered by Codecov. Last update 14aa547...0358b74. Read the comment docs.

jakeorr commented 2 years ago

Thanks for the handy library! Let me know if you would like anything further on the PR. 😄

jakeorr commented 2 years ago

Hi @hoangvvo. Happy New Year! Just wanted to check if you needed anything else on this change or had any further thoughts on it. Thanks!

hoangvvo commented 2 years ago

Happy new year! Thanks for the PR and sorry for the late review. However, I don't think this breaking change is possible at this point. Since checking if response is sent is not really reliable (for cases like redirection, multiple res sent, multipart response, etc. and edge cases like race-condition etc.)

jakeorr commented 2 years ago

Happy new year! Thanks for the PR and sorry for the late review. However, I don't think this breaking change is possible at this point. Since checking if response is sent is not really reliable (for cases like redirection, multiple res sent, multipart response, etc. and edge cases like race-condition etc.)

No problem, I've been working off a fork, so it's not a big deal if this doesn't get merged.

To your point about checking if the response is sent, I hadn't thought about those details you mentioned. Is there a concern that isResSent will return true when the response really hasn't been sent? If you're only worried about the other way around, where this change wouldn't detect the response as sent, then it's no different than it was previously. It wouldn't call done in the new spot and the existing code would continue working as it has been.

hoangvvo commented 2 years ago

Sorry about the wait, I just have some time to revisit this PR today. I think I finally agree that this issue should be resolved. However, one concern is that sometimes I want to do some post processing after the response is sent. In that case, it would not really make sense to end the promise there. What do you think about that case. Example:

const handler = nc()
  .get(async (req, res, next) => {
    res.status(200).send('hello!');
    next();
  })
  .all(async (req, res) => {
    // it would have been resolved before this step
    await logPostRequestToDatabase(req);
  };
export default handler;
jakeorr commented 2 years ago

Sorry about the wait, I just have some time to revisit this PR today. I think I finally agree that this issue should be resolved. However, one concern is that sometimes I want to do some post processing after the response is sent. In that case, it would not really make sense to end the promise there. What do you think about that case. Example:

const handler = nc()
  .get(async (req, res, next) => {
    res.status(200).send('hello!');
    next();
  })
  .all(async (req, res) => {
    // it would have been resolved before this step
    await logPostRequestToDatabase(req);
  };
export default handler;

No worries! Thanks for spending some time on it. Good point about running functions after the response is sent. My initial thought it to call next instead of done if the response is sent, but testing that, I'm seeing next called extra times which causes done to be called multiple times. So, do we do something like this?

isResSent(res) && !next.wasCalled && next();

I'll need to spend some time setting up something to detect if next was called. Any initial thoughts on that idea?

hoangvvo commented 2 years ago

I would say that the current design choice for this library is not ideal. I don't really find a way to really work this out reliably.

Also consider this case below:

const handler = nc()
  .get(async (req, res, next) => {
    somePromise().then(() => res.end()); // <- notice this is not returned and not await for
    // or setTimeout(() => res.end(), 0);
  })

After the handler is resolved. isResSent() is obviously false because res.end() has not been called (it calls in the next tick, or after the promise is resolved, which the function does not wait for). With this the check of isResSent() then resolve promise will never be triggered.

hoangvvo commented 2 years ago

At this point, it just makes the most sense to not returning a promise in a handler, since we never know when that promise is supposed to be resolved.

hoangvvo commented 2 years ago

The other way to solve this is to follow this pattern https://github.com/hoangvvo/next-connect/pull/148 (similar to Koa or Go-Gin). With this we can for sure know the promise will be resolve (since next() is the call to next function, keep awaiting them and when it resolves, it is the time to do so in the handler). However, this breaks current support with express middleware.

hoangvvo commented 2 years ago

Sorry about the wait, I just have some time to revisit this PR today. I think I finally agree that this issue should be resolved. However, one concern is that sometimes I want to do some post processing after the response is sent. In that case, it would not really make sense to end the promise there. What do you think about that case. Example:

const handler = nc()
  .get(async (req, res, next) => {
    res.status(200).send('hello!');
    next();
  })
  .all(async (req, res) => {
    // it would have been resolved before this step
    await logPostRequestToDatabase(req);
  };
export default handler;

No worries! Thanks for spending some time on it. Good point about running functions after the response is sent. My initial thought it to call next instead of done if the response is sent, but testing that, I'm seeing next called extra times which causes done to be called multiple times. So, do we do something like this?

isResSent(res) && !next.wasCalled && next();

I'll need to spend some time setting up something to detect if next was called. Any initial thoughts on that idea?

Funny enough this is not the first time the issue brought up. https://github.com/senchalabs/connect/issues/1042 (so the same case in Express.js) was here long before and the fact that it is not yet solved indicating this is a problematic issue. It is probably due to the bad design choice of Express.js (therefore, of this library too) that makes this nearly impossible to solve.

hoangvvo commented 2 years ago

I gave an attempt to resolve this in https://github.com/hoangvvo/next-connect/pull/178. Could you check it out whenever you have the time?

My solution is to listen to the event res.on("close") https://nodejs.org/api/http.html#event-close_1

jakeorr commented 2 years ago

Thanks for your thoughts on the matter and good points about the design limitations. I had a brief look at your PR, and I'll have time to take a closer look at it tomorrow.

hoangvvo commented 2 years ago

Closing in favor of #178. Thanks for the PR.