Skip to content

Commit a8003b2

Browse files
committed
fix(rivetkit): normalize endpoints before comparing them
1 parent b0175ac commit a8003b2

File tree

7 files changed

+184
-10
lines changed

7 files changed

+184
-10
lines changed

rivetkit-typescript/packages/rivetkit/src/client/config.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import { tryParseEndpoint } from "@/utils/endpoint-parser";
1616
*
1717
* In browser: uses current origin + /api/rivet
1818
*
19-
* Server-side: uses localhost:6420
19+
* Server-side: uses 127.0.0.1:6420
2020
*/
2121
function getDefaultEndpoint(): string {
2222
if (typeof window !== "undefined" && window.location?.origin) {
@@ -38,7 +38,7 @@ export const ClientConfigSchemaBase = z.object({
3838
*
3939
* Can also be set via RIVET_ENDPOINT environment variables.
4040
*
41-
* Defaults to current origin + /api/rivet in browser, or localhost:6420 server-side.
41+
* Defaults to current origin + /api/rivet in browser, or 127.0.0.1:6420 server-side.
4242
*/
4343
endpoint: z
4444
.string()
@@ -88,7 +88,8 @@ export const ClientConfigSchemaBase = z.object({
8888
.default(
8989
() =>
9090
typeof window !== "undefined" &&
91-
window?.location?.hostname === "localhost",
91+
(window?.location?.hostname === "127.0.0.1" ||
92+
window.location?.hostname === "localhost"),
9293
),
9394
});
9495

@@ -144,6 +145,7 @@ export function convertRegistryConfigToClientConfig(
144145
disableMetadataLookup: true,
145146
devtools:
146147
typeof window !== "undefined" &&
147-
window?.location?.hostname === "localhost",
148+
(window?.location?.hostname === "127.0.0.1" ||
149+
window?.location?.hostname === "localhost"),
148150
};
149151
}
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
export const ENGINE_PORT = 6420;
2-
export const ENGINE_ENDPOINT = `http://localhost:${ENGINE_PORT}`;
2+
export const ENGINE_ENDPOINT = `http://127.0.0.1:${ENGINE_PORT}`;

rivetkit-typescript/packages/rivetkit/src/engine-process/mod.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -308,7 +308,7 @@ async function checkIfEngineAlreadyRunningOnPort(
308308
): Promise<boolean> {
309309
let response: Response;
310310
try {
311-
response = await fetch(`http://localhost:${port}/health`);
311+
response = await fetch(`http://127.0.0.1:${port}/health`);
312312
} catch (err) {
313313
// Nothing is running on this port
314314
return false;

rivetkit-typescript/packages/rivetkit/src/inspector/utils.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ export function getInspectorUrl(
5656
const endpoint =
5757
config.inspector.defaultEndpoint ??
5858
(config.managerPort !== 6420
59-
? `http://localhost:${managerPort}`
59+
? `http://127.0.0.1:${managerPort}`
6060
: undefined);
6161
if (endpoint) {
6262
url.searchParams.set("u", endpoint);

rivetkit-typescript/packages/rivetkit/src/registry/config/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -213,11 +213,11 @@ export const RegistryConfigSchema = z
213213
// Determine serveManager: default to true in dev mode without endpoint, false otherwise
214214
const serveManager = config.serveManager ?? (isDevEnv && !endpoint);
215215

216-
// In dev mode, fall back to localhost if serving manager
216+
// In dev mode, fall back to 127.0.0.1 if serving manager
217217
const publicEndpoint =
218218
parsedPublicEndpoint?.endpoint ??
219219
(isDevEnv && (serveManager || config.serverless.spawnEngine)
220-
? `http://localhost:${config.managerPort}`
220+
? `http://127.0.0.1:${config.managerPort}`
221221
: undefined);
222222
// We extract publicNamespace to validate that it matches the backend
223223
// namespace (see validation above), not for functional use.
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
import { describe, expect, test } from "vitest";
2+
import { endpointsMatch, normalizeEndpointUrl } from "./router";
3+
4+
describe("normalizeEndpointUrl", () => {
5+
test("normalizes URL without trailing slash", () => {
6+
expect(normalizeEndpointUrl("http://localhost:6420")).toBe(
7+
"http://localhost:6420/",
8+
);
9+
});
10+
11+
test("normalizes URL with trailing slash", () => {
12+
expect(normalizeEndpointUrl("http://localhost:6420/")).toBe(
13+
"http://localhost:6420/",
14+
);
15+
});
16+
17+
test("normalizes 127.0.0.1 to localhost", () => {
18+
expect(normalizeEndpointUrl("http://127.0.0.1:6420")).toBe(
19+
"http://localhost:6420/",
20+
);
21+
});
22+
23+
test("normalizes 0.0.0.0 to localhost", () => {
24+
expect(normalizeEndpointUrl("http://0.0.0.0:6420")).toBe(
25+
"http://localhost:6420/",
26+
);
27+
});
28+
29+
test("preserves path without trailing slash", () => {
30+
expect(normalizeEndpointUrl("http://example.com/api/v1")).toBe(
31+
"http://example.com/api/v1",
32+
);
33+
});
34+
35+
test("removes trailing slash from path", () => {
36+
expect(normalizeEndpointUrl("http://example.com/api/v1/")).toBe(
37+
"http://example.com/api/v1",
38+
);
39+
});
40+
41+
test("removes multiple trailing slashes", () => {
42+
expect(normalizeEndpointUrl("http://example.com/api///")).toBe(
43+
"http://example.com/api",
44+
);
45+
});
46+
47+
test("preserves port", () => {
48+
expect(normalizeEndpointUrl("https://localhost:3000/api")).toBe(
49+
"https://localhost:3000/api",
50+
);
51+
});
52+
53+
test("strips query string", () => {
54+
expect(normalizeEndpointUrl("http://example.com/api?foo=bar")).toBe(
55+
"http://example.com/api",
56+
);
57+
});
58+
59+
test("strips fragment", () => {
60+
expect(normalizeEndpointUrl("http://example.com/api#section")).toBe(
61+
"http://example.com/api",
62+
);
63+
});
64+
65+
test("returns null for invalid URL", () => {
66+
expect(normalizeEndpointUrl("not-a-url")).toBeNull();
67+
});
68+
69+
test("returns null for empty string", () => {
70+
expect(normalizeEndpointUrl("")).toBeNull();
71+
});
72+
});
73+
74+
describe("endpointsMatch", () => {
75+
test("matches identical URLs", () => {
76+
expect(
77+
endpointsMatch("http://127.0.0.1:6420", "http://127.0.0.1:6420"),
78+
).toBe(true);
79+
});
80+
81+
test("matches URL with and without trailing slash", () => {
82+
expect(
83+
endpointsMatch("http://127.0.0.1:6420", "http://127.0.0.1:6420/"),
84+
).toBe(true);
85+
});
86+
87+
test("matches URLs with paths ignoring trailing slash", () => {
88+
expect(
89+
endpointsMatch("http://example.com/api/v1", "http://example.com/api/v1/"),
90+
).toBe(true);
91+
});
92+
93+
test("matches localhost and 127.0.0.1", () => {
94+
expect(
95+
endpointsMatch("http://localhost:6420", "http://127.0.0.1:6420"),
96+
).toBe(true);
97+
});
98+
99+
test("matches localhost and 0.0.0.0", () => {
100+
expect(
101+
endpointsMatch("http://localhost:6420", "http://0.0.0.0:6420"),
102+
).toBe(true);
103+
});
104+
105+
test("does not match different hosts", () => {
106+
expect(
107+
endpointsMatch("http://localhost:6420", "http://example.com:6420"),
108+
).toBe(false);
109+
});
110+
111+
test("does not match different ports", () => {
112+
expect(
113+
endpointsMatch("http://localhost:6420", "http://localhost:3000"),
114+
).toBe(false);
115+
});
116+
117+
test("does not match different protocols", () => {
118+
expect(
119+
endpointsMatch("http://localhost:6420", "https://localhost:6420"),
120+
).toBe(false);
121+
});
122+
123+
test("does not match different paths", () => {
124+
expect(
125+
endpointsMatch("http://example.com/api/v1", "http://example.com/api/v2"),
126+
).toBe(false);
127+
});
128+
129+
test("falls back to string comparison for invalid URLs", () => {
130+
expect(endpointsMatch("not-a-url", "not-a-url")).toBe(true);
131+
expect(endpointsMatch("not-a-url", "different")).toBe(false);
132+
});
133+
});

rivetkit-typescript/packages/rivetkit/src/serverless/router.ts

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ export function buildServerlessRouter(
5959
// configuring an endpoint indicates you want to assert the
6060
// incoming serverless requests.
6161
if (config.endpoint) {
62-
if (endpoint !== config.endpoint) {
62+
if (!endpointsMatch(endpoint, config.endpoint)) {
6363
throw new EndpointMismatch(config.endpoint, endpoint);
6464
}
6565

@@ -121,3 +121,42 @@ export function buildServerlessRouter(
121121
);
122122
});
123123
}
124+
125+
/**
126+
* Normalizes a URL for comparison by extracting protocol, host, port, and pathname.
127+
* Normalizes 127.0.0.1 and 0.0.0.0 to localhost for consistent comparison.
128+
* Returns null if the URL is invalid.
129+
*/
130+
export function normalizeEndpointUrl(url: string): string | null {
131+
try {
132+
const parsed = new URL(url);
133+
// Normalize pathname by removing trailing slash (except for root)
134+
const pathname =
135+
parsed.pathname === "/" ? "/" : parsed.pathname.replace(/\/+$/, "");
136+
// Normalize loopback addresses to localhost
137+
const hostname =
138+
parsed.hostname === "127.0.0.1" || parsed.hostname === "0.0.0.0"
139+
? "localhost"
140+
: parsed.hostname;
141+
// Reconstruct host with normalized hostname and port
142+
const host = parsed.port ? `${hostname}:${parsed.port}` : hostname;
143+
// Reconstruct normalized URL with protocol, host, and pathname
144+
return `${parsed.protocol}//${host}${pathname}`;
145+
} catch {
146+
return null;
147+
}
148+
}
149+
150+
/**
151+
* Compares two endpoint URLs after normalization.
152+
* Returns true if they match (same protocol, host, port, and path).
153+
*/
154+
export function endpointsMatch(a: string, b: string): boolean {
155+
const normalizedA = normalizeEndpointUrl(a);
156+
const normalizedB = normalizeEndpointUrl(b);
157+
if (normalizedA === null || normalizedB === null) {
158+
// If either URL is invalid, fall back to string comparison
159+
return a === b;
160+
}
161+
return normalizedA === normalizedB;
162+
}

0 commit comments

Comments
 (0)