Skip to content

[fetch-router] Signal on cloned requests are lost with NodeJS on GC #10861

@gyx1000

Description

@gyx1000

Hi Remix!

I noticed that the abort controller on a cloned request is lost if the GC runs between the request and the server response. This can happen if the response body is a ReadableStream and the client interrupts its request in the meantime.

The fetch method of fetch-router clones the initial request with let request = new Request(input, init);.

The following test fails to complete correctly because the signal is lost.
To run the test, be sure to enable the --expose-gc option....

NODE_OPTIONS="--expose-gc" pnpm --filter @remix-run/fetch-router test

  it('handle abort signal on cloned request, not finished and garbage collected', async () => {
    let router = createRouter()
    let aborted = false

    router.get('/', async ({ request }) => {
      let stream = new ReadableStream({
        start(controller) {
          // this signal should not be lost after GC !
          request.signal.addEventListener('abort', () => {
            controller.close()
            aborted = true
          })
        },
      })

      return new Response(stream)
    })

    let ac = new AbortController()
    let request = new Request('https://remix.run', { signal: ac.signal })

    setTimeout(() => {
      // force GC (ensure --expose-gc is specified)
      global.gc!()
      // client cancel the request
      ac.abort()
    })

    let response = await router.fetch(request)
    assert.equal(response.status, 200)

    // drain the content
    await response.arrayBuffer()
    assert.equal(aborted, true)
  })
})

The test works if you comment out global.gc() or if fetch-router use input as request if it is of type Request.

I don't know if this issue is related to: nodejs/undici#4068

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions