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 = 0View on GitHub (pinned to 1a708ee577)
Solutions
- Always close readers in a finally block so slots are returned even on error.
- Raise maxConcurrentReaders in the FsTask options when parallelism is intended and the OS handle limit allows it.
- Gate openReader() calls with a semaphore/p-limit sized to the configured cap.
- 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
- Always close readers in a finally block.
- Gate openReader() with a semaphore sized to the cap (default 3).
- Raise maxConcurrentReaders in opts when intentional parallelism needs it.
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
- plugin.fs.chunk_too_large
- plugin.fs.rename_target_exists
- aria2 returned gid ${actualGid} instead of reserved gid ${re
- TaskNotFound
- taskId must be a string
AI-assisted analysis of agalwood/Motrix@1a708ee577 (2026-08-12).
Data as JSON: /api/errors/d69296c26fd876f8.
Report an issue: GitHub.