feat(server): wire Server.listen() through native HttpApi listener (kill-switch)

When the effect-httpapi backend is selected, Server.listen() now delegates
to HttpApiListener.listen() — a native Bun.serve listener with inline
WebSocket upgrade handling — instead of routing through the Hono runtime
adapter (Hono.fetch + createBunWebSocket).

The Hono backend path is unchanged, and a kill-switch env var
(OPENCODE_HTTPAPI_LEGACY_LISTENER) forces the effect-httpapi backend back
through the Hono adapter as an escape hatch if the native listener
regresses for a user.

This unblocks the Hono deletion arc by giving the native listener real
production traffic on dev/beta/local channels (where
OPENCODE_EXPERIMENTAL_HTTPAPI defaults on) while leaving prod/latest
channels on the Hono path.
This commit is contained in:
Kit Langton
2026-05-03 09:24:18 -04:00
parent 39288a6953
commit adc5f528af
2 changed files with 35 additions and 10 deletions

View File

@@ -94,6 +94,12 @@ export const Flag = {
OPENCODE_EXPERIMENTAL_HTTPAPI:
truthy("OPENCODE_EXPERIMENTAL_HTTPAPI") ||
(!falsy("OPENCODE_EXPERIMENTAL_HTTPAPI") && HTTPAPI_DEFAULT_ON_CHANNELS.has(InstallationChannel)),
// Kill-switch that forces the effect-httpapi backend back through the legacy
// hono runtime adapter (Hono.fetch + createBunWebSocket) instead of the
// native Bun.serve listener. Defaults to false; set to "true"/"1" to revert
// if the native listener regresses for a user. Has no effect when the hono
// backend is selected.
OPENCODE_HTTPAPI_LEGACY_LISTENER: truthy("OPENCODE_HTTPAPI_LEGACY_LISTENER"),
OPENCODE_EXPERIMENTAL_WORKSPACES: OPENCODE_EXPERIMENTAL || truthy("OPENCODE_EXPERIMENTAL_WORKSPACES"),
OPENCODE_EXPERIMENTAL_EVENT_SYSTEM: OPENCODE_EXPERIMENTAL || truthy("OPENCODE_EXPERIMENTAL_EVENT_SYSTEM"),

View File

@@ -19,6 +19,7 @@ import { InstanceMiddleware } from "./routes/instance/middleware"
import { WorkspaceRoutes } from "./routes/control/workspace"
import { ExperimentalHttpApiServer } from "./routes/instance/httpapi/server"
import { PublicApi } from "./routes/instance/httpapi/public"
import { HttpApiListener } from "./httpapi-listener"
import * as ServerBackend from "./backend"
import type { CorsOptions } from "./cors"
@@ -182,35 +183,53 @@ export async function openapiHono() {
export let url: URL
export async function listen(opts: ListenOptions): Promise<Listener> {
const built = create(opts)
const server = await built.runtime.listen(opts)
const selected = select()
const native = selected.backend === "effect-httpapi" && !Flag.OPENCODE_HTTPAPI_LEGACY_LISTENER
let inner: Listener
if (native) {
log.info("server backend selected", {
...ServerBackend.attributes(selected),
"opencode.server.listener": "bun-native",
})
inner = await HttpApiListener.listen(opts)
} else {
const built = create(opts)
const server = await built.runtime.listen(opts)
const innerUrl = new URL("http://localhost")
innerUrl.hostname = opts.hostname
innerUrl.port = String(server.port)
inner = {
hostname: opts.hostname,
port: server.port,
url: innerUrl,
stop: (close?: boolean) => server.stop(close),
}
}
const next = new URL("http://localhost")
next.hostname = opts.hostname
next.port = String(server.port)
const next = new URL(inner.url)
url = next
const mdns =
opts.mdns &&
server.port &&
inner.port &&
opts.hostname !== "127.0.0.1" &&
opts.hostname !== "localhost" &&
opts.hostname !== "::1"
if (mdns) {
MDNS.publish(server.port, opts.mdnsDomain)
MDNS.publish(inner.port, opts.mdnsDomain)
} else if (opts.mdns) {
log.warn("mDNS enabled but hostname is loopback; skipping mDNS publish")
}
let closing: Promise<void> | undefined
return {
hostname: opts.hostname,
port: server.port,
hostname: inner.hostname,
port: inner.port,
url: next,
stop(close?: boolean) {
closing ??= (async () => {
if (mdns) MDNS.unpublish()
await server.stop(close)
await inner.stop(close)
})()
return closing
},