mirror of https://github.com/openclaw/openclaw.git
fix(gateway): keep probe routes reachable with root-mounted control ui (#38199)
* fix(gateway): keep probe routes reachable with root-mounted control ui * Changelog: add root-mounted probe precedence fix entry * Update CHANGELOG.md
This commit is contained in:
parent
4ed5febc38
commit
d070c44091
|
|
@ -205,6 +205,7 @@ Docs: https://docs.openclaw.ai
|
|||
- WhatsApp media upload caps: make outbound media sends and auto-replies honor `channels.whatsapp.mediaMaxMb` with per-account overrides so inbound and outbound limits use the same channel config. Thanks @vincentkoc.
|
||||
- Windows/Plugin install: when OpenClaw runs on Windows via Bun and `npm-cli.js` is not colocated with the runtime binary, fall back to `npm.cmd`/`npx.cmd` through the existing `cmd.exe` wrapper so `openclaw plugins install` no longer fails with `spawn EINVAL`. (#38056) Thanks @0xlin2023.
|
||||
- Telegram/send retry classification: retry grammY `Network request ... failed after N attempts` envelopes in send flows without reclassifying plain `Network request ... failed!` wrappers as transient, restoring the intended retry path while keeping broad send-context message matching tight. (#38056) Thanks @0xlin2023.
|
||||
- Gateway/probe route precedence: keep `/health`, `/healthz`, `/ready`, and `/readyz` reachable when the Control UI is mounted at `/`, so root-mounted SPA fallbacks no longer swallow machine probe routes while plugin-owned routes on those paths still keep precedence. (#18446) Thanks @vibecodooor and @vincentkoc.
|
||||
|
||||
## 2026.3.2
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@ export type ControlUiRequestClassification =
|
|||
| { kind: "redirect"; location: string }
|
||||
| { kind: "serve" };
|
||||
|
||||
const ROOT_MOUNTED_GATEWAY_PROBE_PATHS = new Set(["/health", "/healthz", "/ready", "/readyz"]);
|
||||
|
||||
export function classifyControlUiRequest(params: {
|
||||
basePath: string;
|
||||
pathname: string;
|
||||
|
|
@ -17,6 +19,11 @@ export function classifyControlUiRequest(params: {
|
|||
if (pathname === "/ui" || pathname.startsWith("/ui/")) {
|
||||
return { kind: "not-found" };
|
||||
}
|
||||
// Keep core probe routes outside the root-mounted SPA catch-all so the
|
||||
// gateway probe handler can answer them even when the Control UI owns `/`.
|
||||
if (ROOT_MOUNTED_GATEWAY_PROBE_PATHS.has(pathname)) {
|
||||
return { kind: "not-control-ui" };
|
||||
}
|
||||
// Keep plugin-owned HTTP routes outside the root-mounted Control UI SPA
|
||||
// fallback so untrusted plugins cannot claim arbitrary UI paths.
|
||||
if (pathname === "/plugins" || pathname.startsWith("/plugins/")) {
|
||||
|
|
|
|||
|
|
@ -494,6 +494,58 @@ describe("gateway plugin HTTP auth boundary", () => {
|
|||
});
|
||||
});
|
||||
|
||||
test("root-mounted control ui does not swallow gateway probe routes", async () => {
|
||||
const handlePluginRequest = vi.fn(async () => false);
|
||||
|
||||
await withRootMountedControlUiServer({
|
||||
prefix: "openclaw-plugin-http-control-ui-probes-test-",
|
||||
handlePluginRequest,
|
||||
run: async (server) => {
|
||||
const probeCases = [
|
||||
{ path: "/health", status: "live" },
|
||||
{ path: "/healthz", status: "live" },
|
||||
{ path: "/ready", status: "ready" },
|
||||
{ path: "/readyz", status: "ready" },
|
||||
] as const;
|
||||
|
||||
for (const probeCase of probeCases) {
|
||||
const response = await sendRequest(server, { path: probeCase.path });
|
||||
expect(response.res.statusCode, probeCase.path).toBe(200);
|
||||
expect(response.getBody(), probeCase.path).toBe(
|
||||
JSON.stringify({ ok: true, status: probeCase.status }),
|
||||
);
|
||||
}
|
||||
|
||||
expect(handlePluginRequest).toHaveBeenCalledTimes(probeCases.length);
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test("root-mounted control ui still lets plugins claim probe paths first", async () => {
|
||||
const handlePluginRequest = vi.fn(async (req: IncomingMessage, res: ServerResponse) => {
|
||||
const pathname = new URL(req.url ?? "/", "http://localhost").pathname;
|
||||
if (pathname !== "/healthz") {
|
||||
return false;
|
||||
}
|
||||
res.statusCode = 200;
|
||||
res.setHeader("Content-Type", "application/json; charset=utf-8");
|
||||
res.end(JSON.stringify({ ok: true, route: "plugin-health" }));
|
||||
return true;
|
||||
});
|
||||
|
||||
await withRootMountedControlUiServer({
|
||||
prefix: "openclaw-plugin-http-control-ui-probe-shadow-test-",
|
||||
handlePluginRequest,
|
||||
run: async (server) => {
|
||||
const response = await sendRequest(server, { path: "/healthz" });
|
||||
|
||||
expect(response.res.statusCode).toBe(200);
|
||||
expect(response.getBody()).toBe(JSON.stringify({ ok: true, route: "plugin-health" }));
|
||||
expect(handlePluginRequest).toHaveBeenCalledTimes(1);
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test("requires gateway auth for canonicalized /api/channels variants", async () => {
|
||||
const handlePluginRequest = createCanonicalizedChannelPluginHandler();
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue