sipeed/picoclaw · error

failed to start token refresh: %w

Error message

failed to start token refresh: %w

What it means

QQChannel.Start wraps the error from token.StartRefreshAccessToken, the goroutine/loop that obtains and periodically refreshes the QQ access token using the configured app_id/app_secret. It fails when the very first token request fails — bad credentials, no network to the QQ auth endpoint, or an already-cancelled context — and the underlying error is preserved via %w for inspection.

Source

Thrown at pkg/channels/qq/qq.go:126

	logger.InfoC("qq", "Starting QQ bot (WebSocket mode)")

	// Reinitialize shutdown signal for clean restart.
	c.done = make(chan struct{})
	c.stopOnce = sync.Once{}

	// create token source
	credentials := &token.QQBotCredentials{
		AppID:     c.config.AppID,
		AppSecret: c.config.AppSecret.String(),
	}
	c.tokenSource = token.NewQQBotTokenSource(credentials)

	// create child context
	c.ctx, c.cancel = context.WithCancel(ctx)

	// start auto-refresh token goroutine
	if err := token.StartRefreshAccessToken(c.ctx, c.tokenSource); err != nil {
		return fmt.Errorf("failed to start token refresh: %w", err)
	}

	// initialize OpenAPI client
	c.api = botgo.NewOpenAPI(c.config.AppID, c.tokenSource).WithTimeout(5 * time.Second)

	// register event handlers
	intent := event.RegisterHandlers(
		c.handleC2CMessage(),
		c.handleGroupATMessage(),
	)

	// get WebSocket endpoint
	wsInfo, err := c.api.WS(c.ctx, nil, "")
	if err != nil {
		return fmt.Errorf("failed to get websocket info: %w", err)
	}

	logger.InfoCF("qq", "Got WebSocket info", map[string]any{

View on GitHub (pinned to 49183d7e8d)

Solutions

  1. Print the full chain (fmt.Printf("%+v", err) or errors.As) — the wrapped error distinguishes 401/invalid_client (bad credentials) from timeouts/DNS (network).
  2. Fix credentials in the QQ open platform console if the wrapped error indicates authorization failure, then restart the channel.
  3. Restore outbound connectivity to the QQ API host (firewall, DNS, proxy) if the wrapped error is a network class failure.
  4. Retry Start after a short backoff — QQ open platform occasionally returns transient 5xx on the token endpoint.

Example fix

// before: swallowing the chain
if err := ch.Start(ctx); err != nil {
    log.Fatal("start failed")
}

// after: surface the wrapped cause
if err := ch.Start(ctx); err != nil {
    var netErr net.Error
    if errors.As(err, &netErr) {
        log.Printf("qq: transient network issue, retrying: %v", err)
        time.Sleep(5 * time.Second)
        return ch.Start(ctx)
    }
    return err // credential/config problem
}
Defensive patterns

Strategy: try-catch

Validate before calling

// pre-flight: can we reach the QQ token endpoint with these credentials?
if _, err := http.Head("https://bots.qq.com"); err != nil {
    return fmt.Errorf("no egress to QQ API, Start will fail: %w", err)
}

Try / catch

if err := ch.Start(ctx); err != nil {
    var netErr net.Error
    switch {
    case errors.As(err, &netErr):
        retryAfterBackoff() // transient
    case strings.Contains(err.Error(), "failed to start token refresh"):
        inspectCredentials() // unwrap with %v to see invalid_client vs timeout
    }
}

Prevention

When it happens

Trigger: Calling Start with an incorrect app_secret (auth endpoint returns invalid_client); no DNS/route to openapi.qq.com or the token endpoint from the host; passing an already-cancelled context so the refresh loop exits immediately; QQ open platform outage during startup.

Common situations: Copied app_id correctly but app_secret stale after a console reset; container with no egress or wrong proxy env; clock skew breaking token issuance; restarting the channel after a network partition before connectivity is restored.

Related errors


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