Skip to content

Commit ecf123c

Browse files
committed
🤖 fix: bracket IPv6 host URLs in lockfile
Change-Id: I9f29384286bd75aa25000758c887f608df99d607 Signed-off-by: Thomas Kosiewski <[email protected]>
1 parent f8472ce commit ecf123c

File tree

2 files changed

+74
-4
lines changed

2 files changed

+74
-4
lines changed

src/node/orpc/server.test.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { describe, expect, test } from "bun:test";
2+
import { createOrpcServer } from "./server";
3+
import type { ORPCContext } from "./context";
4+
5+
function getErrorCode(error: unknown): string | null {
6+
if (typeof error !== "object" || error === null) {
7+
return null;
8+
}
9+
10+
if (!("code" in error)) {
11+
return null;
12+
}
13+
14+
const code = (error as { code?: unknown }).code;
15+
return typeof code === "string" ? code : null;
16+
}
17+
18+
describe("createOrpcServer", () => {
19+
test("brackets IPv6 hosts in returned URLs", async () => {
20+
// Minimal context stub - router won't be exercised by this test.
21+
const stubContext: Partial<ORPCContext> = {};
22+
23+
let server: Awaited<ReturnType<typeof createOrpcServer>> | null = null;
24+
25+
try {
26+
server = await createOrpcServer({
27+
host: "::1",
28+
port: 0,
29+
context: stubContext as ORPCContext,
30+
authToken: "test-token",
31+
});
32+
} catch (error) {
33+
const code = getErrorCode(error);
34+
35+
// Some CI environments may not have IPv6 enabled.
36+
if (code === "EAFNOSUPPORT" || code === "EADDRNOTAVAIL") {
37+
return;
38+
}
39+
40+
throw error;
41+
}
42+
43+
try {
44+
expect(server.baseUrl).toMatch(/^http:\/\/\[::1\]:\d+$/);
45+
expect(server.wsUrl).toMatch(/^ws:\/\/\[::1\]:\d+\/orpc\/ws$/);
46+
expect(server.specUrl).toMatch(/^http:\/\/\[::1\]:\d+\/api\/spec\.json$/);
47+
expect(server.docsUrl).toMatch(/^http:\/\/\[::1\]:\d+\/api\/docs$/);
48+
} finally {
49+
await server.close();
50+
}
51+
});
52+
});

src/node/orpc/server.ts

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,23 @@ export interface OrpcServer {
6666

6767
// --- Server Factory ---
6868

69+
function formatHostForUrl(host: string): string {
70+
const trimmed = host.trim();
71+
72+
// IPv6 URLs must be bracketed: http://[::1]:1234
73+
if (trimmed.includes(":")) {
74+
if (trimmed.startsWith("[") && trimmed.endsWith("]")) {
75+
return trimmed;
76+
}
77+
78+
// If the host contains a zone index (e.g. fe80::1%en0), percent must be encoded.
79+
const escaped = trimmed.replaceAll("%", "%25");
80+
return `[${escaped}]`;
81+
}
82+
83+
return trimmed;
84+
}
85+
6986
/**
7087
* Create an oRPC server with HTTP and WebSocket endpoints.
7188
*
@@ -235,16 +252,17 @@ export async function createOrpcServer({
235252

236253
// Wildcard addresses (0.0.0.0, ::) are not routable - convert to loopback for lockfile
237254
const connectableHost = host === "0.0.0.0" || host === "::" ? "127.0.0.1" : host;
255+
const connectableHostForUrl = formatHostForUrl(connectableHost);
238256

239257
return {
240258
httpServer,
241259
wsServer,
242260
app,
243261
port: actualPort,
244-
baseUrl: `http://${connectableHost}:${actualPort}`,
245-
wsUrl: `ws://${connectableHost}:${actualPort}/orpc/ws`,
246-
specUrl: `http://${connectableHost}:${actualPort}/api/spec.json`,
247-
docsUrl: `http://${connectableHost}:${actualPort}/api/docs`,
262+
baseUrl: `http://${connectableHostForUrl}:${actualPort}`,
263+
wsUrl: `ws://${connectableHostForUrl}:${actualPort}/orpc/ws`,
264+
specUrl: `http://${connectableHostForUrl}:${actualPort}/api/spec.json`,
265+
docsUrl: `http://${connectableHostForUrl}:${actualPort}/api/docs`,
248266
close: async () => {
249267
// Close WebSocket server first
250268
wsServer.close();

0 commit comments

Comments
 (0)