Coverage for /pythoncovmergedfiles/medio/medio/src/aiohttp/aiohttp/http_parser.py: 81%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

630 statements  

1import abc 

2import asyncio 

3import re 

4import string 

5import sys 

6from contextlib import suppress 

7from enum import IntEnum 

8from re import Pattern 

9from typing import ( 

10 TYPE_CHECKING, 

11 Any, 

12 ClassVar, 

13 Final, 

14 Generic, 

15 Literal, 

16 NamedTuple, 

17 TypeVar, 

18) 

19 

20from multidict import CIMultiDict, istr 

21from yarl import URL 

22 

23from . import hdrs 

24from .base_protocol import BaseProtocol 

25from .compression_utils import ( 

26 HAS_BROTLI, 

27 HAS_ZSTD, 

28 BrotliDecompressor, 

29 ZLibDecompressor, 

30 ZSTDDecompressor, 

31) 

32from .helpers import ( 

33 _EXC_SENTINEL, 

34 DEBUG, 

35 DEFAULT_CHUNK_SIZE, 

36 EMPTY_BODY_METHODS, 

37 EMPTY_BODY_STATUS_CODES, 

38 NO_EXTENSIONS, 

39 BaseTimerContext, 

40 HeadersDictProxy, 

41 set_exception, 

42) 

43from .http_exceptions import ( 

44 BadHttpMessage, 

45 BadHttpMethod, 

46 BadStatusLine, 

47 ContentEncodingError, 

48 ContentLengthError, 

49 InvalidHeader, 

50 InvalidURLError, 

51 LineTooLong, 

52 TransferEncodingError, 

53) 

54from .http_writer import HttpVersion, HttpVersion10, HttpVersion11 

55from .streams import EMPTY_PAYLOAD, StreamReader 

56from .typedefs import RawHeaders 

57 

58if TYPE_CHECKING: 

59 from .client_proto import ResponseHandler 

60 

61__all__ = ( 

62 "HeadersParser", 

63 "HttpParser", 

64 "HttpRequestParser", 

65 "HttpResponseParser", 

66 "RawRequestMessage", 

67 "RawResponseMessage", 

68) 

69 

70_T = TypeVar("_T") 

71 

72_SEP = Literal[b"\r\n", b"\n"] 

73 

74ASCIISET: Final[set[str]] = set(string.printable) 

75 

76# See https://www.rfc-editor.org/rfc/rfc9110.html#name-overview 

77# and https://www.rfc-editor.org/rfc/rfc9110.html#name-tokens 

78# 

79# method = token 

80# tchar = "!" / "#" / "$" / "%" / "&" / "'" / "*" / "+" / "-" / "." / 

81# "^" / "_" / "`" / "|" / "~" / DIGIT / ALPHA 

82# token = 1*tchar 

83_TCHAR_SPECIALS: Final[str] = re.escape("!#$%&'*+-.^_`|~") 

84TOKENRE: Final[Pattern[str]] = re.compile(f"[0-9A-Za-z{_TCHAR_SPECIALS}]+") 

85# https://www.rfc-editor.org/rfc/rfc9110#section-5.5-5 

86_FIELD_VALUE_FORBIDDEN_CTL_RE: Final[Pattern[str]] = re.compile( 

87 r"[\x00-\x08\x0a-\x1f\x7f]" 

88) 

89VERSRE: Final[Pattern[str]] = re.compile(r"HTTP/(\d)\.(\d)", re.ASCII) 

90DIGITS: Final[Pattern[str]] = re.compile(r"\d+", re.ASCII) 

91HEXDIGITS: Final[Pattern[bytes]] = re.compile(rb"[0-9a-fA-F]+") 

92 

93# RFC 9110 singleton headers — duplicates are rejected in strict mode. 

94# In lax mode (response parser default), the check is skipped entirely 

95# since real-world servers (e.g. Google APIs, Werkzeug) commonly send 

96# duplicate headers like Content-Type or Server. 

97# Lowercased for case-insensitive matching against wire names. 

98SINGLETON_HEADERS: Final[frozenset[str]] = frozenset( 

99 { 

100 "content-length", 

101 "content-location", 

102 "content-range", 

103 "content-type", 

104 "etag", 

105 "host", 

106 "max-forwards", 

107 "server", 

108 "transfer-encoding", 

109 "user-agent", 

110 } 

111) 

112 

113 

114class RawRequestMessage(NamedTuple): 

115 method: str 

116 path: str 

117 version: HttpVersion 

118 headers: HeadersDictProxy 

119 raw_headers: RawHeaders 

120 should_close: bool 

121 compression: str | None 

122 upgrade: bool 

123 chunked: bool 

124 url: URL 

125 

126 

127class RawResponseMessage(NamedTuple): 

128 version: HttpVersion 

129 code: int 

130 reason: str 

131 headers: HeadersDictProxy 

132 raw_headers: RawHeaders 

133 should_close: bool 

134 compression: str | None 

135 upgrade: bool 

136 chunked: bool 

137 

138 

139_MsgT = TypeVar("_MsgT", RawRequestMessage, RawResponseMessage) 

140 

141 

142class PayloadState(IntEnum): 

143 PAYLOAD_COMPLETE = 0 

144 PAYLOAD_NEEDS_INPUT = 1 

145 PAYLOAD_HAS_PENDING_INPUT = 2 

146 

147 

148class ParseState(IntEnum): 

149 PARSE_NONE = 0 

150 PARSE_LENGTH = 1 

151 PARSE_CHUNKED = 2 

152 PARSE_UNTIL_EOF = 3 

153 

154 

155class ChunkState(IntEnum): 

156 PARSE_CHUNKED_SIZE = 0 

157 PARSE_CHUNKED_CHUNK = 1 

158 PARSE_CHUNKED_CHUNK_EOF = 2 

159 PARSE_TRAILERS = 4 

160 

161 

162class HeadersParser: 

163 def __init__(self, max_field_size: int = 8190, lax: bool = False) -> None: 

