{"record":{"id":"42baabb94a68d08d","repo":"chenhg5/cc-connect","slug":"max-upload-audio-w","errorCode":null,"errorMessage":"max: upload audio: %w","messagePattern":"max: upload audio: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"platform/max/max.go","lineNumber":482,"sourceCode":"\t\t}},\n\t}\n\treturn p.postMessage(ctx, rctx.chatID, body)\n}\n\n// SendAudio implements core.AudioSender — uploads a voice/audio blob and sends\n// it as a native MAX audio attachment. Used by the TTS pipeline to reply in\n// voice when [tts] is enabled in config.\nfunc (p *Platform) SendAudio(ctx context.Context, replyCtx any, audio []byte, format string) error {\n\trctx, ok := replyCtx.(replyContext)\n\tif !ok {\n\t\treturn fmt.Errorf(\"max: unexpected replyCtx type %T\", replyCtx)\n\t}\n\tif format == \"\" {\n\t\tformat = \"mp3\"\n\t}\n\ttoken, err := p.uploadAttachment(ctx, \"audio\", audio, \"voice.\"+format)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"max: upload audio: %w\", err)\n\t}\n\tbody := &maxSendBody{\n\t\tAttachments: []maxOutAttachment{{\n\t\t\tType:    \"audio\",\n\t\t\tPayload: maxTokenPayload{Token: token},\n\t\t}},\n\t}\n\treturn p.postMessage(ctx, rctx.chatID, body)\n}\n\n// UpdateMessage implements core.MessageUpdater via PUT /messages?message_id=.\nfunc (p *Platform) UpdateMessage(ctx context.Context, replyCtx any, content string) error {\n\trctx, ok := replyCtx.(replyContext)\n\tif !ok {\n\t\treturn fmt.Errorf(\"max: unexpected replyCtx type %T\", replyCtx)\n\t}\n\tif rctx.messageID == \"\" {\n\t\treturn fmt.Errorf(\"max: update message: no message id in reply context\")","sourceCodeStart":464,"sourceCodeEnd":500,"githubUrl":"https://github.com/chenhg5/cc-connect/blob/4000b2338aa6e850c99df54f8b0ed6ed7460b401/platform/max/max.go#L464-L500","documentation":"SendAudio first uploads the audio blob to MAX via uploadAttachment — a two-step process (POST /uploads?type=audio to get an upload URL, then multipart POST of the bytes) — and wraps any failure with \"max: upload audio: %w\". It means the audio never reached MAX's storage, so no message was sent. The underlying error carries the real cause: an HTTP error from the uploads endpoint, a network/timeout failure, or empty audio data.","triggerScenarios":"Calling SendAudio when: (1) the 5-minute upload context or the dedicated uploadClient timeout expires on a large audio file; (2) MAX's /uploads endpoint returns a non-200 status (auth token invalid/expired, unsupported type); (3) the network to botapi.max.ru is down or a proxy blocks the request; (4) the audio slice is empty (uploadAttachment returns \"empty attachment data\").","commonSituations":"TTS pipeline producing large or malformed audio that exceeds upload timeouts on slow links; expired or misconfigured MAX bot access token in config.toml; corporate proxy/firewall blocking the upload CDN host; a TTS engine returning zero bytes on synthesis failure which then fails the upload with the empty-data error.","solutions":["Inspect the wrapped cause with errors.Unwrap / %v of the returned error to see whether it was a timeout, an HTTP status, or empty data, and fix that root cause first.","Verify the MAX bot token in config.toml is valid — an auth failure surfaces here as a non-200 from /uploads.","Check network/proxy access to the MAX API host from the machine running cc-connect; retry transient failures.","Guard the input: check len(audio) > 0 and that the TTS step succeeded before calling SendAudio.","For large files, ensure the context passed to SendAudio has enough headroom (the platform allows up to 5 minutes)."],"exampleFix":"// before\nif err := p.SendAudio(ctx, rctx, ttsOut, \"opus\"); err != nil {\n    slog.Error(\"tts send failed\", \"err\", err)\n}\n\n// after\nif len(ttsOut) == 0 {\n    return fmt.Errorf(\"tts produced no audio\")\n}\nctx, cancel := context.WithTimeout(ctx, 5*time.Minute)\ndefer cancel()\nif err := p.SendAudio(ctx, rctx, ttsOut, \"opus\"); err != nil {\n    var httpErr interface{ HTTPCode() int }\n    if errors.As(err, &httpErr) {\n        slog.Error(\"max upload rejected\", \"err\", err) // auth/API problem\n    } else if errors.Is(err, context.DeadlineExceeded) {\n        slog.Error(\"audio upload timed out\", \"bytes\", len(ttsOut))\n    }\n    return err\n}","handlingStrategy":"try-catch","validationCode":"if len(audio) == 0 {\n    return fmt.Errorf(\"no audio data to send\")\n}\nif format == \"\" { format = \"mp3\" } // mirror the platform default","typeGuard":null,"tryCatchPattern":"err := platform.SendAudio(ctx, replyCtx, audio, format)\nif err != nil {\n    switch {\n    case errors.Is(err, context.DeadlineExceeded):\n        // retry with a fresh, longer-lived context\n    case strings.Contains(err.Error(), \"HTTP \"):\n        // API rejected the upload: check token/config\n    case strings.Contains(err.Error(), \"empty attachment data\"):\n        // caller bug: check TTS output\n    }\n    return fmt.Errorf(\"send audio: %w\", err)\n}","preventionTips":["Validate audio bytes are non-empty before calling SendAudio.","Keep the bot access token valid and rotated per config; auth failures surface at upload time.","Allow generous context deadlines (platform allows up to 5 min) for large audio files.","Verify outbound network access to the MAX API/upload hosts at deployment time (a doctor/health check).","Retry transient network failures with backoff; uploads are safe to re-attempt (no message sent until postMessage succeeds)."],"tags":["go","network","file-upload","max-platform","audio-sending"],"backgroundTag":"api-request-failed","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"}