summaryrefslogtreecommitdiffhomepage
path: root/packages
diff options
context:
space:
mode:
authorKit Langton <[email protected]>2026-04-30 17:43:18 -0400
committerGitHub <[email protected]>2026-04-30 17:43:18 -0400
commit5518ecaefe69d5c35d9b0cda227f2dba733dba03 (patch)
tree3b99734f1f81975ac288558b8dc5a7800929f2a1 /packages
parent924ba97055adaa02b6684131ae537eddb2b7bfe5 (diff)
downloadopencode-5518ecaefe69d5c35d9b0cda227f2dba733dba03.tar.gz
opencode-5518ecaefe69d5c35d9b0cda227f2dba733dba03.zip
Fix HttpApi web UI fallback (#25163)
Diffstat (limited to 'packages')
-rw-r--r--packages/opencode/src/server/routes/instance/httpapi/server.ts19
-rw-r--r--packages/opencode/test/server/httpapi-ui.test.ts45
2 files changed, 62 insertions, 2 deletions
diff --git a/packages/opencode/src/server/routes/instance/httpapi/server.ts b/packages/opencode/src/server/routes/instance/httpapi/server.ts
index f53ddb3ec..f62636bca 100644
--- a/packages/opencode/src/server/routes/instance/httpapi/server.ts
+++ b/packages/opencode/src/server/routes/instance/httpapi/server.ts
@@ -1,6 +1,6 @@
import { Context, Effect, Layer } from "effect"
import { HttpApiBuilder } from "effect/unstable/httpapi"
-import { HttpMiddleware, HttpRouter, HttpServer } from "effect/unstable/http"
+import { HttpMiddleware, HttpRouter, HttpServer, HttpServerResponse } from "effect/unstable/http"
import * as Socket from "effect/unstable/socket/Socket"
import { Account } from "@/account/account"
import { Agent } from "@/agent/agent"
@@ -38,6 +38,7 @@ import { Vcs } from "@/project/vcs"
import { Worktree } from "@/worktree"
import { Workspace } from "@/control-plane/workspace"
import { isAllowedCorsOrigin } from "@/server/cors"
+import { UIRoutes } from "@/server/routes/ui"
import { InstanceHttpApi, RootHttpApi } from "./api"
import { ServerAuthConfig, authorizationLayer, authorizationRouterMiddleware } from "./middleware/authorization"
import { eventRoute } from "./event"
@@ -119,7 +120,21 @@ const instanceRoutes = Layer.mergeAll(rawInstanceRoutes, instanceApiRoutes).pipe
]),
)
-export const routes = Layer.mergeAll(rootApiRoutes, instanceRoutes).pipe(
+const uiRoutes = lazy(() => UIRoutes())
+const uiRoute = HttpRouter.add("*", "/*", (request) =>
+ Effect.promise(async () =>
+ uiRoutes().fetch(
+ request.source instanceof Request
+ ? request.source
+ : new Request(new URL(request.originalUrl, "http://localhost"), {
+ method: request.method,
+ headers: request.headers,
+ }),
+ ),
+ ).pipe(Effect.map(HttpServerResponse.fromWeb)),
+)
+
+export const routes = Layer.mergeAll(rootApiRoutes, instanceRoutes, uiRoute).pipe(
Layer.provide([
cors,
runtime,
diff --git a/packages/opencode/test/server/httpapi-ui.test.ts b/packages/opencode/test/server/httpapi-ui.test.ts
new file mode 100644
index 000000000..9ca8b49f1
--- /dev/null
+++ b/packages/opencode/test/server/httpapi-ui.test.ts
@@ -0,0 +1,45 @@
+import { afterEach, describe, expect, test } from "bun:test"
+import { Flag } from "@opencode-ai/core/flag/flag"
+import * as Log from "@opencode-ai/core/util/log"
+import { Server } from "../../src/server/server"
+
+void Log.init({ print: false })
+
+const original = {
+ OPENCODE_EXPERIMENTAL_HTTPAPI: Flag.OPENCODE_EXPERIMENTAL_HTTPAPI,
+ fetch: globalThis.fetch,
+}
+
+afterEach(() => {
+ Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = original.OPENCODE_EXPERIMENTAL_HTTPAPI
+ globalThis.fetch = original.fetch
+})
+
+describe("HttpApi UI fallback", () => {
+ test("serves the web UI through the experimental backend", async () => {
+ Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = true
+ let proxiedUrl: string | undefined
+ globalThis.fetch = ((input: RequestInfo | URL) => {
+ proxiedUrl = String(input instanceof Request ? input.url : input)
+ return Promise.resolve(new Response("<html>opencode</html>", { headers: { "content-type": "text/html" } }))
+ }) as typeof fetch
+
+ const response = await Server.Default().app.request("/")
+
+ expect(response.status).toBe(200)
+ expect(response.headers.get("content-type")).toContain("text/html")
+ expect(await response.text()).toBe("<html>opencode</html>")
+ expect(proxiedUrl).toBe("https://app.opencode.ai/")
+ })
+
+ test("keeps matched API routes ahead of the UI fallback", async () => {
+ Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = true
+ globalThis.fetch = (() => {
+ throw new Error("UI fallback should not handle matched API routes")
+ }) as unknown as typeof fetch
+
+ const response = await Server.Default().app.request("/session/nope")
+
+ expect(response.status).toBe(404)
+ })
+})