Skip to content

Fix silent importer failures by surfacing validation errors#28546

Merged
9larsons merged 5 commits into
TryGhost:mainfrom
sawirricardo:fix-importer-silent-failures
Jun 13, 2026
Merged

Fix silent importer failures by surfacing validation errors#28546
9larsons merged 5 commits into
TryGhost:mainfrom
sawirricardo:fix-importer-silent-failures

Conversation

@sawirricardo

Copy link
Copy Markdown
Contributor

Summary

When importing content with invalid data (e.g. invalid email addresses, too-long author bios), errors were rejected as arrays instead of proper Error objects. This caused Node.js to emit only a generic warning:

(node:916) Warning: a promise was rejected with a non-error: [object Array]

No useful information was shown in the console or the error email — making it nearly impossible to diagnose import failures.

Root Cause

In data-importer.js, when validation errors occur, the error array is thrown directly (throw errors) instead of being wrapped in a proper Error object. Node.js considers this a "non-error" rejection and only emits a warning.

Changes

  • data-importer.js: Wrap the error array in a DataImportError with all individual error messages joined together. Attach the original error array as errorDetails for downstream use.
  • import-manager.js: Extract individual error details from the caught error (via errorDetails) instead of wrapping the whole thing as a single-element array. This preserves granularity for the email template.
  • email-template.ts: List each error message individually in the notification email instead of showing a generic "contact support" message. Also fixed typo "error occured" → "errors occurred".

Result

Before:

(node:916) Warning: a promise was rejected with a non-error: [object Array]

Email: "One or more error occured while importing your content. Please contact support."

After:

[DataImportError]: Content import was unsuccessful. Errors:
Validation (bio) failed: bio is too long.
Validation (email) failed: email is invalid.

Email: Lists each individual error with actionable guidance.

Testing

  • Manual testing with a JSON import file containing users with invalid emails and oversized bios
  • Verified error messages appear in both console logs and the completion email

Fixes #26268

@coderabbitai

coderabbitai Bot commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 9d05e9b9-8f5d-43d5-a5b4-8b7621548680

📥 Commits

Reviewing files that changed from the base of the PR and between 5f960ad and 140c97a.

📒 Files selected for processing (1)
  • ghost/core/test/integration/importer/v2.test.js

Walkthrough

This PR collects validation errors in the data importer and throws a DataImportError that includes the full errors array as errorDetails. The import manager now extracts err.errorDetails (or falls back to the raw error) when building importResult.data.errors. The unsuccessful-import email template was updated to escape and list up to five error messages and show an “and N more — check the server logs” note. Unit tests were added for the template and importer error propagation.

Possibly related PRs

  • TryGhost/Ghost#28238: TypeScript conversion of email-template.ts that shares edits around the unsuccessful-import template and its reliance on result?.data?.errors.
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main change: fixing silent importer failures by surfacing validation errors to users.
Description check ✅ Passed The description is comprehensive and directly related to the changeset, explaining the root cause, changes made, and expected outcomes.
Linked Issues check ✅ Passed The PR successfully addresses all objectives from issue #26268: wraps validation errors in proper Error objects, surfaces granular error messages in logs and emails, HTML-escapes content, and limits displayed errors with guidance to check logs.
Out of Scope Changes check ✅ Passed All changes are in-scope: error handling improvements in data-importer.js and import-manager.js, email template enhancements, and related test coverage directly support the linked issue objective.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@ghost/core/core/server/data/importer/email-template.ts`:
- Line 145: The HTML email template injects error text directly in the map
expression (result!.data!.errors!.map(...`${e?.message || e?.toString?.() ||
'Unknown error'}`...)), which is unsafe; add an HTML-escaping step and use it
when building the list items. Implement or import a small escapeHtml helper
(e.g. escapeHtml(s: string): string) and replace the interpolation with escaped
text (escapeHtml(e?.message || e?.toString?.() || 'Unknown error')) inside the
map so all user-controlled error strings are HTML-escaped before insertion.

In `@ghost/core/core/server/data/importer/import-manager.js`:
- Around line 532-533: Normalize err.errorDetails to an array before assigning
to importResult so result.data.errors is always an array; in import-manager.js
where importResult is set (importResult = {data: {errors: errorDetails}}),
replace the direct use of err.errorDetails with a normalized value such as: let
errorDetails = Array.isArray(err.errorDetails) ? err.errorDetails :
(err.errorDetails ? [err.errorDetails] : [err]); then set importResult = {data:
{errors: errorDetails}} to ensure templates using .map() won't crash.

In `@ghost/core/core/server/data/importer/importers/data/data-importer.js`:
- Around line 189-197: The local variable named errors is shadowing the imported
errors module, causing new errors.DataImportError(...) to fail; rename the local
let errors = [] (e.g., to collectedErrors or errorList) and update all
references in this file (including where errorMessages is built and where
importError.errorDetails is set) to use the new name, then instantiate the
actual DataImportError from the imported module (or destructured
DataImportError) instead of the local array so new DataImportError(...) succeeds
and importError contains the real error class.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 4122b631-2944-4a0c-955d-bdd8adaf418c

📥 Commits

Reviewing files that changed from the base of the PR and between cb39915 and 17de0eb.

📒 Files selected for processing (3)
  • ghost/core/core/server/data/importer/email-template.ts
  • ghost/core/core/server/data/importer/import-manager.js
  • ghost/core/core/server/data/importer/importers/data/data-importer.js

Comment thread ghost/core/core/server/data/importer/email-template.ts Outdated
Comment on lines +532 to +533
const errorDetails = err.errorDetails || [err];
importResult = {data: {errors: errorDetails}};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Normalize errorDetails to an array before storing in importResult.

err.errorDetails is assumed to be array-like here, but other Ghost code paths attach non-array payloads to errorDetails. If that happens, result.data.errors is not an array and the email template’s .map(...) path can throw.

Suggested fix
-            const errorDetails = err.errorDetails || [err];
+            const errorDetails = Array.isArray(err?.errorDetails)
+                ? err.errorDetails
+                : [err?.errorDetails ?? err];
             importResult = {data: {errors: errorDetails}};
Based on provided context snippet `ghost/core/core/server/models/base/plugins/bulk-operations.js:13-31`, where `err.errorDetails` is assigned a singular object payload.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ghost/core/core/server/data/importer/import-manager.js` around lines 532 -
533, Normalize err.errorDetails to an array before assigning to importResult so
result.data.errors is always an array; in import-manager.js where importResult
is set (importResult = {data: {errors: errorDetails}}), replace the direct use
of err.errorDetails with a normalized value such as: let errorDetails =
Array.isArray(err.errorDetails) ? err.errorDetails : (err.errorDetails ?
[err.errorDetails] : [err]); then set importResult = {data: {errors:
errorDetails}} to ensure templates using .map() won't crash.

