Files
gitlab-instance-0a899031_cn…/app/api/meetup/_paymentApi.ts
2026-03-13 05:14:28 -05:00

99 lines
3.4 KiB
TypeScript

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");
}