sidorares/node-mysql2 · error · Error

"user" connection config property must be a string

Error message

"user" connection config property must be a string

What it means

Thrown by HandshakeResponse.toPacket() while building the authentication packet sent during connection establishment. The constructor sets this.user = handshake.user || '', which only coerces falsy values to '' — a truthy non-string (number, object, array) passes through the constructor but fails this typeof check at packet serialization time. Because toPacket() runs during the live handshake (not at config validation), the error surfaces late, after a socket is already open.

Solutions

  1. Cast the value to a string at the call site: user: String(opts.user) or user: '' + opts.user.
  2. Validate the full config object before createConnection/createPool so type errors fail fast and early.
  3. Fix the upstream config source (JSON quoting, env parsing) so `user` is always a string.

Example fix

// before
createConnection({ user: 12345, password: process.env.DB_PASS });
// after
createConnection({ user: String(12345), password: process.env.DB_PASS });
Defensive patterns

Strategy: type-guard

Validate before calling

function normalizeConnectionConfig(opts) {
  if ('user' in opts && opts.user != null && typeof opts.user !== 'string') {
    opts = { ...opts, user: String(opts.user) };
  }
  return opts;
}

const safeOpts = normalizeConnectionConfig(rawOpts);
const conn = await createConnection(safeOpts);

Type guard

function isValidUser(opt) {
  return opt == null || typeof opt === 'string';
}

if (!isValidUser(opts.user)) {
  throw new TypeError(`user must be a string, got ${typeof opts.user}`);
}

Prevention

When it happens

Trigger: Passing a truthy non-string as `user` in the connection config: createConnection({ user: 12345 }), createPool({ user: process.env.DB_USER * 1 }), or createConnection({ user: { name: 'root' } }). The check fires the moment the server handshake completes and the client sends its HandshakeResponse.

Common situations: All-numeric MySQL usernames loaded from typed config/JSON where the value was not quoted; TypeORM/Sequelize configs that pass user as a number; reading user from an env var cast with Number(); config builders that forward untyped values.

Related errors


AI-assisted analysis of sidorares/node-mysql2@8b1f829d37 (2026-08-11). Data as JSON: /api/errors/a8a2e3c5695dc44d. Report an issue: GitHub.

Appendix: source

Thrown at lib/packets/handshake_response.js:126

          connectAttributes[attrNames[k]],
          encoding
        );
      }
      packet.writeLengthCodedNumber(keysLength);
      for (k = 0; k < attrNames.length; ++k) {
        packet.writeLengthCodedString(attrNames[k], encoding);
        packet.writeLengthCodedString(
          connectAttributes[attrNames[k]],
          encoding
        );
      }
    }
    return packet;
  }

  toPacket() {
    if (typeof this.user !== 'string') {
      throw new Error('"user" connection config property must be a string');
    }
    if (typeof this.database !== 'string') {
      throw new Error('"database" connection config property must be a string');
    }
    // dry run: calculate resulting packet length
    const p = this.serializeResponse(Packet.MockBuffer());
    return this.serializeResponse(Buffer.alloc(p.offset));
  }
  static fromPacket(packet, serverFlags = 0xffffffff) {
    const args = {};
    args.clientFlags = packet.readInt32();
    function isSet(flag) {
      return args.clientFlags & serverFlags & ClientConstants[flag];
    }
    args.maxPacketSize = packet.readInt32();
    args.charsetNumber = packet.readInt8();
    const encoding = CharsetToEncoding[args.charsetNumber];
    args.encoding = encoding;

View on GitHub (pinned to 8b1f829d37)