Visitar URL original
Invalid state: ReadableStream is already closed · Issue #233 · honojs/node-server · GitHub
Skip to content

Invalid state: ReadableStream is already closed #233

Open

Description

@Plopix

Hello here,
I am starting to suspect something odd in @hono/node-server

We have random ReadableStream is already closed in the app, everything is try/catch but anyway it crashes the container.

We are also using GraphQL Yoga, I found out about this: #77

But I hesitate to do that because GraphQL Yoga is using @whatwg-node/fetch which is supposed to be a good abstraction.

the full trace is:

node:internal/webstreams/readablestream:1162
throw new ERR_INVALID_STATE.TypeError('ReadableStream is already closed');
^
TypeError [ERR_INVALID_STATE]: Invalid state: ReadableStream is already closed
at ReadableByteStreamController.close (node:internal/webstreams/readablestream:1162:13)
at node:internal/deps/undici/undici:1480:28
at node:internal/process/task_queues:151:7
at AsyncResource.runInAsyncScope (node:async_hooks:211:14)
at AsyncResource.runMicrotask (node:internal/process/task_queues:148:8)
at process.processTicksAndRejections (node:internal/process/task_queues:105:5) {
code: 'ERR_INVALID_STATE'
}
Node.js v22.13.1

Of course I don't reproduce easily, it happens randomly

Any idea? to identify the problem better?

