Skip to content

feat: throttle empty HTTP/2 DATA frames that do not end their stream - #1265

Open
pjfanning wants to merge 1 commit into
apache:mainfrom
pjfanning:throttle-http2-data-frames
Open

pjfanning wants to merge 1 commit into
apache:mainfrom
pjfanning:throttle-http2-data-frames

Conversation

@pjfanning

@pjfanning pjfanning commented Aug 31, 2026

Copy link
Copy Markdown
Member

Motivation

A DATA frame carrying no payload has sizeInWindow == 0, so it consumes no flow-control window. The number of such frames is therefore not bounded by flow control at all, and on an open stream a peer can send them continuously — each one costing a pass through the stream state machine, a buffer ++= empty, and a flow-control recompute.

The frame-type-throttle mechanism could not be pointed at them. It maps an alias to a frame type name:

case "reset" => Some("RstStreamFrame")
...
case _       => None

and had no alias for DATA, so DATA frames were unthrottleable by configuration. A plain "data" alias would not have helped: throttling every DATA frame at the configured rate throttles legitimate throughput along with it, which is presumably why the config documents the throttle as being for "non-data frame types".

Not every empty DATA frame is suspect either. An empty DATA frame carrying END_STREAM is how a client closes a request body whose length it did not know up front — clients such as grpc and Go's net/http2 send one per such request — so it is both legitimate and self-limiting: one per stream, after which the stream is half-closed. At the default budget of 100 charged frames per second per connection, charging those would tear down connections carrying ordinary multiplexed traffic. An empty DATA frame that does not end its stream has no such use.

Modification

Add two aliases, so the two cases can be configured separately:

  • empty-data-no-end-stream charges empty DATA frames that do not carry END_STREAM. Nothing legitimate produces those, so it joins reset in the default frame-types.
  • empty-data charges every empty DATA frame, END_STREAM included. Enabling it can throttle legitimate traffic, so it stays off by default.

Both resolve to names that are deliberately not real frameTypeNames, which frameCost recognises:

case d: DataFrame if d.payload.isEmpty && isThrottledEmptyDataFrame(frameTypesForThrottle, d) => 1

frameCost moves out of rapidResetMitigation into a private[http2] method so it can be tested directly. A frame that both aliases match is still charged once.

The default becomes:

frame-types = ["reset", "empty-data-no-end-stream"]

Result

A flood of empty DATA frames that do not end their stream fails the connection out of the box, while the empty DATA frames that close a request body are left alone unless an operator opts in to empty-data.

Tests

  • sbt "http-core/testOnly org.apache.pekko.http.impl.engine.http2.Http2BlueprintSpec" — pass (17 tests); new cases for both aliases and for frameCost covering END_STREAM, data-carrying frames, frames matched by their own type name, and both aliases configured at once.
  • sbt "http2-tests/testOnly ...Http2ServerEmptyDataThrottleSpec ...Http2ServerEmptyDataNoEndStreamThrottleSpec ...Http2ServerEnableFrameTypeThrottleSpec ...Http2ServerDisableFrameTypeThrottleSpec" — pass (4 tests). The new Http2ServerEmptyDataNoEndStreamThrottleSpec sets no frame-types override, so it covers the default; verified it fails with the default put back to ["reset"].
  • sbt "http2-tests/testOnly ...Http2ServerSpec ...Http2ClientServerSpec ...Http2ClientSpec ...WithPriorKnowledgeSpec ...H2cUpgradeSpec" — pass (178 tests, 17 pending), checking the new default does not disturb ordinary traffic.
  • sbt http-core/mimaReportBinaryIssues — pass.
  • Header on the new file generated with sbt http2-tests/headerCreateAll; native scalafmt clean.

References

Refs #332 — extends the frame type throttle to empty DATA frames

