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

Cannot call write after a stream was destroyed #69

Closed
michelalbers opened this issue Jul 18, 2023 · 17 comments · Fixed by #75
Closed

Cannot call write after a stream was destroyed #69

michelalbers opened this issue Jul 18, 2023 · 17 comments · Fixed by #75
Assignees
Labels
bug Something isn't working released Has been released and published

Comments

@michelalbers
Copy link

Screenshot
image

Expected Behaviour
Do not write to the stream after it is closed / destroyed

Actual Behaviour
Writes to stream after it was destroyed which results in an error.

Debug Information
This bug resulted when using the use/express handler created with a createHandler call. According to the code there is no check in place wether the stream was closed in the meantime.

Further Information
Maybe introduce a variable let cancelled = false; and set it to true once the stream was closed. Check for cancelled before trying to do a .write().

@enisdenjo
Copy link
Owner

enisdenjo commented Jul 19, 2023

Hmm, I thought that the close listener above the loop should suffice, but apparently not.

res.once('close', body.return);
for await (const value of body) {
await new Promise<void>((resolve, reject) =>
res.write(value, (err) => (err ? reject(err) : resolve())),
);
}

I am trying to write a failing test now and then I'll proceed with fixing the bug.

Thanks for reporting!

@enisdenjo enisdenjo added the bug Something isn't working label Jul 19, 2023
@enisdenjo
Copy link
Owner

I am struggling a bit with the failing test, can you share more details on when this happens? Or even better, a repro?

@enisdenjo enisdenjo self-assigned this Jul 20, 2023
@enisdenjo
Copy link
Owner

@michelalbers any luck with the repro?

@enisdenjo
Copy link
Owner

Unable to reproduce. Added some tests too.

@enisdenjo enisdenjo closed this as not planned Won't fix, can't repro, duplicate, stale Aug 7, 2023
@groundmuffin
Copy link

groundmuffin commented Aug 21, 2023

Same error:
Node version: 16.14.2

Error: Cannot call write after a stream was destroyed
at new NodeError (node:internal/errors:371:5)
at write_ (node:_http_outgoing:750:11)
at ServerResponse.write (node:_http_outgoing:707:15)
at /Users/john/Projects/graphql-api/node_modules/graphql-sse/lib/use/express.js:75:56
at new Promise ()
at handleRequest (/Users/john/Projects/graphql-api/node_modules/graphql-sse/lib/use/express.js:75:19)
at runMicrotasks ()
at processTicksAndRejections (node:internal/process/task_queues:96:5)
at /Users/john/Projects/graphql-api/src/express.js:162:5 {
code: 'ERR_STREAM_DESTROYED'
}
node:internal/errors:464
ErrorCaptureStackTrace(err);
^

Error: Cannot render headers after they are sent to the client
at new NodeError (node:internal/errors:371:5)
at ServerResponse.writeHead (node:_http_server:312:13)
at ServerResponse.writeHead (/Users/john/Projects/graphql-api/node_modules/on-headers/index.js:44:26)
at ServerResponse.writeHead (/Users/john/Projects/graphql-api/node_modules/on-headers/index.js:44:26)
at /Users/john/Projects/graphql-api/src/express.js:166:9
at runMicrotasks ()
at processTicksAndRejections (node:internal/process/task_queues:96:5) {
code: 'ERR_HTTP_HEADERS_SENT'
}

Process finished with exit code 1

@enisdenjo
Copy link
Owner

A repro would be very helpful as I am unable to create one myself.

@groundmuffin
Copy link

groundmuffin commented Aug 21, 2023

I'm unable to reproduce it every time as well, but the following code seems unsafe in certain situation:
:node_modules/graphql-sse/lib/use/express.js:75:56

for await (const value of body) {
        await new Promise((resolve, reject) => res.write(value, (err) => (err ? reject(err) : resolve())));
}

Error: Cannot call write after a stream was destroyed

It looks like the response stream/object was destroyed (for whatever reason), but the promise callback is not canceled.

Also, I put the subscription handler in a try/catch block, as the docs specifies, but the node process crash with an unhandled exception :

app.use(
  GQL_SUBSCRIPTIONS_ENDPOINT,
  async (req, res) => {
	  try {
		  await subscriptionsHandler(req, res)
	  }
	  catch (err) {
		  console.error(err)
		  res.writeHead(500).end()
	  }
  }
)

