siyuan-note/siyuan · error

Plugin ${pluginLabel} returned invalid input: ${validationEr

Error message

Plugin ${pluginLabel} returned invalid input: ${validationError}

What it means

After a plugin's decision passes the action check, processPlugin validates decision.input twice: validateAssetUploadInput checks the generic shape (object with a files array, kind 'files' with File entries, or kind 'local-files' with objects containing a non-empty string path, optional numeric size, optional boolean isDir), and validateTargetInput checks target-specific fields. If either returns a message, it throws Error("Plugin ${pluginLabel} returned invalid input: ${validationError}"), so the exact validator message is embedded in the error.

Source

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

            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. Read the embedded validationError in the message and shape the plugin's decision.input to match: {kind: 'files', files: [File,...]} or {kind: 'local-files', files: [{path: string, size?: number|null, isDir?: boolean},...]}
  2. For 'files' kind, pass real File objects (check file instanceof File), not Blobs, strings, or plain objects
  3. For 'local-files' kind, ensure every entry has a non-empty string path and size is either null or a non-negative finite number, isDir a boolean if present
  4. If the target validation fails, fix the target fields the plugin replaced (validateTargetInput checks destination-specific constraints) or keep the original target instead of overriding it
  5. Catch the error around prepareAssetUpload and log pluginLabel plus the message to identify and fix the offending plugin

Example fix

// before (plugin side): invalid replace input
respondWith({ action: "replace", input: { files: [path] } });
// after (plugin side): well-formed input
respondWith({
    action: "replace",
    input: { kind: "local-files", files: [{ path, size: file.size }] }
});
Defensive patterns

Strategy: validation

Validate before calling

// mirror kernel-side validateAssetUploadInput before responding
const isValidInput = (input: any): boolean => {
    if (!input || typeof input !== "object" || !Array.isArray(input.files)) return false;
    if (input.kind === "files") {
        return input.files.every((f: unknown) => f instanceof File);
    }
    if (input.kind === "local-files") {
        return input.files.every((f: any) => f && typeof f.path === "string" && f.path.length > 0 &&
            (f.size === null || (typeof f.size === "number" && Number.isFinite(f.size) && f.size >= 0)) &&
            (f.isDir === undefined || typeof f.isDir === "boolean"));
    }
    return false;
};

Type guard

const isAssetUploadInput = (v: unknown): v is IAssetUploadInput =>
    typeof v === "object" && v !== null && Array.isArray((v as any).files) &&
    ((v as any).kind === "files" || (v as any).kind === "local-files");

Try / catch

try {
    await prepareAssetUpload(task, context);
} catch (err) {
    const m = err.message.match(/returned invalid input: (.+)$/);
    if (m) {
        console.error(`Plugin input rejected: ${m[1]}`); // fix decision.input accordingly
    } else { throw err; }
}

Prevention

When it happens

Trigger: A plugin responds with {action: 'replace', input} where input is missing/not an object, input.files is not an array, kind is neither 'files' nor 'local-files', a 'files' entry is not a File, or a 'local-files' entry lacks a non-empty string path / has a bad size or isDir; or the replacement target input fails validateTargetInput.

Common situations: A plugin substituting plain path strings where File objects are required for kind 'files'; empty path values; null or negative size fields; a plugin updated to a new input schema while the kernel still validates the old one; passing undefined input with action 'replace'.

Understand the failure class

Background: Schema validation failed / invalid input schema: payload rejected because its shape doesn't match the expected schema — this error's family across 28 libraries.

Related errors


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