diff --git a/packages/pg-cloudflare/src/index.ts b/packages/pg-cloudflare/src/index.ts index 527b433f5..911c1cde9 100644 --- a/packages/pg-cloudflare/src/index.ts +++ b/packages/pg-cloudflare/src/index.ts @@ -87,22 +87,30 @@ export class CloudflareSocket extends EventEmitter { write( data: Uint8Array | string, encodingOrCallback: BufferEncoding | ((error?: unknown) => void) = 'utf8', - callback: (error?: unknown) => void = () => {} + callback?: (error?: unknown) => void ): true | void { const encoding = typeof encodingOrCallback === 'function' ? 'utf8' : encodingOrCallback if (typeof encodingOrCallback === 'function') callback = encodingOrCallback - if (data.length === 0) return callback() + if (data.length === 0) return callback?.() if (typeof data === 'string') data = Buffer.from(data, encoding) log('sending data direct:', data) this._cfWriter!.write(data).then( () => { log('data sent') - callback() + callback?.() }, (err) => { log('send error', err) - callback(err) + // `Connection._send()` writes without a callback, so a rejected write has + // nowhere to be reported: the query hangs until the socket closes and the + // real reason is lost behind "Connection terminated unexpectedly". Emit + // the failure like a net.Socket would so the connection can report it. + if (callback) { + callback(err) + } else { + this.emit('error', err) + } } ) return true diff --git a/packages/pg-esm-test/pg-cloudflare.test.js b/packages/pg-esm-test/pg-cloudflare.test.js index 683229158..0a425ccc5 100644 --- a/packages/pg-esm-test/pg-cloudflare.test.js +++ b/packages/pg-esm-test/pg-cloudflare.test.js @@ -38,6 +38,34 @@ describe('pg-cloudflare', () => { await promise }) + it('should emit error when a write without a callback fails', async () => { + const socket = new CloudflareSocket() + const writeError = new Error('write failed') + socket._cfWriter = { write: () => Promise.reject(writeError) } + + const emitted = new Promise((resolve, reject) => { + const timer = setTimeout(() => reject(new Error('error event was not emitted')), 100) + socket.once('error', (error) => { + clearTimeout(timer) + resolve(error) + }) + socket.write(Buffer.from('x')) + }) + + assert.equal(await emitted, writeError) + }) + + it('should report a failed write to the callback rather than emitting error', async () => { + const socket = new CloudflareSocket() + const writeError = new Error('write failed') + socket._cfWriter = { write: () => Promise.reject(writeError) } + socket.once('error', () => assert.fail('error event was emitted despite a callback')) + + const reported = await new Promise((resolve) => socket.write(Buffer.from('x'), resolve)) + + assert.equal(reported, writeError) + }) + it('should emit close when ending a socket whose closed promise never settles', async () => { const socket = new CloudflareSocket() socket._cfSocket = {