Skip to content

Fix forms submit response parsing - #1395

Merged
ascorbic merged 5 commits into
emdash-cms:mainfrom
jcheese1:codex/forms-client-unwrapped-api
Jun 12, 2026
Merged

Fix forms submit response parsing#1395
ascorbic merged 5 commits into
emdash-cms:mainfrom
jcheese1:codex/forms-client-unwrapped-api

Conversation

@jcheese1

@jcheese1 jcheese1 commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

What does this PR do?

Fixes enhanced @emdash-cms/plugin-forms submissions so the browser client unwraps the standard EmDash plugin API { data: ... } response envelope before checking success, message, redirect, or field errors.

Without this, successful form submissions can return HTTP 200 with data.success: true while the rendered form still shows Something went wrong. Please try again. because the client only checked top-level success.

Closes #

Type of change

  • Bug fix
  • Feature (requires maintainer-approved Discussion)
  • Refactor (no behavior change)
  • Translation
  • Documentation
  • Performance improvement
  • Tests
  • Chore (dependencies, CI, tooling)

Checklist

  • I have read CONTRIBUTING.md
  • pnpm typecheck passes
  • pnpm lint passes
  • pnpm test passes (or targeted tests for my change)
  • pnpm format has been run
  • I have added/updated tests for my changes (if applicable)
  • User-visible strings in the admin UI are wrapped for translation (if applicable). Do not include messages.po changes except in translation PRs — a workflow extracts catalogs on merge to main.
  • I have added a changeset (if this PR changes a published package)
  • New features link to an approved Discussion: https://github.com/emdash-cms/emdash/discussions/...

AI-generated code disclosure

  • This PR includes AI-generated code — model/tool: GPT-5 / Codex

Screenshots / test output

$ corepack pnpm --filter @emdash-cms/plugin-forms test

Test Files  2 passed (2)
Tests  18 passed (18)

corepack pnpm --filter @emdash-cms/plugin-forms typecheck currently fails on existing upstream type errors in src/index.ts and src/public-definition.ts, unrelated to this change.

@changeset-bot

changeset-bot Bot commented Jun 9, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 7b5a3bc

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 3 packages
Name Type
@emdash-cms/plugin-forms Patch
@emdash-cms/perf-demo-site Patch
@emdash-cms/cache-demo-site Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@pkg-pr-new

pkg-pr-new Bot commented Jun 9, 2026

Copy link
Copy Markdown

Open in StackBlitz

@emdash-cms/admin

npm i https://pkg.pr.new/@emdash-cms/admin@1395

@emdash-cms/auth

npm i https://pkg.pr.new/@emdash-cms/auth@1395

@emdash-cms/auth-atproto

npm i https://pkg.pr.new/@emdash-cms/auth-atproto@1395

@emdash-cms/blocks

npm i https://pkg.pr.new/@emdash-cms/blocks@1395

@emdash-cms/cloudflare

npm i https://pkg.pr.new/@emdash-cms/cloudflare@1395

@emdash-cms/contentful-to-portable-text

npm i https://pkg.pr.new/@emdash-cms/contentful-to-portable-text@1395

emdash

npm i https://pkg.pr.new/emdash@1395

create-emdash

npm i https://pkg.pr.new/create-emdash@1395

@emdash-cms/gutenberg-to-portable-text

npm i https://pkg.pr.new/@emdash-cms/gutenberg-to-portable-text@1395

@emdash-cms/plugin-cli

npm i https://pkg.pr.new/@emdash-cms/plugin-cli@1395

@emdash-cms/plugin-types

npm i https://pkg.pr.new/@emdash-cms/plugin-types@1395

@emdash-cms/registry-client

npm i https://pkg.pr.new/@emdash-cms/registry-client@1395

@emdash-cms/registry-lexicons

npm i https://pkg.pr.new/@emdash-cms/registry-lexicons@1395

@emdash-cms/sandbox-workerd

npm i https://pkg.pr.new/@emdash-cms/sandbox-workerd@1395

@emdash-cms/x402

npm i https://pkg.pr.new/@emdash-cms/x402@1395

@emdash-cms/plugin-ai-moderation

npm i https://pkg.pr.new/@emdash-cms/plugin-ai-moderation@1395

@emdash-cms/plugin-atproto

