Skip to content

Commit eeb262e

Browse files
committed
fix(guardrails): return 499 on a cancelled validation instead of a failed verdict
1 parent a03cba0 commit eeb262e

2 files changed

Lines changed: 41 additions & 0 deletions

File tree

apps/sim/app/api/guardrails/validate/route.test.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -435,4 +435,31 @@ describe('POST /api/guardrails/validate', () => {
435435
await expect(res.json()).resolves.toEqual({ error: 'Failed to resolve billing attribution' })
436436
expect(mockValidateHallucination).not.toHaveBeenCalled()
437437
})
438+
439+
/**
440+
* The signal now reaches the scoring model, so cancellation is reachable here.
441+
* `passed: false` would read to a consumer as the guardrail rejecting the content,
442+
* blocking a run that was abandoned rather than judged.
443+
*/
444+
it('reports a cancelled run as cancellation rather than a failed guardrail', async () => {
445+
mockAuthorizeCredentialUse.mockResolvedValue({ ok: true })
446+
mockValidateHallucination.mockRejectedValueOnce(
447+
Object.assign(new Error('The operation was aborted.'), { name: 'AbortError' })
448+
)
449+
450+
const res = await POST(
451+
createMockRequest('POST', {
452+
validationType: 'hallucination',
453+
input: 'test input',
454+
knowledgeBaseId: 'kb-1',
455+
model: 'openai/gpt-4o',
456+
workflowId: 'wf-1',
457+
})
458+
)
459+
460+
expect(res.status).toBe(499)
461+
const json = await res.json()
462+
expect(json.success).toBe(false)
463+
expect(json.output?.passed).toBeUndefined()
464+
})
438465
})

apps/sim/app/api/guardrails/validate/route.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import {
2828
ProviderNotAllowedError,
2929
} from '@/ee/access-control/utils/permission-check'
3030
import type { ResolvedSecretTraceRegistry } from '@/executor/utils/resolved-secret-trace-registry'
31+
import { isAbortError } from '@/providers/streaming-tool-loop-shared'
3132
import { getProviderFromModel } from '@/providers/utils'
3233

3334
const logger = createLogger('GuardrailsValidateAPI')
@@ -372,6 +373,19 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
372373
},
373374
})
374375
} catch (error: any) {
376+
/**
377+
* A cancelled run must not be reshaped into a verdict. `passed: false` here reads
378+
* to a consumer as the guardrail rejecting the content, so an abandoned run would
379+
* block content that was never actually judged. 499 matches the convention the
380+
* workflow execute route already uses for a client-cancelled request.
381+
*/
382+
if (isAbortError(error)) {
383+
logger.info(`[${requestId}] Guardrails validation cancelled by client`)
384+
return NextResponse.json(
385+
{ success: false, error: 'Client cancelled request' },
386+
{ status: 499 }
387+
)
388+
}
375389
logger.error(`[${requestId}] Guardrails validation failed`, { error })
376390
return NextResponse.json({
377391
success: true,

0 commit comments

Comments
 (0)