164 self.max_field_size = max_field_size 

165 self._lax = lax 

166 

167 def parse_headers(self, lines: list[bytes]) -> tuple[HeadersDictProxy, RawHeaders]: 

168 headers: CIMultiDict[str] = CIMultiDict() 

169 # note: "raw" does not mean inclusion of OWS before/after the field value 

170 raw_headers = [] 

171 

172 lines_idx = 0 

173 line = lines[lines_idx] 

174 line_count = len(lines) 

175 

176 while line: 

177 # Parse initial header name : value pair. 

178 try: 

179 bname, bvalue = line.split(b":", 1) 

180 except ValueError: 

181 raise InvalidHeader(line) from None 

182 

183 if len(bname) == 0: 

184 raise InvalidHeader(bname) 

185 

186 # https://www.rfc-editor.org/rfc/rfc9112.html#section-5.1-2 

187 if {bname[0], bname[-1]} & {32, 9}: # {" ", "\t"} 

188 raise InvalidHeader(line) 

189 

190 bvalue = bvalue.lstrip(b" \t") 

191 name = bname.decode("utf-8", "surrogateescape") 

192 if not TOKENRE.fullmatch(name): 

193 raise InvalidHeader(bname) 

194 

195 # next line 

196 lines_idx += 1 

197 line = lines[lines_idx] 

198 

199 # consume continuation lines 

200 continuation = self._lax and line and line[0] in (32, 9) # (' ', '\t') 

201 

202 # Deprecated: https://www.rfc-editor.org/rfc/rfc9112.html#name-obsolete-line-folding 

203 if continuation: 

204 header_length = len(bvalue) 

205 bvalue_lst = [bvalue] 

206 while continuation: 

207 header_length += len(line) 

208 if header_length > self.max_field_size: 

209 header_line = bname + b": " + b"".join(bvalue_lst) 

210 raise LineTooLong( 

211 header_line[:100] + b"...", self.max_field_size 

212 ) 

213 bvalue_lst.append(line) 

214 

215 # next line 

216 lines_idx += 1 

217 if lines_idx < line_count: 

218 line = lines[lines_idx] 

219 if line: 

220 continuation = line[0] in (32, 9) # (' ', '\t') 

221 else: 

222 line = b"" 

223 break 

224 bvalue = b"".join(bvalue_lst) 

225 

226 bvalue = bvalue.strip(b" \t") 

227 value = bvalue.decode("utf-8", "surrogateescape") 

228 

229 # https://www.rfc-editor.org/rfc/rfc9110.html#section-5.5-5 

230 if self._lax: 

231 if "\n" in value or "\r" in value or "\x00" in value: 

232 raise InvalidHeader(bvalue) 

233 elif _FIELD_VALUE_FORBIDDEN_CTL_RE.search(value): 

234 raise InvalidHeader(bvalue) 

235 

236 if not self._lax and name in headers and name.lower() in SINGLETON_HEADERS: 

237 raise BadHttpMessage(f"Duplicate '{name}' header found.") 

238 headers.add(name, value) 

239 raw_headers.append((bname, bvalue)) 

240 

241 return (HeadersDictProxy(headers), tuple(raw_headers)) 

242 

243 

244def _is_supported_upgrade(headers: HeadersDictProxy) -> bool: 

245 """Check if the upgrade header is supported.""" 

246 u = headers.get(hdrs.UPGRADE, "") 

247 # .lower() can transform non-ascii characters. 

248 return u.isascii() and u.lower() in {"tcp", "websocket"} 

249 

250 

251class HttpParser(abc.ABC, Generic[_MsgT]): 

252 lax: ClassVar[bool] = False 

253 

254 def __init__( 

255 self, 

256 protocol: BaseProtocol, 

257 loop: asyncio.AbstractEventLoop, 

258 limit: int, 

259 max_line_size: int = 8190, 

260 max_headers: int = 128, 

261 max_field_size: int = 8190, 

262 timer: BaseTimerContext | None = None, 

263 code: int | None = None, 

264 method: str | None = None, 

265 payload_exception: type[BaseException] | None = None, 

266 response_with_body: bool = True, 

267 read_until_eof: bool = False, 

268 auto_decompress: bool = True, 

269 max_msg_queue_size: int = 0, 

270 ) -> None: 

271 self.protocol = protocol 

272 self.loop = loop 

273 self.max_line_size = max_line_size 

274 self.max_field_size = max_field_size 

275 self.max_headers = max_headers 

276 self.timer = timer 

277 self.code = code 

278 self.method = method 

279 self.payload_exception = payload_exception 

280 self.response_with_body = response_with_body 

281 self.read_until_eof = read_until_eof 

282 

283 self._lines: list[bytes] = [] 

284 self._tail = b"" 

285 self._upgraded = False 

286 self._pending_upgrade = False 

287 self._payload = None 

288 self._payload_parser: HttpPayloadParser | None = None 

289 self._payload_has_more_data = False 

290 self._auto_decompress = auto_decompress 

291 self._limit = limit 

292 self._headers_parser = HeadersParser(max_field_size, self.lax) 

293 # Stop emitting messages once this many are queued unconsumed (0 = off). 

294 self._max_msg_queue_size = max_msg_queue_size 

295 self._msg_in_flight = 0 

296 

297 @abc.abstractmethod 

298 def parse_message(self, lines: list[bytes]) -> _MsgT: ... 

299 

300 @abc.abstractmethod 

301 def _is_chunked_te(self, te: str) -> bool: ... 

302 

303 def pause_reading(self) -> None: 

304 assert self._payload_parser is not None 

305 self._payload_parser.pause_reading() 

306 

307 def message_consumed(self) -> None: 

308 """Protocol drained a queued message; free a slot for parsing.""" 

309 if self._msg_in_flight > 0: 

310 self._msg_in_flight -= 1 

311 

312 def feed_eof(self) -> _MsgT | None: 

313 if self._payload_parser is not None: 

314 self._payload_parser.feed_eof() 