npm i https://pkg.pr.new/@emdash-cms/plugin-atproto@1395

@emdash-cms/plugin-audit-log

npm i https://pkg.pr.new/@emdash-cms/plugin-audit-log@1395

@emdash-cms/plugin-color

npm i https://pkg.pr.new/@emdash-cms/plugin-color@1395

@emdash-cms/plugin-embeds

npm i https://pkg.pr.new/@emdash-cms/plugin-embeds@1395

@emdash-cms/plugin-field-kit

npm i https://pkg.pr.new/@emdash-cms/plugin-field-kit@1395

@emdash-cms/plugin-forms

npm i https://pkg.pr.new/@emdash-cms/plugin-forms@1395

@emdash-cms/plugin-webhook-notifier

npm i https://pkg.pr.new/@emdash-cms/plugin-webhook-notifier@1395

commit: 7b5a3bc

@jcheese1
jcheese1 marked this pull request as ready for review June 10, 2026 00:35
@github-actions github-actions Bot added the review/needs-review No maintainer or bot review yet label Jun 10, 2026

@emdashbot emdashbot 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.

The sandbox disconnected during review and I cannot complete the investigation. I was in the middle of tracing the plugin API response envelope to verify whether server-side validation errors are also broken (they appear to return { error: { details: { errors } } } which the client still won't surface). I need a working sandbox to finish the review.

@github-actions github-actions Bot added review/awaiting-author Reviewed; waiting on the author to respond and removed review/needs-review No maintainer or bot review yet labels Jun 10, 2026
@ascorbic ascorbic added the bot:review Trigger an emdashbot code review on this PR label Jun 12, 2026

@emdashbot emdashbot 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.

The approach is sound: the plugin API route framework wraps handler return values in { data: ... }, so the browser client needs to unwrap that envelope. The PR correctly fixes successful form submissions.

However, the change is incomplete with respect to field-level validation errors. The PR tests and description mention handling errors, but:

  1. The server never returns validation errors inside data.errors. When submitHandler encounters invalid data, it throws PluginRouteError.badRequest("Validation failed", { errors: result.errors }). The core plugin route handler wraps this in the error envelope ({ error: { code: "BAD_REQUEST", message: "Validation failed", details: { errors: [...] } } }).

  2. The client doesn't check res.ok or read the error envelope. handleSubmit calls parseSubmitResponse(await res.json()) unconditionally. For a 400 response, parseSubmitResponse returns the raw { error: ... } object. The client then finds result.success and result.errors both undefined and falls through to the generic "Something went wrong. Please try again." message.

  3. The core Astro route drops details on the floor. packages/core/src/astro/routes/api/plugins/[pluginId]/[...path].ts calls apiError(code, message, status) without passing result.error.details, so even if the client were fixed, the field errors would never reach it.

  4. The test gives false confidence. It asserts that parseSubmitResponse unwraps data.errors, but the server never produces that shape for validation failures.

Recommended fix (within PR scope): change submitHandler in packages/plugins/forms/src/handlers/submit.ts to return validation failures as data instead of throwing:

if (!result.valid) {
	return { success: false, errors: result.errors };
}

This keeps the response in the data envelope ({ data: { success: false, errors: [...] } }), so the existing client code — including the new parseSubmitResponse — handles it correctly without touching core.

redirect?: string;
errors?: Array<{ field: string; message: string }>;
};
const result = parseSubmitResponse(await res.json());

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.

[needs fixing] parseSubmitResponse correctly unwraps the { data } success envelope, but the caller never checks res.ok before treating the response as success data. When submitHandler encounters validation errors, it throws PluginRouteError.badRequest("Validation failed", { errors: result.errors }), which the core route handler wraps in { error: { code: "BAD_REQUEST", message: "Validation failed", details: { errors: [...] } } }. The client parses that error object, finds no success or errors at top level, and falls through to the generic "Something went wrong. Please try again." message. This means field-level validation errors are still completely broken for end users.

The codebase already follows a response.ok check pattern everywhere else (e.g. parsePublicFormDefinitionResponse, parseApiResponse, and the forms admin UI). The client should do the same.