Comment thread ghost/core/core/server/data/importer/importers/data/data-importer.js Outdated
sawirricardo and others added 2 commits June 12, 2026 12:59
When importing content with invalid data (e.g. invalid email addresses,
too-long bios), errors were rejected as arrays instead of proper Error
objects. This caused Node.js to emit only a generic warning:
"a promise was rejected with a non-error: [object Array]"

Changes:
- Wrap error arrays in DataImportError with individual error messages
  so logging shows useful information instead of [object Array]
- Preserve individual error details for the completion email
- Update email template to list each error message individually
  instead of showing a generic "contact support" message
- Fix typo: "error occured" → "errors occurred"

Fixes TryGhost#26268
ref TryGhost#26268

- the new DataImportError was constructed via `errors.DataImportError`,
  but `errors` at that point is the local validation-error array, so the
  fix threw a TypeError on the exact path it was meant to handle — import
  DataImportError from @tryghost/errors and build it from the first
  validation error (message/context/help, err) with the full array
  preserved as errorDetails for the completion email
- error messages were injected into the failure email HTML unescaped, so
  imported content could smuggle markup into the email — escape every
  message, and fall back to a generic label when an error has no message
- cap the listed errors at 5 with an "and N more" pointer to the server
  logs so a large import can't generate an unbounded email
- added unit tests for the doImport rejection shape (DataImportError with
  message/context and deep-equal errorDetails) and the email template
  (success path, multiple errors, the cap, XSS escaping, message-less
  fallback)
@9larsons 9larsons force-pushed the fix-importer-silent-failures branch from 17de0eb to 5f960ad Compare June 12, 2026 18:04
@nx-cloud

nx-cloud Bot commented Jun 12, 2026

Copy link
Copy Markdown

🤖 Nx Cloud AI Fix

Ensure the fix-ci command is configured to always run in your CI pipeline to get automatic fixes in future runs. For more information, please see https://nx.dev/ci/features/self-healing-ci


View your CI Pipeline Execution ↗ for commit 140c97a

Command Status Duration Result
nx run ghost:test:ci:integration:no-coverage ✅ Succeeded 1m 49s View ↗
nx run ghost:test:ci:integration ✅ Succeeded 1m 8s View ↗
nx run ghost:test:ci:e2e:no-coverage ✅ Succeeded 3m 53s View ↗
nx run ghost:test:ci:e2e ✅ Succeeded 3m 48s View ↗
nx run ghost:test:ci:legacy ✅ Succeeded 2m 24s View ↗
nx build @tryghost/signup-form ✅ Succeeded <1s View ↗
nx build @tryghost/sodo-search ✅ Succeeded <1s View ↗
nx build @tryghost/activitypub ✅ Succeeded 1s View ↗
Additional runs (10) ✅ Succeeded ... View ↗

💡 Verify your cache is correct by running tasks in a sandbox. Read docs ↗


☁️ Nx Cloud last updated this comment at 2026-06-13 17:57:03 UTC

@9larsons

Copy link
Copy Markdown
Contributor

Thanks for the PR! You were first to the right fix for this, and I wanted to get it landed quickly, so I've pushed a commit on top of yours rather than going through review rounds. What the fixup does:

  • imports DataImportError from @tryghost/errors (as written, errors.DataImportError resolved against the local error array and would have thrown at runtime)
  • HTML-escapes the error messages in the email and caps the list at 5 with a "check the logs" suffix, and adds unit tests for the rejection shape and the email rendering (preserves what you had)

@9larsons 9larsons enabled auto-merge (squash) June 13, 2026 16:16
ref TryGhost#26268

The importer now rejects validation failures with DataImportError so the integration test needs to assert the errorDetails payload instead of the old raw array rejection.
@9larsons 9larsons merged commit a7f6f9e into TryGhost:main Jun 13, 2026
46 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Importer failures are silent

2 participants