fix(gateway): prevent /api/* routes from returning SPA HTML when basePath is empty (#30333)

Merged via squash.

Prepared head SHA: 12591f304e5db80b0a49d44b3adeecace5ce228c
Co-authored-by: Sid-Qin <201593046+Sid-Qin@users.noreply.github.com>
Co-authored-by: velvet-shark <126378+velvet-shark@users.noreply.github.com>
Reviewed-by: @velvet-shark
This commit is contained in:
Sid
2026-03-02 05:23:54 +08:00
committed by GitHub
parent e6049345db
commit c1428e8df9
4 changed files with 23 additions and 2 deletions

View File

@@ -100,6 +100,7 @@ Docs: https://docs.openclaw.ai
- Web UI/Chat sessions: add a cron-session visibility toggle in the session selector, fix cron-key detection across `cron:*` and `agent:*:cron:*` formats, and localize the new control labels/tooltips. (#26976) Thanks @ianderrington.
- Web UI/Cron jobs: add schedule-kind and last-run-status filters to the Jobs list, with reset control and client-side filtering over loaded results. (#9510) Thanks @guxu11.
- Web UI/Control UI WebSocket defaults: include normalized `gateway.controlUi.basePath` (or inferred nested route base path) in the default `gatewayUrl` so first-load dashboard connections work behind path-based reverse proxies. (#30228) Thanks @gittb.
- Gateway/Control UI API routing: when `gateway.controlUi.basePath` is unset (default), stop serving Control UI SPA HTML for `/api` and `/api/*` so API paths fall through to normal gateway handlers/404 responses instead of `index.html`. (#30333) Fixes #30295. thanks @Sid-Qin.
- Cron/One-shot reliability: retry transient one-shot failures with bounded backoff and configurable retry policy before disabling. (#24435) Thanks .
- Gateway/Cron auditability: add gateway info logs for successful cron create, update, and remove operations. (#25090) Thanks .
- Cron/Schedule errors: notify users when a job is auto-disabled after repeated schedule computation failures. (#29098) Thanks .

View File

@@ -1325,7 +1325,6 @@ describe("Cron issue regressions", () => {
});
it("respects abort signals while retrying main-session wake-now heartbeat runs", async () => {
vi.useRealTimers();
const abortController = new AbortController();
const runHeartbeatOnce = vi.fn(
async (): Promise<HeartbeatRunResult> => ({
@@ -1364,7 +1363,10 @@ describe("Cron issue regressions", () => {
abortController.abort();
}, 10);
const result = await executeJobCore(state, mainJob, abortController.signal);
const resultPromise = executeJobCore(state, mainJob, abortController.signal);
// Advance virtual time so the abort fires before the busy-wait fallback window expires.
await vi.advanceTimersByTimeAsync(10);
const result = await resultPromise;
expect(result.status).toBe("error");
expect(result.error).toContain("timed out");

View File

@@ -326,6 +326,21 @@ describe("handleControlUiHttpRequest", () => {
});
});
it("does not handle /api paths when basePath is empty", async () => {
await withControlUiRoot({
fn: async (tmp) => {
for (const apiPath of ["/api", "/api/sessions", "/api/channels/nostr"]) {
const { handled } = runControlUiRequest({
url: apiPath,
method: "GET",
rootPath: tmp,
});
expect(handled, `expected ${apiPath} to not be handled`).toBe(false);
}
},
});
});
it("rejects absolute-path escape attempts under basePath routes", async () => {
await withBasePathRootFixture({
siblingDir: "ui-secrets",

View File

@@ -292,6 +292,9 @@ export function handleControlUiHttpRequest(
respondNotFound(res);
return true;
}
if (pathname === "/api" || pathname.startsWith("/api/")) {
return false;
}
}
if (basePath) {