chenhg5/cc-connect · critical
api returned status %d: %s
Error message
api returned status %d: %s
What it means
The DingTalk accessToken endpoint responded with a non-200 HTTP status; getAccessToken reads the response body (which usually contains a JSON error description) and includes both the status code and body in this error. This is an authentication/configuration failure at the DingTalk API level, not a transport issue.
Source
Thrown at platform/dingtalk/dingtalk.go:761
}
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(bodyBytes))
if err != nil {
return "", fmt.Errorf("create request: %w", err)
}
req.Header.Set("Content-Type", "application/json")
resp, err := p.httpClient.Do(req)
if err != nil {
return "", fmt.Errorf("do request: %w", err)
}
defer func() { _ = resp.Body.Close() }()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return "", fmt.Errorf("api returned status %d: %s", resp.StatusCode, body)
}
var tokenResp struct {
AccessToken string `json:"accessToken"`
ExpireIn int `json:"expireIn"`
}
if err := json.NewDecoder(resp.Body).Decode(&tokenResp); err != nil {
return "", fmt.Errorf("decode response: %w", err)
}
if tokenResp.AccessToken == "" {
return "", fmt.Errorf("empty accessToken in response")
}
// Cache token with 5 minutes buffer before expiry.
// When the server omits expireIn (or sends 0/negative), fall back to the
// documented DingTalk default (7200s = 2h) — without this, tokenExpiry
// would land at time.Now() and every subsequent getAccessToken() wouldView on GitHub (pinned to 4000b2338a)
Solutions
- Read the embedded body in the error — DingTalk's message (e.g. badRequest / appKey invalid) tells you the exact cause.
- Verify appKey and appSecret against the DingTalk developer console; update config.toml and restart cc-connect.
- Confirm the app is enabled, published, and the robot is active.
- Check IP allowlist configuration on the DingTalk app if your server IP changed.
- For 5xx statuses, wait and retry — this is a DingTalk-side outage.
Example fix
// before: silent retry loop against a bad secret
for {
tok, err := p.getAccessToken()
if err != nil { time.Sleep(time.Second); continue }
}
// after: fail fast on auth errors, retry only server errors
tok, err := p.getAccessToken()
if err != nil {
if strings.Contains(err.Error(), "status 4") {
return fmt.Errorf("dingtalk auth failed, check appKey/appSecret: %w", err)
}
return fmt.Errorf("dingtalk token: %w", err)
} Defensive patterns
Strategy: try-catch
Validate before calling
if len(p.clientID) < 8 || len(p.clientSecret) < 8 {
return errors.New("dingtalk: appKey/appSecret look invalid before any API call")
} Try / catch
tok, err := p.getAccessToken()
if err != nil {
if strings.Contains(err.Error(), "api returned status 4") {
// stop retrying; alert operator to fix credentials
return fmt.Errorf("dingtalk auth rejected — check appKey/appSecret: %w", err)
}
return err // 5xx: retryable
} Prevention
- Store credentials in config.toml via a tested template; validate at startup.
- Update config immediately after any secret rotation and restart the daemon.
- Keep the DingTalk app published/enabled with correct IP allowlist entries.
- Alert on 4xx token failures; they never resolve by retrying.
When it happens
Trigger: Any token-requiring call when DingTalk rejects the token request: wrong appKey/appSecret (typically 400 with badRequest), app disabled or permissions revoked, or DingTalk server-side errors (5xx).
Common situations: Typo or stale value in clientID/clientSecret in config.toml, rotated secret not updated in the running daemon, app deleted or disabled in the DingTalk developer console, or IP allowlist changes on the DingTalk app.
Related errors
- empty accessToken in response
- create AI card: status=%d, body=%s
- stream AI card: status=%d, body=%s
- create request: %w
- do request: %w
AI-assisted analysis of chenhg5/cc-connect@4000b2338a (2026-09-06).
Data as JSON: /api/errors/3dac99731d670598.
Report an issue: GitHub.