diff --git a/packages/pg-cloudflare/src/index.ts b/packages/pg-cloudflare/src/index.ts index 527b433f5..0b399856b 100644 --- a/packages/pg-cloudflare/src/index.ts +++ b/packages/pg-cloudflare/src/index.ts @@ -10,6 +10,8 @@ export class CloudflareSocket extends EventEmitter { private _upgrading = false private _upgraded = false + private _ending = false + private _closed = false private _cfSocket: Socket | null = null private _cfWriter: WritableStreamDefaultWriter | null = null private _cfReader: ReadableStreamDefaultReader | null = null @@ -65,7 +67,16 @@ export class CloudflareSocket extends EventEmitter { // eslint-disable-next-line no-constant-condition while (true) { log('awaiting receive from CF socket') - const { done, value } = await this._cfReader!.read() + let result: ReadableStreamReadResult + try { + result = await this._cfReader!.read() + } catch (e) { + // closing the socket in end() rejects the read that is still pending, + // and after the peer closed it, 'close' has already reported the end + if (this._ending || this._closed) break + throw e + } + const { done, value } = result log('CF socket received:', done, value) if (done) { log('done') @@ -110,6 +121,7 @@ export class CloudflareSocket extends EventEmitter { end(data = Buffer.alloc(0), encoding: BufferEncoding = 'utf8', callback: (...args: unknown[]) => void = () => {}) { log('ending CF socket') + this._ending = true this.write(data, encoding, (err) => { const socket = this._cfSocket const closePromise = socket?.close() @@ -156,6 +168,7 @@ export class CloudflareSocket extends EventEmitter { log('CF socket closed') if (this._cfSocket === socket) { this._cfSocket = null + this._closed = true this.emit('close') } } else { diff --git a/packages/pg-esm-test/pg-cloudflare.test.js b/packages/pg-esm-test/pg-cloudflare.test.js index 683229158..ebcf8eec8 100644 --- a/packages/pg-esm-test/pg-cloudflare.test.js +++ b/packages/pg-esm-test/pg-cloudflare.test.js @@ -55,4 +55,51 @@ describe('pg-cloudflare', () => { socket.end() }) }) + + it('should stop listening without an error when end() closes the socket', async () => { + const socket = new CloudflareSocket() + let rejectRead + socket._cfReader = { + read: () => + new Promise((resolve, reject) => { + rejectRead = reject + }), + } + socket._cfSocket = { + close: async () => rejectRead(new TypeError('This socket has been closed.')), + } + + const listening = socket._listen() + socket.end() + + await listening + }) + + it('should stop listening without an error when the read fails after the socket closed', async () => { + const socket = new CloudflareSocket() + let rejectRead + socket._cfReader = { + read: () => + new Promise((resolve, reject) => { + rejectRead = reject + }), + } + const underlyingSocket = { closed: Promise.resolve() } + socket._cfSocket = underlyingSocket + socket._addClosedHandler() + + const listening = socket._listen() + await underlyingSocket.closed + assert.equal(socket._cfSocket, null) + rejectRead(new TypeError('This socket has been closed.')) + + await listening + }) + + it('should still fail listening when a read fails before end()', async () => { + const socket = new CloudflareSocket() + socket._cfReader = { read: () => Promise.reject(new Error('Network connection lost.')) } + + await assert.rejects(socket._listen(), /Network connection lost/) + }) })