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() would

View on GitHub (pinned to 4000b2338a)

Solutions

  1. Read the embedded body in the error — DingTalk's message (e.g. badRequest / appKey invalid) tells you the exact cause.
  2. Verify appKey and appSecret against the DingTalk developer console; update config.toml and restart cc-connect.
  3. Confirm the app is enabled, published, and the robot is active.
  4. Check IP allowlist configuration on the DingTalk app if your server IP changed.
  5. 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

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


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