chenhg5/cc-connect · error

qqbot: api %s %s returned %d (after retry): %s

Error message

qqbot: api %s %s returned %d (after retry): %s

What it means

After the 401 retry, if the second response still has an HTTP status >= 300, apiRequestJSON returns this error including the method, URL, status code, and the raw response body. It means the token refresh did not fix the authorization problem, or a non-auth error (400/403/404/413/429) occurred on the retried call.

Source

Thrown at platform/qqbot/qqbot.go:358

			data, _ := json.Marshal(body)
			bodyReader = bytes.NewReader(data)
		}
		req2, err := http.NewRequest(method, url, bodyReader)
		if err != nil {
			return fmt.Errorf("qqbot: build retry request: %w", err)
		}
		req2.Header.Set("Authorization", "QQBot "+token)
		req2.Header.Set("Content-Type", "application/json")

		resp2, err := core.HTTPClient.Do(req2)
		if err != nil {
			return fmt.Errorf("qqbot: api retry failed: %w", err)
		}
		defer resp2.Body.Close()

		if resp2.StatusCode >= 300 {
			raw, _ := io.ReadAll(resp2.Body)
			return fmt.Errorf("qqbot: api %s %s returned %d (after retry): %s", method, url, resp2.StatusCode, raw)
		}
		if result != nil {
			if err := json.NewDecoder(resp2.Body).Decode(result); err != nil {
				return fmt.Errorf("qqbot: decode response: %w", err)
			}
		}
		return nil
	}

	if resp.StatusCode >= 300 {
		raw, _ := io.ReadAll(resp.Body)
		return fmt.Errorf("qqbot: api %s %s returned %d: %s", method, url, resp.StatusCode, raw)
	}
	if result != nil {
		if err := json.NewDecoder(resp.Body).Decode(result); err != nil {
			return fmt.Errorf("qqbot: decode response: %w", err)
		}
	}

View on GitHub (pinned to 4000b2338a)

Solutions

  1. Read the raw body in the error message — the QQ API error code explains the cause (e.g. 11253 rate limit, invalid openID).
  2. Still 401 after retry: re-verify appId/appSecret in config.toml; restart to force fresh tokens.
  3. 403/404: confirm the bot is a member of the target group and the openID matches an existing chat.
  4. 413: reduce file size to QQ Bot rich media limits before uploading.
  5. 429: back off and retry later; check nextMsgSeq/msg_id reuse.
Defensive patterns

Strategy: try-catch

Validate before calling

if len(fileData) > maxQQBotFileSize {
    return fmt.Errorf("file %d bytes exceeds qqbot limit", len(fileData))
}
if groupOpenID == "" {
    return fmt.Errorf("cannot send: empty group openID")
}

Try / catch

var apiErr struct{ Code int `json:"code"`; Message string `json:"message"` }
if m := reBody.FindStringSubmatch(err.Error()); m != nil {
    _ = json.Unmarshal([]byte(m[1]), &apiErr) // parse raw body from the error text
    switch apiErr.Code {
    case rateLimitedCode:
        // backoff and retry
    default:
        slog.Error("qqbot api error after retry", "code", apiErr.Code, "msg", apiErr.Message)
    }
}

Prevention

When it happens

Trigger: The first attempt got 401, refresh + retry succeeded at transport level, but resp2.StatusCode >= 300 — e.g. still 401 (bad credentials), 403 (no permission for that group/user), 404 (bad openID), 413 (file too large), or 429 (rate limited).

Common situations: Wrong or revoked appSecret surviving a refresh; bot not added to the target group; invalid file_type/file size on rich media upload; exceeding QQ message rate limits.

Understand the failure class

Background: "API error: {status}" and "HTTP 401/403/404/429/5xx" errors: non-2xx HTTP responses explained — this error's family across 27 libraries.

Related errors


AI-assisted analysis of chenhg5/cc-connect@4000b2338a (2026-09-06). Data as JSON: /api/errors/2485a81c9688b199. Report an issue: GitHub.