Files
wehub-resource-sync adf0d17497
publish / version_or_publish (push) Has been cancelled
storybook-build / changes (push) Has been cancelled
storybook-build / :storybook-build (push) Has been cancelled
Sync Gradio Skills to Hugging Face / sync-skills (push) Has been cancelled
functional / changes (push) Has been cancelled
functional / build-frontend (push) Has been cancelled
functional / functional-test-SSR=false (push) Has been cancelled
functional / functional-reload (push) Has been cancelled
js / changes (push) Has been cancelled
js / js-test (push) Has been cancelled
docs-build / changes (push) Has been cancelled
docs-build / docs-build (push) Has been cancelled
docs-build / website-build (push) Has been cancelled
functional / functional-test-SSR=true (push) Has been cancelled
hygiene / hygiene-test (push) Has been cancelled
python / changes (push) Has been cancelled
python / build (push) Has been cancelled
python / test-ubuntu-latest-flaky (push) Has been cancelled
python / test-ubuntu-latest-not-flaky (push) Has been cancelled
python / test-windows-latest-flaky (push) Has been cancelled
python / test-windows-latest-not-flaky (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:17:32 +08:00

129 lines
4.2 KiB
JavaScript

import http from "node:http";
import process from "node:process";
import httpProxy from "http-proxy";
import { handler } from "./handler.js";
import { classifyRoute } from "./proxy_routes.js";
const host = process.env.HOST || "0.0.0.0";
const port = parseInt(process.env.PORT || "7860", 10);
const pythonHost = process.env.GRADIO_PYTHON_HOST || "127.0.0.1";
const pythonPort = parseInt(process.env.GRADIO_PYTHON_PORT || "7861", 10);
const serverModeEnabled = process.env.GRADIO_SERVER_MODE_ENABLED;
const staticWorkerPorts = process.env.GRADIO_STATIC_WORKER_PORTS
? process.env.GRADIO_STATIC_WORKER_PORTS.split(",")
.map((p) => parseInt(p.trim(), 10))
.filter((p) => !isNaN(p))
: [];
let workerIndex = 0;
const pythonTarget = `http://${pythonHost}:${pythonPort}`;
const proxy = httpProxy.createProxyServer({
// Don't modify the path
ignorePath: false,
// Forward the original host header
changeOrigin: false
});
proxy.on("error", (err, req, res) => {
// Suppress ECONNRESET from client-initiated disconnects
if (err.code === "ECONNRESET") return;
console.error(`[gradio-proxy] Proxy error for ${req.url}:`, err.message);
if (res.writeHead && !res.headersSent) {
res.writeHead(502, { "Content-Type": "text/plain" });
res.end("Bad Gateway");
}
});
// When the browser closes an SSE/streaming connection, propagate the
// disconnect to Python so that `request.is_disconnected()` returns True.
// http-proxy only listens for the deprecated `aborted` event, which
// doesn't fire reliably in newer Node versions. We listen on `res`
// (the outgoing response to the client) because for GET/SSE requests
// `req` closes immediately after the empty body is consumed — before
// the proxy response even arrives.
proxy.on("proxyRes", (proxyRes, req, res) => {
res.on("close", () => {
if (!res.writableEnded) {
proxyRes.destroy();
}
});
});
const server = http.createServer((req, res) => {
const url = req.url || "/";
const qmark = url.indexOf("?");
const path = qmark === -1 ? url : url.substring(0, qmark);
const queryString = qmark === -1 ? "" : url.substring(qmark + 1);
const { route, workerIndex: affinityIndex } = classifyRoute(path, {
hasWorkers: staticWorkerPorts.length > 0,
serverModeEnabled: !!serverModeEnabled,
numWorkers: staticWorkerPorts.length,
queryString
});
if (route === "worker") {
let targetPort;
if (affinityIndex !== undefined) {
// Affinity routing: upload_id hashed to a specific worker
targetPort = staticWorkerPorts[affinityIndex];
} else {
// Round-robin for non-affinity static routes
targetPort = staticWorkerPorts[workerIndex % staticWorkerPorts.length];
workerIndex = (workerIndex + 1) % staticWorkerPorts.length;
}
proxy.web(req, res, {
target: `http://${pythonHost}:${targetPort}`
});
return;
}
if (route === "python") {
proxy.web(req, res, { target: pythonTarget });
return;
}
// SvelteKit handler (SSR + immutable assets)
// Inject headers that SvelteKit's page.server.ts expects to find the Python backend.
// x-gradio-server is for internal Node->Python fetches (always http).
// x-gradio-original-url is the public-facing URL the browser uses,
// so it must respect x-forwarded-proto (e.g. https on HF Spaces).
const publicScheme = (req.headers["x-forwarded-proto"] || "http")
.split(",")[0]
.trim();
const publicHost =
req.headers["x-forwarded-host"] || req.headers.host || `${host}:${port}`;
req.headers["x-gradio-server"] = pythonTarget;
req.headers["x-gradio-port"] = String(pythonPort);
req.headers["x-gradio-mounted-path"] = "/";
req.headers["x-gradio-original-url"] = `${publicScheme}://${publicHost}`;
handler(req, res);
});
server.listen({ host, port }, () => {
console.log(`[gradio-proxy] Listening on http://${host}:${port}`);
console.log(`[gradio-proxy] Python backend: ${pythonTarget}`);
if (staticWorkerPorts.length > 0) {
console.log(
`[gradio-proxy] Static workers: ${staticWorkerPorts.join(", ")}`
);
}
});
function graceful_shutdown() {
server.closeIdleConnections();
server.close(() => {
proxy.close();
process.exit(0);
});
setTimeout(() => server.closeAllConnections(), 30000);
}
process.on("SIGTERM", graceful_shutdown);
process.on("SIGINT", graceful_shutdown);
export { host, port, server };