1616from propcache import under_cached_property
1717
1818from .abc import AbstractAccessLogger , AbstractStreamWriter
19- from .base_protocol import BaseProtocol
19+ from .base_protocol import PAUSE_RESUME_READING_ERRORS , BaseProtocol
2020from .helpers import DEFAULT_CHUNK_SIZE , ceil_timeout
2121from .http import (
2222 HttpProcessingError ,
3737
3838__all__ = ("RequestHandler" , "RequestPayloadError" , "PayloadAccessError" )
3939
40+ # Max parsed-but-unhandled pipelined requests buffered per connection before
41+ # reading is paused. Bounds memory a client can pin by keeping one handler busy
42+ # and pipelining behind it; reading resumes as the queue drains.
43+ MAX_MSG_QUEUE_SIZE = 32
44+
4045if TYPE_CHECKING :
4146 import ssl
4247
@@ -146,6 +151,9 @@ class RequestHandler(BaseProtocol):
146151 "_keepalive_timeout" ,
147152 "_lingering_time" ,
148153 "_messages" ,
154+ "_max_msg_queue_size" ,
155+ "_msg_queue_resume_size" ,
156+ "_msg_queue_paused" ,
149157 "_message_tail" ,
150158 "_handler_waiter" ,
151159 "_waiter" ,
@@ -186,6 +194,13 @@ def __init__(
186194 auto_decompress : bool = True ,
187195 timeout_ceil_threshold : float = 5 ,
188196 ):
197+ self ._max_msg_queue_size = MAX_MSG_QUEUE_SIZE
198+ # Low-water mark: resume reading once the queue drains to half the limit
199+ # so we refill in batches instead of churning pause/resume per request.
200+ self ._msg_queue_resume_size = MAX_MSG_QUEUE_SIZE // 2
201+ # Set before super().__init__ so _reading_paused_for_msg_queue() is safe
202+ # if BaseProtocol ever triggers a resume during init.
203+ self ._msg_queue_paused = False
189204 parser = HttpRequestParser (
190205 self ,
191206 loop ,
@@ -195,6 +210,7 @@ def __init__(
195210 max_headers = max_headers ,
196211 payload_exception = RequestPayloadError ,
197212 auto_decompress = auto_decompress ,
213+ max_msg_queue_size = MAX_MSG_QUEUE_SIZE ,
198214 )
199215 super ().__init__ (loop , parser )
200216
@@ -431,6 +447,14 @@ def data_received(self, data: bytes) -> None:
431447 # don't set result twice
432448 waiter .set_result (None )
433449
450+ # Queue full: pause the transport (the parser already stopped
451+ # emitting). start() resumes as it drains the queue.
452+ if (
453+ not self ._msg_queue_paused
454+ and len (self ._messages ) >= self ._max_msg_queue_size
455+ ):
456+ self ._pause_msg_queue_reading ()
457+
434458 self ._upgraded = upgraded
435459 if upgraded and tail :
436460 self ._message_tail = tail
@@ -447,6 +471,36 @@ def data_received(self, data: bytes) -> None:
447471 if eof :
448472 self .close ()
449473
474+ def _reading_paused_for_msg_queue (self ) -> bool :
475+ return self ._msg_queue_paused
476+
477+ def _pause_msg_queue_reading (self ) -> None :
478+ self ._msg_queue_paused = True
479+ if self .transport is not None :
480+ try :
481+ self .transport .pause_reading ()
482+ except PAUSE_RESUME_READING_ERRORS :
483+ # Transport lacks flow control; nothing to pause. Intentionally
484+ # ignored (see PAUSE_RESUME_READING_ERRORS; do not use suppress).
485+ pass
486+
487+ def _resume_msg_queue_reading (self ) -> None :
488+ if not self ._upgraded :
489+ # Reparse buffered pipelined requests while still marked paused so
490+ # a refill past the limit does not re-pause an already-paused
491+ # transport; only resume below once it stayed under the limit.
492+ self .data_received (b"" )
493+ if len (self ._messages ) >= self ._max_msg_queue_size :
494+ return
495+ self ._msg_queue_paused = False
496+ if not self ._reading_paused and self .transport is not None :
497+ try :
498+ self .transport .resume_reading ()
499+ except PAUSE_RESUME_READING_ERRORS :
500+ # Transport lacks flow control; nothing to resume. Intentionally
501+ # ignored (see PAUSE_RESUME_READING_ERRORS; do not use suppress).
502+ pass
503+
450504 def keep_alive (self , val : bool ) -> None :
451505 """Set keep-alive connection mode.
452506
@@ -579,6 +633,18 @@ async def start(self) -> None:
579633
580634 message , payload = self ._messages .popleft ()
581635
636+ # Free a parser slot; resume reading once drained to low water so
637+ # pipelining keeps flowing while this request is handled.
638+ # no branch: _parser is only None after connection_lost, whose path
639+ # exits this loop, so the None case is not reachably exercisable.
640+ if self ._parser is not None : # pragma: no branch
641+ self ._parser .message_consumed ()
642+ if (
643+ self ._msg_queue_paused
644+ and len (self ._messages ) <= self ._msg_queue_resume_size
645+ ):
646+ self ._resume_msg_queue_reading ()
647+
582648 # time is only fetched if logging is enabled as otherwise
583649 # its thrown away and never used.
584650 start = loop .time () if self ._logging_enabled else None
0 commit comments