{"record":{"id":"aaacfb913efc4b93","repo":"chenhg5/cc-connect","slug":"upload-finish-empty-media-id","errorCode":null,"errorMessage":"upload finish: empty media_id","messagePattern":"upload finish: empty media_id","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"platform/wecom/websocket_outbound_media.go","lineNumber":120,"sourceCode":"\tfinishFrame := map[string]any{\n\t\t\"cmd\":     \"aibot_upload_media_finish\",\n\t\t\"headers\": map[string]string{\"req_id\": finishReqID},\n\t\t\"body\": map[string]any{\n\t\t\t\"upload_id\": initBody.UploadID,\n\t\t},\n\t}\n\tfinishResp, err := p.writeAndWaitFrameWithTimeout(ctx, finishFrame, finishReqID, wsMediaAckTimeout)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"upload finish: %w\", err)\n\t}\n\tvar finishBody struct {\n\t\tMediaID string `json:\"media_id\"`\n\t}\n\tif err := json.Unmarshal(finishResp.Body, &finishBody); err != nil {\n\t\treturn \"\", fmt.Errorf(\"decode upload finish response: %w\", err)\n\t}\n\tif finishBody.MediaID == \"\" {\n\t\treturn \"\", fmt.Errorf(\"upload finish: empty media_id\")\n\t}\n\treturn finishBody.MediaID, nil\n}\n\nfunc (p *WSPlatform) sendWSMediaMessage(ctx context.Context, chatID, mediaType, mediaID string) error {\n\treqID := p.generateReqID(\"aibot_send_msg\")\n\tframe := map[string]any{\n\t\t\"cmd\":     \"aibot_send_msg\",\n\t\t\"headers\": map[string]string{\"req_id\": reqID},\n\t\t\"body\": map[string]any{\n\t\t\t\"chatid\":  chatID,\n\t\t\t\"msgtype\": mediaType,\n\t\t\tmediaType: map[string]string{\n\t\t\t\t\"media_id\": mediaID,\n\t\t\t},\n\t\t},\n\t}\n\treturn p.writeAndWaitAckStrict(ctx, frame, reqID, wsMediaAckTimeout)","sourceCodeStart":102,"sourceCodeEnd":138,"githubUrl":"https://github.com/chenhg5/cc-connect/blob/4000b2338aa6e850c99df54f8b0ed6ed7460b401/platform/wecom/websocket_outbound_media.go#L102-L138","documentation":"The finish ack decoded successfully but contained an empty media_id, meaning WeCom accepted the finish command yet did not return the identifier needed to send the media message. This is treated as a failed upload because sendWSMediaMessage cannot proceed without a media_id.","triggerScenarios":"Called from SendImage/SendFile via uploadWSMedia when finishBody.MediaID == \"\" after successful JSON decoding of the finish ack.","commonSituations":"WeCom server-side processing not complete when finish acked (async finalization); the upload_id referenced no valid chunks; API schema change moved media_id into a nested field so it decodes to empty.","solutions":["Dump the raw finish response body to check if media_id moved to a nested field and update the struct tags","Retry the upload once — a transient race where finalization is not complete can yield an empty id","Verify all chunk acks succeeded before finish; a rejected chunk can still produce an empty media_id","Check WeCom AI Bot API changelog for response schema changes"],"exampleFix":"// before\nvar finishBody struct {\n\tMediaID string `json:\"media_id\"`\n}\n// after\nvar finishBody struct {\n\tMediaID string `json:\"media_id\"`\n\tData    struct {\n\t\tMediaID string `json:\"media_id\"`\n\t} `json:\"data\"`\n}\nfunc (b finishBodyType) id() string { ... } // pick b.MediaID or b.Data.MediaID","handlingStrategy":"retry","validationCode":null,"typeGuard":null,"tryCatchPattern":"if err := p.SendFile(ctx, rc, file); err != nil && strings.Contains(err.Error(), \"empty media_id\") {\n\t// one retry; if it persists, log the raw finish body for schema drift\n}","preventionTips":["Verify every chunk ack before sending finish","Log the raw finish ack body to catch schema drift early","Retry once for transient empty responses"],"tags":["websocket","file-upload","empty-response","api-response"],"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-14T05:17:10.506Z"}