From e8f031da02ccb626d85d6539fcdd4c52dd3a5a2e Mon Sep 17 00:00:00 2001 From: Mohamed Marrouchi Date: Mon, 3 Feb 2025 18:20:38 +0100 Subject: [PATCH 1/3] fix: handle multer upload in middleware (long pooling only) --- .../channels/web/base-web-channel.ts | 75 ++++++++++--------- 1 file changed, 38 insertions(+), 37 deletions(-) diff --git a/api/src/extensions/channels/web/base-web-channel.ts b/api/src/extensions/channels/web/base-web-channel.ts index 800241071..096f10d42 100644 --- a/api/src/extensions/channels/web/base-web-channel.ts +++ b/api/src/extensions/channels/web/base-web-channel.ts @@ -8,7 +8,7 @@ import { BadRequestException, Injectable } from '@nestjs/common'; import { EventEmitter2, OnEvent } from '@nestjs/event-emitter'; -import { Request, Response } from 'express'; +import { NextFunction, Request, Response } from 'express'; import multer, { diskStorage, memoryStorage } from 'multer'; import { Socket } from 'socket.io'; import { v4 as uuidv4 } from 'uuid'; @@ -616,49 +616,19 @@ export default abstract class BaseWebChannelHandler< */ async handleWebUpload( req: Request, - res: Response, + _res: Response, ): Promise { try { - const upload = multer({ - limits: { - fileSize: config.parameters.maxUploadSize, - }, - storage: (() => { - if (config.parameters.storageMode === 'memory') { - return memoryStorage(); - } else { - return diskStorage({}); - } - })(), - }).single('file'); // 'file' is the field name in the form - - const multerUpload = new Promise( - (resolve, reject) => { - upload(req as Request, res as Response, async (err?: any) => { - if (err) { - this.logger.error('Unable to store uploaded file', err); - reject(new Error('Unable to upload file!')); - } - - if (req.file) { - resolve(req.file); - } - }); - }, - ); - - const file = await multerUpload; - // Check if any file is provided - if (!file) { + if (!req.file) { this.logger.debug('No files provided'); return null; } - return await this.attachmentService.store(file, { - name: file.originalname, - size: file.size, - type: file.mimetype, + return await this.attachmentService.store(req.file, { + name: req.file.originalname, + size: req.file.size, + type: req.file.mimetype, resourceRef: AttachmentResourceRef.MessageAttachment, access: AttachmentAccess.Private, createdByRef: AttachmentCreatedByRef.Subscriber, @@ -1330,4 +1300,35 @@ export default abstract class BaseWebChannelHandler< return !!message; } } + + /** + * Web channel middleware to handle multipart uploads (Long Pooling only) + * @param req Express Request + * @param res Express Response + * @param next Callback function + */ + async middleware(req: Request, res: Response, next: NextFunction) { + if ( + !this.isSocketRequest(req) && + req.headers['content-type']?.includes('multipart/form-data') + ) { + const upload = multer({ + limits: { + fileSize: config.parameters.maxUploadSize, + }, + storage: (() => { + if (config.parameters.storageMode === 'memory') { + return memoryStorage(); + } else { + return diskStorage({}); + } + })(), + }).single('file'); // 'file' is the field name in the form + + upload(req, res, next); + } else { + // Do nothing + next(); + } + } } From 8955ec38f05bb4e3a1373cfc52244c93466e6718 Mon Sep 17 00:00:00 2001 From: Mohamed Marrouchi Date: Mon, 3 Feb 2025 18:42:03 +0100 Subject: [PATCH 2/3] feat: handle plain text payloads --- .../channels/web/base-web-channel.ts | 57 ++++++++++++------- 1 file changed, 36 insertions(+), 21 deletions(-) diff --git a/api/src/extensions/channels/web/base-web-channel.ts b/api/src/extensions/channels/web/base-web-channel.ts index 096f10d42..c9c73243d 100644 --- a/api/src/extensions/channels/web/base-web-channel.ts +++ b/api/src/extensions/channels/web/base-web-channel.ts @@ -8,6 +8,7 @@ import { BadRequestException, Injectable } from '@nestjs/common'; import { EventEmitter2, OnEvent } from '@nestjs/event-emitter'; +import bodyParser from 'body-parser'; import { NextFunction, Request, Response } from 'express'; import multer, { diskStorage, memoryStorage } from 'multer'; import { Socket } from 'socket.io'; @@ -1302,33 +1303,47 @@ export default abstract class BaseWebChannelHandler< } /** - * Web channel middleware to handle multipart uploads (Long Pooling only) + * Web channel middleware + * * @param req Express Request * @param res Express Response * @param next Callback function */ async middleware(req: Request, res: Response, next: NextFunction) { - if ( - !this.isSocketRequest(req) && - req.headers['content-type']?.includes('multipart/form-data') - ) { - const upload = multer({ - limits: { - fileSize: config.parameters.maxUploadSize, - }, - storage: (() => { - if (config.parameters.storageMode === 'memory') { - return memoryStorage(); - } else { - return diskStorage({}); - } - })(), - }).single('file'); // 'file' is the field name in the form + if (!this.isSocketRequest(req)) { + if (req.headers['content-type']?.includes('multipart/form-data')) { + // Handle multipart uploads (Long Pooling only) + const upload = multer({ + limits: { + fileSize: config.parameters.maxUploadSize, + }, + storage: (() => { + if (config.parameters.storageMode === 'memory') { + return memoryStorage(); + } else { + return diskStorage({}); + } + })(), + }).single('file'); // 'file' is the field name in the form - upload(req, res, next); - } else { - // Do nothing - next(); + return upload(req, res, next); + } else if (req.headers['content-type']?.includes('text/plain')) { + // Handle plain text payloads as JSON (retro-compability) + const textParser = bodyParser.text({ type: 'text/plain' }); + + return textParser(req, res, () => { + try { + req.body = + typeof req.body === 'string' ? JSON.parse(req.body) : req.body; + next(); + } catch (err) { + next(err); + } + }); + } } + + // Do nothing + next(); } } From a0f2701e3e51c064e30fc5b4b5345b0b475f52fa Mon Sep 17 00:00:00 2001 From: Mohamed Marrouchi Date: Tue, 4 Feb 2025 17:54:34 +0100 Subject: [PATCH 3/3] refactor: multer upload middleware --- .../channels/web/base-web-channel.ts | 27 ++++++++++--------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/api/src/extensions/channels/web/base-web-channel.ts b/api/src/extensions/channels/web/base-web-channel.ts index c9c73243d..a20791814 100644 --- a/api/src/extensions/channels/web/base-web-channel.ts +++ b/api/src/extensions/channels/web/base-web-channel.ts @@ -64,6 +64,20 @@ import { WEB_CHANNEL_NAME, WEB_CHANNEL_NAMESPACE } from './settings'; import { Web } from './types'; import WebEventWrapper from './wrapper'; +// Handle multipart uploads (Long Pooling only) +const upload = multer({ + limits: { + fileSize: config.parameters.maxUploadSize, + }, + storage: (() => { + if (config.parameters.storageMode === 'memory') { + return memoryStorage(); + } else { + return diskStorage({}); + } + })(), +}).single('file'); // 'file' is the field name in the form + @Injectable() export default abstract class BaseWebChannelHandler< N extends ChannelName, @@ -1313,19 +1327,6 @@ export default abstract class BaseWebChannelHandler< if (!this.isSocketRequest(req)) { if (req.headers['content-type']?.includes('multipart/form-data')) { // Handle multipart uploads (Long Pooling only) - const upload = multer({ - limits: { - fileSize: config.parameters.maxUploadSize, - }, - storage: (() => { - if (config.parameters.storageMode === 'memory') { - return memoryStorage(); - } else { - return diskStorage({}); - } - })(), - }).single('file'); // 'file' is the field name in the form - return upload(req, res, next); } else if (req.headers['content-type']?.includes('text/plain')) { // Handle plain text payloads as JSON (retro-compability)