{"record":{"id":"8afc4136c6075dd9","repo":"mihomo-party-org/clash-party","slug":"unsupported-plugin-file-type","errorCode":null,"errorMessage":"Unsupported plugin file type","messagePattern":"Unsupported plugin file type","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"src/main/resolve/plugin/file.ts","lineNumber":12,"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","sourceCodeStart":1,"sourceCodeEnd":30,"githubUrl":"https://github.com/mihomo-party-org/clash-party/blob/911e090537acdf7c50bee1c3aebecc2ef119a8b5/src/main/resolve/plugin/file.ts#L1-L30","documentation":"readPluginFile() only accepts plugin packages with the .cpx extension. Before touching the filesystem it checks extname(filePath) and throws immediately when it is anything else, because the plugin loader cannot safely parse or trust a payload from an unrecognized container format.","triggerScenarios":"Calling readPluginFile (or the payload helper that wraps it) with a path whose extension is not .cpx — e.g. a .zip, .js, or extension-less path. The check is case-insensitive, so '.CPX' is fine.","commonSituations":"Users pointing the plugin resolver at a raw zip archive downloaded manually, a plugin directory instead of the packaged file, or an older plugin format that predates the .cpx packaging standard.","solutions":["Rename/repackage the plugin so the file has a .cpx extension (use the official packaging command).","Verify the path points to the plugin package file itself, not a folder or README.","Check the plugin source — you may have downloaded the wrong asset (source tarball vs .cpx package)."],"exampleFix":"// before\nawait readPluginFile('/downloads/my-plugin.zip')\n// after\nawait readPluginFile('/downloads/my-plugin.cpx')","handlingStrategy":"validation","validationCode":"import { extname } from 'node:path'\nif (extname(pluginPath).toLowerCase() !== '.cpx') {\n  throw new Error(`Expected a .cpx plugin package, got: ${pluginPath}`)\n}","typeGuard":"const isCpxPath = (p: string): boolean => extname(p).toLowerCase() === '.cpx'","tryCatchPattern":"try {\n  const payload = await readPluginFile(pluginPath)\n} catch (e) {\n  if ((e as Error).message === 'Unsupported plugin file type') {\n    // surface a friendly message about the .cpx requirement\n  } else throw e\n}","preventionTips":["Always distribute plugins as .cpx packages and reference the package file, not extracted folders.","Validate the extension at config-load time, before attempting to load.","Case-insensitive compare extensions; '.CPX' is accepted by the loader."],"tags":["plugin","validation","filesystem"],"backgroundTag":"unsupported-file-extension","analyzedSha":"911e090537acdf7c50bee1c3aebecc2ef119a8b5","analyzedAt":"2026-08-30T13:00:49.174Z","schemaVersion":2},"datasetVersion":"2026-08-30T13:17:10.514Z"}