TestingAPIs is still being built. If something breaks, email support@testingapis.com.

Closing and close codes

1000, 1006, 1009 and the rest, and who gets the last word.

An HTTP request ends when the response is complete. A WebSocket ends when someone decides to end it, or when the network decides for them. How it ends is part of the contract between client and server, and it's the part most often left untested. This lesson covers the close handshake, the codes, and what this API actually does with them.

The close handshake

Either side can start closing. It sends a close frame with a numeric code and an optional short reason. The other side replies with its own close frame, and then the TCP connection is shut. In the browser and Node API, you start it with ws.close(code, reason), and both sides find out it's over through the close event, which carries code, reason and wasClean.

On this API, closing isn't instant. Whether the client called close() or sent the closemessage, the close event arrived between 1,030 and 1,070 milliseconds later, every time it was measured. A test that closes a socket and then checks within half a second that it's closed will fail here, even though nothing is wrong. Give closing its own timeout, generous enough for the slowest environment you run in.

Close codes

Codes from 1000 to 2999 are defined by the protocol. 3000 to 3999 are registered for libraries and frameworks. 4000 to 4999 are free for applications to use however they like. The ones you'll meet most:

CodeNameMeaning
1000Normal closureFinished on purpose. The only code that means everything went to plan.
1001Going awayThe server is shutting down, or the browser is leaving the page.
1002Protocol errorThe other side broke the WebSocket rules.
1003Unsupported dataA kind of message the endpoint can't accept, such as binary to a text-only socket.
1005No status receivedReported when a close frame had no code. Never sent on the wire.
1006Abnormal closureThe connection ended without any close frame. Never sent on the wire.
1007Invalid payloadA text message that wasn't valid UTF-8, for example.
1008Policy violationA rule was broken. Often used for authentication failures.
1009Message too bigA message exceeded the endpoint's limit.
1011Internal errorThe server hit an unexpected problem.
1012Service restartThe server is restarting. Reconnecting soon should work.
1013Try again laterThe server is overloaded.
4000–4999Application codesWhatever the application defines, such as 4401 for an expired token.

1006 is the one to understand

Nobody sends 1006. A client reports it when the connection disappeared without a close frame: the handshake failed, the network dropped, a proxy timed out, the server process crashed, or the laptop lid closed. wasClean is false and the reason is empty. You saw it in the handshake lesson, where every failed handshake ended this way, because there was never a WebSocket to send a close frame on.

From the client's side, 1006 says "something went wrong and I don't know what". That makes it the code that most needs a sensible client response, usually reconnecting, and the one that should never be shown to users as a number.

What this API sends

Every way a connection ended during these lessons:

What happenedCode and reason the client saw
Sent close to the echo socket1000 "Bye!"
The ticker sent its last tick1000 "Ticker finished"
Sent a message over 64KB1009 "Message exceeds 64KB"
Client called close(), with any code1000 "Closed by client"
Handshake failed (bad parameter, wrong path, subprotocol)1006 ""
Connection dropped mid-stream1006 ""

Each server-initiated close has a specific reason, which is good practice and makes logs readable. The fourth row has a subtlety. When the client closes, the code the client sees is the one in the server's reply, not the one it sent:

JavaScript
ws.close(4000, "custom reason");

// close event: code 1000, reason "Closed by client"

The RFC says the replying side typically echoes the code it received. This server doesn't; it always answers 1000 with its own reason. That's allowed, but a client that closes with 4000 and then checks for 4000 in its own close handler, to know it was the one who closed, will be wrong on this server. Keep a flag in your own code instead of relying on the code coming back.

What a client is allowed to send

Browsers and Node only let application code close with 1000 or a code from 3000 to 4999, and the reason can be at most 123 bytes:

JavaScript
ws.close(1001);                 // InvalidAccessError: invalid code
ws.close(1011);                 // InvalidAccessError: invalid code
ws.close(4000, "r".repeat(124)); // SyntaxError: Reason must be less than 123 bytes; received 124

So a client can't send 1001 or 1011 itself. If a spec for the client says "close with 1001 when the user logs out", it can't be implemented in a browser, and that's better found at the design stage than in testing. (The error message says "less than 123", but exactly 123 bytes works.)

Testing the client's side

The server's close codes only matter if the client acts on them. A reasonable close handler looks something like this:

JavaScript
ws.addEventListener("close", (event) => {
  if (event.code === 1000) return;                       // finished normally
  if (event.code === 1008 || event.code === 4401) {       // policy, or an app-defined "unauthorised"
    showSignIn();
    return;
  }
  if (event.code === 1009) {
    reportBug("message too big");
    return;
  }
  scheduleReconnect();                                    // 1006, 1001, 1011, 1012, 1013 ...
});

Each branch is a test case. Make the server close with each code the client distinguishes, and check the client does the right thing: no reconnect after 1000, a sign-in prompt for an auth failure, a reconnect for 1006. The bugs you find are usually in two places. A client that reconnects after 1000 keeps reconnecting forever, because the server keeps finishing normally. A client that only reconnects after 1006 sits dead after 1012, which is exactly what a server sends during a deployment.

What to assert when a connection ends

  1. It ended when it should have: after the last message, not before, and not never.
  2. The code is the documented one, and the reason if the API documents reasons.
  3. wasClean is true for every close that was meant to happen.
  4. No messages arrived after the close started.
  5. The client did what it should next: stopped, signed in, or reconnected.

Exercise

Connect to the echo socket and send a message one byte over the limit. Record the code, reason and wasClean. Then connect again, call close(4321, "exercise"), and record the same three values. Explain why the code you sent isn't the code you received, and say how a client could still tell that it was the one who closed the connection.

Practise and go further