Skip to content

Harden comment submission against missing required fields - #1770

Merged
feruzm merged 4 commits into
developfrom
seer/fix/harden-comment-submission
Sep 9, 2026
Merged

feruzm merged 4 commits into
developfrom
seer/fix/harden-comment-submission

Conversation

@sentry

@sentry sentry Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

This PR addresses the issue where users encountered an [SDK][buildCommentOp] Missing required parameters error when attempting to submit comments or replies, particularly on deeply nested reply pages or with empty comment bodies.

Root Cause:
The buildCommentOp function in the @ecency/sdk was being called with incomplete CommentPayload data because upstream mutations (like useComment and create-reply) lacked sufficient pre-validation. The SDK's internal validation was the first point of failure, leading to an unhandled error.

Changes Implemented:

  1. SDK useComment Pre-validation: Added a comprehensive pre-flight validation block within packages/sdk/src/modules/posts/mutations/use-comment.ts. This block now explicitly checks for the presence of author, permlink, parentPermlink, and body in the CommentPayload before calling buildCommentOp. If any are missing, a descriptive error is thrown, providing clearer feedback to all consumers of this SDK hook.
  2. create-reply Mutation Guards: Enhanced apps/web/src/api/mutations/create-reply.ts with early guards in the mutationFn:
    • A check to prevent submission if the text (comment body) is empty or consists only of whitespace.
    • A check to ensure entry.permlink (representing the parent permlink) is present, preventing issues with unresolved parent references in complex reply scenarios.

Impact:
These changes ensure that attempts to submit comments or replies with missing essential data are caught earlier in the application flow. Users will now receive more immediate and actionable feedback, preventing the SDK-level error and improving the overall stability and user experience of the comment submission process.

Fixes ECENCY-NEXT-1GQ3

This PR was automatically generated by Sentry. You can adjust this setting at any time.

sentry Bot and others added 2 commits September 8, 2026 22:55
Clicking Reply/Update with an empty box called buildCommentOp with an empty
body, which threw "[SDK][buildCommentOp] Missing required parameters" straight
into a toast (ECENCY-NEXT-1GQ3). Ctrl/Cmd+Enter already checked text.trim();
the button did not, so nothing stopped the click.

Disable the button and early-return in submit. The composer is shared, so this
covers the entry page reply box, discussion items, entry edit, curation desk
and the deck viewer in one place, and the error toast never appears at all.

Mirror create-reply's empty-body guard in update-reply as well. Editing still
reached buildCommentOp unguarded. Whitespace-only text is the worse case there:
it passes the !body check and broadcasts a blank edit for real.
@feruzm

feruzm commented Sep 9, 2026

Copy link
Copy Markdown
Member

Pushed 83cff00b65 on top, because the guards here relabel the error rather than remove it.

Reproduced the reported path from the event (reported_via=feedback-toast-report, so the Sentry stack is the synthetic one from the Report button and points at feedback-message.tsx, not at the cause): the composer's Ctrl/Cmd+Enter handler is gated on text?.trim(), but the Reply/Update button is not. Clicking it on an empty box submits "", which is the only payload field the UI can leave empty (author, permlink and parentPermlink are always populated on that path). With the guard in create-reply the user still gets an error toast with a Report button on it, just with different wording.

Two gaps this commit closes:

  1. The edit path was untouched. useUpdateReply and useCrossPost also call buildCommentOp, and update-reply.ts had no body guard, so editing a reply or post to an empty body still threw the exact error in the title. Added the matching guard there.
  2. Whitespace-only text. " " is truthy, so it passes !payload.body in the new SDK check and gets broadcast as a blank edit. create-reply handles it via text.trim(); update-reply now does too.

The actual fix is one level up: disable the submit button and early-return in submit when there is no trimmed text. The composer is shared, so that covers the entry page reply box, discussion items, entry edit, curation desk and the deck viewer at once, and the toast never appears in the first place.

Added two specs pinning it (the submit button does NOT submit an empty composer / ... whitespace only). Both fail on the pre-fix composer and pass after. Full apps/web suite 4152/4152 green, tsc --noEmit clean.

