sipeed/picoclaw · error · ErrTemporary

feishu send text: %w

Error message

feishu send text: %w

What it means

The Lark SDK Message.Create call returned a Go error before an API response body existed — a transport-level failure (connection refused, DNS error, TLS failure, client timeout). The channel deliberately discards the underlying err and wraps only channels.ErrTemporary, so the send is retried with exponential backoff but the true cause (network vs marshalling) is lost from the message.

Source

Thrown at pkg/channels/feishu/feishu_64.go:1120

	return "", nil
}

// sendText sends a plain text message to a chat (fallback when card fails).
func (c *FeishuChannel) sendText(ctx context.Context, chatID, text string) (string, error) {
	content, _ := json.Marshal(map[string]string{"text": text})

	req := larkim.NewCreateMessageReqBuilder().
		ReceiveIdType(larkim.CreateMessageV1ReceiveIDTypeChatId).
		Body(larkim.NewCreateMessageReqBodyBuilder().
			ReceiveId(chatID).
			MsgType(larkim.MsgTypeText).
			Content(string(content)).
			Build()).
		Build()

	resp, err := c.client.Im.V1.Message.Create(ctx, req)
	if err != nil {
		return "", fmt.Errorf("feishu send text: %w", channels.ErrTemporary)
	}

	if !resp.Success() {
		return "", fmt.Errorf("feishu text api error (code=%d msg=%s): %w", resp.Code, resp.Msg, channels.ErrTemporary)
	}

	logger.DebugCF("feishu", "Feishu text message sent (fallback)", map[string]any{
		"chat_id": chatID,
	})

	if resp.Data != nil && resp.Data.MessageId != nil {
		return *resp.Data.MessageId, nil
	}
	return "", nil
}

// sendImage uploads an image and sends it as a message.
func (c *FeishuChannel) sendImage(ctx context.Context, chatID string, file *os.File) error {

View on GitHub (pinned to 49183d7e8d)

Solutions

  1. Verify egress from the same host: curl https://open.feishu.cn/open-apis/ (or a TCP dial to open.feishu.cn:443).
  2. Rely on the built-in retry (ErrTemporary) and check logs for whether a later attempt succeeded.
  3. If persistent, note this path drops the SDK error — temporarily log err before the wrap (or patch it, see exampleFix) to see the real cause.
  4. Check HTTP_PROXY/HTTPS_PROXY/NO_PROXY values in the runtime environment.

Example fix

// before (feishu_64.go:1120) — underlying err is discarded
return "", fmt.Errorf("feishu send text: %w", channels.ErrTemporary)

// after (Go 1.20+ multi-%w): keep the cause AND the retry sentinel
return "", fmt.Errorf("feishu send text: %w: %w", err, channels.ErrTemporary)
Defensive patterns

Strategy: retry

Validate before calling

conn, derr := net.DialTimeout("tcp", "open.feishu.cn:443", 2*time.Second)
if derr != nil {
    // defer the send: egress is down, it would only fail as temporary
}
conn.Close()

Type guard

func isTemporarySendErr(err error) bool {
    return errors.Is(err, channels.ErrTemporary)
}

Try / catch

if _, err := ch.Send(ctx, msg); err != nil {
    if errors.Is(err, channels.ErrTemporary) {
        // backoff and retry; transport causes are dropped from this message, check network health separately
    }
}

Prevention

When it happens

Trigger: msg_type=text Create call fails at transport: no egress to open.feishu.cn, HTTP_PROXY/HTTPS_PROXY pointing at a dead proxy, client timeout on a slow link, or SDK request-marshalling failure on the text content.

Common situations: Container/k8s pod without network egress; corporate TLS-intercepting proxy; transient ISP blip; proxy env vars set for a different environment.

Related errors


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