diff --git a/sdks/nodejs-client/src/client/base.ts b/sdks/nodejs-client/src/client/base.ts index 3f465d2946e..7081100597b 100644 --- a/sdks/nodejs-client/src/client/base.ts +++ b/sdks/nodejs-client/src/client/base.ts @@ -26,8 +26,12 @@ const toConfig = (init: string | DifyClientConfig, baseUrl?: string): DifyClient return init } -const appendUserToFormData = (form: SdkFormData, user: string): void => { - form.append('user', user) +const setUserOnFormData = (form: SdkFormData, user: string): void => { + if ('set' in form && typeof form.set === 'function') { + form.set('user', user) + } else { + form.append('user', user) + } } export class DifyClient { @@ -178,7 +182,7 @@ export class DifyClient { throw new FileUploadError('FormData is required for file uploads') } ensureNonEmptyString(user, 'user') - appendUserToFormData(form, user) + setUserOnFormData(form, user) return this.http.request({ method: 'POST', path: '/files/upload', @@ -205,7 +209,7 @@ export class DifyClient { throw new FileUploadError('FormData is required for audio uploads') } ensureNonEmptyString(user, 'user') - appendUserToFormData(form, user) + setUserOnFormData(form, user) return this.http.request({ method: 'POST', path: '/audio-to-text', diff --git a/sdks/nodejs-client/tests/form-user.integration.test.ts b/sdks/nodejs-client/tests/form-user.integration.test.ts new file mode 100644 index 00000000000..e57315da531 --- /dev/null +++ b/sdks/nodejs-client/tests/form-user.integration.test.ts @@ -0,0 +1,88 @@ +import type { AddressInfo } from 'node:net' +import { createServer } from 'node:http' +import { afterAll, beforeAll, describe, expect, it } from 'vite-plus/test' +import { DifyClient } from '../src/client/base' + +describe('upload user fields', () => { + const server = createServer((req, res) => { + void (async () => { + const chunks: Buffer[] = [] + for await (const chunk of req as AsyncIterable) { + chunks.push(Buffer.from(chunk)) + } + const request = new Request('http://localhost', { + method: 'POST', + headers: { 'content-type': req.headers['content-type'] ?? '' }, + body: new Uint8Array(Buffer.concat(chunks)), + }) + const form = await request.formData() + const file = form.get('file') as File + res.writeHead(200, { 'content-type': 'application/json' }) + res.end( + JSON.stringify({ + users: form.getAll('user'), + fileName: file.name, + fileText: await file.text(), + extra: form.get('extra'), + }), + ) + })().catch(() => { + res.writeHead(500) + res.end() + }) + }) + + let client: DifyClient + beforeAll(async () => { + await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve)) + const address = server.address() as AddressInfo + client = new DifyClient({ + apiKey: 'test', + baseUrl: `http://127.0.0.1:${address.port}`, + maxRetries: 0, + }) + }) + afterAll(async () => { + await new Promise((resolve, reject) => { + server.close((error) => (error ? reject(error) : resolve())) + }) + }) + + const makeForm = () => { + const form = new FormData() + form.append('file', new Blob(['payload']), 'sample.txt') + form.append('extra', 'preserved') + return form + } + + it.each(['fileUpload', 'audioToText'] as const)( + '%s replaces existing user fields', + async (method) => { + const form = makeForm() + form.append('user', 'stale-user') + form.append('user', 'another-user') + + const response = await client[method](form, 'current-user') + + expect(response.data).toEqual({ + users: ['current-user'], + fileName: 'sample.txt', + fileText: 'payload', + extra: 'preserved', + }) + }, + ) + + it.each(['fileUpload', 'audioToText'] as const)( + '%s can reuse native FormData for a new user', + async (method) => { + const form = makeForm() + const first = await client[method](form, 'first-user') + expect(first.data.users).toEqual(['first-user']) + + const second = await client[method](form, 'second-user') + expect(second.data.users).toEqual(['second-user']) + expect(second.data.fileText).toBe('payload') + }, + ) +})