import { readFile } from "node:fs/promises"; import { join } from "node:path"; import { NextResponse } from "next/server"; import { getSiteSettings } from "@/lib/db/queries"; export const runtime = "nodejs"; export const dynamic = "force-dynamic"; export const revalidate = 0; const DEFAULT_CONTENT_TYPE = "image/x-icon"; const FETCH_TIMEOUT_MS = 10_000; function noStoreHeaders(contentType: string) { return { "Content-Type": contentType, "Cache-Control": "no-store, no-cache, must-revalidate, proxy-revalidate", Pragma: "no-cache", Expires: "0", }; } function inferContentType(url: string) { const lower = url.toLowerCase(); if (lower.endsWith(".png")) return "image/png"; if (lower.endsWith(".svg")) return "image/svg+xml"; if (lower.endsWith(".jpg") || lower.endsWith(".jpeg")) return "image/jpeg"; if (lower.endsWith(".webp")) return "image/webp"; return DEFAULT_CONTENT_TYPE; } function resolveFaviconUrl(rawUrl: string, requestUrl: string) { const input = rawUrl.trim(); if (!input) { return null; } try { return new URL(input).toString(); } catch { // ignore and try resolving relative path } const authUrl = process.env.AUTH_URL?.trim(); if (authUrl) { try { return new URL(input, authUrl).toString(); } catch { // ignore and fallback to request origin } } try { return new URL(input, requestUrl).toString(); } catch { return null; } } function uniqueNonEmptyValues(values: string[]) { const seen = new Set(); const result: string[] = []; for (const value of values) { const trimmed = value.trim(); if (!trimmed || seen.has(trimmed)) continue; seen.add(trimmed); result.push(trimmed); } return result; } async function readLocalFallbackFavicon() { const file = await readFile( join(process.cwd(), "public", "default-favicon.ico") ); return new NextResponse(file, { status: 200, headers: noStoreHeaders(DEFAULT_CONTENT_TYPE), }); } export async function GET(request: Request) { try { const requestUrl = new URL(request.url); if (process.env.CANVAS_E2E_LOCAL === "true") { return await readLocalFallbackFavicon(); } const preferDark = requestUrl.searchParams.get("theme") === "dark"; const settings = await getSiteSettings(); const configuredFavicon = String(settings?.favicon || "").trim(); const configuredLightLogo = String(settings?.siteLogo || "").trim(); const configuredDarkLogo = String(settings?.siteLogoDark || "").trim(); const rawFaviconCandidates = configuredFavicon ? [configuredFavicon] : preferDark ? uniqueNonEmptyValues([configuredDarkLogo, configuredLightLogo]) : uniqueNonEmptyValues([configuredLightLogo, configuredDarkLogo]); if (rawFaviconCandidates.length === 0) { return await readLocalFallbackFavicon(); } for (const rawFaviconUrl of rawFaviconCandidates) { const faviconUrl = resolveFaviconUrl(rawFaviconUrl, request.url); if (!faviconUrl) { console.error("[favicon] Invalid favicon URL:", rawFaviconUrl); continue; } const targetUrl = `${faviconUrl}${faviconUrl.includes("?") ? "&" : "?"}v=${Date.now()}`; const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS); let upstream: Response; try { upstream = await fetch(targetUrl, { cache: "no-store", signal: controller.signal, redirect: "follow", headers: { Accept: "image/*,*/*;q=0.8", "User-Agent": "Mozilla/5.0 (compatible; BuyiAIFavicon/1.0)", }, }); } catch (error) { console.error("[favicon] Candidate request failed:", { targetUrl, error, }); continue; } finally { clearTimeout(timeout); } if (!upstream.ok) { console.error("[favicon] Upstream request failed:", { targetUrl, status: upstream.status, statusText: upstream.statusText, }); continue; } const buffer = await upstream.arrayBuffer(); const contentType = upstream.headers.get("content-type") || inferContentType(faviconUrl); return new NextResponse(buffer, { status: 200, headers: noStoreHeaders(contentType), }); } return await readLocalFallbackFavicon(); } catch (error) { console.error("[favicon] Failed to proxy favicon:", error); return await readLocalFallbackFavicon(); } }