diff --git a/packages/opencode/src/server/server.ts b/packages/opencode/src/server/server.ts index ded9f0e2..c33e6deb 100644 --- a/packages/opencode/src/server/server.ts +++ b/packages/opencode/src/server/server.ts @@ -33,6 +33,8 @@ import { lazy } from "../util/lazy" import { Todo } from "../session/todo" import { InstanceBootstrap } from "../project/bootstrap" import { MCP } from "../mcp" +import { Storage } from "../storage/storage" +import type { ContentfulStatusCode } from "hono/utils/http-status" const ERRORS = { 400: { @@ -42,17 +44,33 @@ const ERRORS = { schema: resolver( z .object({ - data: z.record(z.string(), z.any()), + data: z.any().nullable(), + errors: z.array(z.record(z.string(), z.any())), + success: z.literal(false), }) .meta({ - ref: "Error", + ref: "BadRequestError", }), ), }, }, }, + 404: { + description: "Not found", + content: { + "application/json": { + schema: resolver( + Storage.NotFoundError.Schema + ) + }, + }, + }, } as const +function errors(...codes: number[]) { + return Object.fromEntries(codes.map((code) => [code, ERRORS[code as keyof typeof ERRORS]])) +} + export namespace Server { const log = Log.create({ service: "server" }) @@ -68,13 +86,18 @@ export namespace Server { error: err, }) if (err instanceof NamedError) { - return c.json(err.toObject(), { - status: 400, - }) + let status: ContentfulStatusCode + if (err instanceof Storage.NotFoundError) + status = 404 + else if (err instanceof Provider.ModelNotFoundError) + status = 400 + else + status = 500 + return c.json(err.toObject(), { status }) } const message = err instanceof Error && err.stack ? err.stack : err.toString() return c.json(new NamedError.Unknown({ message }).toObject(), { - status: 400, + status: 500, }) }) .use(async (c, next) => { @@ -153,7 +176,7 @@ export namespace Server { }, }, }, - ...ERRORS, + ...errors(400), }, }), validator("json", Config.Info), @@ -177,7 +200,7 @@ export namespace Server { }, }, }, - ...ERRORS, + ...errors(400), }, }), async (c) => { @@ -210,7 +233,7 @@ export namespace Server { }, }, }, - ...ERRORS, + ...errors(400), }, }), validator( @@ -305,6 +328,7 @@ export namespace Server { }, }, }, + ...errors(400, 404), }, }), validator( @@ -333,6 +357,7 @@ export namespace Server { }, }, }, + ...errors(400, 404), }, }), validator( @@ -361,6 +386,7 @@ export namespace Server { }, }, }, + ...errors(400, 404), }, }), validator( @@ -381,7 +407,7 @@ export namespace Server { description: "Create a new session", operationId: "session.create", responses: { - ...ERRORS, + ...errors(400), 200: { description: "Successfully created session", content: { @@ -413,6 +439,7 @@ export namespace Server { }, }, }, + ...errors(400, 404), }, }), validator( @@ -440,6 +467,7 @@ export namespace Server { }, }, }, + ...errors(400, 404), }, }), validator( @@ -481,6 +509,7 @@ export namespace Server { }, }, }, + ...errors(400, 404), }, }), validator( @@ -541,6 +570,7 @@ export namespace Server { }, }, }, + ...errors(400, 404), }, }), validator( @@ -567,6 +597,7 @@ export namespace Server { }, }, }, + ...errors(400, 404), }, }), validator( @@ -596,6 +627,7 @@ export namespace Server { }, }, }, + ...errors(400, 404), }, }), validator( @@ -625,6 +657,7 @@ export namespace Server { }, }, }, + ...errors(400, 404), }, }), validator( @@ -661,6 +694,7 @@ export namespace Server { }, }, }, + ...errors(400, 404), }, }), validator( @@ -693,6 +727,7 @@ export namespace Server { }, }, }, + ...errors(400, 404), }, }), validator( @@ -727,6 +762,7 @@ export namespace Server { }, }, }, + ...errors(400, 404), }, }), validator( @@ -762,6 +798,7 @@ export namespace Server { }, }, }, + ...errors(400, 404), }, }), validator( @@ -792,6 +829,7 @@ export namespace Server { }, }, }, + ...errors(400, 404), }, }), validator( @@ -822,6 +860,7 @@ export namespace Server { }, }, }, + ...errors(400, 404), }, }), validator( @@ -852,6 +891,7 @@ export namespace Server { }, }, }, + ...errors(400, 404), }, }), validator( @@ -879,6 +919,7 @@ export namespace Server { }, }, }, + ...errors(400, 404), }, }), validator( @@ -1132,6 +1173,7 @@ export namespace Server { }, }, }, + ...errors(400), }, }), validator( @@ -1223,6 +1265,7 @@ export namespace Server { }, }, }, + ...errors(400), }, }), validator( @@ -1355,6 +1398,7 @@ export namespace Server { }, }, }, + ...errors(400), }, }), validator( @@ -1406,7 +1450,7 @@ export namespace Server { }, }, }, - ...ERRORS, + ...errors(400), }, }), validator( diff --git a/packages/opencode/src/storage/storage.ts b/packages/opencode/src/storage/storage.ts index 546d123c..e54fcd44 100644 --- a/packages/opencode/src/storage/storage.ts +++ b/packages/opencode/src/storage/storage.ts @@ -5,12 +5,21 @@ import { Global } from "../global" import { lazy } from "../util/lazy" import { Lock } from "../util/lock" import { $ } from "bun" +import { NamedError } from "@/util/error" +import z from "zod" export namespace Storage { const log = Log.create({ service: "storage" }) type Migration = (dir: string) => Promise + export const NotFoundError = NamedError.create( + "NotFoundError", + z.object({ + message: z.string(), + }), + ) + const MIGRATIONS: Migration[] = [ async (dir) => { const project = path.resolve(dir, "../project") @@ -131,31 +140,51 @@ export namespace Storage { export async function remove(key: string[]) { const dir = await state().then((x) => x.dir) const target = path.join(dir, ...key) + ".json" - await fs.unlink(target).catch(() => {}) + return withErrorHandling(async () => { + await fs.unlink(target).catch(() => {}) + }) } export async function read(key: string[]) { const dir = await state().then((x) => x.dir) const target = path.join(dir, ...key) + ".json" - using _ = await Lock.read(target) - return Bun.file(target).json() as Promise + return withErrorHandling(async () => { + using _ = await Lock.read(target) + return Bun.file(target).json() as Promise + }) } export async function update(key: string[], fn: (draft: T) => void) { const dir = await state().then((x) => x.dir) const target = path.join(dir, ...key) + ".json" - using _ = await Lock.write("storage") - const content = await Bun.file(target).json() - fn(content) - await Bun.write(target, JSON.stringify(content, null, 2)) - return content as T + return withErrorHandling(async () => { + using _ = await Lock.write("storage") + const content = await Bun.file(target).json() + fn(content) + await Bun.write(target, JSON.stringify(content, null, 2)) + return content as T + }) } export async function write(key: string[], content: T) { const dir = await state().then((x) => x.dir) const target = path.join(dir, ...key) + ".json" - using _ = await Lock.write("storage") - await Bun.write(target, JSON.stringify(content, null, 2)) + return withErrorHandling(async () => { + using _ = await Lock.write("storage") + await Bun.write(target, JSON.stringify(content, null, 2)) + }) + } + + async function withErrorHandling(body: () => Promise) { + return body().catch((e) => { + if (!(e instanceof Error)) + throw e + const errnoException = e as NodeJS.ErrnoException + if (errnoException.code === "ENOENT") { + throw new NotFoundError({ message: `Resource not found: ${errnoException.path}` }) + } + throw e + }) } const glob = new Bun.Glob("**/*")