fix(httpapi): return session busy error bodies (#28684)

This commit is contained in:
Shoubhit Dash 2026-05-21 22:53:49 +05:30 committed by GitHub
parent 6c24062d2f
commit 82b796ce31
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 48 additions and 8 deletions

View file

@ -19,7 +19,7 @@ import {
WorkspaceRoutingQuery,
WorkspaceRoutingQueryFields,
} from "../middleware/workspace-routing"
import { ApiNotFoundError } from "../errors"
import { ApiNotFoundError, SessionBusyError } from "../errors"
import { described } from "./metadata"
import { QueryBoolean } from "./query"
@ -354,7 +354,7 @@ export const SessionApi = HttpApi.make("session")
query: WorkspaceRoutingQuery,
payload: ShellPayload,
success: described(MessageV2.WithParts, "Created message"),
error: [HttpApiError.BadRequest, ApiNotFoundError],
error: [HttpApiError.BadRequest, ApiNotFoundError, SessionBusyError],
}).annotateMerge(
OpenApi.annotations({
identifier: "session.shell",
@ -367,7 +367,7 @@ export const SessionApi = HttpApi.make("session")
query: WorkspaceRoutingQuery,
payload: RevertPayload,
success: described(Session.Info, "Updated session"),
error: [HttpApiError.BadRequest, ApiNotFoundError],
error: [HttpApiError.BadRequest, ApiNotFoundError, SessionBusyError],
}).annotateMerge(
OpenApi.annotations({
identifier: "session.revert",
@ -380,7 +380,7 @@ export const SessionApi = HttpApi.make("session")
params: { sessionID: SessionID },
query: WorkspaceRoutingQuery,
success: described(Session.Info, "Updated session"),
error: [HttpApiError.BadRequest, ApiNotFoundError],
error: [HttpApiError.BadRequest, ApiNotFoundError, SessionBusyError],
}).annotateMerge(
OpenApi.annotations({
identifier: "session.unrevert",
@ -406,7 +406,7 @@ export const SessionApi = HttpApi.make("session")
params: { sessionID: SessionID, messageID: MessageID },
query: WorkspaceRoutingQuery,
success: described(Schema.Boolean, "Successfully deleted message"),
error: [HttpApiError.BadRequest, ApiNotFoundError],
error: [HttpApiError.BadRequest, ApiNotFoundError, SessionBusyError],
}).annotateMerge(
OpenApi.annotations({
identifier: "session.deleteMessage",

View file

@ -1,7 +1,6 @@
import type { NotFoundError as StorageNotFoundError } from "@/storage/storage"
import type { Session } from "@/session/session"
import { Effect } from "effect"
import { HttpApiError } from "effect/unstable/httpapi"
import * as ApiError from "../errors"
export function mapStorageNotFound<A, R>(self: Effect.Effect<A, StorageNotFoundError, R>) {
@ -9,5 +8,14 @@ export function mapStorageNotFound<A, R>(self: Effect.Effect<A, StorageNotFoundE
}
export function mapBusy<A, R>(self: Effect.Effect<A, Session.BusyError, R>) {
return self.pipe(Effect.catchTag("SessionBusyError", () => Effect.fail(new HttpApiError.BadRequest({}))))
return self.pipe(
Effect.catchTag("SessionBusyError", (error) =>
Effect.fail(
new ApiError.SessionBusyError({
sessionID: error.sessionID,
message: `Session is busy: ${error.sessionID}`,
}),
),
),
)
}

View file

@ -142,4 +142,19 @@ describe("PublicApi OpenAPI v2 errors", () => {
)
}
})
test("documents session busy errors", () => {
const spec = OpenApi.fromApi(PublicApi) as OpenApiSpec
for (const route of [
["post", "/session/{sessionID}/shell"],
["post", "/session/{sessionID}/revert"],
["post", "/session/{sessionID}/unrevert"],
["delete", "/session/{sessionID}/message/{messageID}"],
] as const) {
expect(componentName(responseRef(spec.paths[route[1]]?.[route[0]]?.responses?.["409"]) ?? "")).toBe(
"SessionBusyError",
)
}
})
})

View file

@ -1,7 +1,7 @@
import { afterEach, describe, expect } from "bun:test"
import { mkdir } from "node:fs/promises"
import path from "node:path"
import { Effect, Layer } from "effect"
import { Cause, Effect, Exit, Layer } from "effect"
import { Flag } from "@opencode-ai/core/flag/flag"
import { registerAdapter } from "../../src/control-plane/adapters"
import type { WorkspaceAdapter } from "../../src/control-plane/types"
@ -13,6 +13,7 @@ import { InstanceBootstrap as InstanceBootstrapService } from "../../src/project
import { InstanceStore } from "../../src/project/instance-store"
import { Project } from "../../src/project/project"
import { Server } from "../../src/server/server"
import * as HttpSessionError from "../../src/server/routes/instance/httpapi/handlers/session-errors"
import { SessionPaths } from "../../src/server/routes/instance/httpapi/groups/session"
import { Session } from "@/session/session"
import { MessageID, PartID, SessionID, type SessionID as SessionIDType } from "../../src/session/schema"
@ -215,6 +216,22 @@ afterEach(async () => {
})
describe("session HttpApi", () => {
it.effect("maps busy sessions to public session busy errors", () =>
Effect.gen(function* () {
const sessionID = SessionID.descending()
const exit = yield* HttpSessionError.mapBusy(Effect.fail(new Session.BusyError({ sessionID }))).pipe(Effect.exit)
expect(Exit.isFailure(exit)).toBe(true)
if (Exit.isFailure(exit)) {
expect(Cause.squash(exit.cause)).toMatchObject({
_tag: "SessionBusyError",
sessionID,
message: `Session is busy: ${sessionID}`,
})
}
}),
)
it.instance(
"returns declared not found errors for read routes",
() =>