diff --git a/packages/twenty-e2e-testing/lib/pom/loginPage.ts b/packages/twenty-e2e-testing/lib/pom/loginPage.ts index 7e87d79153..f690319975 100644 --- a/packages/twenty-e2e-testing/lib/pom/loginPage.ts +++ b/packages/twenty-e2e-testing/lib/pom/loginPage.ts @@ -105,6 +105,8 @@ export class LoginPage { } async typeEmail(email: string) { + // Wait for the email field to be visible before trying to interact + await this.emailField.waitFor({ state: 'visible' }); await expect(this.emailField).toBeVisible(); await this.emailField.fill(email); diff --git a/packages/twenty-e2e-testing/tests/workflow-creation.spec.ts b/packages/twenty-e2e-testing/tests/workflow-creation.spec.ts index 1f2fd6ec94..797a302d87 100644 --- a/packages/twenty-e2e-testing/tests/workflow-creation.spec.ts +++ b/packages/twenty-e2e-testing/tests/workflow-creation.spec.ts @@ -47,6 +47,8 @@ test('Create workflow', async ({ page }) => { .getByTestId('top-bar-title') .getByText(NEW_WORKFLOW_NAME); + // Wait for the name to be visible and not hidden + await workflowName.waitFor({ state: 'visible' }); await expect(workflowName).toBeVisible(); await expect(page).toHaveURL(`/object/workflow/${newWorkflowId}`); diff --git a/packages/twenty-server/src/engine/core-modules/file/file-upload/services/file-upload.service.ts b/packages/twenty-server/src/engine/core-modules/file/file-upload/services/file-upload.service.ts index 252c8deb9e..5b3e7df01a 100644 --- a/packages/twenty-server/src/engine/core-modules/file/file-upload/services/file-upload.service.ts +++ b/packages/twenty-server/src/engine/core-modules/file/file-upload/services/file-upload.service.ts @@ -120,10 +120,22 @@ export class FileUploadService { const type = await FileType.fromBuffer(buffer); + if (!type || !type.ext || !type.mime) { + throw new Error( + 'Unable to detect image type from buffer. The file may not be a valid image format.', + ); + } + + if (!type.mime.startsWith('image/')) { + throw new Error( + `Detected file type is not an image: ${type.mime}. Please provide a valid image URL.`, + ); + } + return await this.uploadImage({ file: buffer, - filename: `${v4()}.${type?.ext}`, - mimeType: type?.mime, + filename: `${v4()}.${type.ext}`, + mimeType: type.mime, fileFolder, workspaceId, }); diff --git a/packages/twenty-server/src/utils/image.ts b/packages/twenty-server/src/utils/image.ts index 8e98729aaa..36da75c005 100644 --- a/packages/twenty-server/src/utils/image.ts +++ b/packages/twenty-server/src/utils/image.ts @@ -1,4 +1,4 @@ -import { type Axios } from 'axios'; +import { type Axios, type AxiosError } from 'axios'; const cropRegex = /([w|h])([0-9]+)/; @@ -26,9 +26,68 @@ export const getImageBufferFromUrl = async ( url: string, axiosInstance: Axios, ): Promise => { - const response = await axiosInstance.get(url, { - responseType: 'arraybuffer', - }); + if (!url || typeof url !== 'string' || url.trim().length === 0) { + throw new Error('Invalid URL provided: URL must be a non-empty string'); + } - return Buffer.from(response.data, 'binary'); + try { + const response = await axiosInstance.get(url, { + responseType: 'arraybuffer', + validateStatus: (status) => status >= 200 && status < 300, + maxRedirects: 5, + timeout: 30000, + }); + + if (!response.data) { + throw new Error('Received empty response from image URL'); + } + + const bufferLength = Buffer.isBuffer(response.data) + ? response.data.length + : response.data.byteLength; + + if (bufferLength === 0) { + throw new Error('Received empty response from image URL'); + } + + const contentType = response.headers['content-type']; + + if (contentType && !contentType.startsWith('image/')) { + throw new Error( + `Invalid content type: expected image/*, got ${contentType}`, + ); + } + + return Buffer.from(response.data, 'binary'); + } catch (error) { + const axiosError = error as AxiosError; + const axiosResponse = axiosError.response; + + if (axiosResponse) { + throw new Error( + `Failed to fetch image: HTTP ${axiosResponse.status} from ${url}`, + ); + } + + if (error instanceof Error) { + if ( + axiosError.code === 'ECONNABORTED' || + error.message.includes('timeout') + ) { + throw new Error( + `Request timeout while fetching image from URL: ${url}`, + ); + } + if ( + axiosError.code === 'ENOTFOUND' || + axiosError.code === 'ECONNREFUSED' || + error.message.includes('ENOTFOUND') || + error.message.includes('ECONNREFUSED') + ) { + throw new Error(`Failed to connect to image URL: ${url}`); + } + throw new Error(`Failed to fetch image from URL: ${error.message}`); + } + throw new Error(`Failed to fetch image from URL: ${url}`); + } };