Skip to content

Commit 7977a93

Browse files
bookernathbookernath
authored andcommitted
feat(pglite-socket): handle CancelRequest wire protocol message
Rebased onto main, which now handles SSLRequest (replies 'N') via the extracted handleSslRequest() method (#990). Adapts the remaining CancelRequest handling to that same style as requested in review: adds named CANCEL_REQUEST_CODE / CANCEL_REQUEST_LENGTH constants (no magic numbers) and extracts the logic into a dedicated handleCancelRequest() method called from the handleData() loop alongside handleSslRequest(). PGlite has no backend process to signal, so the request is consumed and silently ignored (the protocol expects no response). Drops the now-redundant inline SSL handling and SSL-specific tests (covered by upstream's ssl-request.test.ts); keeps a CancelRequest integration test.
1 parent 2dff2d2 commit 7977a93

3 files changed

Lines changed: 64 additions & 0 deletions

File tree

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
'@electric-sql/pglite-socket': minor
3+
---
4+
5+
Handle PostgreSQL CancelRequest wire protocol messages
6+
7+
Added support for the CancelRequest message that some clients and connection proxies send during the connection startup phase. PGlite has no backend process to cancel, so the request is consumed and silently ignored (the protocol expects no response), which prevents it from being misinterpreted as a malformed startup/typed message. This complements the existing SSLRequest handling.

packages/pglite-socket/src/index.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import { type Server, type Socket, createServer } from 'net'
55
export const CONNECTION_QUEUE_TIMEOUT = 60000 // 60 seconds
66
export const SSL_REQUEST_CODE = 80877103
77
export const SSL_REQUEST_LENGTH = 8
8+
export const CANCEL_REQUEST_CODE = 80877102
9+
export const CANCEL_REQUEST_LENGTH = 16
810

911
/**
1012
* Represents a queued query waiting for PGlite access
@@ -346,6 +348,26 @@ export class PGLiteSocketHandler extends EventTarget {
346348
return false
347349
}
348350

351+
// CancelRequest arrives on its own connection during startup, before any
352+
// typed message. PGlite has no backend process to signal, so we consume the
353+
// message and silently drop it (the wire protocol expects no response).
354+
private handleCancelRequest(): boolean {
355+
if (this.messageBuffer.length < CANCEL_REQUEST_LENGTH) {
356+
return false
357+
}
358+
359+
const len = this.messageBuffer.readInt32BE(0)
360+
const code = this.messageBuffer.readInt32BE(4)
361+
362+
if (len === CANCEL_REQUEST_LENGTH && code === CANCEL_REQUEST_CODE) {
363+
this.messageBuffer = this.messageBuffer.slice(CANCEL_REQUEST_LENGTH)
364+
this.log('handleData: CancelRequest received, ignoring (not supported)')
365+
return true
366+
}
367+
368+
return false
369+
}
370+
349371
private async handleData(data: Buffer): Promise<number> {
350372
if (!this.socket || !this.active) {
351373
this.log(`handleData: no active socket, ignoring data`)
@@ -368,6 +390,10 @@ export class PGLiteSocketHandler extends EventTarget {
368390
continue
369391
}
370392

393+
if (this.handleCancelRequest()) {
394+
continue
395+
}
396+
371397
// Determine message length
372398
let messageLength = 0
373399
let isComplete = false

packages/pglite-socket/tests/query-with-node-pg.test.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { Client } from 'pg'
1111
import { PGlite } from '@electric-sql/pglite'
1212
import { PGLiteSocketServer } from '../src'
1313
import { spawn, ChildProcess } from 'node:child_process'
14+
import { createConnection } from 'node:net'
1415
import { fileURLToPath } from 'node:url'
1516
import { dirname, join } from 'node:path'
1617
import fs from 'fs'
@@ -792,6 +793,36 @@ describe(`PGLite Socket Server`, () => {
792793
// swallow
793794
}
794795
}, 30000)
796+
797+
it('should handle CancelRequest without crashing', async () => {
798+
// Test raw CancelRequest wire protocol handling
799+
const socket = createConnection({ host: '127.0.0.1', port: TEST_PORT })
800+
801+
await new Promise<void>((resolve, reject) => {
802+
socket.on('connect', resolve)
803+
socket.on('error', reject)
804+
})
805+
806+
// Send CancelRequest: [length=16][code=80877102 (0x04D2162E)][processID=0][secretKey=0]
807+
const cancelRequest = Buffer.alloc(16)
808+
cancelRequest.writeInt32BE(16, 0)
809+
cancelRequest.writeInt32BE(80877102, 4)
810+
cancelRequest.writeInt32BE(0, 8) // processID
811+
cancelRequest.writeInt32BE(0, 12) // secretKey
812+
socket.write(cancelRequest)
813+
814+
// Wait briefly for server to process (no response expected)
815+
await new Promise((resolve) => setTimeout(resolve, 200))
816+
817+
socket.destroy()
818+
819+
// Verify server still works after receiving CancelRequest
820+
const testClient = new Client(connectionConfig)
821+
await testClient.connect()
822+
const result = await testClient.query('SELECT 1 as one')
823+
expect(result.rows[0].one).toBe(1)
824+
await testClient.end()
825+
})
795826
})
796827

797828
describe('with extensions via CLI', () => {

0 commit comments

Comments
 (0)