Suggested change
const result = parseSubmitResponse(await res.json());
const body = (await res.json()) as unknown;
if (!res.ok) {
const error = isRecord(body) && isRecord(body.error) ? body.error : null;
const errors = Array.isArray(error?.details?.errors) ? error.details.errors : undefined;
if (errors) {
showErrors(form, errors);
} else {
showStatus(form, error?.message || "Something went wrong. Please try again.", "error");
}
} else {
const result = parseSubmitResponse(body);
if (result.success) {
clearSavedState(form);
if (result.redirect) {
// prevent xss
if (isSafeRedirectUrl(result.redirect)) {
window.location.href = result.redirect;
} else {
showStatus(form, result.message || "Submitted successfully.", "success");
form.reset();
}
} else {
showStatus(form, result.message || "Submitted successfully.", "success");
form.reset();
}
} else if (result.errors) {
showErrors(form, result.errors);
} else {
showStatus(form, "Something went wrong. Please try again.", "error");
}
}

(Note: this client-side fix alone won't fully resolve the issue because the core Astro route packages/core/src/astro/routes/api/plugins/[pluginId]/[...path].ts currently drops details when calling apiError. The cleanest in-package fix is to change submitHandler to return { success: false, errors: result.errors } instead of throwing, so the response stays in the data envelope and the existing client path handles it.)

});
});

it("unwraps field errors from the standard API envelope", () => {

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.

[needs fixing] This test gives false confidence. It asserts that parseSubmitResponse unwraps data.errors, but the server never returns field-level validation errors inside the data envelope. The forms plugin's submitHandler throws PluginRouteError.badRequest("Validation failed", { errors: result.errors }) on validation failure, which the core plugin route handler wraps in the error envelope ({ error: { details: { errors } } }).

Since the client doesn't read the error envelope and the core Astro route currently drops details when calling apiError, this test documents behavior that doesn't match production. Replace it with a test for the actual error-envelope path (once res.ok handling is added), or remove it once the error path is properly handled.

@github-actions github-actions Bot added review/needs-rereview Author pushed changes since the last review and removed review/awaiting-author Reviewed; waiting on the author to respond labels Jun 12, 2026
@ascorbic ascorbic added bot:review Trigger an emdashbot code review on this PR and removed bot:review Trigger an emdashbot code review on this PR labels Jun 12, 2026

@emdashbot emdashbot 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.

This PR correctly fixes the enhanced @emdash-cms/plugin-forms client so it unwraps the standard EmDash plugin API { data: ... } response envelope before inspecting success, message, redirect, and errors. The approach is sound and the implementation is clean.

What was fixed since the prior review

The previous review identified that validation errors went through the error envelope path (PluginRouteError.badRequest), which meant the new parseSubmitResponse helper would never see them. The author followed the recommendation and changed submitHandler to return { success: false, errors: result.errors } as data. This keeps validation failures inside the { data: ... } success envelope, so the client now correctly displays field-level errors instead of falling through to the generic "Something went wrong" message. The test description was also updated to match the actual server behavior.

What I checked

  • parseSubmitResponse correctly unwraps { data: { success, message, redirect, errors } } and preserves legacy top-level responses.
  • isRecord safely narrows the envelope without tripping on null, arrays, or primitives.
  • submitHandler now returns validation failures as data, aligning the server shape with what the client expects.
  • The test suite covers envelope unwrapping, legacy compatibility, validation errors, and guards against recursive unwrapping.
  • No AGENTS.md conventions are violated: there are no SQL changes, no new admin UI strings, and the changeset is present and accurate.

What remains (pre-existing, not a regression)

The client still does not check res.ok before parsing. Non-validation errors (404 form not found, 403 spam verification failed, 500 internal error) return the { error: ... } envelope, which parseSubmitResponse passes through unchanged. Because result.success and result.errors are then both undefined, the user sees the generic fallback message rather than the specific server error. This behavior is unchanged from before the PR and is outside the stated scope of this fix.

Overall, the PR is focused, correct, and ready to merge.

@ascorbic ascorbic left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Thanks!

@ascorbic
ascorbic merged commit 298895d into emdash-cms:main Jun 12, 2026
46 checks passed
@emdashbot emdashbot Bot mentioned this pull request Jun 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/plugins bot:review Trigger an emdashbot code review on this PR cla: signed review/needs-rereview Author pushed changes since the last review size/M

2 participants