httpunk 0.4.1__tar.gz → 0.4.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (143) hide show
  1. {httpunk-0.4.1 → httpunk-0.4.2}/Cargo.lock +1 -1
  2. {httpunk-0.4.1 → httpunk-0.4.2}/Cargo.toml +1 -1
  3. {httpunk-0.4.1 → httpunk-0.4.2}/PKG-INFO +1 -1
  4. {httpunk-0.4.1 → httpunk-0.4.2}/httpunk/_httpunk.pyi +4 -0
  5. {httpunk-0.4.1 → httpunk-0.4.2}/httpunk/h2/client.py +32 -7
  6. {httpunk-0.4.1 → httpunk-0.4.2}/httpunk/h2/connection.py +56 -41
  7. {httpunk-0.4.1 → httpunk-0.4.2}/src/h2/conn.rs +355 -31
  8. {httpunk-0.4.1 → httpunk-0.4.2}/src/h2/mod.rs +1 -0
  9. {httpunk-0.4.1 → httpunk-0.4.2}/tests/test_h1_server.py +6 -0
  10. {httpunk-0.4.1 → httpunk-0.4.2}/tests/test_h2_flow.py +147 -11
  11. httpunk-0.4.2/tests/test_h2_flow_control.py +384 -0
  12. {httpunk-0.4.1 → httpunk-0.4.2}/tests/test_h2_server.py +10 -4
  13. {httpunk-0.4.1 → httpunk-0.4.2}/tests/test_util_pool.py +76 -70
  14. httpunk-0.4.1/tests/test_h2_flow_control.py +0 -174
  15. {httpunk-0.4.1 → httpunk-0.4.2}/LICENSE +0 -0
  16. {httpunk-0.4.1 → httpunk-0.4.2}/README.md +0 -0
  17. {httpunk-0.4.1 → httpunk-0.4.2}/crates/vendor-h2/Cargo.toml +0 -0
  18. {httpunk-0.4.1 → httpunk-0.4.2}/crates/vendor-h2/LICENSE +0 -0
  19. {httpunk-0.4.1 → httpunk-0.4.2}/crates/vendor-h2/UPSTREAM_VERSION +0 -0
  20. {httpunk-0.4.1 → httpunk-0.4.2}/crates/vendor-h2/src/codec/error.rs +0 -0
  21. {httpunk-0.4.1 → httpunk-0.4.2}/crates/vendor-h2/src/codec/mod.rs +0 -0
  22. {httpunk-0.4.1 → httpunk-0.4.2}/crates/vendor-h2/src/error.rs +0 -0
  23. {httpunk-0.4.1 → httpunk-0.4.2}/crates/vendor-h2/src/ext.rs +0 -0
  24. {httpunk-0.4.1 → httpunk-0.4.2}/crates/vendor-h2/src/frame/data.rs +0 -0
  25. {httpunk-0.4.1 → httpunk-0.4.2}/crates/vendor-h2/src/frame/go_away.rs +0 -0
  26. {httpunk-0.4.1 → httpunk-0.4.2}/crates/vendor-h2/src/frame/head.rs +0 -0
  27. {httpunk-0.4.1 → httpunk-0.4.2}/crates/vendor-h2/src/frame/headers.rs +0 -0
  28. {httpunk-0.4.1 → httpunk-0.4.2}/crates/vendor-h2/src/frame/mod.rs +0 -0
  29. {httpunk-0.4.1 → httpunk-0.4.2}/crates/vendor-h2/src/frame/ping.rs +0 -0
  30. {httpunk-0.4.1 → httpunk-0.4.2}/crates/vendor-h2/src/frame/priority.rs +0 -0
  31. {httpunk-0.4.1 → httpunk-0.4.2}/crates/vendor-h2/src/frame/reason.rs +0 -0
  32. {httpunk-0.4.1 → httpunk-0.4.2}/crates/vendor-h2/src/frame/reset.rs +0 -0
  33. {httpunk-0.4.1 → httpunk-0.4.2}/crates/vendor-h2/src/frame/settings.rs +0 -0
  34. {httpunk-0.4.1 → httpunk-0.4.2}/crates/vendor-h2/src/frame/stream_id.rs +0 -0
  35. {httpunk-0.4.1 → httpunk-0.4.2}/crates/vendor-h2/src/frame/util.rs +0 -0
  36. {httpunk-0.4.1 → httpunk-0.4.2}/crates/vendor-h2/src/frame/window_update.rs +0 -0
  37. {httpunk-0.4.1 → httpunk-0.4.2}/crates/vendor-h2/src/hpack/decoder.rs +0 -0
  38. {httpunk-0.4.1 → httpunk-0.4.2}/crates/vendor-h2/src/hpack/encoder.rs +0 -0
  39. {httpunk-0.4.1 → httpunk-0.4.2}/crates/vendor-h2/src/hpack/header.rs +0 -0
  40. {httpunk-0.4.1 → httpunk-0.4.2}/crates/vendor-h2/src/hpack/huffman/mod.rs +0 -0
  41. {httpunk-0.4.1 → httpunk-0.4.2}/crates/vendor-h2/src/hpack/huffman/table.rs +0 -0
  42. {httpunk-0.4.1 → httpunk-0.4.2}/crates/vendor-h2/src/hpack/mod.rs +0 -0
  43. {httpunk-0.4.1 → httpunk-0.4.2}/crates/vendor-h2/src/hpack/table.rs +0 -0
  44. {httpunk-0.4.1 → httpunk-0.4.2}/crates/vendor-h2/src/lib.rs +0 -0
  45. {httpunk-0.4.1 → httpunk-0.4.2}/crates/vendor-h2/src/proto/error.rs +0 -0
  46. {httpunk-0.4.1 → httpunk-0.4.2}/crates/vendor-h2/src/proto/mod.rs +0 -0
  47. {httpunk-0.4.1 → httpunk-0.4.2}/crates/vendor-h2/src/proto/streams/flow_control.rs +0 -0
  48. {httpunk-0.4.1 → httpunk-0.4.2}/crates/vendor-h2/src/proto/streams/mod.rs +0 -0
  49. {httpunk-0.4.1 → httpunk-0.4.2}/crates/vendor-h2/src/proto/streams/state.rs +0 -0
  50. {httpunk-0.4.1 → httpunk-0.4.2}/crates/vendor-hyper/Cargo.toml +0 -0
  51. {httpunk-0.4.1 → httpunk-0.4.2}/crates/vendor-hyper/LICENSE +0 -0
  52. {httpunk-0.4.1 → httpunk-0.4.2}/crates/vendor-hyper/UPSTREAM_VERSION +0 -0
  53. {httpunk-0.4.1 → httpunk-0.4.2}/crates/vendor-hyper/src/body/length.rs +0 -0
  54. {httpunk-0.4.1 → httpunk-0.4.2}/crates/vendor-hyper/src/body/mod.rs +0 -0
  55. {httpunk-0.4.1 → httpunk-0.4.2}/crates/vendor-hyper/src/cfg.rs +0 -0
  56. {httpunk-0.4.1 → httpunk-0.4.2}/crates/vendor-hyper/src/common/date.rs +0 -0
  57. {httpunk-0.4.1 → httpunk-0.4.2}/crates/vendor-hyper/src/common/mod.rs +0 -0
  58. {httpunk-0.4.1 → httpunk-0.4.2}/crates/vendor-hyper/src/error.rs +0 -0
  59. {httpunk-0.4.1 → httpunk-0.4.2}/crates/vendor-hyper/src/ext/h1_reason_phrase.rs +0 -0
  60. {httpunk-0.4.1 → httpunk-0.4.2}/crates/vendor-hyper/src/ext/informational.rs +0 -0
  61. {httpunk-0.4.1 → httpunk-0.4.2}/crates/vendor-hyper/src/ext/mod.rs +0 -0
  62. {httpunk-0.4.1 → httpunk-0.4.2}/crates/vendor-hyper/src/headers.rs +0 -0
  63. {httpunk-0.4.1 → httpunk-0.4.2}/crates/vendor-hyper/src/lib.rs +0 -0
  64. {httpunk-0.4.1 → httpunk-0.4.2}/crates/vendor-hyper/src/proto/h1/decode.rs +0 -0
  65. {httpunk-0.4.1 → httpunk-0.4.2}/crates/vendor-hyper/src/proto/h1/encode.rs +0 -0
  66. {httpunk-0.4.1 → httpunk-0.4.2}/crates/vendor-hyper/src/proto/h1/httpunk.rs +0 -0
  67. {httpunk-0.4.1 → httpunk-0.4.2}/crates/vendor-hyper/src/proto/h1/io.rs +0 -0
  68. {httpunk-0.4.1 → httpunk-0.4.2}/crates/vendor-hyper/src/proto/h1/mod.rs +0 -0
  69. {httpunk-0.4.1 → httpunk-0.4.2}/crates/vendor-hyper/src/proto/h1/role.rs +0 -0
  70. {httpunk-0.4.1 → httpunk-0.4.2}/crates/vendor-hyper/src/proto/mod.rs +0 -0
  71. {httpunk-0.4.1 → httpunk-0.4.2}/crates/vendor-hyper/src/trace.rs +0 -0
  72. {httpunk-0.4.1 → httpunk-0.4.2}/crates/vendor-hyper-util/Cargo.toml +0 -0
  73. {httpunk-0.4.1 → httpunk-0.4.2}/crates/vendor-hyper-util/LICENSE +0 -0
  74. {httpunk-0.4.1 → httpunk-0.4.2}/crates/vendor-hyper-util/UPSTREAM_VERSION +0 -0
  75. {httpunk-0.4.1 → httpunk-0.4.2}/crates/vendor-hyper-util/src/client/mod.rs +0 -0
  76. {httpunk-0.4.1 → httpunk-0.4.2}/crates/vendor-hyper-util/src/client/proxy/matcher.rs +0 -0
  77. {httpunk-0.4.1 → httpunk-0.4.2}/crates/vendor-hyper-util/src/client/proxy/mod.rs +0 -0
  78. {httpunk-0.4.1 → httpunk-0.4.2}/crates/vendor-hyper-util/src/lib.rs +0 -0
  79. {httpunk-0.4.1 → httpunk-0.4.2}/httpunk/__init__.py +0 -0
  80. {httpunk-0.4.1 → httpunk-0.4.2}/httpunk/_backend/__init__.py +0 -0
  81. {httpunk-0.4.1 → httpunk-0.4.2}/httpunk/_backend/asyncio.py +0 -0
  82. {httpunk-0.4.1 → httpunk-0.4.2}/httpunk/_backend/tonio.py +0 -0
  83. {httpunk-0.4.1 → httpunk-0.4.2}/httpunk/_common.py +0 -0
  84. {httpunk-0.4.1 → httpunk-0.4.2}/httpunk/asyncio.py +0 -0
  85. {httpunk-0.4.1 → httpunk-0.4.2}/httpunk/exceptions.py +0 -0
  86. {httpunk-0.4.1 → httpunk-0.4.2}/httpunk/h1/__init__.py +0 -0
  87. {httpunk-0.4.1 → httpunk-0.4.2}/httpunk/h1/client.py +0 -0
  88. {httpunk-0.4.1 → httpunk-0.4.2}/httpunk/h1/connection.py +0 -0
  89. {httpunk-0.4.1 → httpunk-0.4.2}/httpunk/h1/server.py +0 -0
  90. {httpunk-0.4.1 → httpunk-0.4.2}/httpunk/h1/share.py +0 -0
  91. {httpunk-0.4.1 → httpunk-0.4.2}/httpunk/h2/__init__.py +0 -0
  92. {httpunk-0.4.1 → httpunk-0.4.2}/httpunk/h2/server.py +0 -0
  93. {httpunk-0.4.1 → httpunk-0.4.2}/httpunk/h2/share.py +0 -0
  94. {httpunk-0.4.1 → httpunk-0.4.2}/httpunk/h2/stream.py +0 -0
  95. {httpunk-0.4.1 → httpunk-0.4.2}/httpunk/http.py +0 -0
  96. {httpunk-0.4.1 → httpunk-0.4.2}/httpunk/py.typed +0 -0
  97. {httpunk-0.4.1 → httpunk-0.4.2}/httpunk/types.py +0 -0
  98. {httpunk-0.4.1 → httpunk-0.4.2}/httpunk/util/__init__.py +0 -0
  99. {httpunk-0.4.1 → httpunk-0.4.2}/httpunk/util/auto.py +0 -0
  100. {httpunk-0.4.1 → httpunk-0.4.2}/httpunk/util/client.py +0 -0
  101. {httpunk-0.4.1 → httpunk-0.4.2}/httpunk/util/graceful.py +0 -0
  102. {httpunk-0.4.1 → httpunk-0.4.2}/httpunk/util/pool.py +0 -0
  103. {httpunk-0.4.1 → httpunk-0.4.2}/httpunk/util/proxy.py +0 -0
  104. {httpunk-0.4.1 → httpunk-0.4.2}/pyproject.toml +0 -0
  105. {httpunk-0.4.1 → httpunk-0.4.2}/src/errors.rs +0 -0
  106. {httpunk-0.4.1 → httpunk-0.4.2}/src/h1/codec.rs +0 -0
  107. {httpunk-0.4.1 → httpunk-0.4.2}/src/h1/conn.rs +0 -0
  108. {httpunk-0.4.1 → httpunk-0.4.2}/src/h1/errors.rs +0 -0
  109. {httpunk-0.4.1 → httpunk-0.4.2}/src/h1/mod.rs +0 -0
  110. {httpunk-0.4.1 → httpunk-0.4.2}/src/h2/codec.rs +0 -0
  111. {httpunk-0.4.1 → httpunk-0.4.2}/src/h2/errors.rs +0 -0
  112. {httpunk-0.4.1 → httpunk-0.4.2}/src/h2/reason.rs +0 -0
  113. {httpunk-0.4.1 → httpunk-0.4.2}/src/h2/settings.rs +0 -0
  114. {httpunk-0.4.1 → httpunk-0.4.2}/src/h2/streams.rs +0 -0
  115. {httpunk-0.4.1 → httpunk-0.4.2}/src/http/mod.rs +0 -0
  116. {httpunk-0.4.1 → httpunk-0.4.2}/src/lib.rs +0 -0
  117. {httpunk-0.4.1 → httpunk-0.4.2}/src/proxy.rs +0 -0
  118. {httpunk-0.4.1 → httpunk-0.4.2}/src/py.rs +0 -0
  119. {httpunk-0.4.1 → httpunk-0.4.2}/tests/_client.py +0 -0
  120. {httpunk-0.4.1 → httpunk-0.4.2}/tests/_transport.py +0 -0
  121. {httpunk-0.4.1 → httpunk-0.4.2}/tests/conftest.py +0 -0
  122. {httpunk-0.4.1 → httpunk-0.4.2}/tests/test_asyncio_backend.py +0 -0
  123. {httpunk-0.4.1 → httpunk-0.4.2}/tests/test_asyncio_client_protocol.py +0 -0
  124. {httpunk-0.4.1 → httpunk-0.4.2}/tests/test_asyncio_e2e.py +0 -0
  125. {httpunk-0.4.1 → httpunk-0.4.2}/tests/test_asyncio_protocol.py +0 -0
  126. {httpunk-0.4.1 → httpunk-0.4.2}/tests/test_backend.py +0 -0
  127. {httpunk-0.4.1 → httpunk-0.4.2}/tests/test_error_retention.py +0 -0
  128. {httpunk-0.4.1 → httpunk-0.4.2}/tests/test_h1_codec.py +0 -0
  129. {httpunk-0.4.1 → httpunk-0.4.2}/tests/test_h1_conn.py +0 -0
  130. {httpunk-0.4.1 → httpunk-0.4.2}/tests/test_h1_decode.py +0 -0
  131. {httpunk-0.4.1 → httpunk-0.4.2}/tests/test_h2_codec.py +0 -0
  132. {httpunk-0.4.1 → httpunk-0.4.2}/tests/test_h2_conn.py +0 -0
  133. {httpunk-0.4.1 → httpunk-0.4.2}/tests/test_h2_errors.py +0 -0
  134. {httpunk-0.4.1 → httpunk-0.4.2}/tests/test_h2_ready.py +0 -0
  135. {httpunk-0.4.1 → httpunk-0.4.2}/tests/test_h2_settings.py +0 -0
  136. {httpunk-0.4.1 → httpunk-0.4.2}/tests/test_h2_state.py +0 -0
  137. {httpunk-0.4.1 → httpunk-0.4.2}/tests/test_h2_write_batch.py +0 -0
  138. {httpunk-0.4.1 → httpunk-0.4.2}/tests/test_headermap.py +0 -0
  139. {httpunk-0.4.1 → httpunk-0.4.2}/tests/test_util_auto.py +0 -0
  140. {httpunk-0.4.1 → httpunk-0.4.2}/tests/test_util_connect.py +0 -0
  141. {httpunk-0.4.1 → httpunk-0.4.2}/tests/test_util_graceful.py +0 -0
  142. {httpunk-0.4.1 → httpunk-0.4.2}/tests/test_util_proxy.py +0 -0
  143. {httpunk-0.4.1 → httpunk-0.4.2}/tests/test_util_tls.py +0 -0
