Masks and Control Frames · practice
Say Why You Are Closing
Your decoder now recognizes a close
What a close payload holds
RFC 6455 section 5.5.1 allows two shapes. The payload is either empty, or it starts with a 2-byte unsigned code in
88 02 03 e8
88 06 03 e8 64 6f 6e 65
88 is FIN plus 03 e8 is 1000 as a big-endian 16-bit number, which struct.pack("!H", 1000) builds and struct.unpack("!H", ...)[0] reads back, exactly as in Chapter 3’s 16-bit length. 64 6f 6e 65 is “done” in UTF-8. A close is a control frame, so its whole payload is at most 125 bytes, and the reason gets what the code leaves: at most 123 bytes.
What does this print?
import struct
packed = struct.pack("!H", 1001).hex(" ")
number = struct.unpack("!H", bytes([0x03, 0xF1]))[0]
print(packed, number)
The codes this course uses
Section 7.4.1 of the RFC defines what the codes in the 1000
| Code | Meaning | Sent in a close frame? |
|---|---|---|
| 1000 | normal closure: the conversation is done | yes |
| 1001 | going away, such as a server shutting down | yes |
| 1002 | protocol error: the peer broke a framing rule | yes |
| 1003 | unsupported data: the endpoint cannot accept this kind of data | yes |
| 1005 | no | never; reported by a receiver |
| 1006 | the connection ended without any close frame | never; reported by a receiver |
| 1007 | invalid payload data, such as a text message that is not UTF-8 | yes |
| 1008 | policy violation, the general refusal when no other code fits | yes |
| 1009 | message too big | yes |
| 1011 | the server hit an unexpected | yes |
1005 and 1006 are different from the rest. They describe what a receiver saw, and they exist so that a program can report “the peer sent an empty close” or “the connection just ended” with a number like every other ending. They never travel in a close frame.
Codes that may not travel
The RFC’s table is not the whole
below 1000: not used;
1004: reserved;
1005, 1006 and 1015: labels an endpoint reports about itself, never sent (1015 means the
failed); 1016 to 2999: not assigned to anyone;
3000 to 3999: registered by libraries and frameworks;
4000 to 4999: for an application’s private use.
That leaves 1000 to 1003, 1007 to 1014, and 3000 to 4999 as the codes that may appear in a close frame. This set was checked, when this course was written, against the check the websockets library (the one you use from Chapter 6) applies to close codes, and the two agree on every code; it was not checked against the live registry page.
A server closes with 1012 (service restart). Does parse_close_payload accept it?
What is the longest reason a close frame can carry?
Reading a close
parse_close_payload turns the payload back into {"code": ..., "reason": ...}, and it has four cases to tell apart:
Empty. Valid. No code was present, so report 1005 with the reason
"". This is the receiver’s label at work: the peer did not send 1005, it sent nothing.One byte. Invalid: there is no room for a 2-byte code.
A code that may not travel, such as 1004 or 1005. Invalid: the peer broke the rules.
A reason that is not UTF-8. Invalid.
.decode("utf-8")raisesUnicodeDecodeErroron such bytes, and you can let it go:UnicodeDecodeErroris a kind of, so a caller that catches ValueErrorcatches it too.
try:
bytes.fromhex("c3 28").decode("utf-8")
except ValueError:
print("a UnicodeDecodeError, caught as a ValueError")
The same checks protect the other direction. close_payload(code, reason="") refuses to build a payload with a code that may not be sent, or with a reason that makes the payload longer than 125 bytes, counted after encoding.
A peer's close frame has an empty payload. What code does parse_close_payload report?
Your task
In ws_frames.py, write the three Lesson 4
close_code_allowed(code):Truefor 1000 to 1003, 1007 to 1014 and 3000 to 4999,Falsefor every other code.close_payload(code, reason=""): the code as 2 bytes in network byte order, then the reason in UTF-8. RaiseValueErrorfor a codeclose_code_allowedrefuses, and for a payload over 125 bytes.parse_close_payload(payload):{"code": 1005, "reason": ""}for an empty payload; otherwise the 2-byte code and the reason decoded as UTF-8. RaiseValueErrorfor a one-byte payload and for a code that may not travel; a reason that is not UTF-8 may raiseUnicodeDecodeError.
Press Run. The first Lesson 4 section builds a close 1000 “done” payload and reads it back. The second tries the same with 1005, and show prints the ValueError your close_payload raises; the message is yours, so yours may read differently from this one:
Lesson 4: close 1000 and back
{'payload': '03 e8 64 6f 6e 65', 'parsed': {'code': 1000, 'reason': 'done'}}
Lesson 4: close 1005, which no one may send
ValueError: close code 1005 may not be sent
Lesson 6: what a server reads
not written yet
Then press Submit. The check asks close_code_allowed about every code from 0 to 5,000. It builds payloads for the RFC’s codes and for a code and reason you have not seen, including one with a reason of exactly 123 bytes and one a byte longer, and expects ValueError for codes that may not be sent. It reads payloads with codes from the registry and reasons in UTF-8, and expects ValueError for a one-byte payload, codes that may not travel, and a reason that is not UTF-8. Every frame so far has carried a whole message; the next lesson looks at messages that arrive in pieces.
Task
Write the three Lesson 4 ws_frames.py:
close_code_allowed(code):Truefor 1000 to 1003, 1007 to 1014 and 3000 to 4999;Falsefor every other code.close_payload(code, reason=""): the code as 2 bytes in, then the reason encoded as . Raise for a code close_code_allowedrefuses, and for a payload longer than 125 bytes (a reason of at most 123 bytes after encoding).parse_close_payload(payload):{"code": 1005, "reason": ""}for an empty payload; otherwise{"code": ..., "reason": ...}with the 2-byte big-endian code and the rest decoded as UTF-8. RaiseValueErrorfor a one-byte payload and for a code that may not travel. A reason that is not UTF-8 may raiseUnicodeDecodeError, which is aValueError.
Press Run: the Lesson 4 sections should print {'payload': '03 e8 64 6f 6e 65', 'parsed': {'code': 1000, 'reason': 'done'}} and then your ValueError for 1005. Then press Submit. The check compares close_code_allowed with the rule for every code from 0 to 5,000, builds the RFC’s payloads and an unseen one with a check-mark reason, accepts a 123-byte reason and refuses a 124-byte one, refuses codes such as 1004, 1005, 1006, 1015, 999, 2999 and 5000 in both directions, reads unseen codes 1012, 1014, 3000 and 4999 with UTF-8 reasons, and refuses a one-byte payload and a reason that is not UTF-8. Your result