315 if self._payload_parser.done: 

316 self._payload_parser = None 

317 else: 

318 # try to extract partial message 

319 if self._tail: 

320 self._lines.append(self._tail) 

321 

322 if self._lines: 

323 if self._lines[-1] != "\r\n": 

324 self._lines.append(b"") 

325 with suppress(Exception): 

326 return self.parse_message(self._lines) 

327 return None 

328 

329 def feed_data( 

330 self, 

331 data: bytes, 

332 SEP: _SEP = b"\r\n", 

333 EMPTY: bytes = b"", 

334 CONTENT_LENGTH: istr = hdrs.CONTENT_LENGTH, 

335 METH_CONNECT: str = hdrs.METH_CONNECT, 

336 SEC_WEBSOCKET_KEY1: istr = hdrs.SEC_WEBSOCKET_KEY1, 

337 ) -> tuple[list[tuple[_MsgT, StreamReader]], bool, bytes]: 

338 messages = [] 

339 

340 if self._tail: 

341 data, self._tail = self._tail + data, b"" 

342 

343 data_len = len(data) 

344 start_pos = 0 

345 loop = self.loop 

346 max_line_length = self.max_line_size 

347 

348 should_close = False 

349 while start_pos < data_len or self._payload_has_more_data: 

350 # read HTTP message (request/response line + headers), \r\n\r\n 

351 # and split by lines 

352 if self._payload_parser is None and not self._upgraded: 

353 if ( 

354 self._max_msg_queue_size 

355 and self._msg_in_flight >= self._max_msg_queue_size 

356 ): 

357 # Queue full: buffer the rest and stop. Safe pause point; 

358 # any preceding body is consumed before the next request 

359 # line. Resumes via feed_data(b"") when the queue drains. 

360 self._tail = data[start_pos:] 

361 break 

362 pos = data.find(SEP, start_pos) 

363 # consume \r\n 

364 if pos == start_pos and not self._lines: 

365 start_pos = pos + len(SEP) 

366 continue 

367 

368 if pos >= start_pos: 

369 if should_close: 

370 raise BadHttpMessage("Data after `Connection: close`") 

371 

372 # line found 

373 line = data[start_pos:pos] 

374 if SEP == b"\n": # For lax response parsing 

375 line = line.rstrip(b"\r") 

376 if len(line) > max_line_length: 

377 raise LineTooLong(line[:100] + b"...", max_line_length) 

378 

379 self._lines.append(line) 

380 # After processing the status/request line, everything is a header. 

381 max_line_length = self.max_field_size 

382 

383 if len(self._lines) > self.max_headers: 

384 raise BadHttpMessage("Too many headers received") 

385 

386 start_pos = pos + len(SEP) 

387 

388 # \r\n\r\n found 

389 if self._lines[-1] == EMPTY: 

390 max_trailers = self.max_headers - len(self._lines) 

391 try: 

392 msg: _MsgT = self.parse_message(self._lines) 

393 finally: 

394 self._lines.clear() 

395 

396 def get_content_length() -> int | None: 

397 # payload length 

398 length_hdr = msg.headers.get(CONTENT_LENGTH) 

399 if length_hdr is None: 

400 return None 

401 

402 # Shouldn't allow +/- or other number formats. 

403 # https://www.rfc-editor.org/rfc/rfc9110#section-8.6-2 

404 # msg.headers is already stripped of leading/trailing wsp 

405 if not DIGITS.fullmatch(length_hdr): 

406 raise InvalidHeader(CONTENT_LENGTH) 

407 

408 return int(length_hdr) 

409 

410 length = get_content_length() 

411 # do not support old websocket spec 

412 if SEC_WEBSOCKET_KEY1 in msg.headers: 

413 raise InvalidHeader(SEC_WEBSOCKET_KEY1) 

414 

415 upgraded = msg.upgrade and _is_supported_upgrade(msg.headers) 

416 

417 method = getattr(msg, "method", self.method) 

418 # code is only present on responses 

419 code = getattr(msg, "code", 0) 

420 

421 assert self.protocol is not None 

422 # calculate payload 

423 empty_body = code in EMPTY_BODY_STATUS_CODES or bool( 

424 method and method in EMPTY_BODY_METHODS 

425 ) 

426 if not empty_body and ( 

427 (length is not None and length > 0) or msg.chunked 

428 ): 

429 payload = StreamReader( 

430 self.protocol, 

431 timer=self.timer, 

432 loop=loop, 

433 limit=self._limit, 

434 ) 

435 payload_parser = HttpPayloadParser( 

436 payload, 

437 length=length, 

438 chunked=msg.chunked, 

439 method=method, 

440 compression=msg.compression, 

441 code=self.code, 

442 response_with_body=self.response_with_body, 

443 auto_decompress=self._auto_decompress, 

444 lax=self.lax, 

445 headers_parser=self._headers_parser, 

446 max_line_size=self.max_line_size, 

447 max_field_size=self.max_field_size, 

448 max_trailers=max_trailers, 

449 limit=self._limit, 

450 ) 

451 if not payload_parser.done: 

452 self._payload_parser = payload_parser 

453 # https://www.rfc-editor.org/info/rfc9110/#section-7.8-15 

454 # Defer any requested upgrade until the 

455 # complete request has been read. 

456 self._pending_upgrade = upgraded 

457 elif method == METH_CONNECT: 

458 assert isinstance(msg, RawRequestMessage) 

459 payload = StreamReader( 

460 self.protocol, 

461 timer=self.timer, 

462 loop=loop, 

463 limit=self._limit, 

464 ) 

465 self._upgraded = True 

466 self._payload_parser = HttpPayloadParser( 

467 payload, 

468 method=msg.method, 

469 compression=msg.compression, 

470 auto_decompress=self._auto_decompress, 

471 lax=self.lax, 

472 headers_parser=self._headers_parser, 

473 max_line_size=self.max_line_size, 

474 max_field_size=self.max_field_size, 

475 max_trailers=max_trailers, 

476 limit=self._limit, 

477 ) 

