mihomo-party-org/clash-party · error · Error

Plugin path is not a file

Error message

Plugin path is not a file

What it means

After resolving the path, readPluginFile opens it and stats it; if stat.isFile() is false (a directory, FIFO, device, etc.) it throws 'Plugin path is not a file'. The loader needs a regular file to read bytes from, so anything else is rejected.

Source

Thrown at src/main/resolve/plugin/file.ts:18

import { open } from 'fs/promises'
import { basename, extname, resolve } from 'path'
import { MAX_PLUGIN_FILE_BYTES } from './constants'

export function findPluginFile(args: string[]): string | undefined {
  return args.find(
    (arg) => !arg.startsWith('-') && !arg.includes('://') && extname(arg).toLowerCase() === '.cpx'
  )
}

export async function readPluginFile(filePath: string): Promise<IPluginFilePayload> {
  if (extname(filePath).toLowerCase() !== '.cpx') throw new Error('Unsupported plugin file type')

  const resolvedPath = resolve(filePath)
  const handle = await open(resolvedPath, 'r')
  try {
    const stat = await handle.stat()
    if (!stat.isFile()) throw new Error('Plugin path is not a file')
    if (stat.size > MAX_PLUGIN_FILE_BYTES) throw new Error('Plugin file too large')

    // Read at most one byte beyond the limit so a file growing after stat cannot bypass the cap.
    const bytes = Buffer.alloc(MAX_PLUGIN_FILE_BYTES + 1)
    let length = 0
    while (length < bytes.length) {
      const { bytesRead } = await handle.read(bytes, length, bytes.length - length, null)
      if (bytesRead === 0) break
      length += bytesRead
    }
    if (length > MAX_PLUGIN_FILE_BYTES) throw new Error('Plugin file too large')

    return {
      name: basename(resolvedPath),
      fileBytesB64: bytes.subarray(0, length).toString('base64')
    }
  } finally {
    await handle.close()

View on GitHub (pinned to 911e090537)

Solutions

  1. Point the path at the .cpx file itself, not its containing or extracted directory.
  2. If a symlink is involved, confirm it resolves to a regular file (ls -l / stat).
  3. Re-download the plugin package if the local file was partially extracted or corrupted.

Example fix

// before
await readPluginFile('/plugins/my-plugin.cpx/') // directory
// after
await readPluginFile('/plugins/my-plugin.cpx') // regular file
Defensive patterns

Strategy: validation

Validate before calling

import { statSync } from 'node:fs'
const st = statSync(pluginPath)
if (!st.isFile()) {
  throw new Error(`Plugin path must be a regular file, got: ${pluginPath}`)
}

Type guard

function isRegularFile(p: string): boolean {
  try { return require('node:fs').statSync(p).isFile() } catch { return false }
}

Try / catch

try {
  const payload = await readPluginFile(pluginPath)
} catch (e) {
  if ((e as Error).message === 'Plugin path is not a file') {
    // tell the user to point at the .cpx file itself
  } else throw e
}

Prevention

When it happens

Trigger: Passing a directory path or a special file (socket/FIFO/device node) to readPluginFile via payload. Note the file may have the right .cpx extension but be a directory (e.g. 'my-plugin.cpx/').

Common situations: Configured plugin path points at an extracted folder instead of the package, a mount point, or a broken symlink resolving to a directory.

Related errors


AI-assisted analysis of mihomo-party-org/clash-party@911e090537 (2026-08-30). Data as JSON: /api/errors/4757d71ecc1bb140. Report an issue: GitHub.