Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
103 changes: 103 additions & 0 deletions apps/sim/blocks/utils.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import { describe, expect, it } from 'vitest'
import { normalizeFileInput } from './utils'

describe('normalizeFileInput', () => {
describe('URL string handling', () => {
it('should return URL string as-is for single option', () => {
const url = 'https://example.com/photo.jpg'
const result = normalizeFileInput(url, { single: true })
expect(result).toBe(url)
})

it('should return URL in array for non-single option', () => {
const url = 'https://example.com/photo.jpg'
const result = normalizeFileInput(url, { single: false })
expect(result).toEqual([url])
})

it('should handle HTTP URLs', () => {
const url = 'http://example.com/photo.jpg'
const result = normalizeFileInput(url, { single: true })
expect(result).toBe(url)
})

it('should trim whitespace from URLs', () => {
const url = ' https://example.com/photo.jpg '
const result = normalizeFileInput(url, { single: true })
expect(result).toBe('https://example.com/photo.jpg')
})

it('should return undefined for non-URL strings that fail JSON parse', () => {
const notAUrl = 'just some text'
const result = normalizeFileInput(notAUrl, { single: true })
expect(result).toBeUndefined()
})
})

describe('JSON string handling', () => {
it('should parse JSON string to object for single option', () => {
const fileObj = { name: 'test.jpg', url: 'https://example.com/test.jpg' }
const result = normalizeFileInput(JSON.stringify(fileObj), { single: true })
expect(result).toEqual(fileObj)
})

it('should parse JSON string array for non-single option', () => {
const fileArray = [
{ name: 'test1.jpg', url: 'https://example.com/test1.jpg' },
{ name: 'test2.jpg', url: 'https://example.com/test2.jpg' },
]
const result = normalizeFileInput(JSON.stringify(fileArray), { single: false })
expect(result).toEqual(fileArray)
})
})

describe('Object and array handling', () => {
it('should return single object wrapped in array for non-single option', () => {
const fileObj = { name: 'test.jpg', url: 'https://example.com/test.jpg' }
const result = normalizeFileInput(fileObj, { single: false })
expect(result).toEqual([fileObj])
})

it('should return single object as-is for single option', () => {
const fileObj = { name: 'test.jpg', url: 'https://example.com/test.jpg' }
const result = normalizeFileInput(fileObj, { single: true })
expect(result).toEqual(fileObj)
})

it('should return array as-is for non-single option', () => {
const fileArray = [
{ name: 'test1.jpg', url: 'https://example.com/test1.jpg' },
{ name: 'test2.jpg', url: 'https://example.com/test2.jpg' },
]
const result = normalizeFileInput(fileArray, { single: false })
expect(result).toEqual(fileArray)
})
})

describe('Edge cases', () => {
it('should return undefined for null', () => {
const result = normalizeFileInput(null, { single: true })
expect(result).toBeUndefined()
})

it('should return undefined for undefined', () => {
const result = normalizeFileInput(undefined, { single: true })
expect(result).toBeUndefined()
})

it('should return undefined for empty string', () => {
const result = normalizeFileInput('', { single: true })
expect(result).toBeUndefined()
})

it('should throw error for multiple files when single is true', () => {
const fileArray = [
{ name: 'test1.jpg', url: 'https://example.com/test1.jpg' },
{ name: 'test2.jpg', url: 'https://example.com/test2.jpg' },
]
expect(() => normalizeFileInput(fileArray, { single: true })).toThrow(
'File reference must be a single file'
)
})
})
})
12 changes: 9 additions & 3 deletions apps/sim/blocks/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -326,18 +326,24 @@ const DEFAULT_MULTIPLE_FILES_ERROR =
export function normalizeFileInput(
fileParam: unknown,
options: { single: true; errorMessage?: string }
): object | undefined
): object | string | undefined
export function normalizeFileInput(
fileParam: unknown,
options?: { single?: false }
): object[] | undefined
): object[] | string | undefined
Comment on lines 330 to +333
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Non-single overload return type doesn't include string[]

When a URL is passed with single: false (or no options), the implementation returns [trimmed] — a string[]. However, the non-single overload declares object[] | string | undefined as its return type. TypeScript doesn't flag this because string[] is assignable to the implementation's object variant (arrays are objects), but it means callers receive a typed promise of object[] while actually getting string[] at runtime.

Any caller that iterates over the returned array and accesses file-object properties (.name, .url, etc.) would silently get undefined instead of failing loudly with a type error. The telegram_send_document path (normalizeFileInput(params.files)) is one such non-single caller — a URL string passed as params.files would return [url] typed as object[] | string | undefined, which could confuse downstream handlers.

The overload should reflect the actual runtime shape:

export function normalizeFileInput(
  fileParam: unknown,
  options?: { single?: false }
): (object | string)[] | string | undefined

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non-single overload return type doesn't match actual return

Medium Severity

When single is falsy, the URL path returns [trimmed] which is string[], but the non-single overload declares the return type as object[] | string | undefined. Since string is a primitive and not assignable to object, string[] is neither object[] nor string. A caller using this overload that checks typeof result === 'string' to detect a URL will never match (because it's actually an array), and a caller narrowing to object[] will get string elements incorrectly typed as object, potentially leading to runtime property-access errors on the array items.

Additional Locations (1)
Fix in Cursor Fix in Web

export function normalizeFileInput(
fileParam: unknown,
options?: { single?: boolean; errorMessage?: string }
): object | object[] | undefined {
): object | object[] | string | undefined {
if (!fileParam) return undefined

if (typeof fileParam === 'string') {
// Check if it's a plain URL (http/https) - return as-is for tools that accept URLs
const trimmed = fileParam.trim()
if (trimmed.startsWith('http://') || trimmed.startsWith('https://')) {
return options?.single ? trimmed : [trimmed]
}
Comment on lines +342 to +345
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 URL passthrough affects all normalizeFileInput callers, not just Telegram

This change is correct for Telegram media blocks, but normalizeFileInput is used by ~15 other blocks (Box, Confluence, Google Drive, Fireflies, Jira, Linear, etc.). Previously, passing a URL string to any of these would silently return undefined, causing a validation error. Now the URL string is returned to the caller.

For upload-oriented blocks (e.g. Box's upload_file, Confluence attachments), the caller assigns the result directly to params.file / baseParams.file and passes it to the tool handler. A URL string where those handlers expect a file object with { name, url, size } will likely cause a confusing downstream error instead of the current clear validation failure.

Consider scoping this change to Telegram-only, for example by adding an allowUrl?: boolean option to normalizeFileInput, so the URL shortcut doesn't silently change behaviour for other integrations:

export function normalizeFileInput(
  fileParam: unknown,
  options: { single: true; allowUrl?: boolean; errorMessage?: string }
): object | string | undefined


try {
fileParam = JSON.parse(fileParam)
} catch {
Expand Down