From 6024e1592e52e86e8d6c55eb8f846e3f23541b18 Mon Sep 17 00:00:00 2001 From: Taku Amano Date: Sun, 26 Jan 2025 09:31:10 +0900 Subject: [PATCH 1/2] test: add test for request abort without requestCache Co-authored-by: Hironao OTSUBO --- test/listener.test.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/test/listener.test.ts b/test/listener.test.ts index e2d8f8c..1f20892 100644 --- a/test/listener.test.ts +++ b/test/listener.test.ts @@ -274,6 +274,17 @@ describe('Abort request', () => { } } ) + + it('should handle request abort without requestCache', async () => { + const fetchCallback = async () => { + // NOTE: we don't req.signal + await new Promise(() => {}) // never resolve + } + const requestListener = getRequestListener(fetchCallback) + const server = createServer(requestListener) + const req = request(server).post('/abort').timeout({ deadline: 1 }) + await expect(req).rejects.toHaveProperty('timeout') + }) }) describe('overrideGlobalObjects', () => { From a7584975f99dcc11ad6003b41fa58ca286f1cf12 Mon Sep 17 00:00:00 2001 From: Taku Amano Date: Sun, 26 Jan 2025 09:48:36 +0900 Subject: [PATCH 2/2] fix: Avoid error if connection is aborted before internal request object is created At this time, since the user is not listening for abort events, so we should return immediately. --- src/listener.ts | 11 ++++++++--- src/request.ts | 2 +- test/request.test.ts | 16 ++++++++++++++++ 3 files changed, 25 insertions(+), 4 deletions(-) diff --git a/src/listener.ts b/src/listener.ts index a8d9d24..92f2716 100644 --- a/src/listener.ts +++ b/src/listener.ts @@ -1,7 +1,7 @@ import type { IncomingMessage, ServerResponse, OutgoingHttpHeaders } from 'node:http' import type { Http2ServerRequest, Http2ServerResponse } from 'node:http2' import { - getAbortController, + abortControllerKey, newRequest, Request as LightweightRequest, toRequestError, @@ -187,10 +187,15 @@ export const getRequestListener = ( // Detect if request was aborted. outgoing.on('close', () => { + const abortController = req[abortControllerKey] as AbortController | undefined + if (!abortController) { + return + } + if (incoming.errored) { - req[getAbortController]().abort(incoming.errored.toString()) + req[abortControllerKey].abort(incoming.errored.toString()) } else if (!outgoing.writableFinished) { - req[getAbortController]().abort('Client connection prematurely closed.') + req[abortControllerKey].abort('Client connection prematurely closed.') } }) diff --git a/src/request.ts b/src/request.ts index 28408ba..a77d7eb 100644 --- a/src/request.ts +++ b/src/request.ts @@ -85,7 +85,7 @@ const getRequestCache = Symbol('getRequestCache') const requestCache = Symbol('requestCache') const incomingKey = Symbol('incomingKey') const urlKey = Symbol('urlKey') -const abortControllerKey = Symbol('abortControllerKey') +export const abortControllerKey = Symbol('abortControllerKey') export const getAbortController = Symbol('getAbortController') const requestPrototype: Record = { diff --git a/test/request.test.ts b/test/request.test.ts index 999a371..34fdd26 100644 --- a/test/request.test.ts +++ b/test/request.test.ts @@ -4,6 +4,7 @@ import { Request as LightweightRequest, GlobalRequest, getAbortController, + abortControllerKey, RequestError, } from '../src/request' @@ -80,6 +81,21 @@ describe('Request', () => { expect(z).not.toBe(y) }) + it('should be able to safely check if an AbortController has been initialized by referencing the abortControllerKey', async () => { + const req = newRequest({ + headers: { + host: 'localhost', + }, + rawHeaders: ['host', 'localhost'], + url: '/foo.txt', + } as IncomingMessage) + + expect(req[abortControllerKey]).toBeUndefined() // not initialized, do not initialize internal request object automatically + + expect(req[getAbortController]()).toBeDefined() + expect(req[abortControllerKey]).toBeDefined() // initialized + }) + it('Should throw error if host header contains path', async () => { expect(() => { newRequest({