{"record":{"id":"ec7d379f520b35c4","repo":"chenhg5/cc-connect","slug":"cdn-upload-no-token-in-response-s","errorCode":null,"errorMessage":"cdn upload: no token in response: %s","messagePattern":"cdn upload: no token in response: (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"platform/max/max.go","lineNumber":617,"sourceCode":"\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 {\n\tcase \"image\":\n\t\tvar resp struct {\n\t\t\tPhotos map[string]struct {\n\t\t\t\tToken string `json:\"token\"`\n\t\t\t} `json:\"photos\"`\n\t\t}\n\t\tif err := json.Unmarshal(body, &resp); err == nil {\n\t\t\tfor _, ph := range resp.Photos {\n\t\t\t\tif ph.Token != \"\" {\n\t\t\t\t\treturn ph.Token\n\t\t\t\t}\n\t\t\t}","sourceCodeStart":599,"sourceCodeEnd":635,"githubUrl":"https://github.com/chenhg5/cc-connect/blob/4000b2338aa6e850c99df54f8b0ed6ed7460b401/platform/max/max.go#L599-L635","documentation":"Final fallback of uploadAttachment: the CDN answered 200, extractCDNToken found no token in the body for the given kind, and the urlInfo.Token obtained from /uploads was also empty — so no attachment token is available to embed in the /messages payload. The response body is included in the error to diagnose which shape the CDN actually returned.","triggerScenarios":"SendImage when the CDN response is not the expected {\"photos\":{\"<id>\":{\"token\":\"...\"}}} JSON (e.g. empty body, HTML error page); SendFile when the {\"token\":\"...\"} field is absent; SendAudio/SendVideo when the XML \"<retval>1</retval>\" shape arrives but step 1 returned no token; or a MAX schema change breaking the documented per-kind shapes.","commonSituations":"MAX CDN serving an error page with 200 status; API version drift changing response shapes; uploads through a proxy that rewrites the response; mocking/stub servers returning bodies that don't match real CDN shapes; video/audio uploads where /uploads stopped returning the token.","solutions":["Inspect the body embedded in the error: an HTML page means the CDN rejected the upload despite 200 — fix the underlying request; an unexpected JSON shape means the parser needs updating.","Verify the /uploads step returned a non-empty token (log urlInfo before the CDN POST) since video/audio rely on that fallback.","Update extractCDNToken in platform/max/max.go if MAX changed the per-kind response shape (photos map vs flat token vs XML).","Re-test with curl posting a small file to the upload URL to see the real CDN response shape for your API version.","Ensure the kind passed to uploadAttachment matches what was requested in /uploads?type= — a mismatched kind makes the parser look in the wrong place."],"exampleFix":"// before\nif urlInfo.Token != \"\" {\n\treturn urlInfo.Token, nil\n}\nreturn \"\", fmt.Errorf(\"cdn upload: no token in response: %s\", cdnBody)\n// after: tolerate alternate shapes, e.g. top-level photo token list\nif urlInfo.Token != \"\" {\n\treturn urlInfo.Token, nil\n}\nvar alt struct {\n\tToken string `json:\"token\"`\n}\nif json.Unmarshal(cdnBody, &alt) == nil && alt.Token != \"\" {\n\treturn alt.Token, nil\n}\nreturn \"\", fmt.Errorf(\"cdn upload: no token in response: %s\", cdnBody)","handlingStrategy":"fallback","validationCode":"// Confirm step 1 produced a fallback token before doing the CDN POST\nif urlInfo.Token == \"\" && (kind == \"video\" || kind == \"audio\") {\n\treturn fmt.Errorf(\"max: /uploads returned no token for %s; video/audio rely on it\", kind)\n}","typeGuard":"func cdnTokenShapeKnown(kind string, body []byte) bool {\n\tswitch kind {\n\tcase \"image\":\n\t\treturn bytes.Contains(body, []byte(\"\\\"photos\\\"\"))\n\tcase \"file\":\n\t\treturn bytes.Contains(body, []byte(\"\\\"token\\\"\"))\n\tcase \"video\", \"audio\":\n\t\treturn true // token comes from urlInfo\n\t}\n\treturn false\n}","tryCatchPattern":"token, err := p.uploadAttachment(ctx, kind, data, filename)\nif err != nil {\n\tif strings.Contains(err.Error(), \"no token in response:\") {\n\t\tslog.Error(\"max: cdn token extraction failed\", \"kind\", kind, \"err\", err)\n\t\t// degrade gracefully: send the message without the attachment\n\t\treturn p.SendMessage(ctx, replyCtx, text+\"\\n(attachment failed)\")\n\t}\n\treturn err\n}","preventionTips":["Log the CDN response body on every token-extraction failure to catch API shape drift early.","Add a regression test per kind (image/file/video/audio) pinning the expected CDN response shape.","Always verify /uploads returned a token before video/audio uploads.","Watch MAX API changelogs; update extractCDNToken when shapes change."],"tags":["api","upload","parsing","max"],"backgroundTag":"unexpected-response-shape","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"}