mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-05 15:40:21 +00:00
Matrix: recover from pinned dispatcher runtime failures (#61595)
Merged via squash.
Prepared head SHA: f9a2d9be7f
Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com>
Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com>
Reviewed-by: @gumadeiras
This commit is contained in:
committed by
GitHub
parent
134d309571
commit
427997f989
@@ -2,9 +2,12 @@ import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { MatrixMediaSizeLimitError } from "../media-errors.js";
|
||||
import { performMatrixRequest } from "./transport.js";
|
||||
|
||||
const TEST_UNDICI_RUNTIME_DEPS_KEY = "__OPENCLAW_TEST_UNDICI_RUNTIME_DEPS__";
|
||||
|
||||
describe("performMatrixRequest", () => {
|
||||
beforeEach(() => {
|
||||
vi.unstubAllGlobals();
|
||||
Reflect.deleteProperty(globalThis as object, TEST_UNDICI_RUNTIME_DEPS_KEY);
|
||||
});
|
||||
|
||||
it("rejects oversized raw responses before buffering the whole body", async () => {
|
||||
@@ -107,4 +110,44 @@ describe("performMatrixRequest", () => {
|
||||
vi.useRealTimers();
|
||||
}
|
||||
}, 5_000);
|
||||
|
||||
it("uses undici runtime fetch for pinned Matrix requests so the dispatcher stays bound", async () => {
|
||||
let ambientFetchCalls = 0;
|
||||
vi.stubGlobal("fetch", (async () => {
|
||||
ambientFetchCalls += 1;
|
||||
throw new Error("expected pinned Matrix requests to avoid ambient fetch");
|
||||
}) as typeof fetch);
|
||||
const runtimeFetch = vi.fn(async (_input: RequestInfo | URL, init?: RequestInit) => {
|
||||
const requestInit = init as RequestInit & { dispatcher?: unknown };
|
||||
expect(requestInit.dispatcher).toBeDefined();
|
||||
return new Response('{"ok":true}', {
|
||||
status: 200,
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
},
|
||||
});
|
||||
});
|
||||
(globalThis as Record<string, unknown>)[TEST_UNDICI_RUNTIME_DEPS_KEY] = {
|
||||
Agent: class MockAgent {},
|
||||
EnvHttpProxyAgent: class MockEnvHttpProxyAgent {},
|
||||
ProxyAgent: class MockProxyAgent {},
|
||||
fetch: runtimeFetch,
|
||||
};
|
||||
|
||||
const result = await performMatrixRequest({
|
||||
homeserver: "http://127.0.0.1:8008",
|
||||
accessToken: "token",
|
||||
method: "GET",
|
||||
endpoint: "/_matrix/client/v3/account/whoami",
|
||||
timeoutMs: 5000,
|
||||
ssrfPolicy: { allowPrivateNetwork: true },
|
||||
});
|
||||
|
||||
expect(result.text).toBe('{"ok":true}');
|
||||
expect(ambientFetchCalls).toBe(0);
|
||||
expect(runtimeFetch).toHaveBeenCalledTimes(1);
|
||||
expect(
|
||||
(runtimeFetch.mock.calls[0]?.[1] as RequestInit & { dispatcher?: unknown })?.dispatcher,
|
||||
).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import type { PinnedDispatcherPolicy } from "openclaw/plugin-sdk/infra-runtime";
|
||||
import {
|
||||
fetchWithRuntimeDispatcher,
|
||||
type PinnedDispatcherPolicy,
|
||||
} from "openclaw/plugin-sdk/infra-runtime";
|
||||
import {
|
||||
buildTimeoutAbortSignal,
|
||||
closeDispatcher,
|
||||
@@ -21,6 +24,10 @@ type QueryValue =
|
||||
|
||||
export type QueryParams = Record<string, QueryValue> | null | undefined;
|
||||
|
||||
type MatrixDispatcherRequestInit = RequestInit & {
|
||||
dispatcher?: ReturnType<typeof createPinnedDispatcher>;
|
||||
};
|
||||
|
||||
function normalizeEndpoint(endpoint: string): string {
|
||||
if (!endpoint) {
|
||||
return "/";
|
||||
@@ -84,6 +91,27 @@ function buildBufferedResponse(params: {
|
||||
return response;
|
||||
}
|
||||
|
||||
function isMockedFetch(fetchImpl: typeof fetch | undefined): boolean {
|
||||
if (typeof fetchImpl !== "function") {
|
||||
return false;
|
||||
}
|
||||
return typeof (fetchImpl as typeof fetch & { mock?: unknown }).mock === "object";
|
||||
}
|
||||
|
||||
async function fetchWithMatrixDispatcher(params: {
|
||||
url: string;
|
||||
init: MatrixDispatcherRequestInit;
|
||||
}): Promise<Response> {
|
||||
// Keep this dispatcher-routing logic local to Matrix transport. Shared SSRF
|
||||
// fetches must stay fail-closed unless a retry path can preserve the
|
||||
// validated pinned-address binding. Route dispatcher-attached requests
|
||||
// through undici runtime fetch so the pinned dispatcher is preserved.
|
||||
if (params.init.dispatcher && !isMockedFetch(globalThis.fetch)) {
|
||||
return await fetchWithRuntimeDispatcher(params.url, params.init);
|
||||
}
|
||||
return await fetch(params.url, params.init);
|
||||
}
|
||||
|
||||
async function fetchWithMatrixGuardedRedirects(params: {
|
||||
url: string;
|
||||
init?: RequestInit;
|
||||
@@ -110,15 +138,18 @@ async function fetchWithMatrixGuardedRedirects(params: {
|
||||
policy: params.ssrfPolicy,
|
||||
});
|
||||
dispatcher = createPinnedDispatcher(pinned, params.dispatcherPolicy, params.ssrfPolicy);
|
||||
const response = await fetch(currentUrl.toString(), {
|
||||
...params.init,
|
||||
method,
|
||||
body,
|
||||
headers,
|
||||
redirect: "manual",
|
||||
signal,
|
||||
dispatcher,
|
||||
} as RequestInit & { dispatcher: unknown });
|
||||
const response = await fetchWithMatrixDispatcher({
|
||||
url: currentUrl.toString(),
|
||||
init: {
|
||||
...params.init,
|
||||
method,
|
||||
body,
|
||||
headers,
|
||||
redirect: "manual",
|
||||
signal,
|
||||
dispatcher,
|
||||
} as MatrixDispatcherRequestInit,
|
||||
});
|
||||
|
||||
if (!isRedirectStatus(response.status)) {
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user