remix-run/remix · error · MultipartParseError

Unexpected data after end of stream

Error message

Unexpected data after end of stream

What it means

MultipartParser.write() throws MultipartParseError('Unexpected data after end of stream') when more data arrives after the parser has reached its Done state (the closing boundary was already seen). A well-formed multipart message ends at its terminating delimiter, so extra bytes indicate a corrupted or reused stream.

Source

Thrown at packages/multipart-parser/src/lib/multipart.ts:292

    this.#findOpeningBoundary = createSearch(`--${boundary}`)
    this.#openingBoundaryLength = 2 + boundary.length // length of '--' + boundary
    let boundaryPattern = `\r\n--${boundary}`
    this.#findBoundary = createSearch(boundaryPattern)
    this.#findPartialTailBoundary = createPartialTailSearch(boundaryPattern)
    this.#boundaryLength = 4 + boundary.length // length of '\r\n--' + boundary
    this.#boundaryBytes = encodeAsciiPattern(boundaryPattern)
  }

  /**
   * Write a chunk of data to the parser.
   *
   * @param chunk A chunk of data to write to the parser
   * @yields Parsed {@link MultipartPart} objects that became available from this chunk
   * @returns A generator yielding `MultipartPart` objects as they are parsed
   */
  *write(chunk: Uint8Array): Generator<MultipartPart, void, unknown> {
    if (this.#state === MultipartParserStateDone) {
      throw new MultipartParseError('Unexpected data after end of stream')
    }

    let index = 0
    let chunkLength = chunk.length

    if (this.#buffer !== null) {
      if (this.#state === MultipartParserStateBody) {
        let carry = this.#buffer
        let carryResult = this.#analyzeCarryBoundary(carry, chunk)

        if (carryResult.kind === 'none') {
          this.#append(carry)
        } else if (carryResult.kind === 'partial') {
          if (carryResult.start > 0) {
            this.#append(carry.subarray(0, carryResult.start))
          }

          let tailLength = carry.length + chunk.length - carryResult.start

View on GitHub (pinned to 9696913134)

Solutions

  1. Do not reuse a MultipartParser after it finishes; create a new instance per message
  2. Inspect the raw body for data after the terminating boundary (e.g. log bytes around --boundary--) and fix the client/proxy that appends it
  3. If parsing concatenated messages, split streams per message before feeding the parser

Example fix

// before
let parser = new MultipartParser(boundary)
for (let chunk of chunksA) parser.write(chunk)
for (let chunk of chunksB) parser.write(chunk) // throws if A already terminated

// after
let parserA = new MultipartParser(boundary)
for (let chunk of chunksA) parserA.write(chunk)
let parserB = new MultipartParser(boundary)
for (let chunk of chunksB) parserB.write(chunk)
Defensive patterns

Strategy: validation

Validate before calling

// one parser per message — never reuse after done
let parser = new MultipartParser(boundary)
for (let chunk of messageChunks) {
  if (parser.isDone) break // stop writing once the closing boundary is seen
  parser.write(chunk)
}

Try / catch

try {
  parser.write(chunk)
} catch (error) {
  if (error instanceof MultipartParseError && error.message === 'Unexpected data after end of stream') {
    // stray bytes after the terminating boundary — log and stop reading
    return
  }
  throw error
}

Prevention

When it happens

Trigger: Calling parser.write(chunk) after the closing --boundary-- has been parsed; feeding a concatenated multipart body (two messages glued together) into one parser; reusing a finished parser instance.

Common situations: Keep-alive connection bytes leaking into the body parser, upstream proxies concatenating requests, or client bugs that send the multipart body twice. Rarely, reusing a parser across requests.

Related errors


AI-assisted analysis of remix-run/remix@9696913134 (2026-08-27). Data as JSON: /api/errors/909362db17cffa43. Report an issue: GitHub.