478 elif not empty_body and length is None and self.read_until_eof: 

479 payload = StreamReader( 

480 self.protocol, 

481 timer=self.timer, 

482 loop=loop, 

483 limit=self._limit, 

484 ) 

485 payload_parser = HttpPayloadParser( 

486 payload, 

487 length=length, 

488 chunked=msg.chunked, 

489 method=method, 

490 compression=msg.compression, 

491 code=self.code, 

492 response_with_body=self.response_with_body, 

493 auto_decompress=self._auto_decompress, 

494 lax=self.lax, 

495 headers_parser=self._headers_parser, 

496 max_line_size=self.max_line_size, 

497 max_field_size=self.max_field_size, 

498 max_trailers=max_trailers, 

499 limit=self._limit, 

500 ) 

501 if not payload_parser.done: 

502 self._payload_parser = payload_parser 

503 elif upgraded: 

504 # No body to read, so the connection switches to 

505 # the upgraded protocol immediately. 

506 self._upgraded = True 

507 payload = EMPTY_PAYLOAD 

508 else: 

509 payload = EMPTY_PAYLOAD 

510 

511 messages.append((msg, payload)) 

512 if self._max_msg_queue_size: 

513 self._msg_in_flight += 1 

514 should_close = msg.should_close 

515 else: 

516 self._tail = data[start_pos:] 

517 # A bare LF here means CRLF was required: 

518 # reject instead of buffering, else a following request's 

519 # bytes get appended to this line and leak in the error. 

520 if b"\n" in self._tail: 

521 raise BadHttpMessage("Bad line ending, expected CRLF") 

522 if len(self._tail) > self.max_line_size: 

523 raise LineTooLong(self._tail[:100] + b"...", self.max_line_size) 

524 data = EMPTY 

525 break 

526 

527 # no parser, just store 

528 elif self._payload_parser is None and self._upgraded: 

529 assert not self._lines 

530 break 

531 

532 # feed payload 

533 else: 

534 assert not self._lines 

535 assert self._payload_parser is not None 

536 try: 

537 payload_state, data = self._payload_parser.feed_data( 

538 data[start_pos:], SEP 

539 ) 

540 except Exception as underlying_exc: 

541 reraised_exc: BaseException = underlying_exc 

542 if self.payload_exception is not None: 

543 reraised_exc = self.payload_exception(str(underlying_exc)) 

544 

545 set_exception( 

546 self._payload_parser.payload, 

547 reraised_exc, 

548 underlying_exc, 

549 ) 

550 

551 payload_state = PayloadState.PAYLOAD_COMPLETE 

552 data = b"" 

553 if isinstance( 

554 underlying_exc, (InvalidHeader, TransferEncodingError) 

555 ): 

556 raise 

557 

558 self._payload_has_more_data = ( 

559 payload_state == PayloadState.PAYLOAD_HAS_PENDING_INPUT 

560 ) 

561 

562 if payload_state is not PayloadState.PAYLOAD_COMPLETE: 

563 # We've either consumed all available data, or we're pausing 

564 # until the reader buffer is freed up. 

565 break 

566 

567 start_pos = 0 

568 data_len = len(data) 

569 self._payload_parser = None 

570 if self._pending_upgrade: 

571 # Body fully read: the deferred upgrade takes effect and 

572 # the rest of the connection is the upgraded protocol. 

573 self._upgraded = True 

574 self._pending_upgrade = False 

575 

576 if data and start_pos < data_len: 

577 data = data[start_pos:] 

578 else: 

579 data = EMPTY 

580 

581 return messages, self._upgraded, data 

582 

583 def parse_headers( 

584 self, lines: list[bytes] 

585 ) -> tuple[HeadersDictProxy, RawHeaders, bool | None, str | None, bool, bool]: 

586 """Parses RFC 5322 headers from a stream. 

587 

588 Line continuations are supported. Returns list of header name 

589 and value pairs. Header name is in upper case. 

590 """ 

591 headers, raw_headers = self._headers_parser.parse_headers(lines) 

592 close_conn = None 

593 encoding = None 

594 upgrade = False 

595 chunked = False 

596 

597 # keep-alive and protocol switching 

598 # RFC 9110 section 7.6.1 defines Connection as a comma-separated list. 

599 # We use a simple comma split here rather than getall() for performance, 

600 # as the target tokens (close, keep-alive, upgrade) are simple ASCII 

601 # values that never contain commas. 

602 conn_values = headers.get(hdrs.CONNECTION) 

603 if conn_values: 

604 conn_tokens = { 

605 token.lower() 

606 for token in (part.strip(" \t") for part in conn_values.split(",")) 

607 if token and token.isascii() 

608 } 

609 

610 if "close" in conn_tokens: 

611 close_conn = True 

612 elif "keep-alive" in conn_tokens: 

613 close_conn = False 

614 

615 # https://www.rfc-editor.org/rfc/rfc9110.html#name-101-switching-protocols 

616 if "upgrade" in conn_tokens and headers.get(hdrs.UPGRADE): 

617 upgrade = True 

618 

619 # encoding 

620 enc = headers.get(hdrs.CONTENT_ENCODING, "") 

621 if enc.isascii() and enc.lower() in {"gzip", "deflate", "br", "zstd"}: 

622 encoding = enc 

623 

624 # chunking 

625 te = headers.get(hdrs.TRANSFER_ENCODING) 

626 if te is not None: 

627 if self._is_chunked_te(te): 

628 chunked = True 

629 

630 if hdrs.CONTENT_LENGTH in headers: 

631 raise BadHttpMessage( 

632 "Transfer-Encoding can't be present with Content-Length", 

633 ) 

634 

635 return (headers, raw_headers, close_conn, encoding, upgrade, chunked) 

636 

637 def set_upgraded(self, val: bool) -> None: 

638 """Set connection upgraded (to websocket) mode. 

639 

640 :param bool val: new state. 

641 """ 

642 self._upgraded = val 

643 

644 

