fix(browser): preserve request lifetime on Node 24

This commit is contained in:
Vincent Koc 2026-07-08 16:32:18 -07:00
parent eac4214ff8
commit c0d99ed26e
No known key found for this signature in database
3 changed files with 58 additions and 9 deletions

View file

@ -23,6 +23,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.
- **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.

View file

@ -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"));
});
});

View file

@ -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" }));