{"record":{"id":"5fd00d0c2bfe9aaa","repo":"chenhg5/cc-connect","slug":"read-cdn-response-w","errorCode":null,"errorMessage":"read cdn response: %w","messagePattern":"read cdn response: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"platform/max/max.go","lineNumber":605,"sourceCode":"\tcdnReq, err := http.NewRequestWithContext(uploadCtx, http.MethodPost, urlInfo.URL, &buf)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tp.setAuth(cdnReq)\n\tcdnReq.Header.Set(\"Content-Type\", mw.FormDataContentType())\n\n\tcdnResp, err := p.uploadClient.Do(cdnReq)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"cdn upload: %w\", err)\n\t}\n\tdefer cdnResp.Body.Close()\n\tif cdnResp.StatusCode != http.StatusOK {\n\t\tbody, _ := io.ReadAll(io.LimitReader(cdnResp.Body, 512))\n\t\treturn \"\", fmt.Errorf(\"cdn upload: HTTP %d: %s\", cdnResp.StatusCode, body)\n\t}\n\tcdnBody, err := io.ReadAll(io.LimitReader(cdnResp.Body, 64*1024))\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"read cdn response: %w\", err)\n\t}\n\t// MAX CDN uses different response shapes per attachment kind:\n\t//   image: {\"photos\": {\"<photo_id>\": {\"token\": \"...\"}}}\n\t//   file:  {\"token\": \"...\"}\n\t//   video/audio: \"<retval>1</retval>\" (XML) — the real token is already in urlInfo.Token\n\tif token := extractCDNToken(kind, cdnBody); token != \"\" {\n\t\treturn token, nil\n\t}\n\tif urlInfo.Token != \"\" {\n\t\treturn urlInfo.Token, nil\n\t}\n\treturn \"\", fmt.Errorf(\"cdn upload: no token in response: %s\", cdnBody)\n}\n\n// extractCDNToken parses the token out of a MAX CDN upload response. Returns\n// \"\" if not found; the caller is expected to fall back to urlInfo.Token.\nfunc extractCDNToken(kind string, body []byte) string {\n\tswitch kind {","sourceCodeStart":587,"sourceCodeEnd":623,"githubUrl":"https://github.com/chenhg5/cc-connect/blob/4000b2338aa6e850c99df54f8b0ed6ed7460b401/platform/max/max.go#L587-L623","documentation":"After a 200 from the CDN upload POST, uploadAttachment reads up to 64 KiB of the response body to extract the attachment token. If that read fails (connection reset mid-response, context deadline exceeded while reading, chunked-body corruption), the error is wrapped as \"read cdn response: %w\". The upload itself succeeded server-side, but the token needed for the follow-up /messages attachment could not be retrieved.","triggerScenarios":"SendImage/SendFile/SendAudio where the CDN's 200 response body cannot be fully read within the 5-minute upload context — typically connection reset by the CDN, context cancellation, or a stalled/slow response stream.","commonSituations":"Unstable networks interrupting the response; CDN servers under load returning truncated chunked responses; the 5-minute attachmentUploadTO deadline firing during the read of a very large response (rare, since the body is capped at 64 KiB).","solutions":["Retry the whole upload — since the CDN likely stored the file, a fresh upload replaces it cleanly.","Check network stability to the CDN host (packet loss, MTU issues with VPNs).","Verify the 5-minute context isn't being preemptively canceled by the caller.","If it recurs with one CDN host, log the host and report it; consider pinning IPv4 in the Transport if IPv6 is flaky."],"exampleFix":"// before\nvar token string\nvar err error\ntoken, err = p.uploadAttachment(ctx, kind, data, filename)\n// after: bounded retry for transient read failures\nvar token string\nvar err error\nfor i := 0; i < 2; i++ {\n\ttoken, err = p.uploadAttachment(ctx, kind, data, filename)\n\tif err == nil || !strings.Contains(err.Error(), \"read cdn response\") {\n\t\tbreak\n\t}\n\ttime.Sleep(time.Second)\n}","handlingStrategy":"retry","validationCode":"// Ensure the upload context has enough headroom before starting\nif deadline, ok := ctx.Deadline(); ok && time.Until(deadline) < attachmentUploadTO {\n\treturn fmt.Errorf(\"insufficient context budget for upload: %v left\", time.Until(deadline))\n}","typeGuard":"func isReadFailure(err error) bool {\n\treturn strings.Contains(err.Error(), \"read cdn response:\")\n}","tryCatchPattern":"token, err := p.uploadAttachment(ctx, kind, data, filename)\nif err != nil {\n\tif strings.Contains(err.Error(), \"read cdn response:\") {\n\t\t// retry once: transient stream interruption, full re-upload is idempotent\n\t\ttime.Sleep(time.Second)\n\t\ttoken, err = p.uploadAttachment(ctx, kind, data, filename)\n\t}\n}","preventionTips":["Retry whole uploads on read failures — the CDN state from the failed attempt is harmless.","Monitor network stability (packet loss, MTU) between the bot host and the CDN.","Avoid canceling the session context while an attachment send is in flight.","Keep the 64 KiB response cap in mind; never assume an unbounded response body."],"tags":["network","io","upload","max"],"backgroundTag":"network-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"}