645class HttpRequestParser(HttpParser[RawRequestMessage]): 

646 """Read request status line. 

647 

648 Exception .http_exceptions.BadStatusLine 

649 could be raised in case of any errors in status line. 

650 Returns RawRequestMessage. 

651 """ 

652 

653 def parse_message(self, lines: list[bytes]) -> RawRequestMessage: 

654 # request line 

655 line = lines[0].decode("utf-8", "surrogateescape") 

656 try: 

657 method, path, version = line.split(" ", maxsplit=2) 

658 except ValueError: 

659 raise BadHttpMethod(line) from None 

660 

661 # method 

662 if not TOKENRE.fullmatch(method): 

663 raise BadHttpMethod(method) 

664 method = method.upper() 

665 

666 # version 

667 match = VERSRE.fullmatch(version) 

668 if match is None: 

669 raise BadStatusLine(line) 

670 version_o = HttpVersion(int(match.group(1)), int(match.group(2))) 

671 

672 if method == "CONNECT": 

673 # authority-form, 

674 # https://datatracker.ietf.org/doc/html/rfc7230#section-5.3.3 

675 url = URL.build(authority=path, encoded=True) 

676 elif path.startswith("/"): 

677 # origin-form, 

678 # https://datatracker.ietf.org/doc/html/rfc7230#section-5.3.1 

679 path_part, _hash_separator, url_fragment = path.partition("#") 

680 path_part, _question_mark_separator, qs_part = path_part.partition("?") 

681 

682 # NOTE: `yarl.URL.build()` is used to mimic what the Cython-based 

683 # NOTE: parser does, otherwise it results into the same 

684 # NOTE: HTTP Request-Line input producing different 

685 # NOTE: `yarl.URL()` objects 

686 url = URL.build( 

687 path=path_part, 

688 query_string=qs_part, 

689 fragment=url_fragment, 

690 encoded=True, 

691 ) 

692 elif path == "*" and method == "OPTIONS": 

693 # asterisk-form, 

694 url = URL(path, encoded=True) 

695 else: 

696 # absolute-form for proxy maybe, 

697 # https://datatracker.ietf.org/doc/html/rfc7230#section-5.3.2 

698 url = URL(path, encoded=True) 

699 if not url.absolute: 

700 # authority-form is only allowed with CONNECT 

701 # https://www.rfc-editor.org/info/rfc9112/#section-3.2.3-1 

702 raise InvalidURLError( 

703 path.encode(errors="surrogateescape").decode("latin1") 

704 ) 

705 

706 # read headers 

707 ( 

708 headers, 

709 raw_headers, 

710 close, 

711 compression, 

712 upgrade, 

713 chunked, 

714 ) = self.parse_headers(lines[1:]) 

715 

716 if version_o == HttpVersion11 and hdrs.HOST not in headers: 

717 raise BadHttpMessage("Missing 'Host' header in request.") 

718 

719 if close is None: # then the headers weren't set in the request 

720 if version_o <= HttpVersion10: # HTTP 1.0 must asks to not close 

721 close = True 

722 else: # HTTP 1.1 must ask to close. 

723 close = False 

724 

725 return RawRequestMessage( 

726 method, 

727 path, 

728 version_o, 

729 headers, 

730 raw_headers, 

731 close, 

732 compression, 

733 upgrade, 

734 chunked, 

735 url, 

736 ) 

737 

738 def _is_chunked_te(self, te: str) -> bool: 

739 # https://www.rfc-editor.org/rfc/rfc9112#section-7.1-3 

740 # "A sender MUST NOT apply the chunked transfer coding more 

741 # than once to a message body" 

742 parts = [p.strip(" \t") for p in te.split(",")] 

743 chunked_count = sum(1 for p in parts if p.isascii() and p.lower() == "chunked") 

744 if chunked_count > 1: 

745 raise BadHttpMessage("Request has duplicate `chunked` Transfer-Encoding") 

746 last = parts[-1] 

747 # .lower() transforms some non-ascii chars, so must check first. 

748 if last.isascii() and last.lower() == "chunked": 

749 return True 

750 # https://www.rfc-editor.org/rfc/rfc9112#section-6.3-2.4.3 

751 raise BadHttpMessage("Request has invalid `Transfer-Encoding`") 

752 

753 

754class HttpResponseParser(HttpParser[RawResponseMessage]): 

755 """Read response status line and headers. 

756 

757 BadStatusLine could be raised in case of any errors in status line. 

758 Returns RawResponseMessage. 

759 """ 

760 

761 protocol: "ResponseHandler" 

762 

763 # Lax mode should only be enabled on response parser. 

764 lax = not DEBUG 

765 

766 def feed_data( 

767 self, 

768 data: bytes, 

769 SEP: _SEP | None = None, 

770 *args: Any, 

771 **kwargs: Any, 

772 ) -> tuple[list[tuple[RawResponseMessage, StreamReader]], bool, bytes]: 

773 if SEP is None: 

774 SEP = b"\r\n" if DEBUG else b"\n" 

775 return super().feed_data(data, SEP, *args, **kwargs) 

776 

777 def parse_message(self, lines: list[bytes]) -> RawResponseMessage: 

778 line = lines[0].decode("utf-8", "surrogateescape") 

779 try: 

780 version, status = line.split(maxsplit=1) 

781 except ValueError: 

782 raise BadStatusLine(line) from None 

783 

784 try: 

785 status, reason = status.split(maxsplit=1) 

786 except ValueError: 

787 status = status.strip() 

788 reason = "" 

789 

790 # version 

791 match = VERSRE.fullmatch(version) 

792 if match is None: 

793 raise BadStatusLine(line) 

794 version_o = HttpVersion(int(match.group(1)), int(match.group(2))) 

795 

796 # The status code is a three-digit ASCII number, no padding 

797 if len(status) != 3 or not DIGITS.fullmatch(status): 

798 raise BadStatusLine(line) 

799 status_i = int(status) 

800 

801 # read headers 

