/** * Same-origin proxy for a post's first attached image. * * Why this exists: * * navigator.share({ files }) needs a Blob built from a successful * cross-origin fetch. The R2 bucket sits behind a custom domain * (`s3.prabhupadasays.com`), so even with a correct CORS policy on * the bucket, the browser will reuse pre-CORS cached responses from * when the image was first loaded by an tag without a `Vary: * Origin` header. The cached copy lacks `Access-Control-Allow-Origin`, * so subsequent CORS fetches from the share flow blow up with a CORS * error even though R2 itself is now configured correctly. * * Routing the fetch through this same-origin endpoint sidesteps the * whole CORS layer. The browser's request to /api/share-image/ * is same-origin and never carries an Origin header to R2; Next.js * fetches the image server-side (no CORS at all) and pipes the * bytes back to the browser. * * Security: we fetch by post id (not by raw URL) so the route can't * be coerced into SSRF — only images attached to a real post are * reachable, and the post lookup goes through the BFF. */ import { bff } from "@/lib/bff"; export const runtime = "nodejs"; type PostShape = { id: string; images: { url: string }[]; }; export async function GET( _request: Request, context: { params: Promise<{ id: string }> }, ) { try { const { id } = await context.params; const post = await bff(`/posts/${encodeURIComponent(id)}`, { revalidate: 60, }); if (!post || !post.images?.[0]?.url) { return new Response("Image not found", { status: 404 }); } const imageUrl = post.images[0].url; // Server-side fetch — no CORS at all from this side of the wire. const upstream = await fetch(imageUrl); if (!upstream.ok || !upstream.body) { return new Response("Upstream fetch failed", { status: upstream.status || 502, }); } const contentType = upstream.headers.get("content-type") || "image/jpeg"; const contentLength = upstream.headers.get("content-length"); return new Response(upstream.body, { status: 200, headers: { "Content-Type": contentType, // Allow shared CDN caching for a day — image content for a // given post id is immutable in practice (replacing the image // mints a new key), so a long TTL is safe. Browser cache // shorter so updates are picked up faster on the share flow. "Cache-Control": "public, max-age=3600, s-maxage=86400, stale-while-revalidate=86400", ...(contentLength ? { "Content-Length": contentLength } : {}), }, }); } catch (e) { console.error("share-image proxy failed:", e); return new Response("Proxy error", { status: 500 }); } }