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]