larksuite/cli · error

draft has no unique primary body part; use replace_body with

Error message

draft has no unique primary body part; use replace_body with body_kind

What it means

set_body resolves a unique 'primary' body: text-only drafts edit the text part, HTML-only drafts edit the HTML part, and coupled pairs are handled together. When the draft has neither primary part (both IDs empty), there is no unique body to target, so set_body directs you to replace_body with an explicit body_kind (which can create the part when rewriting is allowed).

Source

Thrown at shortcuts/mail/draft/patch.go:301

// body edits — giving consistent mental model: attachments and signature
// are draft-level concerns, not body content.
func setBody(snapshot *DraftSnapshot, value string, options PatchOptions) error {
	value = autoPreserveSystemManagedRegions(snapshot, value)
	switch {
	case snapshot.PrimaryTextPartID != "" && snapshot.PrimaryHTMLPartID == "":
		return replaceBody(snapshot, "text/plain", value, options)
	case snapshot.PrimaryTextPartID == "" && snapshot.PrimaryHTMLPartID != "":
		return replaceBody(snapshot, "text/html", value, options)
	case snapshot.PrimaryTextPartID != "" && snapshot.PrimaryHTMLPartID != "":
		if err := coupledBodySetBodyInputError(snapshot, value); err != nil {
			return err
		}
		if tryApplyCoupledBodySetBody(snapshot, value) {
			return nil
		}
		return fmt.Errorf("draft has both text/plain and text/html body parts, but they are not a supported summary+html pair")
	default:
		return fmt.Errorf("draft has no unique primary body part; use replace_body with body_kind")
	}
}

// autoPreserveSystemManagedRegions extracts system-managed elements
// (signature block and large attachment card) from the draft's old HTML
// body and injects them into value (before any quote block in value, or
// appended when no quote). Order is [sig][card], matching compose-time
// layout [user][sig][card][quote].
//
// For each element, auto-injection is skipped when value's
// user-authored region (before any quote block in value) already
// contains that element — so users who explicitly reconstruct the body
// with their own signature / card are respected. Elements inside a
// quote block in value belong to the quoted original message and are
// ignored for this check.
//
// No-op when the draft has no HTML body, or neither element exists in
// the old body.

View on GitHub (pinned to 7fd6ef3c07)

Solutions

  1. Use replace_body with body_kind set to text/plain or text/html and allow rewrite so the part is created
  2. Verify the draft's MIME structure — set_body expects an existing primary body part
  3. Set the body at draft creation time instead of patching an empty draft

Example fix

// before
applyOp(Op{Op: "set_body", Value: "hello"}) // draft has no primary body part
// after
applyOp(Op{Op: "replace_body", BodyKind: "text/html", Value: "<p>hello</p>", RewriteEntireDraft: true})
Defensive patterns

Strategy: validation

Validate before calling

if snap.PrimaryTextPartID == "" && snap.PrimaryHTMLPartID == "" {
    // use replace_body with body_kind and rewrite enabled
}

Try / catch

if err := applyOp(op); err != nil && strings.Contains(err.Error(), "no unique primary body part") {
    // retry with Op{Op: "replace_body", BodyKind: "text/html", RewriteEntireDraft: true}
}

Prevention

When it happens

Trigger: A set_body patch op on a snapshot where both PrimaryTextPartID and PrimaryHTMLPartID are empty — e.g. a draft with no body part yet, or a body structure the parser could not map to a primary part.

Common situations: Drafts created empty or with only attachments; drafts whose MIME tree the snapshot parser does not recognize as a primary body; operating before any body was ever set.

Related errors


AI-assisted analysis of larksuite/cli@7fd6ef3c07 (2026-09-04). Data as JSON: /api/errors/15be7bc37977617f. Report an issue: GitHub.