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.startView on GitHub (pinned to 9696913134)
Solutions
- Do not reuse a MultipartParser after it finishes; create a new instance per message
- Inspect the raw body for data after the terminating boundary (e.g. log bytes around --boundary--) and fix the client/proxy that appends it
- 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
- Create a fresh MultipartParser per request/message
- Treat trailing data after --boundary-- as a protocol error upstream, not a parser bug
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
- Multipart stream not finished
- Request is not a multipart request
- Request body is empty
- Invalid Content-Type header: missing boundary
- Request is not a multipart request
AI-assisted analysis of remix-run/remix@9696913134 (2026-08-27).
Data as JSON: /api/errors/909362db17cffa43.
Report an issue: GitHub.