feat(gateway): add HTTP health endpoints for K8s probes
This commit is contained in:
parent
109ac1c549
commit
1355c527bd
@ -6,6 +6,7 @@ Docs: https://docs.molt.bot
|
|||||||
Status: beta.
|
Status: beta.
|
||||||
|
|
||||||
### Changes
|
### Changes
|
||||||
|
- Gateway: add HTTP health endpoints (`/health`, `/ready`, `/health/deep`) for Kubernetes liveness/readiness probes. (#3507)
|
||||||
- Rebrand: rename the npm package/CLI to `moltbot`, add a `moltbot` compatibility shim, and move extensions to the `@moltbot/*` scope.
|
- Rebrand: rename the npm package/CLI to `moltbot`, add a `moltbot` compatibility shim, and move extensions to the `@moltbot/*` scope.
|
||||||
- Commands: group /help and /commands output with Telegram paging. (#2504) Thanks @hougangdev.
|
- Commands: group /help and /commands output with Telegram paging. (#2504) Thanks @hougangdev.
|
||||||
- macOS: limit project-local `node_modules/.bin` PATH preference to debug builds (reduce PATH hijacking risk).
|
- macOS: limit project-local `node_modules/.bin` PATH preference to debug builds (reduce PATH hijacking risk).
|
||||||
|
|||||||
@ -191,6 +191,19 @@ export type GatewayHttpConfig = {
|
|||||||
endpoints?: GatewayHttpEndpointsConfig;
|
endpoints?: GatewayHttpEndpointsConfig;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Health endpoint configuration for container orchestration.
|
||||||
|
* Provides K8s-style liveness/readiness probes.
|
||||||
|
*/
|
||||||
|
export type GatewayHealthConfig = {
|
||||||
|
/** Enable health endpoints (default: true). */
|
||||||
|
enabled?: boolean;
|
||||||
|
/** Base path for health endpoints (default: ""). */
|
||||||
|
basePath?: string;
|
||||||
|
/** Require auth for /health/deep (default: true). */
|
||||||
|
deepAuthRequired?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
export type GatewayNodesConfig = {
|
export type GatewayNodesConfig = {
|
||||||
/** Browser routing policy for node-hosted browser proxies. */
|
/** Browser routing policy for node-hosted browser proxies. */
|
||||||
browser?: {
|
browser?: {
|
||||||
@ -233,6 +246,8 @@ export type GatewayConfig = {
|
|||||||
tls?: GatewayTlsConfig;
|
tls?: GatewayTlsConfig;
|
||||||
http?: GatewayHttpConfig;
|
http?: GatewayHttpConfig;
|
||||||
nodes?: GatewayNodesConfig;
|
nodes?: GatewayNodesConfig;
|
||||||
|
/** Health endpoint configuration for K8s/container probes. */
|
||||||
|
health?: GatewayHealthConfig;
|
||||||
/**
|
/**
|
||||||
* IPs of trusted reverse proxies (e.g. Traefik, nginx). When a connection
|
* IPs of trusted reverse proxies (e.g. Traefik, nginx). When a connection
|
||||||
* arrives from one of these IPs, the Gateway trusts `x-forwarded-for` (or
|
* arrives from one of these IPs, the Gateway trusts `x-forwarded-for` (or
|
||||||
|
|||||||
194
src/gateway/http-health.test.ts
Normal file
194
src/gateway/http-health.test.ts
Normal file
@ -0,0 +1,194 @@
|
|||||||
|
import { describe, expect, it, afterEach } from "vitest";
|
||||||
|
import { createServer, type Server, type IncomingMessage, type ServerResponse } from "node:http";
|
||||||
|
import type { AddressInfo } from "node:net";
|
||||||
|
import { createHealthEndpointsHandler } from "./http-health.js";
|
||||||
|
import type { ResolvedGatewayAuth } from "./auth.js";
|
||||||
|
|
||||||
|
const mockAuth: ResolvedGatewayAuth = {
|
||||||
|
mode: "token",
|
||||||
|
token: "test-token-123",
|
||||||
|
allowTailscale: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
function createTestServer(
|
||||||
|
handler: (req: IncomingMessage, res: ServerResponse) => Promise<boolean>,
|
||||||
|
) {
|
||||||
|
const server = createServer(async (req, res) => {
|
||||||
|
const handled = await handler(req, res);
|
||||||
|
if (!handled) {
|
||||||
|
res.statusCode = 404;
|
||||||
|
res.end("Not Found");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return server;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function startServer(server: Server): Promise<string> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
server.listen(0, "127.0.0.1", () => {
|
||||||
|
const addr = server.address() as AddressInfo;
|
||||||
|
resolve(`http://127.0.0.1:${addr.port}`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function stopServer(server: Server): Promise<void> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
server.close(() => resolve());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("http-health", () => {
|
||||||
|
let server: Server;
|
||||||
|
let baseUrl: string;
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
if (server) {
|
||||||
|
await stopServer(server);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("GET /health", () => {
|
||||||
|
it("returns 200 with healthy status when enabled", async () => {
|
||||||
|
const handler = createHealthEndpointsHandler({
|
||||||
|
config: { enabled: true },
|
||||||
|
resolvedAuth: mockAuth,
|
||||||
|
});
|
||||||
|
server = createTestServer(handler);
|
||||||
|
baseUrl = await startServer(server);
|
||||||
|
|
||||||
|
const res = await fetch(`${baseUrl}/health`);
|
||||||
|
expect(res.status).toBe(200);
|
||||||
|
|
||||||
|
const body = await res.json();
|
||||||
|
expect(body.status).toBe("healthy");
|
||||||
|
expect(body.version).toBeDefined();
|
||||||
|
expect(body.uptimeMs).toBeGreaterThanOrEqual(0);
|
||||||
|
expect(body.timestamp).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns 404 when disabled", async () => {
|
||||||
|
const handler = createHealthEndpointsHandler({
|
||||||
|
config: { enabled: false },
|
||||||
|
resolvedAuth: mockAuth,
|
||||||
|
});
|
||||||
|
server = createTestServer(handler);
|
||||||
|
baseUrl = await startServer(server);
|
||||||
|
|
||||||
|
const res = await fetch(`${baseUrl}/health`);
|
||||||
|
expect(res.status).toBe(404);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("respects custom basePath", async () => {
|
||||||
|
const handler = createHealthEndpointsHandler({
|
||||||
|
config: { enabled: true, basePath: "/api/v1" },
|
||||||
|
resolvedAuth: mockAuth,
|
||||||
|
});
|
||||||
|
server = createTestServer(handler);
|
||||||
|
baseUrl = await startServer(server);
|
||||||
|
|
||||||
|
// Original path should 404
|
||||||
|
const res1 = await fetch(`${baseUrl}/health`);
|
||||||
|
expect(res1.status).toBe(404);
|
||||||
|
|
||||||
|
// Custom path should work
|
||||||
|
const res2 = await fetch(`${baseUrl}/api/v1/health`);
|
||||||
|
expect(res2.status).toBe(200);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("GET /ready", () => {
|
||||||
|
it("returns 200 when healthy", async () => {
|
||||||
|
const handler = createHealthEndpointsHandler({
|
||||||
|
config: { enabled: true },
|
||||||
|
resolvedAuth: mockAuth,
|
||||||
|
});
|
||||||
|
server = createTestServer(handler);
|
||||||
|
baseUrl = await startServer(server);
|
||||||
|
|
||||||
|
const res = await fetch(`${baseUrl}/ready`);
|
||||||
|
expect(res.status).toBe(200);
|
||||||
|
|
||||||
|
const body = await res.json();
|
||||||
|
expect(body.status).toBe("healthy");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("GET /health/deep", () => {
|
||||||
|
it("returns 401 without auth when deepAuthRequired is true", async () => {
|
||||||
|
const handler = createHealthEndpointsHandler({
|
||||||
|
config: { enabled: true, deepAuthRequired: true },
|
||||||
|
resolvedAuth: mockAuth,
|
||||||
|
});
|
||||||
|
server = createTestServer(handler);
|
||||||
|
baseUrl = await startServer(server);
|
||||||
|
|
||||||
|
const res = await fetch(`${baseUrl}/health/deep`);
|
||||||
|
expect(res.status).toBe(401);
|
||||||
|
|
||||||
|
const body = await res.json();
|
||||||
|
expect(body.error).toBe("Unauthorized");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns 200 with valid auth", async () => {
|
||||||
|
const handler = createHealthEndpointsHandler({
|
||||||
|
config: { enabled: true, deepAuthRequired: true },
|
||||||
|
resolvedAuth: mockAuth,
|
||||||
|
});
|
||||||
|
server = createTestServer(handler);
|
||||||
|
baseUrl = await startServer(server);
|
||||||
|
|
||||||
|
const res = await fetch(`${baseUrl}/health/deep`, {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${mockAuth.token}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(res.status).toBe(200);
|
||||||
|
|
||||||
|
const body = await res.json();
|
||||||
|
expect(body.status).toBeDefined();
|
||||||
|
expect(body.checks).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns 200 without auth when deepAuthRequired is false", async () => {
|
||||||
|
const handler = createHealthEndpointsHandler({
|
||||||
|
config: { enabled: true, deepAuthRequired: false },
|
||||||
|
resolvedAuth: mockAuth,
|
||||||
|
});
|
||||||
|
server = createTestServer(handler);
|
||||||
|
baseUrl = await startServer(server);
|
||||||
|
|
||||||
|
const res = await fetch(`${baseUrl}/health/deep`);
|
||||||
|
expect(res.status).toBe(200);
|
||||||
|
|
||||||
|
const body = await res.json();
|
||||||
|
expect(body.checks).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("response format", () => {
|
||||||
|
it("includes Cache-Control: no-store header", async () => {
|
||||||
|
const handler = createHealthEndpointsHandler({
|
||||||
|
config: { enabled: true },
|
||||||
|
resolvedAuth: mockAuth,
|
||||||
|
});
|
||||||
|
server = createTestServer(handler);
|
||||||
|
baseUrl = await startServer(server);
|
||||||
|
|
||||||
|
const res = await fetch(`${baseUrl}/health`);
|
||||||
|
expect(res.headers.get("Cache-Control")).toBe("no-store");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("includes Content-Type: application/json header", async () => {
|
||||||
|
const handler = createHealthEndpointsHandler({
|
||||||
|
config: { enabled: true },
|
||||||
|
resolvedAuth: mockAuth,
|
||||||
|
});
|
||||||
|
server = createTestServer(handler);
|
||||||
|
baseUrl = await startServer(server);
|
||||||
|
|
||||||
|
const res = await fetch(`${baseUrl}/health`);
|
||||||
|
expect(res.headers.get("Content-Type")).toContain("application/json");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
203
src/gateway/http-health.ts
Normal file
203
src/gateway/http-health.ts
Normal file
@ -0,0 +1,203 @@
|
|||||||
|
/**
|
||||||
|
* HTTP health endpoints for container orchestration (K8s liveness/readiness).
|
||||||
|
*
|
||||||
|
* - GET /health - Liveness probe: 200 if process alive
|
||||||
|
* - GET /ready - Readiness probe: 200 if channels ready, 503 if degraded
|
||||||
|
* - GET /health/deep - Detailed status (auth-protected)
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { IncomingMessage, ServerResponse } from "node:http";
|
||||||
|
import { VERSION } from "../version.js";
|
||||||
|
import type { HealthSummary } from "../commands/health.js";
|
||||||
|
import type { GatewayHealthConfig } from "../config/types.gateway.js";
|
||||||
|
import { authorizeGatewayConnect, type ResolvedGatewayAuth } from "./auth.js";
|
||||||
|
import { getBearerToken } from "./http-utils.js";
|
||||||
|
import { getHealthCache, refreshGatewayHealthSnapshot } from "./server/health-state.js";
|
||||||
|
|
||||||
|
export type HealthStatus = "healthy" | "degraded" | "unhealthy";
|
||||||
|
|
||||||
|
export type HealthResponse = {
|
||||||
|
status: HealthStatus;
|
||||||
|
version: string;
|
||||||
|
uptimeMs: number;
|
||||||
|
timestamp: string;
|
||||||
|
checks?: {
|
||||||
|
channels?: {
|
||||||
|
total: number;
|
||||||
|
configured: number;
|
||||||
|
healthy: number;
|
||||||
|
degraded: string[];
|
||||||
|
};
|
||||||
|
agents?: {
|
||||||
|
total: number;
|
||||||
|
default: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const startTime = Date.now();
|
||||||
|
|
||||||
|
function sendJson(res: ServerResponse, status: number, body: unknown) {
|
||||||
|
res.statusCode = status;
|
||||||
|
res.setHeader("Content-Type", "application/json; charset=utf-8");
|
||||||
|
res.setHeader("Cache-Control", "no-store");
|
||||||
|
res.end(JSON.stringify(body));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine overall health status from channel health summaries.
|
||||||
|
* - healthy: all configured channels have successful probes
|
||||||
|
* - degraded: some channels have failed probes
|
||||||
|
* - unhealthy: all channels have failed probes or critical failure
|
||||||
|
*/
|
||||||
|
function determineHealthStatus(health: HealthSummary | null): {
|
||||||
|
status: HealthStatus;
|
||||||
|
degradedChannels: string[];
|
||||||
|
} {
|
||||||
|
if (!health) {
|
||||||
|
return { status: "healthy", degradedChannels: [] };
|
||||||
|
}
|
||||||
|
|
||||||
|
const channels = health.channels ?? {};
|
||||||
|
const channelIds = Object.keys(channels);
|
||||||
|
const degradedChannels: string[] = [];
|
||||||
|
|
||||||
|
for (const channelId of channelIds) {
|
||||||
|
const channelSummary = channels[channelId];
|
||||||
|
if (!channelSummary) continue;
|
||||||
|
|
||||||
|
// Skip unconfigured channels
|
||||||
|
if (channelSummary.configured === false) continue;
|
||||||
|
|
||||||
|
// Check probe status
|
||||||
|
const probe = channelSummary.probe as { ok?: boolean } | undefined;
|
||||||
|
if (probe && probe.ok === false) {
|
||||||
|
degradedChannels.push(channelId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check account-level probes
|
||||||
|
const accounts = channelSummary.accounts ?? {};
|
||||||
|
for (const [accountId, accountSummary] of Object.entries(accounts)) {
|
||||||
|
const accountProbe = accountSummary.probe as { ok?: boolean } | undefined;
|
||||||
|
if (accountProbe && accountProbe.ok === false) {
|
||||||
|
const key = `${channelId}:${accountId}`;
|
||||||
|
if (!degradedChannels.includes(key)) {
|
||||||
|
degradedChannels.push(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const configuredCount = channelIds.filter((id) => channels[id]?.configured !== false).length;
|
||||||
|
|
||||||
|
if (degradedChannels.length === 0) {
|
||||||
|
return { status: "healthy", degradedChannels };
|
||||||
|
}
|
||||||
|
if (degradedChannels.length >= configuredCount && configuredCount > 0) {
|
||||||
|
return { status: "unhealthy", degradedChannels };
|
||||||
|
}
|
||||||
|
return { status: "degraded", degradedChannels };
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildHealthResponse(health: HealthSummary | null, includeChecks: boolean): HealthResponse {
|
||||||
|
const uptimeMs = Date.now() - startTime;
|
||||||
|
const { status, degradedChannels } = determineHealthStatus(health);
|
||||||
|
|
||||||
|
const response: HealthResponse = {
|
||||||
|
status,
|
||||||
|
version: VERSION,
|
||||||
|
uptimeMs,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (includeChecks && health) {
|
||||||
|
const channels = health.channels ?? {};
|
||||||
|
const channelIds = Object.keys(channels);
|
||||||
|
const configuredCount = channelIds.filter((id) => channels[id]?.configured !== false).length;
|
||||||
|
const healthyCount = configuredCount - degradedChannels.length;
|
||||||
|
|
||||||
|
response.checks = {
|
||||||
|
channels: {
|
||||||
|
total: channelIds.length,
|
||||||
|
configured: configuredCount,
|
||||||
|
healthy: healthyCount,
|
||||||
|
degraded: degradedChannels,
|
||||||
|
},
|
||||||
|
agents: {
|
||||||
|
total: health.agents?.length ?? 0,
|
||||||
|
default: health.defaultAgentId ?? "pi",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type HealthEndpointsHandler = (
|
||||||
|
req: IncomingMessage,
|
||||||
|
res: ServerResponse,
|
||||||
|
) => Promise<boolean>;
|
||||||
|
|
||||||
|
export function createHealthEndpointsHandler(opts: {
|
||||||
|
config?: GatewayHealthConfig;
|
||||||
|
resolvedAuth: ResolvedGatewayAuth;
|
||||||
|
trustedProxies?: string[];
|
||||||
|
}): HealthEndpointsHandler {
|
||||||
|
const { config, resolvedAuth, trustedProxies } = opts;
|
||||||
|
const basePath = config?.basePath ?? "";
|
||||||
|
const deepAuthRequired = config?.deepAuthRequired !== false;
|
||||||
|
|
||||||
|
return async (req, res) => {
|
||||||
|
if (config?.enabled === false) return false;
|
||||||
|
|
||||||
|
const url = new URL(req.url ?? "/", "http://localhost");
|
||||||
|
const path = url.pathname;
|
||||||
|
|
||||||
|
// GET /health - Liveness probe (always 200 if process is running)
|
||||||
|
if (path === `${basePath}/health` && req.method === "GET") {
|
||||||
|
const health = getHealthCache();
|
||||||
|
const response = buildHealthResponse(health, false);
|
||||||
|
sendJson(res, 200, response);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// GET /ready - Readiness probe (503 if degraded/unhealthy)
|
||||||
|
if (path === `${basePath}/ready` && req.method === "GET") {
|
||||||
|
const health = getHealthCache();
|
||||||
|
const { status } = determineHealthStatus(health);
|
||||||
|
const response = buildHealthResponse(health, false);
|
||||||
|
|
||||||
|
const httpStatus = status === "healthy" ? 200 : 503;
|
||||||
|
sendJson(res, httpStatus, response);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// GET /health/deep - Detailed health with auth
|
||||||
|
if (path === `${basePath}/health/deep` && req.method === "GET") {
|
||||||
|
if (deepAuthRequired) {
|
||||||
|
const token = getBearerToken(req);
|
||||||
|
const authResult = await authorizeGatewayConnect({
|
||||||
|
auth: resolvedAuth,
|
||||||
|
connectAuth: token ? { token, password: token } : null,
|
||||||
|
req,
|
||||||
|
trustedProxies,
|
||||||
|
});
|
||||||
|
if (!authResult.ok) {
|
||||||
|
sendJson(res, 401, {
|
||||||
|
error: "Unauthorized",
|
||||||
|
reason: authResult.reason ?? "Authentication required",
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refresh health snapshot for deep check
|
||||||
|
const health = await refreshGatewayHealthSnapshot({ probe: true });
|
||||||
|
const response = buildHealthResponse(health, true);
|
||||||
|
sendJson(res, 200, response);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
}
|
||||||
@ -10,6 +10,8 @@ import type { WebSocketServer } from "ws";
|
|||||||
import { handleA2uiHttpRequest } from "../canvas-host/a2ui.js";
|
import { handleA2uiHttpRequest } from "../canvas-host/a2ui.js";
|
||||||
import type { CanvasHostHandler } from "../canvas-host/server.js";
|
import type { CanvasHostHandler } from "../canvas-host/server.js";
|
||||||
import { loadConfig } from "../config/config.js";
|
import { loadConfig } from "../config/config.js";
|
||||||
|
import type { GatewayHealthConfig } from "../config/types.gateway.js";
|
||||||
|
import { createHealthEndpointsHandler, type HealthEndpointsHandler } from "./http-health.js";
|
||||||
import type { createSubsystemLogger } from "../logging/subsystem.js";
|
import type { createSubsystemLogger } from "../logging/subsystem.js";
|
||||||
import { handleSlackHttpRequest } from "../slack/http/index.js";
|
import { handleSlackHttpRequest } from "../slack/http/index.js";
|
||||||
import { resolveAgentAvatar } from "../agents/identity-avatar.js";
|
import { resolveAgentAvatar } from "../agents/identity-avatar.js";
|
||||||
@ -211,6 +213,7 @@ export function createGatewayHttpServer(opts: {
|
|||||||
handlePluginRequest?: HooksRequestHandler;
|
handlePluginRequest?: HooksRequestHandler;
|
||||||
resolvedAuth: import("./auth.js").ResolvedGatewayAuth;
|
resolvedAuth: import("./auth.js").ResolvedGatewayAuth;
|
||||||
tlsOptions?: TlsOptions;
|
tlsOptions?: TlsOptions;
|
||||||
|
healthConfig?: GatewayHealthConfig;
|
||||||
}): HttpServer {
|
}): HttpServer {
|
||||||
const {
|
const {
|
||||||
canvasHost,
|
canvasHost,
|
||||||
@ -222,7 +225,14 @@ export function createGatewayHttpServer(opts: {
|
|||||||
handleHooksRequest,
|
handleHooksRequest,
|
||||||
handlePluginRequest,
|
handlePluginRequest,
|
||||||
resolvedAuth,
|
resolvedAuth,
|
||||||
|
healthConfig,
|
||||||
} = opts;
|
} = opts;
|
||||||
|
|
||||||
|
// Create health endpoints handler
|
||||||
|
const handleHealthRequest: HealthEndpointsHandler = createHealthEndpointsHandler({
|
||||||
|
config: healthConfig,
|
||||||
|
resolvedAuth,
|
||||||
|
});
|
||||||
const httpServer: HttpServer = opts.tlsOptions
|
const httpServer: HttpServer = opts.tlsOptions
|
||||||
? createHttpsServer(opts.tlsOptions, (req, res) => {
|
? createHttpsServer(opts.tlsOptions, (req, res) => {
|
||||||
void handleRequest(req, res);
|
void handleRequest(req, res);
|
||||||
@ -238,6 +248,10 @@ export function createGatewayHttpServer(opts: {
|
|||||||
try {
|
try {
|
||||||
const configSnapshot = loadConfig();
|
const configSnapshot = loadConfig();
|
||||||
const trustedProxies = configSnapshot.gateway?.trustedProxies ?? [];
|
const trustedProxies = configSnapshot.gateway?.trustedProxies ?? [];
|
||||||
|
|
||||||
|
// Health endpoints for container orchestration (early, minimal auth)
|
||||||
|
if (await handleHealthRequest(req, res)) return;
|
||||||
|
|
||||||
if (await handleHooksRequest(req, res)) return;
|
if (await handleHooksRequest(req, res)) return;
|
||||||
if (
|
if (
|
||||||
await handleToolsInvokeHttpRequest(req, res, {
|
await handleToolsInvokeHttpRequest(req, res, {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user