@pjfanning pjfanning added this to the 2.0.0-M2 milestone Sep 6, 2026
Motivation:
A DATA frame carrying no payload has `sizeInWindow == 0`, so it consumes no
flow-control window. The *number* of such frames is therefore not bounded by
flow control at all, and on an open stream a peer can send them continuously -
each one costing a pass through the stream state machine, a `buffer ++= empty`,
and a flow-control recompute.

The `frame-type-throttle` mechanism could not be pointed at them. It maps an
alias to a frame type name and had no alias for DATA, so DATA frames were
unthrottleable by configuration. A plain `"data"` alias would not have helped:
throttling every DATA frame at the configured rate throttles legitimate
throughput along with it, which is why the config documents the throttle as
being for "non-data frame types".

Not every empty DATA frame is suspect either. An empty DATA frame carrying
END_STREAM is how a client closes a request body whose length it did not know
up front - clients such as grpc and Go's net/http2 send one per such request -
so it is both legitimate and self-limiting: one per stream, after which the
stream is half-closed. At the default budget of 100 charged frames per second
per connection, charging those would tear down connections carrying ordinary
multiplexed traffic. An empty DATA frame that does not end its stream has no
such use.

Modification:
Add two aliases:

- `empty-data-no-end-stream` charges the empty DATA frames that do not carry
  END_STREAM. A peer has no reason to send those at all, so it joins `reset`
  in the default `frame-types`.
- `empty-data` charges every empty DATA frame, END_STREAM included. Enabling
  it can throttle legitimate traffic, so it stays off by default.

Both resolve to names that are deliberately not real `frameTypeName`s, which
`frameCost` recognises. `frameCost` moves out of `rapidResetMitigation` into a
`private[http2]` method so it can be tested directly; a frame that both aliases
match is still charged once.

Result:
A flood of empty DATA frames that do not end their stream fails the connection
out of the box, while the empty DATA frames that close a request body are left
alone unless an operator opts in to `empty-data`.

Tests:
- sbt "http-core/testOnly org.apache.pekko.http.impl.engine.http2.Http2BlueprintSpec" - pass (17 tests); new cases for both aliases and for `frameCost` covering END_STREAM, data-carrying frames, frames matched by their own type name, and both aliases configured at once.
- sbt "http2-tests/testOnly org.apache.pekko.http.impl.engine.http2.Http2ServerEmptyDataThrottleSpec org.apache.pekko.http.impl.engine.http2.Http2ServerEmptyDataNoEndStreamThrottleSpec org.apache.pekko.http.impl.engine.http2.Http2ServerEnableFrameTypeThrottleSpec org.apache.pekko.http.impl.engine.http2.Http2ServerDisableFrameTypeThrottleSpec" - pass (4 tests). The new Http2ServerEmptyDataNoEndStreamThrottleSpec sets no `frame-types` override, so it covers the default; verified it fails with the default put back to `["reset"]`.
- sbt "http2-tests/testOnly org.apache.pekko.http.impl.engine.http2.Http2ServerSpec org.apache.pekko.http.impl.engine.http2.Http2ClientServerSpec org.apache.pekko.http.impl.engine.http2.Http2ClientSpec org.apache.pekko.http.impl.engine.http2.WithPriorKnowledgeSpec org.apache.pekko.http.impl.engine.http2.H2cUpgradeSpec" - pass (178 tests, 17 pending), checking the new default does not disturb ordinary traffic.
- sbt http-core/mimaReportBinaryIssues - pass.
- Header on the new file generated with `sbt http2-tests/headerCreateAll`; native `scalafmt` clean.

References:
Refs apache#332 - extends the frame type throttle to empty DATA frames
@pjfanning
pjfanning force-pushed the throttle-http2-data-frames branch from 1c34209 to ce03d6e Compare September 10, 2026 10:24
@pjfanning pjfanning changed the title feat: allow empty HTTP/2 DATA frames to be throttled feat: throttle empty HTTP/2 DATA frames that do not end their stream Sep 10, 2026
@pjfanning pjfanning modified the milestones: 2.0.0-M2, 2.0.0-M3 Sep 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant