{"record":{"id":"6d9df7891b12804f","repo":"chenhg5/cc-connect","slug":"upload-api-error-d-s","errorCode":null,"errorMessage":"upload API error %d: %s","messagePattern":"upload API error (.+?): (.+?)","errorType":"error_code","errorClass":null,"httpStatus":null,"severity":"error","filePath":"platform/dingtalk/dingtalk.go","lineNumber":1398,"sourceCode":"\n\tif resp.StatusCode != http.StatusOK {\n\t\treturn \"\", fmt.Errorf(\"upload returned status %d: %s\", resp.StatusCode, respBody)\n\t}\n\n\tslog.Debug(\"dingtalk: media upload response\", \"status\", resp.StatusCode, \"body\", string(respBody))\n\n\tvar uploadResp struct {\n\t\tErrCode int    `json:\"errcode\"`\n\t\tErrMsg  string `json:\"errmsg\"`\n\t\tMediaID string `json:\"media_id\"`\n\t\tType    string `json:\"type\"`\n\t}\n\tif err := json.Unmarshal(respBody, &uploadResp); err != nil {\n\t\treturn \"\", fmt.Errorf(\"decode upload response: %w, body: %s\", err, respBody)\n\t}\n\n\tif uploadResp.ErrCode != 0 {\n\t\treturn \"\", fmt.Errorf(\"upload API error %d: %s\", uploadResp.ErrCode, uploadResp.ErrMsg)\n\t}\n\n\tif uploadResp.MediaID == \"\" {\n\t\treturn \"\", fmt.Errorf(\"empty media_id in upload response: %s\", respBody)\n\t}\n\n\tslog.Debug(\"dingtalk: media uploaded successfully\", \"media_id\", uploadResp.MediaID, \"type\", mediaType, \"size\", len(data))\n\treturn uploadResp.MediaID, nil\n}\n\nfunc (p *Platform) Stop() error {\n\tif p.streamCtxCancel != nil {\n\t\tp.streamCtxCancel()\n\t}\n\tif p.streamClient != nil {\n\t\tp.streamClient.Close()\n\t}\n\treturn nil","sourceCodeStart":1380,"sourceCodeEnd":1416,"githubUrl":"https://github.com/chenhg5/cc-connect/blob/4000b2338aa6e850c99df54f8b0ed6ed7460b401/platform/dingtalk/dingtalk.go#L1380-L1416","documentation":"DingTalk's media upload API returned HTTP 200, but the JSON envelope carries a business-level errcode != 0 with an errmsg describing the failure. This is the standard DingTalk convention: transport succeeded, but the API rejected the operation. The error surfaces errcode and errmsg verbatim.","triggerScenarios":"Uploading media with an invalid or unsupported 'type' value; exceeding DingTalk's media size limits (e.g. image > 2MB, file > 20MB, voice > 2MB or wrong format); access_token invalid at the business layer; app lacking upload permission.","commonSituations":"Sending files larger than DingTalk's per-type limits; wrong Content-Type for voice (must be amr MP3-era formats per docs); expired/revoked access_token returning errcode 40001/40014; using credentials from the wrong app in the developer console.","solutions":["Read errcode/errmsg in the message and look it up in DingTalk's global error code table (e.g. 40001 = invalid credential, 40014 = invalid access_token).","If the code indicates a token problem, clear the cached token and call getAccessToken again before retrying.","Check media size/format against DingTalk limits: image ≤2MB, voice ≤2MB (amr), video ≤20MB, file ≤20MB.","Verify the mediaType parameter is exactly one of image/voice/video/file.","Confirm the app's permissions in the DingTalk developer console include media upload for the target bot."],"exampleFix":"// before\nif uploadResp.ErrCode != 0 {\n    return \"\", fmt.Errorf(\"upload API error %d: %s\", uploadResp.ErrCode, uploadResp.ErrMsg)\n}\n// after\nif uploadResp.ErrCode == 40001 || uploadResp.ErrCode == 40014 {\n    p.invalidateToken()\n    return \"\", fmt.Errorf(\"upload API error %d (token invalid, refresh needed): %s\", uploadResp.ErrCode, uploadResp.ErrMsg)\n}\nif uploadResp.ErrCode != 0 {\n    return \"\", fmt.Errorf(\"upload API error %d: %s\", uploadResp.ErrCode, uploadResp.ErrMsg)\n}","handlingStrategy":"try-catch","validationCode":"// pre-validate what errcode commonly rejects\nif int64(len(data)) > 20<<20 {\n    return fmt.Errorf(\"media exceeds DingTalk 20MB limit\")\n}\nif mediaType == \"voice\" && !strings.HasSuffix(filename, \".amr\") {\n    return fmt.Errorf(\"voice media must be amr format\")\n}","typeGuard":"func isTokenErrCode(code int) bool {\n    return code == 40001 || code == 40014 || code == 42001 // invalid/expired credential codes\n}","tryCatchPattern":"mediaID, err := uploadMedia(ctx, data, mediaType)\nif err != nil {\n    if isTokenErrCode(extractErrCode(err)) {\n        p.invalidateToken()\n        mediaID, err = uploadMedia(ctx, data, mediaType) // retry with fresh token\n    }\n    if err != nil {\n        return err\n    }\n}","preventionTips":["Keep a mapping of common DingTalk errcodes and their fixes next to your runbook.","Pre-check media size/format against DingTalk per-type limits.","Proactively refresh tokens before the 2-hour expiry window closes.","Confirm app permissions in the developer console after any app changes."],"tags":["api","upload","dingtalk"],"backgroundTag":"api-error-response","analyzedSha":"4000b2338aa6e850c99df54f8b0ed6ed7460b401","analyzedAt":"2026-09-06T11:45:09.575Z","contentChangedAt":"2026-09-06T11:45:09.575Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}