diff --git a/packages/opencode/src/server/routes/instance/httpapi/middleware/error.ts b/packages/opencode/src/server/routes/instance/httpapi/middleware/error.ts new file mode 100644 index 000000000000..6f3c33a647a5 --- /dev/null +++ b/packages/opencode/src/server/routes/instance/httpapi/middleware/error.ts @@ -0,0 +1,58 @@ +import { Provider } from "@/provider/provider" +import { Session } from "@/session/session" +import { NotFoundError } from "@/storage/storage" +import { iife } from "@/util/iife" +import { NamedError } from "@opencode-ai/core/util/error" +import * as Log from "@opencode-ai/core/util/log" +import { Cause, Effect } from "effect" +import { HttpRouter, HttpServerError, HttpServerRespondable, HttpServerResponse } from "effect/unstable/http" + +const log = Log.create({ service: "server" }) + +// Keep typed HttpApi failures on their declared error path; this boundary only replaces defect-only empty 500s. +export const errorLayer = HttpRouter.middleware<{ handles: unknown }>()((effect) => + effect.pipe( + Effect.catchCause((cause) => { + const defect = cause.reasons.filter(Cause.isDieReason).find((reason) => { + if (HttpServerResponse.isHttpServerResponse(reason.defect)) return false + if (HttpServerError.isHttpServerError(reason.defect)) return false + if (HttpServerRespondable.isRespondable(reason.defect)) return false + return true + }) + if (!defect) return Effect.failCause(cause) + + const error = defect.defect + log.error("failed", { error, cause: Cause.pretty(cause) }) + + if (error instanceof NamedError) { + return Effect.succeed( + HttpServerResponse.jsonUnsafe(error.toObject(), { + status: iife(() => { + if (error instanceof NotFoundError) return 404 + if (error instanceof Provider.ModelNotFoundError) return 400 + if (error.name === "ProviderAuthValidationFailed") return 400 + if (error.name.startsWith("Worktree")) return 400 + return 500 + }), + }), + ) + } + if (error instanceof Session.BusyError) { + return Effect.succeed( + HttpServerResponse.jsonUnsafe(new NamedError.Unknown({ message: error.message }).toObject(), { + status: 400, + }), + ) + } + + return Effect.succeed( + HttpServerResponse.jsonUnsafe( + new NamedError.Unknown({ + message: error instanceof Error && error.stack ? error.stack : String(error), + }).toObject(), + { status: 500 }, + ), + ) + }), + ), +).layer diff --git a/packages/opencode/src/server/routes/instance/httpapi/server.ts b/packages/opencode/src/server/routes/instance/httpapi/server.ts index a3754c2e1907..ef966036a94f 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/server.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/server.ts @@ -73,6 +73,7 @@ import { workspaceRouterMiddleware, workspaceRoutingLayer } from "./middleware/w import { disposeMiddleware } from "./lifecycle" import { memoMap } from "@opencode-ai/core/effect/memo-map" import * as ServerBackend from "@/server/backend" +import { errorLayer } from "./middleware/error" export const context = Context.makeUnsafe(new Map()) @@ -144,6 +145,7 @@ const uiRoute = HttpRouter.use((router) => export function createRoutes(corsOptions?: CorsOptions) { return Layer.mergeAll(rootApiRoutes, eventApiRoutes, instanceRoutes, uiRoute).pipe( Layer.provide([ + errorLayer, cors(corsOptions), runtime, Account.defaultLayer,