Skip to content

fix: start each multipart part with its own header state - #1279

Open
pjfanning wants to merge 1 commit into
apache:mainfrom
pjfanning:fix-multipart-header-leak
Open

fix: start each multipart part with its own header state#1279
pjfanning wants to merge 1 commit into
apache:mainfrom
pjfanning:fix-multipart-header-leak

Conversation

@pjfanning

Copy link
Copy Markdown
Member

Motivation

Fixes #1278.

BodyPartParser carried one body part's accumulated headers and headerCount into the next part whenever a part ended without a header-separating empty line. In parseHeaderLines, the BoundaryHeader branch emits the part it has just finished and then continues into the next one:

case BoundaryHeader =>
  emit(BodyPartStart(headers.toList, _ => HttpEntity.empty(contentType)))
  val ix = lineStart + eolConfiguration.boundaryLength
  if (eolConfiguration.isEndOfLine(input, ix))
    parseHeaderLines(input, ix + eolConfiguration.eolLength, headers, headerCount, None)

The boundary starts a new part, so its header state should start empty. cth was already reset to None on that same call, which suggests the intent was to reset here and that headers/headerCount were simply missed.

This is long-standing and inherited from akka-http; it is not a regression.

Modification

Start the next part with a fresh ListBuffer and a headerCount of 0.

The reset deliberately stays a direct self-call to parseHeaderLines rather than being routed through a helper: that call is what makes the method @tailrec, and a mutual recursion here would be unoptimised — the stack overflow the trampoline in parseEntity already guards against.

Result

Two visible effects are fixed:

  • 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 individually well within the limit.

Measured before the fix, for a three-part entity where each part declares one Age header and no separating empty line — the parts inherit each other's headers:

List(Age: 12), List(Age: 12, Age: 13), List(Age: 12, Age: 13)

and with max-header-count = 2, two parts of two headers each failed outright with multipart part contains more than the configured limit of 2 headers.

Tests

Three cases added to MultipartUnmarshallersSpec (so each runs in both the CRLF and LF variants):

  • consecutive parts without header separation each keep their own headers;
  • header counting is per part, not across parts (max-header-count = 2);
  • a part's Content-Type is not carried into the next part — this one already passed before the fix and is a regression guard for the cth reset that was already correct.

Verified the first two fail on main and pass with the fix.

sbt "http-tests/testOnly *MultipartUnmarshallersSpec* *MarshallingSpec *FileUploadDirectivesSpec" — 103 pass. Native scalafmt clean. No MiMa run: the change is confined to a method body inside the anonymous GraphStageLogic.

References

Fixes #1278. Noticed while reviewing #1266, which touches the same branch but does not change this behaviour; the two do not depend on each other, though they will conflict textually and whichever lands second will need a trivial rebase.

🤖 Generated with Claude Code

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 apache#1278.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Multipart parser leaks a part's headers into the following part

1 participant