mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-19 11:28:38 +00:00
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:
@@ -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.
|
- 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.
|
- 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.
|
- 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
|
## 2026.3.2
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ export type ControlUiRequestClassification =
|
|||||||
| { kind: "redirect"; location: string }
|
| { kind: "redirect"; location: string }
|
||||||
| { kind: "serve" };
|
| { kind: "serve" };
|
||||||
|
|
||||||
|
const ROOT_MOUNTED_GATEWAY_PROBE_PATHS = new Set(["/health", "/healthz", "/ready", "/readyz"]);
|
||||||
|
|
||||||
export function classifyControlUiRequest(params: {
|
export function classifyControlUiRequest(params: {
|
||||||
basePath: string;
|
basePath: string;
|
||||||
pathname: string;
|
pathname: string;
|
||||||
@@ -17,6 +19,11 @@ export function classifyControlUiRequest(params: {
|
|||||||
if (pathname === "/ui" || pathname.startsWith("/ui/")) {
|
if (pathname === "/ui" || pathname.startsWith("/ui/")) {
|
||||||
return { kind: "not-found" };
|
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
|
// Keep plugin-owned HTTP routes outside the root-mounted Control UI SPA
|
||||||
// fallback so untrusted plugins cannot claim arbitrary UI paths.
|
// fallback so untrusted plugins cannot claim arbitrary UI paths.
|
||||||
if (pathname === "/plugins" || pathname.startsWith("/plugins/")) {
|
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 () => {
|
test("requires gateway auth for canonicalized /api/channels variants", async () => {
|
||||||
const handlePluginRequest = createCanonicalizedChannelPluginHandler();
|
const handlePluginRequest = createCanonicalizedChannelPluginHandler();
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user