0%

The websockets Library, Without asyncio · practice

Read How a Connection Ended

The handler from Lesson 2 echoes correctly, and every conversation it serves still ends in a . Read the block Run printed: recv raised ConnectionClosedOK because the client closed first, nothing in echo_handler caught it, so the left the handler and the library logged it as connection handler failed. The connection itself closed cleanly, as the block’s last line said. A client that closes is the normal end of a conversation, so the handler should end its there. This lesson makes it do that, and then uses the same exceptions to report how a conversation ended.

OK and Error

Once a connection is closed, every later recv and send on it raises an exception from websockets.exceptions. Which one depends on how the connection ended:

  • ConnectionClosedOK when a close went each way and both carried 1000, 1001 or no code at all (an empty close, which is reported as 1005);

  • ConnectionClosedError in every other case: a close with any other code, such as 1008, or a connection that ended with no close frame at all.

That split is the library’s own choice of what counts as a normal ending, not a rule in RFC 6455. Both classes are kinds of ConnectionClosed, the name echo_library.py already imports. An except clause that names a class also catches every exception that is a kind of that class, so except ConnectionClosed: catches both.

The server closes with 1008. What does the client's next recv raise?

What arrived

Each of these exceptions carries the close frames. Name the exception in the except line to read them:

try:
    connection.recv(timeout=WAIT)
except ConnectionClosed as exc:
    print(exc.rcvd)

exc.rcvd is the close frame that arrived from the peer, with .code and .reason, and exc.sent is the one this side sent. Either can be None. When exc.rcvd is None, no close frame arrived: the ended without one. Chapter 5 reported that case as 1006, the code that is never sent and only ever reports a connection that ended without a close frame, and describe_close does the same.

So describe_close(exc) checks exc.rcvd is None first and then returns {"code": 1006, "reason": ""}. Otherwise it returns the code and reason of exc.rcvd in the same kind of . Use is None for the check, as you would for any None test.

Older code reads exc.code. It still works in the version you pinned, but it issues a DeprecationWarning that points to rcvd.code, so read the close frame through rcvd.

exc.rcvd is None. What happened?

Guard send too

recv is not the only call that can find the connection closed. The client can close after your recv returned and before your send runs; then send raises ConnectionClosedOK. So the try covers both lines of the loop body:

try:
    message = connection.recv(timeout=WAIT)
    connection.send(message)
except ConnectionClosed:
    return

The other ending is a client that says nothing. When no message arrives within WAIT, recv raises Python’s built-in TimeoutError. A client that has sent nothing for 2 seconds is not waiting for anything from this server, so close it yourself, with 1000 and a reason such as "no message arrived in time", and return. Give the try a second clause, except TimeoutError:, that does that.

Why does the try block cover send as well as recv?

Your task

In echo_library.py:

  1. write describe_close(exc): {"code": 1006, "reason": ""} when exc.rcvd is None, and otherwise the code and reason from exc.rcvd;

  2. finish echo_handler: inside the loop, put recv and send in a try; on ConnectionClosed, return; on TimeoutError, call connection.close(1000, "no message arrived in time") and return. The limit and the final close after MAX_MESSAGES stay as they are.

Run

Press Run. Standard error stays empty this time: no connection handler failed block. Three new sections connect with the library’s client, read until the connection is closed, and print the exception’s class name and what your describe_close made of it. A finished version, run on the site, printed:

Lesson 4: you close
ConnectionClosedOK, {'code': 1000, 'reason': 'done'}
Lesson 4: the server closes with 1008
ConnectionClosedError, {'code': 1008, 'reason': 'this path is closed'}
Lesson 4: the server vanishes
ConnectionClosedError, {'code': 1006, 'reason': ''}

In the first, the client closes with 1000 and done, and the server answers with the same close, so both frames carry 1000: OK. In the second, a library server’s handler closes at once with 1008, so the ending is an Error, and rcvd holds the server’s code and reason. In the third, a server built by hand from Chapter 2’s serve_handshake answers the upgrade with a 101 and then closes the TCP connection without a close frame: an Error with rcvd set to None, which your reports as 1006. Read ending and read_the_ending in library_demo.py to see how each is produced. Then press Submit.

Your handler is a handful of lines, and your client is a few more. The next lesson lays them next to the functions you wrote in Chapters 2 to 5 and names what the library does that those never did.

Task

In echo_library.py:

  1. Write describe_close(exc). When exc.rcvd is None, return {"code": 1006, "reason": ""}. Otherwise return {"code": exc.rcvd.code, "reason": exc.rcvd.reason}.

  2. Finish echo_handler. Inside the , put the recv and the send in one try. On ConnectionClosed, return. On TimeoutError, call connection.close(1000, "no message arrived in time") and return. Keep the limit and the close with 1000 after MAX_MESSAGES messages.

Press Run: no connection handler failed block appears, and the three Lesson 4 sections print ConnectionClosedOK, {'code': 1000, 'reason': 'done'}, ConnectionClosedError, {'code': 1008, 'reason': 'this path is closed'} and ConnectionClosedError, {'code': 1006, 'reason': ''}.

Then press Submit. The check builds the the library raises from the library’s own classes and passes them to describe_close: OK and Error endings with codes and reasons you have not seen, a close that arrived when none was sent back, a close frame sent when none arrived, and no close frame at all. Reading exc.code instead of exc.rcvd.code fails, because the check turns the library’s deprecation warning into an error. It gives echo_handler stand-in connections: a client that closes after two messages, a connection that ends with no close frame, a send that finds the connection closed, a client that sends nothing in time (a close with 1000 is expected, and no further recv), and ten messages followed by more. Each must end with the handler returning, not raising. Two except clauses, one for ConnectionClosedOK and one for ConnectionClosedError, are fine instead of except ConnectionClosed:.