import type { NextRequest } from "next/server"; import { NextResponse } from "next/server"; import { applyAuthResponseCookies, buildBackendRequestHeaders, } from "@/lib/backend-auth"; import { buildCachedJsonResponse } from "@/lib/http-cache"; import { finishProxyTimedResponse, type ProxyTimer, } from "@/lib/proxy-timing"; const PASSTHROUGH_UPSTREAM_STATUSES = new Set([ 400, 401, 402, 403, 404, 409, 422, 429, ]); function shouldExposeProxyErrorDetail() { return ( process.env.NODE_ENV !== "production" || process.env.POLYWEATHER_EXPOSE_PROXY_ERROR_DETAIL === "true" ); } export function clientStatusFromUpstream(status: number) { if (PASSTHROUGH_UPSTREAM_STATUSES.has(status)) { return status; } return 502; } export function buildUpstreamErrorResponse( upstreamStatus: number, rawDetail: string, options?: { detailLimit?: number; error?: string; extraDebug?: Record; }, ) { const body: Record = { error: options?.error || "Upstream request failed", upstream_status: upstreamStatus, }; if (shouldExposeProxyErrorDetail()) { body.detail = String(rawDetail || "").slice(0, options?.detailLimit ?? 300); if (options?.extraDebug) { body.proxy_debug = options.extraDebug; } } return NextResponse.json(body, { status: clientStatusFromUpstream(upstreamStatus), }); } export function buildProxyExceptionResponse( error: unknown, options: { status?: number; publicMessage: string; extra?: Record; }, ) { const body: Record = { error: options.publicMessage, ...(options.extra || {}), }; if (shouldExposeProxyErrorDetail()) { body.detail = String(error); } return NextResponse.json(body, { status: options.status ?? 500 }); } export async function proxyBackendJsonGet( req: NextRequest, options: { cacheControl?: string; conditionalResponse?: boolean; detailLimit?: number; error?: string; fetchCache?: RequestCache; includeSupabaseIdentity?: boolean; publicMessage: string; revalidateSeconds?: number; signal?: AbortSignal; statusOnException?: number; timeoutPublicMessage?: string; timing?: ProxyTimer; url: string; }, ) { let auth: Awaited> | null = null; const timing = options.timing; try { auth = await (timing ? timing.measure("auth_headers", () => buildBackendRequestHeaders(req, { includeSupabaseIdentity: options.includeSupabaseIdentity ?? false, }), ) : buildBackendRequestHeaders(req, { includeSupabaseIdentity: options.includeSupabaseIdentity ?? false, })); const res = await (timing ? timing.measure("backend_fetch", () => fetch(options.url, { headers: auth!.headers, ...(options.fetchCache ? { cache: options.fetchCache } : { next: { revalidate: options.revalidateSeconds ?? 30 } }), signal: options.signal, }), ) : fetch(options.url, { headers: auth.headers, ...(options.fetchCache ? { cache: options.fetchCache } : { next: { revalidate: options.revalidateSeconds ?? 30 } }), signal: options.signal, })); const backendServerTiming = res.headers.get("server-timing") || ""; if (!res.ok) { const raw = await (timing ? timing.measure("backend_read", () => res.text()) : res.text()); const response = buildUpstreamErrorResponse(res.status, raw, { detailLimit: options.detailLimit, error: options.error, }); const withCookies = applyAuthResponseCookies(response, auth.response); return timing ? finishProxyTimedResponse(withCookies, timing, `upstream_${res.status}`, { backendServerTiming, }) : withCookies; } const data = await (timing ? timing.measure("backend_read", () => res.json()) : res.json()); const response = options.cacheControl && options.conditionalResponse !== false ? buildCachedJsonResponse(req, data, options.cacheControl) : NextResponse.json(data, { headers: options.cacheControl ? { "Cache-Control": options.cacheControl } : undefined, }); const withCookies = applyAuthResponseCookies(response, auth.response); return timing ? finishProxyTimedResponse(withCookies, timing, "ok", { backendServerTiming, }) : withCookies; } catch (error) { const timedOut = options.signal?.aborted === true; const response = buildProxyExceptionResponse(error, { publicMessage: timedOut && options.timeoutPublicMessage ? options.timeoutPublicMessage : options.publicMessage, status: timedOut ? 504 : options.statusOnException, }); const withCookies = auth ? applyAuthResponseCookies(response, auth.response) : response; return timing ? finishProxyTimedResponse(withCookies, timing, timedOut ? "timeout" : "exception") : withCookies; } }