mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
fix(matrix): reverse-proxy path support for Matrix homeserver URLs (#93516)
* feat(matrix): add configurable apiPrefix for non-standard homeserver API paths
* fix(matrix): remove public apiPrefix config, keep transport path fix
The apiPrefix config surface was incomplete: it was not carried through
resolveMatrixConfigForAccount, MatrixAuth, shared client keying, or
createMatrixClient, making it a no-op in normal runtime paths.
Following ClawSweeper review direction, this narrow fix:
- Removes apiPrefix from MatrixConfigSchema and MatrixConfig types
- Keeps apiPrefix as a private internal constant in MatrixAuthedHttpClient
for bare-path resolution (DEFAULT_API_PREFIX = /_matrix/client/v3)
- Stops MatrixClient from accepting/passing apiPrefix
- Retains the transport.ts normalizeEndpoint() fix for homeserver URLs
behind reverse proxies (avoids losing path segments on trailing-slash
mismatches)
Test: 1393 tests pass across 121 matrix extension test files.
* fix(matrix): ensure homeserver URL ends with slash for new URL() resolution
Without a trailing slash, new URL(relPath, homeserver) treats the final
path segment as a filename, losing the reverse-proxy prefix path.
For example:
new URL('_matrix/...', 'https://host/proxy')
-> https://host/_matrix/... (wrong, 'proxy' dropped)
With trailing slash:
new URL('_matrix/...', 'https://host/proxy/')
-> https://host/proxy/_matrix/... (correct)
* test(matrix): add URL resolution coverage for path-prefixed homeservers
* fix(matrix): preserve reverse-proxy homeserver paths
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
@@ -29,6 +29,71 @@ describe("performMatrixRequest", () => {
|
||||
clearTestUndiciRuntimeDepsOverride();
|
||||
});
|
||||
|
||||
it.each([
|
||||
{
|
||||
name: "a root homeserver",
|
||||
homeserverPath: "",
|
||||
expectedPath: "/_matrix/client/v3/account/whoami",
|
||||
},
|
||||
{
|
||||
name: "a proxy prefix without a trailing slash",
|
||||
homeserverPath: "/matrix-proxy",
|
||||
expectedPath: "/matrix-proxy/_matrix/client/v3/account/whoami",
|
||||
},
|
||||
{
|
||||
name: "a proxy prefix with a trailing slash",
|
||||
homeserverPath: "/matrix-proxy/",
|
||||
expectedPath: "/matrix-proxy/_matrix/client/v3/account/whoami",
|
||||
},
|
||||
{
|
||||
name: "an encoded nested proxy prefix",
|
||||
homeserverPath: "/proxy%20base/tenant/",
|
||||
expectedPath: "/proxy%20base/tenant/_matrix/client/v3/account/whoami",
|
||||
},
|
||||
])(
|
||||
"preserves $name through the real HTTP transport",
|
||||
async ({ homeserverPath, expectedPath }) => {
|
||||
const requests: Array<{ url: string | undefined; authorization: string | undefined }> = [];
|
||||
const server = http.createServer((request, response) => {
|
||||
requests.push({
|
||||
url: request.url,
|
||||
authorization: request.headers.authorization,
|
||||
});
|
||||
response.writeHead(200, { "content-type": "application/json" });
|
||||
response.end(JSON.stringify({ user_id: "@bot:example.org" }));
|
||||
});
|
||||
await new Promise<void>((resolve) => {
|
||||
server.listen(0, "127.0.0.1", resolve);
|
||||
});
|
||||
const { port } = server.address() as { port: number };
|
||||
|
||||
try {
|
||||
const result = await performMatrixRequest({
|
||||
homeserver: `http://127.0.0.1:${port}${homeserverPath}`,
|
||||
accessToken: "test-token",
|
||||
method: "GET",
|
||||
endpoint: "/_matrix/client/v3/account/whoami",
|
||||
qs: { via: "proxy path" },
|
||||
timeoutMs: 5000,
|
||||
ssrfPolicy: { allowPrivateNetwork: true },
|
||||
});
|
||||
|
||||
expect(result.response.status).toBe(200);
|
||||
expect(JSON.parse(result.text)).toEqual({ user_id: "@bot:example.org" });
|
||||
expect(requests).toEqual([
|
||||
{
|
||||
url: `${expectedPath}?via=proxy+path`,
|
||||
authorization: "Bearer test-token",
|
||||
},
|
||||
]);
|
||||
} finally {
|
||||
await new Promise<void>((resolve) => {
|
||||
server.close(() => resolve());
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
it("rejects oversized raw responses before buffering the whole body", async () => {
|
||||
const cancel = vi.fn();
|
||||
const stream = new ReadableStream<Uint8Array>({ cancel });
|
||||
|
||||
@@ -328,7 +328,7 @@ export async function performMatrixRequest(params: {
|
||||
|
||||
const baseUrl = isAbsoluteEndpoint
|
||||
? new URL(params.endpoint)
|
||||
: new URL(normalizeEndpoint(params.endpoint), params.homeserver);
|
||||
: new URL(`${params.homeserver.replace(/\/+$/u, "")}${normalizeEndpoint(params.endpoint)}`);
|
||||
applyQuery(baseUrl, params.qs);
|
||||
|
||||
const headers = new Headers();
|
||||
|
||||
Reference in New Issue
Block a user