802 ( 

803 headers, 

804 raw_headers, 

805 close, 

806 compression, 

807 upgrade, 

808 chunked, 

809 ) = self.parse_headers(lines[1:]) 

810 

811 if close is None: 

812 if version_o <= HttpVersion10: 

813 close = True 

814 # https://www.rfc-editor.org/rfc/rfc9112.html#name-message-body-length 

815 elif 100 <= status_i < 200 or status_i in {204, 304}: 

816 close = False 

817 elif hdrs.CONTENT_LENGTH in headers or hdrs.TRANSFER_ENCODING in headers: 

818 close = False 

819 else: 

820 # https://www.rfc-editor.org/rfc/rfc9112.html#section-6.3-2.8 

821 close = True 

822 

823 return RawResponseMessage( 

824 version_o, 

825 status_i, 

826 reason.strip(), 

827 headers, 

828 raw_headers, 

829 close, 

830 compression, 

831 upgrade, 

832 chunked, 

833 ) 

834 

835 def _is_chunked_te(self, te: str) -> bool: 

836 # https://www.rfc-editor.org/rfc/rfc9112#section-6.3-2.4.2 

837 return te.rsplit(",", maxsplit=1)[-1].strip(" \t").lower() == "chunked" 

838 

839 

840class HttpPayloadParser: 

841 def __init__( 

842 self, 

843 payload: StreamReader, 

844 length: int | None = None, 

845 chunked: bool = False, 

846 compression: str | None = None, 

847 code: int | None = None, 

848 method: str | None = None, 

849 response_with_body: bool = True, 

850 auto_decompress: bool = True, 

851 lax: bool = False, 

852 *, 

853 headers_parser: HeadersParser, 

854 max_line_size: int = 8190, 

855 max_field_size: int = 8190, 

856 max_trailers: int = 128, 

857 limit: int = DEFAULT_CHUNK_SIZE, 

858 ) -> None: 

859 self._length = 0 

860 self._paused = False 

861 self._type = ParseState.PARSE_UNTIL_EOF 

862 self._chunk = ChunkState.PARSE_CHUNKED_SIZE 

863 self._chunk_size = 0 

864 self._chunk_tail = b"" 

865 self._auto_decompress = auto_decompress 

866 self._lax = lax 

867 self._headers_parser = headers_parser 

868 self._max_line_size = max_line_size 

869 self._max_field_size = max_field_size 

870 self._max_trailers = max_trailers 

871 self._more_data_available = False 

872 self._trailer_lines: list[bytes] = [] 

873 self.done = False 

874 self._eof_pending = False 

875 

876 # payload decompression wrapper 

877 if response_with_body and compression and self._auto_decompress: 

878 real_payload: StreamReader | DeflateBuffer = DeflateBuffer( 

879 payload, compression, max_decompress_size=limit 

880 ) 

881 else: 

882 real_payload = payload 

883 

884 # payload parser 

885 if not response_with_body: 

886 # don't parse payload if it's not expected to be received 

887 self._type = ParseState.PARSE_NONE 

888 real_payload.feed_eof() 

889 self.done = True 

890 elif chunked: 

891 self._type = ParseState.PARSE_CHUNKED 

892 elif length is not None: 

893 self._type = ParseState.PARSE_LENGTH 

894 self._length = length 

895 self._length_expected = length 

896 if self._length == 0: 

897 real_payload.feed_eof() 

898 self.done = True 

899 

900 self.payload = real_payload 

901 

902 def pause_reading(self) -> None: 

903 self._paused = True 

904 

905 def feed_eof(self) -> None: 

906 if self._type == ParseState.PARSE_UNTIL_EOF: 

907 self._eof_pending = True 

908 while self._more_data_available: 

909 if self._paused: 

910 self._paused = False 

911 return # Will resume via feed_data(b"") later 

912 self._more_data_available = self.payload.feed_data(b"") 

913 self.payload.feed_eof() 

914 self.done = True 

915 self._eof_pending = False 

916 elif self._type == ParseState.PARSE_LENGTH: 

917 received = self._length_expected - self._length 

918 raise ContentLengthError( 

919 f"Not enough data to satisfy content length header " 

920 f"(received {received} of {self._length_expected} bytes)." 

921 ) 

922 elif self._type == ParseState.PARSE_CHUNKED: 

923 raise TransferEncodingError( 

924 "Not enough data to satisfy transfer length header." 

925 ) 

926 

927 def feed_data( 

928 self, chunk: bytes, SEP: _SEP = b"\r\n", CHUNK_EXT: bytes = b";" 

929 ) -> tuple[PayloadState, bytes]: 

930 """Receive a chunk of data to process. 

931 

932 Return: 

933 PayloadState - The current state of payload processing. 

934 This function may be called with empty bytes after returning 

935 PAYLOAD_HAS_PENDING_INPUT to continue processing after a pause. 

936 bytes - If payload is complete, this is the unconsumed bytes intended for the 

937 next message/payload, b"" otherwise. 

938 """ 

939 # Read specified amount of bytes 

940 if self._type == ParseState.PARSE_LENGTH: 

941 if self._chunk_tail: 

942 chunk = self._chunk_tail + chunk 

943 self._chunk_tail = b"" 

944 

945 required = self._length 

946 self._length = max(required - len(chunk), 0) 

947 self._more_data_available = self.payload.feed_data(chunk[:required]) 

948 while self._more_data_available: 

949 if self._paused: 

950 self._paused = False 

951 self._chunk_tail = chunk[required:] 

952 return PayloadState.PAYLOAD_HAS_PENDING_INPUT, b"" 

953 self._more_data_available = self.payload.feed_data(b"") 

954 

955 if self._length == 0: 

956 self.payload.feed_eof() 

957 return PayloadState.PAYLOAD_COMPLETE, chunk[required:] 

958 # Chunked transfer encoding parser 

959 elif self._type == ParseState.PARSE_CHUNKED: 

960 if self._chunk_tail: 

961 # We should check the length is sane when not processing payload body. 

