{"record":{"id":"0265ba24cb01ff03","repo":"chenhg5/cc-connect","slug":"cdn-upload-http-d-s","errorCode":null,"errorMessage":"cdn upload: HTTP %d: %s","messagePattern":"cdn upload: HTTP (.+?): (.+?)","errorType":"http","errorClass":null,"httpStatus":null,"severity":"error","filePath":"platform/max/max.go","lineNumber":601,"sourceCode":"\tif err := mw.Close(); err != nil {\n\t\treturn \"\", err\n\t}\n\n\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","sourceCodeStart":583,"sourceCodeEnd":619,"githubUrl":"https://github.com/chenhg5/cc-connect/blob/4000b2338aa6e850c99df54f8b0ed6ed7460b401/platform/max/max.go#L583-L619","documentation":"The MAX CDN returned a non-200 HTTP status to the multipart upload POST. The platform reads up to 512 bytes of the response body and reports the status code plus a truncated body so the developer can see the CDN's own error message. This means the upload request reached the CDN and was rejected (auth, payload, or URL problems), not a transport failure.","triggerScenarios":"SendImage/SendFile/SendAudio where the CDN answers e.g. 401/403 (setAuth token invalid or expired), 400 (malformed multipart, wrong Content-Type, missing \"data\" field), 404 (the presigned URL expired between step 1 and step 2), or 413 (payload too large).","commonSituations":"Delaying too long between obtaining the upload URL and POSTing the file (presigned URL expiry); uploading files exceeding the CDN's per-kind size cap; revoked bot tokens; sending a kind mismatched with the URL (e.g. image bytes to a file-type URL).","solutions":["Read the truncated body in the error — it contains the CDN's specific reason (auth vs. size vs. bad request).","Minimize the delay between the /uploads call and the CDN POST; regenerate the upload URL if more than a few minutes elapsed.","Verify the bot token is still valid and has upload permissions; re-test step 1 alone with curl.","Check the file size against MAX's attachment limits per kind and compress or truncate as needed.","Ensure the multipart field name is exactly \"data\" and Content-Type comes from mw.FormDataContentType() — deviations cause 400s."],"exampleFix":"// before\nurlInfo, err := getUploadURL(...)\n// ... long processing ...\ntoken, err := p.uploadAttachment(ctx, kind, data, filename) // URL may be stale\n// after: fetch upload URL and POST back-to-back\nurlInfo, err := requestUploadURL(ctx, kind)\nif err != nil {\n\treturn fmt.Errorf(\"max: get upload url: %w\", err)\n}\ntoken, err := p.uploadAttachment(ctx, kind, data, filename)\nif err != nil {\n\treturn fmt.Errorf(\"max: upload %s: %w\", kind, err)\n}","handlingStrategy":"try-catch","validationCode":"// Pre-check size against a conservative MAX attachment cap before uploading\nconst maxAttachmentBytes = 30 << 20\nif len(data) > maxAttachmentBytes {\n\treturn fmt.Errorf(\"attachment %s too large: %d > %d bytes\", filename, len(data), maxAttachmentBytes)\n}","typeGuard":null,"tryCatchPattern":"token, err := p.uploadAttachment(ctx, kind, data, filename)\nif err != nil {\n\tvar statusErr interface{ HTTPStatus() int }\n\tif strings.Contains(err.Error(), \"cdn upload: HTTP \") {\n\t\t// parse status; regenerate upload URL and retry once on 4xx\n\t\tif newURL, uerr := requestUploadURL(ctx, kind); uerr == nil {\n\t\t\t_ = newURL\n\t\t}\n\t}\n}","preventionTips":["Upload immediately after requesting the upload URL — presigned URLs expire.","Respect per-kind size limits; compress images before sending.","Keep bot tokens valid; most CDN 401/403s trace back to expired credentials.","Keep the multipart field name \"data\" and use mw.FormDataContentType() exactly."],"tags":["http","upload","max","api"],"backgroundTag":"http-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"}