mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 07:10:43 +00:00
fix(gateway): keep liveness probes independent of config load
This commit is contained in:
@@ -265,6 +265,27 @@ describe("gateway probe endpoints", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("serves /healthz before loading gateway config", async () => {
|
||||
const loadConfig = vi.fn(() => {
|
||||
throw new Error("config load blocked");
|
||||
});
|
||||
|
||||
await withGatewayServer({
|
||||
prefix: "probe-healthz-before-config",
|
||||
resolvedAuth: AUTH_NONE,
|
||||
overrides: { loadConfig },
|
||||
run: async (server) => {
|
||||
const req = createRequest({ path: "/healthz" });
|
||||
const { res, getBody } = createResponse();
|
||||
await dispatchRequest(server, req, res);
|
||||
|
||||
expect(res.statusCode).toBe(200);
|
||||
expect(getBody()).toBe(JSON.stringify({ ok: true, status: "live" }));
|
||||
expect(loadConfig).not.toHaveBeenCalled();
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("serves probes before stalled request stages", async () => {
|
||||
const handleHooksRequest = vi.fn((): Promise<boolean> => new Promise(() => {}));
|
||||
const getReadiness = vi.fn(() => ({
|
||||
|
||||
@@ -900,6 +900,7 @@ export function createGatewayHttpServer(opts: {
|
||||
/** Optional rate limiter for auth brute-force protection. */
|
||||
rateLimiter?: AuthRateLimiter;
|
||||
getReadiness?: ReadinessChecker;
|
||||
loadConfig?: () => OpenClawConfig;
|
||||
tlsOptions?: TlsOptions;
|
||||
}): HttpServer {
|
||||
const {
|
||||
@@ -921,6 +922,7 @@ export function createGatewayHttpServer(opts: {
|
||||
getReadiness,
|
||||
} = opts;
|
||||
const getResolvedAuth = opts.getResolvedAuth ?? (() => resolvedAuth);
|
||||
const loadGatewayConfig = opts.loadConfig ?? loadConfig;
|
||||
const openAiCompatEnabled = openAiChatCompletionsEnabled || openResponsesEnabled;
|
||||
const httpServer: HttpServer = opts.tlsOptions
|
||||
? createHttpsServer(opts.tlsOptions, (req, res) => {
|
||||
@@ -947,7 +949,21 @@ export function createGatewayHttpServer(opts: {
|
||||
}
|
||||
|
||||
try {
|
||||
const configSnapshot = loadConfig();
|
||||
const requestPath = new URL(req.url ?? "/", "http://localhost").pathname;
|
||||
if (GATEWAY_PROBE_STATUS_BY_PATH.get(requestPath) === "live") {
|
||||
await handleGatewayProbeRequest(
|
||||
req,
|
||||
res,
|
||||
requestPath,
|
||||
getResolvedAuth(),
|
||||
[],
|
||||
false,
|
||||
getReadiness,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const configSnapshot = loadGatewayConfig();
|
||||
const trustedProxies = configSnapshot.gateway?.trustedProxies ?? [];
|
||||
const allowRealIpFallback = configSnapshot.gateway?.allowRealIpFallback === true;
|
||||
const scopedCanvas = normalizeCanvasScopedUrl(req.url ?? "/");
|
||||
@@ -958,9 +974,9 @@ export function createGatewayHttpServer(opts: {
|
||||
if (scopedCanvas.rewrittenUrl) {
|
||||
req.url = scopedCanvas.rewrittenUrl;
|
||||
}
|
||||
const requestPath = new URL(req.url ?? "/", "http://localhost").pathname;
|
||||
const scopedRequestPath = new URL(req.url ?? "/", "http://localhost").pathname;
|
||||
const pluginPathContext = handlePluginRequest
|
||||
? resolvePluginRoutePathContext(requestPath)
|
||||
? resolvePluginRoutePathContext(scopedRequestPath)
|
||||
: null;
|
||||
const resolvedAuth = getResolvedAuth();
|
||||
const requestStages: GatewayHttpRequestStage[] = [
|
||||
@@ -970,7 +986,7 @@ export function createGatewayHttpServer(opts: {
|
||||
handleGatewayProbeRequest(
|
||||
req,
|
||||
res,
|
||||
requestPath,
|
||||
scopedRequestPath,
|
||||
resolvedAuth,
|
||||
trustedProxies,
|
||||
allowRealIpFallback,
|
||||
@@ -982,7 +998,7 @@ export function createGatewayHttpServer(opts: {
|
||||
run: () => handleHooksRequest(req, res),
|
||||
},
|
||||
];
|
||||
if (openAiCompatEnabled && isOpenAiModelsPath(requestPath)) {
|
||||
if (openAiCompatEnabled && isOpenAiModelsPath(scopedRequestPath)) {
|
||||
requestStages.push({
|
||||
name: "models",
|
||||
run: async () =>
|
||||
@@ -994,7 +1010,7 @@ export function createGatewayHttpServer(opts: {
|
||||
}),
|
||||
});
|
||||
}
|
||||
if (openAiCompatEnabled && isEmbeddingsPath(requestPath)) {
|
||||
if (openAiCompatEnabled && isEmbeddingsPath(scopedRequestPath)) {
|
||||
requestStages.push({
|
||||
name: "embeddings",
|
||||
run: async () =>
|
||||
@@ -1006,7 +1022,7 @@ export function createGatewayHttpServer(opts: {
|
||||
}),
|
||||
});
|
||||
}
|
||||
if (isToolsInvokePath(requestPath)) {
|
||||
if (isToolsInvokePath(scopedRequestPath)) {
|
||||
requestStages.push({
|
||||
name: "tools-invoke",
|
||||
run: async () =>
|
||||
@@ -1018,7 +1034,7 @@ export function createGatewayHttpServer(opts: {
|
||||
}),
|
||||
});
|
||||
}
|
||||
if (isSessionKillPath(requestPath)) {
|
||||
if (isSessionKillPath(scopedRequestPath)) {
|
||||
requestStages.push({
|
||||
name: "sessions-kill",
|
||||
run: async () =>
|
||||
@@ -1030,7 +1046,7 @@ export function createGatewayHttpServer(opts: {
|
||||
}),
|
||||
});
|
||||
}
|
||||
if (isSessionHistoryPath(requestPath)) {
|
||||
if (isSessionHistoryPath(scopedRequestPath)) {
|
||||
requestStages.push({
|
||||
name: "sessions-history",
|
||||
run: async () =>
|
||||
@@ -1043,7 +1059,7 @@ export function createGatewayHttpServer(opts: {
|
||||
}),
|
||||
});
|
||||
}
|
||||
if (openResponsesEnabled && isOpenResponsesPath(requestPath)) {
|
||||
if (openResponsesEnabled && isOpenResponsesPath(scopedRequestPath)) {
|
||||
requestStages.push({
|
||||
name: "openresponses",
|
||||
run: async () =>
|
||||
@@ -1056,7 +1072,7 @@ export function createGatewayHttpServer(opts: {
|
||||
}),
|
||||
});
|
||||
}
|
||||
if (openAiChatCompletionsEnabled && isOpenAiChatCompletionsPath(requestPath)) {
|
||||
if (openAiChatCompletionsEnabled && isOpenAiChatCompletionsPath(scopedRequestPath)) {
|
||||
requestStages.push({
|
||||
name: "openai",
|
||||
run: async () =>
|
||||
@@ -1073,7 +1089,7 @@ export function createGatewayHttpServer(opts: {
|
||||
requestStages.push({
|
||||
name: "canvas-auth",
|
||||
run: async () => {
|
||||
if (!isCanvasPath(requestPath)) {
|
||||
if (!isCanvasPath(scopedRequestPath)) {
|
||||
return false;
|
||||
}
|
||||
const ok = await authorizeCanvasRequest({
|
||||
@@ -1095,7 +1111,7 @@ export function createGatewayHttpServer(opts: {
|
||||
});
|
||||
requestStages.push({
|
||||
name: "a2ui",
|
||||
run: () => (isA2uiPath(requestPath) ? handleA2uiHttpRequest(req, res) : false),
|
||||
run: () => (isA2uiPath(scopedRequestPath) ? handleA2uiHttpRequest(req, res) : false),
|
||||
});
|
||||
requestStages.push({
|
||||
name: "canvas-http",
|
||||
@@ -1109,7 +1125,7 @@ export function createGatewayHttpServer(opts: {
|
||||
...buildPluginRequestStages({
|
||||
req,
|
||||
res,
|
||||
requestPath,
|
||||
requestPath: scopedRequestPath,
|
||||
getGatewayAuthBypassPaths: () => getCachedPluginGatewayAuthBypassPaths(configSnapshot),
|
||||
pluginPathContext,
|
||||
handlePluginRequest,
|
||||
|
||||
@@ -49,7 +49,7 @@ describe("gateway startup log", () => {
|
||||
expect(warn).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("logs a compact ready line with loaded plugin ids and duration", () => {
|
||||
it("logs a compact listening line with loaded plugin ids and duration", () => {
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(new Date("2026-04-03T10:00:16.000Z"));
|
||||
|
||||
@@ -67,9 +67,11 @@ describe("gateway startup log", () => {
|
||||
isNixMode: false,
|
||||
});
|
||||
|
||||
const readyMessages = info.mock.calls
|
||||
const listeningMessages = info.mock.calls
|
||||
.map((call) => call[0])
|
||||
.filter((message) => message.startsWith("ready ("));
|
||||
expect(readyMessages).toEqual(["ready (3 plugins: alpha, beta, delta; 16.0s)"]);
|
||||
.filter((message) => message.startsWith("http server listening ("));
|
||||
expect(listeningMessages).toEqual([
|
||||
"http server listening (3 plugins: alpha, beta, delta; 16.0s)",
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -29,7 +29,9 @@ export function logGatewayStartup(params: {
|
||||
typeof params.startupStartedAt === "number" ? Date.now() - params.startupStartedAt : null;
|
||||
const startupDurationLabel =
|
||||
startupDurationMs == null ? null : `${(startupDurationMs / 1000).toFixed(1)}s`;
|
||||
params.log.info(`ready (${formatReadyDetails(params.loadedPluginIds, startupDurationLabel)})`);
|
||||
params.log.info(
|
||||
`http server listening (${formatReadyDetails(params.loadedPluginIds, startupDurationLabel)})`,
|
||||
);
|
||||
params.log.info(`log file: ${getResolvedLoggerSettings().file}`);
|
||||
if (params.isNixMode) {
|
||||
params.log.info("gateway: running in Nix mode (config managed externally)");
|
||||
|
||||
@@ -157,9 +157,11 @@ describe("startGatewayPostAttachRuntime", () => {
|
||||
it("re-enables startup-gated methods after post-attach sidecars start", async () => {
|
||||
const unavailableGatewayMethods = new Set<string>(["chat.history", "models.list"]);
|
||||
const onSidecarsReady = vi.fn();
|
||||
const log = { info: vi.fn(), warn: vi.fn() };
|
||||
|
||||
await startGatewayPostAttachRuntime({
|
||||
...createPostAttachParams(),
|
||||
log,
|
||||
unavailableGatewayMethods,
|
||||
onSidecarsReady,
|
||||
});
|
||||
@@ -174,6 +176,7 @@ describe("startGatewayPostAttachRuntime", () => {
|
||||
expect(hoisted.logGatewayStartup).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ loadedPluginIds: ["beta", "alpha"] }),
|
||||
);
|
||||
expect(log.info).toHaveBeenCalledWith("gateway ready");
|
||||
expect(hoisted.startGatewayMemoryBackend).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
|
||||
@@ -538,6 +538,7 @@ export async function startGatewayPostAttachRuntime(
|
||||
params.onPluginServices?.(result.pluginServices);
|
||||
params.onSidecarsReady?.();
|
||||
params.startupTrace?.mark("sidecars.ready");
|
||||
params.log.info("gateway ready");
|
||||
return result;
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user