File tree Expand file tree Collapse file tree
apps/sim/app/api/guardrails/validate Expand file tree Collapse file tree Original file line number Diff line number Diff 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} )
Original file line number Diff line number Diff line change @@ -28,6 +28,7 @@ import {
2828 ProviderNotAllowedError ,
2929} from '@/ee/access-control/utils/permission-check'
3030import type { ResolvedSecretTraceRegistry } from '@/executor/utils/resolved-secret-trace-registry'
31+ import { isAbortError } from '@/providers/streaming-tool-loop-shared'
3132import { getProviderFromModel } from '@/providers/utils'
3233
3334const 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 ,
You can’t perform that action at this time.
0 commit comments