From 803e01f377ae0fdce79144cda2ca538f7bb3b581 Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Mon, 4 May 2026 09:24:04 -0500 Subject: [PATCH 1/2] fix: enure effect server middleware properly parses errors --- .../instance/httpapi/middleware/error.ts | 57 +++++++++++++++++++ .../server/routes/instance/httpapi/server.ts | 2 + 2 files changed, 59 insertions(+) create mode 100644 packages/opencode/src/server/routes/instance/httpapi/middleware/error.ts 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..1df33f4a79cf --- /dev/null +++ b/packages/opencode/src/server/routes/instance/httpapi/middleware/error.ts @@ -0,0 +1,57 @@ +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, 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.catchCauseIf( + (cause) => Cause.hasDies(cause), + (cause) => { + const error = Cause.squash(cause) + if (HttpServerResponse.isHttpServerResponse(error)) return Effect.succeed(error) + if (HttpServerError.isHttpServerError(error)) + return HttpServerError.causeResponse(cause).pipe(Effect.map((response) => response[0])) + + 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, From 0c0cd6845ac816bcc7d5a257d70165eecd5375fa Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Mon, 4 May 2026 09:45:19 -0500 Subject: [PATCH 2/2] fix: tests --- .../instance/httpapi/middleware/error.ts | 75 ++++++++++--------- 1 file changed, 38 insertions(+), 37 deletions(-) diff --git a/packages/opencode/src/server/routes/instance/httpapi/middleware/error.ts b/packages/opencode/src/server/routes/instance/httpapi/middleware/error.ts index 1df33f4a79cf..6f3c33a647a5 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/middleware/error.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/middleware/error.ts @@ -5,53 +5,54 @@ 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, HttpServerResponse } from "effect/unstable/http" +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.catchCauseIf( - (cause) => Cause.hasDies(cause), - (cause) => { - const error = Cause.squash(cause) - if (HttpServerResponse.isHttpServerResponse(error)) return Effect.succeed(error) - if (HttpServerError.isHttpServerError(error)) - return HttpServerError.causeResponse(cause).pipe(Effect.map((response) => response[0])) + 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) - log.error("failed", { error, cause: Cause.pretty(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, + 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 instanceof Error && error.stack ? error.stack : String(error), - }).toObject(), - { status: 500 }, - ), + 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