@enisdenjo
Copy link
Owner

The write loop should break as soon as the connection closes (see #69 (comment)).

This line takes care of it:

res.once('close', body.return);

I am suspicious that express itself is late in reporting the closed connection event.

@groundmuffin
Copy link

Lets assume express actually is late in reporting the closed connection event. Isn't that enough reason to check the res availability before calling .write on it?

@groundmuffin
Copy link

groundmuffin commented Aug 22, 2023

Also I suppose that setting a handler on a 'close' event put it on a Macrotask queue (Node Event Loop), while setting a promise callback put it on a Microtask queue. Because of the fact that Microtask queue is consulted more frequently by design, we can meet a situation when the "for loop" handler is called before the "once" one.

@enisdenjo
Copy link
Owner

Both of your points make perfect sense @groundmuffin, I'll make the adjustments. Thanks!

@enisdenjo enisdenjo reopened this Aug 22, 2023
enisdenjo added a commit that referenced this issue Aug 22, 2023
enisdenjo pushed a commit that referenced this issue Aug 22, 2023
## [2.2.2](v2.2.1...v2.2.2) (2023-08-22)

### Bug Fixes

* **use/http,use/http2,use/express,use/fastify:** Handle cases where response's `close` event is late ([#75](#75)) ([4457cba](4457cba)), closes [#69](#69)
@enisdenjo
Copy link
Owner

🎉 This issue has been resolved in version 2.2.2 🎉

The release is available on:

Your semantic-release bot 📦🚀

@enisdenjo enisdenjo added the released Has been released and published label Aug 22, 2023
@enisdenjo
Copy link
Owner

@michelalbers, @groundmuffin the v2.2.2 includes a fix, would be great if you can test it out and report if the issue persists. Thank you for reporting and advising!

@groundmuffin
Copy link

groundmuffin commented Aug 23, 2023

It's still happening

Error: Cannot call write after a stream was destroyed
    at new NodeError (node:internal/errors:371:5)
    at write_ (node:_http_outgoing:750:11)
    at ServerResponse.write (node:_http_outgoing:707:15)
    at closed (/Users/john/Projects/graphql-api/node_modules/graphql-sse/lib/use/express.js:81:25)
    at new Promise (<anonymous>)
    at handleRequest (/Users/john/Projects/graphql-api/node_modules/graphql-sse/lib/use/express.js:75:34)
    at processTicksAndRejections (node:internal/process/task_queues:96:5)
    at /Users/john/Projects/graphql-api/src/express.js:162:5 {
  code: 'ERR_STREAM_DESTROYED'
}
node:internal/errors:464
    ErrorCaptureStackTrace(err);
    ^

Error: Cannot render headers after they are sent to the client
    at new NodeError (node:internal/errors:371:5)
    at ServerResponse.writeHead (node:_http_server:312:13)
    at ServerResponse.writeHead (/Users/john/Projects/graphql-api/node_modules/on-headers/index.js:44:26)
    at ServerResponse.writeHead (/Users/john/Projects/graphql-api/node_modules/on-headers/index.js:44:26)
    at /Users/john/Projects/graphql-api/src/express.js:166:9
    at processTicksAndRejections (node:internal/process/task_queues:96:5) {
  code: 'ERR_HTTP_HEADERS_SENT'
}

@enisdenjo
Copy link
Owner

Wow, then the .closed flag is wrong too. 🤔

enisdenjo added a commit that referenced this issue Aug 23, 2023
…tead of `closed` before writing to response

Closes #69
enisdenjo pushed a commit that referenced this issue Aug 23, 2023
## [2.2.3](v2.2.2...v2.2.3) (2023-08-23)

### Bug Fixes

* **use/http,use/http2,use/express,use/fastify:** Check `writable` instead of `closed` before writing to response ([3c71f69](3c71f69)), closes [#69](#69)
@enisdenjo
Copy link
Owner

🎉 This issue has been resolved in version 2.2.3 🎉

The release is available on:

Your semantic-release bot 📦🚀

@enisdenjo
Copy link
Owner

@groundmuffin, @michelalbers can you try out v2.2.3? I use the .writable flag there instead.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working released Has been released and published
Projects
None yet
3 participants