Skip to content

attachments download returns preview rendition on Basecamp 5 accounts — SDK URL rewrite missing the storage.app.basecamp.com host #491

@wofer357

Description

@wofer357

Summary

On Basecamp 5 accounts, basecamp attachments download <id> --type comment (and <recording> show --download-attachments=<dir>) saves the preview rendition instead of the original blob. Same on the SDK layer for any integration that uses richtext.ExtractAttachments / the SDK's URL rewriter.

This is a regression of #290 / PR #360 caused by Basecamp 5's storage host change.

Reproduction

CLI: basecamp version 0.7.2 (latest).
Account: any Basecamp 5 account with an inline image attachment ≥ 1500×1875 px on a comment.

basecamp attachments download <comment-id> --type comment --account <id> --project <id> --out /tmp/x
sips -g pixelWidth -g pixelHeight /tmp/x/*.png

Expected: files at upload dimensions (e.g. 1500×1875 for a 4:5 static).

Actual: files at preview dimensions (e.g. 480×600), nondeterministically variable across runs (one run yields 338×600, next yields 1080×1920 for the same blob — preview server returns variable-size renditions).

Root cause

The <bc-attachment> element in Basecamp 5 rich text contains both URLs:

<bc-attachment
  sgid="..."
  content-type="image/png"
  url="https://preview.app.basecamp.com/<acct>/blobs/<uuid>/previews/full"
  href="https://storage.app.basecamp.com/<acct>/blobs/<uuid>/download/<filename>"
  filename="image.png"
  filesize="1974425"
  width="1500" height="1875">

The CLI's COMMUNIQUE (COMMUNIQUE-inline-attachments-api.md) documents the intended flow:

"The href values in <bc-attachment> tags are storage URLs (https://storage.3.basecamp.com/...). The SDK rewrites these through the API host for auth, then follows a redirect to a signed S3 URL."

The SDK's URL rewrite table handles storage.3.basecamp.com (BC3-era host) but not Basecamp 5's storage.app.basecamp.com. When the rewrite fails to recognize the storage host, the downloader falls back to the url attribute (preview) instead of href (original).

Empirical confirmation

After manually rewriting storage.app.basecamp.com3.basecampapi.com (the API host the SDK would have rewritten to), Bearer auth works:

TOKEN=$(basecamp auth token --account <acct>)
curl -sL -H "Authorization: Bearer $TOKEN" \
  "https://3.basecampapi.com/<acct>/blobs/<uuid>/download/<filename>" \
  -o /tmp/orig.png -w "%{http_code} %{content_type} %{size_download}\n"
# → 200 image/png 1974425  (exactly matches bc-attachment filesize)

sips -g pixelWidth -g pixelHeight /tmp/orig.png
# → 1500 / 1875  (exactly matches bc-attachment width/height)

So the originals are reachable via Bearer auth on the API host — the only missing piece is the SDK's URL rewriter recognizing the new storage host.

Proposed fix

Add storage.app.basecamp.com to the SDK's URL rewrite table alongside storage.3.basecamp.com. Likely in basecamp-sdk somewhere near the existing host mapping. Two-line change.

If preview-vs-original is also a design intent we want to expose, an --original / --rendition <full|preview> flag on attachments download would be a clean follow-up — but the immediate regression fix is just the host map.

Workaround for downstream users

Until the SDK ships the fix, a small wrapper that does the URL rewrite + Bearer curl works around the issue. Happy to share the script if helpful.

Environment

  • basecamp version 0.7.2 (from curl -fsSL https://basecamp.com/install-cli | bash, March 2026)
  • macOS 14 (Sonoma); same behavior reported by other developers on a separate platform (see n8n community thread linking storage-CDN auth as known limitation)
  • BC5 account on app.basecamp.com

References

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions