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 e202ffd09 perf: decode parser byte ranges in bulk instead of char by 
char (#1229)
e202ffd09 is described below

commit e202ffd09197dca9c068cfae0cb02cc9f689f601
Author: PJ Fanning <[email protected]>
AuthorDate: Fri Sep 11 09:55:09 2026 +0100

    perf: decode parser byte ranges in bulk instead of char by char (#1229)
    
    Motivation:
    asciiString built its String one character at a time through indexed 
ByteString
    access and a StringBuilder. Indexed access into a multi-fragment ByteString 
walks
    the fragment list on every byte, and the loop cannot use the JDK's bulk 
decode.
    ByteStringParserInput.sliceString already does the fast thing for the same 
job.
    
    It also sign extended: input(ix).toChar turns byte 0xC3 into U+FFC3 rather 
than
    U+00C3. Three of the call sites can see bytes above 0x7F - the response 
reason
    phrase, chunk extensions, and header names when
    illegal-response-header-name-processing-mode is warn or ignore - so those 
ranges
    were decoded into replacement-looking garbage. The two call sites in
    scanHeaderValue cannot: they only ever cover a range already checked to be 
legal
    7-bit ASCII, where the old and new behaviour agree.
    
    Modification:
    asciiString now slices and calls decodeString(ISO-8859-1), matching
    ByteStringParserInput.sliceString. Reuses the existing ISO88591 constant 
from
    pekko.http.impl.util.
    
    Result:
    One bulk decode instead of a per-byte loop, and byte ranges above 0x7F 
decode to
    the Latin-1 character rather than a sign extended one. Measured over 3M
    extractions of a 4-character header name from a request-sized ByteString: 
234 ms
    before, 167 ms after.
    
    Tests:
    - sbt "http-core / Test / testOnly 
org.apache.pekko.http.impl.engine.parsing.*" - 242 passed
    - New ResponseParserSpec case parses a non-ASCII header name with illegal 
names ignored; it fails before this change, where the name comes out as 
f\uFFC3\uFFB6o
    - sbt http-core/mimaReportBinaryIssues - clean
    - scalafmt --mode diff-ref=upstream/main - clean
    
    References:
    None - found while auditing ByteString usage across the code base
---
 .../apache/pekko/http/impl/engine/parsing/package.scala   | 15 ++++++++-------
 .../http/impl/engine/parsing/ResponseParserSpec.scala     | 14 ++++++++++++++
 2 files changed, 22 insertions(+), 7 deletions(-)

diff --git 
a/http-core/src/main/scala/org/apache/pekko/http/impl/engine/parsing/package.scala
 
b/http-core/src/main/scala/org/apache/pekko/http/impl/engine/parsing/package.scala
index bc42944fd..92b2036fb 100644
--- 
a/http-core/src/main/scala/org/apache/pekko/http/impl/engine/parsing/package.scala
+++ 
b/http-core/src/main/scala/org/apache/pekko/http/impl/engine/parsing/package.scala
@@ -13,14 +13,13 @@
 
 package org.apache.pekko.http.impl.engine
 
-import java.lang.{ StringBuilder => JStringBuilder }
 import org.apache.pekko
 import pekko.http.scaladsl.settings.ParserSettings
 
-import scala.annotation.tailrec
 import pekko.event.LoggingAdapter
 import pekko.util.ByteString
 import pekko.http.scaladsl.model.{ ErrorInfo, StatusCode, StatusCodes }
+import pekko.http.impl.util.ISO88591
 import pekko.http.impl.util.SingletonException
 
 /**
@@ -41,11 +40,13 @@ package object parsing {
   private[http] def byteAt(input: ByteString, ix: Int): Byte =
     if (ix < input.length) input(ix) else throw NotEnoughDataException
 
-  private[http] def asciiString(input: ByteString, start: Int, end: Int): 
String = {
-    @tailrec def build(ix: Int = start, sb: JStringBuilder = new 
JStringBuilder(end - start)): String =
-      if (ix == end) sb.toString else build(ix + 1, 
sb.append(input(ix).toChar))
-    if (start == end) "" else build()
-  }
+  /**
+   * Decodes the given range as a String, one character per byte. Bytes above 
0x7F are decoded as
+   * ISO-8859-1, as [[pekko.http.impl.util.ByteStringParserInput.sliceString]] 
already does; most
+   * callers have validated the range as 7-bit ASCII, for which the two agree.
+   */
+  private[http] def asciiString(input: ByteString, start: Int, end: Int): 
String =
+    if (start == end) "" else input.slice(start, end).decodeString(ISO88591)
 
   private[http] def logParsingError(info: ErrorInfo, log: LoggingAdapter,
       settings: ParserSettings.ErrorLoggingVerbosity,
diff --git 
a/http-core/src/test/scala/org/apache/pekko/http/impl/engine/parsing/ResponseParserSpec.scala
 
b/http-core/src/test/scala/org/apache/pekko/http/impl/engine/parsing/ResponseParserSpec.scala
index 470ba6a2d..cf0ef5acb 100644
--- 
a/http-core/src/test/scala/org/apache/pekko/http/impl/engine/parsing/ResponseParserSpec.scala
+++ 
b/http-core/src/test/scala/org/apache/pekko/http/impl/engine/parsing/ResponseParserSpec.scala
@@ -103,6 +103,20 @@ abstract class ResponseParserSpec(mode: String, newLine: 
String) extends PekkoSp
         closeAfterResponseCompletion shouldEqual Seq(false)
       }
 
+      "a response with a non-ASCII header name, when illegal header names are 
ignored" in new Test {
+        override def parserSettings: ParserSettings =
+          super.parserSettings.withIllegalResponseHeaderNameProcessingMode(
+            ParserSettings.IllegalResponseHeaderNameProcessingMode.Ignore)
+
+        // a header name that is not 7-bit ASCII is an opaque byte range, 
decoded one character per
+        // byte (ISO-8859-1) rather than sign extended into \uFFxx characters
+        val name = new 
String("f\u00f6o".getBytes(java.nio.charset.StandardCharsets.UTF_8),
+          java.nio.charset.StandardCharsets.ISO_8859_1)
+        s"HTTP/1.1 200 OK${newLine}föo: bar${newLine}Content-Length: 
0${newLine}${newLine}" should parseTo(
+          HttpResponse(headers = List(RawHeader(name, "bar"))))
+        closeAfterResponseCompletion shouldEqual Seq(false)
+      }
+
       "a response with a missing reason phrase" in new Test {
         s"HTTP/1.1 404 ${newLine}Content-Length: 0${newLine}${newLine}" should 
parseTo(HttpResponse(NotFound))
         closeAfterResponseCompletion shouldEqual Seq(false)


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

Reply via email to