fix(browser): preserve request lifetime on Node 24

This commit is contained in:
Vincent Koc
2026-07-08 16:32:18 -07:00
parent f7f2ec2e5b
commit 52044e1345
3 changed files with 60 additions and 11 deletions
+3 -2
View File
@@ -51,6 +51,7 @@ Docs: https://docs.openclaw.ai
### Fixes
- **Browser actions on Node 24:** keep browser request cancellation bound to the client and response lifetime instead of Node 24.16+'s prematurely aborted body-stream signal, preventing valid POST actions from failing after JSON parsing. Thanks @obviyus and @vincentkoc.
- **SecretRef model credentials:** keep resolved provider secrets behind process-local sentinels through auth storage, stream setup, SDK configuration, and managed local-provider probing, then inject plaintext only at the final network or provider-plugin boundary while retaining exact-value log redaction. (#102008, #102009)
- **Lean local model shell access:** keep `exec` directly visible beside the default structured Tool Search controls so coding-tuned local models can use their shell fallback instead of searching for missing domain tools. (#87587) Thanks @vincentkoc and @maweibin.
- **OAuth refresh contention diagnostics:** keep local lock paths out of user-facing refresh failures and avoid duplicate failure prefixes while preserving structured provider and profile classification. (#83383) Thanks @vincentkoc.
@@ -75,7 +76,7 @@ Docs: https://docs.openclaw.ai
- **Microsoft Teams attachment metadata:** bound Bot Framework `attachmentInfo` JSON reads and cancel oversized streams before they can exhaust Gateway memory. (#99125) Thanks @ly85206559.
- **Agent auth copy order:** preserve the source agent's portable auth-profile precedence when copying credentials to a new agent while excluding skipped profiles and transient auth state. (#100833) Thanks @machine3at.
- **Memory session repair:** keep daily dreaming ingestion bookkeeping outside session-corpus audit and repair so `memory status --fix` preserves healthy daily state. (#93389) Thanks @Alix-007 and @vincentkoc.
- **Remote browser CDP policy:** allow the configured CDP control host through an existing hostname allowlist without widening page navigation policy, while keeping strict-policy discovery bound to the configured control authority. (#100986, #100819) Thanks @NianJiuZst and @SarinV.
- **Remote browser CDP policy:** allow the configured CDP control host through an existing hostname allowlist without widening page navigation policy, while keeping strict-policy discovery bound to the configured control authority. (#100986, #100819) Thanks @NianJiuZst, @SarinV, and @vincentkoc.
- **Config unset diagnostics:** explain when an inherited or default configuration value cannot be unset instead of reporting a misleading successful deletion. (#96557) Thanks @moeghashim.
- **Crestodian command probes:** contain stdout and stderr stream failures while keeping child-process close and spawn errors authoritative, preventing unhandled probe crashes. (#100741) Thanks @lsr911.
- **Feishu mention forwarding:** fail closed when the bot Open ID is unavailable so group messages cannot be misclassified as explicit bot mentions. (#100891) Thanks @zhangguiping-xydt.
@@ -202,7 +203,7 @@ Docs: https://docs.openclaw.ai
### Complete contribution record
This audited record covers the complete v2026.6.11..b81666ca6af25c86cc099983a4358cdc5ea9ced8 history: 1974 merged PRs. The generation manifest also supplies direct commits as editorial input; the grouped notes above prioritize user impact.
This audited record covers the complete v2026.6.11..HEAD history: 1974 merged PRs. The generation manifest also supplies direct commits as editorial input; the grouped notes above prioritize user impact.
#### Pull requests
@@ -0,0 +1,51 @@
import { EventEmitter } from "node:events";
import type { Express, NextFunction, Request, Response } from "express";
import { describe, expect, it, vi } from "vitest";
import { installBrowserCommonMiddleware } from "./server-middleware.js";
type Middleware = (req: Request, res: Response, next: NextFunction) => void;
describe("installBrowserCommonMiddleware", () => {
it("shadows native request signals with the browser response-lifetime signal", () => {
const middleware: Middleware[] = [];
const app = {
use: vi.fn((...handlers: unknown[]) => {
for (const handler of handlers) {
if (typeof handler === "function") {
middleware.push(handler as Middleware);
}
}
return app;
}),
} as unknown as Express;
installBrowserCommonMiddleware(app);
const nativeController = new AbortController();
const req = new EventEmitter() as EventEmitter & Request;
const requestPrototype = Object.create(Object.getPrototypeOf(req)) as object;
Object.defineProperty(requestPrototype, "signal", {
configurable: true,
get: () => nativeController.signal,
});
Object.setPrototypeOf(req, requestPrototype);
const res = new EventEmitter() as EventEmitter & Response;
Object.defineProperty(res, "writableEnded", { value: false, writable: true });
const next = vi.fn();
const commonMiddleware = middleware[0];
if (!commonMiddleware) {
throw new Error("browser common middleware was not installed");
}
commonMiddleware(req, res, next);
expect(next).toHaveBeenCalledOnce();
expect(Object.hasOwn(req, "signal")).toBe(true);
expect(req.signal).not.toBe(nativeController.signal);
expect(req.signal.aborted).toBe(false);
req.emit("aborted");
expect(req.signal.aborted).toBe(true);
expect(req.signal.reason).toEqual(new Error("request aborted"));
});
});
@@ -33,15 +33,12 @@ export function installBrowserCommonMiddleware(app: Express) {
abort();
}
});
// Make the signal available to browser route handlers on Node versions
// whose IncomingMessage does not already expose a native read-only signal.
const requestWithSignal = req as Request & { signal?: AbortSignal };
if (!(requestWithSignal.signal instanceof AbortSignal)) {
Object.defineProperty(req, "signal", {
value: ctrl.signal,
configurable: true,
});
}
// Node 24.16+'s native request signal aborts when a POST body finishes.
// Browser work follows the client/response lifetime instead.
Object.defineProperty(req, "signal", {
value: ctrl.signal,
configurable: true,
});
next();
});
app.use(express.json({ limit: "1mb" }));