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

Api route req on close event not triggered when request is cancelled #52809

Closed
1 task done
jbojcic1 opened this issue Jul 17, 2023 · 15 comments · Fixed by #52281
Closed
1 task done

Api route req on close event not triggered when request is cancelled #52809

jbojcic1 opened this issue Jul 17, 2023 · 15 comments · Fixed by #52281
Labels
bug Issue was opened via the bug report template. locked Runtime Related to Node.js or Edge Runtime with Next.js.

Comments

@jbojcic1
Copy link

jbojcic1 commented Jul 17, 2023

Verify canary release

  • I verified that the issue exists in the latest Next.js canary release

Provide environment information

Operating System:
      Platform: linux
      Arch: x64
      Version: #22 SMP Tue Jan 10 18:39:00 UTC 2023
    Binaries:
      Node: 18.14.2
      npm: 9.5.0
      Yarn: 1.22.19
      pnpm: N/A
    Relevant Packages:
      next: 13.4.10-canary.8
      eslint-config-next: 13.4.8
      react: 18.2.0
      react-dom: 18.2.0
      typescript: 5.0.4
    Next.js Config:
      output: N/A

Which area(s) of Next.js are affected? (leave empty if unsure)

Middleware / Edge (API routes, runtime)

Link to the code that reproduces this issue or a replay of the bug

https://codesandbox.io/p/sandbox/blissful-driscoll-y78mrd

