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 dc2222585 fix: start each multipart part with its own header state 
(#1279)
dc2222585 is described below

commit dc22225857f0cdbbe32d80b3685ea4de74764392
Author: PJ Fanning <[email protected]>
AuthorDate: Fri Sep 11 12:22:23 2026 +0100

    fix: start each multipart part with its own header state (#1279)
    
    When a body part ends without a header-separating empty line, the
    `BoundaryHeader` branch of `parseHeaderLines` emits that part and continues
    into the next one, but passed its own `headers` and `headerCount` along. The
    boundary starts a new part, so both should start empty -- `cth` was already
    reset to `None` on the same call, which suggests the other two were simply
    missed.
    
    Two visible effects, both fixed here:
    
      - the following part was reported carrying headers it never declared;
      - `headerCount` was never reset either, so a run of such parts accumulated
        towards `max-header-count` across parts rather than per part, failing
        parts that are individually well within the limit.
    
    The reset stays a direct self-call to `parseHeaderLines`: that call is what
    makes the method `@tailrec`, and routing it through a helper would turn it
    into an unoptimised mutual recursion.
    
    Fixes #1278.
    
    Co-authored-by: Claude Opus 5 (1M context) <[email protected]>
---
 .../http/impl/engine/parsing/BodyPartParser.scala  | 11 +++---
 .../unmarshalling/MultipartUnmarshallersSpec.scala | 39 ++++++++++++++++++++++
 2 files changed, 45 insertions(+), 5 deletions(-)

diff --git 
a/http-core/src/main/scala/org/apache/pekko/http/impl/engine/parsing/BodyPartParser.scala
 
b/http-core/src/main/scala/org/apache/pekko/http/impl/engine/parsing/BodyPartParser.scala
index fefb0fa5f..85fd09e83 100644
--- 
a/http-core/src/main/scala/org/apache/pekko/http/impl/engine/parsing/BodyPartParser.scala
+++ 
b/http-core/src/main/scala/org/apache/pekko/http/impl/engine/parsing/BodyPartParser.scala
@@ -198,11 +198,12 @@ private[http] final class BodyPartParser(
             emit(BodyPartStart(headers.toList, _ => 
HttpEntity.empty(contentType)))
             val ix = lineStart + eolConfiguration.boundaryLength
             if (eolConfiguration.isEndOfLine(input, ix)) {
-              // an empty part; the boundary starts another one, so it counts 
towards the limit as well. We must not
-              // route this through `parsePartHeaderLines`: the self-recursive 
call below is what keeps this method
-              // tail-recursive, and a mutual recursion here would risk the 
stack overflow the trampoline in
-              // `parseEntity` guards against.
-              if (startPart()) parseHeaderLines(input, ix + 
eolConfiguration.eolLength, headers, headerCount, None)
+              // an empty part; the boundary starts another one, so it counts 
towards the limit as well and its
+              // header state starts empty. We must not route this through 
`parsePartHeaderLines`: the
+              // self-recursive call below is what keeps this method 
tail-recursive, and a mutual recursion here
+              // would risk the stack overflow the trampoline in `parseEntity` 
guards against.
+              if (startPart())
+                parseHeaderLines(input, ix + eolConfiguration.eolLength, 
ListBuffer[HttpHeader](), 0, None)
               else failMaxPartCount()
             } else if (doubleDash(input, ix)) setShouldTerminate()
             else fail("Illegal multipart boundary in message content")
diff --git 
a/http-tests/src/test/scala/org/apache/pekko/http/scaladsl/unmarshalling/MultipartUnmarshallersSpec.scala
 
b/http-tests/src/test/scala/org/apache/pekko/http/scaladsl/unmarshalling/MultipartUnmarshallersSpec.scala
index 01e6c63ae..faec22cf1 100644
--- 
a/http-tests/src/test/scala/org/apache/pekko/http/scaladsl/unmarshalling/MultipartUnmarshallersSpec.scala
+++ 
b/http-tests/src/test/scala/org/apache/pekko/http/scaladsl/unmarshalling/MultipartUnmarshallersSpec.scala
@@ -63,6 +63,45 @@ trait MultipartUnmarshallersSpec extends 
PekkoSpecWithMaterializer {
                        
|--XYZABC--""".stripMarginWithNewline(lineFeed)))).to[Multipart.General] should 
haveParts(
           
Multipart.General.BodyPart.Strict(HttpEntity.empty(ContentTypes.`text/xml(UTF-8)`),
 List(Age(12))))
       }
+      "consecutive parts without header separation, each keeping its own 
headers" in {
+        Unmarshal(HttpEntity(
+          `multipart/mixed`.withBoundary("XYZABC"),
+          ByteString("""--XYZABC
+                       |Age: 12
+                       |--XYZABC
+                       |Age: 13
+                       |--XYZABC
+                       
|--XYZABC--""".stripMarginWithNewline(lineFeed)))).to[Multipart.General] should 
haveParts(
+          
Multipart.General.BodyPart.Strict(HttpEntity.empty(ContentTypes.`text/plain(UTF-8)`),
 List(Age(12))),
+          
Multipart.General.BodyPart.Strict(HttpEntity.empty(ContentTypes.`text/plain(UTF-8)`),
 List(Age(13))),
+          
Multipart.General.BodyPart.Strict(HttpEntity.empty(ContentTypes.`text/plain(UTF-8)`)))
+      }
+      "a part without header separation not carrying its Content-Type into the 
next part" in {
+        Unmarshal(HttpEntity(
+          `multipart/mixed`.withBoundary("XYZABC"),
+          ByteString("""--XYZABC
+                       |Content-type: text/xml; charset=UTF-8
+                       |--XYZABC
+                       
|--XYZABC--""".stripMarginWithNewline(lineFeed)))).to[Multipart.General] should 
haveParts(
+          
Multipart.General.BodyPart.Strict(HttpEntity.empty(ContentTypes.`text/xml(UTF-8)`)),
+          
Multipart.General.BodyPart.Strict(HttpEntity.empty(ContentTypes.`text/plain(UTF-8)`)))
+      }
+      "parts without header separation counting headers per part, not across 
parts" in {
+        implicit val parserSettings: ParserSettings = 
ParserSettings(system).withMaxHeaderCount(2)
+        Unmarshal(HttpEntity(
+          `multipart/mixed`.withBoundary("XYZABC"),
+          ByteString("""--XYZABC
+                       |Age: 12
+                       |X-Foo: bar
+                       |--XYZABC
+                       |Age: 13
+                       |X-Foo: baz
+                       
|--XYZABC--""".stripMarginWithNewline(lineFeed)))).to[Multipart.General] should 
haveParts(
+          
Multipart.General.BodyPart.Strict(HttpEntity.empty(ContentTypes.`text/plain(UTF-8)`),
+            List(Age(12), RawHeader("X-Foo", "bar"))),
+          
Multipart.General.BodyPart.Strict(HttpEntity.empty(ContentTypes.`text/plain(UTF-8)`),
+            List(Age(13), RawHeader("X-Foo", "baz"))))
+      }
       "an implicitly typed part (without headers) (Strict)" in {
         Unmarshal(HttpEntity(
           `multipart/mixed`.withBoundary("XYZABC"),


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

Reply via email to