@@ -5,6 +5,7 @@ import { validate as uuidValidate, v4 as uuidv4 } from 'uuid'
55import { z } from 'zod'
66import { checkHybridAuth } from '@/lib/auth/hybrid'
77import { isTriggerDevEnabled } from '@/lib/core/config/feature-flags'
8+ import { getTimeoutErrorMessage , isTimeoutError } from '@/lib/core/execution-limits'
89import { generateRequestId } from '@/lib/core/utils/request'
910import { SSE_HEADERS } from '@/lib/core/utils/sse'
1011import { getBaseUrl } from '@/lib/core/utils/urls'
@@ -120,10 +121,6 @@ type AsyncExecutionParams = {
120121 triggerType : CoreTriggerType
121122}
122123
123- /**
124- * Handles async workflow execution by queueing a background job.
125- * Returns immediately with a 202 Accepted response containing the job ID.
126- */
127124async function handleAsyncExecution ( params : AsyncExecutionParams ) : Promise < NextResponse > {
128125 const { requestId, workflowId, userId, input, triggerType } = params
129126
@@ -405,6 +402,7 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
405402
406403 if ( ! enableSSE ) {
407404 logger . info ( `[${ requestId } ] Using non-SSE execution (direct JSON response)` )
405+ const syncTimeout = preprocessResult . executionTimeout ?. sync
408406 try {
409407 const metadata : ExecutionMetadata = {
410408 requestId,
@@ -438,6 +436,7 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
438436 includeFileBase64,
439437 base64MaxBytes,
440438 stopAfterBlockId,
439+ abortSignal : syncTimeout ? AbortSignal . timeout ( syncTimeout ) : undefined ,
441440 } )
442441
443442 const outputWithBase64 = includeFileBase64
@@ -473,11 +472,23 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
473472
474473 return NextResponse . json ( filteredResult )
475474 } catch ( error : unknown ) {
476- const errorMessage = error instanceof Error ? error . message : 'Unknown error'
477- logger . error ( `[${ requestId } ] Non-SSE execution failed: ${ errorMessage } ` )
475+ const isTimeout = isTimeoutError ( error )
476+ const errorMessage = isTimeout
477+ ? getTimeoutErrorMessage ( error , syncTimeout )
478+ : error instanceof Error
479+ ? error . message
480+ : 'Unknown error'
481+
482+ logger . error ( `[${ requestId } ] Non-SSE execution failed: ${ errorMessage } ` , { isTimeout } )
478483
479484 const executionResult = hasExecutionResult ( error ) ? error . executionResult : undefined
480485
486+ await loggingSession . safeCompleteWithError ( {
487+ totalDurationMs : executionResult ?. metadata ?. duration ,
488+ error : { message : errorMessage } ,
489+ traceSpans : executionResult ?. logs as any ,
490+ } )
491+
481492 return NextResponse . json (
482493 {
483494 success : false ,
@@ -491,7 +502,7 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
491502 }
492503 : undefined ,
493504 } ,
494- { status : 500 }
505+ { status : isTimeout ? 408 : 500 }
495506 )
496507 }
497508 }
@@ -537,6 +548,16 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
537548 const encoder = new TextEncoder ( )
538549 const abortController = new AbortController ( )
539550 let isStreamClosed = false
551+ let isTimedOut = false
552+
553+ const syncTimeout = preprocessResult . executionTimeout ?. sync
554+ let timeoutId : NodeJS . Timeout | undefined
555+ if ( syncTimeout ) {
556+ timeoutId = setTimeout ( ( ) => {
557+ isTimedOut = true
558+ abortController . abort ( )
559+ } , syncTimeout )
560+ }
540561
541562 const stream = new ReadableStream < Uint8Array > ( {
542563 async start ( controller ) {
@@ -763,16 +784,35 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
763784 }
764785
765786 if ( result . status === 'cancelled' ) {
766- logger . info ( `[${ requestId } ] Workflow execution was cancelled` )
767- sendEvent ( {
768- type : 'execution:cancelled' ,
769- timestamp : new Date ( ) . toISOString ( ) ,
770- executionId,
771- workflowId,
772- data : {
773- duration : result . metadata ?. duration || 0 ,
774- } ,
775- } )
787+ if ( isTimedOut && syncTimeout ) {
788+ const timeoutErrorMessage = getTimeoutErrorMessage ( null , syncTimeout )
789+ logger . info ( `[${ requestId } ] Workflow execution timed out` , { timeoutMs : syncTimeout } )
790+
791+ await loggingSession . markAsFailed ( timeoutErrorMessage )
792+
793+ sendEvent ( {
794+ type : 'execution:error' ,
795+ timestamp : new Date ( ) . toISOString ( ) ,
796+ executionId,
797+ workflowId,
798+ data : {
799+ error : timeoutErrorMessage ,
800+ duration : result . metadata ?. duration || 0 ,
801+ } ,
802+ } )
803+ } else {
804+ logger . info ( `[${ requestId } ] Workflow execution was cancelled` )
805+
806+ sendEvent ( {
807+ type : 'execution:cancelled' ,
808+ timestamp : new Date ( ) . toISOString ( ) ,
809+ executionId,
810+ workflowId,
811+ data : {
812+ duration : result . metadata ?. duration || 0 ,
813+ } ,
814+ } )
815+ }
776816 return
777817 }
778818
@@ -799,11 +839,23 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
799839 // Cleanup base64 cache for this execution
800840 await cleanupExecutionBase64Cache ( executionId )
801841 } catch ( error : unknown ) {
802- const errorMessage = error instanceof Error ? error . message : 'Unknown error'
803- logger . error ( `[${ requestId } ] SSE execution failed: ${ errorMessage } ` )
842+ const isTimeout = isTimeoutError ( error ) || isTimedOut
843+ const errorMessage = isTimeout
844+ ? getTimeoutErrorMessage ( error , syncTimeout )
845+ : error instanceof Error
846+ ? error . message
847+ : 'Unknown error'
848+
849+ logger . error ( `[${ requestId } ] SSE execution failed: ${ errorMessage } ` , { isTimeout } )
804850
805851 const executionResult = hasExecutionResult ( error ) ? error . executionResult : undefined
806852
853+ await loggingSession . safeCompleteWithError ( {
854+ totalDurationMs : executionResult ?. metadata ?. duration ,
855+ error : { message : errorMessage } ,
856+ traceSpans : executionResult ?. logs as any ,
857+ } )
858+
807859 sendEvent ( {
808860 type : 'execution:error' ,
809861 timestamp : new Date ( ) . toISOString ( ) ,
@@ -815,18 +867,18 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
815867 } ,
816868 } )
817869 } finally {
870+ if ( timeoutId ) clearTimeout ( timeoutId )
818871 if ( ! isStreamClosed ) {
819872 try {
820873 controller . enqueue ( encoder . encode ( 'data: [DONE]\n\n' ) )
821874 controller . close ( )
822- } catch {
823- // Stream already closed - nothing to do
824- }
875+ } catch { }
825876 }
826877 }
827878 } ,
828879 cancel ( ) {
829880 isStreamClosed = true
881+ if ( timeoutId ) clearTimeout ( timeoutId )
830882 logger . info ( `[${ requestId } ] Client aborted SSE stream, signalling cancellation` )
831883 abortController . abort ( )
832884 markExecutionCancelled ( executionId ) . catch ( ( ) => { } )
0 commit comments