{"record":{"id":"32836d20283c8de8","repo":"siyuan-note/siyuan","slug":"plugin-pluginlabel-returned-an-invalid-action","errorCode":null,"errorMessage":"Plugin ${pluginLabel} returned an invalid action","messagePattern":"Plugin (.+?) returned an invalid action","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"app/src/protyle/upload/pluginEvent.ts","lineNumber":439,"sourceCode":"            discardResponse();\n            return fail(new Error(`Plugin ${pluginLabel} must use respondWith to replace or cancel an asset upload`));\n        }\n        if (responseError) {\n            discardResponse();\n            return fail(responseError);\n        }\n        if (emitResult.hasAsyncListener && !responseClaimed) {\n            return fail(new Error(`Plugin ${pluginLabel} must call respondWith synchronously before awaiting`));\n        }\n        if (!response) {\n            return processPlugin(index + 1);\n        }\n        return waitForDecision(response, task, pluginLabel, timeout).then(decision => {\n            if (decision?.action === \"cancel\") {\n                return cancel();\n            }\n            if (decision?.action !== \"replace\") {\n                throw new Error(`Plugin ${pluginLabel} returned an invalid action`);\n            }\n            const validationError = validateAssetUploadInput(decision.input);\n            if (validationError) {\n                throw new Error(`Plugin ${pluginLabel} returned invalid input: ${validationError}`);\n            }\n            const targetValidationError = validateTargetInput(decision.input, context);\n            if (targetValidationError) {\n                throw new Error(`Plugin ${pluginLabel} returned invalid input: ${targetValidationError}`);\n            }\n            task.input = cloneInput(decision.input);\n            return processPlugin(index + 1);\n        }).catch(error => error instanceof AssetUploadCanceledError ? cancel(error.message) : fail(error));\n    };\n    return processPlugin(0);\n};\n","sourceCodeStart":421,"sourceCodeEnd":455,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/8641553a1f07374001902d3ce773285db1292b2d/app/src/protyle/upload/pluginEvent.ts#L421-L455","documentation":"During the before-upload-assets pipeline, processPlugin waits for a plugin's respondWith decision and only accepts action 'replace' or 'cancel'. If the resolved decision has any other (or missing) action, it throws Error(\"Plugin ${pluginLabel} returned an invalid action\"), aborting the upload chain for that plugin. This enforces the plugin API contract that a claimant must either replace or cancel the asset upload.","triggerScenarios":"A plugin's before-upload-assets listener calls respondWith with a promise resolving to {action: 'skip'}, an unknown string, or an object lacking action; or the decision resolves undefined/non-conforming after waitForDecision, and the plugin was the one that claimed the response.","commonSituations":"A plugin written against an older or imagined API version returning an action value not in the enum; a typo like 'Replace' or 'replaced'; a plugin resolving respondWith without setting action; two plugins interacting so a stale response object is consumed.","solutions":["Fix the plugin so its respondWith promise resolves to {action: 'replace', input} or {action: 'cancel'} exactly (lowercase, no extras)","Update the plugin to the current petal/plugin API version — only 'replace' and 'cancel' actions are accepted","If the plugin cannot respond correctly, remove/disable it so processPlugin skips to the next plugin (a non-responding plugin is skipped)","In your integration, catch this error from prepareAssetUpload and surface which pluginLabel misbehaved for debugging"],"exampleFix":"// before (plugin side): invalid action\nrespondWith({ action: \"skip\", input });\n// after (plugin side): only replace or cancel\nrespondWith({ action: \"replace\", input });\n// or\nrespondWith({ action: \"cancel\" });","handlingStrategy":"type-guard","validationCode":"// validate a plugin decision before passing it to respondWith\nconst isValidDecision = (d: any) =>\n    d && (d.action === \"cancel\" ||\n        (d.action === \"replace\" && d.input && typeof d.input === \"object\" && Array.isArray(d.input.files)));\nif (!isValidDecision(decision)) { throw new Error(\"decision must use action replace or cancel\"); }","typeGuard":"const isUploadDecision = (d: unknown): d is { action: \"replace\" | \"cancel\"; input?: unknown } =>\n    typeof d === \"object\" && d !== null &&\n    ((d as any).action === \"replace\" || (d as any).action === \"cancel\");","tryCatchPattern":"try {\n    await prepareAssetUpload(task, context);\n} catch (err) {\n    if (err.message.includes(\"returned an invalid action\")) {\n        console.error(\"Misbehaving plugin:\", err.message); // identify pluginLabel\n    } else { throw err; }\n}","preventionTips":["Only ever pass 'replace' or 'cancel' (exact lowercase strings) as decision.action in respondWith","Write plugin TypeScript against the current petal declarations so the action union is enforced at compile time","Unit-test your plugin's before-upload-assets handler to assert the resolved decision shape","Keep respondWith's promise resolving to a single, well-formed decision — never undefined or a partial object"],"tags":["plugin","upload","contract","validation"],"backgroundTag":"invalid-enum-value","analyzedSha":"8641553a1f07374001902d3ce773285db1292b2d","analyzedAt":"2026-09-11T16:08:28.414Z","contentChangedAt":"2026-09-11T16:08:28.414Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}