Skip to content

Commit ed73cc5

Browse files
committed
handle connection close error message
A custom ERR_PACKET is sent by the server on MySQL 8.0.24 (and higher) when the server disconnects inactive clients. This causes some integration tests that destroy the socket connection to fail due to packet number and order mismatch. The patch introduces logic to handle this potential new and unexpected packet and report the custom server message to the application.
1 parent 9cc56fb commit ed73cc5

File tree

4 files changed

+90
-5
lines changed

4 files changed

+90
-5
lines changed

lib/connection.js

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
// This file was modified by Oracle on June 1, 2021.
2+
// The changes involve new logic to handle an additional ERR Packet sent by
3+
// the MySQL server when the connection is closed unexpectedly.
4+
// Modifications copyright (c) 2021, Oracle and/or its affiliates.
5+
16
'use strict';
27

38
const Net = require('net');
@@ -188,7 +193,7 @@ class Connection extends EventEmitter {
188193
if (this.connectTimeout) {
189194
Timers.clearTimeout(this.connectTimeout);
190195
this.connectTimeout = null;
191-
}
196+
}
192197
// prevent from emitting 'PROTOCOL_CONNECTION_LOST' after EPIPE or ECONNRESET
193198
if (this._fatalError) {
194199
return;
@@ -368,6 +373,14 @@ class Connection extends EventEmitter {
368373
}
369374

370375
protocolError(message, code) {
376+
// Starting with MySQL 8.0.24, if the client closes the connection
377+
// unexpectedly, the server will send a last ERR Packet, which we can
378+
// safely ignore.
379+
// https://dev.mysql.com/worklog/task/?id=12999
380+
if (this._closing) {
381+
return;
382+
}
383+
371384
const err = new Error(message);
372385
err.fatal = true;
373386
err.code = code || 'PROTOCOL_ERROR';
@@ -415,10 +428,18 @@ class Connection extends EventEmitter {
415428
}
416429
}
417430
if (!this._command) {
418-
this.protocolError(
419-
'Unexpected packet while no commands in the queue',
420-
'PROTOCOL_UNEXPECTED_PACKET'
421-
);
431+
const marker = packet.peekByte();
432+
// If it's an Err Packet, we should use it.
433+
if (marker === 0xff) {
434+
const error = Packets.Error.fromPacket(packet);
435+
this.protocolError(error.message, error.code);
436+
} else {
437+
// Otherwise, it means it's some other unexpected packet.
438+
this.protocolError(
439+
'Unexpected packet while no commands in the queue',
440+
'PROTOCOL_UNEXPECTED_PACKET'
441+
);
442+
}
422443
this.close();
423444
return;
424445
}

lib/constants/errors.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
// This file was modified by Oracle on June 1, 2021.
2+
// An entry was created for a new error reported by the MySQL server due to
3+
// client inactivity.
4+
// Modifications copyright (c) 2021, Oracle and/or its affiliates.
5+
16
'use strict';
27

38
// copy from https://raw.githubusercontent.com/mysqljs/mysql/7770ee5bb13260c56a160b91fe480d9165dbeeba/lib/protocol/constants/errors.js
@@ -994,6 +999,7 @@ exports.ER_INNODB_FT_AUX_NOT_HEX_ID = 1879;
994999
exports.ER_OLD_TEMPORALS_UPGRADED = 1880;
9951000
exports.ER_INNODB_FORCED_RECOVERY = 1881;
9961001
exports.ER_AES_INVALID_IV = 1882;
1002+
exports.ER_CLIENT_INTERACTION_TIMEOUT = 4031;
9971003

9981004
// Lookup-by-number table
9991005
exports[1] = 'EE_CANTCREATEFILE';
@@ -1982,3 +1988,4 @@ exports[1879] = 'ER_INNODB_FT_AUX_NOT_HEX_ID';
19821988
exports[1880] = 'ER_OLD_TEMPORALS_UPGRADED';
19831989
exports[1881] = 'ER_INNODB_FORCED_RECOVERY';
19841990
exports[1882] = 'ER_AES_INVALID_IV';
1991+
exports[4031] = 'ER_CLIENT_INTERACTION_TIMEOUT';

lib/packets/index.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
// This file was modified by Oracle on June 1, 2021.
2+
// A utility method was introduced to generate an Error instance from a
3+
// binary server packet.
4+
// Modifications copyright (c) 2021, Oracle and/or its affiliates.
5+
16
'use strict';
27

38
const process = require('process');
@@ -122,6 +127,20 @@ class Error {
122127
packet._name = 'Error';
123128
return packet;
124129
}
130+
131+
static fromPacket(packet) {
132+
packet.readInt8(); // marker
133+
const code = packet.readInt16();
134+
packet.readString(1, 'ascii'); // sql state marker
135+
// The SQL state of the ERR_Packet which is always 5 bytes long.
136+
// https://dev.mysql.com/doc/dev/mysql-server/8.0.11/page_protocol_basic_dt_strings.html#sect_protocol_basic_dt_string_fix
137+
packet.readString(5, 'ascii'); // sql state (ignore for now)
138+
const message = packet.readNullTerminatedString('utf8');
139+
const error = new Error();
140+
error.message = message;
141+
error.code = code;
142+
return error;
143+
}
125144
}
126145

127146
exports.Error = Error;

test/integration/test-server-close.js

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
// Copyright (c) 2021, Oracle and/or its affiliates.
2+
3+
'use strict';
4+
5+
const errors = require('../../lib/constants/errors');
6+
const common = require('../common');
7+
const connection = common.createConnection();
8+
const assert = require('assert');
9+
10+
const customWaitTimeout = 1; // seconds
11+
12+
let error;
13+
14+
connection.on('error', err => {
15+
error = err;
16+
17+
connection.close();
18+
});
19+
20+
connection.query(`set wait_timeout=${customWaitTimeout}`, () => {
21+
setTimeout(() => {}, customWaitTimeout * 1000 * 2);
22+
});
23+
24+
process.on('uncaughtException', err => {
25+
// The ERR Packet is only sent by MySQL server 8.0.24 or higher, so we
26+
// need to account for the fact it is not sent by older server versions.
27+
if (err.code !== 'ERR_ASSERTION') {
28+
throw err;
29+
}
30+
31+
assert.equal(error.message, 'Connection lost: The server closed the connection.');
32+
assert.equal(error.code, 'PROTOCOL_CONNECTION_LOST');
33+
});
34+
35+
process.on('exit', () => {
36+
assert.equal(error.message, 'The client was disconnected by the server because of inactivity. See wait_timeout and interactive_timeout for configuring this behavior.');
37+
assert.equal(error.code, errors.ER_CLIENT_INTERACTION_TIMEOUT);
38+
});

0 commit comments

Comments
 (0)