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

  1. 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.
  2. Verify the /uploads step returned a non-empty token (log urlInfo before the CDN POST) since video/audio rely on that fallback.
  3. Update extractCDNToken in platform/max/max.go if MAX changed the per-kind response shape (photos map vs flat token vs XML).
  4. Re-test with curl posting a small file to the upload URL to see the real CDN response shape for your API version.
  5. 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

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


AI-assisted analysis of chenhg5/cc-connect@4000b2338a (2026-09-06). Data as JSON: /api/errors/ec7d379f520b35c4. Report an issue: GitHub.