remix-run/remix · error · MultipartParseError

Multipart stream not finished

Error message

Multipart stream not finished

What it means

MultipartParser.finish() throws MultipartParseError('Multipart stream not finished') unless the parser reached its Done state, i.e. the closing --boundary-- delimiter was seen. Calling finish on a truncated or unterminated message surfaces the incompleteness instead of silently returning partial results.

Source

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

      if (availableLength >= this.#boundaryLength) {
        return { kind: 'full', start }
      }

      return { kind: 'partial', start }
    }

    return { kind: 'none' }
  }

  /**
   * Should be called after all data has been written to the parser.
   *
   * Note: This will throw if the multipart message is incomplete or
   * wasn't properly terminated.
   */
  finish(): void {
    if (this.#state !== MultipartParserStateDone) {
      throw new MultipartParseError('Multipart stream not finished')
    }
  }
}

let decoder: TextDecoder | undefined

function decodeUtf8(input: Uint8Array): string {
  decoder ??= new TextDecoder('utf-8', { fatal: true })
  return decoder.decode(input as BufferSource)
}

/**
 * The decoded headers for a multipart part, keyed by lower-case header name.
 */
export interface MultipartHeaders {
  readonly [name: string]: string | undefined
}

View on GitHub (pinned to 9696913134)

Solutions

  1. Ensure the sender writes the full body including the terminating --boundary-- delimiter
  2. Verify the boundary used to construct the parser matches the one in the headers
  3. Treat the error as a signal to retry the upload / return 400 to the client

Example fix

# before (client body missing terminator)
--BOUNDARY
Content-Disposition: form-data; name="f"

v

# after
--BOUNDARY
Content-Disposition: form-data; name="f"

v
--BOUNDARY--
Defensive patterns

Strategy: try-catch

Try / catch

try {
  parser.finish()
} catch (error) {
  if (error instanceof MultipartParseError && error.message === 'Multipart stream not finished') {
    // truncated upload — ask the client to retry
    return new Response('Upload was truncated', { status: 400 })
  }
  throw error
}

Prevention

When it happens

Trigger: finish() called after the body ended without the terminating boundary — truncated uploads, dropped connection bytes, a wrong boundary that never matches, or forgetting to write the final chunk.

Common situations: Network interruptions mid-upload, proxies cutting bodies at size thresholds, client bugs omitting the closing delimiter, or tests with hand-written bodies missing --boundary-- at the end.

Related errors


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