sipeed/picoclaw · warning · channels.ErrTemporary

whatsapp connection not established: %w

Error message

whatsapp connection not established: %w

What it means

Returned by the WhatsApp bridge channel's Send when c.conn is nil - i.e. Start() never completed a bridge connection or Stop() has torn it down. It wraps channels.ErrTemporary, so the channel manager treats it as transient and retries with backoff rather than dropping the message permanently.

Source

Thrown at pkg/channels/whatsapp/whatsapp.go:127

}

func (c *WhatsAppChannel) Send(ctx context.Context, msg bus.OutboundMessage) ([]string, error) {
	if !c.IsRunning() {
		return nil, channels.ErrNotRunning
	}

	// Check ctx before acquiring lock
	select {
	case <-ctx.Done():
		return nil, ctx.Err()
	default:
	}

	c.mu.Lock()
	defer c.mu.Unlock()

	if c.conn == nil {
		return nil, fmt.Errorf("whatsapp connection not established: %w", channels.ErrTemporary)
	}

	payload := map[string]any{
		"type":    "message",
		"to":      msg.ChatID,
		"content": msg.Content,
	}

	data, err := json.Marshal(payload)
	if err != nil {
		return nil, fmt.Errorf("failed to marshal message: %w", err)
	}

	_ = c.conn.SetWriteDeadline(time.Now().Add(10 * time.Second))
	if err := c.conn.WriteMessage(websocket.TextMessage, data); err != nil {
		_ = c.conn.SetWriteDeadline(time.Time{})
		return nil, fmt.Errorf("whatsapp send: %w", channels.ErrTemporary)
	}

View on GitHub (pinned to 49183d7e8d)

Solutions

  1. Check whether the channel reports running state before send - IsRunning() gate usually prevents this path.
  2. Fix the underlying bridge connectivity so Start() succeeds (see bridge URL, process, proxy checks).
  3. Rely on the manager's ErrTemporary backoff: queued outbound messages retry automatically once the bridge is back.
  4. If it persists, inspect logs for a prior 'failed to connect to WhatsApp bridge' error explaining why conn is nil.
Defensive patterns

Strategy: retry

Validate before calling

if !ch.IsRunning() {
    return errors.New("whatsapp bridge channel not running")
}

Type guard

func isTemporary(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) {
        // conn nil or send failure: retry after bridge reconnects
    }
}

Prevention

When it happens

Trigger: Send(ctx, msg) is invoked after Start() failed (bridge unreachable), after Stop() released the websocket, or while the channel is between a connection drop and restart.

Common situations: Bridge outage that crashed Start; sending queued messages right after process restart before the channel reconnects; a Stop/Start race during reconfiguration where Send slips in with conn still nil.

Related errors


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