{"record":{"id":"e996d962f5566970","repo":"chenhg5/cc-connect","slug":"qqbot-token-refresh-on-401-w","errorCode":null,"errorMessage":"qqbot: token refresh on 401: %w","messagePattern":"qqbot: token refresh on 401: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"platform/qqbot/qqbot.go","lineNumber":335,"sourceCode":"\t}\n\n\treq, err := http.NewRequest(method, url, bodyReader)\n\tif err != nil {\n\t\treturn err\n\t}\n\treq.Header.Set(\"Authorization\", \"QQBot \"+token)\n\treq.Header.Set(\"Content-Type\", \"application/json\")\n\n\tresp, err := core.HTTPClient.Do(req)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"qqbot: api request failed: %w\", err)\n\t}\n\tdefer resp.Body.Close()\n\n\t// Retry once on 401\n\tif resp.StatusCode == http.StatusUnauthorized {\n\t\tif err := p.refreshToken(); err != nil {\n\t\t\treturn fmt.Errorf(\"qqbot: token refresh on 401: %w\", err)\n\t\t}\n\t\ttoken, _ = p.getAccessToken()\n\n\t\tif body != nil {\n\t\t\tdata, _ := json.Marshal(body)\n\t\t\tbodyReader = bytes.NewReader(data)\n\t\t}\n\t\treq2, err := http.NewRequest(method, url, bodyReader)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"qqbot: build retry request: %w\", err)\n\t\t}\n\t\treq2.Header.Set(\"Authorization\", \"QQBot \"+token)\n\t\treq2.Header.Set(\"Content-Type\", \"application/json\")\n\n\t\tresp2, err := core.HTTPClient.Do(req2)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"qqbot: api retry failed: %w\", err)\n\t\t}","sourceCodeStart":317,"sourceCodeEnd":353,"githubUrl":"https://github.com/chenhg5/cc-connect/blob/4000b2338aa6e850c99df54f8b0ed6ed7460b401/platform/qqbot/qqbot.go#L317-L353","documentation":"When the QQ API answers 401 (stale/expired cached token), apiRequestJSON attempts one recovery: it calls p.refreshToken() to force a new token. If that refresh itself fails, this error wraps the refresh failure. The original 401 never reaches the caller as such — the caller only sees that the recovery path broke.","triggerScenarios":"The API returned HTTP 401 and the subsequent p.refreshToken() call failed — typically because the token endpoint rejected the new token request (bad AppSecret) or was unreachable at that moment.","commonSituations":"Token cache invalidated server-side (credential rotation, app suspension) while the secret in config is stale; QQ Open Platform outage coinciding with token expiry; clock skew making token issuance fail.","solutions":["Fix the underlying refresh failure: verify appId/appSecret are current (see wrapped error).","Restart cc-connect to clear in-memory token state, then retry.","Check QQ Open Platform status — a 401 followed by refresh failure often indicates an auth-service outage.","Confirm host clock is synchronized (NTP) since token issuance validates timestamps.","If persistent, re-create credentials in the QQ console and update config.toml."],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":"// Pre-flight: force a token fetch at startup so bad credentials fail fast\nif _, err := p.getAccessToken(); err != nil {\n    return fmt.Errorf(\"qqbot: startup token check failed: %w\", err)\n}","typeGuard":null,"tryCatchPattern":"if err := send(); err != nil {\n    if strings.Contains(err.Error(), \"token refresh on 401\") {\n        slog.Error(\"qqbot credentials rejected; verify appId/appSecret\", \"err\", err)\n        // backoff and alert; retrying won't help until creds are fixed\n    }\n}","preventionTips":["Run a startup token pre-flight to catch bad credentials immediately.","Keep host clock synchronized via NTP.","Alert on 401+refresh-failure pairs — they indicate rejected credentials, not transient auth.","Re-verify secrets after any rotation in the QQ Open Platform console."],"tags":["qqbot","authentication","token","retry"],"backgroundTag":"jwt-token-expired","analyzedSha":"4000b2338aa6e850c99df54f8b0ed6ed7460b401","analyzedAt":"2026-09-06T11:45:09.575Z","contentChangedAt":"2026-09-06T11:45:09.575Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}