To Reproduce

  1. open the link from above
  2. click on "Trigger API call" button
  3. click on "Cancel API call" button and note in browser network tab that the request which was previously pending is now cancelled
  4. note how req.on("close" event handler didn't run and API route work is still being done

Describe the Bug

Request close event is not firing in the API route when the caller cancels the request

Expected Behavior

Request close event should be fired so that any necessary cleanup can be done

Which browser are you using? (if relevant)

No response

How are you deploying your application? (if relevant)

No response

@jbojcic1 jbojcic1 added the bug Issue was opened via the bug report template. label Jul 17, 2023
@github-actions github-actions bot added the Runtime Related to Node.js or Edge Runtime with Next.js. label Jul 17, 2023
@masterkain
Copy link

perhaps related #52281

@jbojcic1
Copy link
Author

I also just noticed that req.destroyed is set to true immediately in the API route handler (see the log I just added to the same demo). Isn't that supposed to be true only if the request was forcefully closed (e.g. cancelled by the client)?

@jridgewell
Copy link
Contributor

I also just noticed that req.destroyed is set to true immediately in the API route handler (see the log I just added to the same demo). Isn't that supposed to be true only if the request was forcefully closed (e.g. cancelled by the client)?

request is destroyed when the stream's data is fully read into the program, which happens automatically because Next provides you with a req.body. If the stream data isn't read, then it's also destroyed when the client disconnects.

response is only destroyed when the client disconnects, so it's the one you should use for the disconnect signal.

So, this is working as intended. req.on('close', cb) will never invoke the callback, because the request is already closed. Try using res.on('close') instead, and possibly listening to 'end' event as well if you want to distinguish the client closing the stream from you ending the response with res.end().

@jbojcic1
Copy link
Author

jbojcic1 commented Jul 18, 2023

request is destroyed when the stream's data is fully read into the program, which happens automatically because Next provides you with a req.body. If the stream data isn't read, then it's also destroyed when the client disconnects.

@jridgewell I see. Thx for the explanation. However, not even res.on('close') is emitting. I've updated my example to demonstrate. You can see in the network tab that hitting cancel API call button cancels the request but server logs don't show that response close event is emitted

@jbojcic1
Copy link
Author

jbojcic1 commented Jul 18, 2023

Also, if I disable bodyParser, the request destroyed and closed flags are false but still when the request is then canceled by the client, the request closed event is not emitted + destroyed and closed flags stay false. An example of this can be seen here

@jridgewell jridgewell reopened this Jul 18, 2023
@jridgewell
Copy link
Contributor

Strange, I'm not sure why the close event isn't firing.

@jridgewell
Copy link
Contributor

Ok, now I see what's happening. We're using an async iterator with a break condition:

for await (const chunk of invokeRes) {
if (originalResponse.closed) break

The loop's body only runs after data has been received (you write something in your API response), where we can check if the client has disconnected and break. The break is the thing that forwards the client disconnect to the API response. But you're not writing any data, so we're stuck waiting for something to be written, and we can never reach the break.

I'll update so that we listen for the close signal outside of the loop as well.

@jridgewell
Copy link
Contributor

Confirmed, by writing to the response it will eventually get the close signal (and a few modifications to the client code, because you're immediately setRunning(false) in the makeApiCall function). I'll get this fixed in #52281

@jbojcic1
Copy link
Author

Confirmed

not sure if I am doing something wrong, but in the linked sandbox I still don't see res closed printed after I cancel the request

@tangye1234
Copy link
Contributor

tangye1234 commented Jul 19, 2023

When invoking the response piping in the Node server, special attention should be paid to the issue of backpressure.

I highly recommend using the node Readable.from(iterable) method to obtain the readable stream, and then using the node stream.pipe method to pipe the readable stream to the outgoing stream.

If this occurs on the edge, simply employ the approach utilized in the edge-runtime. It also takes into account the issue of back pressure by actively monitoring the drain event from the node res.

This will also provide support for user abortion since the pipe function listens for the underlying response close event and will handle any error issues appropriately.

@jridgewell

ijjk pushed a commit that referenced this issue Jul 26, 2023
### What?

This reimplements our stream cancellation code for a few more cases:
1. Adds support in all stream-returning APIs
2. Fixes cancellation detection in node 16
3. Implements out-of-band detection, so can cancel in the middle of a
read

It also (finally) adds tests for all the cases I'm aware of.

### Why?

To allow disconnecting from an AI service when a client disconnects. $$$

### How?

1. Reuses a single pipe function in all paths to push data from the
dev's `ReadableStream` into our `ServerResponse`
2. Uses `ServerResponse` to detect disconnect, instead of the
`IncomingMessage` (request)
    - The `close` event fire once all incoming body data is read
- The request `abort` event will not fire after the incoming body data
has been fully read
3. Using `on('close')` on the writable destination allows us to detect
close
- Checking for `res.destroyed` in the body of the loop meant we had to
wait for the `await stream.read()` to complete before we could possibly
cancel the stream

- - -

#52157 (and #51594) had an issue with Node 16, because I was using
`res.closed` to detect when the server response was closed by the client
disconnecting. But, `closed` wasn't
[added](nodejs/node#45672) until
[v18.13.0](https://nodejs.org/en/blog/release/v18.13.0#:~:text=%5Bcbd710bbf4%5D%20%2D%20http%3A%20make%20OutgoingMessage%20more%20streamlike%20(Robert%20Nagy)%20%2345672).
This fixes it by using `res.destroyed`.

Reverts #52277
Relands #52157
Fixes #52809

---------
Strift pushed a commit to Strift/next.js that referenced this issue Jul 27, 2023
### What?

This reimplements our stream cancellation code for a few more cases:
1. Adds support in all stream-returning APIs
2. Fixes cancellation detection in node 16
3. Implements out-of-band detection, so can cancel in the middle of a
read

It also (finally) adds tests for all the cases I'm aware of.

### Why?

To allow disconnecting from an AI service when a client disconnects. $$$

### How?

1. Reuses a single pipe function in all paths to push data from the
dev's `ReadableStream` into our `ServerResponse`
2. Uses `ServerResponse` to detect disconnect, instead of the
`IncomingMessage` (request)
    - The `close` event fire once all incoming body data is read
- The request `abort` event will not fire after the incoming body data
has been fully read
3. Using `on('close')` on the writable destination allows us to detect
close
- Checking for `res.destroyed` in the body of the loop meant we had to
wait for the `await stream.read()` to complete before we could possibly
cancel the stream

- - -

vercel#52157 (and vercel#51594) had an issue with Node 16, because I was using
`res.closed` to detect when the server response was closed by the client
disconnecting. But, `closed` wasn't
[added](nodejs/node#45672) until
[v18.13.0](https://nodejs.org/en/blog/release/v18.13.0#:~:text=%5Bcbd710bbf4%5D%20%2D%20http%3A%20make%20OutgoingMessage%20more%20streamlike%20(Robert%20Nagy)%20%2345672).
This fixes it by using `res.destroyed`.

Reverts vercel#52277
Relands vercel#52157
Fixes vercel#52809

---------
@jbojcic1
Copy link
Author

jbojcic1 commented Aug 7, 2023

@ijjk @jridgewell I don't think the issue is fixed. I've updated my example to use 13.4.13 but I still see the same issue. Here is also the @jridgewell's updated version which also shows that it's not fixed

@bkrausz
Copy link

bkrausz commented Aug 17, 2023

This now works in dev, but we're still seeing it not working in standalone production builds. I'm digging into the cause but wanted to give a heads up.

@jridgewell
Copy link
Contributor

I've updated my example to use 13.4.13 but I still see the same issue. Here is also the @jridgewell's updated version which also shows that it's not fixed

Given that this works in dev, I think it might be CodeSandbox's servers that are breaking it. Even an app route handler doesn't work.

This now works in dev, but we're still seeing it not working in standalone production builds

Are you deploying to Vercel? It was pointed out to me yesterday that we don't support streaming Pages API when running the nodejs runtime. I'm trying to bring this up to the team, and I'll see if we can fix that.

@bkrausz
Copy link

bkrausz commented Aug 18, 2023

Are you deploying to Vercel? It was pointed out to me yesterday that we don't support streaming Pages API when running the nodejs runtime. I'm trying to bring this up to the team, and I'll see if we can fix that.

We're using a standalone NodeJS build that we deploy to GCP cloud run, and we're on app, not pages. If you think that should work I can try to make a minimal repro.

@github-actions
Copy link
Contributor

github-actions bot commented Sep 1, 2023

This closed issue has been automatically locked because it had no new activity for 2 weeks. If you are running into a similar issue, please create a new issue with the steps to reproduce. Thank you.

@github-actions github-actions bot added the locked label Sep 1, 2023
@github-actions github-actions bot locked as resolved and limited conversation to collaborators Sep 1, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
bug Issue was opened via the bug report template. locked Runtime Related to Node.js or Edge Runtime with Next.js.
Projects
None yet
Development

Successfully merging a pull request may close this issue.

5 participants