fix(server): provide fresh ConfigProvider per HttpApi listener (#25726)

This commit is contained in:
Kit Langton 2026-05-04 13:06:29 -04:00 committed by GitHub
parent 25dc6f09bc
commit fb07c2070c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 25 additions and 17 deletions

View file

@ -5,7 +5,7 @@ import { lazy } from "@/util/lazy"
import * as Log from "@opencode-ai/core/util/log"
import { Flag } from "@opencode-ai/core/flag/flag"
import { WorkspaceID } from "@/control-plane/schema"
import { Context, Effect, Exit, Layer, Scope } from "effect"
import { ConfigProvider, Context, Effect, Exit, Layer, Scope } from "effect"
import { HttpRouter, HttpServer } from "effect/unstable/http"
import { OpenApi } from "effect/unstable/httpapi"
import * as HttpApiServer from "#httpapi-server"
@ -259,6 +259,12 @@ async function listenHttpApi(opts: ListenOptions, selection: ServerBackend.Selec
}).pipe(
Layer.provideMerge(WebSocketTracker.layer),
Layer.provideMerge(HttpApiServer.layer({ port, hostname: opts.hostname })),
// Install a fresh `ConfigProvider` per listener so `Config.string(...)`
// reads reflect the current `process.env`. Effect's default
// `ConfigProvider` snapshots `process.env` on first read and caches the
// result on a module-singleton Reference; without overriding it here,
// every later `Server.listen()` keeps observing that initial snapshot.
Layer.provide(ConfigProvider.layer(ConfigProvider.fromEnv())),
)
const start = async (port: number) => {

View file

@ -40,8 +40,8 @@ async function startListener(backend: "effect-httpapi" | "hono" = "effect-httpap
return Server.listen({ hostname: "127.0.0.1", port: 0 })
}
async function startNoAuthListener() {
Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = false
async function startNoAuthListener(backend: "effect-httpapi" | "hono" = "effect-httpapi") {
Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = backend === "effect-httpapi"
Flag.OPENCODE_SERVER_PASSWORD = undefined
Flag.OPENCODE_SERVER_USERNAME = auth.username
delete process.env.OPENCODE_SERVER_PASSWORD
@ -300,18 +300,20 @@ describe("HttpApi Server.listen", () => {
}
})
testPty("keeps PTY websocket tickets optional when server auth is disabled", async () => {
await using tmp = await tmpdir({ git: true, config: { formatter: false, lsp: false } })
const listener = await startNoAuthListener()
try {
const info = await createCat(listener, tmp.path)
const ws = await openSocket(socketURL(listener, info.id, tmp.path))
const message = waitForMessage(ws, (message) => message.includes("ping-no-auth"))
ws.send("ping-no-auth\n")
expect(await message).toContain("ping-no-auth")
ws.close(1000)
} finally {
await stop(listener, "timed out cleaning up no-auth listener").catch(() => undefined)
}
})
for (const backend of ["effect-httpapi", "hono"] as const) {
testPty(`keeps PTY websocket tickets optional when server auth is disabled (${backend})`, async () => {
await using tmp = await tmpdir({ git: true, config: { formatter: false, lsp: false } })
const listener = await startNoAuthListener(backend)
try {
const info = await createCat(listener, tmp.path)
const ws = await openSocket(socketURL(listener, info.id, tmp.path))
const message = waitForMessage(ws, (message) => message.includes(`ping-no-auth-${backend}`))
ws.send(`ping-no-auth-${backend}\n`)
expect(await message).toContain(`ping-no-auth-${backend}`)
ws.close(1000)
} finally {
await stop(listener, "timed out cleaning up no-auth listener").catch(() => undefined)
}
})
}
})