fix(server): include cors headers on 404s

This commit is contained in:
Brendan Allan
2026-07-17 06:57:13 +00:00
parent 08ea08e830
commit efaeda00a1
2 changed files with 61 additions and 3 deletions
@@ -190,17 +190,45 @@ const docRoute = HttpRouter.use((router) => router.add("GET", "/doc", () => Effe
Layer.provide(authOnlyRouterLayer),
)
const uiRoute = HttpRouter.use((router) =>
const uiRoute = (corsOptions?: CorsOptions) => HttpRouter.use((router) =>
Effect.gen(function* () {
const fs = yield* FSUtil.Service
const client = yield* HttpClient.HttpClient
const flags = yield* RuntimeFlags.Service
yield* router.add("*", "/*", (request) =>
serveUIEffect(request, { fs, client, disableEmbeddedWebUi: flags.disableEmbeddedWebUi }),
serveUIEffect(request, { fs, client, disableEmbeddedWebUi: flags.disableEmbeddedWebUi }).pipe(
Effect.map((response) => withNotFoundCors(response, request.headers.origin, corsOptions)),
),
)
}),
).pipe(Layer.provide(authOnlyRouterLayer))
const apiNotFoundRoute = (corsOptions?: CorsOptions) =>
HttpRouter.use((router) =>
router.add("*", "/api/*", (request) =>
Effect.succeed(
withNotFoundCors(
HttpServerResponse.jsonUnsafe({ error: "Not Found" }, { status: 404 }),
request.headers.origin,
corsOptions,
),
),
),
)
function withNotFoundCors(
response: HttpServerResponse.HttpServerResponse,
origin: string | undefined,
opts?: CorsOptions,
) {
if (response.status !== 404 || !origin || !isAllowedCorsOrigin(origin, opts)) return response
const vary = response.headers["vary"]
const next = HttpServerResponse.setHeader(response, "access-control-allow-origin", origin)
if (!vary) return HttpServerResponse.setHeader(next, "vary", "Origin")
if (vary.split(",").some((value) => value.trim().toLowerCase() === "origin")) return next
return HttpServerResponse.setHeader(next, "vary", `${vary}, Origin`)
}
type RouteRequirements =
| HttpRouter.HttpRouter
| HttpRouter.Request<"Error", unknown>
@@ -278,7 +306,8 @@ export function createRoutes(
instanceRoutes,
serverRoutes,
docRoute,
uiRoute,
apiNotFoundRoute(corsOptions),
uiRoute(corsOptions),
).pipe(
Layer.provide([
errorLayer,
@@ -82,6 +82,35 @@ describe("HttpApi CORS", () => {
}),
)
it.live("adds CORS headers to not found responses", () =>
Effect.gen(function* () {
const handler = HttpRouter.toWebHandler(
HttpApiApp.createRoutes().pipe(
Layer.provide(
ConfigProvider.layer(
ConfigProvider.fromUnknown({
OPENCODE_DISABLE_EMBEDDED_WEB_UI: "true",
}),
),
),
),
{ disableLogger: true },
).handler
const response = yield* Effect.promise(() =>
handler(
new Request(new URL("/api/health", "http://localhost"), {
method: "POST",
headers: { origin: "https://app.opencode.ai" },
}),
HttpApiApp.context,
),
)
expect(response.status).toBe(404)
expect(response.headers.get("access-control-allow-origin")).toBe("https://app.opencode.ai")
}),
)
it.live("uses custom CORS origins passed to the server", () =>
Effect.gen(function* () {
const listener = yield* Effect.acquireRelease(