Core Question
Core Question
Before any header semantics or routing, what shape does raw HTTP text have to match, and what breaks that shape?
Outcome
Outcome
By the end of this session, the learner should be able to:
- name the four
ParseOutcomevalues and which check produces each one - explain why the request line is checked before any header line
- explain why a trailing
\r\n\r\nproduces two empty strings, not one, after splitting - state which HTTP versions this toy accepts and why the rest are rejected outright
Read Order
Read Order
- Read
SUPPORTED_VERSIONSandParseOutcome - Read
RequestandParseResult - Read
_parse_request_line() - Read
_parse_header_line() - Read
parse_request() - Run
examples/http-quic/session_01_walkthrough.py
Read It Like Code
Read It Like Code
Request(
method,
target,
version,
headers,
)Fields That Matter
Fields That Matter
| Field | Why it matters |
|---|---|
method | The first token of the request line. Not validated against a known verb list here — any non-empty token passes. |
target | The second token. Path, query string, or * — this layer does not interpret it. |
version | The third token. Gates everything after it: an unsupported version stops parsing before headers are even read. |
headers | A tuple of (name, value) pairs, in arrival order, duplicates and all. Nothing is deduplicated or folded at this layer. |
Decision Flow
Decision Flow
lines has 0 lines after stripping trailing blanks -> BadRequestLine request line does not split into exactly 3 tokens -> BadRequestLine any of method/target/version is empty -> BadRequestLine version not in SUPPORTED_VERSIONS -> UnsupportedVersion any remaining line has no ":" -> BadHeaderLine otherwise -> Ok
Reading Lens
Reading Lens
The important move in this session is to stop thinking of an HTTP request as "a URL with some headers" and start asking:
- what are the exact three tokens on the request line, and did the split produce exactly three?
- how many trailing empty strings does
raw.split("\r\n")leave behind, and why does a singleifundercount them? - which outcome does the function return the moment it finds a problem, and what does it skip checking after that?
Toy Model Boundary
Toy Model Boundary
Real HTTP/1.1 parsing handles obs-fold (header values continued on the next line with leading whitespace), chunked and content-length bodies, and a request line grammar defined by ABNF rather than a plain split(" "). This lesson keeps parsing to request-line-plus-headers only — there is no body handling anywhere in parse.py. _parse_request_line() also does not reject method or target strings containing spaces beyond the split, and does not validate target as a well-formed request-target; the reading target is the three-outcome branching, not full RFC 9112 conformance.
Code Landmarks
Code Landmarks
SUPPORTED_VERSIONS
A two-element tuple, ("HTTP/1.0", "HTTP/1.1"). HTTP/2.0 and HTTP/0.9 are both rejected the same way, by the same membership test — the version string is compared literally, not parsed into major/minor numbers.
parse_request() trailing-blank stripping
while lines and lines[-1] == "":
lines = lines[:-1]This is a while, not an if. A request ending in \r\n\r\n splits into a trailing empty string for the blank header/body separator line, *and* another trailing empty string from the final \r\n itself — two empties, not one. A single if here was a real bug caught in verification: it left one stray empty string in lines, which _parse_header_line() then rejected as BadHeaderLine because an empty string contains no :.
_parse_request_line()
Returns None — not an outcome — on any structural failure. parse_request() is the only place that turns None into ParseOutcome.BAD_REQUEST_LINE. Note the emptiness check runs after the length check: three empty-string tokens (e.g. from " ") would still fail on not method.
_parse_header_line()
":" not in line is checked before partition. partition always succeeds (it returns (line, "", "") if there's no separator), so the explicit membership check is what actually catches the missing-colon case — the _, _, _ shape of partition's result alone would not.
parse_request() main loop
Header parsing happens in lines[1:] — the request line itself, lines[0], is never re-examined as a header. The loop returns ParseOutcome.BAD_HEADER_LINE on the *first* bad line, so a request with a good header, then a bad one, then another good one still reports only BadHeaderLine and constructs no Request.
Failure Questions
Failure Questions
Use the source file to answer these:
- Why does
"GET /index.html\r\nHost: example.com\r\n\r\n"come backBAD_REQUEST_LINErather thanBAD_HEADER_LINE, even though it has a well-formed header line? - A raw string ends in exactly one
\r\n\r\n. How many empty strings doesraw.split("\r\n")leave at the end oflines, and which line of code removes all of them? - Why is
version not in SUPPORTED_VERSIONSchecked before the header loop runs at all, instead of after headers are parsed? - If
rawis"GET / HTTP/1.1\r\nHost: example.com\r\nBad Line\r\nX: y\r\n\r\n", does parsing report anything about theX: yline? Why or why not? - What does
parse_request("")return, and which line inparse_request()produces that result?
Walkthrough
Walkthrough
Run this:
PYTHONPATH=src python3 examples/http-quic/session_01_walkthrough.py
The walkthrough shows a good request parsing cleanly, three distinct kinds of malformed input each producing their own ParseOutcome, and confirms that trailing blank lines beyond the normal separator do not break parsing.
Done When
Done When
The learner can say all of the following without looking at notes:
- "The request line is checked, then the version, then headers — in that order, and each stage can stop parsing before the next begins."
- "Splitting on
\r\nafter a\r\n\r\nterminator leaves two trailing empty strings, not one, which is why the stripping loop must be awhile." - "An unsupported version is rejected before a single header line is looked at."
References
References
- RFC 9112 (HTTP/1.1 Message Syntax and Routing)
Continue