fix(server): serve embedded UI from bunfs (#25632)
This commit is contained in:
@@ -45,6 +45,31 @@ export function embeddedUI() {
|
|||||||
return embeddedUIPromise
|
return embeddedUIPromise
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function notFound() {
|
||||||
|
return HttpServerResponse.jsonUnsafe({ error: "Not Found" }, { status: 404 })
|
||||||
|
}
|
||||||
|
|
||||||
|
function embeddedUIResponse(file: string, body: Uint8Array) {
|
||||||
|
const mime = AppFileSystem.mimeType(file)
|
||||||
|
const headers = new Headers({ "content-type": mime })
|
||||||
|
if (mime.startsWith("text/html")) headers.set("content-security-policy", DEFAULT_CSP)
|
||||||
|
return HttpServerResponse.raw(body, { headers })
|
||||||
|
}
|
||||||
|
|
||||||
|
export function serveEmbeddedUIEffect(
|
||||||
|
requestPath: string,
|
||||||
|
fs: AppFileSystem.Interface,
|
||||||
|
embeddedWebUI: Record<string, string>,
|
||||||
|
) {
|
||||||
|
const file = embeddedWebUI[requestPath.replace(/^\//, "")] ?? embeddedWebUI["index.html"] ?? null
|
||||||
|
if (!file) return Effect.succeed(notFound())
|
||||||
|
|
||||||
|
return fs.readFile(file).pipe(
|
||||||
|
Effect.map((body) => embeddedUIResponse(file, body)),
|
||||||
|
Effect.catchReason("PlatformError", "NotFound", () => Effect.succeed(notFound())),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export function serveUIEffect(
|
export function serveUIEffect(
|
||||||
request: HttpServerRequest.HttpServerRequest,
|
request: HttpServerRequest.HttpServerRequest,
|
||||||
services: { fs: AppFileSystem.Interface; client: HttpClient.HttpClient },
|
services: { fs: AppFileSystem.Interface; client: HttpClient.HttpClient },
|
||||||
@@ -53,19 +78,7 @@ export function serveUIEffect(
|
|||||||
const embeddedWebUI = yield* Effect.promise(() => embeddedUI())
|
const embeddedWebUI = yield* Effect.promise(() => embeddedUI())
|
||||||
const path = new URL(request.url, "http://localhost").pathname
|
const path = new URL(request.url, "http://localhost").pathname
|
||||||
|
|
||||||
if (embeddedWebUI) {
|
if (embeddedWebUI) return yield* serveEmbeddedUIEffect(path, services.fs, embeddedWebUI)
|
||||||
const match = embeddedWebUI[path.replace(/^\//, "")] ?? embeddedWebUI["index.html"] ?? null
|
|
||||||
if (!match) return HttpServerResponse.jsonUnsafe({ error: "Not Found" }, { status: 404 })
|
|
||||||
|
|
||||||
if (yield* services.fs.existsSafe(match)) {
|
|
||||||
const mime = AppFileSystem.mimeType(match)
|
|
||||||
const headers = new Headers({ "content-type": mime })
|
|
||||||
if (mime.startsWith("text/html")) headers.set("content-security-policy", DEFAULT_CSP)
|
|
||||||
return HttpServerResponse.raw(yield* services.fs.readFile(match), { headers })
|
|
||||||
}
|
|
||||||
|
|
||||||
return HttpServerResponse.jsonUnsafe({ error: "Not Found" }, { status: 404 })
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = yield* services.client.execute(
|
const response = yield* services.client.execute(
|
||||||
HttpClientRequest.make(request.method)(upstreamURL(path), {
|
HttpClientRequest.make(request.method)(upstreamURL(path), {
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import { AppFileSystem } from "@opencode-ai/core/filesystem"
|
|||||||
import { ServerAuth } from "../../src/server/auth"
|
import { ServerAuth } from "../../src/server/auth"
|
||||||
import { authorizationRouterMiddleware } from "../../src/server/routes/instance/httpapi/middleware/authorization"
|
import { authorizationRouterMiddleware } from "../../src/server/routes/instance/httpapi/middleware/authorization"
|
||||||
import { ExperimentalHttpApiServer } from "../../src/server/routes/instance/httpapi/server"
|
import { ExperimentalHttpApiServer } from "../../src/server/routes/instance/httpapi/server"
|
||||||
import { serveUIEffect } from "../../src/server/shared/ui"
|
import { serveEmbeddedUIEffect, serveUIEffect } from "../../src/server/shared/ui"
|
||||||
import { Server } from "../../src/server/server"
|
import { Server } from "../../src/server/server"
|
||||||
|
|
||||||
void Log.init({ print: false })
|
void Log.init({ print: false })
|
||||||
@@ -184,6 +184,39 @@ describe("HttpApi UI fallback", () => {
|
|||||||
expect(await response.text()).toBe("console.log('ok')")
|
expect(await response.text()).toBe("console.log('ok')")
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("serves embedded UI assets when Bun can read them but access reports missing", async () => {
|
||||||
|
Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = true
|
||||||
|
let readPath: string | undefined
|
||||||
|
|
||||||
|
const response = await Effect.runPromise(
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const fs = yield* AppFileSystem.Service
|
||||||
|
return yield* serveEmbeddedUIEffect(
|
||||||
|
"/assets/app.js",
|
||||||
|
{
|
||||||
|
...fs,
|
||||||
|
existsSafe: () => Effect.die("embedded UI should not rely on filesystem access checks"),
|
||||||
|
readFile: (path) => {
|
||||||
|
readPath = path
|
||||||
|
return path === "/$bunfs/root/assets/app.js"
|
||||||
|
? Effect.succeed(new TextEncoder().encode("console.log('embedded')"))
|
||||||
|
: Effect.die(`unexpected embedded UI path: ${path}`)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ "assets/app.js": "/$bunfs/root/assets/app.js" },
|
||||||
|
)
|
||||||
|
}).pipe(
|
||||||
|
Effect.provide(AppFileSystem.defaultLayer),
|
||||||
|
Effect.map(HttpServerResponse.toWeb),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(response.status).toBe(200)
|
||||||
|
expect(readPath).toBe("/$bunfs/root/assets/app.js")
|
||||||
|
expect(response.headers.get("content-type")).toContain("text/javascript")
|
||||||
|
expect(await response.text()).toBe("console.log('embedded')")
|
||||||
|
})
|
||||||
|
|
||||||
test("keeps matched API routes ahead of the UI fallback", async () => {
|
test("keeps matched API routes ahead of the UI fallback", async () => {
|
||||||
Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = true
|
Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = true
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user