6666_DATA , _HEADERS , _RST_STREAM , _SETTINGS , _GOAWAY , _CONTINUATION = 0x0 , 0x1 , 0x3 , 0x4 , 0x7 , 0x9
6767_END_STREAM_FLAG , _ACK_FLAG , _END_HEADERS_FLAG = 0x1 , 0x1 , 0x4
6868_MAX_CONCURRENT_STREAMS = 0x3
69- _PROTOCOL_ERROR , _REFUSED_STREAM = 0x1 , 0x7
69+ _PUSH_PROMISE = 0x5
70+ _PROTOCOL_ERROR , _CANCEL , _ENHANCE_YOUR_CALM = 0x1 , 0x8 , 0xB
7071# Calls one relayed connection may hold open when the gRPC server advertises no
7172# SETTINGS_MAX_CONCURRENT_STREAMS (grpcio advertises none unless the service sets
72- # grpc.max_concurrent_streams, as the scaffold's server does). A call past the limit is
73- # refused alone, with REFUSED_STREAM, which a client retries safely: the server never
74- # processed it. What the face tracks per connection is bounded by it.
73+ # grpc.max_concurrent_streams, as the scaffold's server does). Past it the face resets
74+ # the call alone: ENHANCE_YOUR_CALM to the client (gRPC's RESOURCE_EXHAUSTED, which a
75+ # client does not retry on its own), and CANCEL to the server. The server still receives
76+ # the call's header block, which keeps the connection's header compression in step, so it
77+ # may have started the call before the cancel: the face never promises it did not. What
78+ # the face tracks per connection is bounded by it. When the server advertises a limit the
79+ # face enforces nothing: the server refuses what is past it (REFUSED_STREAM), truthfully.
7580DEFAULT_MAX_OPEN_CALLS = 1000
7681# Bytes of frames for the client (refusals, a GOAWAY) the relay holds while the server's
7782# frame in flight completes; past it the relay reads nothing more from the client until
@@ -265,49 +270,72 @@ def __init__(self, on_frame):
265270 self ._payload = bytearray ()
266271 self ._keep = 0 # payload bytes still to read
267272 self ._skip = 0 # payload bytes still to skip
273+ self ._in_block = False # inside a header block: HEADERS ... CONTINUATION
268274 self .error : _ProtocolError | None = None
269275
270276 @property
271277 def at_boundary (self ) -> bool :
272- return not self ._header and not self ._keep and not self ._skip
273-
274- def feed (self , data : bytes ) -> list [tuple [int , bytes ]]:
278+ """Between two frames, and outside a header block: where another frame may go
279+ (RFC 9113 section 6.10: nothing comes between a HEADERS and its CONTINUATION)."""
280+ return not self ._header and not self ._keep and not self ._skip and not self ._in_block
281+
282+ def feed (self , data : bytes , every_end : bool = False ) -> list [tuple [int , bytes ]]:
283+ """Where frames end in ``data``: each end where ``on_frame`` returned bytes to
284+ write after the frame, and, with ``every_end``, each end where another frame may
285+ go. After a ``_ProtocolError``, the last end is the last place another frame
286+ may go before the offending frame."""
275287 ends : list [tuple [int , bytes ]] = []
276- at = 0
288+ at , size = 0 , len (data )
289+ clear = 0 if self .at_boundary else - 1 # the last such place in ``data``
277290 try :
278- while at < len ( data ) :
291+ while at < size :
279292 if self ._keep :
280- step = min (self ._keep , len ( data ) - at )
293+ step = min (self ._keep , size - at )
281294 self ._payload += data [at : at + step ]
282295 self ._keep -= step
283296 at += step
284- if not self ._keep :
285- self ._emit ()
297+ if self ._keep :
298+ break
299+ self ._emit ()
286300 elif self ._skip :
287- step = min (self ._skip , len ( data ) - at )
301+ step = min (self ._skip , size - at )
288302 self ._skip -= step
289303 at += step
290- else :
291- step = min (_FRAME .size - len (self ._header ), len (data ) - at )
304+ if self ._skip :
305+ break
306+ elif self ._header or size - at < _FRAME .size :
307+ step = min (_FRAME .size - len (self ._header ), size - at )
292308 self ._header += data [at : at + step ]
293309 at += step
294310 if len (self ._header ) < _FRAME .size :
295- continue
296- high , low , kind , flags , stream = _FRAME .unpack (self ._header )
311+ break
312+ self . _start ( * _FRAME .unpack (self ._header ) )
297313 self ._header .clear ()
298- length = high << 16 | low
299- self ._frame = (kind , flags , stream & 0x7FFFFFFF )
300- self ._keep = min (length , _CONTROL_PAYLOAD ) if kind in (_SETTINGS , _GOAWAY ) else 0
301- self ._skip = length - self ._keep
302- if not self ._keep :
303- self ._emit ()
304- if self .at_boundary :
305- ends .append ((at , self ._after ))
306- self ._after = b""
314+ else : # a whole frame header in this chunk: read it in place
315+ self ._start (* _FRAME .unpack_from (data , at ))
316+ at += _FRAME .size
317+ if not (self ._keep or self ._skip or self ._in_block ):
318+ clear = at
319+ if every_end or self ._after :
320+ ends .append ((at , self ._after ))
321+ self ._after = b""
307322 except _ProtocolError as refusal :
308323 self .error = refusal
324+ if clear > (ends [- 1 ][0 ] if ends else 0 ):
325+ ends .append ((clear , b"" ))
309326 return ends
310327
328+ def _start (self , high , low , kind , flags , stream ):
329+ """A frame header read: what of its payload to keep, and its frame if none."""
330+ length = high << 16 | low
331+ self ._frame = (kind , flags , stream & 0x7FFFFFFF )
332+ if kind in (_HEADERS , _PUSH_PROMISE , _CONTINUATION ):
333+ self ._in_block = not flags & _END_HEADERS_FLAG
334+ self ._keep = min (length , _CONTROL_PAYLOAD ) if kind in (_SETTINGS , _GOAWAY ) else 0
335+ self ._skip = length - self ._keep
336+ if not self ._keep :
337+ self ._emit ()
338+
311339 def _emit (self ):
312340 payload = bytes (self ._payload )
313341 self ._payload .clear ()
@@ -339,14 +367,13 @@ async def _relay(self, reader, writer, head):
339367 The frame headers say which calls are open: a client HEADERS on a new stream (odd,
340368 above every stream before it) opens one, and the server's END_STREAM, either
341369 side's RST_STREAM, or the server's GOAWAY above the stream ends it. A call past
342- what the server allows at once (SETTINGS_MAX_CONCURRENT_STREAMS,
343- ``DEFAULT_MAX_OPEN_CALLS`` when it advertises none) is refused alone: the client
344- gets RST_STREAM REFUSED_STREAM, and the server its header block, which keeps the
345- connection's header compression in step, then the same reset. A refusal is
346- written between two of the server's frames; while more than
347- ``_MAX_QUEUED_FOR_CLIENT`` bytes of them wait, the client is not read. A client
348- that opens an even stream, which no HTTP/2 client does, gets a GOAWAY naming why,
349- and the connection is closed."""
370+ the server's own SETTINGS_MAX_CONCURRENT_STREAMS is relayed as it came, for the
371+ server to refuse. When the server advertises none, a call past
372+ ``DEFAULT_MAX_OPEN_CALLS`` is reset alone (see there). The face's frames for the
373+ client are written only between two of the server's frames and outside a header
374+ block; while more than ``_MAX_QUEUED_FOR_CLIENT`` bytes of them wait, the client
375+ is not read. A client that opens an even stream, which no HTTP/2 client does, gets
376+ a GOAWAY naming why, and the connection is closed."""
350377 upstream_reader , upstream_writer = await asyncio .open_connection (* self .grpc_address )
351378 upstream_writer .write (head )
352379 loop = asyncio .get_running_loop ()
@@ -355,6 +382,7 @@ async def _relay(self, reader, writer, head):
355382 highest = accepted = 0
356383 refusing = 0 # the refused stream whose header block is still arriving
357384 max_calls = DEFAULT_MAX_OPEN_CALLS
385+ advertised = False # the server sent SETTINGS_MAX_CONCURRENT_STREAMS
358386 to_client = bytearray () # frames for the client, written at a frame boundary
359387 flushed = asyncio .Event () # set when the server pump wrote them
360388 told = False # the face wrote to the client since the client pump last drained
@@ -368,9 +396,10 @@ def from_client(kind, flags, stream, payload):
368396 if len (calls ) < max_calls :
369397 calls .add (stream )
370398 accepted = stream
371- else :
399+ elif not advertised :
372400 refusing = stream
373- tell_client (_rst_stream (stream , _REFUSED_STREAM ))
401+ tell_client (_rst_stream (stream , _ENHANCE_YOUR_CALM ))
402+ # Past an advertised limit the server refuses the call itself.
374403 elif kind == _HEADERS and stream % 2 == 0 :
375404 raise _ProtocolError (f"HEADERS on stream { stream } , which a server opens" )
376405 elif kind == _RST_STREAM :
@@ -380,18 +409,18 @@ def from_client(kind, flags, stream, payload):
380409 if refusing and stream == refusing and kind in (_HEADERS , _CONTINUATION ):
381410 if flags & _END_HEADERS_FLAG :
382411 refusing = 0
383- return _rst_stream (stream , _REFUSED_STREAM ) # after its header block
412+ return _rst_stream (stream , _CANCEL ) # after its header block
384413 return None
385414
386415 def from_server (kind , flags , stream , payload ):
387- nonlocal max_calls
416+ nonlocal max_calls , advertised
388417 if kind == _RST_STREAM or (kind in (_DATA , _HEADERS ) and flags & _END_STREAM_FLAG ):
389418 calls .discard (stream )
390419 elif kind == _SETTINGS and not flags & _ACK_FLAG :
391420 for at in range (0 , len (payload ) - 5 , 6 ):
392421 key , value = struct .unpack_from (">HI" , payload , at )
393422 if key == _MAX_CONCURRENT_STREAMS :
394- max_calls = value
423+ max_calls , advertised = value , True
395424 elif kind == _GOAWAY and len (payload ) >= 4 :
396425 # Streams above the last one the server processes are never answered.
397426 last_stream = struct .unpack_from (">I" , payload )[0 ] & 0x7FFFFFFF
@@ -403,7 +432,9 @@ def from_server(kind, flags, stream, payload):
403432 def tell_client (frame ):
404433 nonlocal told
405434 # Only between two of the server's frames: queued while one is half relayed,
406- # and written by the server pump at that frame's end.
435+ # and written by the server pump at that frame's end. Nothing to a client gone.
436+ if writer .is_closing ():
437+ return
407438 if server .at_boundary and not to_client :
408439 writer .write (frame )
409440 told = True
@@ -417,30 +448,40 @@ async def written():
417448 await flushed .wait ()
418449
419450 def relay (chunk , sink , frames , ends , queued ):
451+ # As few writes as the frames to insert allow: one per insertion point.
420452 at = 0
421453 for end , after in ends :
454+ if not after and not queued :
455+ continue
422456 sink .write (chunk [at :end ] + after )
423457 at = end
424458 if queued :
425459 sink .write (bytes (queued ))
426460 queued .clear ()
427461 flushed .set ()
428- if frames .error is None :
429- sink .write (chunk [at :])
462+ stop = len (chunk ) if frames .error is None else (ends [- 1 ][0 ] if ends else at )
463+ if stop > at :
464+ sink .write (chunk [at :stop ])
430465
431466 async def pump (source , sink , frames , queued ):
432467 nonlocal last , told
433468 try :
434469 while chunk := await source .read (65536 ):
470+ if writer .is_closing ():
471+ return # the client is gone: nothing left to relay
435472 last = loop .time ()
436- relay (chunk , sink , frames , frames .feed (chunk ), queued )
473+ relay (chunk , sink , frames , frames .feed (chunk , bool ( queued ) ), queued )
437474 if frames .error is not None :
438475 _log .info ("connect face: closing a gRPC connection: %s" , frames .error )
439476 tell_client (_goaway (accepted , _PROTOCOL_ERROR , str (frames .error )))
440477 try :
441478 await asyncio .wait_for (written (), _GOAWAY_GRACE_S )
442479 except TimeoutError :
443- pass
480+ _log .warning (
481+ "connect face: the GOAWAY was not delivered: the server's "
482+ "frame in flight did not complete within %s s" ,
483+ _GOAWAY_GRACE_S ,
484+ )
444485 return
445486 await sink .drain ()
446487 if sink is not writer :
0 commit comments