The useComment pre-check is redundant with buildCommentOp's own validation, but harmless. I checked the two call sites that pass parentPermlink: "" (waves and decks community APIs); both are tempEntry arguments, not the broadcast payload, so the stricter check is not a regression.

Follow-up on review feedback.

.trim() strips Unicode whitespace but not format characters, so a body made only
of U+200B, ZWNJ, ZWJ or a word joiner survived every guard in this PR and got
broadcast as a comment that renders as nothing. isBlankBody() strips \p{Cf}
before trimming. It is now the single rule behind the composer gate, create-reply,
update-reply, the waves form and the decks thread form. Those last two gated on
!text alone, so whitespace-only waves and threads could still be posted.

The SDK had the same check twice with different semantics. buildCommentOp is the
one point create, update and cross-post all pass through, so the missing-field
list moved there and the copy in useComment is gone. That copy only ever covered
create, and it rejected an empty parentPermlink where the builder allows one.

The user-facing message now goes through i18next as comment.empty-body. Dropped
the em dash from the parent permlink error while there.

Tests: is-blank-body covers the zero-width cases plus a ZWJ emoji that must not
read as blank, and buildCommentOp names each missing field.
@feruzm

feruzm commented Sep 9, 2026

Copy link
Copy Markdown
Member

Second round, from review feedback. Pushed 0ddfe43205.

Confirmed and fixed

  • Zero-width characters defeated every guard. .trim() strips Unicode whitespace, so NBSP and U+FEFF were already covered, but U+200B, ZWNJ, ZWJ and the word joiner are format characters and survive it. A body of only those passed the UI gate, both mutation guards and !payload.body, then broadcast a comment that renders as nothing. There is now one predicate, isBlankBody(), which strips \p{Cf} before trimming. A ZWJ emoji sequence still counts as content (test covers 👨‍👩‍👧).
  • The check was duplicated with inconsistent semantics. isBlankBody() is now the single rule behind the composer gate, create-reply, update-reply, the waves form and the decks thread form. The last two gated on !text alone, so whitespace-only waves and threads could still be posted. That hole is closed too.
  • SDK asymmetry. Agreed the per-hook copy was the wrong place: it only covered create, while useUpdateReply and useCrossPost reach the same builder. The missing-field list moved into buildCommentOp, which all three pass through, and the useComment copy is gone. Note the copy also rejected an empty parentPermlink, where the builder deliberately allows "" and only rejects undefined.
  • i18n. The user-facing message is now comment.empty-body via i18next. The parent-permlink error stays a plain internal invariant, matching the existing "Missing active user or entry" next to it, minus the em dash.

One claim I could not reproduce: the update-reply guard breaking usePinReply.

Pin calls updateReply({ text: parent.body, ... }), so the guard fires only when parent.body is already empty. In that state buildCommentOp threw on !body before this PR existed, so pin never worked there. The guard changes the message and moves the throw ahead of the optimistic cache write, which is strictly better. Happy to look again if there is a case where a pin succeeded with an empty parent body.

On "fix it once at buildCommentOp": half agreed, and that is what the SDK change does for the field list. But the builder cannot own the blank-body rule for the UI, because a throw from there is precisely what produced the toast in ECENCY-NEXT-1GQ3. The builder stays a last-resort assertion; the composer decides whether the button is clickable.

Verification: apps/web 4157/4157, packages/sdk 960/960, pnpm -r typecheck clean, next lint clean. packages/sdk/dist deliberately not committed, since the public API is unchanged and the deploy workflows run build:packages before build.

@feruzm feruzm added the patch Bug fixes and patches (1.0.0 → 1.0.1) label Sep 9, 2026
@feruzm
feruzm merged commit 8ce8152 into develop Sep 9, 2026
7 checks passed
@feruzm
feruzm deleted the seer/fix/harden-comment-submission branch September 9, 2026 07:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

patch Bug fixes and patches (1.0.0 → 1.0.1)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant