fix(httpapi): return request not found errors (#28693)

This commit is contained in:
Shoubhit Dash
2026-05-22 16:29:12 +05:30
committed by GitHub
parent 76d9c2cd76
commit 4ce247eaba
15 changed files with 213 additions and 56 deletions
+6 -2
View File
@@ -100,6 +100,10 @@ export class DeniedError extends Schema.TaggedErrorClass<DeniedError>()("Permiss
}
}
export class NotFoundError extends Schema.TaggedErrorClass<NotFoundError>()("Permission.NotFoundError", {
requestID: PermissionID,
}) {}
export type Error = DeniedError | RejectedError | CorrectedError
export const AskInput = Schema.Struct({
@@ -117,7 +121,7 @@ export type ReplyInput = Schema.Schema.Type<typeof ReplyInput>
export interface Interface {
readonly ask: (input: AskInput) => Effect.Effect<void, Error>
readonly reply: (input: ReplyInput) => Effect.Effect<void>
readonly reply: (input: ReplyInput) => Effect.Effect<void, NotFoundError>
readonly list: () => Effect.Effect<ReadonlyArray<Request>>
}
@@ -209,7 +213,7 @@ export const layer = Layer.effect(
const reply = Effect.fn("Permission.reply")(function* (input: ReplyInput) {
const { approved, pending } = yield* InstanceState.get(state)
const existing = pending.get(input.requestID)
if (!existing) return
if (!existing) return yield* new NotFoundError({ requestID: input.requestID })
pending.delete(input.requestID)
yield* bus.publish(Event.Replied, {
+8 -4
View File
@@ -98,6 +98,10 @@ export class RejectedError extends Schema.TaggedErrorClass<RejectedError>()("Que
}
}
export class NotFoundError extends Schema.TaggedErrorClass<NotFoundError>()("Question.NotFoundError", {
requestID: QuestionID,
}) {}
interface PendingEntry {
info: Request
deferred: Deferred.Deferred<ReadonlyArray<Answer>, RejectedError>
@@ -115,8 +119,8 @@ export interface Interface {
questions: ReadonlyArray<Info>
tool?: Tool
}) => Effect.Effect<ReadonlyArray<Answer>, RejectedError>
readonly reply: (input: { requestID: QuestionID; answers: ReadonlyArray<Answer> }) => Effect.Effect<void>
readonly reject: (requestID: QuestionID) => Effect.Effect<void>
readonly reply: (input: { requestID: QuestionID; answers: ReadonlyArray<Answer> }) => Effect.Effect<void, NotFoundError>
readonly reject: (requestID: QuestionID) => Effect.Effect<void, NotFoundError>
readonly list: () => Effect.Effect<ReadonlyArray<Request>>
}
@@ -180,7 +184,7 @@ export const layer = Layer.effect(
const existing = pending.get(input.requestID)
if (!existing) {
log.warn("reply for unknown request", { requestID: input.requestID })
return
return yield* new NotFoundError({ requestID: input.requestID })
}
pending.delete(input.requestID)
log.info("replied", { requestID: input.requestID, answers: input.answers })
@@ -197,7 +201,7 @@ export const layer = Layer.effect(
const existing = pending.get(requestID)
if (!existing) {
log.warn("reject for unknown request", { requestID })
return
return yield* new NotFoundError({ requestID })
}
pending.delete(requestID)
log.info("rejected", { requestID })
@@ -122,6 +122,24 @@ export class SessionBusyError extends Schema.TaggedErrorClass<SessionBusyError>(
{ httpApiStatus: 409 },
) {}
export class QuestionNotFoundError extends Schema.TaggedErrorClass<QuestionNotFoundError>()(
"QuestionNotFoundError",
{
requestID: Schema.String,
message: Schema.String,
},
{ httpApiStatus: 404 },
) {}
export class PermissionNotFoundError extends Schema.TaggedErrorClass<PermissionNotFoundError>()(
"PermissionNotFoundError",
{
requestID: Schema.String,
message: Schema.String,
},
{ httpApiStatus: 404 },
) {}
export class ApiNotFoundError extends Schema.ErrorClass<ApiNotFoundError>("NotFoundError")(
{
name: Schema.Literal("NotFoundError"),
@@ -2,6 +2,7 @@ import { Permission } from "@/permission"
import { PermissionID } from "@/permission/schema"
import { Schema } from "effect"
import { HttpApi, HttpApiEndpoint, HttpApiError, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
import { PermissionNotFoundError } from "../errors"
import { Authorization } from "../middleware/authorization"
import { InstanceContextMiddleware } from "../middleware/instance-context"
import { WorkspaceRoutingMiddleware, WorkspaceRoutingQuery } from "../middleware/workspace-routing"
@@ -32,7 +33,7 @@ export const PermissionApi = HttpApi.make("permission")
query: WorkspaceRoutingQuery,
payload: ReplyPayload,
success: described(Schema.Boolean, "Permission processed successfully"),
error: [HttpApiError.BadRequest, HttpApiError.NotFound],
error: [HttpApiError.BadRequest, PermissionNotFoundError],
}).annotateMerge(
OpenApi.annotations({
identifier: "permission.reply",
@@ -2,6 +2,7 @@ import { Question } from "@/question"
import { QuestionID } from "@/question/schema"
import { Schema } from "effect"
import { HttpApi, HttpApiEndpoint, HttpApiError, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
import { QuestionNotFoundError } from "../errors"
import { Authorization } from "../middleware/authorization"
import { InstanceContextMiddleware } from "../middleware/instance-context"
import { WorkspaceRoutingMiddleware, WorkspaceRoutingQuery } from "../middleware/workspace-routing"
@@ -33,7 +34,7 @@ export const QuestionApi = HttpApi.make("question")
query: WorkspaceRoutingQuery,
payload: ReplyPayload,
success: described(Schema.Boolean, "Question answered successfully"),
error: [HttpApiError.BadRequest, HttpApiError.NotFound],
error: [HttpApiError.BadRequest, QuestionNotFoundError],
}).annotateMerge(
OpenApi.annotations({
identifier: "question.reply",
@@ -45,7 +46,7 @@ export const QuestionApi = HttpApi.make("question")
params: { requestID: QuestionID },
query: WorkspaceRoutingQuery,
success: described(Schema.Boolean, "Question rejected successfully"),
error: [HttpApiError.BadRequest, HttpApiError.NotFound],
error: [HttpApiError.BadRequest, QuestionNotFoundError],
}).annotateMerge(
OpenApi.annotations({
identifier: "question.reject",
@@ -19,7 +19,7 @@ import {
WorkspaceRoutingQuery,
WorkspaceRoutingQueryFields,
} from "../middleware/workspace-routing"
import { ApiNotFoundError, SessionBusyError } from "../errors"
import { ApiNotFoundError, PermissionNotFoundError, SessionBusyError } from "../errors"
import { described } from "./metadata"
import { QueryBoolean } from "./query"
@@ -393,7 +393,7 @@ export const SessionApi = HttpApi.make("session")
query: WorkspaceRoutingQuery,
payload: PermissionResponsePayload,
success: described(Schema.Boolean, "Permission processed successfully"),
error: [HttpApiError.BadRequest, ApiNotFoundError],
error: [HttpApiError.BadRequest, ApiNotFoundError, PermissionNotFoundError],
}).annotateMerge(
OpenApi.annotations({
identifier: "permission.respond",
@@ -3,6 +3,7 @@ import { PermissionID } from "@/permission/schema"
import { Effect } from "effect"
import { HttpApiBuilder } from "effect/unstable/httpapi"
import { InstanceHttpApi } from "../api"
import { PermissionNotFoundError } from "../errors"
export const permissionHandlers = HttpApiBuilder.group(InstanceHttpApi, "permission", (handlers) =>
Effect.gen(function* () {
@@ -16,11 +17,22 @@ export const permissionHandlers = HttpApiBuilder.group(InstanceHttpApi, "permiss
params: { requestID: PermissionID }
payload: Permission.ReplyBody
}) {
yield* svc.reply({
requestID: ctx.params.requestID,
reply: ctx.payload.reply,
message: ctx.payload.message,
})
yield* svc
.reply({
requestID: ctx.params.requestID,
reply: ctx.payload.reply,
message: ctx.payload.message,
})
.pipe(
Effect.catchTag("Permission.NotFoundError", (error) =>
Effect.fail(
new PermissionNotFoundError({
requestID: String(error.requestID),
message: `Permission request not found: ${error.requestID}`,
}),
),
),
)
return true
})
@@ -3,6 +3,7 @@ import { QuestionID } from "@/question/schema"
import { Effect } from "effect"
import { HttpApiBuilder } from "effect/unstable/httpapi"
import { InstanceHttpApi } from "../api"
import { QuestionNotFoundError } from "../errors"
export const questionHandlers = HttpApiBuilder.group(InstanceHttpApi, "question", (handlers) =>
Effect.gen(function* () {
@@ -16,15 +17,35 @@ export const questionHandlers = HttpApiBuilder.group(InstanceHttpApi, "question"
params: { requestID: QuestionID }
payload: Question.Reply
}) {
yield* svc.reply({
requestID: ctx.params.requestID,
answers: ctx.payload.answers,
})
yield* svc
.reply({
requestID: ctx.params.requestID,
answers: ctx.payload.answers,
})
.pipe(
Effect.catchTag("Question.NotFoundError", (error) =>
Effect.fail(
new QuestionNotFoundError({
requestID: String(error.requestID),
message: `Question request not found: ${error.requestID}`,
}),
),
),
)
return true
})
const reject = Effect.fn("QuestionHttpApi.reject")(function* (ctx: { params: { requestID: QuestionID } }) {
yield* svc.reject(ctx.params.requestID)
yield* svc.reject(ctx.params.requestID).pipe(
Effect.catchTag("Question.NotFoundError", (error) =>
Effect.fail(
new QuestionNotFoundError({
requestID: String(error.requestID),
message: `Question request not found: ${error.requestID}`,
}),
),
),
)
return true
})
@@ -34,6 +34,7 @@ import {
SummarizePayload,
UpdatePayload,
} from "../groups/session"
import { PermissionNotFoundError } from "../errors"
import * as SessionError from "./session-errors"
const tryParseJson = (text: string) =>
@@ -356,7 +357,16 @@ export const sessionHandlers = HttpApiBuilder.group(InstanceHttpApi, "session",
payload: typeof PermissionResponsePayload.Type
}) {
yield* requireSession(ctx.params.sessionID)
yield* permissionSvc.reply({ requestID: ctx.params.permissionID, reply: ctx.payload.response })
yield* permissionSvc.reply({ requestID: ctx.params.permissionID, reply: ctx.payload.response }).pipe(
Effect.catchTag("Permission.NotFoundError", (error) =>
Effect.fail(
new PermissionNotFoundError({
requestID: String(error.requestID),
message: `Permission request not found: ${error.requestID}`,
}),
),
),
)
return true
})