airysoul/discovery-agent
0
1const DEFAULT_TIMEOUT_MS = 120000;2 3function normalizeBaseUrl(rawUrl) {4 if (!rawUrl) return "";5 return rawUrl.endsWith("/") ? rawUrl.slice(0, -1) : rawUrl;6}7 8function getSearchPart(reqUrl) {9 if (!reqUrl) return "";10 const qIndex = reqUrl.indexOf("?");11 return qIndex === -1 ? "" : reqUrl.slice(qIndex);12}13 14function getProxyPath(req) {15 const pathPart = req.query?.path;16 const normalize = (segments) => {17 if (segments.length > 0 && String(segments[0]).toLowerCase() === "api") {18 return segments.slice(1);19 }20 return segments;21 };22 23 if (Array.isArray(pathPart) && pathPart.length > 0) {24 const parts = normalize(pathPart);25 return parts.length26 ? "/" + parts.map((segment) => encodeURIComponent(String(segment))).join("/")27 : "/";28 }29 if (typeof pathPart === "string" && pathPart.length > 0) {30 const rawParts = pathPart.split("/").filter(Boolean);31 const parts = normalize(rawParts);32 return parts.length33 ? "/" + parts.map((segment) => encodeURIComponent(String(segment))).join("/")34 : "/";35 }36 return "/";37}38 39function getForwardHeaders(req, token) {40 const headers = {};41 const drop = new Set([42 "host",43 "connection",44 "content-length",45 "x-forwarded-host",46 "x-forwarded-proto",47 "x-forwarded-for",48 ]);49 50 for (const [key, value] of Object.entries(req.headers || {})) {51 const lower = key.toLowerCase();52 if (drop.has(lower) || value == null) continue;53 headers[key] = Array.isArray(value) ? value.join(", ") : String(value);54 }55 56 headers["user-agent"] = headers["user-agent"] || "discovery-agent-vercel-proxy";57 if (token) {58 headers["authorization"] = `Bearer ${token}`;59 }60 61 return headers;62}63 64async function getRequestBody(req) {65 if (req.method === "GET" || req.method === "HEAD") {66 return undefined;67 }68 69 if (req.body !== undefined) {70 if (Buffer.isBuffer(req.body)) return req.body;71 if (typeof req.body === "string") return Buffer.from(req.body);72 if (req.body instanceof Uint8Array) return Buffer.from(req.body);73 if (req.body && typeof req.body === "object") {74 return Buffer.from(JSON.stringify(req.body));75 }76 }77 78 const chunks = [];79 for await (const chunk of req) {80 chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));81 }82 return chunks.length ? Buffer.concat(chunks) : undefined;83}84 85function setResponseHeaders(res, upstream) {86 const skip = new Set([87 "connection",88 "content-length",89 "content-encoding",90 "keep-alive",91 "proxy-authenticate",92 "proxy-authorization",93 "te",94 "trailer",95 "transfer-encoding",96 "upgrade",97 ]);98 99 for (const [key, value] of upstream.headers.entries()) {100 if (skip.has(key.toLowerCase())) continue;101 res.setHeader(key, value);102 }103}104 105module.exports = async function handler(req, res) {106 const baseUrl = normalizeBaseUrl(process.env.HF_SPACE_BASE_URL);107 const token = process.env.HF_SPACE_TOKEN || process.env.HF_TOKEN || "";108 const timeoutMs = Number(process.env.PROXY_TIMEOUT_MS || DEFAULT_TIMEOUT_MS);109 110 if (!baseUrl) {111 res.status(500).json({112 error: "HF_SPACE_BASE_URL is not configured",113 });114 return;115 }116 117 const upstreamUrl = `${baseUrl}/api${getProxyPath(req)}${getSearchPart(req.url)}`;118 const headers = getForwardHeaders(req, token);119 const body = await getRequestBody(req);120 121 const controller = new AbortController();122 const timeout = setTimeout(() => controller.abort(), timeoutMs);123 124 try {125 const upstream = await fetch(upstreamUrl, {126 method: req.method,127 headers,128 body,129 redirect: "manual",130 signal: controller.signal,131 });132 133 clearTimeout(timeout);134 setResponseHeaders(res, upstream);135 res.status(upstream.status);136 137 const buffer = Buffer.from(await upstream.arrayBuffer());138 res.send(buffer);139 } catch (error) {140 clearTimeout(timeout);141 142 const status = error?.name === "AbortError" ? 504 : 502;143 const message = error?.name === "AbortError"144 ? "Upstream timeout"145 : "Upstream request failed";146 147 res.status(status).json({ error: message });148 }149};150 