Browser: reuse extension relay when relay port is already occupied (#20035)

Merged via /review-pr -> /prepare-pr -> /merge-pr.

Prepared head SHA: b310666d39
Co-authored-by: mbelinky <132747814+mbelinky@users.noreply.github.com>
Co-authored-by: mbelinky <132747814+mbelinky@users.noreply.github.com>
Reviewed-by: @mbelinky
This commit is contained in:
Mariano
2026-02-18 13:13:04 +00:00
committed by GitHub
parent f4db58a5fd
commit 39881a318a
3 changed files with 161 additions and 8 deletions

View File

@@ -1,3 +1,4 @@
import { createServer } from "node:http";
import { afterEach, describe, expect, it } from "vitest";
import WebSocket from "ws";
import {
@@ -152,6 +153,23 @@ describe("chrome extension relay server", () => {
ext.close();
});
it("derives relay auth headers from gateway token for loopback URLs", async () => {
const port = await getFreePort();
const prev = process.env.OPENCLAW_GATEWAY_TOKEN;
process.env.OPENCLAW_GATEWAY_TOKEN = "test-gateway-token";
try {
const headers = getChromeExtensionRelayAuthHeaders(`http://127.0.0.1:${port}`);
expect(Object.keys(headers)).toContain("x-openclaw-relay-token");
expect((headers["x-openclaw-relay-token"] ?? "").length).toBeGreaterThan(20);
} finally {
if (prev === undefined) {
delete process.env.OPENCLAW_GATEWAY_TOKEN;
} else {
process.env.OPENCLAW_GATEWAY_TOKEN = prev;
}
}
});
it("rejects CDP access without relay auth token", async () => {
const port = await getFreePort();
cdpUrl = `http://127.0.0.1:${port}`;
@@ -349,4 +367,57 @@ describe("chrome extension relay server", () => {
cdp.close();
ext.close();
});
it("reuses an already-bound relay port when another process owns it", async () => {
const port = await getFreePort();
const fakeRelay = createServer((req, res) => {
if (req.url?.startsWith("/extension/status")) {
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify({ connected: false }));
return;
}
res.writeHead(200, { "Content-Type": "text/plain; charset=utf-8" });
res.end("OK");
});
await new Promise<void>((resolve, reject) => {
fakeRelay.listen(port, "127.0.0.1", () => resolve());
fakeRelay.once("error", reject);
});
const prev = process.env.OPENCLAW_GATEWAY_TOKEN;
process.env.OPENCLAW_GATEWAY_TOKEN = "test-gateway-token";
try {
cdpUrl = `http://127.0.0.1:${port}`;
const relay = await ensureChromeExtensionRelayServer({ cdpUrl });
expect(relay.port).toBe(port);
const status = (await fetch(`${cdpUrl}/extension/status`).then((r) => r.json())) as {
connected?: boolean;
};
expect(status.connected).toBe(false);
} finally {
if (prev === undefined) {
delete process.env.OPENCLAW_GATEWAY_TOKEN;
} else {
process.env.OPENCLAW_GATEWAY_TOKEN = prev;
}
await new Promise<void>((resolve) => fakeRelay.close(() => resolve()));
}
});
it("does not swallow EADDRINUSE when occupied port is not an openclaw relay", async () => {
const port = await getFreePort();
const blocker = createServer((_, res) => {
res.writeHead(200, { "Content-Type": "text/plain; charset=utf-8" });
res.end("not-relay");
});
await new Promise<void>((resolve, reject) => {
blocker.listen(port, "127.0.0.1", () => resolve());
blocker.once("error", reject);
});
const blockedUrl = `http://127.0.0.1:${port}`;
await expect(ensureChromeExtensionRelayServer({ cdpUrl: blockedUrl })).rejects.toThrow(
/EADDRINUSE/i,
);
await new Promise<void>((resolve) => blocker.close(() => resolve()));
});
});