Skip to content

Commit b658be0

Browse files
authored
fix: handle prepare response with actual number of parameter definition less than reported in the prepare header. Fixes #2052
* chore: route synchronous exceptions from packet deserialisation as a fatal connection error * chore: initial steps in migration to builtin node test runner * chore: add test:builtin-node-runner npm script * add a test case reproducing #2052 issue * add mysql:8.0.33 to linux matrix * debug test execution * cleanup debug log * simple connection test * simple connection test * end connection in the test * add timeout * fix: handle prepare response with actual number of parameter definition less than reported in the prepare header. Fixes #2052 * test commit * lint
1 parent 2c27961 commit b658be0

File tree

5 files changed

+163
-24
lines changed

5 files changed

+163
-24
lines changed

.github/workflows/ci-linux.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,20 +30,20 @@ jobs:
3030
fail-fast: false
3131
matrix:
3232
node-version: [16.x, 18.x, 20.x]
33-
mysql-version: ["mysql:8.0.18", "mysql:8.0.22", "mysql:5.7"]
33+
mysql-version: ["mysql:8.0.18", "mysql:8.0.22", "mysql:8.0.33", "mysql:5.7"]
3434
use-compression: [0]
3535
use-tls: [0]
3636
mysql_connection_url_key: [""]
3737
# TODO - add mariadb to the matrix. currently few tests are broken due to mariadb incompatibilities
3838
include:
3939
# 20.x
4040
- node-version: "20.x"
41-
mysql-version: "mysql:8.0.18"
41+
mysql-version: "mysql:8.0.33"
4242
use-compression: 1
4343
use-tls: 0
4444
use-builtin-test-runner: 1
4545
- node-version: "20.x"
46-
mysql-version: "mysql:8.0.18"
46+
mysql-version: "mysql:8.0.33"
4747
use-compression: 0
4848
use-tls: 1
4949
use-builtin-test-runner: 1

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
## Node MySQL 2
1+
## MySQL 2
22

