agalwood/Motrix · error · FsTaskError

plugin.fs.too_many_readers

plugin.fs.too_many_readers

Error message

plugin.fs.too_many_readers: max ${this.maxConcurrentReaders} concurrent readers

What it means

Thrown by FsTask.openReader() when `activeReaders.size >= maxConcurrentReaders` (default 3 per DEFAULT_MAX_READERS). Each open reader holds a file handle and an idle timer; the cap prevents handle exhaustion. The check is synchronous and throws before allocating any state. Code is `plugin.fs.too_many_readers`.

Source

Thrown at src/core/plugin/capabilities/fs-task.ts:129

  // exists
  // -------------------------------------------------------------------------

  async exists(): Promise<boolean> {
    try {
      await fs.access(this._filePath)
      return true
    } catch {
      return false
    }
  }

  // -------------------------------------------------------------------------
  // openReader
  // -------------------------------------------------------------------------

  openReader(opts: { offset?: number; length?: number }): FsTaskReader {
    if (this.activeReaders.size >= this.maxConcurrentReaders) {
      throw new FsTaskError(
        'plugin.fs.too_many_readers',
        `plugin.fs.too_many_readers: max ${this.maxConcurrentReaders} concurrent readers`
      )
    }

    const offset = opts.offset ?? 0
    const maxLength = opts.length ?? Infinity

    const state: ReaderState = {
      handle: null as unknown as Awaited<ReturnType<typeof fs.open>>,
      position: offset,
      closed: false,
      idleTimer: null,
    }

    // Lazily opened — we open on first read to keep openReader() sync
    let openPromise: Promise<void> | null = null
    let bytesDelivered = 0

View on GitHub (pinned to 1a708ee577)

Solutions

  1. Always close readers in a finally block so slots are returned even on error.
  2. Raise maxConcurrentReaders in the FsTask options when parallelism is intended and the OS handle limit allows it.
  3. Gate openReader() calls with a semaphore/p-limit sized to the configured cap.
  4. Audit for leaked readers by checking activeReaders before opening a new one.

Example fix

// before
const readers = await Promise.all(
  ranges.map(r => task.openReader(r))
) // >3 ranges -> throws

// after — pool with a limiter + always close
const pool = pLimit(3)
await Promise.all(ranges.map(r => pool(async () => {
  const rd = task.openReader(r)
  try { /* read loop */ } finally { await rd.close() }
})))
Defensive patterns

Strategy: validation

Validate before calling

function openReaderBounded(task: FsTask, opts: { offset?: number; length?: number }, max: number) {
  // caller-side semaphore sized to the task's maxConcurrentReaders
  if (activeCount >= max) throw new Error('reader cap reached; queue instead')
  return task.openReader(opts)
}

Type guard

function isTooManyReaders(e: unknown): boolean {
  return e instanceof Error && (e as FsTaskError).code === 'plugin.fs.too_many_readers'
}

Try / catch

try {
  const rd = task.openReader(opts)
  try { /* read loop */ } finally { await rd.close() }
} catch (e) {
  if (isTooManyReaders(e)) { /* queue and retry after a reader closes */ }
  else throw e
}

Prevention

When it happens

Trigger: Opening more than maxConcurrentReaders readers on the same FsTask without closing prior ones; raising the count via fan-out parallelism without raising the cap; forgetting to call reader.close() so the slot never frees.

Common situations: Parallel chunked reads spawned by a Promise.all without a concurrency limiter; leaked readers from early-return/throw paths that skip close(); test fan-out exceeding the default 3.

Related errors


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