From 8f858501b1d2706f1abbfb6a8430ba54818acdf0 Mon Sep 17 00:00:00 2001 From: PJ Fanning Date: Fri, 26 Sep 2025 01:35:07 +0100 Subject: [PATCH 1/4] avoid ByteString when parsing HTTP/2 headers --- .../http/impl/engine/http2/RequestParsing.scala | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/http-core/src/main/scala/org/apache/pekko/http/impl/engine/http2/RequestParsing.scala b/http-core/src/main/scala/org/apache/pekko/http/impl/engine/http2/RequestParsing.scala index e5f557ee7..f63e3418e 100644 --- a/http-core/src/main/scala/org/apache/pekko/http/impl/engine/http2/RequestParsing.scala +++ b/http-core/src/main/scala/org/apache/pekko/http/impl/engine/http2/RequestParsing.scala @@ -24,7 +24,6 @@ import pekko.http.scaladsl.model._ import pekko.http.scaladsl.model.headers.`Tls-Session-Info` import pekko.http.scaladsl.settings.ServerSettings import pekko.stream.Attributes -import pekko.util.ByteString import pekko.util.OptionVal import scala.annotation.tailrec @@ -191,13 +190,12 @@ private[http2] object RequestParsing { } private[http2] def parseHeaderPair(httpHeaderParser: HttpHeaderParser, name: String, value: String): HttpHeader = { - // FIXME: later modify by adding HttpHeaderParser.parseHttp2Header that would use (name, value) pair directly - // or use a separate, simpler, parser for Http2 - // The odd-looking 'x' below is a by-product of how current parser and HTTP/1.1 work. - // Without '\r\n\x' (x being any additional byte) parsing will fail. See HttpHeaderParserSpec for examples. - val concHeaderLine = name + ": " + value + "\r\nx" - httpHeaderParser.parseHeaderLine(ByteString(concHeaderLine))() - httpHeaderParser.resultHeader + import HttpHeader.ParsingResult + HttpHeader.parse(name, value, httpHeaderParser.settings) match { + case ParsingResult.Ok(header, errors) if errors.isEmpty => header + case ParsingResult.Ok(_, errors) => throw ParsingException(errors.head) + case ParsingResult.Error(info) => throw ParsingException(info) + } } private[http2] def checkRequiredPseudoHeader(name: String, value: AnyRef): Unit = From 12a3c941bbe779309b03a0e71896df312f5efbe4 Mon Sep 17 00:00:00 2001 From: PJ Fanning Date: Fri, 26 Sep 2025 01:37:33 +0100 Subject: [PATCH 2/4] scalafmt --- .../apache/pekko/http/impl/engine/http2/RequestParsing.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/http-core/src/main/scala/org/apache/pekko/http/impl/engine/http2/RequestParsing.scala b/http-core/src/main/scala/org/apache/pekko/http/impl/engine/http2/RequestParsing.scala index f63e3418e..4513e16fd 100644 --- a/http-core/src/main/scala/org/apache/pekko/http/impl/engine/http2/RequestParsing.scala +++ b/http-core/src/main/scala/org/apache/pekko/http/impl/engine/http2/RequestParsing.scala @@ -193,8 +193,8 @@ private[http2] object RequestParsing { import HttpHeader.ParsingResult HttpHeader.parse(name, value, httpHeaderParser.settings) match { case ParsingResult.Ok(header, errors) if errors.isEmpty => header - case ParsingResult.Ok(_, errors) => throw ParsingException(errors.head) - case ParsingResult.Error(info) => throw ParsingException(info) + case ParsingResult.Ok(_, errors) => throw ParsingException(errors.head) + case ParsingResult.Error(info) => throw ParsingException(info) } } From a1ac95295ae3a5fad0afa79a52777e0610bd08d5 Mon Sep 17 00:00:00 2001 From: PJ Fanning Date: Thu, 2 Oct 2025 16:45:53 +0100 Subject: [PATCH 3/4] workaround --- .../pekko/http/impl/engine/http2/Http2Demux.scala | 5 +++-- .../http/impl/engine/http2/RequestParsing.scala | 14 ++++++++++---- 2 files changed, 13 insertions(+), 6 deletions(-) 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..cf42e6779 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 @@ -59,11 +59,12 @@ private[http2] class Http2ClientDemux(http2Settings: Http2ClientSettings, master extends Http2Demux(http2Settings, initialRemoteSettings = Nil, upgraded = false, isServer = false) { def wrapTrailingHeaders(headers: ParsedHeadersFrame): Option[ChunkStreamPart] = { - val headerParser = masterHttpHeaderParser.createShallowCopy() Some(LastChunk(extension = "", headers.keyValuePairs.map { case (name, value: HttpHeader) => value - case (name, value) => parseHeaderPair(headerParser, name, value.asInstanceOf[String]) + case (name, value) => + val headerParser = masterHttpHeaderParser.createShallowCopy() + parseHeaderPair(headerParser, name, value.asInstanceOf[String]) }.toList)) } diff --git a/http-core/src/main/scala/org/apache/pekko/http/impl/engine/http2/RequestParsing.scala b/http-core/src/main/scala/org/apache/pekko/http/impl/engine/http2/RequestParsing.scala index 4513e16fd..2450cbdc3 100644 --- a/http-core/src/main/scala/org/apache/pekko/http/impl/engine/http2/RequestParsing.scala +++ b/http-core/src/main/scala/org/apache/pekko/http/impl/engine/http2/RequestParsing.scala @@ -191,10 +191,16 @@ private[http2] object RequestParsing { private[http2] def parseHeaderPair(httpHeaderParser: HttpHeaderParser, name: String, value: String): HttpHeader = { import HttpHeader.ParsingResult - HttpHeader.parse(name, value, httpHeaderParser.settings) match { - case ParsingResult.Ok(header, errors) if errors.isEmpty => header - case ParsingResult.Ok(_, errors) => throw ParsingException(errors.head) - case ParsingResult.Error(info) => throw ParsingException(info) + if (name.startsWith(":")) { + val concHeaderLine = s"$name: $value\r\nx" + httpHeaderParser.parseHeaderLine(pekko.util.ByteString(concHeaderLine))() + httpHeaderParser.resultHeader + } else { + HttpHeader.parse(name, value, httpHeaderParser.settings) match { + case ParsingResult.Ok(header, errors) if errors.isEmpty => header + case ParsingResult.Ok(_, errors) => throw ParsingException(errors.head) + case ParsingResult.Error(info) => throw ParsingException(info) + } } } From 7e3c22962851352fe8499a18bd67dd4f0f8aa5a6 Mon Sep 17 00:00:00 2001 From: PJ Fanning Date: Thu, 2 Oct 2025 17:28:24 +0100 Subject: [PATCH 4/4] Clarify pseudo-header handling in RequestParsing Added comments to clarify handling of pseudo-headers in the parseHeaderPair method. --- .../apache/pekko/http/impl/engine/http2/RequestParsing.scala | 3 +++ 1 file changed, 3 insertions(+) diff --git a/http-core/src/main/scala/org/apache/pekko/http/impl/engine/http2/RequestParsing.scala b/http-core/src/main/scala/org/apache/pekko/http/impl/engine/http2/RequestParsing.scala index 2450cbdc3..07b343a6e 100644 --- a/http-core/src/main/scala/org/apache/pekko/http/impl/engine/http2/RequestParsing.scala +++ b/http-core/src/main/scala/org/apache/pekko/http/impl/engine/http2/RequestParsing.scala @@ -192,6 +192,9 @@ private[http2] object RequestParsing { private[http2] def parseHeaderPair(httpHeaderParser: HttpHeaderParser, name: String, value: String): HttpHeader = { import HttpHeader.ParsingResult if (name.startsWith(":")) { + // HttpHeader.parse used in `else` block does not support pseudo-headers (that have ':' prefix in header name) + // The odd-looking 'x' below is a by-product of how current parser and HTTP/1.1 work. + // Without '\r\n\x' (x being any additional byte) parsing will fail. See HttpHeaderParserSpec for examples. val concHeaderLine = s"$name: $value\r\nx" httpHeaderParser.parseHeaderLine(pekko.util.ByteString(concHeaderLine))() httpHeaderParser.resultHeader