brianc/node-postgres · error · Error

SASL: SCRAM-SERVER-FIRST-MESSAGE: client password must be a

Error message

SASL: SCRAM-SERVER-FIRST-MESSAGE: client password must be a string

What it means

Thrown during SCRAM session continuation (sasl.js:66-68) when the password used for authentication is not a string. The SCRAM-SHA-256 protocol requires the plaintext password to derive a salted key via PBKDF2, so a non-string (number, object, null, undefined) cannot be processed. This is a client-side guard fired before any crypto operation, ensuring the type contract is met.

Source

Thrown at packages/pg/lib/crypto/sasl.js:67

  const clientNonce = crypto.randomBytes(18).toString('base64')
  const gs2Header = mechanism === 'SCRAM-SHA-256-PLUS' ? 'p=tls-server-end-point' : stream ? 'y' : 'n'

  return {
    mechanism,
    clientNonce,
    response: gs2Header + ',,n=*,r=' + clientNonce,
    message: 'SASLInitialResponse',
    scramMaxIterations,
  }
}

async function continueSession(session, password, serverData, stream) {
  if (session.message !== 'SASLInitialResponse') {
    throw new Error('SASL: Last message was not SASLInitialResponse')
  }
  if (typeof password !== 'string') {
    throw new Error('SASL: SCRAM-SERVER-FIRST-MESSAGE: client password must be a string')
  }
  if (password === '') {
    throw new Error('SASL: SCRAM-SERVER-FIRST-MESSAGE: client password must be a non-empty string')
  }
  if (typeof serverData !== 'string') {
    throw new Error('SASL: SCRAM-SERVER-FIRST-MESSAGE: serverData must be a string')
  }

  const sv = parseServerFirstMessage(serverData)

  if (!sv.nonce.startsWith(session.clientNonce)) {
    throw new Error('SASL: SCRAM-SERVER-FIRST-MESSAGE: server nonce does not start with client nonce')
  } else if (sv.nonce.length === session.clientNonce.length) {
    throw new Error('SASL: SCRAM-SERVER-FIRST-MESSAGE: server nonce is too short')
  }

  const scramMaxIterations =
    typeof session.scramMaxIterations === 'number' ? session.scramMaxIterations : DEFAULT_MAX_SCRAM_ITERATIONS

View on GitHub (pinned to c5e8c9a57b)

Solutions

  1. Ensure the password is a string: set PGPASSWORD, add it to the connection string, or pass { password: '...' } to the Client/Pool config.
  2. If using a dynamic password provider (password as a function), make sure it resolves to a string.
  3. Verify the .pgpass file is readable if relying on it, though note pgpass support is deprecated.

Example fix

// before
const client = new Client({ user: 'me', host: 'localhost' }); // no password

// after
const client = new Client({
  user: 'me',
  host: 'localhost',
  password: process.env.PGPASSWORD,
});
Defensive patterns

Strategy: type-guard

Validate before calling

function ensureStringPassword(config) {
  const pw = config.password ?? process.env.PGPASSWORD;
  if (typeof pw !== 'string') {
    throw new TypeError('Password must be a string for SCRAM authentication');
  }
  config.password = pw;
}

Type guard

function isStringPassword(password) {
  return typeof password === 'string';
}

Try / catch

try {
  await client.connect();
} catch (err) {
  if (/password must be a string/i.test(err.message)) {
    console.error('Password is not set or not a string — check PGPASSWORD / config.');
  }
  throw err;
}

Prevention

When it happens

Trigger: The client's password property is undefined (e.g., PGPASSWORD not set and no password in config), or it was set to a non-string value like a number or an object. This surfaces inside continueSession when typeof password !== 'string'.

Common situations: PGPASSWORD env var is unset and no password is in the connection string/config for a role requiring SCRAM auth. A dynamic password provider function was used incorrectly and returned a non-string. The password was accidentally set to null in a config merge.

Related errors


AI-assisted analysis of brianc/node-postgres@c5e8c9a57b (2026-08-03). Data as JSON: /data/errors/d6fc97cccf9e7b89.json. Report an issue: GitHub.