33
[![Greenkeeper badge](https://badges.greenkeeper.io/sidorares/node-mysql2.svg)](https://greenkeeper.io/)
44
[![NPM Version][npm-image]][npm-url]

lib/commands/prepare.js

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,10 +73,19 @@ class Prepare extends Command {
7373
return Prepare.prototype.readField;
7474
}
7575
return this.prepareDone(connection);
76-
7776
}
7877

7978
readParameter(packet, connection) {
79+
// there might be scenarios when mysql server reports more parameters than
80+
// are actually present in the array of parameter definitions.
81+
// if EOF packet is received we switch to "read fields" state if there are
82+
// any fields reported by the server, otherwise we finish the command.
83+
if (packet.isEOF()) {
84+
if (this.fieldCount > 0) {
85+
return Prepare.prototype.readField;
86+
}
87+
return this.prepareDone(connection);
88+
}
8089
const def = new Packets.ColumnDefinition(packet, connection.clientEncoding);
8190
this.parameterDefinitions.push(def);
8291
if (this.parameterDefinitions.length === this.parameterCount) {
@@ -86,6 +95,9 @@ class Prepare extends Command {
8695
}
8796

8897
readField(packet, connection) {
98+
if (packet.isEOF()) {
99+
return this.prepareDone(connection);
100+
}
89101
const def = new Packets.ColumnDefinition(packet, connection.clientEncoding);
90102
this.fields.push(def);
91103
if (this.fields.length === this.fieldCount) {

lib/connection.js

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -489,14 +489,23 @@ class Connection extends EventEmitter {
489489
}
490490
this._bumpSequenceId(packet.numPackets);
491491
}
492-
const done = this._command.execute(packet, this);
493-
if (done) {
494-
this._command = this._commands.shift();
495-
if (this._command) {
496-
this.sequenceId = 0;
497-
this.compressedSequenceId = 0;
498-
this.handlePacket();
492+
try {
493+
if (this._fatalError) {
494+
// skip remaining packets after client is in the error state
495+
return;
496+
}
497+
const done = this._command.execute(packet, this);
498+
if (done) {
499+
this._command = this._commands.shift();
500+
if (this._command) {
501+
this.sequenceId = 0;
502+
this.compressedSequenceId = 0;
503+
this.handlePacket();
504+
}
499505
}
506+
} catch (err) {
507+
this._handleFatalError(err);
508+
this.stream.destroy();
500509
}
501510
}
502511

Lines changed: 130 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,147 @@
1-
import { describe, it } from 'node:test';
1+
import { describe, it, beforeEach, afterEach } from 'node:test';
22
import assert from 'node:assert';
3-
// import common from '../../common.js';
4-
5-
describe('Prepare result when CLIENT_OPTIONAL_RESULTSET_METADATA is set or metadata_follows is unset', () => {
6-
it('should not throw an exception', (t, done) => {
7-
assert(true);
8-
done(null);
9-
/*
10-
const connection = common.createConnection({
11-
database: 'mysql',
3+
import common from '../../common.js';
4+
import PrepareCommand from '../../../lib/commands/prepare.js';
5+
import packets from '../../../lib/packets/index.js';
6+
7+
describe(
8+
'Unit test - prepare result with number of parameters incorrectly reported by the server',
9+
{ timeout: 1000 },
10+
() => {
11+
it('should report 0 actual parameters when 1 placeholder is used in ORDER BY ?', (t, done) => {
12+
const connection = {
13+
constructor: {
14+
statementKey: () => 0,
15+
},
16+
_handshakePacket: {},
17+
_resetSequenceId: () => {},
18+
_statements: new Map(),
19+
serverEncoding: 'utf8',
20+
clientEncoding: 'utf8',
21+
config: {
22+
charsetNumber: 33,
23+
},
24+
writePacket: (packet) => {
25+
// client -> server COM_PREPARE
26+
assert.equal(
27+
packet.buffer.toString('hex'),
28+
'000000001673656c656374202a2066726f6d207573657273206f72646572206279203f'
29+
);
30+
},
31+
};
32+
const prepareCommand = new PrepareCommand(
33+
{ sql: 'select * from users order by ?' },
34+
(err, result) => {
35+
assert.equal(err, null);
36+
debugger;
37+
assert.equal(result.parameters.length, 0);
38+
assert.equal(result.columns.length, 51);
39+
assert.equal(result.id, 1);
40+
done(null);
41+
}
42+
);
43+
44+
prepareCommand.execute(null, connection);
45+
const headerPacket = new packets.Packet(
46+
0,
47+
Buffer.from('0000000000010000003300010000000005000002', 'hex'),
48+
0,
49+
20
50+
);
51+
prepareCommand.execute(headerPacket, connection);
52+
const paramsEofPacket = new packets.Packet(0, Buffer.from('00000000fe000002002b000004', 'hex'), 0, 11);
53+
prepareCommand.execute(paramsEofPacket, connection);
54+
for (let i = 0; i < 51; ++i) {
55+
const columnDefinitionPacket = new packets.Packet(
56+
0,
57+
Buffer.from(
58+
'0000000003646566056d7973716c0475736572047573657204486f737404486f73740ce000fc030000fe034000000005000005', 'hex'
59+
),
60+
0,
61+
47
62+
);
63+
prepareCommand.execute(columnDefinitionPacket, connection);
64+
}
65+
const columnsEofPacket = new packets.Packet(0, Buffer.from('00000000fe000002002b000004', 'hex'), 0, 11);
66+
prepareCommand.execute(columnsEofPacket, connection);
1267
});
68+
}
69+
);
70+
71+
describe('E2E Prepare result with number of parameters incorrectly reported by the server', { timeout: 1000 }, () => {
72+
let connection;
73+
74+
function isNewerThan8_0_22() {
75+
const { serverVersion } = connection._handshakePacket;
76+
const [major, minor, patch] = serverVersion.split('.').map((x) => parseInt(x, 10));
77+
if (major > 8) {
78+
return true;
79+
}
80+
if (major === 8 && minor > 0) {
81+
return true;
82+
}
83+
if (major === 8 && minor === 0 && patch >= 22) {
84+
return true;
85+
}
86+
return false;
87+
}
1388

89+
beforeEach((t, done) => {
90+
connection = common.createConnection({
91+
database: 'mysql',
92+
});
1493
connection.on('error', (err) => {
1594
done(err);
1695
});
96+
connection.on('connect', () => {
97+
done(null);
98+
});
99+
});
17100

101+
afterEach((t, done) => {
102+
connection.end(err => {
103+
done(null)
104+
});
105+
});
106+
107+
// see https://github.com/sidorares/node-mysql2/issues/2052#issuecomment-1620318928
108+
it('should report 0 actual parameters when 1 placeholder is used in ORDER BY ?', (t, done) => {
18109
connection.prepare('select * from user order by ?', (err, stmt) => {
19-
console.log(err);
20110
if (err) {
21111
if (!err.fatal) {
22112
connection.end();
23113
}
24114
done(err);
115+
} else {
116+
if(isNewerThan8_0_22()) {
117+
// mysql 8.0.22 and newer report 0 parameters
118+
assert.equal(stmt.parameters.length, 0);
119+
} else {
120+
// mariadb, mysql 8.0.21 and older report 1 parameter
121+
assert.equal(stmt.parameters.length, 1);
122+
}
123+
done(null);
124+
}
125+
});
126+
});
127+
128+
it('should report 1 actual parameters when 2 placeholders used in ORDER BY?', (t, done) => {
129+
connection.prepare('select * from user where user.User like ? order by ?', (err, stmt) => {
130+
if (err) {
131+
if (!err.fatal) {
132+
connection.end();
133+
}
134+
done(err);
135+
} else {
136+
if(isNewerThan8_0_22()) {
137+
// mysql 8.0.22 and newer report 1 parameter
138+
assert.equal(stmt.parameters.length, 1);
139+
} else {
140+
// mariadb, mysql 8.0.21 and older report 2 parameters
141+
assert.equal(stmt.parameters.length, 2);
142+
}
143+
done(null);
25144
}
26145
});
27-
*/
28146
});
29147
});

0 commit comments

Comments
 (0)