@@ -148,6 +148,13 @@ mutable struct H2Connection
148148 conn_send_window:: Int64
149149 initial_stream_send_window:: Int64
150150 stream_send_window:: Dict{UInt32,Int64}
151+ # Streams whose send side is closed: the peer completed its response
152+ # (END_STREAM) or reset the stream, or the request was canceled. Guarded by
153+ # `state_lock` and checked by `_reserve_send_window!` so a body writer
154+ # parked on flow control aborts instead of waiting for WINDOW_UPDATE
155+ # frames the peer will never send (RFC 9113 §8.1 permits a server to
156+ # respond before the request body completes).
157+ send_closed_streams:: Set{UInt32}
151158 peer_max_concurrent_streams:: Int
152159 peer_max_header_list_size:: Int
153160 peer_goaway_last_stream_id:: UInt32
@@ -258,10 +265,43 @@ end
258265@inline function _h2_send_window_ready_locked (conn:: H2Connection , stream_id:: UInt32 ):: Bool
259266 (@atomic :acquire conn. closed) && return true
260267 conn. conn_error != = nothing && return true
261- stream_window = get (() -> Int64 (0 ), conn. stream_send_window, stream_id)
268+ # A send-closed or unregistered stream will never receive more window
269+ # credit; report ready so the waiter re-checks and aborts.
270+ stream_id in conn. send_closed_streams && return true
271+ haskey (conn. stream_send_window, stream_id) || return true
272+ stream_window = conn. stream_send_window[stream_id]
262273 return stream_window > 0 && conn. conn_send_window > 0
263274end
264275
276+ """
277+ _mark_h2_send_closed!(conn, stream_id)
278+
279+ Record that no more request DATA may be sent on `stream_id` (the peer ended or
280+ reset the stream, or the request was canceled) and wake any body writer parked
281+ in `_reserve_send_window!` waiting for flow-control credit.
282+ """
283+ function _mark_h2_send_closed! (conn:: H2Connection , stream_id:: UInt32 )
284+ lock (conn. state_lock)
285+ try
286+ if haskey (conn. streams, stream_id)
287+ push! (conn. send_closed_streams, stream_id)
288+ end
289+ notify (conn. window_condition; all= true )
290+ finally
291+ unlock (conn. state_lock)
292+ end
293+ return nothing
294+ end
295+
296+ @inline function _h2_send_closed (conn:: H2Connection , stream_id:: UInt32 ):: Bool
297+ lock (conn. state_lock)
298+ try
299+ return stream_id in conn. send_closed_streams || ! haskey (conn. stream_send_window, stream_id)
300+ finally
301+ unlock (conn. state_lock)
302+ end
303+ end
304+
265305function _wait_h2_send_window_locked! (conn:: H2Connection , stream_id:: UInt32 , deadline_ns:: Int64 ):: Nothing
266306 if deadline_ns == 0
267307 wait (conn. window_condition)
@@ -286,13 +326,18 @@ function _wait_h2_send_window_locked!(conn::H2Connection, stream_id::UInt32, dea
286326 return nothing
287327end
288328
329+ # Returns the number of DATA payload bytes the caller may send (> 0), or 0 when
330+ # the stream's send side closed (peer response complete, stream reset, request
331+ # canceled, or stream unregistered) and the caller must stop sending.
289332function _reserve_send_window! (conn:: H2Connection , stream_id:: UInt32 , wanted:: Int , deadline_ns:: Int64 ):: Int
290333 wanted > 0 || throw (ArgumentError (" wanted send window must be > 0" ))
291334 lock (conn. state_lock)
292335 try
293336 while true
294337 (@atomic :acquire conn. closed) && throw (ProtocolError (" HTTP/2 connection is closed" ))
295338 conn. conn_error === nothing || throw (conn. conn_error:: ProtocolError )
339+ stream_id in conn. send_closed_streams && return 0
340+ haskey (conn. stream_send_window, stream_id) || return 0
296341 stream_window = get (() -> Int64 (0 ), conn. stream_send_window, stream_id)
297342 if stream_window <= 0 || conn. conn_send_window <= 0
298343 # Both the connection-level and per-stream windows must allow
@@ -310,21 +355,24 @@ function _reserve_send_window!(conn::H2Connection, stream_id::UInt32, wanted::In
310355 end
311356end
312357
313- function _write_data_frames_h2! (conn:: H2Connection , stream_id:: UInt32 , request:: Request , data:: Vector{UInt8} , end_stream:: Bool )
314- isempty (data) && return nothing
358+ # Returns `true` when all of `data` was written, `false` when the stream's send
359+ # side closed mid-body and the remaining payload must be abandoned.
360+ function _write_data_frames_h2! (conn:: H2Connection , stream_id:: UInt32 , request:: Request , data:: Vector{UInt8} , end_stream:: Bool ):: Bool
361+ isempty (data) && return true
315362 offset = 1
316363 total_len = length (data)
317364 while offset <= total_len
318365 remaining = total_len - offset + 1
319366 write_deadline_ns = _request_write_deadline_ns (request)
320367 chunk_len = _reserve_send_window! (conn, stream_id, remaining, write_deadline_ns)
368+ chunk_len == 0 && return false
321369 chunk = Vector {UInt8} (undef, chunk_len)
322370 copyto! (chunk, 1 , data, offset, chunk_len)
323371 final_chunk = (offset + chunk_len - 1 ) == total_len
324372 _write_frame_h2_threadsafe! (conn, DataFrame (stream_id, end_stream && final_chunk, chunk), write_deadline_ns)
325373 offset += chunk_len
326374 end
327- return nothing
375+ return true
328376end
329377
330378function _apply_peer_settings! (conn:: H2Connection , settings:: Vector{Pair{UInt16,UInt32}} )
@@ -646,6 +694,7 @@ function _unregister_stream!(conn::H2Connection, stream_id::UInt32)
646694 try
647695 pop! (conn. streams, stream_id, nothing )
648696 pop! (conn. stream_send_window, stream_id, nothing )
697+ delete! (conn. send_closed_streams, stream_id)
649698 notify (conn. stream_condition; all= true )
650699 finally
651700 unlock (conn. state_lock)
@@ -871,7 +920,21 @@ function _process_incoming_frame!(conn::H2Connection, frame::AbstractFrame)
871920 rst = frame:: RSTStreamFrame
872921 state = _stream_state (conn, rst. stream_id)
873922 state === nothing && return nothing
874- _set_stream_error! (state:: H2StreamState , H2StreamResetError (rst. error_code))
923+ # RFC 9113 §8.1: a server MAY send RST_STREAM with NO_ERROR after a
924+ # complete response to ask the client to abort the request body;
925+ # "clients MUST NOT discard responses as a result". Only surface the
926+ # reset as a stream error when it does not follow a complete response.
927+ benign = false
928+ lock ((state:: H2StreamState ). lock)
929+ try
930+ benign = rst. error_code == UInt32 (0 ) && (state:: H2StreamState ). stream_done
931+ finally
932+ unlock ((state:: H2StreamState ). lock)
933+ end
934+ benign || _set_stream_error! (state:: H2StreamState , H2StreamResetError (rst. error_code))
935+ # Either way the peer will grant no more send window: wake any body
936+ # writer parked on flow control so it stops uploading.
937+ _mark_h2_send_closed! (conn, rst. stream_id)
875938 return nothing
876939 elseif frame isa WindowUpdateFrame
877940 update = frame:: WindowUpdateFrame
@@ -900,6 +963,10 @@ function _process_incoming_frame!(conn::H2Connection, frame::AbstractFrame)
900963 _consume_orphan_header_fragment! (conn, headers. stream_id, headers. header_block_fragment, headers. end_headers)
901964 return nothing
902965 end
966+ # A response bearing END_STREAM means the peer is done with the stream
967+ # and will grant no more send window; wake any body writer parked on
968+ # flow control so the roundtrip can return this early response.
969+ headers. end_stream && _mark_h2_send_closed! (conn, headers. stream_id)
903970 _handle_stream_header_fragment! (conn, state:: H2StreamState , headers. header_block_fragment, headers. end_headers, headers. end_stream)
904971 # Track an open header block so a missing END_HEADERS forces CONTINUATION
905972 # (RFC 7540 6.10) and the trailing frames still reach this stream even if
@@ -938,6 +1005,9 @@ function _process_incoming_frame!(conn::H2Connection, frame::AbstractFrame)
9381005 data = frame:: DataFrame
9391006 state = _stream_state (conn, data. stream_id)
9401007 state === nothing && return nothing
1008+ # Mark before `_handle_stream_data!`, which can block while the
1009+ # buffered body is drained.
1010+ data. end_stream && _mark_h2_send_closed! (conn, data. stream_id)
9411011 _handle_stream_data! (state:: H2StreamState , data)
9421012 return nothing
9431013 end
@@ -1114,6 +1184,7 @@ function _connect_h2_from_tcp!(
11141184 Int64 (65_535 ),
11151185 Int64 (65_535 ),
11161186 Dict {UInt32,Int64} (),
1187+ Set {UInt32} (),
11171188 typemax (Int),
11181189 0 ,
11191190 typemax (UInt32),
@@ -1226,8 +1297,13 @@ function Base.close(conn::H2Connection)
12261297 return nothing
12271298end
12281299
1229- function _write_request_body_h2! (conn:: H2Connection , stream_id:: UInt32 , request:: Request )
1230- request. body isa EmptyBody && return nothing
1300+ # Returns `true` when the full body (and END_STREAM) was sent, `false` when the
1301+ # upload was abandoned because the stream's send side closed first — e.g. the
1302+ # peer answered with a complete response before reading the whole request body
1303+ # (RFC 9113 §8.1), reset the stream, or the request was canceled. The caller
1304+ # decides how to close out the abandoned stream.
1305+ function _write_request_body_h2! (conn:: H2Connection , stream_id:: UInt32 , request:: Request ):: Bool
1306+ request. body isa EmptyBody && return true
12311307 buf = Vector {UInt8} (undef, 16 * 1024 )
12321308 pending = UInt8[]
12331309 have_pending = false
@@ -1236,16 +1312,16 @@ function _write_request_body_h2!(conn::H2Connection, stream_id::UInt32, request:
12361312 n = body_read! (request. body, buf)
12371313 if n == 0
12381314 if have_pending
1239- _write_data_frames_h2! (conn, stream_id, request, pending, true )
1240- else
1241- _write_frame_h2_threadsafe! (conn, DataFrame (stream_id, true , UInt8[]), _request_write_deadline_ns (request))
1315+ return _write_data_frames_h2! (conn, stream_id, request, pending, true )
12421316 end
1243- return nothing
1317+ _h2_send_closed (conn, stream_id) && return false
1318+ _write_frame_h2_threadsafe! (conn, DataFrame (stream_id, true , UInt8[]), _request_write_deadline_ns (request))
1319+ return true
12441320 end
12451321 current = Vector {UInt8} (undef, n)
12461322 copyto! (current, 1 , buf, 1 , n)
12471323 if have_pending
1248- _write_data_frames_h2! (conn, stream_id, request, pending, false )
1324+ _write_data_frames_h2! (conn, stream_id, request, pending, false ) || return false
12491325 end
12501326 pending = current
12511327 have_pending = true
@@ -1302,6 +1378,14 @@ function _request_headers_for_h2(address::String, request::Request, secure::Bool
13021378 push! (fields, HeaderField (lowered, normalized, false ))
13031379 end
13041380 end
1381+ # HTTP/2 carries no Transfer-Encoding; DATA framing alone delimits the
1382+ # body. Still advertise `content-length` when the body size is known
1383+ # (RFC 9113 §8.1.1), matching the HTTP/1.1 client: intermediaries that
1384+ # translate to an HTTP/1.1 origin otherwise forward the body without a
1385+ # declared length, and origins commonly treat that as an empty body.
1386+ if request. content_length >= 0 && ! (request. body isa EmptyBody) && ! hasheader (request. headers, " content-length" )
1387+ push! (fields, HeaderField (" content-length" , string (request. content_length), false ))
1388+ end
13051389 return fields
13061390end
13071391
@@ -1606,6 +1690,9 @@ function _h2_roundtrip_incoming!(
16061690 cancel_cb = let conn = conn, stream_state = stream_state, request_ctx = request_ctx
16071691 () -> begin
16081692 _set_stream_error! (stream_state, _canceled_error (request_ctx))
1693+ # Stop a body writer parked on flow control; cancellation means no
1694+ # more request DATA may be sent on this stream.
1695+ _mark_h2_send_closed! (conn, stream_state. stream_id)
16091696 @try_ignore _write_frame_h2_threadsafe! (conn, RSTStreamFrame (stream_state. stream_id, UInt32 (0x8 )))
16101697 lock (conn. state_lock)
16111698 try
@@ -1625,7 +1712,19 @@ function _h2_roundtrip_incoming!(
16251712 end
16261713 try
16271714 end_stream = request. body isa EmptyBody
1628- end_stream || _write_request_body_h2! (conn, stream_state. stream_id, request)
1715+ if ! end_stream && ! _write_request_body_h2! (conn, stream_state. stream_id, request)
1716+ # The upload was abandoned because the peer finished with the
1717+ # stream first. When that was a benign early complete response
1718+ # (no stream error), actively cancel our half of the stream
1719+ # with NO_ERROR so the peer can release its stream state; a
1720+ # peer-initiated reset or cancellation needs no reply.
1721+ send_rst = lock (stream_state. lock) do
1722+ stream_state. stream_error === nothing && ! stream_state. conn_errored
1723+ end
1724+ if send_rst
1725+ @try_ignore _write_frame_h2_threadsafe! (conn, RSTStreamFrame (stream_state. stream_id, UInt32 (0x0 )))
1726+ end
1727+ end
16291728 catch err
16301729 _fail_h2_connection! (conn, ProtocolError (" HTTP/2 write failed" , err:: Exception ))
16311730 rethrow ()
0 commit comments