Tencent/WeKnora · error

empty qqbot access token: code=

Error message

empty qqbot access token: code=%d message=%s

What it means

After a successful token request, the QQ bot API responded with an empty access_token. The client treats this as a hard failure and includes the API's code and message fields, which usually indicate credential rejection.

Solutions

  1. Verify AppID and AppSecret in the channel credentials against the QQ developer console
  2. Read the code/message fields in the error — they state the API's rejection reason
  3. Confirm the app is approved/enabled and the correct api_base_url is used

Example fix

// before
creds := map[string]string{"appId": "", "clientSecret": ""}
// after
creds := map[string]string{"appId": "1020xxxxx", "clientSecret": "correct-secret-from-console"}
Defensive patterns

Strategy: validation

Validate before calling

if creds["appId"] == "" || creds["clientSecret"] == "" {
    return errors.New("qqbot appId and clientSecret are required before requesting a token")
}

Try / catch

token, err := client.AccessToken(ctx)
if err != nil {
    if strings.Contains(err.Error(), "empty qqbot access token") {
        return fmt.Errorf("check AppID/AppSecret in QQ console: %w", err)
    }
    return err
}

Prevention

When it happens

Trigger: AccessToken POSTs appId/clientSecret to the app token endpoint and the response struct has AccessToken == "" — typically because credentials are wrong, the app is disabled, or the API returned an error payload with 2xx/4xx that still decoded.

Common situations: Misconfigured QQBOT_APPID / QQBOT_APPSECRET; app not yet approved or sandbox restrictions; wrong api_base_url routing to a mock returning empty tokens; app secret rotated server-side.

Related errors


AI-assisted analysis of Tencent/WeKnora@988cbb0330 (2026-09-02). Data as JSON: /api/errors/af2e2f0fa5509f81. Report an issue: GitHub.

Appendix: source

Thrown at internal/im/qqbot/client.go:195

func (c *Client) AccessToken(ctx context.Context) (string, error) {
	c.mu.Lock()
	if c.accessToken != "" && time.Until(c.expiresAt) > time.Minute {
		token := c.accessToken
		c.mu.Unlock()
		return token, nil
	}
	c.mu.Unlock()

	body := map[string]string{
		"appId":        c.appID,
		"clientSecret": c.clientSecret,
	}
	var result tokenResponse
	if err := c.doJSON(ctx, http.MethodPost, appTokenURL, body, &result); err != nil {
		return "", err
	}
	if result.AccessToken == "" {
		return "", fmt.Errorf("empty qqbot access token: code=%d message=%s", result.Code, result.Message)
	}
	expiresIn := parseExpiresIn(result.ExpiresIn)

	c.mu.Lock()
	c.accessToken = result.AccessToken
	c.expiresAt = time.Now().Add(time.Duration(expiresIn) * time.Second)
	c.mu.Unlock()
	return result.AccessToken, nil
}

func parseExpiresIn(raw json.RawMessage) int {
	if len(raw) == 0 {
		return 7200
	}
	var number int
	if err := json.Unmarshal(raw, &number); err == nil && number > 0 {
		return number
	}

View on GitHub (pinned to 988cbb0330)