mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 10:30:44 +00:00
fix(gateway): reserve health probes before route stages
This commit is contained in:
@@ -79,6 +79,7 @@ Docs: https://docs.openclaw.ai
|
||||
binary when the config was last written by a newer version, preventing
|
||||
split-brain installs from stopping or rewriting newer gateway services. Fixes
|
||||
#57079.
|
||||
- Gateway: reserve `/healthz` and `/readyz` ahead of plugin, canvas, and Control UI HTTP stages so liveness/readiness probes still answer when a later route handler stalls. Fixes #69674. Thanks @Xike-Creek.
|
||||
- Agents/groups: treat clean empty assistant stops as silent `NO_REPLY` only for always-on groups where silent replies are allowed, while keeping direct and mention-gated sessions on the incomplete-turn retry path. Thanks @MagnaAI.
|
||||
- macOS/Node: keep native remote app nodes from advertising `browser.proxy`,
|
||||
start browser-capable CLI node services through the restored
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
AUTH_TOKEN,
|
||||
AUTH_NONE,
|
||||
@@ -265,6 +265,41 @@ describe("gateway probe endpoints", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("serves probes before stalled request stages", async () => {
|
||||
const handleHooksRequest = vi.fn((): Promise<boolean> => new Promise(() => {}));
|
||||
const getReadiness = vi.fn(() => ({
|
||||
ready: true,
|
||||
failing: [],
|
||||
uptimeMs: 123,
|
||||
}));
|
||||
|
||||
await withGatewayServer({
|
||||
prefix: "probe-before-stalled-stages",
|
||||
resolvedAuth: AUTH_NONE,
|
||||
overrides: { getReadiness, handleHooksRequest },
|
||||
run: async (server) => {
|
||||
const healthReq = createRequest({ path: "/healthz" });
|
||||
const healthResponse = createResponse();
|
||||
await dispatchRequest(server, healthReq, healthResponse.res);
|
||||
|
||||
expect(healthResponse.res.statusCode).toBe(200);
|
||||
expect(healthResponse.getBody()).toBe(JSON.stringify({ ok: true, status: "live" }));
|
||||
|
||||
const readyReq = createRequest({ path: "/readyz" });
|
||||
const readyResponse = createResponse();
|
||||
await dispatchRequest(server, readyReq, readyResponse.res);
|
||||
|
||||
expect(readyResponse.res.statusCode).toBe(200);
|
||||
expect(JSON.parse(readyResponse.getBody())).toEqual({
|
||||
ready: true,
|
||||
failing: [],
|
||||
uptimeMs: 123,
|
||||
});
|
||||
expect(handleHooksRequest).not.toHaveBeenCalled();
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("reflects readiness status on HEAD /readyz without a response body", async () => {
|
||||
const getReadiness: ReadinessChecker = () => ({
|
||||
ready: false,
|
||||
|
||||
@@ -945,6 +945,19 @@ export function createGatewayHttpServer(opts: {
|
||||
: null;
|
||||
const resolvedAuth = getResolvedAuth();
|
||||
const requestStages: GatewayHttpRequestStage[] = [
|
||||
{
|
||||
name: "gateway-probes",
|
||||
run: () =>
|
||||
handleGatewayProbeRequest(
|
||||
req,
|
||||
res,
|
||||
requestPath,
|
||||
resolvedAuth,
|
||||
trustedProxies,
|
||||
allowRealIpFallback,
|
||||
getReadiness,
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "hooks",
|
||||
run: () => handleHooksRequest(req, res),
|
||||
@@ -1150,20 +1163,6 @@ export function createGatewayHttpServer(opts: {
|
||||
});
|
||||
}
|
||||
|
||||
requestStages.push({
|
||||
name: "gateway-probes",
|
||||
run: () =>
|
||||
handleGatewayProbeRequest(
|
||||
req,
|
||||
res,
|
||||
requestPath,
|
||||
resolvedAuth,
|
||||
trustedProxies,
|
||||
allowRealIpFallback,
|
||||
getReadiness,
|
||||
),
|
||||
});
|
||||
|
||||
if (await runGatewayHttpRequestStages(requestStages)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -49,14 +49,14 @@ function createHealthzPluginHandler() {
|
||||
});
|
||||
}
|
||||
|
||||
async function expectHealthzPluginShadow(params: {
|
||||
async function expectHealthzProbeReserved(params: {
|
||||
server: Parameters<typeof sendRequest>[0];
|
||||
handlePluginRequest: ReturnType<typeof createHealthzPluginHandler>;
|
||||
}) {
|
||||
const response = await sendRequest(params.server, { path: "/healthz" });
|
||||
expect(response.res.statusCode).toBe(200);
|
||||
expect(response.getBody()).toBe(JSON.stringify({ ok: true, route: "plugin-health" }));
|
||||
expect(params.handlePluginRequest).toHaveBeenCalledTimes(1);
|
||||
expect(response.getBody()).toBe(JSON.stringify({ ok: true, status: "live" }));
|
||||
expect(params.handlePluginRequest).not.toHaveBeenCalled();
|
||||
}
|
||||
|
||||
function createMattermostCallbackConfig(callbackPath: string) {
|
||||
@@ -197,7 +197,7 @@ describe("gateway plugin HTTP auth boundary", () => {
|
||||
});
|
||||
});
|
||||
|
||||
test("does not shadow plugin routes mounted on probe paths", async () => {
|
||||
test("reserves gateway probe routes ahead of plugin routes", async () => {
|
||||
const handlePluginRequest = createHealthzPluginHandler();
|
||||
|
||||
await withGatewayServer({
|
||||
@@ -205,7 +205,7 @@ describe("gateway plugin HTTP auth boundary", () => {
|
||||
resolvedAuth: AUTH_NONE,
|
||||
overrides: { handlePluginRequest },
|
||||
run: async (server) => {
|
||||
await expectHealthzPluginShadow({ server, handlePluginRequest });
|
||||
await expectHealthzProbeReserved({ server, handlePluginRequest });
|
||||
},
|
||||
});
|
||||
});
|
||||
@@ -697,19 +697,19 @@ describe("gateway plugin HTTP auth boundary", () => {
|
||||
handlePluginRequest,
|
||||
run: async (server) => {
|
||||
await expectProbeRoutesHealthy(server);
|
||||
expect(handlePluginRequest).toHaveBeenCalledTimes(PROBE_CASES.length);
|
||||
expect(handlePluginRequest).not.toHaveBeenCalled();
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test("root-mounted control ui still lets plugins claim probe paths first", async () => {
|
||||
test("root-mounted control ui keeps gateway probe routes reserved ahead of plugins", async () => {
|
||||
const handlePluginRequest = createHealthzPluginHandler();
|
||||
|
||||
await withRootMountedControlUiServer({
|
||||
prefix: "openclaw-plugin-http-control-ui-probe-shadow-test-",
|
||||
handlePluginRequest,
|
||||
run: async (server) => {
|
||||
await expectHealthzPluginShadow({ server, handlePluginRequest });
|
||||
await expectHealthzProbeReserved({ server, handlePluginRequest });
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user