Activity

  1. Plopix commented on Mar 21, 2025

    @Plopix
    Author

    could it be related to this: https://github.com/honojs/node-server/blob/main/src/request.ts#L76

    but it's so random (we have hundred of thousand request that are OK)

  2. usualoma commented on Apr 1, 2025

    @usualoma
    Member

    Hi @Plopix. Thank you for the report. I will investigate.

  3. usualoma commented on Apr 13, 2025

    @usualoma
    Member

    After thinking about it for a while, I think it would be better to catch it by ‘uncaughtException’ as follows.

    process.on('uncaughtException', (err) => {
      console.error('uncaughtException:', err);
    });
    

    Where did the error occur?

    The following areas are related, and it is due to a race condition in the loading and disconnecting of the response body.

    https://github.com/nodejs/node/blob/v22.13.1/deps/undici/undici.js#L1480
    https://github.com/nodejs/node/blob/v22.13.1/deps/undici/undici.js#L5578

    Because it is an error in queueMicrotask(), I think Hono and node-server (this project) cannot catch the exception.

    If this problem can be solved using the Node.js native APIs in GraphQL Yoga, I think it would be good to consider it.

  4. ImLunaHey commented on Dec 27, 2025

    @ImLunaHey

    using Readable.toWeb seems to have resolved this for me.

  5. cpojer commented on Jan 22, 2026

    @cpojer

    I'm running into this issue as well. Repro:

    plugins: [
      useGraphQLSSE({
        endpoint: '/graphql/stream',
      }),
    ],
    • Connect to it using GraphQL subscriptions with a client
    • Press cmd+r to reload the client page in the browser → it crashes the Hono server.
  6. yusukebe commented on Jan 23, 2026

    @yusukebe
    Member

    Can you share a minimal project to reproduce it? The repo is too big and complex. It's hard to reproduce for me.

  7. yusukebe commented on Jan 25, 2026

    @yusukebe
    Member

    @cpojer First, thank you for sharing the project to reproduce your problem. I investigated and concluded it was the same as @usualoma 's comment: #233 (comment)

    As it is said, this problem is caused by undici in Node.js, and this Node.js Adapter (or JavaScript layer) can't handle it.

    So, to remove the error, I think the best solution is catching it with process.on('uncaughtException'). I created the PR #299 with the method. It's worth accepting.

    Hey @usualoma! What do you think of this? If you agree to go with it, can you review it? (The test is nothing, since I don't know how to write one).

  8. ImLunaHey commented on Jan 26, 2026

    @ImLunaHey

    @yusukebe this approach seems problematic. using process.on('uncaughtException') to simply return is essentially suppressing the error rather than handling it properly. this hides the underlying issue instead of fixing it.

  9. yusukebe commented on Jan 26, 2026

    @yusukebe
    Member

    @ImLunaHey

    Any idea to resolve this problem?

  10. ImLunaHey commented on Jan 26, 2026

    @ImLunaHey

    node-server needs to add state tracking to prevent multiple close() calls on the same ReadableStream. subsequent attempts should no-op.

  11. yusukebe commented on Jan 30, 2026

    @yusukebe
    Member

    @ImLunaHey

    I've tried the same things (maybe), but they don't work well. Can you create a PR to do so?

  12. ImLunaHey commented on Jan 30, 2026

    @ImLunaHey

    @yusukebe something like this should work. test included.

    diff --git a/src/utils.ts b/src/utils.ts
    index 7962eee..5224a1b 100644
    --- a/src/utils.ts
    +++ b/src/utils.ts
    @@ -12,7 +12,12 @@ export function writeFromReadableStreamDefaultReader(
       writable: Writable,
       currentReadPromise?: Promise<ReadableStreamReadResult<Uint8Array>> | undefined
     ) {
    +  let cancelled = false
       const cancel = (error?: unknown) => {
    +    if (cancelled) {
    +      return
    +    }
    +    cancelled = true
         reader.cancel(error).catch(() => {})
       }
     
    @@ -21,6 +26,7 @@ export function writeFromReadableStreamDefaultReader(
       ;(currentReadPromise ?? reader.read()).then(flow, handleStreamError)
     
       return reader.closed.finally(() => {
    +    cancelled = true
         writable.off('close', cancel)
         writable.off('error', cancel)
       })
    diff --git a/test/utils.test.ts b/test/utils.test.ts
    index f105b89..ede7678 100644
    --- a/test/utils.test.ts
    +++ b/test/utils.test.ts
    @@ -115,6 +115,44 @@ describe('writeFromReadableStream', () => {
         expect(enqueueCalled).toBe(false) // enqueue should not be called
         expect(cancelCalled).toBe(true) // cancel should be called
       })
    +
    +  it('should handle multiple close/error events without calling cancel twice', async () => {
    +    let cancelCallCount = 0
    +    let enqueueTimeout: NodeJS.Timeout | undefined
    +
    +    const stream = new ReadableStream({
    +      start(controller) {
    +        enqueueTimeout = setTimeout(() => {
    +          try {
    +            controller.enqueue(new TextEncoder().encode('test'))
    +            controller.close()
    +          } catch {
    +            // Controller may already be closed
    +          }
    +        }, 100)
    +      },
    +      cancel() {
    +        cancelCallCount++
    +        clearTimeout(enqueueTimeout)
    +      },
    +    })
    +
    +    const writable = new Writable({
    +      write(_chunk, _encoding, callback) {
    +        callback()
    +      },
    +    })
    +
    +    // Simulate client disconnect with error - destroy() emits both 'error' and 'close'
    +    setTimeout(() => {
    +      writable.destroy(new Error('connection reset'))
    +    }, 50)
    +
    +    await writeFromReadableStream(stream, writable)
    +
    +    // cancel should only be called once despite multiple events from destroy()
    +    expect(cancelCallCount).toBe(1)
    +  })
     })
     
     describe('readWithoutBlocking', () => {
  13. ImLunaHey commented on Jan 30, 2026

    @ImLunaHey

    the only other solution i have is to fix this in undici. there's also likely other places this needs to be tracked and avoided like in static streaming.

  14. gaearon commented on Feb 25, 2026

    @gaearon

    Has either of these fixes been verified? Not sure if I understand if these are guesses or definitely correct fixes. I.e. do we know exactly why this is happening and which side is at fault?

  15. gaearon commented on Feb 25, 2026

    @gaearon

    Possible fix: honojs/hono#4770

  16. yusukebe commented on Mar 5, 2026

    @yusukebe
    Member

    The problem fixed with honojs/hono#4770 by @gaearon (thanks!) is not the same problem as this issue. Keep going.

  17. alexander-akait commented on Mar 18, 2026

    @alexander-akait

    We are facing with the same issue - https://github.com/webpack/webpack-dev-middleware/actions/runs/23242789591/job/67563322741, attempts to debug were not successful at all, tests fail only related ones when we read a file from disk using fs.createReadStream and then context.body(body, 200), it's worth saying that we don't have the same issue with other frameworks like express/connect/koa/etc (we support all popular frameworks) and yes we have everything in try/catch and a lot of tests, crashes happen randomly, usually during testing when a lot of requests are running in parallel

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions