{"record":{"id":"5c133f06fcc16f24","repo":"chenhg5/cc-connect","slug":"empty-accesstoken-in-response","errorCode":null,"errorMessage":"empty accessToken in response","messagePattern":"empty accessToken in response","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"critical","filePath":"platform/dingtalk/dingtalk.go","lineNumber":773,"sourceCode":"\t\treturn \"\", fmt.Errorf(\"do request: %w\", err)\n\t}\n\tdefer func() { _ = resp.Body.Close() }()\n\n\tif resp.StatusCode != http.StatusOK {\n\t\tbody, _ := io.ReadAll(resp.Body)\n\t\treturn \"\", fmt.Errorf(\"api returned status %d: %s\", resp.StatusCode, body)\n\t}\n\n\tvar tokenResp struct {\n\t\tAccessToken string `json:\"accessToken\"`\n\t\tExpireIn    int    `json:\"expireIn\"`\n\t}\n\tif err := json.NewDecoder(resp.Body).Decode(&tokenResp); err != nil {\n\t\treturn \"\", fmt.Errorf(\"decode response: %w\", err)\n\t}\n\n\tif tokenResp.AccessToken == \"\" {\n\t\treturn \"\", fmt.Errorf(\"empty accessToken in response\")\n\t}\n\n\t// Cache token with 5 minutes buffer before expiry.\n\t// When the server omits expireIn (or sends 0/negative), fall back to the\n\t// documented DingTalk default (7200s = 2h) — without this, tokenExpiry\n\t// would land at time.Now() and every subsequent getAccessToken() would\n\t// re-fetch a fresh token, hammering the access-token API.\n\tp.accessToken = tokenResp.AccessToken\n\texpiry := tokenResp.ExpireIn\n\tif expiry <= 0 {\n\t\tslog.Warn(\"dingtalk: missing/invalid expireIn in token response, defaulting to 7200s\", \"got\", tokenResp.ExpireIn)\n\t\texpiry = 7200\n\t}\n\tif expiry > 300 {\n\t\texpiry -= 300 // 5 minute buffer\n\t}\n\tp.tokenExpiry = time.Now().Add(time.Duration(expiry) * time.Second)\n","sourceCodeStart":755,"sourceCodeEnd":791,"githubUrl":"https://github.com/chenhg5/cc-connect/blob/4000b2338aa6e850c99df54f8b0ed6ed7460b401/platform/dingtalk/dingtalk.go#L755-L791","documentation":"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.)","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":"// startup credential sanity check\nif p.clientID == \"\" || p.clientSecret == \"\" {\n    return errors.New(\"dingtalk: missing appKey/appSecret in config\")\n}","typeGuard":null,"tryCatchPattern":"tok, err := p.getAccessToken()\nif err != nil {\n    if strings.Contains(err.Error(), \"empty accessToken\") {\n        // do not retry blindly: fix app permissions/publish state first\n        return fmt.Errorf(\"dingtalk did not issue a token — check app console: %w\", err)\n    }\n    return err\n}","preventionTips":["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."],"tags":["auth","dingtalk","empty-response","api","credentials"],"backgroundTag":"empty-required-field","analyzedSha":"4000b2338aa6e850c99df54f8b0ed6ed7460b401","analyzedAt":"2026-09-06T11:45:09.575Z","contentChangedAt":"2026-09-06T11:45:09.575Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}