import { NextRequest } from "next/server"; import { DEFAULT_PAYMENT_API_URL } from "@/config/domain.config"; import { getPaymentConfig } from "@/config/services.config"; function isPrivateHost(hostname: string): boolean { if (!hostname) return false; if (hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1") { return true; } if (/^10\./.test(hostname) || /^192\.168\./.test(hostname)) { return true; } const match = hostname.match(/^172\.(\d{1,2})\./); if (!match) return false; const secondOctet = Number(match[1]); return secondOctet >= 16 && secondOctet <= 31; } function normalizeBaseUrl(url: string | undefined): string { return String(url || "").trim().replace(/\/$/, ""); } function getRequestHostname(request: NextRequest): string { const forwardedHost = request.headers.get("x-forwarded-host"); const host = forwardedHost || request.headers.get("host") || ""; return host.split(",")[0].trim().replace(/:\d+$/, ""); } function getMeetupApiCandidates(request: NextRequest): string[] { const configuredApiUrl = normalizeBaseUrl(getPaymentConfig().apiUrl); const localApiUrl = normalizeBaseUrl(DEFAULT_PAYMENT_API_URL); const hostname = getRequestHostname(request); const shouldPreferLocal = process.env.NODE_ENV === "development" || isPrivateHost(hostname); const urls = shouldPreferLocal ? [localApiUrl, configuredApiUrl] : [configuredApiUrl, localApiUrl]; return urls.filter((url, index) => url && urls.indexOf(url) === index); } /** 上游 API 请求超时时间(毫秒),避免 serverless 超时导致 502 */ const MEETUP_API_TIMEOUT_MS = 20000; export async function postMeetupApi( request: NextRequest, path: string, body: unknown ): Promise<{ status: number; data: unknown }> { const apiCandidates = getMeetupApiCandidates(request); if (apiCandidates.length === 0) { throw new Error("PAYMENT_API_URL is not configured"); } let lastResponse: { status: number; data: unknown } | null = null; let lastError: unknown = null; for (const apiUrl of apiCandidates) { const url = `${apiUrl}${path}`; const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), MEETUP_API_TIMEOUT_MS); try { const res = await fetch(url, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body), cache: "no-store", signal: controller.signal, }); clearTimeout(timeoutId); const data = await res.json().catch(() => ({})); if (res.ok || (res.status !== 404 && res.status < 500)) { return { status: res.status, data }; } lastResponse = { status: res.status, data }; if (res.status >= 500) { console.error(`[meetup-api] ${path} ${apiUrl} returned ${res.status}`, data); } } catch (error) { clearTimeout(timeoutId); const errMsg = error instanceof Error ? error.message : String(error); const errName = error instanceof Error && "name" in error ? (error as { name?: string }).name : ""; const isTimeout = errName === "AbortError" || errMsg.toLowerCase().includes("abort") || errMsg.toLowerCase().includes("timeout"); console.error(`[meetup-api] ${path} ${apiUrl} failed:`, isTimeout ? "timeout" : errMsg); lastError = error; } } if (lastResponse) { return lastResponse; } throw lastError instanceof Error ? lastError : new Error("Meetup API request failed"); }