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

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


The following commit(s) were added to refs/heads/1.4.x by this push:
     new bcd5fd41c http/2: bound incoming header blocks with 
max-header-list-size (#1247)
bcd5fd41c is described below

commit bcd5fd41cda60b4934d538dcb82168ee9dc39a5f
Author: PJ Fanning <[email protected]>
AuthorDate: Sun Aug 30 19:27:17 2026 +0100

    http/2: bound incoming header blocks with max-header-list-size (#1247)
    
    * http/2: bound incoming header blocks with max-header-list-size (#1216)
    
    Motivation:
    The HTTP/2 header decompression stage had no upper bound on the incoming
    side. The HPACK decoder was constructed with
    Http2Protocol.InitialMaxHeaderListSize (Int.MaxValue) and the header block
    fragments of a HEADERS frame and its CONTINUATION frames were accumulated
    until END_HEADERS was seen, so the memory used for a single header block
    was limited only by what the peer chose to send. Neither endpoint
    advertised SETTINGS_MAX_HEADER_LIST_SIZE, so a peer had no way of knowing
    what it may send either.
    
    Modification:
    Add a `max-header-list-size` setting (64 KiB by default) to
    `pekko.http.server.http2` and `pekko.http.client.http2` and pass it to
    `HeaderDecompression`, which now
    
    * constructs the HPACK decoder with that limit and checks the truncation
      result of `Decoder.endHeaderBlock()`, which was previously ignored,
    * applies the same limit to the accumulated header block fragments,
      accounting each fragment with its frame header size so that the number
      of empty CONTINUATION frames per header block is bounded as well,
    * fails the connection with GOAWAY(ENHANCE_YOUR_CALM) when the limit is
      exceeded.
    
    The configured value is advertised to the peer in the initial SETTINGS
    frame.
    
    Result:
    The memory used for a single incoming header block is bounded by the
    configured limit on both the server and the client side, and peers are
    told about the limit up front.
    
    Tests:
    - sbt "http2-tests/testOnly 
org.apache.pekko.http.impl.engine.http2.Http2ServerSpec" - pass, 5 new tests
    - sbt http2-tests/test - pass
    - sbt http-core/test - pass (HostConnectionPoolSpec flaked in the full run, 
passes on its own)
    - sbt +http-core/mimaReportBinaryIssues - pass
    - sbt http-core/scalafmt http2-tests/Test/scalafmt - clean
    - sbt http-core/headerCreateAll - no changes
    
    References:
    None - bounds the memory used for incoming HTTP/2 header blocks
    
    * Create http2-max-header-list-size.excludes
    
    * docs: the max-header-list-size settings ship in 1.4.1, not 2.0.0
    
    Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
    
    * fix: drop main-only changes that the backport pulled in
    
    `ParsedHeadersFrame` has no error-info field on 1.4.x and the parsing
    exception is not routed through it, so the header decompression stage
    keeps 1.4.x's four-argument frame and its IOException-only handling,
    along with `ByteStringInputStream`. Only the header list size checks are
    new. `RequestParsingSpec` keeps unpacking `futureValue` exceptions, since
    a malformed request still fails the stream here.
    
    Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
    
    ---------
    
    Co-authored-by: Claude Opus 5 (1M context) <[email protected]>
---
 .../http2-max-header-list-size.excludes            | 24 ++++++++
 http-core/src/main/resources/reference.conf        | 30 ++++++++++
 .../http/impl/engine/http2/Http2Blueprint.scala    |  8 +--
 .../pekko/http/impl/engine/http2/Http2Demux.scala  |  3 +-
 .../engine/http2/hpack/HeaderDecompression.scala   | 43 +++++++++++---
 .../javadsl/settings/Http2ClientSettings.scala     | 14 +++++
 .../javadsl/settings/Http2ServerSettings.scala     | 14 +++++
 .../scaladsl/settings/Http2ServerSettings.scala    | 35 +++++++++++
 .../http/impl/engine/http2/Http2ServerSpec.scala   | 69 ++++++++++++++++++++++
 .../impl/engine/http2/RequestParsingSpec.scala     |  2 +-
 10 files changed, 229 insertions(+), 13 deletions(-)

diff --git 
a/http-core/src/main/mima-filters/1.4.x.backwards.excludes/http2-max-header-list-size.excludes
 
b/http-core/src/main/mima-filters/1.4.x.backwards.excludes/http2-max-header-list-size.excludes
new file mode 100644
index 000000000..b069604f7
--- /dev/null
+++ 
b/http-core/src/main/mima-filters/1.4.x.backwards.excludes/http2-max-header-list-size.excludes
@@ -0,0 +1,24 @@
+# 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-header-list-size setting for HTTP/2 (1.4.1)
+ProblemFilters.exclude[ReversedMissingMethodProblem]("org.apache.pekko.http.javadsl.settings.Http2ClientSettings.maxHeaderListSize")
+ProblemFilters.exclude[ReversedMissingMethodProblem]("org.apache.pekko.http.javadsl.settings.Http2ClientSettings.withMaxHeaderListSize")
+ProblemFilters.exclude[ReversedMissingMethodProblem]("org.apache.pekko.http.javadsl.settings.Http2ServerSettings.getMaxHeaderListSize")
+ProblemFilters.exclude[ReversedMissingMethodProblem]("org.apache.pekko.http.javadsl.settings.Http2ServerSettings.withMaxHeaderListSize")
+ProblemFilters.exclude[ReversedMissingMethodProblem]("org.apache.pekko.http.scaladsl.settings.Http2ClientSettings.maxHeaderListSize")
+ProblemFilters.exclude[ReversedMissingMethodProblem]("org.apache.pekko.http.scaladsl.settings.Http2ServerSettings.maxHeaderListSize")
diff --git a/http-core/src/main/resources/reference.conf 
b/http-core/src/main/resources/reference.conf
index 9fe7894fa..8b47a7719 100644
--- a/http-core/src/main/resources/reference.conf
+++ b/http-core/src/main/resources/reference.conf
@@ -243,6 +243,21 @@ pekko.http {
       # the connection was established but before it received our SETTINGS.
       max-concurrent-streams = 256
 
+      # The maximum size of the header list (the sum of the sizes of the 
decompressed header names and values) that
+      # this endpoint is prepared to accept, in bytes. The value is advertised 
to the peer using the
+      # SETTINGS_MAX_HEADER_LIST_SIZE setting. A header block that 
decompresses to more than this amount is rejected
+      # with a GOAWAY(ENHANCE_YOUR_CALM) frame instead of being buffered.
+      #
+      # The same limit is applied to the accumulated header block fragments 
carried by a HEADERS frame and its
+      # subsequent CONTINUATION frames, so that the memory used for a header 
block that the peer never completes
+      # (END_HEADERS is never set) stays bounded. Each fragment is accounted 
with its frame header size on top of
+      # its payload size, which also bounds the number of empty fragments that 
are accepted for one header block.
+      #
+      # Note that peers calculate the header list size with an extra overhead 
of 32 octets per header field (see
+      # RFC 9113, section 6.5.2) while this implementation only counts the 
actual name and value bytes, so the
+      # effective limit for a well-behaved peer is somewhat stricter than the 
configured value.
+      max-header-list-size = 64 KiB
+
       # 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
@@ -438,6 +453,21 @@ pekko.http {
       # the connection was established but before it received our SETTINGS.
       max-concurrent-streams = 256
 
+      # The maximum size of the header list (the sum of the sizes of the 
decompressed header names and values) that
+      # this endpoint is prepared to accept, in bytes. The value is advertised 
to the peer using the
+      # SETTINGS_MAX_HEADER_LIST_SIZE setting. A header block that 
decompresses to more than this amount is rejected
+      # with a GOAWAY(ENHANCE_YOUR_CALM) frame instead of being buffered.
+      #
+      # The same limit is applied to the accumulated header block fragments 
carried by a HEADERS frame and its
+      # subsequent CONTINUATION frames, so that the memory used for a header 
block that the peer never completes
+      # (END_HEADERS is never set) stays bounded. Each fragment is accounted 
with its frame header size on top of
+      # its payload size, which also bounds the number of empty fragments that 
are accepted for one header block.
+      #
+      # Note that peers calculate the header list size with an extra overhead 
of 32 octets per header field (see
+      # RFC 9113, section 6.5.2) while this implementation only counts the 
actual name and value bytes, so the
+      # effective limit for a well-behaved peer is somewhat stricter than the 
configured value.
+      max-header-list-size = 64 KiB
+
       # 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 f732085b0..7ed03461c 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
@@ -127,7 +127,7 @@ private[http] object Http2Blueprint {
       httpLayer(settings, log, dateHeaderRendering) atopKeepRight
       serverDemux(settings.http2Settings, initialDemuxerSettings, upgraded) 
atop
       FrameLogger.logFramesIfEnabled(settings.http2Settings.logFrames) atop // 
enable for debugging
-      hpackCoding(masterHttpHeaderParser, settings.parserSettings)
+      hpackCoding(masterHttpHeaderParser, settings.parserSettings, 
settings.http2Settings.maxHeaderListSize)
 
     val frameTypesForThrottle = 
getFrameTypesForThrottle(settings.http2Settings)
     
@@ -153,7 +153,7 @@ private[http] object Http2Blueprint {
       httpLayerClient(masterHttpHeaderParser, settings, log)).atop(
       clientDemux(settings.http2Settings, masterHttpHeaderParser)).atop(
       FrameLogger.logFramesIfEnabled(settings.http2Settings.logFrames)).atop( 
// enable for debugging
-      hpackCoding(masterHttpHeaderParser, settings.parserSettings)).atop(
+      hpackCoding(masterHttpHeaderParser, settings.parserSettings, 
settings.http2Settings.maxHeaderListSize)).atop(
       framingClient(log)).atop(
       errorHandling(log)).atop(
       idleTimeoutIfConfigured(settings.idleTimeout))
@@ -247,11 +247,11 @@ private[http] object Http2Blueprint {
    * TODO: introduce another FrameEvent type that exclude HeadersFrame and 
ContinuationFrame from
    * reaching the higher-level.
    */
-  def hpackCoding(masterHttpHeaderParser: HttpHeaderParser, parserSettings: 
ParserSettings)
+  def hpackCoding(masterHttpHeaderParser: HttpHeaderParser, parserSettings: 
ParserSettings, maxHeaderListSize: Int)
       : BidiFlow[FrameEvent, FrameEvent, FrameEvent, FrameEvent, NotUsed] =
     BidiFlow.fromFlows(
       Flow[FrameEvent].via(HeaderCompression),
-      Flow[FrameEvent].via(new HeaderDecompression(masterHttpHeaderParser, 
parserSettings)))
+      Flow[FrameEvent].via(new HeaderDecompression(masterHttpHeaderParser, 
parserSettings, maxHeaderListSize)))
 
   /**
    * Creates substreams for every stream and manages stream state machines
diff --git 
a/http-core/src/main/scala/org/apache/pekko/http/impl/engine/http2/Http2Demux.scala
 
b/http-core/src/main/scala/org/apache/pekko/http/impl/engine/http2/Http2Demux.scala
index f05bbb446..aff2b5c84 100644
--- 
a/http-core/src/main/scala/org/apache/pekko/http/impl/engine/http2/Http2Demux.scala
+++ 
b/http-core/src/main/scala/org/apache/pekko/http/impl/engine/http2/Http2Demux.scala
@@ -295,7 +295,8 @@ private[http2] abstract class Http2Demux(http2Settings: 
Http2CommonSettings,
       // enforced immediately even before the acknowledgement is received.
       // Reminder: the receiver of a SETTINGS frame must process them in the 
order they are received.
       val initialLocalSettings: immutable.Seq[Setting] = immutable.Seq(
-        Setting(SettingIdentifier.SETTINGS_MAX_CONCURRENT_STREAMS, 
http2Settings.maxConcurrentStreams)) ++
+        Setting(SettingIdentifier.SETTINGS_MAX_CONCURRENT_STREAMS, 
http2Settings.maxConcurrentStreams),
+        Setting(SettingIdentifier.SETTINGS_MAX_HEADER_LIST_SIZE, 
http2Settings.maxHeaderListSize)) ++
         immutable.Seq(Setting(SettingIdentifier.SETTINGS_ENABLE_PUSH, 
0)).filter(_ => !isServer) // only on client
 
       override def preStart(): Unit = {
diff --git 
a/http-core/src/main/scala/org/apache/pekko/http/impl/engine/http2/hpack/HeaderDecompression.scala
 
b/http-core/src/main/scala/org/apache/pekko/http/impl/engine/http2/hpack/HeaderDecompression.scala
index a22e7c6c4..fc7aafb36 100644
--- 
a/http-core/src/main/scala/org/apache/pekko/http/impl/engine/http2/hpack/HeaderDecompression.scala
+++ 
b/http-core/src/main/scala/org/apache/pekko/http/impl/engine/http2/hpack/HeaderDecompression.scala
@@ -35,13 +35,23 @@ import scala.collection.immutable.VectorBuilder
  * INTERNAL API
  *
  * Can be used on server and client side.
+ *
+ * @param maxHeaderListSize the maximum size of a decoded header list. The 
same limit is applied to the accumulated
+ *                          header block fragments of a HEADERS frame and its 
CONTINUATION frames, so that the memory
+ *                          used for a header block that the peer never 
completes stays bounded
+ *                          (see RFC 9113, section 10.5).
  */
 @InternalApi
-private[http2] final class HeaderDecompression(masterHeaderParser: 
HttpHeaderParser, parserSettings: ParserSettings)
+private[http2] final class HeaderDecompression(masterHeaderParser: 
HttpHeaderParser, parserSettings: ParserSettings,
+    maxHeaderListSize: Int)
     extends GraphStage[FlowShape[FrameEvent, FrameEvent]] {
   val UTF8 = StandardCharsets.UTF_8
   val US_ASCII = StandardCharsets.US_ASCII
 
+  // Each fragment is accounted with the size of its frame header on top of 
its payload size. Without that, empty
+  // CONTINUATION frames would never add to the accumulated header block and 
their number would be unbounded.
+  private val FrameHeaderSize = 9
+
   val eventsIn = Inlet[FrameEvent]("HeaderDecompression.eventsIn")
   val eventsOut = Outlet[FrameEvent]("HeaderDecompression.eventsOut")
 
@@ -50,8 +60,8 @@ private[http2] final class 
HeaderDecompression(masterHeaderParser: HttpHeaderPar
   def createLogic(inheritedAttributes: Attributes): GraphStageLogic =
     new HandleOrPassOnStage[FrameEvent, FrameEvent](shape) {
       val httpHeaderParser = masterHeaderParser.createShallowCopy()
-      val decoder = new 
pekko.http.shaded.com.twitter.hpack.Decoder(Http2Protocol.InitialMaxHeaderListSize,
-        Http2Protocol.InitialMaxHeaderTableSize)
+      val decoder =
+        new pekko.http.shaded.com.twitter.hpack.Decoder(maxHeaderListSize, 
Http2Protocol.InitialMaxHeaderTableSize)
 
       become(Idle)
 
@@ -94,11 +104,13 @@ private[http2] final class 
HeaderDecompression(masterHeaderParser: HttpHeaderPar
         }
         try {
           decoder.decode(ByteStringInputStream(payload), Receiver)
-          decoder.endHeaderBlock() // TODO: do we have to check the result 
here?
+          // the decoder stops emitting headers as soon as the limit is 
exceeded and reports that here
+          val truncated = decoder.endHeaderBlock()
 
-          push(eventsOut, ParsedHeadersFrame(streamId, endStream, 
headers.result(), prioInfo))
+          if (truncated) headerListSizeExceeded(streamId)
+          else push(eventsOut, ParsedHeadersFrame(streamId, endStream, 
headers.result(), prioInfo))
         } catch {
-          case ex: IOException =>
+          case _: IOException =>
             // this is signalled by the decoder when it failed, we want to 
react to this by rendering a GOAWAY frame
             fail(eventsOut,
               new 
Http2Compliance.Http2ProtocolException(ErrorCode.COMPRESSION_ERROR, 
"Decompression failed."))
@@ -109,6 +121,7 @@ private[http2] final class 
HeaderDecompression(masterHeaderParser: HttpHeaderPar
         val handleEvent: PartialFunction[FrameEvent, Unit] = {
           case HeadersFrame(streamId, endStream, endHeaders, fragment, 
prioInfo) =>
             if (endHeaders) parseAndEmit(streamId, endStream, fragment, 
prioInfo)
+            else if (exceedsMaxHeaderListSize(0, fragment)) 
headerListSizeExceeded(streamId)
             else {
               become(new ReceivingHeaders(streamId, endStream, fragment, 
prioInfo))
               pull(eventsIn)
@@ -122,14 +135,21 @@ private[http2] final class 
HeaderDecompression(masterHeaderParser: HttpHeaderPar
       class ReceivingHeaders(streamId: Int, endStream: Boolean, 
initiallyReceivedData: ByteString,
           priorityInfo: Option[PriorityFrame]) extends State {
         var receivedData = initiallyReceivedData
+        // includes the frame headers of the fragments received so far, see 
`FrameHeaderSize`
+        var accountedSize: Long = FrameHeaderSize + initiallyReceivedData.size
 
         val handleEvent: PartialFunction[FrameEvent, Unit] = {
           case ContinuationFrame(`streamId`, endHeaders, payload) =>
-            if (endHeaders) {
+            if (exceedsMaxHeaderListSize(accountedSize, payload))
+              // Neither the HPACK decoder nor any of the checks further down 
the line run before the header block
+              // is complete, so this is the only place where the size of an 
unfinished header block is bounded.
+              headerListSizeExceeded(streamId)
+            else if (endHeaders) {
               parseAndEmit(streamId, endStream, receivedData ++ payload, 
priorityInfo)
               become(Idle)
             } else {
               receivedData ++= payload
+              accountedSize += FrameHeaderSize + payload.size
               pull(eventsIn)
             }
           case x =>
@@ -137,6 +157,15 @@ private[http2] final class 
HeaderDecompression(masterHeaderParser: HttpHeaderPar
         }
       }
 
+      def exceedsMaxHeaderListSize(accountedSize: Long, payload: ByteString): 
Boolean =
+        accountedSize + FrameHeaderSize + payload.size > maxHeaderListSize
+
+      def headerListSizeExceeded(streamId: Int): Unit =
+        fail(eventsOut,
+          new Http2ProtocolException(
+            ErrorCode.ENHANCE_YOUR_CALM,
+            s"Header block of stream $streamId exceeded the configured 
max-header-list-size of $maxHeaderListSize bytes"))
+
       def protocolError(msg: String): Unit = failStage(new 
Http2ProtocolException(msg))
     }
 }
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 1ac4be45a..7f7428cea 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
@@ -34,6 +34,20 @@ trait Http2ClientSettings { self: 
scaladsl.settings.Http2ClientSettings.Http2Cli
   def maxConcurrentStreams: Int
   def withMaxConcurrentStreams(newValue: Int): Http2ClientSettings = 
copy(maxConcurrentStreams = newValue)
 
+  /**
+   * The maximum size of a decoded header list that this endpoint is prepared 
to accept, in bytes. The value is
+   * advertised to the peer via SETTINGS_MAX_HEADER_LIST_SIZE and the same 
limit is applied to the accumulated
+   * header block fragments of a HEADERS frame and its CONTINUATION frames.
+   *
+   * @since 1.4.1
+   */
+  def maxHeaderListSize: Int
+
+  /**
+   * @since 1.4.1
+   */
+  def withMaxHeaderListSize(newValue: Int): Http2ClientSettings = 
copy(maxHeaderListSize = 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 e9830fb6b..0ec10020c 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
@@ -42,6 +42,20 @@ trait Http2ServerSettings {
   def getMaxConcurrentStreams: Int = maxConcurrentStreams
   def withMaxConcurrentStreams(newValue: Int): Http2ServerSettings
 
+  /**
+   * The maximum size of a decoded header list that this endpoint is prepared 
to accept, in bytes. The value is
+   * advertised to the peer via SETTINGS_MAX_HEADER_LIST_SIZE and the same 
limit is applied to the accumulated
+   * header block fragments of a HEADERS frame and its CONTINUATION frames.
+   *
+   * @since 1.4.1
+   */
+  def getMaxHeaderListSize: Int = maxHeaderListSize
+
+  /**
+   * @since 1.4.1
+   */
+  def withMaxHeaderListSize(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 e2c7eac5d..8bf3a1790 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
@@ -41,6 +41,7 @@ private[http] trait Http2CommonSettings {
 
   def logFrames: Boolean
   def maxConcurrentStreams: Int
+  def maxHeaderListSize: Int
   def outgoingControlFrameBufferSize: Int
 
   def pingInterval: FiniteDuration
@@ -90,6 +91,20 @@ trait Http2ServerSettings extends 
javadsl.settings.Http2ServerSettings with Http
   def maxConcurrentStreams: Int
   override def withMaxConcurrentStreams(newValue: Int): Http2ServerSettings = 
copy(maxConcurrentStreams = newValue)
 
+  /**
+   * The maximum size of a decoded header list that this endpoint is prepared 
to accept, in bytes. The value is
+   * advertised to the peer via SETTINGS_MAX_HEADER_LIST_SIZE and the same 
limit is applied to the accumulated
+   * header block fragments of a HEADERS frame and its CONTINUATION frames.
+   *
+   * @since 1.4.1
+   */
+  def maxHeaderListSize: Int
+
+  /**
+   * @since 1.4.1
+   */
+  override def withMaxHeaderListSize(newValue: Int): Http2ServerSettings = 
copy(maxHeaderListSize = newValue)
+
   def outgoingControlFrameBufferSize: Int
   override def withOutgoingControlFrameBufferSize(newValue: Int): 
Http2ServerSettings =
     copy(outgoingControlFrameBufferSize = newValue)
@@ -129,6 +144,7 @@ object Http2ServerSettings extends 
SettingsCompanion[Http2ServerSettings] {
 
   private[http] case class Http2ServerSettingsImpl(
       maxConcurrentStreams: Int,
+      maxHeaderListSize: Int,
       requestEntityChunkSize: Int,
       incomingConnectionLevelBufferSize: Int,
       incomingStreamLevelBufferSize: Int,
@@ -144,6 +160,7 @@ object Http2ServerSettings extends 
SettingsCompanion[Http2ServerSettings] {
       internalSettings: Option[Http2InternalServerSettings])
       extends Http2ServerSettings {
     require(maxConcurrentStreams >= 0, "max-concurrent-streams must be >= 0")
+    require(maxHeaderListSize > 0, "max-header-list-size must be > 0")
     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")
@@ -161,6 +178,7 @@ object Http2ServerSettings extends 
SettingsCompanion[Http2ServerSettings] {
       extends 
pekko.http.impl.util.SettingsCompanionImpl[Http2ServerSettingsImpl]("pekko.http.server.http2")
 {
     def fromSubConfig(root: Config, c: Config): Http2ServerSettingsImpl = 
Http2ServerSettingsImpl(
       maxConcurrentStreams = c.getInt("max-concurrent-streams"),
+      maxHeaderListSize = c.getIntBytes("max-header-list-size"),
       requestEntityChunkSize = c.getIntBytes("request-entity-chunk-size"),
       incomingConnectionLevelBufferSize = 
c.getIntBytes("incoming-connection-level-buffer-size"),
       incomingStreamLevelBufferSize = 
c.getIntBytes("incoming-stream-level-buffer-size"),
@@ -205,6 +223,20 @@ trait Http2ClientSettings extends 
javadsl.settings.Http2ClientSettings with Http
   def maxConcurrentStreams: Int
   override def withMaxConcurrentStreams(newValue: Int): Http2ClientSettings = 
copy(maxConcurrentStreams = newValue)
 
+  /**
+   * The maximum size of a decoded header list that this endpoint is prepared 
to accept, in bytes. The value is
+   * advertised to the peer via SETTINGS_MAX_HEADER_LIST_SIZE and the same 
limit is applied to the accumulated
+   * header block fragments of a HEADERS frame and its CONTINUATION frames.
+   *
+   * @since 1.4.1
+   */
+  def maxHeaderListSize: Int
+
+  /**
+   * @since 1.4.1
+   */
+  override def withMaxHeaderListSize(newValue: Int): Http2ClientSettings = 
copy(maxHeaderListSize = newValue)
+
   def outgoingControlFrameBufferSize: Int
   override def withOutgoingControlFrameBufferSize(newValue: Int): 
Http2ClientSettings =
     copy(outgoingControlFrameBufferSize = newValue)
@@ -244,6 +276,7 @@ object Http2ClientSettings extends 
SettingsCompanion[Http2ClientSettings] {
 
   private[http] case class Http2ClientSettingsImpl(
       maxConcurrentStreams: Int,
+      maxHeaderListSize: Int,
       requestEntityChunkSize: Int,
       incomingConnectionLevelBufferSize: Int,
       incomingStreamLevelBufferSize: Int,
@@ -258,6 +291,7 @@ object Http2ClientSettings extends 
SettingsCompanion[Http2ClientSettings] {
       internalSettings: Option[Http2InternalClientSettings])
       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")
     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")
@@ -272,6 +306,7 @@ object Http2ClientSettings extends 
SettingsCompanion[Http2ClientSettings] {
       extends 
pekko.http.impl.util.SettingsCompanionImpl[Http2ClientSettingsImpl]("pekko.http.client.http2")
 {
     def fromSubConfig(root: Config, c: Config): Http2ClientSettingsImpl = 
Http2ClientSettingsImpl(
       maxConcurrentStreams = c.getInt("max-concurrent-streams"),
+      maxHeaderListSize = c.getIntBytes("max-header-list-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 cd3f459be..95892435e 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
@@ -17,6 +17,7 @@ import org.apache.pekko
 import pekko.NotUsed
 import pekko.http.impl.engine.http2.FrameEvent._
 import pekko.http.impl.engine.http2.Http2Protocol.{ ErrorCode, Flags, 
FrameType, SettingIdentifier }
+import pekko.http.impl.engine.http2.framing.FrameRenderer
 import pekko.http.impl.engine.server.{ HttpAttributes, ServerTerminator }
 import pekko.http.impl.engine.ws.ByteStringSinkProbe
 import pekko.http.scaladsl.client.RequestBuilding.Get
@@ -191,6 +192,74 @@ class Http2ServerSpec extends Http2SpecWithMaterializer("""
           headerPayload shouldBe HPackSpecExamples.C61FirstResponseWithHuffman
         })
 
+      "reject an unfinished header block that grows beyond 
max-header-list-size".inAssertAllStagesStopped(
+        new TestSetup with RequestResponseProbes {
+          override def settings: ServerSettings = 
super.settings.mapHttp2Settings(_.withMaxHeaderListSize(4096))
+
+          val headerBlock = HPackSpecExamples.C41FirstRequestWithHuffman
+          network.sendHEADERS(1, endStream = true, endHeaders = false, 
headerBlock)
+
+          // the peer never sets END_HEADERS, so neither the HPACK decoder nor 
request dispatch ever run
+          val fragment = ByteString(new Array[Byte](1024))
+          (1 to 5).foreach(_ => network.sendCONTINUATION(1, endHeaders = 
false, fragment))
+
+          user.requestIn.ensureSubscription()
+          user.requestIn.expectNoMessage(100.millis)
+
+          val (_, errorCode) = network.expectGOAWAY()
+          errorCode should ===(ErrorCode.ENHANCE_YOUR_CALM)
+        })
+      "reject an unfinished header block made up of empty CONTINUATION 
frames".inAssertAllStagesStopped(
+        new TestSetup with RequestResponseProbes {
+          override def settings: ServerSettings = 
super.settings.mapHttp2Settings(_.withMaxHeaderListSize(256))
+
+          val headerBlock = HPackSpecExamples.C41FirstRequestWithHuffman
+          network.sendHEADERS(1, endStream = true, endHeaders = false, 
headerBlock)
+
+          // empty fragments don't grow the header block but are accounted 
with their frame header size, so their
+          // number is bounded as well (sent in one go because the connection 
is failed in between)
+          network.sendBytes((1 to 64).map(_ =>
+            FrameRenderer.render(ContinuationFrame(1, endHeaders = false, 
ByteString.empty))).reduce(_ ++ _))
+
+          user.requestIn.ensureSubscription()
+          user.requestIn.expectNoMessage(100.millis)
+
+          val (_, errorCode) = network.expectGOAWAY()
+          errorCode should ===(ErrorCode.ENHANCE_YOUR_CALM)
+        })
+      "reject a header block that decodes to more than 
max-header-list-size".inAssertAllStagesStopped(
+        new TestSetup with RequestResponseProbes {
+          override def settings: ServerSettings = 
super.settings.mapHttp2Settings(_.withMaxHeaderListSize(1024))
+
+          val request = HttpRequest(
+            uri = "http://www.example.com/";,
+            headers = RawHeader("big-header", "x" * 2000) :: Nil)
+          network.sendHEADERS(1, endStream = true, endHeaders = true, 
network.encodeRequestHeaders(request))
+
+          user.requestIn.ensureSubscription()
+          user.requestIn.expectNoMessage(100.millis)
+
+          val (_, errorCode) = network.expectGOAWAY()
+          errorCode should ===(ErrorCode.ENHANCE_YOUR_CALM)
+        })
+      "accept a header block that stays within 
max-header-list-size".inAssertAllStagesStopped(
+        new TestSetup with RequestResponseProbes {
+          override def settings: ServerSettings = 
super.settings.mapHttp2Settings(_.withMaxHeaderListSize(1024))
+
+          val request =
+            HttpRequest(uri = "http://www.example.com/";, headers = 
RawHeader("small-header", "x" * 100) :: Nil)
+          network.sendHEADERS(1, endStream = true, endHeaders = true, 
network.encodeRequestHeaders(request))
+
+          user.expectRequest().headers should 
contain(RawHeader("small-header", "x" * 100))
+        })
+
+      "advertise SETTINGS_MAX_HEADER_LIST_SIZE to the peer" in
+      new TestSetupWithoutHandshake with RequestResponseProbes {
+        network.sendBytes(Http2Protocol.ClientConnectionPreface)
+        network.expectSETTINGS().settings should contain(
+          Setting(SettingIdentifier.SETTINGS_MAX_HEADER_LIST_SIZE, 
settings.http2Settings.maxHeaderListSize))
+      }
+
       "fail if Http2StreamIdHeader missing" in pending
       "automatically add `Date` header" in pending
 
diff --git 
a/http2-tests/src/test/scala/org/apache/pekko/http/impl/engine/http2/RequestParsingSpec.scala
 
b/http2-tests/src/test/scala/org/apache/pekko/http/impl/engine/http2/RequestParsingSpec.scala
index b05334c5d..57579b500 100644
--- 
a/http2-tests/src/test/scala/org/apache/pekko/http/impl/engine/http2/RequestParsingSpec.scala
+++ 
b/http2-tests/src/test/scala/org/apache/pekko/http/impl/engine/http2/RequestParsingSpec.scala
@@ -54,7 +54,7 @@ class RequestParsingSpec extends PekkoSpecWithMaterializer 
with Inside with Insp
         RequestParsing.parseRequest(headerParser, serverSettings, attributes)
 
       try Source.single(frame)
-          .via(new HeaderDecompression(headerParser, parserSettings))
+          .via(new HeaderDecompression(headerParser, parserSettings, 
serverSettings.http2Settings.maxHeaderListSize))
           .map { // emulate demux
             case headers: ParsedHeadersFrame =>
               Http2SubStream(


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

Reply via email to