fix(media): load transformed images from storage, not via HTTP request#1549
Conversation
Wrap Astro's image endpoint so EmDash media bytes are read straight from the storage adapter instead of being fetched over HTTP. The stock endpoint fetches the (absolute) media URL to load the source; on Cloudflare that is a self-subrequest that fails behind Access / global_fetch_strictly_public, 404ing from /_image. The wrapped endpoint matches the internal media route by pathname, reads bytes from storage (no fetch), and transforms them with sharp (Node) or the IMAGES binding (Cloudflare); everything else delegates to the stock endpoint unchanged. On by default where the image service is local; opt out with images: false.
🦋 Changeset detectedLatest commit: 743d837 The changes in this PR will be included in the next version bump. This PR includes changesets to release 16 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
Scope checkThis PR changes 753 lines across 16 files. Large PRs are harder to review and more likely to be closed without review. If this scope is intentional, no action needed. A maintainer will review it. If not, please consider splitting this into smaller PRs. See CONTRIBUTING.md for contribution guidelines. |
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ✅ Deployment successful! View logs |
docs | 743d837 | Jun 19 2026, 08:58 PM |
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ✅ Deployment successful! View logs |
emdash-playground | 743d837 | Jun 19 2026, 08:58 PM |
@emdash-cms/admin
@emdash-cms/auth
@emdash-cms/auth-atproto
@emdash-cms/blocks
@emdash-cms/cloudflare
@emdash-cms/contentful-to-portable-text
emdash
create-emdash
@emdash-cms/gutenberg-to-portable-text
@emdash-cms/plugin-cli
@emdash-cms/plugin-types
@emdash-cms/registry-client
@emdash-cms/registry-lexicons
@emdash-cms/sandbox-workerd
@emdash-cms/x402
@emdash-cms/plugin-ai-moderation
@emdash-cms/plugin-atproto
@emdash-cms/plugin-audit-log
@emdash-cms/plugin-color
@emdash-cms/plugin-embeds
@emdash-cms/plugin-field-kit
@emdash-cms/plugin-forms
@emdash-cms/plugin-webhook-notifier
commit: |
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ❌ Deployment failed View logs |
emdash-demo-cache | 743d837 | Jun 19 2026, 08:58 PM |
Overlapping PRsThis PR modifies files that are also changed by other open PRs:
This may cause merge conflicts or duplicated work. A maintainer will coordinate. |
What does this PR do?
Fixes responsive image optimization images behind auth, proxy etc by loadig the source images form storage rather than via an HTTP request. The responsive-
srcsetfeature (#1438) hands Astro's image service an absolute media URL (required — Astro'sbaseService.getURLonly optimizes sources that passisRemoteAllowed, andisRemoteAllowedrejects relative URLs outright). Astro's stock image endpoint thenfetch()es that absolute URL to load the source bytes. On Cloudflare that's a self-subrequest to the Worker's own origin, which fails behind Cloudflare Access — surfacing as a 404 from/_image.This wraps Astro's image endpoint with one that, for an EmDash media URL, reads the source bytes straight from the storage adapter and transforms them in place — the absolute
hrefis never fetched, so there's no loopback and Access is never in the way. Every other image (bundled assets, allowed-remote,publicUrlCDN media) is delegated to the platform's stock endpoint unchanged.astro:assets(sharp) and delegates toastro/assets/endpoint/generic; Cloudflare uses theIMAGESbinding and delegates to@astrojs/cloudflare/image-transform-endpoint.image.remotePatterns, andastro:assetsflow are unchanged from feat: responsive srcset for media via astro:assets #1438 — the fix is entirely in the endpoint.Image/getImagedrive everything; there's no parallel route and no per-image component branching.cloudflare-binding). Opt out withimages: false. Raw-bytes fallbacks carry the same sandbox-CSP /Content-Dispositionprotections as the media file route.Supersedes #1494 (keeps its
IMAGES-binding transform, drops its parallel route + component branching). Design discussion: #1547.Type of change
Checklist
pnpm typecheckpassespnpm lintpassespnpm testpasses (or targeted tests for my change)pnpm formathas been runAI-generated code disclosure
Screenshots / test output
The e2e loads the seeded
/posts/post-with-image, asserts the rendered<img>src goes through/_image, and fetches that URL to confirm the wrapped endpoint returns a real image (200,image/*) — on both sharp (Node) and theIMAGESbinding (workerd).