sipeed/picoclaw · error

wecom response body is empty

Error message

wecom response body is empty

What it means

decodeWeComEnvelopeBody got an ack envelope with a zero-length Body (media.go:659-662). It is used for the chunked upload protocol's init and finish acks (media.go:710,747): sendCommandAck returned successfully, but the reply envelope carried no JSON payload to decode into wecomUploadMediaInitResponse/FinishResponse.

Source

Thrown at pkg/channels/wecom/media.go:662

func buildWeComVideoContent(mediaID, filename, description string) *wecomVideoContent {
	title := strings.TrimSuffix(filename, filepath.Ext(filename))
	title = trimWeComBytes(title, 64)
	if title == "" {
		title = "video"
	}
	description = trimWeComBytes(description, 512)
	return &wecomVideoContent{
		MediaID:     mediaID,
		Title:       title,
		Description: description,
	}
}

func decodeWeComEnvelopeBody[T any](env wecomEnvelope) (T, error) {
	var out T
	if len(env.Body) == 0 {
		return out, fmt.Errorf("wecom response body is empty")
	}
	if err := json.Unmarshal(env.Body, &out); err != nil {
		return out, fmt.Errorf("decode wecom response body: %w", err)
	}
	return out, nil
}

func (c *WeComChannel) uploadOutboundMedia(
	ctx context.Context,
	localPath, filename, contentType string,
	part bus.MediaPart,
) (*wecomOutboundMedia, error) {
	_ = ctx

	contentType = detectLocalWeComContentType(localPath, contentType)
	filename = ensureWeComOutboundFilename(filename, localPath, contentType)

	data, err := os.ReadFile(localPath)

View on GitHub (pinned to 49183d7e8d)

Solutions

  1. Retry the upload once from scratch (new upload_id) - empty-body acks are usually transient
  2. If persistent: dump the raw envelope (cmd, req_id, body length) in debug logs and compare against the expected protocol
  3. Align versions of the channel and gateway/bridge components
  4. Check whether an error ack was sent separately and swallowed; correlate by ReqID

Example fix

// before: single attempt, empty body kills the send
initEnv, err := c.sendCommandAck(cmd, wecomUploadTimeout)
initResp, err := decodeWeComEnvelopeBody[wecomUploadMediaInitResponse](initEnv)

// after: one bounded retry on the empty-body case
var initResp wecomUploadMediaInitResponse
for attempt := 0; attempt < 2; attempt++ {
    initEnv, err := c.sendCommandAck(cmd, wecomUploadTimeout)
    if err != nil { return nil, err }
    initResp, err = decodeWeComEnvelopeBody[wecomUploadMediaInitResponse](initEnv)
    if err == nil || !strings.Contains(err.Error(), "body is empty") { break }
}
Defensive patterns

Strategy: retry

Try / catch

var errEmptyBody = errors.New("wecom response body is empty")

// bounded single retry for transient empty-body acks
func uploadWithRetry(ctx context.Context, do func() error) error {
    if err := do(); err == nil {
        return nil
    } else if !errors.Is(err, errEmptyBody) && !strings.Contains(err.Error(), "body is empty") {
        return err
    }
    time.Sleep(500 * time.Millisecond)
    return do()
}

Prevention

When it happens

Trigger: The WeCom gateway/bridge acknowledged the upload init (or finish) command but omitted the body: transient gateway hiccup, protocol/version drift where the server answers ack-only, or an error path that drops the payload while keeping the envelope.

Common situations: Upgrading one side of the picoclaw WeCom bridge without the other; gateway restarts mid-upload; custom gateways that never send bodies on certain commands.

Related errors


AI-assisted analysis of sipeed/picoclaw@49183d7e8d (2026-08-15). Data as JSON: /api/errors/595fff6fd6121c57. Report an issue: GitHub.