larksuite/cli · error
body_kind must be text/plain or text/html
Error message
body_kind must be text/plain or text/html
What it means
Thrown by PatchOp.Validate() in shortcuts/mail/draft/model.go:295 when replace_body or append_body ops carry a body_kind that is not exactly "text/plain" or "text/html". isBodyKind(op.BodyKind) enforces the whitelist, including rejecting an empty body_kind. This tells the patch layer which MIME part to operate on.
Source
Thrown at shortcuts/mail/draft/model.go:295
return fmt.Errorf("set_recipients requires non-empty addresses")
}
}
case "add_recipient", "remove_recipient":
if !isRecipientField(op.Field) {
return fmt.Errorf("recipient field must be one of to/cc/bcc")
}
if strings.TrimSpace(op.Address) == "" {
return fmt.Errorf("%s requires address", op.Op)
}
case "set_reply_to":
if len(op.Addresses) == 0 {
return fmt.Errorf("set_reply_to requires addresses")
}
case "clear_reply_to":
case "set_body", "set_reply_body":
case "replace_body", "append_body":
if !isBodyKind(op.BodyKind) {
return fmt.Errorf("body_kind must be text/plain or text/html")
}
if op.Selector != "" && op.Selector != "primary" {
return fmt.Errorf("selector must be primary")
}
case "set_header":
if strings.TrimSpace(op.Name) == "" {
return fmt.Errorf("set_header requires name")
}
if strings.ContainsAny(op.Name, ":\r\n") {
return fmt.Errorf("set_header: header name must not contain ':', CR, or LF")
}
if strings.ContainsAny(op.Value, "\r\n") {
return fmt.Errorf("set_header: header value must not contain CR or LF")
}
case "remove_header":
if strings.TrimSpace(op.Name) == "" {
return fmt.Errorf("remove_header requires name")
}View on GitHub (pinned to 7fd6ef3c07)
Solutions
- Set body_kind to exactly "text/plain" or "text/html".
- Normalize user/config input before constructing the op: map "html"->"text/html", "text"->"text/plain", lower-case the string.
- Check the wrapped op index (invalid patch op #N) to find the offending op.
Example fix
// before
{"op":"replace_body","body_kind":"html","value":"<p>hi</p>"}
// after
{"op":"replace_body","body_kind":"text/html","value":"<p>hi</p>"} Defensive patterns
Strategy: validation
Validate before calling
func normalizeBodyKind(k string) (string, error) {
switch strings.ToLower(strings.TrimSpace(k)) {
case "text/plain", "plain", "text": return "text/plain", nil
case "text/html", "html": return "text/html", nil
}
return "", fmt.Errorf("body_kind must be text/plain or text/html")
} Type guard
func isBodyKind(k string) bool {
return k == "text/plain" || k == "text/html"
} Prevention
- Use string constants for body kinds instead of inline literals.
- Normalize shorthand ("html", "text") and casing before constructing the op.
- Remember body_kind is required for replace_body/append_body even though set_body omits it.
When it happens
Trigger: Ops like {"op":"replace_body","body_kind":"html"}, body_kind:"text", body_kind:"plain", or body_kind omitted entirely; values with different casing ("text/HTML"); an empty string after template substitution.
Common situations: Assuming shorthand values ("html", "text") are accepted; case-sensitivity slips; forgetting body_kind is required for replace/append (unlike set_body/set_reply_body which have no kind constraint); building ops from user config with free-form kind strings.
Understand the failure class
Background: Invalid enum value errors: "Unknown type", "Invalid scope", "must be one of" — when a string is not on the library's allowed list — this error's family across 23 libraries.
Related errors
- selector must be primary
- set_recipients requires non-empty addresses
- %s requires address
- set_reply_to requires addresses
- set_header requires name
AI-assisted analysis of larksuite/cli@7fd6ef3c07 (2026-09-04).
Data as JSON: /api/errors/eda8b584b7abfc1e.
Report an issue: GitHub.