import { type NextRequest, NextResponse } from "next/server"; import sharp from "sharp"; import { fetchAvatarImage, parseAvatarQuery } from "@/lib/imager-upstream"; type AvatarFormat = "png" | "webp" | "avif"; function resolveFormat(raw: string | null): AvatarFormat { switch (raw?.toLowerCase()) { case "webp": return "webp"; case "avif": return "avif"; default: return "png"; } } export async function GET(request: NextRequest) { const { searchParams } = new URL(request.url); const parsed = parseAvatarQuery(searchParams); if (!parsed.ok) { return NextResponse.json({ error: parsed.message }, { status: 400 }); } const format = resolveFormat(searchParams.get("format")); try { const { body, contentType, source } = await fetchAvatarImage( new URL(request.url).origin, parsed.params, ); // Re-encode PNG avatars to WebP/AVIF with sharp to cut bandwidth. // Falls back to the original PNG if conversion fails. let respBody: Uint8Array = body; let respContentType = contentType; if (format !== "png") { try { if (format === "webp") { respBody = new Uint8Array( await sharp(Buffer.from(body)).webp().toBuffer(), ); respContentType = "image/webp"; } else { respBody = new Uint8Array( await sharp(Buffer.from(body)).avif().toBuffer(), ); respContentType = "image/avif"; } } catch { respBody = body; respContentType = contentType; } } return new NextResponse(respBody as unknown as BodyInit, { status: 200, headers: { "Content-Type": respContentType, "Content-Length": String(respBody.length), // s-maxage lets Cloudflare/edge cache avatars (currently DYNAMIC), // stale-while-revalidate keeps them fresh without blocking requests. // The ?format= param is part of the URL, so variants cache separately. // Fallback renders use a shorter TTL so the configured imager is // retried soon instead of being masked for hours. "Cache-Control": source === "fallback" ? "public, max-age=300, s-maxage=3600, stale-while-revalidate=3600" : "public, max-age=3600, s-maxage=86400, stale-while-revalidate=86400", "X-Imager-Source": source, "Access-Control-Allow-Origin": "*", }, }); } catch { return NextResponse.json( { error: "Failed to fetch avatar from upstream" }, { status: 502 }, ); } } export async function OPTIONS() { return new NextResponse(null, { status: 204, headers: { "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "GET, OPTIONS", "Access-Control-Allow-Headers": "Content-Type, If-None-Match", }, }); }