Tachyon /Users/simon/Documents/Codex/2026-10-12/i-need-a-python-3-15/.venv/lib/python3.15/site-packages/h11/_readers.py
1
Self Samples
4
Cumulative
1
Lines Hit
0.01%
% of Total
1
Max Self
4
Max Total
Intensity:
Cold Hot
Self Time
Total Time
Show All
Hot Only
Heat
Specialization
Line
Self
Total
Code
1
# Code to read HTTP data
2
#
3
# Strategy: each reader is a callable which takes a ReceiveBuffer object, and
4
# either:
5
# 1) consumes some of it and returns an Event
6
# 2) raises a LocalProtocolError (for consistency -- e.g. we call validate()
7
# and it might raise a LocalProtocolError, so simpler just to always use
8
# this)
9
# 3) returns None, meaning "I need more data"
10
#
11
# If they have a .read_eof attribute, then this will be called if an EOF is
12
# received -- but this is optional. Either way, the actual ConnectionClosed
13
# event will be generated afterwards.
14
#
15
# READERS is a dict describing how to pick a reader. It maps states to either:
16
# - a reader
17
# - or, for body readers, a dict of per-framing reader factories
18
19
import re
20
from typing import Any, Callable, Dict, Iterable, NoReturn, Optional, Tuple, Type, Union
21
22
from ._abnf import chunk_header, header_field, request_line, status_line
23
from ._events import Data, EndOfMessage, InformationalResponse, Request, Response
24
from ._receivebuffer import ReceiveBuffer
25
from ._state import (
26
CLIENT,
27
CLOSED,
28
DONE,
29
IDLE,
30
MUST_CLOSE,
31
SEND_BODY,
32
SEND_RESPONSE,
33
SERVER,
34
)
35
from ._util import LocalProtocolError, RemoteProtocolError, Sentinel, validate
36
37
__all__ = ["READERS"]
38
39
header_field_re = re.compile(header_field.encode("ascii"))
40
obs_fold_re = re.compile(rb"[ \t]+")
41
42
43
def _obsolete_line_fold(lines: Iterable[bytes]) -> Iterable[bytes]:
44
it = iter(lines)
45
last: Optional[bytes] = None
46
for line in it:
47
match = obs_fold_re.match(line)
48
if match:
49
if last is None:
50
raise LocalProtocolError("continuation line at start of headers")
51
if not isinstance(last, bytearray):
52
# Cast to a mutable type, avoiding copy on append to ensure O(n) time
53
last = bytearray(last)
54
last += b" "
55
last += line[match.end() :]
56
else:
57
if last is not None:
58
yield last
59
last = line
60
if last is not None:
61
yield last
62
63
64
def _decode_header_lines(
65
lines: Iterable[bytes],
66
) -> Iterable[Tuple[bytes, bytes]]:
67
for line in _obsolete_line_fold(lines):
68
matches = validate(header_field_re, line, "illegal header line: {!r}", line)
69
yield (matches["field_name"], matches["field_value"])
70
71
72
request_line_re = re.compile(request_line.encode("ascii"))
73
74
75
def maybe_read_from_IDLE_client(buf: ReceiveBuffer) -> Optional[Request]:
76
lines = buf.maybe_extract_lines()
77
if lines is None:
78
if buf.is_next_line_obviously_invalid_request_line():
79
raise LocalProtocolError("illegal request line")
80
return None
81
if not lines:
82
raise LocalProtocolError("no request line received")
83
matches = validate(
84
request_line_re, lines[0], "illegal request line: {!r}", lines[0]
85
)
86
1
4
return Request(
87
headers=list(_decode_header_lines(lines[1:])), _parsed=True, **matches
88
)
89
90
91
status_line_re = re.compile(status_line.encode("ascii"))
92
93
94
def maybe_read_from_SEND_RESPONSE_server(
95
buf: ReceiveBuffer,
96
) -> Union[InformationalResponse, Response, None]:
97
lines = buf.maybe_extract_lines()
98
if lines is None:
99
if buf.is_next_line_obviously_invalid_request_line():
100
raise LocalProtocolError("illegal request line")
101
return None
102
if not lines:
103
raise LocalProtocolError("no response line received")
104
matches = validate(status_line_re, lines[0], "illegal status line: {!r}", lines[0])
105
http_version = (
106
b"1.1" if matches["http_version"] is None else matches["http_version"]
107
)
108
reason = b"" if matches["reason"] is None else matches["reason"]
109
status_code = int(matches["status_code"])
110
class_: Union[Type[InformationalResponse], Type[Response]] = (
111
InformationalResponse if status_code < 200 else Response
112
)
113
return class_(
114
headers=list(_decode_header_lines(lines[1:])),
115
_parsed=True,
116
status_code=status_code,
117
reason=reason,
118
http_version=http_version,
119
)
120
121
122
class ContentLengthReader:
123
def __init__(self, length: int) -> None:
124
self._length = length
125
self._remaining = length
126
127
def __call__(self, buf: ReceiveBuffer) -> Union[Data, EndOfMessage, None]:
128
if self._remaining == 0:
129
return EndOfMessage()
130
data = buf.maybe_extract_at_most(self._remaining)
131
if data is None:
132
return None
133
self._remaining -= len(data)
134
return Data(data=data)
135
136
def read_eof(self) -> NoReturn:
137
raise RemoteProtocolError(
138
"peer closed connection without sending complete message body "
139
"(received {} bytes, expected {})".format(
140
self._length - self._remaining, self._length
141
)
142
)
143
144
145
chunk_header_re = re.compile(chunk_header.encode("ascii"))
146
147
148
class ChunkedReader:
149
def __init__(self) -> None:
150
self._bytes_in_chunk = 0
151
# After reading a chunk, we have to throw away the trailing \r\n.
152
# This tracks the bytes that we need to match and throw away.
153
self._bytes_to_discard = b""
154
self._reading_trailer = False
155
156
def __call__(self, buf: ReceiveBuffer) -> Union[Data, EndOfMessage, None]:
157
if self._reading_trailer:
158
lines = buf.maybe_extract_lines()
159
if lines is None:
160
return None
161
return EndOfMessage(headers=list(_decode_header_lines(lines)))
162
if self._bytes_to_discard:
163
data = buf.maybe_extract_at_most(len(self._bytes_to_discard))
164
if data is None:
165
return None
166
if data != self._bytes_to_discard[: len(data)]:
167
raise LocalProtocolError(
168
f"malformed chunk footer: {data!r} (expected {self._bytes_to_discard!r})"
169
)
170
self._bytes_to_discard = self._bytes_to_discard[len(data) :]
171
if self._bytes_to_discard:
172
return None
173
# else, fall through and read some more
174
assert self._bytes_to_discard == b""
175
if self._bytes_in_chunk == 0:
176
# We need to refill our chunk count
177
chunk_header = buf.maybe_extract_next_line()
178
if chunk_header is None:
179
return None
180
matches = validate(
181
chunk_header_re,
182
chunk_header,
183
"illegal chunk header: {!r}",
184
chunk_header,
185
)
186
# XX FIXME: we discard chunk extensions. Does anyone care?
187
self._bytes_in_chunk = int(matches["chunk_size"], base=16)
188
if self._bytes_in_chunk == 0:
189
self._reading_trailer = True
190
return self(buf)
191
chunk_start = True
192
else:
193
chunk_start = False
194
assert self._bytes_in_chunk > 0
195
data = buf.maybe_extract_at_most(self._bytes_in_chunk)
196
if data is None:
197
return None
198
self._bytes_in_chunk -= len(data)
199
if self._bytes_in_chunk == 0:
200
self._bytes_to_discard = b"\r\n"
201
chunk_end = True
202
else:
203
chunk_end = False
204
return Data(data=data, chunk_start=chunk_start, chunk_end=chunk_end)
205
206
def read_eof(self) -> NoReturn:
207
raise RemoteProtocolError(
208
"peer closed connection without sending complete message body "
209
"(incomplete chunked read)"
210
)
211
212
213
class Http10Reader:
214
def __call__(self, buf: ReceiveBuffer) -> Optional[Data]:
215
data = buf.maybe_extract_at_most(999999999)
216
if data is None:
217
return None
218
return Data(data=data)
219
220
def read_eof(self) -> EndOfMessage:
221
return EndOfMessage()
222
223
224
def expect_nothing(buf: ReceiveBuffer) -> None:
225
if buf:
226
raise LocalProtocolError("Got data when expecting EOF")
227
return None
228
229
230
ReadersType = Dict[
231
Union[Type[Sentinel], Tuple[Type[Sentinel], Type[Sentinel]]],
232
Union[Callable[..., Any], Dict[str, Callable[..., Any]]],
233
]
234
235
READERS: ReadersType = {
236
(CLIENT, IDLE): maybe_read_from_IDLE_client,
237
(SERVER, IDLE): maybe_read_from_SEND_RESPONSE_server,
238
(SERVER, SEND_RESPONSE): maybe_read_from_SEND_RESPONSE_server,
239
(CLIENT, DONE): expect_nothing,
240
(CLIENT, MUST_CLOSE): expect_nothing,
241
(CLIENT, CLOSED): expect_nothing,
242
(SERVER, DONE): expect_nothing,
243
(SERVER, MUST_CLOSE): expect_nothing,
244
(SERVER, CLOSED): expect_nothing,
245
SEND_BODY: {
246
"chunked": ChunkedReader,
247
"content-length": ContentLengthReader,
248
"http/1.0": Http10Reader,
249
},
250
}