962 if self._chunk != ChunkState.PARSE_CHUNKED_CHUNK: 

963 max_line_length = self._max_line_size 

964 if self._chunk == ChunkState.PARSE_TRAILERS: 

965 max_line_length = self._max_field_size 

966 if len(self._chunk_tail) > max_line_length: 

967 raise LineTooLong( 

968 self._chunk_tail[:100] + b"...", max_line_length 

969 ) 

970 

971 chunk = self._chunk_tail + chunk 

972 self._chunk_tail = b"" 

973 

974 while chunk or self._more_data_available: 

975 # read next chunk size 

976 if self._chunk == ChunkState.PARSE_CHUNKED_SIZE: 

977 pos = chunk.find(SEP) 

978 if pos >= 0: 

979 # Only chunk-size lines reach here; trailers enforce 

980 # _max_field_size separately in PARSE_TRAILERS below. 

981 if pos > self._max_line_size: 

982 raise LineTooLong(chunk[:100] + b"...", self._max_line_size) 

983 i = chunk.find(CHUNK_EXT, 0, pos) 

984 if i >= 0: 

985 size_b = chunk[:i] # strip chunk-extensions 

986 # Verify no LF in the chunk-extension 

987 if b"\n" in (ext := chunk[i:pos]): 

988 exc = TransferEncodingError( 

989 f"Unexpected LF in chunk-extension: {ext!r}" 

990 ) 

991 set_exception(self.payload, exc) 

992 raise exc 

993 else: 

994 size_b = chunk[:pos] 

995 

996 if self._lax: # Allow whitespace in lax mode. 

997 size_b = size_b.strip() 

998 

999 if not re.fullmatch(HEXDIGITS, size_b): 

1000 exc = TransferEncodingError( 

1001 chunk[:pos].decode("ascii", "surrogateescape") 

1002 ) 

1003 set_exception(self.payload, exc) 

1004 raise exc 

1005 size = int(bytes(size_b), 16) 

1006 

1007 chunk = chunk[pos + len(SEP) :] 

1008 if size == 0: # eof marker 

1009 self._chunk = ChunkState.PARSE_TRAILERS 

1010 if self._lax and chunk.startswith(b"\r"): 

1011 chunk = chunk[1:] 

1012 else: 

1013 self._chunk = ChunkState.PARSE_CHUNKED_CHUNK 

1014 self._chunk_size = size 

1015 self.payload.begin_http_chunk_receiving() 

1016 else: 

1017 if b"\n" in chunk: 

1018 exc = TransferEncodingError( 

1019 "Bad chunk-size line ending, expected CRLF" 

1020 ) 

1021 set_exception(self.payload, exc) 

1022 raise exc 

1023 self._chunk_tail = chunk 

1024 return PayloadState.PAYLOAD_NEEDS_INPUT, b"" 

1025 

1026 # read chunk and feed buffer 

1027 if self._chunk == ChunkState.PARSE_CHUNKED_CHUNK: 

1028 if self._paused: 

1029 self._paused = False 

1030 self._chunk_tail = chunk 

1031 return PayloadState.PAYLOAD_HAS_PENDING_INPUT, b"" 

1032 

1033 required = self._chunk_size 

1034 self._chunk_size = max(required - len(chunk), 0) 

1035 self._more_data_available = self.payload.feed_data(chunk[:required]) 

1036 chunk = chunk[required:] 

1037 

1038 if self._more_data_available: 

1039 continue 

1040 

1041 if self._chunk_size: 

1042 self._paused = False 

1043 return PayloadState.PAYLOAD_NEEDS_INPUT, b"" 

1044 self._chunk = ChunkState.PARSE_CHUNKED_CHUNK_EOF 

1045 self.payload.end_http_chunk_receiving() 

1046 

1047 # toss the CRLF at the end of the chunk 

1048 if self._chunk == ChunkState.PARSE_CHUNKED_CHUNK_EOF: 

1049 if self._lax and chunk.startswith(b"\r"): 

1050 chunk = chunk[1:] 

1051 if chunk[: len(SEP)] == SEP: 

1052 chunk = chunk[len(SEP) :] 

1053 self._chunk = ChunkState.PARSE_CHUNKED_SIZE 

1054 elif len(chunk) >= len(SEP) or chunk != SEP[: len(chunk)]: 

1055 exc = TransferEncodingError( 

1056 "Chunk size mismatch: expected CRLF after chunk data" 

1057 ) 

1058 set_exception(self.payload, exc) 

1059 raise exc 

1060 else: 

1061 self._chunk_tail = chunk 

1062 return PayloadState.PAYLOAD_NEEDS_INPUT, b"" 

1063 

1064 if self._chunk == ChunkState.PARSE_TRAILERS: 

1065 pos = chunk.find(SEP) 

1066 if pos < 0: # No line found 

1067 if b"\n" in chunk: 

1068 exc = TransferEncodingError( 

1069 "Bad trailer line ending, expected CRLF" 

1070 ) 

1071 set_exception(self.payload, exc) 

1072 raise exc 

1073 self._chunk_tail = chunk 

1074 return PayloadState.PAYLOAD_NEEDS_INPUT, b"" 

1075 

1076 line = chunk[:pos] 

1077 chunk = chunk[pos + len(SEP) :] 

1078 if SEP == b"\n": # For lax response parsing 

1079 line = line.rstrip(b"\r") 

1080 

1081 if len(line) > self._max_field_size: 

1082 raise LineTooLong(line[:100] + b"...", self._max_field_size) 

1083 

1084 self._trailer_lines.append(line) 

1085 

1086 if len(self._trailer_lines) > self._max_trailers: 

1087 raise BadHttpMessage("Too many trailers received") 

1088 

1089 # \r\n\r\n found, end of stream 

1090 if self._trailer_lines[-1] == b"": 

1091 # Headers and trailers are defined the same way, 

1092 # so we reuse the HeadersParser here. 

1093 try: 

1094 trailers, raw_trailers = self._headers_parser.parse_headers( 

1095 self._trailer_lines 

1096 ) 

