mirror of
https://github.com/anomalyco/opencode.git
synced 2026-05-13 15:44:56 +00:00
fix(server): revert structured validation error wire format (#26457)
PR #26457 changed the 400 wire format on the HTTP API in three ways: 1. \`Effect.catch(HttpApiError.BadRequest)\` → \`{ data: {}, errors: [], success: false }\` 2. \`HttpApiSchemaError\` defect → same shape 3. \`normalizeEmptyBadRequest\` post-processor that rewrites ANY empty 400 into the same shape In v1.14.42 (post-Hono-deletion) at least one external plugin (\`opencode-gemini-auth\`, per #26546 reports) makes an HTTP API call during startup that hits one of these paths and breaks on the new body shape — the unparseable JSON ends up printed to stdout and the TUI never starts. \`opencode --pure\` works (no plugins), \`opencode run\` works (no in-process server interaction), but \`opencode\` (TUI) crashes. Revert all three changes from #26457. The default HttpApi serialization of typed errors comes back, matching v1.14.41 wire format. We can re-introduce structured validation responses behind an opt-in or contract change once plugin authors have a chance to migrate. Closes #26546.
This commit is contained in:
@@ -6,39 +6,13 @@ 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"
|
||||
import { HttpApiError } from "effect/unstable/httpapi"
|
||||
import { HttpApiSchemaError } from "effect/unstable/httpapi/HttpApiError"
|
||||
|
||||
const log = Log.create({ service: "server" })
|
||||
|
||||
function badRequestResponse() {
|
||||
return HttpServerResponse.jsonUnsafe(
|
||||
{
|
||||
data: {},
|
||||
errors: [],
|
||||
success: false,
|
||||
},
|
||||
{ status: 400 },
|
||||
)
|
||||
}
|
||||
|
||||
function normalizeEmptyBadRequest(response: HttpServerResponse.HttpServerResponse) {
|
||||
if (response.status !== 400 || response.body._tag !== "Empty") return response
|
||||
return badRequestResponse()
|
||||
}
|
||||
|
||||
// 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.catch((error) => {
|
||||
if (error instanceof HttpApiError.BadRequest) return Effect.succeed(badRequestResponse())
|
||||
return Effect.fail(error)
|
||||
}),
|
||||
Effect.map(normalizeEmptyBadRequest),
|
||||
Effect.catchCause((cause) => {
|
||||
const schemaError = cause.reasons.filter(Cause.isDieReason).find((reason) => HttpApiSchemaError.is(reason.defect))
|
||||
if (schemaError) return Effect.succeed(badRequestResponse())
|
||||
|
||||
const defect = cause.reasons.filter(Cause.isDieReason).find((reason) => {
|
||||
if (HttpServerResponse.isHttpServerResponse(reason.defect)) return false
|
||||
if (HttpServerError.isHttpServerError(reason.defect)) return false
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import { afterEach, describe, expect, mock, spyOn, test } from "bun:test"
|
||||
import { Context, Effect } from "effect"
|
||||
import { Effect } from "effect"
|
||||
import { Flag } from "@opencode-ai/core/flag/flag"
|
||||
import { Instance } from "../../src/project/instance"
|
||||
import { WithInstance } from "../../src/project/with-instance"
|
||||
import { Server } from "../../src/server/server"
|
||||
import { SyncPaths } from "../../src/server/routes/instance/httpapi/groups/sync"
|
||||
import { ExperimentalHttpApiServer } from "../../src/server/routes/instance/httpapi/server"
|
||||
import { Session } from "@/session/session"
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
import { resetDatabase } from "../fixture/db"
|
||||
@@ -14,7 +13,6 @@ import { disposeAllInstances, tmpdir } from "../fixture/fixture"
|
||||
void Log.init({ print: false })
|
||||
|
||||
const originalWorkspaces = Flag.OPENCODE_EXPERIMENTAL_WORKSPACES
|
||||
const context = Context.empty() as Context.Context<unknown>
|
||||
|
||||
function app() {
|
||||
return Server.Default().app
|
||||
@@ -121,22 +119,4 @@ describe("sync HttpApi", () => {
|
||||
expect(response.status).toBe(400)
|
||||
}
|
||||
})
|
||||
|
||||
test("returns structured validation errors", async () => {
|
||||
await using tmp = await tmpdir({ git: true, config: { formatter: false, lsp: false } })
|
||||
const response = await ExperimentalHttpApiServer.webHandler().handler(
|
||||
new Request(`http://localhost${SyncPaths.history}`, {
|
||||
method: "POST",
|
||||
headers: { "x-opencode-directory": tmp.path, "content-type": "application/json" },
|
||||
body: JSON.stringify({ aggregate: -1 }),
|
||||
}),
|
||||
context,
|
||||
)
|
||||
|
||||
expect(response.status).toBe(400)
|
||||
expect(response.headers.get("content-type") ?? "").toContain("application/json")
|
||||
const body = (await response.json()) as Record<string, unknown>
|
||||
expect(body.success).toBe(false)
|
||||
expect(Array.isArray(body.error) || Array.isArray(body.errors)).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user