brianc/node-postgres · critical · Error
SASL: SCRAM-SERVER-FINAL-MESSAGE: server returned error: "${
Error message
SASL: SCRAM-SERVER-FINAL-MESSAGE: server returned error: "${error}" What it means
Thrown by parseServerFinalMessage when the SCRAM final message contains an `e=` attribute (sasl.js:227-228) — the server's formal error indicator per RFC 5802. Common values: `invalid-proof` (bad password), `channel-binding-not-supported`, `other-error`. Unlike the parse errors, this is *expected* server behavior and usually denotes a real authentication failure rather than frame corruption.
Source
Thrown at packages/pg/lib/crypto/sasl.js:228
} else if (!/^[1-9][0-9]*$/.test(iterationText)) {
throw new Error('SASL: SCRAM-SERVER-FIRST-MESSAGE: invalid iteration count')
}
const iteration = parseInt(iterationText, 10)
return {
nonce,
salt,
iteration,
}
}
function parseServerFinalMessage(serverData) {
const attrPairs = parseAttributePairs(serverData)
const error = attrPairs.get('e')
const serverSignature = attrPairs.get('v')
if (error) {
throw new Error(`SASL: SCRAM-SERVER-FINAL-MESSAGE: server returned error: "${error}"`)
}
if (!serverSignature) {
throw new Error('SASL: SCRAM-SERVER-FINAL-MESSAGE: server signature is missing')
} else if (!isBase64(serverSignature)) {
throw new Error('SASL: SCRAM-SERVER-FINAL-MESSAGE: server signature must be base64')
}
return {
serverSignature,
}
}
function xorBuffers(a, b) {
if (!Buffer.isBuffer(a)) {
throw new TypeError('first argument must be a Buffer')
}
if (!Buffer.isBuffer(b)) {
throw new TypeError('second argument must be a Buffer')View on GitHub (pinned to c5e8c9a57b)
Solutions
- Verify the password/credential is current and correct against the actual role.
- Confirm the PostgreSQL role exists: SELECT rolname FROM pg_roles WHERE rolname = $1.
- For `channel-binding-not-supported`, retry over plain SCRAM-SHA-256 (drop channel binding or fix TLS).
- Regenerate the role password if the stored verifier may be corrupted.
Example fix
// before
const client = new Client({ user: 'app', password: process.env.PGPASSWORD })
// after -- reload the credential from a live secret source
const client = new Client({ user: 'app', password: await secrets.fetch('pg/app') }) Defensive patterns
Strategy: try-catch
Try / catch
try {
await client.connect()
} catch (err) {
const m = err.message.match(/server returned error: "([^"]+)"/)
if (m) {
const code = m[1]
if (code === 'invalid-proof') throw new AuthError('bad credentials')
if (code === 'channel-binding-not-supported') return connectWithoutChannelBinding()
throw new AuthError(`scram failure: ${code}`)
}
throw err
} Prevention
- Pull credentials from a live secret manager rather than a cached env var.
- Test role existence and password validity in CI before deploy.
- Only enable channel binding when both client and server advertise it cleanly.
When it happens
Trigger: finalizeSession (sasl.js:129) is invoked after the client sends its proof, and the server's final frame contains `e=...` instead of (or alongside) the `v=` verifier. attrPairs.get('e') at line 224 is truthy.
Common situations: Wrong password; password rotated since a connection pool was built; role dropped; SCRAM verifier corrupted server-side; channel-binding negotiation mismatch (server demands binding the client cannot provide).
Related errors
- SASL: SCRAM-SERVER-FINAL-MESSAGE: server signature does not
- SASL: Only mechanism(s) ${candidates.join(' and ')} are supp
- SASL: SCRAM-SERVER-FIRST-MESSAGE: client password must be a
- SASL: SCRAM-SERVER-FIRST-MESSAGE: client password must be a
- SASL: SCRAM-SERVER-FIRST-MESSAGE: salt must be base64
AI-assisted analysis of brianc/node-postgres@c5e8c9a57b (2026-08-03).
Data as JSON: /data/errors/4c91f49b1e897788.json.
Report an issue: GitHub.