diff --git a/lib/js-api/src/fetchHttpClient.ts b/lib/js-api/src/fetchHttpClient.ts index 03488b45d1..1af869b34b 100644 --- a/lib/js-api/src/fetchHttpClient.ts +++ b/lib/js-api/src/fetchHttpClient.ts @@ -367,7 +367,12 @@ export class FetchHttpClient implements HttpClient { const normalizedParams = FetchHttpClient.normalizeParams(formParams); const formData = new FormData(); for (const [key, value] of Object.entries(normalizedParams)) { - formData.append(key, value as any); + const { data, filename } = FetchHttpClient.toFormDataValue(value); + if (filename) { + formData.append(key, data, filename); + } else { + formData.append(key, data); + } } return formData; } @@ -524,4 +529,24 @@ export class FetchHttpClient implements HttpClient { } return false; } + + private static toFormDataValue(value: any): { data: any; filename?: string } { + if (value && typeof value === 'object' && value.path && typeof value.path === 'string' && !(value instanceof Blob)) { + try { + const nodeFs = Function('return require("fs")')(); + const nodePath = Function('return require("path")')(); + const buffer = nodeFs.readFileSync(value.path); + const filename: string = nodePath.basename(value.path); + return { data: new Blob([buffer]), filename }; + } catch { + return { data: value }; + } + } + + if (typeof Buffer === 'function' && value instanceof Buffer) { + return { data: new Blob([value]) }; + } + + return { data: value }; + } } diff --git a/lib/js-api/test/fetchHttpClient.spec.ts b/lib/js-api/test/fetchHttpClient.spec.ts index d0e1779ab9..6692705bcd 100644 --- a/lib/js-api/test/fetchHttpClient.spec.ts +++ b/lib/js-api/test/fetchHttpClient.spec.ts @@ -19,6 +19,8 @@ import { FetchHttpClient } from '../src/fetchHttpClient'; import { EventEmitter } from 'eventemitter3'; import { getGlobalMockAgent, mockHost } from './mockObjects/base.mock'; +import * as fs from 'fs'; +import * as path from 'path'; describe('FetchHttpClient', () => { const host = 'https://127.0.0.1:8080'; @@ -1097,4 +1099,123 @@ describe('FetchHttpClient', () => { ]); }); }); + + describe('multipart form data upload', () => { + let originalFunction: typeof global.Function; + + beforeEach(() => { + originalFunction = global.Function; + global.Function = ((code: string) => { + if (code.includes('require')) { + return () => require(/"([^"]+)"/.exec(code)?.[1] || ''); + } + return originalFunction(code); + }) as FunctionConstructor; + }); + + afterEach(() => { + global.Function = originalFunction; + }); + + it('should send Blob when form param is a Blob', async () => { + mockHost(host).post('/api/upload').reply(200, { success: true }); + + const blob = new Blob(['content'], { type: 'text/plain' }); + const result = await client.post( + host + '/api/upload', + { + httpMethod: 'POST', + queryParams: {}, + headerParams: {}, + formParams: { file: blob }, + bodyParam: null, + contentType: 'multipart/form-data', + accept: 'application/json', + responseType: null, + returnType: null + }, + defaultSecurityOptions, + emitters + ); + + expect(result).toEqual({ success: true }); + }); + + it('should convert Buffer to Blob when form param is a Buffer', async () => { + mockHost(host).post('/api/upload').reply(200, { success: true }); + + const buffer = Buffer.from('file content'); + const result = await client.post( + host + '/api/upload', + { + httpMethod: 'POST', + queryParams: {}, + headerParams: {}, + formParams: { file: buffer }, + bodyParam: null, + contentType: 'multipart/form-data', + accept: 'application/json', + responseType: null, + returnType: null + }, + defaultSecurityOptions, + emitters + ); + + expect(result).toEqual({ success: true }); + }); + + it('should read file and send as Blob when form param has .path property', async () => { + mockHost(host).post('/api/upload').reply(200, { success: true }); + + const tmpFile = path.join(__dirname, '__test_upload__.txt'); + fs.writeFileSync(tmpFile, 'test content'); + + try { + const result = await client.post( + host + '/api/upload', + { + httpMethod: 'POST', + queryParams: {}, + headerParams: {}, + formParams: { file: { path: tmpFile } }, + bodyParam: null, + contentType: 'multipart/form-data', + accept: 'application/json', + responseType: null, + returnType: null + }, + defaultSecurityOptions, + emitters + ); + + expect(result).toEqual({ success: true }); + } finally { + fs.unlinkSync(tmpFile); + } + }); + + it('should pass through string values when form params are strings', async () => { + mockHost(host).post('/api/upload').reply(200, { success: true }); + + const result = await client.post( + host + '/api/upload', + { + httpMethod: 'POST', + queryParams: {}, + headerParams: {}, + formParams: { name: 'test-file', description: 'a test' }, + bodyParam: null, + contentType: 'multipart/form-data', + accept: 'application/json', + responseType: null, + returnType: null + }, + defaultSecurityOptions, + emitters + ); + + expect(result).toEqual({ success: true }); + }); + }); });