Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions docs/pages/apis/client.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,31 @@ try {
await client.end()
```

## error codes

Errors sent by the PostgreSQL server have the server's [SQLSTATE](https://www.postgresql.org/docs/current/errcodes-appendix.html) in `err.code`, and network errors keep Node's own codes such as `ECONNREFUSED`. Errors that the client creates itself have a code starting with `ERR_PG_`, so you can check `err.code` instead of the message:

| code | when |
| ------------------------------- | --------------------------------------------------------------------------- |
| `ERR_PG_CONNECTION_TIMEOUT` | the connection was not established within `connectionTimeoutMillis` |
| `ERR_PG_CONNECTION_TERMINATED` | the connection ended unexpectedly, for example because the server closed it |
| `ERR_PG_CONNECTION_ENDED` | a query was still pending when `client.end()` was called |
| `ERR_PG_QUERY_TIMEOUT` | a query did not finish within `query_timeout` |
| `ERR_PG_SSL_NOT_SUPPORTED` | `ssl` is enabled but the server does not support SSL connections |
| `ERR_PG_SSL_NEGOTIATION_FAILED` | the server answered the SSL request with something other than yes or no |

With the [native bindings](/features/native), connection and SSL errors come from libpq and have no code; `ERR_PG_QUERY_TIMEOUT` and `ERR_PG_CONNECTION_ENDED` are set by both clients.

```js
try {
await client.query('SELECT pg_sleep(10)')
} catch (err) {
if (err.code === 'ERR_PG_QUERY_TIMEOUT') {
// retry, or report a timeout
}
}
```

## events

### error
Expand Down
9 changes: 9 additions & 0 deletions docs/pages/apis/pool.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,15 @@ The number of clients which are not checked out but are currently idle in the po

The number of queued requests waiting on a client when all clients are checked out. It can be helpful to monitor this number to see if you need to adjust the size of the pool.

## error codes

Besides the [client's error codes](/apis/client#error-codes), the pool sets these codes on the errors it creates itself:

| code | when |
| ------------------------------ | ------------------------------------------------------------------------------------------------------ |
| `ERR_PG_POOL_CONNECT_TIMEOUT` | a new client did not connect within `connectionTimeoutMillis`; the client's error is in `err.cause` |
| `ERR_PG_POOL_CHECKOUT_TIMEOUT` | no client was released to the waiting request within `connectionTimeoutMillis` while the pool was full |

## events

`Pool` instances are also instances of [`EventEmitter`](https://nodejs.org/api/events.html).
Expand Down
8 changes: 6 additions & 2 deletions packages/pg-pool/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,9 @@ class Pool extends EventEmitter {
// we're going to call it with a timeout error
removeWhere(this._pendingQueue, (i) => i.callback === queueCallback)
pendingItem.timedOut = true
response.callback(new Error('timeout exceeded when trying to connect'))
response.callback(
Object.assign(new Error('timeout exceeded when trying to connect'), { code: 'ERR_PG_POOL_CHECKOUT_TIMEOUT' })
)
}, this.options.connectionTimeoutMillis)

if (tid.unref) {
Expand Down Expand Up @@ -273,7 +275,9 @@ class Pool extends EventEmitter {
// remove the dead client from our list of clients
this._clients = this._clients.filter((c) => c !== client)
if (timeoutHit) {
err = new Error('Connection terminated due to connection timeout', { cause: err })
err = Object.assign(new Error('Connection terminated due to connection timeout', { cause: err }), {
code: 'ERR_PG_POOL_CONNECT_TIMEOUT',
})
}

// this client won’t be released, so move on immediately
Expand Down
13 changes: 13 additions & 0 deletions packages/pg-pool/test/connection-timeout.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ describe('connection timeout', () => {
pool.connect((err, client, release) => {
expect(err).to.be.an(Error)
expect(err.message).to.contain('timeout')
expect(err.code).to.equal('ERR_PG_POOL_CONNECT_TIMEOUT')
expect(client).to.equal(undefined)
expect(pool.idleCount).to.equal(0)
done()
Expand All @@ -46,11 +47,23 @@ describe('connection timeout', () => {
pool.connect().catch((err) => {
expect(err).to.be.an(Error)
expect(err.message).to.contain('timeout')
expect(err.code).to.equal('ERR_PG_POOL_CONNECT_TIMEOUT')
expect(pool.idleCount).to.equal(0)
done()
})
})

it('should give a checkout timeout its own code when the pool is full', (done) => {
const pool = new Pool({ connectionTimeoutMillis: 50, max: 1, port: this.port, host: 'localhost' })
pool.connect(() => {}) // takes the only slot; never finishes connecting to the silent server
pool.connect((err, client) => {
expect(err.message).to.equal('timeout exceeded when trying to connect')
expect(err.code).to.equal('ERR_PG_POOL_CHECKOUT_TIMEOUT')
expect(client).to.equal(undefined)
done()
})
})

it(
'should handle multiple timeouts',
co.wrap(
Expand Down
10 changes: 4 additions & 6 deletions packages/pg-pool/test/error-handling.js
Original file line number Diff line number Diff line change
Expand Up @@ -227,15 +227,13 @@ describe('pool error handling', function () {
const pool = new Pool({ max: 1, port: closeServer.address().port, host: 'localhost' })
pool.connect((err) => {
expect(err).to.be.an(Error)
if (err.code) {
expect(err.code).to.be('ECONNRESET')
}
// the socket is either reset or closed cleanly, depending on the platform
expect(['ECONNRESET', 'ERR_PG_CONNECTION_TERMINATED']).to.contain(err.code)
})
pool.connect((err) => {
expect(err).to.be.an(Error)
if (err.code) {
expect(err.code).to.be('ECONNRESET')
}
// the socket is either reset or closed cleanly, depending on the platform
expect(['ECONNRESET', 'ERR_PG_CONNECTION_TERMINATED']).to.contain(err.code)
closeServer.close(() => {
pool.end(done)
})
Expand Down
8 changes: 5 additions & 3 deletions packages/pg/lib/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ class Client extends EventEmitter {
if (this._connectionTimeoutMillis > 0) {
this.connectionTimeoutHandle = setTimeout(() => {
con._ending = true
con.stream.destroy(new Error('timeout expired'))
con.stream.destroy(Object.assign(new Error('timeout expired'), { code: 'ERR_PG_CONNECTION_TIMEOUT' }))
}, this._connectionTimeoutMillis)

if (this.connectionTimeoutHandle.unref) {
Expand Down Expand Up @@ -201,7 +201,9 @@ class Client extends EventEmitter {
this._attachListeners(con)

con.once('end', () => {
const error = this._ending ? new Error('Connection terminated') : new Error('Connection terminated unexpectedly')
const error = this._ending
? Object.assign(new Error('Connection terminated'), { code: 'ERR_PG_CONNECTION_ENDED' })
: Object.assign(new Error('Connection terminated unexpectedly'), { code: 'ERR_PG_CONNECTION_TERMINATED' })

clearTimeout(this.connectionTimeoutHandle)
this._errorAllQueries(error)
Expand Down Expand Up @@ -704,7 +706,7 @@ class Client extends EventEmitter {
const queryCallback = query.callback || (() => {})

const readTimeoutTimer = setTimeout(() => {
const error = new Error('Query read timeout')
const error = Object.assign(new Error('Query read timeout'), { code: 'ERR_PG_QUERY_TIMEOUT' })

process.nextTick(() => {
query.handleError(error, this.connection)
Expand Down
14 changes: 12 additions & 2 deletions packages/pg/lib/connection.js
Original file line number Diff line number Diff line change
Expand Up @@ -83,11 +83,21 @@ class Connection extends EventEmitter {
break
case 'N': // Server does not support SSL connections
self.stream.end()
return self.emit('error', new Error('The server does not support SSL connections'))
return self.emit(
'error',
Object.assign(new Error('The server does not support SSL connections'), {
code: 'ERR_PG_SSL_NOT_SUPPORTED',
})
)
default:
// Any other response byte, including 'E' (ErrorResponse) indicating a server error
self.stream.end()
return self.emit('error', new Error('There was an error establishing an SSL connection'))
return self.emit(
'error',
Object.assign(new Error('There was an error establishing an SSL connection'), {
code: 'ERR_PG_SSL_NEGOTIATION_FAILED',
})
)
}
self.upgradeToSSL(host, reportStreamError)
})
Expand Down
4 changes: 2 additions & 2 deletions packages/pg/lib/native/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,7 @@ Client.prototype.query = function (config, values, callback) {
queryCallback = query.callback || (() => {})

readTimeoutTimer = setTimeout(() => {
const error = new Error('Query read timeout')
const error = Object.assign(new Error('Query read timeout'), { code: 'ERR_PG_QUERY_TIMEOUT' })

process.nextTick(() => {
query.handleError(error, this.connection)
Expand Down Expand Up @@ -267,7 +267,7 @@ Client.prototype.end = function (cb) {
self.native.end(function () {
self._connected = false

self._errorAllQueries(new Error('Connection terminated'))
self._errorAllQueries(Object.assign(new Error('Connection terminated'), { code: 'ERR_PG_CONNECTION_ENDED' }))

process.nextTick(() => {
self.emit('end')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,10 @@ suite.test('expired connection timeout', (done) => {
.catch((err) => {
assert(err instanceof Error)
assert(/timeout expired\s*/.test(err.message))
// the native client gets this error from libpq, without a code
if (!helper.args.native) {
assert.equal(err.code, 'ERR_PG_CONNECTION_TIMEOUT')
}
closeServer(done)
})
.then(() => clearTimeout(timeoutId))
Expand Down
2 changes: 2 additions & 0 deletions packages/pg/test/unit/client/query-timeout-tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ test('query timeout with Submittable without callback delivers error via handleE
const query = new Query({ text: 'SELECT 1' })
query.handleError = (err) => {
assert.equal(err.message, 'Query read timeout')
assert.equal(err.code, 'ERR_PG_QUERY_TIMEOUT')
done()
}

Expand All @@ -29,6 +30,7 @@ test('query timeout with Submittable with callback delivers error via callback',

client.query(query, (err) => {
assert.equal(err.message, 'Query read timeout')
assert.equal(err.code, 'ERR_PG_QUERY_TIMEOUT')
done()
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ suite.test('emits end when not in query', function () {
client.query(
'SELECT NOW()',
assert.calls(function (err, result) {
assert(err)
assert.equal(err.message, 'Connection terminated unexpectedly')
assert.equal(err.code, 'ERR_PG_CONNECTION_TERMINATED')
})
)
})
Expand Down
4 changes: 4 additions & 0 deletions packages/pg/test/unit/connection/error-tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,18 +65,21 @@ const SSLNegotiationPacketTests = [
{
testName: 'connection does not emit ECONNRESET errors during disconnect also when using SSL',
errorMessage: null,
errorCode: null,
response: 'S',
responseType: 'sslconnect',
},
{
testName: 'connection emits an error when SSL is not supported',
errorMessage: 'The server does not support SSL connections',
errorCode: 'ERR_PG_SSL_NOT_SUPPORTED',
response: 'N',
responseType: 'error',
},
{
testName: 'connection emits an error when postmaster responds to SSL negotiation packet',
errorMessage: 'There was an error establishing an SSL connection',
errorCode: 'ERR_PG_SSL_NEGOTIATION_FAILED',
response: 'E',
responseType: 'error',
},
Expand Down Expand Up @@ -170,6 +173,7 @@ for (const tc of SSLNegotiationPacketTests) {
assert.emits(con, tc.responseType, function (err) {
if (tc.errorMessage !== null || err) {
assert.equal(err.message, tc.errorMessage)
assert.equal(err.code, tc.errorCode)
}
con.end()
socket.destroy()
Expand Down
Loading