chenhg5/cc-connect · critical
empty accessToken in response
Error message
empty accessToken in response
What it means
The DingTalk token endpoint returned HTTP 200 with decodable JSON, but the accessToken field was empty. The API succeeded at the transport level yet did not issue a token, so no authenticated API calls can proceed. (The surrounding code falls back to the documented 7200s expiry when expireIn is missing, but an empty token is always fatal.)
Source
Thrown at platform/dingtalk/dingtalk.go:773
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() would
// re-fetch a fresh token, hammering the access-token API.
p.accessToken = tokenResp.AccessToken
expiry := tokenResp.ExpireIn
if expiry <= 0 {
slog.Warn("dingtalk: missing/invalid expireIn in token response, defaulting to 7200s", "got", tokenResp.ExpireIn)
expiry = 7200
}
if expiry > 300 {
expiry -= 300 // 5 minute buffer
}
p.tokenExpiry = time.Now().Add(time.Duration(expiry) * time.Second)
View on GitHub (pinned to 4000b2338a)
Solutions
- Log the full response JSON to see companion error fields DingTalk may include.
- In the DingTalk developer console, confirm the app is published, enabled, and has the required API permissions (robot, media, message).
- Verify you're using the correct app type (enterprise internal app) and its appKey/appSecret.
- Refresh the access token once after fixing permissions, then restart cc-connect.
- Check DingTalk changelog/announcements for recent API behavior changes.
Defensive patterns
Strategy: try-catch
Validate before calling
// startup credential sanity check
if p.clientID == "" || p.clientSecret == "" {
return errors.New("dingtalk: missing appKey/appSecret in config")
} Try / catch
tok, err := p.getAccessToken()
if err != nil {
if strings.Contains(err.Error(), "empty accessToken") {
// do not retry blindly: fix app permissions/publish state first
return fmt.Errorf("dingtalk did not issue a token — check app console: %w", err)
}
return err
} Prevention
- Publish and enable the app in the DingTalk developer console before production.
- Grant robot and message/media API scopes to the app.
- Use the correct app type (enterprise internal app) for cc-connect.
- Log the full token response to catch DingTalk error envelopes that return 200.
When it happens
Trigger: Any token-requiring call when DingTalk returns 200 with a JSON body lacking a non-empty accessToken — usually an app-credentials or app-status problem surfaced as a success envelope, or a credential pair that authenticates but has no robot/API scope granted.
Common situations: App created but robot/API permissions not granted in the DingTalk console, app pending review/publish, using a mini-app appKey where a corp app is required, or DingTalk rolling out schema changes.
Understand the failure class
Background: "must not be empty", "cannot be empty" — required-field validation errors across open-source libraries — this error's family across 41 libraries.
Related errors
- api returned status %d: %s
- empty downloadUrl in response
- get access token: %w
- create AI card: status=%d, body=%s
- AI card delivery failed: %s
AI-assisted analysis of chenhg5/cc-connect@4000b2338a (2026-09-06).
Data as JSON: /api/errors/5c133f06fcc16f24.
Report an issue: GitHub.