agalwood/Motrix · error · AppError

TaskFinalizeMetaMissing

TaskFinalizeMetaMissing

Error message

Torrent metadata missing for task ${task.id}

What it means

Thrown by `readBtMetadata` in re-add-task.ts:75 during the re-seed/retry path when `task.torrentMetaPath` is falsy. Re-adding a torrent-like task into aria2 requires the original `.torrent` bytes; without the persisted sidecar path the re-add cannot reconstruct engine inputs.

Source

Thrown at src/core/task/actions/re-add-task.ts:75

    // not-found idempotence) is authoritative absence. forceRemove may have
    // raced a row that was already stopped, so its failure alone must not
    // retain a ghost owner.
    return true
  } catch (err) {
    log.error(
      { err: String(err), engineTaskId },
      'reAddTask: failed to remove replacement result after add failure'
    )
    return false
  }
}

async function readBtMetadata(
  task: DownloadTask,
  deps: ReAddTaskDeps
): Promise<Uint8Array> {
  if (!task.torrentMetaPath) {
    throw new AppError(
      ErrorCode.TaskFinalizeMetaMissing,
      `Torrent metadata missing for task ${task.id}`
    )
  }
  return deps.torrentMetaStore.read(task.torrentMetaPath)
}

/**
 * Where aria2 should write this re-add.
 *
 * A Completed reseed points at `finalPath`: finalize already renamed the
 * container away from `.motrix`, and that is where the seedable content
 * lives (`diskPath` has been normalized to the same value anyway).
 *
 * A retry of a failed or removed download is the opposite case — finalize
 * never ran, so the partial content is still in the in-flight `.motrix`
 * container that `createTaskHandler` passed as `dir` at add time. Pointing
 * `checkIntegrity` at `finalPath` there would scan an empty directory and

View on GitHub (pinned to 1a708ee577)

Solutions

  1. Block retry in the UI when `canRebuildTaskInputs(task)` / `canReseed(task)` return false — surface the task as non-retryable instead of throwing.
  2. If the original `.torrent` is recoverable elsewhere, persist it via `torrentMetaStore.persist(taskId, bytes)` then retry.
  3. For migrated rows missing torrentMetaPath, mark them non-retryable and re-create from the source link instead.
  4. Ensure all BT create paths persist torrentMetaPath before the task becomes retryable.

Example fix

// before
async function readBtMetadata(task, deps) {
  if (!task.torrentMetaPath) {
    throw new AppError(ErrorCode.TaskFinalizeMetaMissing, `Torrent metadata missing for task ${task.id}`)
  }
  return deps.torrentMetaStore.read(task.torrentMetaPath)
}

// after — caller guards before invoking
if (!canReseed(task)) {
  deps.log.warn({ taskId: task.id }, 'reAddTask: skipping reseed, no torrentMetaPath')
  return false
}
Defensive patterns

Strategy: validation

Validate before calling

// Guard at the re-add entry before calling readBtMetadata
import { canReseed } from '@core/task/shared'
if (!canReseed(task)) {
  deps.log.warn({ taskId: task.id }, 'reAddTask: skipping reseed, torrentMetaPath missing')
  return false
}

Type guard

function isReAddMetaMissing(e) { return e instanceof AppError && e.code === ErrorCode.TaskFinalizeMetaMissing && /Torrent metadata missing for task/.test(e.message) }

Try / catch

if (!task.torrentMetaPath) {
  // mark non-retryable instead of throwing
  deps.log.warn({ taskId: task.id }, 'reAddTask: no torrentMetaPath')
  return false
}

Prevention

When it happens

Trigger: User clicks retry on a stopped BT task that was created without persisted torrent bytes; a task imported via migration where torrentMetaPath was not backfilled; a re-add invoked on a task that originally only had an info-hash with no .torrent file.

Common situations: Tasks created in an older version before torrentMetaPath was mandatory; magnet tasks that never reached MetadataReady before being stopped; DB rows altered by maintenance scripts.

Related errors


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