From 0fc4b3d476b199525bfd2e26786a75d47f7cf6e1 Mon Sep 17 00:00:00 2001 From: PJ Fanning Date: Mon, 31 Aug 2026 17:52:36 +0100 Subject: [PATCH 1/3] feat: bound the size of an incoming HTTP/2 frame Motivation: `Http2FrameParsing` read a frame's 24-bit length field and then took that many bytes with no upper bound, so a peer could declare a length up to the field's maximum of 16 MiB - 1 and make the frame parser buffer that much for a single frame. That happens before the HPACK and entity limits apply, so `max-header-list-size`, `incoming-stream-level-buffer-size` and `incoming-connection-level- buffer-size` do not bound it, and it is multiplied by the number of connections. Modification: Add a `max-frame-size` setting to the HTTP/2 server and client settings, defaulting to 512kB, and reject a larger frame with a FRAME_SIZE_ERROR on its frame header, before the payload is buffered. The value is validated against the bounds RFC 9113, section 4.2 sets for SETTINGS_MAX_FRAME_SIZE, 16 KiB to 16 MiB - 1. The setting is a limit on what is accepted, not an advertisement. An earlier revision did advertise it as SETTINGS_MAX_FRAME_SIZE, which h2spec's "4.2 Frame Size" case showed to be actively harmful: DATA frames are flow controlled and the initial window is 64 KiB, so a peer that sizes its first frame to a larger advertised value trips a FLOW_CONTROL_ERROR before the window has grown. Advertising nothing keeps a well-behaved peer at the 16 KiB default it already assumes, and the configured value is then pure leniency for peers that exceed it - always accepting at least as much as any peer is told it may send. Result: A single frame can no longer make the parser hold up to 16 MiB; the bound is 512kB by default and configurable. Frames from a spec compliant peer are unaffected, and pekko's own existing behaviour of accepting frames well above 16 KiB is preserved. Tests: - sbt "http2-tests/testOnly org.apache.pekko.http.impl.engine.http2.Http2ServerSpec org.apache.pekko.http.impl.engine.http2.Http2ClientSpec org.apache.pekko.http.impl.engine.http2.H2SpecIntegrationSpec" - pass (236 tests), including the h2spec conformance suite. A new test sends a frame one byte over a configured 16 KiB limit and expects GOAWAY(FRAME_SIZE_ERROR); verified it fails with the size check disabled. The existing "fail if more data is received than stream-level window allows" test deliberately sends a single 512001 byte frame to exceed the stream buffer, so it now raises max-frame-size to 1 MiB to reach the flow-control check it is about. - sbt http-core/mimaReportBinaryIssues - pass - sbt "+http-core/compile" - pass on 2.13.18 and 3.3.8 References: None - bounds how much a single HTTP/2 frame can buffer --- http-core/src/main/resources/reference.conf | 28 +++++++++++++ .../impl/engine/http2/Http2Blueprint.scala | 17 ++++---- .../http2/framing/Http2FrameParsing.scala | 9 +++- .../settings/Http2ClientSettings.scala | 15 +++++++ .../settings/Http2ServerSettings.scala | 15 +++++++ .../settings/Http2ServerSettings.scala | 42 +++++++++++++++++++ .../impl/engine/http2/Http2ServerSpec.scala | 16 +++++++ 7 files changed, 134 insertions(+), 8 deletions(-) diff --git a/http-core/src/main/resources/reference.conf b/http-core/src/main/resources/reference.conf index a61326119..33e011528 100644 --- a/http-core/src/main/resources/reference.conf +++ b/http-core/src/main/resources/reference.conf @@ -263,6 +263,20 @@ pekko.http { # effective limit for a well-behaved peer is somewhat stricter than the configured value. max-header-list-size = 64 KiB + # The largest frame payload this endpoint accepts, in bytes. A larger incoming frame is rejected with a + # FRAME_SIZE_ERROR on its frame header, before the payload is buffered, so this bounds how much a single frame + # can make this endpoint hold at once. The length field of a frame header allows up to 16 MiB - 1. + # + # This is a limit, not an invitation: no larger SETTINGS_MAX_FRAME_SIZE is advertised, so a peer that follows + # the spec keeps to the 16 KiB default of RFC 9113, section 4.2 and the extra room here is only leniency for + # peers that do not. Advertising a value far above the initial 64 KiB flow-control window would invite frames + # that cannot be received without breaking flow control anyway. + # + # RFC 9113, section 4.2 constrains this to be between 16 KiB and 16 MiB - 1. The amount of request data + # buffered overall is bounded separately by the incoming-connection-level-buffer-size and + # incoming-stream-level-buffer-size settings above. + max-frame-size = 512kB + # The maximum number of bytes to receive from a request entity in a single chunk. # # The reasoning to limit that amount (instead of delivering all buffered data for a stream) is that @@ -512,6 +526,20 @@ pekko.http { # effective limit for a well-behaved peer is somewhat stricter than the configured value. max-header-list-size = 64 KiB + # The largest frame payload this endpoint accepts, in bytes. A larger incoming frame is rejected with a + # FRAME_SIZE_ERROR on its frame header, before the payload is buffered, so this bounds how much a single frame + # can make this endpoint hold at once. The length field of a frame header allows up to 16 MiB - 1. + # + # This is a limit, not an invitation: no larger SETTINGS_MAX_FRAME_SIZE is advertised, so a peer that follows + # the spec keeps to the 16 KiB default of RFC 9113, section 4.2 and the extra room here is only leniency for + # peers that do not. Advertising a value far above the initial 64 KiB flow-control window would invite frames + # that cannot be received without breaking flow control anyway. + # + # RFC 9113, section 4.2 constrains this to be between 16 KiB and 16 MiB - 1. The amount of request data + # buffered overall is bounded separately by the incoming-connection-level-buffer-size and + # incoming-stream-level-buffer-size settings above. + max-frame-size = 512kB + # The maximum number of bytes to receive from a request entity in a single chunk. # # The reasoning to limit that amount (instead of delivering all buffered data for a stream) is that diff --git a/http-core/src/main/scala/org/apache/pekko/http/impl/engine/http2/Http2Blueprint.scala b/http-core/src/main/scala/org/apache/pekko/http/impl/engine/http2/Http2Blueprint.scala index aff6e14ae..676a131cf 100644 --- a/http-core/src/main/scala/org/apache/pekko/http/impl/engine/http2/Http2Blueprint.scala +++ b/http-core/src/main/scala/org/apache/pekko/http/impl/engine/http2/Http2Blueprint.scala @@ -144,8 +144,9 @@ private[http] object Http2Blueprint { val frameTypesForThrottle = getFrameTypesForThrottle(settings.http2Settings) val flowWithPossibleThrottle = if (frameTypesForThrottle.nonEmpty) { - initialFlow atop rapidResetMitigation(settings.http2Settings, frameTypesForThrottle) atopKeepLeft framing(log) - } else initialFlow atop framing(log) + initialFlow atop rapidResetMitigation(settings.http2Settings, frameTypesForThrottle) atopKeepLeft framing(log, + settings.http2Settings.maxFrameSize) + } else initialFlow atop framing(log, settings.http2Settings.maxFrameSize) flowWithPossibleThrottle atop errorHandling(log) atop @@ -166,7 +167,7 @@ private[http] object Http2Blueprint { clientDemux(settings.http2Settings, masterHttpHeaderParser)).atop( FrameLogger.logFramesIfEnabled(settings.http2Settings.logFrames)).atop( // enable for debugging hpackCoding(masterHttpHeaderParser, settings.parserSettings, settings.http2Settings.maxHeaderListSize)).atop( - framingClient(log)).atop( + framingClient(log, settings.http2Settings.maxFrameSize)).atop( errorHandling(log)).atop( idleTimeoutIfConfigured(settings.idleTimeout)) } @@ -208,15 +209,17 @@ private[http] object Http2Blueprint { }, Flow[ByteString]) - def framing(log: LoggingAdapter): BidiFlow[FrameEvent, ByteString, ByteString, FrameEvent, NotUsed] = + def framing(log: LoggingAdapter, maxFrameSize: Int) + : BidiFlow[FrameEvent, ByteString, ByteString, FrameEvent, NotUsed] = BidiFlow.fromFlows( Flow[FrameEvent].map(FrameRenderer.render), - Flow[ByteString].via(new Http2FrameParsing(shouldReadPreface = true, log))) + Flow[ByteString].via(new Http2FrameParsing(shouldReadPreface = true, log, maxFrameSize))) - def framingClient(log: LoggingAdapter): BidiFlow[FrameEvent, ByteString, ByteString, FrameEvent, NotUsed] = + def framingClient(log: LoggingAdapter, + maxFrameSize: Int): BidiFlow[FrameEvent, ByteString, ByteString, FrameEvent, NotUsed] = BidiFlow.fromFlows( Flow[FrameEvent].map(FrameRenderer.render).prepend(Source.single(Http2Protocol.ClientConnectionPreface)), - Flow[ByteString].via(new Http2FrameParsing(shouldReadPreface = false, log))) + Flow[ByteString].via(new Http2FrameParsing(shouldReadPreface = false, log, maxFrameSize))) private def rapidResetMitigation(settings: Http2ServerSettings, frameTypesForThrottle: Set[String]): BidiFlow[FrameEvent, FrameEvent, FrameEvent, FrameEvent, NotUsed] = { diff --git a/http-core/src/main/scala/org/apache/pekko/http/impl/engine/http2/framing/Http2FrameParsing.scala b/http-core/src/main/scala/org/apache/pekko/http/impl/engine/http2/framing/Http2FrameParsing.scala index d236e06c7..42944a40b 100644 --- a/http-core/src/main/scala/org/apache/pekko/http/impl/engine/http2/framing/Http2FrameParsing.scala +++ b/http-core/src/main/scala/org/apache/pekko/http/impl/engine/http2/framing/Http2FrameParsing.scala @@ -165,7 +165,8 @@ private[http] object Http2FrameParsing { /** INTERNAL API */ @InternalApi private[http2] class Http2FrameParsing( - shouldReadPreface: Boolean, log: LoggingAdapter) extends ByteStringParser[FrameEvent] { + shouldReadPreface: Boolean, log: LoggingAdapter, + maxFrameSize: Int = Http2Protocol.InitialMaxFrameSize) extends ByteStringParser[FrameEvent] { import ByteStringParser._ import Http2FrameParsing._ @@ -190,6 +191,12 @@ private[http2] class Http2FrameParsing( object ReadFrame extends Step { override def parse(reader: ByteReader): ParseResult[FrameEvent] = { val length = reader.readShortBE() << 8 | reader.readByte() + // Reject before `reader.take(length)` below buffers the payload: the length field allows up to 16 MiB, so + // without this a peer could make the parser hold that much for a single frame. FRAME_SIZE_ERROR is what + // RFC 9113, section 4.2 asks for; we accept up to `maxFrameSize` rather than the 16 KiB default the peer + // is expected to keep to, so this only rejects a peer that already exceeds what it was told. + if (length > maxFrameSize) + throw new Http2Compliance.IllegalHttp2FrameSize(length, s"exceeds the maximum frame size of $maxFrameSize") val tpe = reader.readByte() val flags = new ByteFlag(reader.readByte()) // RFC 9113 5.1.1: the high bit of the stream identifier is reserved and MUST be ignored when receiving. diff --git a/http-core/src/main/scala/org/apache/pekko/http/javadsl/settings/Http2ClientSettings.scala b/http-core/src/main/scala/org/apache/pekko/http/javadsl/settings/Http2ClientSettings.scala index bfcf87be7..2dbd2b299 100644 --- a/http-core/src/main/scala/org/apache/pekko/http/javadsl/settings/Http2ClientSettings.scala +++ b/http-core/src/main/scala/org/apache/pekko/http/javadsl/settings/Http2ClientSettings.scala @@ -48,6 +48,21 @@ trait Http2ClientSettings { self: scaladsl.settings.Http2ClientSettings.Http2Cli */ def withMaxHeaderListSize(newValue: Int): Http2ClientSettings = copy(maxHeaderListSize = newValue) + /** + * The largest frame payload this endpoint accepts, in bytes. A larger incoming frame is rejected with a + * FRAME_SIZE_ERROR on its frame header, before the payload is buffered. No larger SETTINGS_MAX_FRAME_SIZE is + * advertised, so a peer that follows the spec keeps to the 16 KiB default and the extra room is leniency for peers + * that do not. RFC 9113, section 4.2 constrains it to be between 16 KiB and 16 MiB - 1. + * + * @since 2.0.0 + */ + def maxFrameSize: Int + + /** + * @since 2.0.0 + */ + def withMaxFrameSize(newValue: Int): Http2ClientSettings = copy(maxFrameSize = newValue) + def outgoingControlFrameBufferSize: Int def withOutgoingControlFrameBufferSize(newValue: Int): Http2ClientSettings = copy(outgoingControlFrameBufferSize = newValue) diff --git a/http-core/src/main/scala/org/apache/pekko/http/javadsl/settings/Http2ServerSettings.scala b/http-core/src/main/scala/org/apache/pekko/http/javadsl/settings/Http2ServerSettings.scala index 1c9165e16..dd85fe4ce 100644 --- a/http-core/src/main/scala/org/apache/pekko/http/javadsl/settings/Http2ServerSettings.scala +++ b/http-core/src/main/scala/org/apache/pekko/http/javadsl/settings/Http2ServerSettings.scala @@ -56,6 +56,21 @@ trait Http2ServerSettings { */ def withMaxHeaderListSize(newValue: Int): Http2ServerSettings + /** + * The largest frame payload this endpoint accepts, in bytes. A larger incoming frame is rejected with a + * FRAME_SIZE_ERROR on its frame header, before the payload is buffered. No larger SETTINGS_MAX_FRAME_SIZE is + * advertised, so a peer that follows the spec keeps to the 16 KiB default and the extra room is leniency for peers + * that do not. RFC 9113, section 4.2 constrains it to be between 16 KiB and 16 MiB - 1. + * + * @since 2.0.0 + */ + def getMaxFrameSize: Int = maxFrameSize + + /** + * @since 2.0.0 + */ + def withMaxFrameSize(newValue: Int): Http2ServerSettings + def getOutgoingControlFrameBufferSize: Int = outgoingControlFrameBufferSize def withOutgoingControlFrameBufferSize(newValue: Int): Http2ServerSettings diff --git a/http-core/src/main/scala/org/apache/pekko/http/scaladsl/settings/Http2ServerSettings.scala b/http-core/src/main/scala/org/apache/pekko/http/scaladsl/settings/Http2ServerSettings.scala index 19847648f..c6787c779 100644 --- a/http-core/src/main/scala/org/apache/pekko/http/scaladsl/settings/Http2ServerSettings.scala +++ b/http-core/src/main/scala/org/apache/pekko/http/scaladsl/settings/Http2ServerSettings.scala @@ -17,6 +17,7 @@ import org.apache.pekko import pekko.annotation.ApiMayChange import pekko.annotation.DoNotInherit import pekko.annotation.InternalApi +import pekko.http.impl.engine.http2.Http2Protocol import pekko.http.impl.util._ import pekko.http.javadsl import com.typesafe.config.Config @@ -42,6 +43,7 @@ private[http] trait Http2CommonSettings { def logFrames: Boolean def maxConcurrentStreams: Int def maxHeaderListSize: Int + def maxFrameSize: Int def outgoingControlFrameBufferSize: Int def pingInterval: FiniteDuration @@ -105,6 +107,21 @@ trait Http2ServerSettings extends javadsl.settings.Http2ServerSettings with Http */ override def withMaxHeaderListSize(newValue: Int): Http2ServerSettings = copy(maxHeaderListSize = newValue) + /** + * The largest frame payload this endpoint accepts, in bytes. A larger incoming frame is rejected with a + * FRAME_SIZE_ERROR on its frame header, before the payload is buffered. No larger SETTINGS_MAX_FRAME_SIZE is + * advertised, so a peer that follows the spec keeps to the 16 KiB default and the extra room is leniency for peers + * that do not. RFC 9113, section 4.2 constrains it to be between 16 KiB and 16 MiB - 1. + * + * @since 2.0.0 + */ + def maxFrameSize: Int + + /** + * @since 2.0.0 + */ + override def withMaxFrameSize(newValue: Int): Http2ServerSettings = copy(maxFrameSize = newValue) + def outgoingControlFrameBufferSize: Int override def withOutgoingControlFrameBufferSize(newValue: Int): Http2ServerSettings = copy(outgoingControlFrameBufferSize = newValue) @@ -145,6 +162,7 @@ object Http2ServerSettings extends SettingsCompanion[Http2ServerSettings] { private[http] case class Http2ServerSettingsImpl( maxConcurrentStreams: Int, maxHeaderListSize: Int, + maxFrameSize: Int, requestEntityChunkSize: Int, incomingConnectionLevelBufferSize: Int, incomingStreamLevelBufferSize: Int, @@ -161,6 +179,9 @@ object Http2ServerSettings extends SettingsCompanion[Http2ServerSettings] { extends Http2ServerSettings { require(maxConcurrentStreams >= 0, "max-concurrent-streams must be >= 0") require(maxHeaderListSize > 0, "max-header-list-size must be > 0") + // RFC 9113, section 4.2: SETTINGS_MAX_FRAME_SIZE must be within these bounds + require(maxFrameSize >= Http2Protocol.MinFrameSize && maxFrameSize <= Http2Protocol.MaxFrameSize, + s"max-frame-size must be between ${Http2Protocol.MinFrameSize} and ${Http2Protocol.MaxFrameSize}") require(requestEntityChunkSize > 0, "request-entity-chunk-size must be > 0") require(incomingConnectionLevelBufferSize > 0, "incoming-connection-level-buffer-size must be > 0") require(incomingStreamLevelBufferSize > 0, "incoming-stream-level-buffer-size must be > 0") @@ -179,6 +200,7 @@ object Http2ServerSettings extends SettingsCompanion[Http2ServerSettings] { def fromSubConfig(root: Config, c: Config): Http2ServerSettingsImpl = Http2ServerSettingsImpl( maxConcurrentStreams = c.getInt("max-concurrent-streams"), maxHeaderListSize = c.getIntBytes("max-header-list-size"), + maxFrameSize = c.getIntBytes("max-frame-size"), requestEntityChunkSize = c.getIntBytes("request-entity-chunk-size"), incomingConnectionLevelBufferSize = c.getIntBytes("incoming-connection-level-buffer-size"), incomingStreamLevelBufferSize = c.getIntBytes("incoming-stream-level-buffer-size"), @@ -237,6 +259,21 @@ trait Http2ClientSettings extends javadsl.settings.Http2ClientSettings with Http */ override def withMaxHeaderListSize(newValue: Int): Http2ClientSettings = copy(maxHeaderListSize = newValue) + /** + * The largest frame payload this endpoint accepts, in bytes. A larger incoming frame is rejected with a + * FRAME_SIZE_ERROR on its frame header, before the payload is buffered. No larger SETTINGS_MAX_FRAME_SIZE is + * advertised, so a peer that follows the spec keeps to the 16 KiB default and the extra room is leniency for peers + * that do not. RFC 9113, section 4.2 constrains it to be between 16 KiB and 16 MiB - 1. + * + * @since 2.0.0 + */ + def maxFrameSize: Int + + /** + * @since 2.0.0 + */ + override def withMaxFrameSize(newValue: Int): Http2ClientSettings = copy(maxFrameSize = newValue) + def outgoingControlFrameBufferSize: Int override def withOutgoingControlFrameBufferSize(newValue: Int): Http2ClientSettings = copy(outgoingControlFrameBufferSize = newValue) @@ -277,6 +314,7 @@ object Http2ClientSettings extends SettingsCompanion[Http2ClientSettings] { private[http] case class Http2ClientSettingsImpl( maxConcurrentStreams: Int, maxHeaderListSize: Int, + maxFrameSize: Int, requestEntityChunkSize: Int, incomingConnectionLevelBufferSize: Int, incomingStreamLevelBufferSize: Int, @@ -292,6 +330,9 @@ object Http2ClientSettings extends SettingsCompanion[Http2ClientSettings] { extends Http2ClientSettings with javadsl.settings.Http2ClientSettings { require(maxConcurrentStreams >= 0, "max-concurrent-streams must be >= 0") require(maxHeaderListSize > 0, "max-header-list-size must be > 0") + // RFC 9113, section 4.2: SETTINGS_MAX_FRAME_SIZE must be within these bounds + require(maxFrameSize >= Http2Protocol.MinFrameSize && maxFrameSize <= Http2Protocol.MaxFrameSize, + s"max-frame-size must be between ${Http2Protocol.MinFrameSize} and ${Http2Protocol.MaxFrameSize}") require(requestEntityChunkSize > 0, "request-entity-chunk-size must be > 0") require(incomingConnectionLevelBufferSize > 0, "incoming-connection-level-buffer-size must be > 0") require(incomingStreamLevelBufferSize > 0, "incoming-stream-level-buffer-size must be > 0") @@ -307,6 +348,7 @@ object Http2ClientSettings extends SettingsCompanion[Http2ClientSettings] { def fromSubConfig(root: Config, c: Config): Http2ClientSettingsImpl = Http2ClientSettingsImpl( maxConcurrentStreams = c.getInt("max-concurrent-streams"), maxHeaderListSize = c.getIntBytes("max-header-list-size"), + maxFrameSize = c.getIntBytes("max-frame-size"), requestEntityChunkSize = c.getIntBytes("request-entity-chunk-size"), incomingConnectionLevelBufferSize = c.getIntBytes("incoming-connection-level-buffer-size"), incomingStreamLevelBufferSize = c.getIntBytes("incoming-stream-level-buffer-size"), diff --git a/http2-tests/src/test/scala/org/apache/pekko/http/impl/engine/http2/Http2ServerSpec.scala b/http2-tests/src/test/scala/org/apache/pekko/http/impl/engine/http2/Http2ServerSpec.scala index 8bbab8a0d..40ba7c644 100644 --- a/http2-tests/src/test/scala/org/apache/pekko/http/impl/engine/http2/Http2ServerSpec.scala +++ b/http2-tests/src/test/scala/org/apache/pekko/http/impl/engine/http2/Http2ServerSpec.scala @@ -288,6 +288,18 @@ class Http2ServerSpec extends Http2SpecWithMaterializer(""" user.expectRequest().headers should contain(RawHeader("small-header", "x" * 100)) }) + "reject a frame larger than max-frame-size".inAssertAllStagesStopped( + new TestSetup with RequestResponseProbes { + override def settings: ServerSettings = super.settings.mapHttp2Settings(_.withMaxFrameSize(16384)) + + // the length field allows up to 16 MiB, so an oversized frame has to be rejected on the frame header + // rather than accepted and buffered + network.sendDATA(1, endStream = true, ByteString(new Array[Byte](16385))) + + val (_, errorCode) = network.expectGOAWAY() + errorCode should ===(ErrorCode.FRAME_SIZE_ERROR) + }) + "advertise SETTINGS_MAX_HEADER_LIST_SIZE to the peer" in new TestSetupWithoutHandshake with RequestResponseProbes { network.sendBytes(Http2Protocol.ClientConnectionPreface) @@ -793,6 +805,10 @@ class Http2ServerSpec extends Http2SpecWithMaterializer(""" }) "fail if more data is received than stream-level window allows".inAssertAllStagesStopped( new WaitingForRequestData { + // the single frame below is deliberately bigger than the stream-level buffer, and so also bigger than the + // default max-frame-size; raise that limit so the frame reaches the flow-control check this test is about + override def settings: ServerSettings = super.settings.mapHttp2Settings(_.withMaxFrameSize(1024 * 1024)) + // trigger a connection-level WINDOW_UPDATE network.sendDATA(TheStreamId, endStream = false, ByteString("0000")) entityDataIn.expectUtf8EncodedString("0000") From 8822bbb86bc306490f35c5582e56c0b230283c77 Mon Sep 17 00:00:00 2001 From: PJ Fanning Date: Mon, 31 Aug 2026 20:12:41 +0100 Subject: [PATCH 2/3] Add the Scala 3 MiMa excludes for the new max-frame-size members The new abstract members on the HTTP/2 settings traits are only flagged on Scala 3; the 2.13 check filters them under a broader rule, so a scoped run on the default Scala version alone did not surface them. --- .../max-frame-size.excludes | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 http-core/src/main/mima-filters/2.0.x.backwards.excludes/max-frame-size.excludes diff --git a/http-core/src/main/mima-filters/2.0.x.backwards.excludes/max-frame-size.excludes b/http-core/src/main/mima-filters/2.0.x.backwards.excludes/max-frame-size.excludes new file mode 100644 index 000000000..35d3636d5 --- /dev/null +++ b/http-core/src/main/mima-filters/2.0.x.backwards.excludes/max-frame-size.excludes @@ -0,0 +1,22 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +# new max-frame-size setting +ProblemFilters.exclude[ReversedMissingMethodProblem]("org.apache.pekko.http.javadsl.settings.Http2ClientSettings.maxFrameSize") +ProblemFilters.exclude[ReversedMissingMethodProblem]("org.apache.pekko.http.javadsl.settings.Http2ServerSettings.withMaxFrameSize") +ProblemFilters.exclude[ReversedMissingMethodProblem]("org.apache.pekko.http.scaladsl.settings.Http2ClientSettings.maxFrameSize") +ProblemFilters.exclude[ReversedMissingMethodProblem]("org.apache.pekko.http.scaladsl.settings.Http2ServerSettings.maxFrameSize") From 7d5398ef26c959edab181a2d8421f86c45404291 Mon Sep 17 00:00:00 2001 From: PJ Fanning Date: Sat, 12 Sep 2026 10:52:01 +0100 Subject: [PATCH 3/3] Address review: second oversized-frame test, MiMa filters, conf wording The "release connection-level flow control accounting when a stream-level window is exceeded" test that #1281 added sends the same single 512001-byte DATA frame, so it also needs max-frame-size raised to reach the flow-control check it is about. MiMa filters now cover the concrete javadsl withMaxFrameSize and getMaxFrameSize members, which MiMa still reports as ReversedMissingMethodProblem, plus the problems the 2.13 check against 1.0.0 surfaces after rebasing onto main: the replaced internal Http2FrameParsing constructor, the abstract Http2CommonSettings member, and the copy$default$N shifts from the new case-class field. The reference.conf comment pointed at the buffer-size settings as being above max-frame-size; they are below it. --- .../2.0.x.backwards.excludes/max-frame-size.excludes | 11 +++++++++++ http-core/src/main/resources/reference.conf | 4 ++-- .../http/impl/engine/http2/Http2ServerSpec.scala | 4 +++- 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/http-core/src/main/mima-filters/2.0.x.backwards.excludes/max-frame-size.excludes b/http-core/src/main/mima-filters/2.0.x.backwards.excludes/max-frame-size.excludes index 35d3636d5..1f9d0a43e 100644 --- a/http-core/src/main/mima-filters/2.0.x.backwards.excludes/max-frame-size.excludes +++ b/http-core/src/main/mima-filters/2.0.x.backwards.excludes/max-frame-size.excludes @@ -17,6 +17,17 @@ # new max-frame-size setting ProblemFilters.exclude[ReversedMissingMethodProblem]("org.apache.pekko.http.javadsl.settings.Http2ClientSettings.maxFrameSize") +ProblemFilters.exclude[ReversedMissingMethodProblem]("org.apache.pekko.http.javadsl.settings.Http2ClientSettings.withMaxFrameSize") +ProblemFilters.exclude[ReversedMissingMethodProblem]("org.apache.pekko.http.javadsl.settings.Http2ServerSettings.getMaxFrameSize") ProblemFilters.exclude[ReversedMissingMethodProblem]("org.apache.pekko.http.javadsl.settings.Http2ServerSettings.withMaxFrameSize") ProblemFilters.exclude[ReversedMissingMethodProblem]("org.apache.pekko.http.scaladsl.settings.Http2ClientSettings.maxFrameSize") ProblemFilters.exclude[ReversedMissingMethodProblem]("org.apache.pekko.http.scaladsl.settings.Http2ServerSettings.maxFrameSize") +ProblemFilters.exclude[ReversedMissingMethodProblem]("org.apache.pekko.http.scaladsl.settings.Http2CommonSettings.maxFrameSize") + +# internal frame parser now takes the limit as a constructor parameter +ProblemFilters.exclude[DirectMissingMethodProblem]("org.apache.pekko.http.impl.engine.http2.framing.Http2FrameParsing.this") + +# the new field shifts the positions of the private settings case class parameters after it +ProblemFilters.exclude[IncompatibleResultTypeProblem]("org.apache.pekko.http.scaladsl.settings.Http2ClientSettings#Http2ClientSettingsImpl.copy$default$8") +ProblemFilters.exclude[IncompatibleResultTypeProblem]("org.apache.pekko.http.scaladsl.settings.Http2ClientSettings#Http2ClientSettingsImpl.copy$default$11") +ProblemFilters.exclude[IncompatibleResultTypeProblem]("org.apache.pekko.http.scaladsl.settings.Http2ServerSettings#Http2ServerSettingsImpl.copy$default$9") diff --git a/http-core/src/main/resources/reference.conf b/http-core/src/main/resources/reference.conf index 33e011528..8ca31252f 100644 --- a/http-core/src/main/resources/reference.conf +++ b/http-core/src/main/resources/reference.conf @@ -274,7 +274,7 @@ pekko.http { # # RFC 9113, section 4.2 constrains this to be between 16 KiB and 16 MiB - 1. The amount of request data # buffered overall is bounded separately by the incoming-connection-level-buffer-size and - # incoming-stream-level-buffer-size settings above. + # incoming-stream-level-buffer-size settings below. max-frame-size = 512kB # The maximum number of bytes to receive from a request entity in a single chunk. @@ -537,7 +537,7 @@ pekko.http { # # RFC 9113, section 4.2 constrains this to be between 16 KiB and 16 MiB - 1. The amount of request data # buffered overall is bounded separately by the incoming-connection-level-buffer-size and - # incoming-stream-level-buffer-size settings above. + # incoming-stream-level-buffer-size settings below. max-frame-size = 512kB # The maximum number of bytes to receive from a request entity in a single chunk. diff --git a/http2-tests/src/test/scala/org/apache/pekko/http/impl/engine/http2/Http2ServerSpec.scala b/http2-tests/src/test/scala/org/apache/pekko/http/impl/engine/http2/Http2ServerSpec.scala index 40ba7c644..2319ace06 100644 --- a/http2-tests/src/test/scala/org/apache/pekko/http/impl/engine/http2/Http2ServerSpec.scala +++ b/http2-tests/src/test/scala/org/apache/pekko/http/impl/engine/http2/Http2ServerSpec.scala @@ -825,8 +825,10 @@ class Http2ServerSpec extends Http2SpecWithMaterializer(""" val ConnectionBufferSize = 700000 "release connection-level flow control accounting when a stream-level window is exceeded" .inAssertAllStagesStopped(new WaitingForRequestData { + // the oversized frame also exceeds the default max-frame-size; raise that limit so it reaches flow control override def settings: ServerSettings = - super.settings.mapHttp2Settings(_.withIncomingConnectionLevelBufferSize(ConnectionBufferSize)) + super.settings.mapHttp2Settings( + _.withIncomingConnectionLevelBufferSize(ConnectionBufferSize).withMaxFrameSize(1024 * 1024)) // get the request dispatched and both windows replenished to their configured sizes network.sendDATA(TheStreamId, endStream = false, ByteString("0000"))