siyuan-note/siyuan · error

Plugin ${pluginLabel} returned an invalid action

Error message

Plugin ${pluginLabel} returned an invalid action

What it means

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.

Source

Thrown at app/src/protyle/upload/pluginEvent.ts:439

            discardResponse();
            return fail(new Error(`Plugin ${pluginLabel} must use respondWith to replace or cancel an asset upload`));
        }
        if (responseError) {
            discardResponse();
            return fail(responseError);
        }
        if (emitResult.hasAsyncListener && !responseClaimed) {
            return fail(new Error(`Plugin ${pluginLabel} must call respondWith synchronously before awaiting`));
        }
        if (!response) {
            return processPlugin(index + 1);
        }
        return waitForDecision(response, task, pluginLabel, timeout).then(decision => {
            if (decision?.action === "cancel") {
                return cancel();
            }
            if (decision?.action !== "replace") {
                throw new Error(`Plugin ${pluginLabel} returned an invalid action`);
            }
            const validationError = validateAssetUploadInput(decision.input);
            if (validationError) {
                throw new Error(`Plugin ${pluginLabel} returned invalid input: ${validationError}`);
            }
            const targetValidationError = validateTargetInput(decision.input, context);
            if (targetValidationError) {
                throw new Error(`Plugin ${pluginLabel} returned invalid input: ${targetValidationError}`);
            }
            task.input = cloneInput(decision.input);
            return processPlugin(index + 1);
        }).catch(error => error instanceof AssetUploadCanceledError ? cancel(error.message) : fail(error));
    };
    return processPlugin(0);
};

View on GitHub (pinned to 8641553a1f)

Solutions

  1. Fix the plugin so its respondWith promise resolves to {action: 'replace', input} or {action: 'cancel'} exactly (lowercase, no extras)
  2. Update the plugin to the current petal/plugin API version — only 'replace' and 'cancel' actions are accepted
  3. If the plugin cannot respond correctly, remove/disable it so processPlugin skips to the next plugin (a non-responding plugin is skipped)
  4. In your integration, catch this error from prepareAssetUpload and surface which pluginLabel misbehaved for debugging

Example fix

// before (plugin side): invalid action
respondWith({ action: "skip", input });
// after (plugin side): only replace or cancel
respondWith({ action: "replace", input });
// or
respondWith({ action: "cancel" });
Defensive patterns

Strategy: type-guard

Validate before calling

// validate a plugin decision before passing it to respondWith
const isValidDecision = (d: any) =>
    d && (d.action === "cancel" ||
        (d.action === "replace" && d.input && typeof d.input === "object" && Array.isArray(d.input.files)));
if (!isValidDecision(decision)) { throw new Error("decision must use action replace or cancel"); }

Type guard

const isUploadDecision = (d: unknown): d is { action: "replace" | "cancel"; input?: unknown } =>
    typeof d === "object" && d !== null &&
    ((d as any).action === "replace" || (d as any).action === "cancel");

Try / catch

try {
    await prepareAssetUpload(task, context);
} catch (err) {
    if (err.message.includes("returned an invalid action")) {
        console.error("Misbehaving plugin:", err.message); // identify pluginLabel
    } else { throw err; }
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Understand the failure class

Background: Invalid enum value errors: "Unknown type", "Invalid scope", "must be one of" — when a string is not on the library's allowed list — this error's family across 23 libraries.

Related errors


AI-assisted analysis of siyuan-note/siyuan@8641553a1f (2026-09-11). Data as JSON: /api/errors/32836d20283c8de8. Report an issue: GitHub.