{"record":{"id":"202023ba8e39762f","repo":"chenhg5/cc-connect","slug":"empty-downloadurl-in-response","errorCode":null,"errorMessage":"empty downloadUrl in response","messagePattern":"empty downloadUrl in response","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"platform/dingtalk/dingtalk.go","lineNumber":717,"sourceCode":"\treq.Header.Set(\"x-acs-dingtalk-access-token\", token)\n\n\tresp, err := p.httpClient.Do(req)\n\tif err != nil {\n\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\treturn \"\", fmt.Errorf(\"api returned status %d\", resp.StatusCode)\n\t}\n\n\tvar result downloadResponse\n\tif err := json.NewDecoder(resp.Body).Decode(&result); err != nil {\n\t\treturn \"\", fmt.Errorf(\"decode response: %w\", err)\n\t}\n\n\tif result.DownloadUrl == \"\" {\n\t\treturn \"\", fmt.Errorf(\"empty downloadUrl in response\")\n\t}\n\n\treturn result.DownloadUrl, nil\n}\n\nfunc (p *Platform) getAccessToken() (string, error) {\n\tp.tokenMu.Lock()\n\tdefer p.tokenMu.Unlock()\n\n\t// Return cached token if still valid\n\tif p.accessToken != \"\" && time.Now().Before(p.tokenExpiry) {\n\t\treturn p.accessToken, nil\n\t}\n\n\t// Request new access token using DingTalk's new API (api.dingtalk.com/v1.0/oauth2/accessToken)\n\t// This requires POST request with JSON body\n\turl := \"https://api.dingtalk.com/v1.0/oauth2/accessToken\"\n","sourceCodeStart":699,"sourceCodeEnd":735,"githubUrl":"https://github.com/chenhg5/cc-connect/blob/4000b2338aa6e850c99df54f8b0ed6ed7460b401/platform/dingtalk/dingtalk.go#L699-L735","documentation":"getDownloadURL got a valid, decodable 200 response from the DingTalk media download API but the downloadUrl field is empty. The API accepted the request yet produced no usable URL, so there is nothing to fetch.","triggerScenarios":"handleImageMessage / handleFileMessage / downloadAudio receive a 200 response whose JSON body lacks a non-empty downloadUrl — e.g. the downloadCode is semantically invalid (expired/consumed) while the API still returns 200, or the media was deleted from DingTalk servers.","commonSituations":"Re-processing an old queued message whose media code expired, a DingTalk API change in field naming/casing, or the bot app lacking permission so the API returns a success envelope without a URL.","solutions":["Ask the user to resend the image/file — the downloadCode is most likely expired or already consumed.","Log the full response JSON to confirm the exact field names returned by your API version.","Check the app's media download scope/permission in the DingTalk developer console.","Upgrade cc-connect / verify against current DingTalk API docs in case the field was renamed.","Treat this as retryable once after refreshing the access token, then fail the message."],"exampleFix":null,"handlingStrategy":"fallback","validationCode":"if time.Since(mediaCodeReceivedAt) > 10*time.Minute {\n    // skip calling the API; ask user to resend\n}","typeGuard":null,"tryCatchPattern":"url, err := p.getDownloadURL(ctx, code)\nif err != nil {\n    if strings.Contains(err.Error(), \"empty downloadUrl\") {\n        return p.replyText(ctx, \"The media has expired, please resend it.\")\n    }\n    return err\n}","preventionTips":["Process media promptly — stale codes yield empty URLs.","Grant the app media-download permissions before deploying.","Alert on this error pattern; it usually signals expired media or permission gaps.","Provide a user-facing resend prompt as the fallback path."],"tags":["api","dingtalk","empty-response","media"],"backgroundTag":"empty-api-response","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"}