From 08c3a5c70d466ad3c52879649c4fc480a0c05f24 Mon Sep 17 00:00:00 2001 From: brenoepics Date: Wed, 18 Mar 2026 06:10:50 -0300 Subject: [PATCH 1/2] refactor: update server URL to handle ipv6 --- packages/opencode/src/cli/cmd/acp.ts | 2 +- packages/opencode/src/cli/cmd/serve.ts | 2 +- .../opencode/src/cli/cmd/workspace-serve.ts | 2 +- packages/opencode/src/server/server.ts | 23 ++++++++++++++++++- 4 files changed, 25 insertions(+), 4 deletions(-) diff --git a/packages/opencode/src/cli/cmd/acp.ts b/packages/opencode/src/cli/cmd/acp.ts index 99a9a81ab9c..adc7e315446 100644 --- a/packages/opencode/src/cli/cmd/acp.ts +++ b/packages/opencode/src/cli/cmd/acp.ts @@ -26,7 +26,7 @@ export const AcpCommand = cmd({ const server = Server.listen(opts) const sdk = createOpencodeClient({ - baseUrl: `http://${server.hostname}:${server.port}`, + baseUrl: server.url.origin, }) const input = new WritableStream({ diff --git a/packages/opencode/src/cli/cmd/serve.ts b/packages/opencode/src/cli/cmd/serve.ts index ab51fe8c3e3..8aca15205b0 100644 --- a/packages/opencode/src/cli/cmd/serve.ts +++ b/packages/opencode/src/cli/cmd/serve.ts @@ -16,7 +16,7 @@ export const ServeCommand = cmd({ } const opts = await resolveNetworkOptions(args) const server = Server.listen(opts) - console.log(`opencode server listening on http://${server.hostname}:${server.port}`) + console.log(`opencode server listening on ${server.url.origin}`) await new Promise(() => {}) await server.stop() diff --git a/packages/opencode/src/cli/cmd/workspace-serve.ts b/packages/opencode/src/cli/cmd/workspace-serve.ts index cb5c304e4b9..d607803a23e 100644 --- a/packages/opencode/src/cli/cmd/workspace-serve.ts +++ b/packages/opencode/src/cli/cmd/workspace-serve.ts @@ -9,7 +9,7 @@ export const WorkspaceServeCommand = cmd({ handler: async (args) => { const opts = await resolveNetworkOptions(args) const server = WorkspaceServer.Listen(opts) - console.log(`workspace event server listening on http://${server.hostname}:${server.port}/event`) + console.log(`workspace event server listening on ${new URL("/event", server.url).toString()}`) await new Promise(() => {}) await server.stop() }, diff --git a/packages/opencode/src/server/server.ts b/packages/opencode/src/server/server.ts index 677af4da87f..0c4f8906312 100644 --- a/packages/opencode/src/server/server.ts +++ b/packages/opencode/src/server/server.ts @@ -591,6 +591,25 @@ export namespace Server { /** @deprecated do not use this dumb shit */ export let url: URL + /** + * Normalizes a hostname for safe interpolation into an HTTP URL authority. + * + * - Trims surrounding whitespace. + * - Removes IPv6 brackets if already present so we can normalize once. + * - Escapes stray `%` as `%25` to avoid invalid percent-encoding in URL parsing. + * - Re-wraps values containing `:` in `[]` so IPv6 literals are valid in `host:port`. + * + * @param hostname Hostname or IP literal (IPv4/IPv6), optionally already bracketed. + * @returns A normalized host string suitable for `http://${host}:${port}` construction. + */ + function host(hostname: string) { + const raw = hostname.trim() + const inner = raw.startsWith("[") && raw.endsWith("]") ? raw.slice(1, -1) : raw + const safe = inner.replace(/%(?!25)/g, "%25") + if (!safe.includes(":")) return safe + return `[${safe}]` + } + export function listen(opts: { port: number hostname: string @@ -598,7 +617,7 @@ export namespace Server { mdnsDomain?: string cors?: string[] }) { - url = new URL(`http://${opts.hostname}:${opts.port}`) + url = new URL(`http://${host(opts.hostname)}:${opts.port}`) const app = createApp(opts) const args = { hostname: opts.hostname, @@ -616,6 +635,8 @@ export namespace Server { const server = opts.port === 0 ? (tryServe(4096) ?? tryServe(0)) : tryServe(opts.port) if (!server) throw new Error(`Failed to start server on port ${opts.port}`) + url.port = String(server.port) + const shouldPublishMDNS = opts.mdns && server.port && From 40321f7542de1d91b3ff8963b430757dda03b5b8 Mon Sep 17 00:00:00 2001 From: brenoepics Date: Wed, 18 Mar 2026 06:11:35 -0300 Subject: [PATCH 2/2] test: add unit tests for Server.listen URL handling of ipv4 and ipv6 --- packages/opencode/test/server/url.test.ts | 29 +++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 packages/opencode/test/server/url.test.ts diff --git a/packages/opencode/test/server/url.test.ts b/packages/opencode/test/server/url.test.ts new file mode 100644 index 00000000000..6a2d862da78 --- /dev/null +++ b/packages/opencode/test/server/url.test.ts @@ -0,0 +1,29 @@ +import { describe, expect, test } from "bun:test" +import { Server } from "../../src/server/server" +import { Log } from "../../src/util/log" + +Log.init({ print: false }) + +function want(hostname: string, port: number) { + const raw = hostname.trim() + const host = raw.startsWith("[") && raw.endsWith("]") ? raw.slice(1, -1) : raw + if (!host.includes(":")) return `http://${host}:${port}` + return `http://[${host}]:${port}` +} + +describe("Server.listen URL", () => { + test("accepts ipv4, ipv6, and domains", async () => { + const cases = ["127.0.0.1", "0.0.0.0", "localhost", "::", "::1", "[::]", "[::1]"] + + for (const hostname of cases) { + const server = Server.listen({ hostname, port: 0 }) + try { + expect(server.port).toBeGreaterThan(0) + expect(Server.url.origin).toBe(new URL(want(hostname, server.port!)).origin) + expect(new URL(Server.url.toString()).origin).toBe(Server.url.origin) + } finally { + await server.stop(true) + } + } + }) +})