@@ -10,6 +10,7 @@ import { requestJson } from '@/lib/api/client/request'
1010import { cancelWorkflowExecutionContract , workflowLogContract } from '@/lib/api/contracts/workflows'
1111import { buildTraceSpans } from '@/lib/logs/execution/trace-spans/trace-spans'
1212import { processStreamingBlockLogs } from '@/lib/tokenization'
13+ import { DirectUploadError , runUploadStrategy } from '@/lib/uploads/client/direct-upload'
1314import type { ExecutionPausedData } from '@/lib/workflows/executor/execution-events'
1415import {
1516 extractTriggerMockPayload ,
@@ -505,65 +506,82 @@ export function useWorkflowExecution() {
505506 typeof ( value as any ) . onUploadError === 'function'
506507 if ( workflowInput . files && Array . isArray ( workflowInput . files ) ) {
507508 try {
509+ const presignedEndpoint = `/api/files/presigned?type=execution&workflowId=${ encodeURIComponent ( activeWorkflowId ) } &executionId=${ encodeURIComponent ( executionId ) } &workspaceId=${ encodeURIComponent ( workspaceId ) } `
508510 for ( const fileData of workflowInput . files ) {
509- // Create FormData for upload
510- const formData = new FormData ( )
511- formData . append ( 'file' , fileData . file )
512- formData . append ( 'context' , 'execution' )
513- formData . append ( 'workflowId' , activeWorkflowId )
514- formData . append ( 'executionId' , executionId )
515- formData . append ( 'workspaceId' , workspaceId )
516-
517- // boundary-raw-fetch: multipart/form-data file upload, requestJson only supports JSON bodies
518- const response = await fetch ( '/api/files/upload' , {
519- method : 'POST' ,
520- body : formData ,
521- } )
522-
523- if ( response . ok ) {
524- const uploadResult = await response . json ( )
525- // Convert upload result to clean UserFile format
526- const processUploadResult = ( result : any ) => ( {
527- id :
528- result . id ||
529- `file_${ Date . now ( ) } _${ Math . random ( ) . toString ( 36 ) . substring ( 2 , 9 ) } ` ,
530- name : result . name ,
531- url : result . url ,
532- size : result . size ,
533- type : result . type ,
511+ try {
512+ const result = await runUploadStrategy ( {
513+ file : fileData . file ,
514+ workspaceId,
515+ context : 'execution' ,
516+ presignedEndpoint,
517+ } )
518+ uploadedFiles . push ( {
519+ id : `file_${ Date . now ( ) } _${ Math . random ( ) . toString ( 36 ) . substring ( 2 , 9 ) } ` ,
520+ name : fileData . file . name ,
521+ url : result . path ,
522+ size : fileData . file . size ,
523+ type : fileData . file . type ,
534524 key : result . key ,
535- uploadedAt : result . uploadedAt ,
536- expiresAt : result . expiresAt ,
537525 } )
538-
539- // The API returns the file directly for single uploads
540- // or { files: [...] } for multiple uploads
541- if ( uploadResult . files && Array . isArray ( uploadResult . files ) ) {
542- uploadedFiles . push ( ...uploadResult . files . map ( processUploadResult ) )
543- } else if ( uploadResult . path || uploadResult . url ) {
544- // Single file upload - the result IS the file object
545- uploadedFiles . push ( processUploadResult ( uploadResult ) )
526+ } catch ( uploadError ) {
527+ if (
528+ uploadError instanceof DirectUploadError &&
529+ uploadError . code === 'FALLBACK_REQUIRED'
530+ ) {
531+ const formData = new FormData ( )
532+ formData . append ( 'file' , fileData . file )
533+ formData . append ( 'context' , 'execution' )
534+ formData . append ( 'workflowId' , activeWorkflowId )
535+ formData . append ( 'executionId' , executionId )
536+ formData . append ( 'workspaceId' , workspaceId )
537+
538+ // boundary-raw-fetch: local-dev fallback when cloud storage is not configured; multipart upload incompatible with requestJson
539+ const response = await fetch ( '/api/files/upload' , {
540+ method : 'POST' ,
541+ body : formData ,
542+ } )
543+ if ( ! response . ok ) {
544+ const errorData = await response . json ( ) . catch ( ( ) => null )
545+ const reason =
546+ errorData ?. message || errorData ?. error || `${ response . status } `
547+ const message = `Failed to upload ${ fileData . name } : ${ reason } `
548+ logger . error ( message )
549+ if ( isUploadErrorCapable ( workflowInput ) ) {
550+ try {
551+ workflowInput . onUploadError ( message )
552+ } catch { }
553+ }
554+ continue
555+ }
556+ const uploadResult = await response . json ( )
557+ const processUploadResult = ( r : any ) => ( {
558+ id :
559+ r . id ||
560+ `file_${ Date . now ( ) } _${ Math . random ( ) . toString ( 36 ) . substring ( 2 , 9 ) } ` ,
561+ name : r . name ,
562+ url : r . url ,
563+ size : r . size ,
564+ type : r . type ,
565+ key : r . key ,
566+ uploadedAt : r . uploadedAt ,
567+ expiresAt : r . expiresAt ,
568+ } )
569+ if ( uploadResult . files && Array . isArray ( uploadResult . files ) ) {
570+ uploadedFiles . push ( ...uploadResult . files . map ( processUploadResult ) )
571+ } else if ( uploadResult . path || uploadResult . url ) {
572+ uploadedFiles . push ( processUploadResult ( uploadResult ) )
573+ }
546574 } else {
547- logger . error ( 'Unexpected upload response format:' , uploadResult )
548- }
549- } else {
550- const cloned = response . clone ( )
551- const errorData = await response . json ( ) . catch ( ( ) => null )
552- const reason =
553- errorData ?. message ||
554- errorData ?. error ||
555- ( await cloned . text ( ) . catch ( ( ) => '' ) ) ||
556- `${ response . status } `
557- const message = `Failed to upload ${ fileData . name } : ${ reason } `
558- logger . error ( message )
559- if ( isUploadErrorCapable ( workflowInput ) ) {
560- try {
561- workflowInput . onUploadError ( message )
562- } catch { }
575+ const message = `Failed to upload ${ fileData . name } : ${ toError ( uploadError ) . message } `
576+ logger . error ( message )
577+ if ( isUploadErrorCapable ( workflowInput ) ) {
578+ try {
579+ workflowInput . onUploadError ( message )
580+ } catch { }
581+ }
563582 }
564583 }
565584 }
566- // Update workflow input with uploaded files
567585 workflowInput . files = uploadedFiles
568586 } catch ( error ) {
569587 logger . error ( 'Error uploading files:' , error )
0 commit comments