larksuite/cli · error

draft has both text/plain and text/html body parts, but they

Error message

draft has both text/plain and text/html body parts, but they are not a supported summary+html pair

What it means

When a draft has both a text/plain and a text/html primary part, set_body can only replace them together if they form a recognized summary+html pair. If the two parts are not such a supported pair (and the input was valid for the coupled path), set_body gives up rather than guessing which part the value should replace.

Source

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

//
// This mirrors how normal attachments (independent MIME parts) survive
// 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.
//

View on GitHub (pinned to 7fd6ef3c07)

Solutions

  1. Use replace_body with an explicit body_kind (text/plain or text/html) to target one part
  2. Inspect the draft's MIME structure to confirm which parts exist and their relationship
  3. Remove or rebuild one of the body parts so the draft has a unique primary body

Example fix

// before
applyOp(Op{Op: "set_body", Value: text}) // draft has unrelated text+html parts
// after
applyOp(Op{Op: "replace_body", BodyKind: "text/html", Value: html})
Defensive patterns

Strategy: validation

Validate before calling

if snap.PrimaryTextPartID != "" && snap.PrimaryHTMLPartID != "" && !isCoupledSummaryPair(snap) {
    // use replace_body with explicit body_kind
}

Try / catch

if err := applyOp(op); err != nil && strings.Contains(err.Error(), "not a supported summary+html pair") {
    // retry with Op{Op: "replace_body", BodyKind: "text/html", ...}
}

Prevention

When it happens

Trigger: A set_body patch op on a snapshot with both PrimaryTextPartID and PrimaryHTMLPartID set, where the parts are NOT a coupled summary pair (so coupledBodySetBodyInputError passes) and tryApplyCoupledBodySetBody fails to apply the value to both parts.

Common situations: Multipart/alternative drafts authored externally with genuinely different text and HTML content (not a summary); forwarded or quoted drafts with two independent body parts.

Related errors


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