diff --git a/src/bounties/bounty-state-machine.ts b/src/bounties/bounty-state-machine.ts index b78b0714..f32edb08 100644 --- a/src/bounties/bounty-state-machine.ts +++ b/src/bounties/bounty-state-machine.ts @@ -40,9 +40,21 @@ export const BOUNTY_TRANSITIONS: Record = { [BountyStatus.EXPIRED]: [BountyStatus.REFUNDED], }; -export class InvalidBountyTransitionError extends Error { +import { HttpException, HttpStatus } from '@nestjs/common'; + +/** + * A state-transition rejection is a CLIENT-facing error — the bounty's + * current state conflicts with the requested transition (e.g. claiming a + * bounty already claimed). Extending HttpException(409) surfaces it as a real + * HTTP status instead of an opaque 500 (#579) — a plain Error would hit the + * global filter's internal-error path and leak nothing useful. + */ +export class InvalidBountyTransitionError extends HttpException { constructor(from: BountyStatus, to: BountyStatus) { - super(`Cannot transition bounty from "${from}" to "${to}"`); + super( + `Cannot transition bounty from "${from}" to "${to}"`, + HttpStatus.CONFLICT, + ); this.name = 'InvalidBountyTransitionError'; } } diff --git a/src/milestones/milestone-state-machine.ts b/src/milestones/milestone-state-machine.ts index 05a7e287..b487881b 100644 --- a/src/milestones/milestone-state-machine.ts +++ b/src/milestones/milestone-state-machine.ts @@ -21,18 +21,30 @@ export const MILESTONE_TRANSITIONS: Record = [MilestoneStatus.CLOSED]: [], }; -export class InvalidMilestoneTransitionError extends Error { +import { HttpException, HttpStatus } from '@nestjs/common'; + +/** A milestone transition-rejection is a client-facing 409, not a 500 (#579). */ +export class InvalidMilestoneTransitionError extends HttpException { constructor(from: MilestoneStatus, to: MilestoneStatus) { - super(`Cannot transition milestone from "${from}" to "${to}"`); + super( + `Cannot transition milestone from "${from}" to "${to}"`, + HttpStatus.CONFLICT, + ); this.name = 'InvalidMilestoneTransitionError'; } } -export function canTransition(from: MilestoneStatus, to: MilestoneStatus): boolean { +export function canTransition( + from: MilestoneStatus, + to: MilestoneStatus, +): boolean { return MILESTONE_TRANSITIONS[from]?.includes(to) ?? false; } -export function assertTransition(from: MilestoneStatus, to: MilestoneStatus): void { +export function assertTransition( + from: MilestoneStatus, + to: MilestoneStatus, +): void { if (!canTransition(from, to)) { throw new InvalidMilestoneTransitionError(from, to); }