@@ -118,7 +118,7 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
118
118
 
119
119
  [[package]]
120
120
  name = "httpunk"
121
- version = "0.4.1"
121
+ version = "0.4.2"
122
122
  dependencies = [
123
123
  "bytes",
124
124
  "h2",
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "httpunk"
3
- version = "0.4.1"
3
+ version = "0.4.2"
4
4
  description = "The Rust HTTP library for Python"
5
5
  authors = [
6
6
  "Giovanni Barillari <g@baro.dev>",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: httpunk
3
- Version: 0.4.1
3
+ Version: 0.4.2
4
4
  Classifier: Development Status :: 4 - Beta
5
5
  Classifier: Intended Audience :: Developers
6
6
  Classifier: License :: OSI Approved :: BSD License
@@ -867,6 +867,7 @@ H2_FLAG_WAKE: int # bytes were queued: wake the write pump
867
867
  H2_FLAG_SLOT_FREED: int # client: a MAX_CONCURRENT slot freed / the limit changed
868
868
  H2_FLAG_CONN_DONE: int # the connection failed or finished: wake the role waiters
869
869
  H2_FLAG_STOP_ACCEPTING: int # server: the graceful drain completed
870
+ H2_FLAG_CAPACITY: int # the connection-capacity queue moved: ask `next_capacity_wake`
870
871
 
871
872
  # `H2RecvHeadersVerdict.kind`.
872
873
  H2_HEADERS_IGNORED: int
@@ -1060,6 +1061,9 @@ class H2Streams:
1060
1061
  def stream_send_window(self, sid: int) -> int | None: ...
1061
1062
  @property
1062
1063
  def conn_send_window(self) -> int: ...
1064
+ def next_capacity_wake(self) -> object | None:
1065
+ """After `H2_FLAG_CAPACITY`: the stream handle now first in line for connection
1066
+ window (wake its `window_evt`), or None."""
1063
1067
  @property
1064
1068
  def conn_recv_available(self) -> int: ...
1065
1069
  @property
@@ -169,16 +169,41 @@ class Connection(H2ConnectionBase):
169
169
  """Send the request body concurrently with (not before) the caller awaiting
170
170
  the response head: h2's `SendStream` (body) and `ResponseFuture` (head) are
171
171
  independent — an early response that resets the request body (413 / redirect
172
- during upload) must still deliver the received response. The write runs in the
173
- connection's write scope so it can outlive `send_request` (full duplex) and is
174
- torn down when the connection closes."""
175
- self._write_scope.spawn(self._write_body(stream, body, trailers))
172
+ during upload) must still deliver the received response. hyper client.rs
173
+ `poll_pipe`: the pipe is polled once inline, and only if that leaves it pending
174
+ is it handed to the executor as a bare task that outlives `send_request` (full
175
+ duplex) and ends on its own — the body sent, or the stream stopped; `close()`
176
+ waits for the last one (`release_writers`), it never cancels them."""
177
+ offset = 0
178
+ if body is None or isinstance(body, bytes):
179
+ offset = self._send_body_inline(stream, body, trailers)
180
+ if offset is None:
181
+ return
182
+ if self.writer_start(): # refused: the stream is stopped already, nothing to write
183
+ self.backend.spawn_without_results(self._write_body(stream, body, trailers, offset))
184
+
185
+ def _send_body_inline(self, stream, body, trailers):
186
+ """The inline first poll of a buffered body (hyper's `Full`: one chunk, EOS
187
+ set): queue what the windows take NOW. Returns the offset a background writer
188
+ resumes from, or None when nothing is left to write — the body (and trailers)
189
+ fully queued, or the stream already stopped, which the response reader sees."""
190
+ try:
191
+ offset = self._try_send_data(stream, body, 0, trailers is None) if body else None
192
+ if offset is None and trailers is not None:
193
+ self._send_trailers(stream, trailers)
194
+ except Exception:
195
+ return None
196
+ return offset
176
197
 
177
- async def _write_body(self, stream, body, trailers=None):
198
+ async def _write_body(self, stream, body, trailers, offset):
178
199
  # The stream may error/reset mid-send: that is surfaced when the response body
179
200
  # is read; a background writer has nowhere to propagate to.
180
- with contextlib.suppress(Exception):
181
- await self._send_body(stream, body, trailers)
201
+ try:
202
+ with contextlib.suppress(Exception):
203
+ await self._send_body(stream, body, trailers, offset)
204
+ finally:
205
+ if self.writer_end():
206
+ self._writers_evt.set()
182
207
 
183
208
 
184
209
  class H2Connection(BaseClientConnection):
@@ -27,6 +27,7 @@ import contextlib
27
27
  from .. import _backend
28
28
  from .._common import PUMP_ABANDONED, PUMP_DONE, aclose_body, aiter_body, event_result
29
29
  from .._httpunk import (
30
+ H2_FLAG_CAPACITY,
30
31
  H2_FLAG_CONN_DONE,
31
32
  H2_FLAG_SLOT_FREED,
32
33
  H2_FLAG_STOP_ACCEPTING,
@@ -70,9 +71,12 @@ class H2ConnectionBase(H2Streams):
70
71
  # pumps read it, `close()` closes it — never reassigned.
71
72
  self.backend = _backend.resolve(backend)
72
73
  self._transport = transport
73
- # A scope for background request-body writers (client full-duplex send, F6) —
74
- # the one group that legitimately gets CANCELLED at teardown.
75
- self._write_scope = self.backend.scope()
74
+ # Set by the last background request-body writer to finish once `close()` is
75
+ # draining (client full-duplex send, F6): hyper's connection task ends only
76
+ # after every `PipeToSendStream` dropped its `conn_drop_ref`. Writers are bare
77
+ # tasks, never cancelled: a stopped stream (peer reset, GOAWAY, the connection
78
+ # failed or closed) is what ends one.
79
+ self._writers_evt = self.backend.event()
76
80
  # Orders socket writes: the write pump and the inline flushes (`_flush`).
77
81
  self._send_lock = self.backend.lock()
78
82
  # Wakes the write pump. Set by the driver after any verdict that appended to
@@ -109,6 +113,12 @@ class H2ConnectionBase(H2Streams):
109
113
  self._on_conn_done()
110
114
  if flags & H2_FLAG_STOP_ACCEPTING:
111
115
  self._on_stop_accepting()
116
+ if flags & H2_FLAG_CAPACITY:
117
+ # The front of the connection-capacity queue moved on: hand the turn
118
+ # to the next sender in line (h2 `assign_connection_capacity`).
119
+ handle = self.next_capacity_wake()
120
+ if handle is not None:
121
+ handle.window_evt.set()
112
122
 
113
123
  @staticmethod
114
124
  def _notify_stopped(handle, stop, notify_body):
@@ -187,7 +197,6 @@ class H2ConnectionBase(H2Streams):
187
197
  # Shared handshake start (h2 client.rs/server.rs `handshake`): the preface
188
198
  # (client) + our initial SETTINGS + the initial WINDOW_UPDATE(0) are queued by
189
199
  # the state and flushed here, BEFORE the pumps start, so nothing can precede them.
190
- await self._write_scope.__aenter__()
191
200
  self.begin()
192
201
  await self._flush()
193
202
  # Bare tasks (`spawn_without_results`), never cancelled: every point they can
@@ -213,20 +222,21 @@ class H2ConnectionBase(H2Streams):
213
222
  async def close(self):
214
223
  # Stop the write pump by SIGNAL, flush-then-exit, and JOIN — never cancel it
215
224
  # (a cancellation landing between its take and its flush would lose control
216
- # frames). Then cancel + join the body writers, drain what their teardown
217
- # enqueued (h2 `codec.shutdown` = flush THEN shutdown, F23), shut the
218
- # transport down — `FramedWrite::shutdown` ends in the IO's `poll_shutdown`
219
- # (`close_notify` over TLS), which is also what ends the read pump's parked
220
- # read — join it, and wake every straggler. On a transport that already died
221
- # the alert cannot go out and the shutdown is the plain close hyper's drop
222
- # would be: wire-identical.
225
+ # frames). Drain what is enqueued (h2 `codec.shutdown` = flush THEN shutdown,
226
+ # F23), shut the transport down — `FramedWrite::shutdown` ends in the IO's
227
+ # `poll_shutdown` (`close_notify` over TLS), which is also what ends the read
228
+ # pump's parked read — join it, and wake every straggler. On a transport that
229
+ # already died the alert cannot go out and the shutdown is the plain close
230
+ # hyper's drop would be: wire-identical. Last, wait for the background
231
+ # request-body writers: every stream is stopped by now, so each one wakes and
232
+ # exits — hyper's connection task likewise ends only after the last
233
+ # `PipeToSendStream` dropped its `conn_drop_ref`; the writers are never
234
+ # cancelled, here or there.
223
235
  self.stop_pump()
224
236
  self._write_evt.set()
225
237
  handle = self.take_pump_handle()
226
238
  if handle is not None:
227
239
  await handle
228
- self._write_scope.cancel()
229
- await self._write_scope.__aexit__(None, None, None)
230
240
  with contextlib.suppress(Exception): # best-effort: the connection is closing regardless
231
241
  await self._flush()
232
242
  await self.backend.shutdown_transport(self._transport)
@@ -234,6 +244,8 @@ class H2ConnectionBase(H2Streams):
234
244
  if handle is not None:
235
245
  await handle
236
246
  self._fail(None) # no-op when the pump already failed everyone (F44)
247
+ if not self.release_writers():
248
+ await self._writers_evt.wait()
237
249
 
238
250
  # ----- the write pump (h2 `poll_complete` draining its frame queue) -----
239
251
 
@@ -382,18 +394,19 @@ class H2ConnectionBase(H2Streams):
382
394
 
383
395
  # ----- sending (h2 share.rs SendStream over send.rs, hyper PipeToSendStream) -----
384
396
 
385
- async def _send_body(self, st, body, trailers=None):
397
+ async def _send_body(self, st, body, trailers=None, offset=0):
386
398
  """Stream a body, marking END_STREAM on the final DATA frame (or on the
387
399
  trailing HEADERS), then close the send half. A bodyless message never reaches
388
- here — its END_STREAM rode the HEADERS frame."""
400
+ here — its END_STREAM rode the HEADERS frame. `offset`: where a bytes body
401
+ resumes after the client's inline attempt queued its head (`_send_body_inline`)."""
389
402
  if isinstance(body, bytes):
390
403
  # hyper's `Full` body: one poll yields the whole chunk with `is_end_stream()`
391
404
  # set — one DATA carrying END_STREAM (or the trailers carry it, F45).
392
405
  if trailers is None:
393
- await self._send_data(st, body, end_stream=True)
406
+ await self._send_data(st, body, end_stream=True, offset=offset)
394
407
  return
395
408
  if body:
396
- await self._send_data(st, body, end_stream=False)
409
+ await self._send_data(st, body, end_stream=False, offset=offset)
397
410
  self._send_trailers(st, trailers)
398
411
  return
399
412
  if body is not None and not isinstance(body, bytearray) and hasattr(body, "__aiter__"):
@@ -459,16 +472,16 @@ class H2ConnectionBase(H2Streams):
459
472
  await aclose_body(body)
460
473
  done.set()
461
474
 
462
- async def _send_data(self, st, data, end_stream):
463
- """Queue `data` as DATA frame(s), each reserved against min(connection window,
464
- stream window, send-buffer room, peer max_frame_size) — h2 send.rs `send_data`
465
- behind `poll_capacity`. `send_data` is one locked step (reserve + encode +
466
- append — and, with END_STREAM on the last frame, the send half's close, as h2's;
467
- on the server the request's recv half goes with it, hyper dropping the
468
- `RecvStream`: `handle` is the reader to notify when its unread body was reset);
469
- "no window now" is awaited on `window_evt` with the fixed waiter idiom: try,
470
- clear, try again, wait (design §4)."""
471
- offset = 0
475
+ def _try_send_data(self, st, data, offset, end_stream):
476
+ """Queue DATA frames from `data[offset:]` while the windows take them, each
477
+ reserved against min(connection window, stream window, send-buffer room, peer
478
+ max_frame_size) — h2 send.rs `send_data` behind `poll_capacity`. `send_data` is
479
+ one locked step (reserve + encode + append — and, with END_STREAM on the last
480
+ frame, the send half's close, as h2's; on the server the request's recv half
481
+ goes with it, hyper dropping the `RecvStream`: `handle` is the reader to notify
482
+ when its unread body was reset). Returns None once the last frame is queued, or
483
+ the offset to resume from on "no window now" (`sent == 0`, not `done`). Raises
484
+ the stopped error."""
472
485
  while True:
473
486
  v = self.send_data(st.id, data, offset, end_stream)
474
487
  if v.stopped is not None:
@@ -478,23 +491,25 @@ class H2ConnectionBase(H2Streams):
478
491
  if v.done:
479
492
  if v.handle is not None:
480
493
  self._notify_reset(v.handle, v.reader_stop)
481
- return
494
+ return None
495
+ if not v.sent:
496
+ return offset
482
497
  offset += v.sent
483
- if v.sent:
484
- continue
498
+
499
+ async def _send_data(self, st, data, end_stream, offset=0):
500
+ """`_try_send_data` until the body is queued, awaiting "no window now" on
501
+ `window_evt` with the fixed waiter idiom: try, clear, try again, wait
502
+ (design §4)."""
503
+ while True:
504
+ offset = self._try_send_data(st, data, offset, end_stream)
505
+ if offset is None:
506
+ return
485
507
  st.window_evt.clear()
486
- v = self.send_data(st.id, data, offset, end_stream) # re-check after the clear
487
- if v.stopped is not None:
488
- raise self._stopped_error(st, v.stopped)
489
- if v.flags:
490
- self._after(v.flags)
491
- if v.done:
492
- if v.handle is not None:
493
- self._notify_reset(v.handle, v.reader_stop)
508
+ resumed = self._try_send_data(st, data, offset, end_stream) # re-check after the clear
509
+ if resumed is None:
494
510
  return
495
- offset += v.sent
496
- if not v.sent:
497
- await st.window_evt.wait()
511
+ offset = resumed
512
+ await st.window_evt.wait()
498
513
 
499
514
  def _send_trailers(self, st, trailers):
500
515
  """The trailing HEADERS (END_STREAM) closes the send half in the same step