TryGhost/Ghost · error

Preview response was incomplete

Error message

Preview response was incomplete

What it means

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.

Source

Thrown at apps/admin/src/automations/components/email-modal/use-email-preview.ts:104

        setErrors({});
        setPreviewState({status: 'loading'});

        try {
            // Only the latest preview request is allowed to update preview state.
            const response = await previewAutomationEmail({
                id: automationId,
                subject: draft.subject,
                lexical: draft.lexical
            });

            if (previewRequestIdRef.current !== requestId) {
                return;
            }

            const preview = response.automation_email_previews?.[0];

            if (!preview?.html || !preview?.plaintext || !preview?.subject) {
                throw new Error('Preview response was incomplete');
            }

            setPreviewState({
                status: 'success',
                preview: {
                    ...preview,
                    html: preparePreviewHtml(preview.html)
                }
            });
        } catch (error) {
            if (previewRequestIdRef.current !== requestId) {
                return;
            }

            setPreviewState({
                status: 'error',
                message: getPreviewErrorMessage(error)
            });

View on GitHub (pinned to 47d8b0e2ad)

Solutions

  1. Inspect the actual API response in DevTools (Network tab, the preview request) to see which of html/plaintext/subject is missing or empty.
  2. 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.
  3. Verify the admin app and ghost/core backend are on compatible versions (the automation_email_previews serializer must emit all three fields).
  4. Confirm the automationId passed to the hook still exists and the preview endpoint isn't silently returning an empty array for a missing automation.
  5. Reproduce with a non-empty subject and a Lexical doc containing a paragraph node to rule out empty-input edge cases.

Example fix

// before
if (!preview?.html || !preview?.plaintext || !preview?.subject) {
    throw new Error('Preview response was incomplete');
}

// after — require only html for the iframe; surface missing text fields as a warning, not a hard failure
if (!preview?.html) {
    throw new Error('Preview response did not contain rendered HTML');
}
Defensive patterns

Strategy: fallback

Validate before calling

// Before calling enterPreview, ensure the draft has real content so the backend can render all fields
import {getEmailValidationErrors} from './validation';
const errs = getEmailValidationErrors(draft);
if (errs.lexical || !draft.subject?.trim()) {
    // don't request a preview the backend can't fully render
    return;
}

Type guard

// Narrow on the hook's previewFrameState before rendering
import type {EmailPreviewFrameState} from './use-email-preview';
function isErrorState(state: EmailPreviewFrameState): state is {status: 'error' | 'invalid'; message: string} {
    return state.status === 'error' || state.status === 'invalid';
}

Try / catch

// enterPreview already catches internally and sets status:'error'; consumers should treat any non-success state as a fallback UI branch
const {previewFrameState} = useEmailPreview({...});
if (previewFrameState.status === 'success') {
    return <iframe srcDoc={previewFrameState.html} />;
}
if (previewFrameState.status === 'error' || previewFrameState.status === 'invalid') {
    return <PreviewError message={previewFrameState.message} />;
}
return <PreviewSkeleton />; // loading

Prevention

When it happens

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

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

Related errors


AI-assisted analysis of TryGhost/Ghost@47d8b0e2ad (2026-08-13). Data as JSON: /api/errors/c1f7cb02d29bf8d7. Report an issue: GitHub.