{"record":{"id":"6854db39b39c6c9e","repo":"chenhg5/cc-connect","slug":"upload-finish-w","errorCode":null,"errorMessage":"upload finish: %w","messagePattern":"upload finish: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"platform/wecom/websocket_outbound_media.go","lineNumber":111,"sourceCode":"\t\t\t\t\"base64_data\": base64.StdEncoding.EncodeToString(data[start:end]),\n\t\t\t},\n\t\t}\n\t\tif _, err := p.writeAndWaitFrameWithTimeout(ctx, chunkFrame, reqID, wsMediaAckTimeout); err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"upload chunk %d: %w\", i, err)\n\t\t}\n\t}\n\n\tfinishReqID := p.generateReqID(\"aibot_upload_media_finish\")\n\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},","sourceCodeStart":93,"sourceCodeEnd":129,"githubUrl":"https://github.com/chenhg5/cc-connect/blob/4000b2338aa6e850c99df54f8b0ed6ed7460b401/platform/wecom/websocket_outbound_media.go#L93-L129","documentation":"After all chunks are sent, uploadWSMedia sends an 'aibot_upload_media_finish' frame and waits for the ack that carries the final media_id. This error wraps a failure of that finish round trip (timeout, closed socket, or server rejection of the finish command). Without a successful finish the uploaded chunks are discarded server-side and no media_id is produced.","triggerScenarios":"Called from SendImage/SendFile via uploadWSMedia when writeAndWaitFrameWithTimeout for the aibot_upload_media_finish frame returns an error.","commonSituations":"WeCom rejects the finish because a chunk failed validation server-side; the WS ack timed out (wsMediaAckTimeout) under load; connection dropped between last chunk and finish; upload_id no longer valid due to server-side session expiry.","solutions":["Inspect the wrapped cause; on timeout retry the entire upload (init → chunks → finish) with a fresh upload_id","Verify the WS connection is still open and reconnect before retrying","Increase wsMediaAckTimeout if finishes regularly time out on large media","Check WeCom AI Bot API status/permissions if finish is consistently rejected"],"exampleFix":null,"handlingStrategy":"retry","validationCode":"if !p.IsConnected() {\n\tif err := p.reconnect(ctx); err != nil {\n\t\treturn err\n\t}\n}","typeGuard":null,"tryCatchPattern":"mediaID, err := p.SendFile(ctx, rc, file)\nif err != nil && strings.Contains(err.Error(), \"upload finish\") {\n\t// whole upload is discarded; retry once end-to-end\n\treturn p.SendFile(ctx, rc, file)\n}","preventionTips":["Treat finish failure as full-upload failure — never reuse the upload_id","Reconnect the WS before retrying finish","Alert on repeated finish timeouts (server-side or quota issue)"],"tags":["websocket","file-upload","network"],"backgroundTag":"request-timeout","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"}