This is an automated email from the ASF dual-hosted git repository.

pjfanning pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/pekko-http.git


The following commit(s) were added to refs/heads/main by this push:
     new 638a214dc feat: bound the size of an incoming HTTP/2 frame (#1264)
638a214dc is described below

commit 638a214dccb4629de876c26b5cac998bfc5d8f49
Author: PJ Fanning <[email protected]>
AuthorDate: Thu Oct 1 19:26:33 2026 +0100

    feat: bound the size of an incoming HTTP/2 frame (#1264)
    
    * 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 [...]
    - 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
    
    * 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.
    
    * 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.
---
 .../max-frame-size.excludes                        | 33 +++++++++++++++++
 http-core/src/main/resources/reference.conf        | 28 +++++++++++++++
 .../http/impl/engine/http2/Http2Blueprint.scala    | 17 +++++----
 .../engine/http2/framing/Http2FrameParsing.scala   |  9 ++++-
 .../javadsl/settings/Http2ClientSettings.scala     | 15 ++++++++
 .../javadsl/settings/Http2ServerSettings.scala     | 15 ++++++++
 .../scaladsl/settings/Http2ServerSettings.scala    | 42 ++++++++++++++++++++++
 .../http/impl/engine/http2/Http2ServerSpec.scala   | 20 ++++++++++-
 8 files changed, 170 insertions(+), 9 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
new file mode 100644
index 000000000..1f9d0a43e
--- /dev/null
+++ 
b/http-core/src/main/mima-filters/2.0.x.backwards.excludes/max-frame-size.excludes
@@ -0,0 +1,33 @@
+# 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.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 a61326119..8ca31252f 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 below.
+      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 below.
+      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 7f7428cea..230413739 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 56286382b..855533df5 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 a1bfce4fb..a2762156c 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..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
@@ -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")
@@ -809,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"))


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to