1414from propcache import under_cached_property
1515
1616from .abc import AbstractAccessLogger , AbstractAsyncAccessLogger , AbstractStreamWriter
17- from .base_protocol import BaseProtocol
17+ from .base_protocol import PAUSE_RESUME_READING_ERRORS , BaseProtocol
1818from .helpers import DEFAULT_CHUNK_SIZE , ceil_timeout , frozen_dataclass_decorator
1919from .http import (
2020 HttpProcessingError ,
3535
3636__all__ = ("RequestHandler" , "RequestPayloadError" , "PayloadAccessError" )
3737
38+ # Max parsed-but-unhandled pipelined requests buffered per connection before
39+ # reading is paused. Bounds memory a client can pin by keeping one handler busy
40+ # and pipelining behind it; reading resumes as the queue drains.
41+ MAX_MSG_QUEUE_SIZE = 32
42+
3843if TYPE_CHECKING :
3944 import ssl
4045
@@ -168,6 +173,9 @@ class RequestHandler(BaseProtocol, Generic[_Request]):
168173 "_keepalive_timeout" ,
169174 "_lingering_time" ,
170175 "_messages" ,
176+ "_max_msg_queue_size" ,
177+ "_msg_queue_resume_size" ,
178+ "_msg_queue_paused" ,
171179 "_message_tail" ,
172180 "_handler_waiter" ,
173181 "_waiter" ,
@@ -206,6 +214,13 @@ def __init__(
206214 auto_decompress : bool = True ,
207215 timeout_ceil_threshold : float = 5 ,
208216 ):
217+ self ._max_msg_queue_size = MAX_MSG_QUEUE_SIZE
218+ # Low-water mark: resume reading once the queue drains to half the limit
219+ # so we refill in batches instead of churning pause/resume per request.
220+ self ._msg_queue_resume_size = MAX_MSG_QUEUE_SIZE // 2
221+ # Set before super().__init__ so _reading_paused_for_msg_queue() is safe
222+ # if BaseProtocol ever triggers a resume during init.
223+ self ._msg_queue_paused = False
209224 parser = HttpRequestParser (
210225 self ,
211226 loop ,
@@ -215,6 +230,7 @@ def __init__(
215230 max_headers = max_headers ,
216231 payload_exception = RequestPayloadError ,
217232 auto_decompress = auto_decompress ,
233+ max_msg_queue_size = MAX_MSG_QUEUE_SIZE ,
218234 )
219235 super ().__init__ (loop , parser )
220236
@@ -461,6 +477,14 @@ def data_received(self, data: bytes) -> None:
461477 # don't set result twice
462478 waiter .set_result (None )
463479
480+ # Queue full: pause the transport (the parser already stopped
481+ # emitting). start() resumes as it drains the queue.
482+ if (
483+ not self ._msg_queue_paused
484+ and len (self ._messages ) >= self ._max_msg_queue_size
485+ ):
486+ self ._pause_msg_queue_reading ()
487+
464488 self ._upgraded = upgraded
465489 if upgraded and tail :
466490 self ._message_tail = tail
@@ -477,6 +501,36 @@ def data_received(self, data: bytes) -> None:
477501 if eof :
478502 self .close ()
479503
504+ def _reading_paused_for_msg_queue (self ) -> bool :
505+ return self ._msg_queue_paused
506+
507+ def _pause_msg_queue_reading (self ) -> None :
508+ self ._msg_queue_paused = True
509+ if self .transport is not None :
510+ try :
511+ self .transport .pause_reading ()
512+ except PAUSE_RESUME_READING_ERRORS :
513+ # Transport lacks flow control; nothing to pause. Intentionally
514+ # ignored (see PAUSE_RESUME_READING_ERRORS; do not use suppress).
515+ pass
516+
517+ def _resume_msg_queue_reading (self ) -> None :
518+ if not self ._upgraded :
519+ # Reparse buffered pipelined requests while still marked paused so
520+ # a refill past the limit does not re-pause an already-paused
521+ # transport; only resume below once it stayed under the limit.
522+ self .data_received (b"" )
523+ if len (self ._messages ) >= self ._max_msg_queue_size :
524+ return
525+ self ._msg_queue_paused = False
526+ if not self ._reading_paused and self .transport is not None :
527+ try :
528+ self .transport .resume_reading ()
529+ except PAUSE_RESUME_READING_ERRORS :
530+ # Transport lacks flow control; nothing to resume. Intentionally
531+ # ignored (see PAUSE_RESUME_READING_ERRORS; do not use suppress).
532+ pass
533+
480534 def keep_alive (self , val : bool ) -> None :
481535 """Set keep-alive connection mode.
482536
@@ -606,6 +660,18 @@ async def start(self) -> None:
606660
607661 message , payload = self ._messages .popleft ()
608662
663+ # Free a parser slot; resume reading once drained to low water so
664+ # pipelining keeps flowing while this request is handled.
665+ # no branch: _parser is only None after connection_lost, whose path
666+ # exits this loop, so the None case is not reachably exercisable.
667+ if self ._parser is not None : # pragma: no branch
668+ self ._parser .message_consumed ()
669+ if (
670+ self ._msg_queue_paused
671+ and len (self ._messages ) <= self ._msg_queue_resume_size
672+ ):
673+ self ._resume_msg_queue_reading ()
674+
609675 # time is only fetched if logging is enabled as otherwise
610676 # its thrown away and never used.
611677 start = loop .time () if self ._logging_enabled else None
0 commit comments