agalwood/Motrix · error · Error

aria2 returned gid ${actualGid} instead of reserved gid ${re

Error message

aria2 returned gid ${actualGid} instead of reserved gid ${requestedGid}

What it means

Thrown as a plain Error by Aria2Adapter.addTorrent after the RPC call aria2.addTorrent returns a gid that does not match the requestedGid the adapter sent. The adapter passes opts.gid = requestedGid to the engine; if aria2 returns a different gid (case-insensitive comparison), it means the requested GID was already in use or otherwise rejected by aria2 internally. This is a consistency assertion, not a pre-validation.

Source

Thrown at src/core/engine/aria2/aria2-adapter.ts:445

    }
    if (params.ulLimit !== undefined) {
      opts['max-upload-limit'] = `${params.ulLimit}K`
    }
    if (params.extraEngineOptions) {
      for (const [k, v] of Object.entries(params.extraEngineOptions)) {
        opts[k] = v as string
      }
    }
    if (requestedGid !== undefined) {
      opts.gid = requestedGid
    }
    const b64 = Buffer.from(params.metadata).toString('base64')
    const actualGid = await this.rpc.addTorrent(b64, [], opts)
    if (
      requestedGid !== undefined &&
      actualGid.toLowerCase() !== requestedGid.toLowerCase()
    ) {
      throw new Error(
        `aria2 returned gid ${actualGid} instead of reserved gid ${requestedGid}`
      )
    }
    return params.gid ?? actualGid
  }

  /**
   * Whether a "GID is not found" reply from this engine can be trusted as
   * durably absent. On a sqlite3-persistence engine that predates
   * 1.37.0-motrix.3, that wording also covers evicted-but-persisted gids and
   * FAILED persistent deletes — so treating it as removed would let callers
   * erase local records while the durable engine row survives. Without
   * persistence there is no durable row a not-found could be hiding, so it is
   * always safe. Shared by the single and batch remove paths.
   */
  private trustsNotFound(): boolean {
    return (
      !this.featureReport.hasSqlitePersistence ||

View on GitHub (pinned to 1a708ee577)

Solutions

  1. Do not assume a specific GID is available — if you need a reserved GID, generate a fresh random one each time and handle the mismatch by retrying with a new GID
  2. Ensure prior tasks using that GID are fully removed (adapter.removeTask + removeDownloadResult) before re-reserving it
  3. Let aria2 auto-assign the GID (omit params.gid) and use the returned actualGid for all subsequent operations
  4. Catch this Error and fall back to using actualGid from the return value if the reservation is not strictly required
Defensive patterns

Strategy: try-catch

Try / catch

try {
  return await adapter.addTorrent(params)
} catch (e) {
  if (e instanceof Error && /aria2 returned gid .* instead of reserved gid/.test(e.message)) {
    // The requested GID was taken — generate a fresh one and retry
    params.gid = crypto.randomBytes(8).toString('hex')
    return await adapter.addTorrent(params)
  }
  throw e
}

Prevention

When it happens

Trigger: addTorrent sends a specific gid in opts; aria2.addTorrent succeeds but returns an actualGid that differs (case-insensitive) from the reserved requestedGid. The most common cause is the gid was already assigned to an existing (non-removed) download in aria2's session, so aria2 silently allocated a new one.

Common situations: Caller tries to reserve a GID that is still active in aria2 from a previous task that wasn't cleaned up; a stale session file causes aria2 to retain the GID; concurrent calls race to claim the same GID; the GID was evicted from aria2's in-memory window but persists in sqlite3 history on the Motrix fork.

Related errors


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