{"record":{"id":"4757d71ecc1bb140","repo":"mihomo-party-org/clash-party","slug":"plugin-path-is-not-a-file","errorCode":null,"errorMessage":"Plugin path is not a file","messagePattern":"Plugin path is not a file","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"src/main/resolve/plugin/file.ts","lineNumber":18,"sourceCode":"import { open } from 'fs/promises'\nimport { basename, extname, resolve } from 'path'\nimport { MAX_PLUGIN_FILE_BYTES } from './constants'\n\nexport function findPluginFile(args: string[]): string | undefined {\n  return args.find(\n    (arg) => !arg.startsWith('-') && !arg.includes('://') && extname(arg).toLowerCase() === '.cpx'\n  )\n}\n\nexport async function readPluginFile(filePath: string): Promise<IPluginFilePayload> {\n  if (extname(filePath).toLowerCase() !== '.cpx') throw new Error('Unsupported plugin file type')\n\n  const resolvedPath = resolve(filePath)\n  const handle = await open(resolvedPath, 'r')\n  try {\n    const stat = await handle.stat()\n    if (!stat.isFile()) throw new Error('Plugin path is not a file')\n    if (stat.size > MAX_PLUGIN_FILE_BYTES) throw new Error('Plugin file too large')\n\n    // Read at most one byte beyond the limit so a file growing after stat cannot bypass the cap.\n    const bytes = Buffer.alloc(MAX_PLUGIN_FILE_BYTES + 1)\n    let length = 0\n    while (length < bytes.length) {\n      const { bytesRead } = await handle.read(bytes, length, bytes.length - length, null)\n      if (bytesRead === 0) break\n      length += bytesRead\n    }\n    if (length > MAX_PLUGIN_FILE_BYTES) throw new Error('Plugin file too large')\n\n    return {\n      name: basename(resolvedPath),\n      fileBytesB64: bytes.subarray(0, length).toString('base64')\n    }\n  } finally {\n    await handle.close()","sourceCodeStart":1,"sourceCodeEnd":36,"githubUrl":"https://github.com/mihomo-party-org/clash-party/blob/911e090537acdf7c50bee1c3aebecc2ef119a8b5/src/main/resolve/plugin/file.ts#L1-L36","documentation":"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.","triggerScenarios":"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/').","commonSituations":"Configured plugin path points at an extracted folder instead of the package, a mount point, or a broken symlink resolving to a directory.","solutions":["Point the path at the .cpx file itself, not its containing or extracted directory.","If a symlink is involved, confirm it resolves to a regular file (ls -l / stat).","Re-download the plugin package if the local file was partially extracted or corrupted."],"exampleFix":"// before\nawait readPluginFile('/plugins/my-plugin.cpx/') // directory\n// after\nawait readPluginFile('/plugins/my-plugin.cpx') // regular file","handlingStrategy":"validation","validationCode":"import { statSync } from 'node:fs'\nconst st = statSync(pluginPath)\nif (!st.isFile()) {\n  throw new Error(`Plugin path must be a regular file, got: ${pluginPath}`)\n}","typeGuard":"function isRegularFile(p: string): boolean {\n  try { return require('node:fs').statSync(p).isFile() } catch { return false }\n}","tryCatchPattern":"try {\n  const payload = await readPluginFile(pluginPath)\n} catch (e) {\n  if ((e as Error).message === 'Plugin path is not a file') {\n    // tell the user to point at the .cpx file itself\n  } else throw e\n}","preventionTips":["Store the full path to the .cpx file in config, never a directory.","Check symlinks resolve to regular files (stat follows symlinks).","Wrap path setup in an isFile check during startup/config validation."],"tags":["plugin","filesystem","validation"],"backgroundTag":"path-is-not-a-file","analyzedSha":"911e090537acdf7c50bee1c3aebecc2ef119a8b5","analyzedAt":"2026-08-30T13:00:49.174Z","schemaVersion":2},"datasetVersion":"2026-08-30T13:17:10.514Z"}