0%

A Conversation on Loopback · practice

Fail the Connection

Two endings work now: either side can close, and both report 1000. The rude client in Run still gets the old treatment. After Lesson 2 its lines read:

A rude client:
client: the server closed the connection without a close frame
server: stopped by ValueError: (1002, 'the frame breaks the masking rule')

The codec did its job: it refused an unmasked and said why, with the code 1002. But the went straight up and out of serve_conversation, ended the server’s thread, and the client never learned what it did wrong. This lesson turns that exception into a close frame.

Closing versus failing

A close is a conversation both sides finish together. RFC 6455 section 7.1.7 describes the other ending, failing the connection, for when a peer has broken the protocol. The endpoint that notices “SHOULD send a Close frame with an appropriate ”, “MAY omit” it when it believes the peer is unlikely to read it, and must stop processing what the peer sends, including a close sent in answer.

So a failing server does three things: it sends one close frame carrying the code for the broken rule, it reads nothing more, and it returns. This course always sends that close when the still works, because the code tells the peer, and you, exactly which rule was broken.

Codes from the codec

Since Chapter 4’s project, every refusal in read_message is a ValueError(code, reason). The code is exc.args[0] and the reason exc.args[1]:

  • 1002, protocol error: an unmasked frame to a server, reserved bits or , a control frame that is too long or not final, a that may not travel;

  • 1003, a message this codec cannot take: any fragment, because the learning codec does not reassemble fragmented messages;

  • 1007, a text message or close reason that is not valid ;

  • 1009, message too big: a frame whose header announces more than MAX_SIZE bytes, refused before a single payload byte is read.

failure_frame(exc) builds the close from those two values: ws_frames.close_payload(exc.args[0], exc.args[1]) in an unmasked close frame. For ValueError(1009, "x") that is:

88 03 03 f1 78

03 f1 is 1009, and 78 is “x”.

A masked binary frame announces 2,000,000 bytes, over MAX_SIZE. What does the server send?

A peer that vanishes

Two other exceptions can end a read, and neither is a broken rule:

  • ConnectionError: read_exact got b"" because the peer closed the without a close frame, as Lesson 1’s client did;

  • TimeoutError: the peer sent nothing for 2 seconds, the timeout the wiring sets on every accepted socket.

In both cases there is no one to send a close to, or no reason to believe a close would be read. The server sends nothing and reports the code 1006, which RFC 6455 reserves for exactly this: the connection ended without a close frame. 1006 only ever appears in reports. Like 1005, it may never travel in a frame.

The client's connection drops in the middle of a conversation. What does the server send?

Your task

Write failure_frame(exc). Then wrap the of serve_conversation, the closing steps after it included, in try, with two handlers:

def serve_conversation(sock, read_exact, *, max_messages=MAX_MESSAGES):
    count = 0
    try:
        ...  # Lesson 2's loop and closing steps, unchanged
    except ValueError as exc:
        sock.sendall(failure_frame(exc))
        return {"messages": count, "code": exc.args[0]}
    except (ConnectionError, TimeoutError):
        return {"messages": count, "code": 1006}

count is whatever name your loop uses for the messages echoed so far. Two separate except ConnectionError and except TimeoutError clauses work just as well. Keep the handlers to these exception : a broad except Exception would treat a broken rule like a vanished peer and send no close at all.

Why not wait

After a close in Lesson 2, the server waited for the answer. After a failure, it does not. Waiting would mean trusting the peer one more time. A client that sent an unmasked frame may be a broken program or something that is not a client at all, and what it sends next could be another bad frame, a close, or nothing; in the last case the server would sit out its 2-second timeout with its thread held for no reason. Sending the close and returning at once gives the peer the one thing it needs, the code, and ends the server’s part.

Why does the server not wait for the client's close after failing the connection?

Run

Press Run. After this lesson, the rude client’s lines become:

A rude client:
client: {'type': 'close', 'code': 1002, 'reason': 'the frame breaks the masking rule'}
server: {'messages': 0, 'code': 1002}
server thread still running: False

The rude client now reads a close frame that names its mistake, and the server’s thread ends with a result instead of an exception. The first conversation still reports 1000 on both sides.

Then press Submit. It feeds serve_conversation frames that break each rule above, a header that announces too many bytes with nothing behind it, and clients that vanish or go silent after one message, and checks the close you send, the code you return, and that nothing is read after a failure.

Your server now has an answer for every way a conversation can end. So far you have seen that in Run and Submit has checked it; the next lesson writes tests of your own that prove it, over two connected sockets with no at all.

Task

Write one and extend one in loopback_demo.py.

  1. failure_frame(exc): return the unmasked close for a ValueError(code, reason): ws_frames.close_payload(exc.args[0], exc.args[1]) inside ws_frames.encode_frame(ws_frames.OP_CLOSE, ...).

  2. serve_conversation: wrap the and the closing steps after it in try. On ValueError as exc, send failure_frame(exc) and return {"messages": n, "code": exc.args[0]} at once, without reading again. On ConnectionError or TimeoutError, send nothing and return {"messages": n, "code": 1006}. Catch only these , not Exception.

Press Run: the rude client should now receive {'type': 'close', 'code': 1002, ...}, and the first conversation should still report 1000 on both sides.

Then press Submit. The check calls failure_frame directly, and gives serve_conversation stand-in clients that send an unmasked text, a text that is not , a first fragment, and a header announcing 1,000,001 bytes with no payload behind it, each delivered whole, one byte at a time and two bytes at a time. Each must get one close with 1002, 1007, 1003 or 1009, and the stand-in goes quiet afterwards, so a server that reads on, or reads the oversized payload, waits forever and fails. Clients that close their connection or go silent after one text must get their echo and nothing else, and a result with 1006. A clean close must still work as in Lesson 2.