{"record":{"id":"da2df546d5c89936","repo":"brianc/node-postgres","slug":"sasl-scram-server-final-message-serverdata-must","errorCode":null,"errorMessage":"SASL: SCRAM-SERVER-FINAL-MESSAGE: serverData must be a string","messagePattern":"SASL: SCRAM-SERVER-FINAL-MESSAGE: serverData must be a string","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/pg/lib/crypto/sasl.js","lineNumber":134,"sourceCode":"  const saltedPassword = await crypto.deriveKey(saslprep(password), saltBytes, sv.iteration)\n  const clientKey = await crypto.hmacSha256(saltedPassword, 'Client Key')\n  const storedKey = await crypto.sha256(clientKey)\n  const clientSignature = await crypto.hmacSha256(storedKey, authMessage)\n  const clientProof = xorBuffers(Buffer.from(clientKey), Buffer.from(clientSignature)).toString('base64')\n  const serverKey = await crypto.hmacSha256(saltedPassword, 'Server Key')\n  const serverSignatureBytes = await crypto.hmacSha256(serverKey, authMessage)\n\n  session.message = 'SASLResponse'\n  session.serverSignature = Buffer.from(serverSignatureBytes).toString('base64')\n  session.response = clientFinalMessageWithoutProof + ',p=' + clientProof\n}\n\nfunction finalizeSession(session, serverData) {\n  if (session.message !== 'SASLResponse') {\n    throw new Error('SASL: Last message was not SASLResponse')\n  }\n  if (typeof serverData !== 'string') {\n    throw new Error('SASL: SCRAM-SERVER-FINAL-MESSAGE: serverData must be a string')\n  }\n\n  const { serverSignature } = parseServerFinalMessage(serverData)\n\n  if (serverSignature !== session.serverSignature) {\n    throw new Error('SASL: SCRAM-SERVER-FINAL-MESSAGE: server signature does not match')\n  }\n}\n\n/**\n * printable       = %x21-2B / %x2D-7E\n *                   ;; Printable ASCII except \",\".\n *                   ;; Note that any \"printable\" is also\n *                   ;; a valid \"value\".\n */\nfunction isPrintableChars(text) {\n  if (typeof text !== 'string') {\n    throw new TypeError('SASL: text must be a string')","sourceCodeStart":116,"sourceCodeEnd":152,"githubUrl":"https://github.com/brianc/node-postgres/blob/ff9d775abd12f29dd6df03945253b54eabbb29f2/packages/pg/lib/crypto/sasl.js#L116-L152","documentation":"Thrown during the final step of SCRAM-SHA-256 authentication inside finalizeSession(). The function expects the server's final SASL message as a string argument, but received something else (null, undefined, Buffer, number). This is an internal invariant guard — node-postgres's own connection handler calls finalizeSession with the payload from the AuthenticationSASLFinal message, so a normal user almost never triggers it directly.","triggerScenarios":"The connection handler passes msg.data from the server's final SASL frame to finalizeSession(session, serverData). If msg.data is not a string — due to a protocol parser delivering a Buffer or null, a custom Client subclass overriding the auth flow, or a corrupted/partial auth exchange where the final message payload is empty.","commonSituations":"Using a forked or heavily patched pg version with a broken SASL handler; an SSL terminator or proxy (e.g., a custom TLS proxy) that strips or corrupts the final SASL frame; connecting to a server that abruptly closes the connection mid-auth, leaving the final payload null. Extremely rare with stock pg against real PostgreSQL.","solutions":["Update node-postgres (pg) to the latest version — this guard has existed since SCRAM support was added and internal fixes have shipped.","Remove or audit any custom Client subclass or monkeypatch that overrides the SASL handshake (startSession/continueSession/finalizeSession calls).","Eliminate proxies or SSL terminators between the client and PostgreSQL that might alter or truncate the SASL message stream.","If using pg-native, verify the native binding version is compatible with your pg version."],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":"// This is an internal SASL error — pre-validation of server data is not possible.\n// Instead, wrap the connection in a try-catch and verify server reachability:\nconst testConn = new Client(connStr)\ntry {\n  await testConn.connect()\n  await testConn.end()\n} catch (e) {\n  console.error('Connection failed:', e.message)\n}","typeGuard":null,"tryCatchPattern":"try {\n  await client.connect()\n} catch (err) {\n  if (err.message.includes('serverData must be a string')) {\n    // Internal SASL protocol error — likely a corrupting proxy or incompatible server\n    console.error('SASL finalization failed — check for proxies or non-standard servers')\n  }\n  throw err\n}","preventionTips":["Connect directly to PostgreSQL without intermediary proxies that might corrupt the SASL stream.","Keep node-postgres updated to the latest version.","Avoid custom Client subclasses that override the authentication flow.","Ensure pg-native bindings (if used) match your pg version."],"tags":["authentication","sasl","scram","connection","internal-invariant"],"backgroundTag":null,"analyzedSha":"ff9d775abd12f29dd6df03945253b54eabbb29f2","analyzedAt":"2026-08-11T15:33:59.644Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}