agalwood/Motrix · error · AppError

TaskFinalizeRenameFailed

TaskFinalizeRenameFailed

Error message

Failed to rename file: ${cause}

What it means

Thrown by finalize-task.ts:230 when the atomic rename of a completed single-file (HTTP/FTP) download from its `.motrix` temp path to the desired final path fails. `deps.fs.renameAtomic(task.diskPath, desiredFinalPath)` rejected, so finalize cannot move the file into place; `failFinalize` records the failure before rethrowing. The cause string is the underlying FS error (EACCES, EXDEV, ENOSPC, ENAMETOOLONG, etc.).

Source

Thrown at src/core/task/actions/finalize-task.ts:230

  // RPC error) is logged and we fall through with whatever we already had.
  await refreshTaskBytesBeforeFinalize(task, deps)

  // removeDownloadResult before rename so aria2 releases the file
  // handle — on Windows an open handle causes a sharing violation.
  await deps.adapter.removeDownloadResult(task.engineTaskId)

  try {
    await deps.fs.renameAtomic(task.diskPath, desiredFinalPath)
  } catch (e) {
    const cause = (e as Error).message
    const errorMessage = `Failed to rename file: ${cause}`
    await failFinalize(task, deps, {
      errorMessage,
      errorDetailKey: 'task.error.detail.renameFileFailed',
      errorDetailParams: { cause },
      hookCode: 'TASK_FINALIZE_RENAME_FAILED',
    })
    throw new AppError(ErrorCode.TaskFinalizeRenameFailed, errorMessage, e)
  }
  const completedAt = Date.now()

  // Commit staged plugin metadata now that rename succeeded. The SQLite
  // tx body itself is empty here — the rename happened outside the tx
  // (it's async IO; SQLite transactions must complete synchronously).
  try {
    finalizeOutcome.commit(() => {})
  } catch (err) {
    deps.log.warn(
      { taskId: task.id, err: (err as Error).message },
      'finalize_http_metadata_commit_failed'
    )
  }

  task.finalPath = desiredFinalPath
  const previousStatus = task.status
  completeTaskAfterRename(

View on GitHub (pinned to 1a708ee577)

Solutions

  1. Inspect the `cause` value in the `task.error.detail.renameFileFailed` detail — an `EXDEV` means cross-device; move both temp and final under the same filesystem or implement a copy+delete fallback.
  2. Verify the user account has write permission on `desiredFinalPath`'s parent directory and that the volume has free space.
  3. Ensure `diskPath` (the `.motrix` file) still exists when finalize runs — no external process is cleaning the temp dir mid-finalize.
  4. If renaming across volumes is a supported scenario, fall back to a stream copy followed by unlink instead of `renameAtomic`.

Example fix

// before
await deps.fs.renameAtomic(task.diskPath, desiredFinalPath)

// after (cross-volume safe)
try {
  await deps.fs.renameAtomic(task.diskPath, desiredFinalPath)
} catch (e) {
  if ((e as NodeJS.ErrnoException).code === 'EXDEV') {
    await deps.fs.copyFile(task.diskPath, desiredFinalPath)
    await deps.fs.unlink(task.diskPath)
  } else {
    throw e
  }
}
Defensive patterns

Strategy: try-catch

Validate before calling

// Before finalize, confirm same-filesystem + writability
import path from 'node:path'
async function canRenameAtomic(fs, src, dst) {
  try {
    const [s, d] = await Promise.all([fs.stat(path.dirname(src)), fs.stat(path.dirname(dst))])
    if (s.dev !== d.dev) return { ok: false, reason: 'cross-device' }
    await fs.access(path.dirname(dst), fs.constants.W_OK)
    return { ok: true }
  } catch (e) { return { ok: false, reason: (e).message } }
}
const pre = await canRenameAtomic(deps.fs, task.diskPath, desiredFinalPath)
if (!pre.ok) { /* copy+delete fallback or surface clear error */ }

Type guard

function isAppError(e, code = ErrorCode.TaskFinalizeRenameFailed) { return e instanceof AppError && e.code === code }

Try / catch

try {
  await deps.fs.renameAtomic(task.diskPath, desiredFinalPath)
} catch (e) {
  const errno = (e).code
  if (errno === 'EXDEV') {
    await deps.fs.copyFile(task.diskPath, desiredFinalPath)
    await deps.fs.unlink(task.diskPath)
  } else if (errno === 'ENOSPC') {
    // surface disk-full to user, do not retry blindly
    throw e
  } else {
    throw e
  }
}

Prevention

When it happens

Trigger: Calling finalize on a task whose `diskPath` and `desiredFinalPath` live on different filesystems (renameAtomic cannot cross mount boundaries); destination already exists or directory lacks write permission; disk full during the rename; path exceeds OS length limits; the `.motrix` source was already moved/deleted by another process.

Common situations: User changed `defaultSaveDir` to a different volume after the download started; antivirus/another process holds or quarantines the file; NAS/SMB mount with rename restrictions; finalPath target on a read-only share.

Related errors


AI-assisted analysis of agalwood/Motrix@1a708ee577 (2026-08-12). Data as JSON: /api/errors/7dde711b6c9ff3bd. Report an issue: GitHub.