brianc/node-postgres · error · 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 server's final SASL message contains an e= attribute (error) instead of a v= attribute (verifier/signature). Per RFC 5802, a server that rejects the client's proof sends an error attribute rather than a signature. The embedded error string typically contains a SCRAM error indicator such as 'invalid-proof' (wrong password) or 'channel-binding' (channel binding mismatch).

Solutions

  1. Verify the password is correct by connecting with psql using the same credentials.
  2. If the error mentions channel-binding, verify SSL/TLS configuration matches between client and server.
  3. Check whether the password was recently rotated on the server.
  4. Inspect the full error message — the embedded error string (e.g., 'invalid-proof') indicates the specific rejection reason.

Example fix

// The error message includes the server's reason:
// e.g. "SASL: SCRAM-SERVER-FINAL-MESSAGE: server returned error: \"invalid-proof\""
// 'invalid-proof' almost always means wrong password — verify via psql:
//   PGPASSWORD=your_password psql -h host -U user -d db
// Then use the confirmed password in your connection config
Defensive patterns

Strategy: try-catch

Validate before calling

// Verify credentials before opening a connection pool:
const probe = new Client(connStr)
try {
  await probe.connect()
  await probe.end()
} catch (e) {
  console.error('Credentials invalid:', e.message)
}

Try / catch

try {
  await client.connect()
} catch (err) {
  if (err.message.includes('server returned error')) {
    // Extract the SCRAM error reason from the message
    const match = err.message.match(/server returned error: "(.+)"/)
    const scramError = match ? match[1] : 'unknown'
    if (scramError === 'invalid-proof') {
      throw new Error('Authentication rejected: wrong password')
    }
    throw new Error(`Authentication rejected by server: ${scramError}`)
  }
  throw err
}

Prevention

When it happens

Trigger: At sasl.js:224-228, attrPairs.get('e') is truthy. The server's final message includes an e= attribute, indicating it rejected the authentication. The specific error value is interpolated into the message string.

Common situations: Wrong password (the server computed a different proof and rejects the client's); the server-side role password was changed between the salt retrieval and the proof submission; channel binding configuration mismatch (client and server disagree on TLS channel binding); server-side authentication policy rejection.

Related errors


AI-assisted analysis of brianc/node-postgres@ff9d775abd (2026-08-11). Data as JSON: /api/errors/4c91f49b1e897788. Report an issue: GitHub.

Appendix: 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 ff9d775abd)