Improve http error codes (#3186)

This commit is contained in:
Haris Gušić
2025-10-15 17:53:09 +02:00
committed by GitHub
parent 1ba5535460
commit 92bc78a2d3
2 changed files with 94 additions and 21 deletions

View File

@@ -33,6 +33,8 @@ import { lazy } from "../util/lazy"
import { Todo } from "../session/todo" import { Todo } from "../session/todo"
import { InstanceBootstrap } from "../project/bootstrap" import { InstanceBootstrap } from "../project/bootstrap"
import { MCP } from "../mcp" import { MCP } from "../mcp"
import { Storage } from "../storage/storage"
import type { ContentfulStatusCode } from "hono/utils/http-status"
const ERRORS = { const ERRORS = {
400: { 400: {
@@ -42,17 +44,33 @@ const ERRORS = {
schema: resolver( schema: resolver(
z z
.object({ .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({ .meta({
ref: "Error", ref: "BadRequestError",
}), }),
), ),
}, },
}, },
}, },
404: {
description: "Not found",
content: {
"application/json": {
schema: resolver(
Storage.NotFoundError.Schema
)
},
},
},
} as const } as const
function errors(...codes: number[]) {
return Object.fromEntries(codes.map((code) => [code, ERRORS[code as keyof typeof ERRORS]]))
}
export namespace Server { export namespace Server {
const log = Log.create({ service: "server" }) const log = Log.create({ service: "server" })
@@ -68,13 +86,18 @@ export namespace Server {
error: err, error: err,
}) })
if (err instanceof NamedError) { if (err instanceof NamedError) {
return c.json(err.toObject(), { let status: ContentfulStatusCode
status: 400, 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() const message = err instanceof Error && err.stack ? err.stack : err.toString()
return c.json(new NamedError.Unknown({ message }).toObject(), { return c.json(new NamedError.Unknown({ message }).toObject(), {
status: 400, status: 500,
}) })
}) })
.use(async (c, next) => { .use(async (c, next) => {
@@ -153,7 +176,7 @@ export namespace Server {
}, },
}, },
}, },
...ERRORS, ...errors(400),
}, },
}), }),
validator("json", Config.Info), validator("json", Config.Info),
@@ -177,7 +200,7 @@ export namespace Server {
}, },
}, },
}, },
...ERRORS, ...errors(400),
}, },
}), }),
async (c) => { async (c) => {
@@ -210,7 +233,7 @@ export namespace Server {
}, },
}, },
}, },
...ERRORS, ...errors(400),
}, },
}), }),
validator( validator(
@@ -305,6 +328,7 @@ export namespace Server {
}, },
}, },
}, },
...errors(400, 404),
}, },
}), }),
validator( validator(
@@ -333,6 +357,7 @@ export namespace Server {
}, },
}, },
}, },
...errors(400, 404),
}, },
}), }),
validator( validator(
@@ -361,6 +386,7 @@ export namespace Server {
}, },
}, },
}, },
...errors(400, 404),
}, },
}), }),
validator( validator(
@@ -381,7 +407,7 @@ export namespace Server {
description: "Create a new session", description: "Create a new session",
operationId: "session.create", operationId: "session.create",
responses: { responses: {
...ERRORS, ...errors(400),
200: { 200: {
description: "Successfully created session", description: "Successfully created session",
content: { content: {
@@ -413,6 +439,7 @@ export namespace Server {
}, },
}, },
}, },
...errors(400, 404),
}, },
}), }),
validator( validator(
@@ -440,6 +467,7 @@ export namespace Server {
}, },
}, },
}, },
...errors(400, 404),
}, },
}), }),
validator( validator(
@@ -481,6 +509,7 @@ export namespace Server {
}, },
}, },
}, },
...errors(400, 404),
}, },
}), }),
validator( validator(
@@ -541,6 +570,7 @@ export namespace Server {
}, },
}, },
}, },
...errors(400, 404),
}, },
}), }),
validator( validator(
@@ -567,6 +597,7 @@ export namespace Server {
}, },
}, },
}, },
...errors(400, 404),
}, },
}), }),
validator( validator(
@@ -596,6 +627,7 @@ export namespace Server {
}, },
}, },
}, },
...errors(400, 404),
}, },
}), }),
validator( validator(
@@ -625,6 +657,7 @@ export namespace Server {
}, },
}, },
}, },
...errors(400, 404),
}, },
}), }),
validator( validator(
@@ -661,6 +694,7 @@ export namespace Server {
}, },
}, },
}, },
...errors(400, 404),
}, },
}), }),
validator( validator(
@@ -693,6 +727,7 @@ export namespace Server {
}, },
}, },
}, },
...errors(400, 404),
}, },
}), }),
validator( validator(
@@ -727,6 +762,7 @@ export namespace Server {
}, },
}, },
}, },
...errors(400, 404),
}, },
}), }),
validator( validator(
@@ -762,6 +798,7 @@ export namespace Server {
}, },
}, },
}, },
...errors(400, 404),
}, },
}), }),
validator( validator(
@@ -792,6 +829,7 @@ export namespace Server {
}, },
}, },
}, },
...errors(400, 404),
}, },
}), }),
validator( validator(
@@ -822,6 +860,7 @@ export namespace Server {
}, },
}, },
}, },
...errors(400, 404),
}, },
}), }),
validator( validator(
@@ -852,6 +891,7 @@ export namespace Server {
}, },
}, },
}, },
...errors(400, 404),
}, },
}), }),
validator( validator(
@@ -879,6 +919,7 @@ export namespace Server {
}, },
}, },
}, },
...errors(400, 404),
}, },
}), }),
validator( validator(
@@ -1132,6 +1173,7 @@ export namespace Server {
}, },
}, },
}, },
...errors(400),
}, },
}), }),
validator( validator(
@@ -1223,6 +1265,7 @@ export namespace Server {
}, },
}, },
}, },
...errors(400),
}, },
}), }),
validator( validator(
@@ -1355,6 +1398,7 @@ export namespace Server {
}, },
}, },
}, },
...errors(400),
}, },
}), }),
validator( validator(
@@ -1406,7 +1450,7 @@ export namespace Server {
}, },
}, },
}, },
...ERRORS, ...errors(400),
}, },
}), }),
validator( validator(

View File

@@ -5,12 +5,21 @@ import { Global } from "../global"
import { lazy } from "../util/lazy" import { lazy } from "../util/lazy"
import { Lock } from "../util/lock" import { Lock } from "../util/lock"
import { $ } from "bun" import { $ } from "bun"
import { NamedError } from "@/util/error"
import z from "zod"
export namespace Storage { export namespace Storage {
const log = Log.create({ service: "storage" }) const log = Log.create({ service: "storage" })
type Migration = (dir: string) => Promise<void> type Migration = (dir: string) => Promise<void>
export const NotFoundError = NamedError.create(
"NotFoundError",
z.object({
message: z.string(),
}),
)
const MIGRATIONS: Migration[] = [ const MIGRATIONS: Migration[] = [
async (dir) => { async (dir) => {
const project = path.resolve(dir, "../project") const project = path.resolve(dir, "../project")
@@ -131,31 +140,51 @@ export namespace Storage {
export async function remove(key: string[]) { export async function remove(key: string[]) {
const dir = await state().then((x) => x.dir) const dir = await state().then((x) => x.dir)
const target = path.join(dir, ...key) + ".json" const target = path.join(dir, ...key) + ".json"
await fs.unlink(target).catch(() => {}) return withErrorHandling(async () => {
await fs.unlink(target).catch(() => {})
})
} }
export async function read<T>(key: string[]) { export async function read<T>(key: string[]) {
const dir = await state().then((x) => x.dir) const dir = await state().then((x) => x.dir)
const target = path.join(dir, ...key) + ".json" const target = path.join(dir, ...key) + ".json"
using _ = await Lock.read(target) return withErrorHandling(async () => {
return Bun.file(target).json() as Promise<T> using _ = await Lock.read(target)
return Bun.file(target).json() as Promise<T>
})
} }
export async function update<T>(key: string[], fn: (draft: T) => void) { export async function update<T>(key: string[], fn: (draft: T) => void) {
const dir = await state().then((x) => x.dir) const dir = await state().then((x) => x.dir)
const target = path.join(dir, ...key) + ".json" const target = path.join(dir, ...key) + ".json"
using _ = await Lock.write("storage") return withErrorHandling(async () => {
const content = await Bun.file(target).json() using _ = await Lock.write("storage")
fn(content) const content = await Bun.file(target).json()
await Bun.write(target, JSON.stringify(content, null, 2)) fn(content)
return content as T await Bun.write(target, JSON.stringify(content, null, 2))
return content as T
})
} }
export async function write<T>(key: string[], content: T) { export async function write<T>(key: string[], content: T) {
const dir = await state().then((x) => x.dir) const dir = await state().then((x) => x.dir)
const target = path.join(dir, ...key) + ".json" const target = path.join(dir, ...key) + ".json"
using _ = await Lock.write("storage") return withErrorHandling(async () => {
await Bun.write(target, JSON.stringify(content, null, 2)) using _ = await Lock.write("storage")
await Bun.write(target, JSON.stringify(content, null, 2))
})
}
async function withErrorHandling<T>(body: () => Promise<T>) {
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("**/*") const glob = new Bun.Glob("**/*")