Unverified Commit 17cc1b9c authored by Robert Knight's avatar Robert Knight Committed by GitHub

Merge pull request #1941 from hypothesis/fix-reconnect-on-navigate

Fix spurious WebSocket disconnection errors
parents eb404698 1a0ebb8b
import Socket from '../websocket'; import Socket, {
CLOSE_NORMAL,
CLOSE_GOING_AWAY,
CLOSE_ABNORMAL,
} from '../websocket';
describe('websocket wrapper', function() { describe('websocket wrapper', function() {
let fakeSocket; let fakeSocket;
...@@ -28,49 +32,57 @@ describe('websocket wrapper', function() { ...@@ -28,49 +32,57 @@ describe('websocket wrapper', function() {
console.warn.restore(); console.warn.restore();
}); });
it('should reconnect after an abnormal disconnection', function() { context('when the connection is closed by the browser or server', () => {
new Socket('ws://test:1234'); it('should reconnect after an abnormal disconnection', function() {
assert.ok(fakeSocket); new Socket('ws://test:1234');
const initialSocket = fakeSocket; assert.ok(fakeSocket);
fakeSocket.onopen({}); const initialSocket = fakeSocket;
fakeSocket.onclose({ code: 1006 }); fakeSocket.onopen({});
clock.tick(2000); fakeSocket.onclose({ code: CLOSE_ABNORMAL });
assert.ok(fakeSocket); clock.tick(2000);
assert.notEqual(fakeSocket, initialSocket); assert.ok(fakeSocket);
}); assert.notEqual(fakeSocket, initialSocket);
});
it('should reconnect if initial connection fails', function() { it('should reconnect if initial connection fails', function() {
new Socket('ws://test:1234'); new Socket('ws://test:1234');
assert.ok(fakeSocket); assert.ok(fakeSocket);
const initialSocket = fakeSocket; const initialSocket = fakeSocket;
fakeSocket.onopen({}); fakeSocket.onopen({});
fakeSocket.onclose({ code: 1006 }); fakeSocket.onclose({ code: CLOSE_ABNORMAL });
clock.tick(4000); clock.tick(4000);
assert.ok(fakeSocket); assert.ok(fakeSocket);
assert.notEqual(fakeSocket, initialSocket); assert.notEqual(fakeSocket, initialSocket);
}); });
it('should send queued messages after a reconnect', function() { it('should send queued messages after a reconnect', function() {
// simulate WebSocket setup and initial connection // simulate WebSocket setup and initial connection
const socket = new Socket('ws://test:1234'); const socket = new Socket('ws://test:1234');
fakeSocket.onopen({}); fakeSocket.onopen({});
// simulate abnormal disconnection // simulate abnormal disconnection
fakeSocket.onclose({ code: 1006 }); fakeSocket.onclose({ code: CLOSE_ABNORMAL });
// enqueue a message and check that it is sent after the WS reconnects // enqueue a message and check that it is sent after the WS reconnects
socket.send({ aKey: 'aValue' }); socket.send({ aKey: 'aValue' });
fakeSocket.onopen({}); fakeSocket.onopen({});
assert.calledWith(fakeSocket.send, '{"aKey":"aValue"}'); assert.calledWith(fakeSocket.send, '{"aKey":"aValue"}');
}); });
it('should not reconnect after a normal disconnection', function() { [CLOSE_NORMAL, CLOSE_GOING_AWAY].forEach(closeCode => {
const socket = new Socket('ws://test:1234'); it('should not reconnect after a normal disconnection', function() {
socket.close(); new Socket('ws://test:1234');
assert.called(fakeSocket.close); assert.ok(fakeSocket);
const initialSocket = fakeSocket; const initialSocket = fakeSocket;
clock.tick(2000);
assert.equal(fakeSocket, initialSocket); fakeSocket.onopen({});
fakeSocket.onclose({ code: closeCode });
clock.tick(4000);
assert.ok(fakeSocket);
assert.equal(fakeSocket, initialSocket);
});
});
}); });
it('should queue messages sent prior to connection', function() { it('should queue messages sent prior to connection', function() {
...@@ -87,4 +99,22 @@ describe('websocket wrapper', function() { ...@@ -87,4 +99,22 @@ describe('websocket wrapper', function() {
socket.send({ abc: 'foo' }); socket.send({ abc: 'foo' });
assert.calledWith(fakeSocket.send, '{"abc":"foo"}'); assert.calledWith(fakeSocket.send, '{"abc":"foo"}');
}); });
describe('#close', () => {
it('should close the socket with a normal status', () => {
const socket = new Socket('ws://test:1234');
socket.close();
assert.calledWith(fakeSocket.close, CLOSE_NORMAL);
});
it('should not reconnect after closing', () => {
const socket = new Socket('ws://test:1234');
const initialSocket = fakeSocket;
socket.close();
clock.tick(2000);
assert.equal(fakeSocket, initialSocket);
});
});
}); });
import retry from 'retry'; import retry from 'retry';
import EventEmitter from 'tiny-emitter'; import EventEmitter from 'tiny-emitter';
// see https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent // Status codes indicating the reason why a WebSocket connection closed.
const CLOSE_NORMAL = 1000; // See https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent and
// https://tools.ietf.org/html/rfc6455#section-7.4.
// "Normal" closures.
export const CLOSE_NORMAL = 1000;
export const CLOSE_GOING_AWAY = 1001;
// "Abnormal" closures.
export const CLOSE_ABNORMAL = 1006;
// There are other possible close status codes not listed here. They are all
// considered abnormal closures.
// Minimum delay, in ms, before reconnecting after an abnormal connection close. // Minimum delay, in ms, before reconnecting after an abnormal connection close.
const RECONNECT_MIN_DELAY = 1000; const RECONNECT_MIN_DELAY = 1000;
...@@ -60,7 +71,7 @@ export default class Socket extends EventEmitter { ...@@ -60,7 +71,7 @@ export default class Socket extends EventEmitter {
self.emit('open', event); self.emit('open', event);
}; };
socket.onclose = function(event) { socket.onclose = function(event) {
if (event.code === CLOSE_NORMAL) { if (event.code === CLOSE_NORMAL || event.code === CLOSE_GOING_AWAY) {
self.emit('close', event); self.emit('close', event);
return; return;
} }
...@@ -109,7 +120,20 @@ export default class Socket extends EventEmitter { ...@@ -109,7 +120,20 @@ export default class Socket extends EventEmitter {
/** Close the underlying WebSocket connection */ /** Close the underlying WebSocket connection */
this.close = function() { this.close = function() {
socket.close(); // nb. Always sent a status code in the `close()` call to work around
// a problem in the backend's ws4py library.
//
// If no status code is provided in the `close()` call, the browser will
// send a close frame with no payload, which is allowed by the spec.
// ws4py however, will respond by sending back a close frame with a 1005
// status code, which is not allowed by the spec. What ws4py should do in
// that scenario is send back a close frame with no payload itself. This
// invalid close frame causes browsers to report an abnormal WS
// termination, even though nothing really went wrong.
//
// To avoid the problem, we just explicitly send a "closed normally"
// status code here and ws4py will respond with the same status.
socket.close(CLOSE_NORMAL);
}; };
/** /**
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment