{"record":{"id":"c1f7cb02d29bf8d7","repo":"TryGhost/Ghost","slug":"preview-response-was-incomplete","errorCode":null,"errorMessage":"Preview response was incomplete","messagePattern":"Preview response was incomplete","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"apps/admin/src/automations/components/email-modal/use-email-preview.ts","lineNumber":104,"sourceCode":"        setErrors({});\n        setPreviewState({status: 'loading'});\n\n        try {\n            // Only the latest preview request is allowed to update preview state.\n            const response = await previewAutomationEmail({\n                id: automationId,\n                subject: draft.subject,\n                lexical: draft.lexical\n            });\n\n            if (previewRequestIdRef.current !== requestId) {\n                return;\n            }\n\n            const preview = response.automation_email_previews?.[0];\n\n            if (!preview?.html || !preview?.plaintext || !preview?.subject) {\n                throw new Error('Preview response was incomplete');\n            }\n\n            setPreviewState({\n                status: 'success',\n                preview: {\n                    ...preview,\n                    html: preparePreviewHtml(preview.html)\n                }\n            });\n        } catch (error) {\n            if (previewRequestIdRef.current !== requestId) {\n                return;\n            }\n\n            setPreviewState({\n                status: 'error',\n                message: getPreviewErrorMessage(error)\n            });","sourceCodeStart":86,"sourceCodeEnd":122,"githubUrl":"https://github.com/TryGhost/Ghost/blob/47d8b0e2ad2fd4757d3bc45f46c3ac165ff8a1fe/apps/admin/src/automations/components/email-modal/use-email-preview.ts#L86-L122","documentation":"Thrown by the automation email preview hook (useEmailPreview) after a successful call to previewAutomationEmail when the resolved response's first automation_email_previews entry is missing any of html, plaintext, or subject (undefined or empty string). It is a defensive contract check: the Ghost Admin API preview endpoint is expected to return all three rendered fields, and the hook refuses to render a partial preview rather than show a broken iframe.","triggerScenarios":"response.automation_email_previews is undefined, empty, or its [0] element has an empty/missing html, plaintext, or subject field. Concretely: the backend omits plaintext because the Lexical doc produced no text content; the API version returns a different shape; the preview render partially failed server-side but still 200-ed; or the automation_email_previews array came back empty because automationId didn't match an automation.","commonSituations":"Admin frontend version newer/older than the backend API (field renamed/removed); email rendering service (mailer) misconfigured or down so it returns no html; empty subject or empty Lexical draft reaching the backend; automation deleted between opening the modal and clicking preview; feature flag for automation emails partially enabled.","solutions":["Inspect the actual API response in DevTools (Network tab, the preview request) to see which of html/plaintext/subject is missing or empty.","If the backend legitimately returns empty plaintext for a text-less draft, relax the guard to require only html (the iframe payload) and treat missing plaintext/subject as non-fatal.","Verify the admin app and ghost/core backend are on compatible versions (the automation_email_previews serializer must emit all three fields).","Confirm the automationId passed to the hook still exists and the preview endpoint isn't silently returning an empty array for a missing automation.","Reproduce with a non-empty subject and a Lexical doc containing a paragraph node to rule out empty-input edge cases."],"exampleFix":"// before\nif (!preview?.html || !preview?.plaintext || !preview?.subject) {\n    throw new Error('Preview response was incomplete');\n}\n\n// after — require only html for the iframe; surface missing text fields as a warning, not a hard failure\nif (!preview?.html) {\n    throw new Error('Preview response did not contain rendered HTML');\n}","handlingStrategy":"fallback","validationCode":"// Before calling enterPreview, ensure the draft has real content so the backend can render all fields\nimport {getEmailValidationErrors} from './validation';\nconst errs = getEmailValidationErrors(draft);\nif (errs.lexical || !draft.subject?.trim()) {\n    // don't request a preview the backend can't fully render\n    return;\n}","typeGuard":"// Narrow on the hook's previewFrameState before rendering\nimport type {EmailPreviewFrameState} from './use-email-preview';\nfunction isErrorState(state: EmailPreviewFrameState): state is {status: 'error' | 'invalid'; message: string} {\n    return state.status === 'error' || state.status === 'invalid';\n}","tryCatchPattern":"// enterPreview already catches internally and sets status:'error'; consumers should treat any non-success state as a fallback UI branch\nconst {previewFrameState} = useEmailPreview({...});\nif (previewFrameState.status === 'success') {\n    return <iframe srcDoc={previewFrameState.html} />;\n}\nif (previewFrameState.status === 'error' || previewFrameState.status === 'invalid') {\n    return <PreviewError message={previewFrameState.message} />;\n}\nreturn <PreviewSkeleton />; // loading","preventionTips":["Keep the admin frontend and ghost/core backend on compatible versions so the automation_email_previews serializer returns html, plaintext, and subject.","Don't request a preview for an empty/invalid draft — validate subject and Lexical content first.","Treat the hook's error state as a first-class UI branch, not an edge case."],"tags":["preview","api","react","async","ghost"],"backgroundTag":null,"analyzedSha":"47d8b0e2ad2fd4757d3bc45f46c3ac165ff8a1fe","analyzedAt":"2026-08-13T01:25:26.651Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}