fix(server): return diagnosable body for schema rejections (#26631)
This commit is contained in:
@@ -21,6 +21,7 @@ import { TuiApi } from "./groups/tui"
|
||||
import { WorkspaceApi } from "./groups/workspace"
|
||||
import { V2Api } from "./groups/v2"
|
||||
import { Authorization } from "./middleware/authorization"
|
||||
import { SchemaErrorMiddleware } from "./middleware/schema-error"
|
||||
|
||||
// SSE event schemas built from the BusEvent/SyncEvent registries.
|
||||
const EventSchema = Schema.Union(BusEvent.effectPayloads()).annotate({ identifier: "Event" })
|
||||
@@ -29,6 +30,7 @@ const SyncEventSchemas = SyncEvent.effectPayloads()
|
||||
export const RootHttpApi = HttpApi.make("opencode-root")
|
||||
.addHttpApi(ControlApi)
|
||||
.addHttpApi(GlobalApi)
|
||||
.middleware(SchemaErrorMiddleware)
|
||||
.middleware(Authorization)
|
||||
|
||||
export const InstanceHttpApi = HttpApi.make("opencode-instance")
|
||||
@@ -47,6 +49,7 @@ export const InstanceHttpApi = HttpApi.make("opencode-instance")
|
||||
.addHttpApi(V2Api)
|
||||
.addHttpApi(TuiApi)
|
||||
.addHttpApi(WorkspaceApi)
|
||||
.middleware(SchemaErrorMiddleware)
|
||||
|
||||
export const OpenCodeHttpApi = HttpApi.make("opencode")
|
||||
.addHttpApi(RootHttpApi)
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import { Effect } from "effect"
|
||||
import { HttpServerResponse } from "effect/unstable/http"
|
||||
import { HttpApiMiddleware } from "effect/unstable/httpapi"
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
|
||||
const log = Log.create({ service: "server" })
|
||||
|
||||
// Effect's Issue formatter recursively dumps the rejected `actual` value with
|
||||
// no truncation, so a 5KB invalid array produces a ~360KB string. Cap to keep
|
||||
// 4xx responses small and avoid mirroring entire request payloads (which may
|
||||
// contain secrets) into the response body and log file.
|
||||
const REASON_LIMIT = 1024
|
||||
function truncateReason(reason: string) {
|
||||
if (reason.length <= REASON_LIMIT) return reason
|
||||
return reason.slice(0, REASON_LIMIT) + `… (${reason.length - REASON_LIMIT} more chars)`
|
||||
}
|
||||
|
||||
// Default Respondable returns an empty 400 body. Match the NamedError shape
|
||||
// used by other 4xx/5xx so the SDK's `wrapClientError` extracts `.data.message`.
|
||||
export class SchemaErrorMiddleware extends HttpApiMiddleware.Service<SchemaErrorMiddleware>()(
|
||||
"@opencode/HttpApiSchemaError",
|
||||
) {}
|
||||
|
||||
export const schemaErrorLayer = HttpApiMiddleware.layerSchemaErrorTransform(
|
||||
SchemaErrorMiddleware,
|
||||
(error) => {
|
||||
const reason = truncateReason(error.cause.message)
|
||||
log.warn("schema rejection", { kind: error.kind, reason })
|
||||
return Effect.succeed(
|
||||
HttpServerResponse.jsonUnsafe(
|
||||
{ name: "BadRequest", data: { message: reason, kind: error.kind } },
|
||||
{ status: 400 },
|
||||
),
|
||||
)
|
||||
},
|
||||
)
|
||||
@@ -178,17 +178,20 @@ function addLegacyErrorSchemas(spec: OpenApiSpec) {
|
||||
if (!spec.components?.schemas) return
|
||||
spec.components.schemas.BadRequestError = {
|
||||
type: "object",
|
||||
required: ["data", "errors", "success"],
|
||||
required: ["name", "data"],
|
||||
properties: {
|
||||
data: {},
|
||||
errors: {
|
||||
type: "array",
|
||||
items: {
|
||||
type: "object",
|
||||
additionalProperties: {},
|
||||
name: { type: "string", enum: ["BadRequest"] },
|
||||
data: {
|
||||
type: "object",
|
||||
required: ["message"],
|
||||
properties: {
|
||||
message: { type: "string" },
|
||||
kind: {
|
||||
type: "string",
|
||||
enum: ["Params", "Headers", "Query", "Body", "Payload"],
|
||||
},
|
||||
},
|
||||
},
|
||||
success: { type: "boolean", enum: [false] },
|
||||
},
|
||||
}
|
||||
spec.components.schemas.NotFoundError = {
|
||||
|
||||
@@ -84,6 +84,7 @@ import { compressionLayer } from "./middleware/compression"
|
||||
import { corsVaryFix } from "./middleware/cors-vary"
|
||||
import { errorLayer } from "./middleware/error"
|
||||
import { fenceLayer } from "./middleware/fence"
|
||||
import { schemaErrorLayer } from "./middleware/schema-error"
|
||||
|
||||
export const context = Context.makeUnsafe<unknown>(new Map())
|
||||
|
||||
@@ -114,6 +115,7 @@ const authOnlyRouterLayer = authorizationRouterMiddleware.layer.pipe(Layer.provi
|
||||
const httpApiAuthLayer = authorizationLayer.pipe(Layer.provide(ServerAuth.Config.defaultLayer))
|
||||
const rootApiRoutes = HttpApiBuilder.layer(RootHttpApi).pipe(
|
||||
Layer.provide([controlHandlers, globalHandlers]),
|
||||
Layer.provide(schemaErrorLayer),
|
||||
Layer.provide(httpApiAuthLayer),
|
||||
)
|
||||
const instanceRouterLayer = authorizationRouterMiddleware
|
||||
@@ -150,6 +152,7 @@ const instanceRoutes = Layer.mergeAll(rawInstanceRoutes, instanceApiRoutes).pipe
|
||||
httpApiAuthLayer,
|
||||
workspaceRoutingLayer.pipe(Layer.provide(Socket.layerWebSocketConstructorGlobal)),
|
||||
instanceContextLayer,
|
||||
schemaErrorLayer,
|
||||
]),
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user