)]}'
{
  "commit": "3998b87cda18ee289a4f4340ae527045fdef0813",
  "tree": "aca27899793c82049c595f9fcdb7dbc5da821061",
  "parents": [
    "be2b51fa259b4e99d527df382b6d50af12b8c4bb"
  ],
  "author": {
    "name": "Moritz",
    "email": "27859082+mosuem@users.noreply.github.com",
    "time": "Fri Oct 23 07:31:29 2026 +0200"
  },
  "committer": {
    "name": "GitHub",
    "email": "noreply@github.com",
    "time": "Fri Oct 23 05:31:29 2026 +0000"
  },
  "message": "fix(http2): do not head-of-line block HEADERS, RST_STREAM and GOAWAY behind window-exhausted DATA (#2013)\n\n\u003e [!NOTE]\n\u003e This PR was generated by an AI coding agent (Jetski) on behalf of @mosuem.\n\n`ConnectionMessageQueueOut` is a strict FIFO. Once the connection-level send window is exhausted, a `DataMessage` at the head of the queue blocks *everything* behind it, including frames that are not subject to flow control (RFC 9113 Section 6.9: *\"Flow control only applies to frames that are identified as being subject to flow control. Of the frame types defined in this document, this includes only DATA frames.\"*). In practice: while one stream was waiting for `WINDOW_UPDATE`, no new request could be opened (`HEADERS`), no stream could be cancelled (`RST_STREAM`), and the connection could not be shut down gracefully (`GOAWAY`) until the peer granted credit.\n\n- **`lib/src/flowcontrol/connection_queues.dart`**:\n  - `_peekSendableMessage()` replaces the \"first message or nothing\" rule. When the window is exhausted and the head of the queue is a `DataMessage`, it returns the first `GOAWAY`, or the first `HEADERS` / `RST_STREAM` whose stream has no earlier queued message. Skipping a message of a stream blocks every later message of that stream (and, for a `PUSH_PROMISE`, of the promised stream), so per-stream frame order is preserved: trailers never overtake their own `DATA`.\n  - The scan is linear in the queue length, and `StreamMessageQueueOut` keeps forwarding `DATA` into this queue while the connection window is exhausted (it only checks the stream window). To avoid rescanning on every `sendData()` call, `enqueueMessage` skips `_trySendMessages()` when a `DataMessage` is enqueued while `_connectionWindow.positiveWindow.wouldBuffer` is `true` (since the new `DataMessage` cannot be sent and cannot unblock any earlier queued message). Enqueueing a non-DATA message while blocked still costs one pass over the queue.\n\nAdded to `test/client_test.dart`:\n- `headers-and-rst-not-blocked-or-reordered-on-exhausted-window`: stream 1 exhausts the connection window; stream 3\u0027s `HEADERS` and stream 5\u0027s `HEADERS` + `RST_STREAM` must still go out, in that order, before the window is replenished.\n- `trailers-stay-behind-blocked-data-while-other-headers-bypass`: stream 1 has blocked `DATA` *and* queued trailers; stream 3\u0027s `HEADERS` bypass them, stream 1\u0027s trailers are only written after its `DATA`.\n\n**Before fix:**\n```text\n00:05 +0 -1: client-tests client-errors headers-and-rst-not-blocked-or-reordered-on-exhausted-window [E]\n  TimeoutException after 0:00:05.000000: Test timed out after 5 seconds.\n00:10 +0 -2: client-tests client-errors trailers-stay-behind-blocked-data-while-other-headers-bypass [E]\n  TimeoutException after 0:00:05.000000: Test timed out after 5 seconds.\n```\n(stream 3\u0027s `HEADERS` never leave the client while stream 1 waits for credit)\n\n**After fix:**\n```text\n00:00 +2: All tests passed!\n```",
  "tree_diff": [
    {
      "type": "modify",
      "old_id": "a5ca6db13ac33e6e005bd4e97a9c32822863f188",
      "old_mode": 33188,
      "old_path": "pkgs/http2/CHANGELOG.md",
      "new_id": "fc2d454a672f8fb5f79c31b1ebc974e2c6e167d1",
      "new_mode": 33188,
      "new_path": "pkgs/http2/CHANGELOG.md"
    },
    {
      "type": "modify",
      "old_id": "77bb52c01a26da434bb2586423fc577dda7662fc",
      "old_mode": 33188,
      "old_path": "pkgs/http2/lib/src/flowcontrol/connection_queues.dart",
      "new_id": "60158ee45db64cc8ce2eb0b043fb523ce065fb4a",
      "new_mode": 33188,
      "new_path": "pkgs/http2/lib/src/flowcontrol/connection_queues.dart"
    },
    {
      "type": "modify",
      "old_id": "19e208d285b15bb1ae29dee10f2fc0440bc1ddd1",
      "old_mode": 33188,
      "old_path": "pkgs/http2/test/client_test.dart",
      "new_id": "662476fdb63aabb8a68d1f570d0d2ccd64510834",
      "new_mode": 33188,
      "new_path": "pkgs/http2/test/client_test.dart"
    }
  ]
}