1097 finally: 

1098 self._trailer_lines.clear() 

1099 self.payload.feed_eof() 

1100 return PayloadState.PAYLOAD_COMPLETE, chunk 

1101 

1102 # Read all bytes until eof 

1103 elif self._type == ParseState.PARSE_UNTIL_EOF: 

1104 self._more_data_available = self.payload.feed_data(chunk) 

1105 while self._more_data_available: 

1106 if self._paused: 

1107 self._paused = False 

1108 return PayloadState.PAYLOAD_HAS_PENDING_INPUT, b"" 

1109 self._more_data_available = self.payload.feed_data(b"") 

1110 

1111 if self._eof_pending: 

1112 self.payload.feed_eof() 

1113 self.done = True 

1114 self._eof_pending = False 

1115 return PayloadState.PAYLOAD_COMPLETE, b"" 

1116 

1117 return PayloadState.PAYLOAD_NEEDS_INPUT, b"" 

1118 

1119 

1120class DeflateBuffer: 

1121 """DeflateStream decompress stream and feed data into specified stream.""" 

1122 

1123 def __init__( 

1124 self, 

1125 out: StreamReader, 

1126 encoding: str | None, 

1127 max_decompress_size: int = DEFAULT_CHUNK_SIZE, 

1128 ) -> None: 

1129 self.out = out 

1130 self.size = 0 

1131 out.total_compressed_bytes = self.size 

1132 self.encoding = encoding 

1133 self._started_decoding = False 

1134 

1135 self.decompressor: BrotliDecompressor | ZLibDecompressor | ZSTDDecompressor 

1136 if encoding == "br": 

1137 if not HAS_BROTLI: 

1138 raise ContentEncodingError( 

1139 "Can not decode content-encoding: brotli (br). " 

1140 "Please install `Brotli`" 

1141 ) 

1142 self.decompressor = BrotliDecompressor() 

1143 elif encoding == "zstd": 

1144 if not HAS_ZSTD: 

1145 raise ContentEncodingError( 

1146 "Can not decode content-encoding: zstandard (zstd). " 

1147 "Please install `backports.zstd`" 

1148 ) 

1149 self.decompressor = ZSTDDecompressor() 

1150 else: 

1151 self.decompressor = ZLibDecompressor(encoding=encoding) 

1152 

1153 self._max_decompress_size = max_decompress_size 

1154 

1155 def set_exception( 

1156 self, 

1157 exc: type[BaseException] | BaseException, 

1158 exc_cause: BaseException = _EXC_SENTINEL, 

1159 ) -> None: 

1160 set_exception(self.out, exc, exc_cause) 

1161 

1162 def feed_data(self, chunk: bytes) -> bool: 

1163 """Return True if more data is available and this method should be called again with b"".""" 

1164 self.size += len(chunk) 

1165 self.out.total_compressed_bytes = self.size 

1166 

1167 # Inspect the first real byte once to choose the decompressor. An empty 

1168 # chunk (e.g. a chunk-size line arriving without body bytes) has no 

1169 # header to sniff, so skip it and wait for the first data byte. 

1170 if not self._started_decoding and chunk: 

1171 # RFC1950 

1172 # bits 0..3 = CM = 0b1000 = 8 = "deflate" 

1173 # bits 4..7 = CINFO = 1..7 = windows size. 

1174 if self.encoding == "deflate" and chunk[0] & 0xF != 8: 

1175 # Change the decoder to decompress incorrectly compressed data 

1176 # Actually we should issue a warning about non-RFC-compliant data. 

1177 self.decompressor = ZLibDecompressor( 

1178 encoding=self.encoding, suppress_deflate_header=True 

1179 ) 

1180 self._started_decoding = True 

1181 

1182 low_water = self.out._low_water 

1183 max_length = ( 

1184 0 if low_water >= sys.maxsize else max(self._max_decompress_size, low_water) 

1185 ) 

1186 try: 

1187 chunk = self.decompressor.decompress_sync(chunk, max_length=max_length) 

1188 except Exception: 

1189 raise ContentEncodingError( 

1190 "Can not decode content-encoding: %s" % self.encoding 

1191 ) 

1192 

1193 if chunk: 

1194 self.out.feed_data(chunk) 

1195 return self.decompressor.data_available 

1196 

1197 def feed_eof(self) -> None: 

1198 chunk = self.decompressor.flush() 

1199 # This should never contain data as we defer the call until exhausting 

1200 # the decompression. If .flush() is returning data, this may indicate a 

1201 # zip bomb vulnerability as it will decompress all remaining data at once. 

1202 assert not chunk 

1203 

1204 if self.size > 0: 

1205 # decompressor is not brotli unless encoding is "br" 

1206 if self.encoding == "deflate" and not self.decompressor.eof: # type: ignore[union-attr] 

1207 raise ContentEncodingError("deflate") 

1208 

1209 self.out.feed_eof() 

1210 

1211 def begin_http_chunk_receiving(self) -> None: 

1212 self.out.begin_http_chunk_receiving() 

1213 

1214 def end_http_chunk_receiving(self) -> None: 

1215 self.out.end_http_chunk_receiving() 

1216 

1217 

1218HttpRequestParserPy = HttpRequestParser 

1219HttpResponseParserPy = HttpResponseParser 

1220RawRequestMessagePy = RawRequestMessage 

1221RawResponseMessagePy = RawResponseMessage 

1222 

1223with suppress(ImportError): 

1224 if not NO_EXTENSIONS: 

1225 from ._http_parser import ( # type: ignore[import-not-found,no-redef] 

1226 HttpRequestParser, 

1227 HttpResponseParser, 

1228 RawRequestMessage, 

1229 RawResponseMessage, 

1230 ) 

1231 

1232 HttpRequestParserC = HttpRequestParser 

1233 HttpResponseParserC = HttpResponseParser 

1234 RawRequestMessageC = RawRequestMessage 

1235 RawResponseMessageC = RawResponseMessage