From c5a2d8f7d76788b64d05e0764351ba7eb0a6d504 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Tue, 14 Apr 2026 14:20:49 -0700 Subject: [PATCH 1/7] fix(google-drive): add auto export format and Azure storage debug logging --- .../api/tools/google_drive/download/route.ts | 78 +++++++++++++++---- apps/sim/blocks/blocks/google_drive.ts | 6 +- .../workspace/workspace-file-manager.ts | 15 ++++ apps/sim/lib/uploads/providers/blob/client.ts | 13 ++++ 4 files changed, 97 insertions(+), 15 deletions(-) diff --git a/apps/sim/app/api/tools/google_drive/download/route.ts b/apps/sim/app/api/tools/google_drive/download/route.ts index e4131423f91..0c8d7874b3c 100644 --- a/apps/sim/app/api/tools/google_drive/download/route.ts +++ b/apps/sim/app/api/tools/google_drive/download/route.ts @@ -65,10 +65,12 @@ export async function POST(request: NextRequest) { const { accessToken, fileId, - mimeType: exportMimeType, + mimeType: rawExportMimeType, fileName, includeRevisions, } = validatedData + const exportMimeType = + rawExportMimeType && rawExportMimeType !== 'auto' ? rawExportMimeType : null const authHeader = `Bearer ${accessToken}` logger.info(`[${requestId}] Getting file metadata from Google Drive`, { fileId }) @@ -129,7 +131,7 @@ export async function POST(request: NextRequest) { ) } - const exportResponse = await secureFetchWithPinnedIP( + let exportResponse = await secureFetchWithPinnedIP( exportUrl, exportUrlValidation.resolvedIP!, { headers: { Authorization: authHeader } } @@ -139,17 +141,67 @@ export async function POST(request: NextRequest) { const exportError = (await exportResponse .json() .catch(() => ({}))) as GoogleApiErrorResponse - logger.error(`[${requestId}] Failed to export file`, { - status: exportResponse.status, - error: exportError, - }) - return NextResponse.json( - { - success: false, - error: exportError.error?.message || 'Failed to export Google Workspace file', - }, - { status: 400 } - ) + const errorMessage = exportError.error?.message || '' + + const hasCustomFormat = !!exportMimeType + const defaultFormat = DEFAULT_EXPORT_FORMATS[fileMimeType] + if ( + hasCustomFormat && + defaultFormat && + exportMimeType !== defaultFormat && + errorMessage.toLowerCase().includes('conversion') + ) { + logger.warn(`[${requestId}] Export format not supported, falling back to default`, { + requestedFormat: exportFormat, + fallbackFormat: defaultFormat, + fileMimeType, + }) + + finalMimeType = defaultFormat + const fallbackUrl = `https://www.googleapis.com/drive/v3/files/${fileId}/export?mimeType=${encodeURIComponent(defaultFormat)}&supportsAllDrives=true` + const fallbackUrlValidation = await validateUrlWithDNS(fallbackUrl, 'fallbackExportUrl') + if (!fallbackUrlValidation.isValid) { + return NextResponse.json( + { success: false, error: fallbackUrlValidation.error }, + { status: 400 } + ) + } + + exportResponse = await secureFetchWithPinnedIP( + fallbackUrl, + fallbackUrlValidation.resolvedIP!, + { headers: { Authorization: authHeader } } + ) + + if (!exportResponse.ok) { + const fallbackError = (await exportResponse + .json() + .catch(() => ({}))) as GoogleApiErrorResponse + logger.error(`[${requestId}] Fallback export also failed`, { + status: exportResponse.status, + error: fallbackError, + }) + return NextResponse.json( + { + success: false, + error: fallbackError.error?.message || 'Failed to export Google Workspace file', + }, + { status: 400 } + ) + } + } else { + logger.error(`[${requestId}] Failed to export file`, { + status: exportResponse.status, + error: exportError, + }) + return NextResponse.json( + { + success: false, + error: errorMessage || 'Failed to export Google Workspace file', + }, + { status: 400 } + ) + } } const arrayBuffer = await exportResponse.arrayBuffer() diff --git a/apps/sim/blocks/blocks/google_drive.ts b/apps/sim/blocks/blocks/google_drive.ts index 79ab814e04e..f9737c145ba 100644 --- a/apps/sim/blocks/blocks/google_drive.ts +++ b/apps/sim/blocks/blocks/google_drive.ts @@ -316,6 +316,7 @@ Return ONLY the query string - no explanations, no quotes around the whole thing title: 'Export Format', type: 'dropdown', options: [ + { label: 'Auto (best format for file type)', id: 'auto' }, { label: 'Plain Text (text/plain)', id: 'text/plain' }, { label: 'HTML (text/html)', id: 'text/html' }, { label: 'PDF (application/pdf)', id: 'application/pdf' }, @@ -333,7 +334,8 @@ Return ONLY the query string - no explanations, no quotes around the whole thing }, { label: 'CSV (text/csv)', id: 'text/csv' }, ], - placeholder: 'Optional: Choose export format for Google Docs/Sheets/Slides', + value: 'auto', + placeholder: 'Export format for Google Docs/Sheets/Slides', condition: { field: 'operation', value: 'download' }, }, { @@ -867,7 +869,7 @@ Return ONLY the message text - no subject line, no greetings/signatures, no extr destinationFolderId: effectiveDestinationFolderId, file: normalizedFile, pageSize: rest.pageSize ? Number.parseInt(rest.pageSize as string, 10) : undefined, - mimeType: mimeType, + mimeType: mimeType === 'auto' ? undefined : mimeType, type: shareType, // Map shareType to type for share tool starred: starredValue, sendNotification: sendNotificationValue, diff --git a/apps/sim/lib/uploads/contexts/workspace/workspace-file-manager.ts b/apps/sim/lib/uploads/contexts/workspace/workspace-file-manager.ts index 6c3ea54b8b2..9e33e82679b 100644 --- a/apps/sim/lib/uploads/contexts/workspace/workspace-file-manager.ts +++ b/apps/sim/lib/uploads/contexts/workspace/workspace-file-manager.ts @@ -247,6 +247,21 @@ export async function uploadWorkspaceFile( continue } logger.error(`Failed to upload workspace file ${fileName}:`, error) + if ( + error instanceof Error && + 'code' in error && + (error as { code?: string }).code === 'AuthorizationFailure' + ) { + const { getStorageProvider, BLOB_CONFIG } = await import('@/lib/uploads/config') + logger.error('Azure storage authorization diagnosis', { + provider: getStorageProvider(), + accountName: BLOB_CONFIG.accountName || '(empty)', + containerName: BLOB_CONFIG.containerName || '(empty)', + hasAccountKey: !!BLOB_CONFIG.accountKey, + keyLength: BLOB_CONFIG.accountKey?.length || 0, + hasConnectionString: !!BLOB_CONFIG.connectionString, + }) + } throw new Error( `Failed to upload file: ${error instanceof Error ? error.message : 'Unknown error'}` ) diff --git a/apps/sim/lib/uploads/providers/blob/client.ts b/apps/sim/lib/uploads/providers/blob/client.ts index 7b863b7d6cf..9344783216e 100644 --- a/apps/sim/lib/uploads/providers/blob/client.ts +++ b/apps/sim/lib/uploads/providers/blob/client.ts @@ -72,8 +72,15 @@ export async function getBlobServiceClient(): Promise const { accountName, accountKey, connectionString } = BLOB_CONFIG if (connectionString) { + logger.info('Initializing Azure Blob client using connection string', { + accountName: accountName || '(from connection string)', + }) _blobServiceClient = BlobServiceClient.fromConnectionString(connectionString) } else if (accountName && accountKey) { + logger.info('Initializing Azure Blob client using account name and key', { + accountName, + keyLength: accountKey.length, + }) const sharedKeyCredential = new StorageSharedKeyCredential(accountName, accountKey) _blobServiceClient = new BlobServiceClient( `https://${accountName}.blob.core.windows.net`, @@ -131,6 +138,12 @@ export async function uploadToBlob( const uniqueKey = shouldPreserveKey ? fileName : `${Date.now()}-${safeFileName}` const blobServiceClient = await getBlobServiceClient() + logger.info('Uploading to Azure Blob Storage', { + containerName: config.containerName, + key: uniqueKey, + contentType, + fileSize, + }) const containerClient = blobServiceClient.getContainerClient(config.containerName) const blockBlobClient = containerClient.getBlockBlobClient(uniqueKey) From 108d4e09f642408cf410ba7e514ea30388756219 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Tue, 14 Apr 2026 14:28:03 -0700 Subject: [PATCH 2/7] chore: remove Azure storage debug logging --- .../contexts/workspace/workspace-file-manager.ts | 15 --------------- apps/sim/lib/uploads/providers/blob/client.ts | 13 ------------- 2 files changed, 28 deletions(-) diff --git a/apps/sim/lib/uploads/contexts/workspace/workspace-file-manager.ts b/apps/sim/lib/uploads/contexts/workspace/workspace-file-manager.ts index 9e33e82679b..6c3ea54b8b2 100644 --- a/apps/sim/lib/uploads/contexts/workspace/workspace-file-manager.ts +++ b/apps/sim/lib/uploads/contexts/workspace/workspace-file-manager.ts @@ -247,21 +247,6 @@ export async function uploadWorkspaceFile( continue } logger.error(`Failed to upload workspace file ${fileName}:`, error) - if ( - error instanceof Error && - 'code' in error && - (error as { code?: string }).code === 'AuthorizationFailure' - ) { - const { getStorageProvider, BLOB_CONFIG } = await import('@/lib/uploads/config') - logger.error('Azure storage authorization diagnosis', { - provider: getStorageProvider(), - accountName: BLOB_CONFIG.accountName || '(empty)', - containerName: BLOB_CONFIG.containerName || '(empty)', - hasAccountKey: !!BLOB_CONFIG.accountKey, - keyLength: BLOB_CONFIG.accountKey?.length || 0, - hasConnectionString: !!BLOB_CONFIG.connectionString, - }) - } throw new Error( `Failed to upload file: ${error instanceof Error ? error.message : 'Unknown error'}` ) diff --git a/apps/sim/lib/uploads/providers/blob/client.ts b/apps/sim/lib/uploads/providers/blob/client.ts index 9344783216e..7b863b7d6cf 100644 --- a/apps/sim/lib/uploads/providers/blob/client.ts +++ b/apps/sim/lib/uploads/providers/blob/client.ts @@ -72,15 +72,8 @@ export async function getBlobServiceClient(): Promise const { accountName, accountKey, connectionString } = BLOB_CONFIG if (connectionString) { - logger.info('Initializing Azure Blob client using connection string', { - accountName: accountName || '(from connection string)', - }) _blobServiceClient = BlobServiceClient.fromConnectionString(connectionString) } else if (accountName && accountKey) { - logger.info('Initializing Azure Blob client using account name and key', { - accountName, - keyLength: accountKey.length, - }) const sharedKeyCredential = new StorageSharedKeyCredential(accountName, accountKey) _blobServiceClient = new BlobServiceClient( `https://${accountName}.blob.core.windows.net`, @@ -138,12 +131,6 @@ export async function uploadToBlob( const uniqueKey = shouldPreserveKey ? fileName : `${Date.now()}-${safeFileName}` const blobServiceClient = await getBlobServiceClient() - logger.info('Uploading to Azure Blob Storage', { - containerName: config.containerName, - key: uniqueKey, - contentType, - fileSize, - }) const containerClient = blobServiceClient.getContainerClient(config.containerName) const blockBlobClient = containerClient.getBlockBlobClient(uniqueKey) From 40f3b455bef5e37bc251803d5b718ba293dfada7 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Tue, 14 Apr 2026 14:31:16 -0700 Subject: [PATCH 3/7] fix(google-drive): use status-based fallback instead of string matching for export errors --- .../app/api/tools/google_drive/download/route.ts | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/apps/sim/app/api/tools/google_drive/download/route.ts b/apps/sim/app/api/tools/google_drive/download/route.ts index 0c8d7874b3c..b64fff5f30b 100644 --- a/apps/sim/app/api/tools/google_drive/download/route.ts +++ b/apps/sim/app/api/tools/google_drive/download/route.ts @@ -143,18 +143,15 @@ export async function POST(request: NextRequest) { .catch(() => ({}))) as GoogleApiErrorResponse const errorMessage = exportError.error?.message || '' - const hasCustomFormat = !!exportMimeType const defaultFormat = DEFAULT_EXPORT_FORMATS[fileMimeType] - if ( - hasCustomFormat && - defaultFormat && - exportMimeType !== defaultFormat && - errorMessage.toLowerCase().includes('conversion') - ) { - logger.warn(`[${requestId}] Export format not supported, falling back to default`, { + const canFallback = !!exportMimeType && !!defaultFormat && exportMimeType !== defaultFormat + + if (canFallback) { + logger.warn(`[${requestId}] Export failed with custom format, falling back to default`, { requestedFormat: exportFormat, fallbackFormat: defaultFormat, fileMimeType, + status: exportResponse.status, }) finalMimeType = defaultFormat From 762a9d9c5ca628eff828a7ad5799924672325538 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Tue, 14 Apr 2026 14:34:58 -0700 Subject: [PATCH 4/7] fix(google-drive): validate export formats against Drive API docs, remove fallback --- .../api/tools/google_drive/download/route.ts | 90 +++++++------------ apps/sim/tools/google_drive/utils.ts | 45 +++++++++- 2 files changed, 74 insertions(+), 61 deletions(-) diff --git a/apps/sim/app/api/tools/google_drive/download/route.ts b/apps/sim/app/api/tools/google_drive/download/route.ts index b64fff5f30b..e2733e73abc 100644 --- a/apps/sim/app/api/tools/google_drive/download/route.ts +++ b/apps/sim/app/api/tools/google_drive/download/route.ts @@ -13,6 +13,7 @@ import { ALL_REVISION_FIELDS, DEFAULT_EXPORT_FORMATS, GOOGLE_WORKSPACE_MIME_TYPES, + VALID_EXPORT_FORMATS, } from '@/tools/google_drive/utils' export const dynamic = 'force-dynamic' @@ -114,6 +115,24 @@ export async function POST(request: NextRequest) { if (GOOGLE_WORKSPACE_MIME_TYPES.includes(fileMimeType)) { const exportFormat = exportMimeType || DEFAULT_EXPORT_FORMATS[fileMimeType] || 'text/plain' + + const validFormats = VALID_EXPORT_FORMATS[fileMimeType] + if (validFormats && !validFormats.includes(exportFormat)) { + logger.warn(`[${requestId}] Unsupported export format requested`, { + fileId, + fileMimeType, + requestedFormat: exportFormat, + validFormats, + }) + return NextResponse.json( + { + success: false, + error: `Export format "${exportFormat}" is not supported for this file type. Supported formats: ${validFormats.join(', ')}`, + }, + { status: 400 } + ) + } + finalMimeType = exportFormat logger.info(`[${requestId}] Exporting Google Workspace file`, { @@ -131,7 +150,7 @@ export async function POST(request: NextRequest) { ) } - let exportResponse = await secureFetchWithPinnedIP( + const exportResponse = await secureFetchWithPinnedIP( exportUrl, exportUrlValidation.resolvedIP!, { headers: { Authorization: authHeader } } @@ -141,64 +160,17 @@ export async function POST(request: NextRequest) { const exportError = (await exportResponse .json() .catch(() => ({}))) as GoogleApiErrorResponse - const errorMessage = exportError.error?.message || '' - - const defaultFormat = DEFAULT_EXPORT_FORMATS[fileMimeType] - const canFallback = !!exportMimeType && !!defaultFormat && exportMimeType !== defaultFormat - - if (canFallback) { - logger.warn(`[${requestId}] Export failed with custom format, falling back to default`, { - requestedFormat: exportFormat, - fallbackFormat: defaultFormat, - fileMimeType, - status: exportResponse.status, - }) - - finalMimeType = defaultFormat - const fallbackUrl = `https://www.googleapis.com/drive/v3/files/${fileId}/export?mimeType=${encodeURIComponent(defaultFormat)}&supportsAllDrives=true` - const fallbackUrlValidation = await validateUrlWithDNS(fallbackUrl, 'fallbackExportUrl') - if (!fallbackUrlValidation.isValid) { - return NextResponse.json( - { success: false, error: fallbackUrlValidation.error }, - { status: 400 } - ) - } - - exportResponse = await secureFetchWithPinnedIP( - fallbackUrl, - fallbackUrlValidation.resolvedIP!, - { headers: { Authorization: authHeader } } - ) - - if (!exportResponse.ok) { - const fallbackError = (await exportResponse - .json() - .catch(() => ({}))) as GoogleApiErrorResponse - logger.error(`[${requestId}] Fallback export also failed`, { - status: exportResponse.status, - error: fallbackError, - }) - return NextResponse.json( - { - success: false, - error: fallbackError.error?.message || 'Failed to export Google Workspace file', - }, - { status: 400 } - ) - } - } else { - logger.error(`[${requestId}] Failed to export file`, { - status: exportResponse.status, - error: exportError, - }) - return NextResponse.json( - { - success: false, - error: errorMessage || 'Failed to export Google Workspace file', - }, - { status: 400 } - ) - } + logger.error(`[${requestId}] Failed to export file`, { + status: exportResponse.status, + error: exportError, + }) + return NextResponse.json( + { + success: false, + error: exportError.error?.message || 'Failed to export Google Workspace file', + }, + { status: 400 } + ) } const arrayBuffer = await exportResponse.arrayBuffer() diff --git a/apps/sim/tools/google_drive/utils.ts b/apps/sim/tools/google_drive/utils.ts index e58b27eb989..534e7e3f458 100644 --- a/apps/sim/tools/google_drive/utils.ts +++ b/apps/sim/tools/google_drive/utils.ts @@ -111,8 +111,49 @@ export const DEFAULT_EXPORT_FORMATS: Record = { 'application/vnd.google-apps.spreadsheet': 'text/csv', 'application/vnd.google-apps.presentation': 'text/plain', 'application/vnd.google-apps.drawing': 'image/png', - 'application/vnd.google-apps.form': 'application/pdf', - 'application/vnd.google-apps.script': 'application/json', + 'application/vnd.google-apps.form': 'application/zip', + 'application/vnd.google-apps.script': 'application/vnd.google-apps.script+json', +} + +/** + * Valid export formats per Google Workspace file type. + * See: https://developers.google.com/drive/api/guides/ref-export-formats + */ +export const VALID_EXPORT_FORMATS: Record = { + 'application/vnd.google-apps.document': [ + 'text/plain', + 'text/html', + 'application/pdf', + 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + 'application/vnd.oasis.opendocument.text', + 'application/rtf', + 'application/epub+zip', + ], + 'application/vnd.google-apps.spreadsheet': [ + 'text/csv', + 'text/tab-separated-values', + 'application/pdf', + 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + 'application/x-vnd.oasis.opendocument.spreadsheet', + 'application/zip', + ], + 'application/vnd.google-apps.presentation': [ + 'text/plain', + 'application/pdf', + 'application/vnd.openxmlformats-officedocument.presentationml.presentation', + 'application/vnd.oasis.opendocument.presentation', + 'image/jpeg', + 'image/png', + 'image/svg+xml', + ], + 'application/vnd.google-apps.drawing': [ + 'application/pdf', + 'image/jpeg', + 'image/png', + 'image/svg+xml', + ], + 'application/vnd.google-apps.form': ['application/zip'], + 'application/vnd.google-apps.script': ['application/vnd.google-apps.script+json'], } export const SOURCE_MIME_TYPES: Record = { From 6b35f4c23c3320aeeb5c57d88765b50edd5ac3cd Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Tue, 14 Apr 2026 14:44:41 -0700 Subject: [PATCH 5/7] fix(google-drive): use value function for dropdown default --- apps/sim/blocks/blocks/google_drive.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/sim/blocks/blocks/google_drive.ts b/apps/sim/blocks/blocks/google_drive.ts index f9737c145ba..6feff2d80f9 100644 --- a/apps/sim/blocks/blocks/google_drive.ts +++ b/apps/sim/blocks/blocks/google_drive.ts @@ -334,7 +334,7 @@ Return ONLY the query string - no explanations, no quotes around the whole thing }, { label: 'CSV (text/csv)', id: 'text/csv' }, ], - value: 'auto', + value: () => 'auto', placeholder: 'Export format for Google Docs/Sheets/Slides', condition: { field: 'operation', value: 'download' }, }, From 291b2eefeff833afe8ea13dd2025f03c42a211f4 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Tue, 14 Apr 2026 14:54:37 -0700 Subject: [PATCH 6/7] fix(google-drive): add text/markdown to valid export formats for Google Docs --- apps/sim/tools/google_drive/utils.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/sim/tools/google_drive/utils.ts b/apps/sim/tools/google_drive/utils.ts index 534e7e3f458..1fa78d1bd84 100644 --- a/apps/sim/tools/google_drive/utils.ts +++ b/apps/sim/tools/google_drive/utils.ts @@ -128,6 +128,7 @@ export const VALID_EXPORT_FORMATS: Record = { 'application/vnd.oasis.opendocument.text', 'application/rtf', 'application/epub+zip', + 'text/markdown', ], 'application/vnd.google-apps.spreadsheet': [ 'text/csv', From 07f793415a8cde9af80dde113eef09dadd4ded99 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Tue, 14 Apr 2026 15:09:14 -0700 Subject: [PATCH 7/7] fix(google-drive): correct ODS MIME type for Sheets export format --- apps/sim/tools/google_drive/utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/sim/tools/google_drive/utils.ts b/apps/sim/tools/google_drive/utils.ts index 1fa78d1bd84..fd046cf1b83 100644 --- a/apps/sim/tools/google_drive/utils.ts +++ b/apps/sim/tools/google_drive/utils.ts @@ -135,7 +135,7 @@ export const VALID_EXPORT_FORMATS: Record = { 'text/tab-separated-values', 'application/pdf', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', - 'application/x-vnd.oasis.opendocument.spreadsheet', + 'application/vnd.oasis.opendocument.spreadsheet', 'application/zip', ], 'application/vnd.google-apps.presentation': [