From 7f7f79aa3143f3a5228c3ba5dd3c1384f9aaee7a Mon Sep 17 00:00:00 2001 From: shrimbly Date: Thu, 19 Feb 2026 07:56:11 +1300 Subject: [PATCH] fix: harden prompt type coercion in nanoBanana executor and connectedInputs - Add defensive type coercion guard for promptText (handles non-string at runtime) - Sanitize dynamicInputs.prompt to string before including in request payload - Add final assertion guard before fetch to provide clear error if prompt is not a string - Ensure text values in connectedInputs are always strings via String() coercion --- src/store/execution/nanoBananaExecutor.ts | 25 ++++++++++++++++++++++- src/store/utils/connectedInputs.ts | 4 +++- 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/src/store/execution/nanoBananaExecutor.ts b/src/store/execution/nanoBananaExecutor.ts index 42fa944c..f6e79597 100644 --- a/src/store/execution/nanoBananaExecutor.ts +++ b/src/store/execution/nanoBananaExecutor.ts @@ -62,6 +62,13 @@ export async function executeNanoBanana( promptText = connectedText || promptFromDynamic || null; } + // Defensive: ensure promptText is actually a string at runtime + // (Guards against corrupted node data or race conditions in parallel execution) + if (promptText !== null && typeof promptText !== 'string') { + console.warn('[nanoBanana] promptText was not a string, coercing:', typeof promptText, promptText); + promptText = Array.isArray(promptText) ? (promptText as string[])[0] ?? null : String(promptText); + } + if (!promptText) { updateNodeData(node.id, { status: "error", @@ -80,6 +87,13 @@ export async function executeNanoBanana( const provider = nodeData.selectedModel?.provider || "gemini"; const headers = buildGenerateHeaders(provider, providerSettings); + // Sanitize dynamicInputs: coerce prompt to string if it's an array + // (Defensive guard for parallel execution race conditions) + const sanitizedDynamicInputs = { ...dynamicInputs }; + if (Array.isArray(sanitizedDynamicInputs.prompt)) { + sanitizedDynamicInputs.prompt = (sanitizedDynamicInputs.prompt as string[])[0] ?? ''; + } + const requestPayload = { images, prompt: promptText, @@ -89,9 +103,18 @@ export async function executeNanoBanana( useGoogleSearch: nodeData.useGoogleSearch, selectedModel: nodeData.selectedModel, parameters: nodeData.parameters, - dynamicInputs, + dynamicInputs: sanitizedDynamicInputs, }; + // Final guard: assert that prompt is a string before sending to API + // This catches any remaining edge cases and provides a clear error message + if (typeof requestPayload.prompt !== 'string') { + const errorMsg = `Internal error: prompt is ${typeof requestPayload.prompt}, expected string`; + console.error('[nanoBanana]', errorMsg); + updateNodeData(node.id, { status: 'error', error: errorMsg }); + throw new Error(errorMsg); + } + try { const response = await fetch("/api/generate", { method: "POST", diff --git a/src/store/utils/connectedInputs.ts b/src/store/utils/connectedInputs.ts index 575c866d..de99cca9 100644 --- a/src/store/utils/connectedInputs.ts +++ b/src/store/utils/connectedInputs.ts @@ -162,7 +162,9 @@ export function getConnectedInputsPure( } else if (type === "audio") { audio.push(value); } else if (type === "text" || isTextHandle(handleId)) { - text = value; + // Defensive: ensure text values are always strings + // (Guards against corrupted node data during parallel execution) + text = typeof value === 'string' ? value : String(value); } else if (isImageHandle(handleId) || !handleId) { images.push(value); }