serve: require explicit tcp listener type

This commit is contained in:
Dax Raad 2026-05-16 00:49:19 -04:00
commit d1f44d4cd2
6 changed files with 22 additions and 24 deletions

View file

@ -72,7 +72,7 @@ export const rpc = {
},
async server(input: { port: number; hostname: string; mdns?: boolean; cors?: string[] }) {
if (server) await server.stop(true)
server = await Server.listen(input)
server = await Server.listen({ type: "tcp", ...input })
return { url: server.url.toString() }
},
async checkUpgrade(input: { directory: string }) {

View file

@ -58,5 +58,5 @@ export function resolveNetworkOptionsNoConfig(args: NetworkOptions, config?: Con
const argsCors = Array.isArray(args.cors) ? args.cors : args.cors ? [args.cors] : []
const cors = [...configCors, ...argsCors]
return { hostname, port, mdns, mdnsDomain, cors }
return { type: "tcp" as const, hostname, port, mdns, mdnsDomain, cors }
}

View file

@ -42,7 +42,7 @@ type ServerApp = {
}
export type TcpListenOptions = CorsOptions & {
type?: "tcp"
type: "tcp"
port: number
hostname: string
mdns?: boolean
@ -119,7 +119,17 @@ export async function listen(opts: ListenOptions): Promise<Listener> {
const listenEffect: (opts: ListenOptions) => Effect.Effect<EffectListener, unknown> = Effect.fn("Server.listen")(
function* (opts: ListenOptions) {
const state = yield* startWithPortFallback(opts)
if (opts.type === "socket") return yield* makeSocketListener(state, opts.socket)
if (opts.type === "socket") {
const listenerUrl = makeURL("localhost", 0)
url = listenerUrl
return {
type: "socket" as const,
socket: opts.socket,
url: listenerUrl,
stop: yield* makeStop(state, Effect.void),
}
}
const address = yield* tcpAddress(state)
const listenerUrl = makeURL(opts.hostname, address.port)
@ -177,20 +187,6 @@ function startListener(opts: ListenOptions) {
)
}
function makeSocketListener(state: ListenerState, socket: string) {
return Effect.gen(function* () {
const listenerUrl = makeURL("localhost", 0)
url = listenerUrl
return {
type: "socket" as const,
socket,
url: listenerUrl,
stop: yield* makeStop(state, Effect.void),
}
})
}
function tcpAddress(state: ListenerState) {
return Effect.gen(function* () {
if (state.server.address._tag === "TcpAddress") return state.server.address

View file

@ -85,7 +85,9 @@ describe("HttpApi CORS", () => {
it.live("uses custom CORS origins passed to the server", () =>
Effect.gen(function* () {
const listener = yield* Effect.acquireRelease(
Effect.promise(() => Server.listen({ hostname: "127.0.0.1", port: 0, cors: ["https://custom.example"] })),
Effect.promise(() =>
Server.listen({ type: "tcp", hostname: "127.0.0.1", port: 0, cors: ["https://custom.example"] }),
),
(listener) => Effect.promise(() => listener.stop(true)),
)

View file

@ -37,7 +37,7 @@ async function startListener() {
Flag.OPENCODE_SERVER_USERNAME = auth.username
process.env.OPENCODE_SERVER_PASSWORD = auth.password
process.env.OPENCODE_SERVER_USERNAME = auth.username
return Server.listen({ hostname: "127.0.0.1", port: 0 })
return Server.listen({ type: "tcp", hostname: "127.0.0.1", port: 0 })
}
async function startNoAuthListener() {
@ -45,7 +45,7 @@ async function startNoAuthListener() {
Flag.OPENCODE_SERVER_USERNAME = auth.username
delete process.env.OPENCODE_SERVER_PASSWORD
process.env.OPENCODE_SERVER_USERNAME = auth.username
return Server.listen({ hostname: "127.0.0.1", port: 0 })
return Server.listen({ type: "tcp", hostname: "127.0.0.1", port: 0 })
}
function authorization() {

View file

@ -45,7 +45,7 @@ describe("HttpApi Server.listen mDNS", () => {
test("skips publish for loopback hostnames", async () => {
Flag.OPENCODE_SERVER_PASSWORD = "mdns-secret"
Flag.OPENCODE_SERVER_USERNAME = "opencode"
const listener = await Server.listen({ hostname: "127.0.0.1", port: 0, mdns: true })
const listener = await Server.listen({ type: "tcp", hostname: "127.0.0.1", port: 0, mdns: true })
try {
expect(events.filter((e) => e.kind === "publish")).toEqual([])
} finally {
@ -57,7 +57,7 @@ describe("HttpApi Server.listen mDNS", () => {
test("publishes for non-loopback hostnames and unpublishes on stop", async () => {
Flag.OPENCODE_SERVER_PASSWORD = "mdns-secret"
Flag.OPENCODE_SERVER_USERNAME = "opencode"
const listener = await Server.listen({ hostname: "0.0.0.0", port: 0, mdns: true })
const listener = await Server.listen({ type: "tcp", hostname: "0.0.0.0", port: 0, mdns: true })
try {
const published = events.filter((e) => e.kind === "publish")
expect(published.length).toBe(1)
@ -73,7 +73,7 @@ describe("HttpApi Server.listen mDNS", () => {
test("scope finalizer unpublishes even if stop() is not called for force-close", async () => {
Flag.OPENCODE_SERVER_PASSWORD = "mdns-secret"
Flag.OPENCODE_SERVER_USERNAME = "opencode"
const listener = await Server.listen({ hostname: "0.0.0.0", port: 0, mdns: true })
const listener = await Server.listen({ type: "tcp", hostname: "0.0.0.0", port: 0, mdns: true })
expect(events.filter((e) => e.kind === "publish").length).toBe(1)
// Plain (graceful) stop without close=true should still unpublish.
await withTimeout(listener.stop(), 10_000, "timed out stopping graceful mdns listener")