chenhg5/cc-connect · error
cdn upload: no token in response: %s
Error message
cdn upload: no token in response: %s
What it means
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.
Source
Thrown at platform/max/max.go:617
if cdnResp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(io.LimitReader(cdnResp.Body, 512))
return "", fmt.Errorf("cdn upload: HTTP %d: %s", cdnResp.StatusCode, body)
}
cdnBody, err := io.ReadAll(io.LimitReader(cdnResp.Body, 64*1024))
if err != nil {
return "", fmt.Errorf("read cdn response: %w", err)
}
// MAX CDN uses different response shapes per attachment kind:
// image: {"photos": {"<photo_id>": {"token": "..."}}}
// file: {"token": "..."}
// video/audio: "<retval>1</retval>" (XML) — the real token is already in urlInfo.Token
if token := extractCDNToken(kind, cdnBody); token != "" {
return token, nil
}
if urlInfo.Token != "" {
return urlInfo.Token, nil
}
return "", fmt.Errorf("cdn upload: no token in response: %s", cdnBody)
}
// extractCDNToken parses the token out of a MAX CDN upload response. Returns
// "" if not found; the caller is expected to fall back to urlInfo.Token.
func extractCDNToken(kind string, body []byte) string {
switch kind {
case "image":
var resp struct {
Photos map[string]struct {
Token string `json:"token"`
} `json:"photos"`
}
if err := json.Unmarshal(body, &resp); err == nil {
for _, ph := range resp.Photos {
if ph.Token != "" {
return ph.Token
}
}View on GitHub (pinned to 4000b2338a)
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.
Example fix
// before
if urlInfo.Token != "" {
return urlInfo.Token, nil
}
return "", fmt.Errorf("cdn upload: no token in response: %s", cdnBody)
// after: tolerate alternate shapes, e.g. top-level photo token list
if urlInfo.Token != "" {
return urlInfo.Token, nil
}
var alt struct {
Token string `json:"token"`
}
if json.Unmarshal(cdnBody, &alt) == nil && alt.Token != "" {
return alt.Token, nil
}
return "", fmt.Errorf("cdn upload: no token in response: %s", cdnBody) Defensive patterns
Strategy: fallback
Validate before calling
// Confirm step 1 produced a fallback token before doing the CDN POST
if urlInfo.Token == "" && (kind == "video" || kind == "audio") {
return fmt.Errorf("max: /uploads returned no token for %s; video/audio rely on it", kind)
} Type guard
func cdnTokenShapeKnown(kind string, body []byte) bool {
switch kind {
case "image":
return bytes.Contains(body, []byte("\"photos\""))
case "file":
return bytes.Contains(body, []byte("\"token\""))
case "video", "audio":
return true // token comes from urlInfo
}
return false
} Try / catch
token, err := p.uploadAttachment(ctx, kind, data, filename)
if err != nil {
if strings.Contains(err.Error(), "no token in response:") {
slog.Error("max: cdn token extraction failed", "kind", kind, "err", err)
// degrade gracefully: send the message without the attachment
return p.SendMessage(ctx, replyCtx, text+"\n(attachment failed)")
}
return err
} Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- upload url: HTTP %d: %s
- upload url: empty url in response
- cdn upload: HTTP %d: %s
- upload returned status %d: %s
- decode upload response: %w, body: %s
AI-assisted analysis of chenhg5/cc-connect@4000b2338a (2026-09-06).
Data as JSON: /api/errors/ec7d379f520b35c4.
Report an issue: GitHub.