From 034e3e85e98edabfdbae1dd7532a178dc6b35d01 Mon Sep 17 00:00:00 2001 From: yyh <92089059+lyzno1@users.noreply.github.com> Date: Mon, 24 Nov 2025 21:00:40 +0800 Subject: [PATCH] Fix Node.js SDK routes and multipart handling (#28573) --- sdks/nodejs-client/babel.config.cjs | 12 ++++ sdks/nodejs-client/index.js | 30 ++++------ sdks/nodejs-client/index.test.js | 88 +++++++++++++++++++++++++++-- sdks/nodejs-client/jest.config.cjs | 6 ++ sdks/nodejs-client/package.json | 5 -- 5 files changed, 111 insertions(+), 30 deletions(-) create mode 100644 sdks/nodejs-client/babel.config.cjs create mode 100644 sdks/nodejs-client/jest.config.cjs diff --git a/sdks/nodejs-client/babel.config.cjs b/sdks/nodejs-client/babel.config.cjs new file mode 100644 index 0000000000..392abb66d8 --- /dev/null +++ b/sdks/nodejs-client/babel.config.cjs @@ -0,0 +1,12 @@ +module.exports = { + presets: [ + [ + "@babel/preset-env", + { + targets: { + node: "current", + }, + }, + ], + ], +}; diff --git a/sdks/nodejs-client/index.js b/sdks/nodejs-client/index.js index 3025cc2ab6..9743ae358c 100644 --- a/sdks/nodejs-client/index.js +++ b/sdks/nodejs-client/index.js @@ -71,7 +71,7 @@ export const routes = { }, stopWorkflow: { method: "POST", - url: (task_id) => `/workflows/${task_id}/stop`, + url: (task_id) => `/workflows/tasks/${task_id}/stop`, } }; @@ -94,11 +94,13 @@ export class DifyClient { stream = false, headerParams = {} ) { + const isFormData = + (typeof FormData !== "undefined" && data instanceof FormData) || + (data && data.constructor && data.constructor.name === "FormData"); const headers = { - - Authorization: `Bearer ${this.apiKey}`, - "Content-Type": "application/json", - ...headerParams + Authorization: `Bearer ${this.apiKey}`, + ...(isFormData ? {} : { "Content-Type": "application/json" }), + ...headerParams, }; const url = `${this.baseUrl}${endpoint}`; @@ -152,12 +154,7 @@ export class DifyClient { return this.sendRequest( routes.fileUpload.method, routes.fileUpload.url(), - data, - null, - false, - { - "Content-Type": 'multipart/form-data' - } + data ); } @@ -179,8 +176,8 @@ export class DifyClient { getMeta(user) { const params = { user }; return this.sendRequest( - routes.meta.method, - routes.meta.url(), + routes.getMeta.method, + routes.getMeta.url(), null, params ); @@ -320,12 +317,7 @@ export class ChatClient extends DifyClient { return this.sendRequest( routes.audioToText.method, routes.audioToText.url(), - data, - null, - false, - { - "Content-Type": 'multipart/form-data' - } + data ); } diff --git a/sdks/nodejs-client/index.test.js b/sdks/nodejs-client/index.test.js index 1f5d6edb06..e3a1715238 100644 --- a/sdks/nodejs-client/index.test.js +++ b/sdks/nodejs-client/index.test.js @@ -1,9 +1,13 @@ -import { DifyClient, BASE_URL, routes } from "."; +import { DifyClient, WorkflowClient, BASE_URL, routes } from "."; import axios from 'axios' jest.mock('axios') +afterEach(() => { + jest.resetAllMocks() +}) + describe('Client', () => { let difyClient beforeEach(() => { @@ -27,13 +31,9 @@ describe('Send Requests', () => { difyClient = new DifyClient('test') }) - afterEach(() => { - jest.resetAllMocks() - }) - it('should make a successful request to the application parameter', async () => { const method = 'GET' - const endpoint = routes.application.url + const endpoint = routes.application.url() const expectedResponse = { data: 'response' } axios.mockResolvedValue(expectedResponse) @@ -62,4 +62,80 @@ describe('Send Requests', () => { errorMessage ) }) + + it('uses the getMeta route configuration', async () => { + axios.mockResolvedValue({ data: 'ok' }) + await difyClient.getMeta('end-user') + + expect(axios).toHaveBeenCalledWith({ + method: routes.getMeta.method, + url: `${BASE_URL}${routes.getMeta.url()}`, + params: { user: 'end-user' }, + headers: { + Authorization: `Bearer ${difyClient.apiKey}`, + 'Content-Type': 'application/json', + }, + responseType: 'json', + }) + }) +}) + +describe('File uploads', () => { + let difyClient + const OriginalFormData = global.FormData + + beforeAll(() => { + global.FormData = class FormDataMock {} + }) + + afterAll(() => { + global.FormData = OriginalFormData + }) + + beforeEach(() => { + difyClient = new DifyClient('test') + }) + + it('does not override multipart boundary headers for FormData', async () => { + const form = new FormData() + axios.mockResolvedValue({ data: 'ok' }) + + await difyClient.fileUpload(form) + + expect(axios).toHaveBeenCalledWith({ + method: routes.fileUpload.method, + url: `${BASE_URL}${routes.fileUpload.url()}`, + data: form, + params: null, + headers: { + Authorization: `Bearer ${difyClient.apiKey}`, + }, + responseType: 'json', + }) + }) +}) + +describe('Workflow client', () => { + let workflowClient + + beforeEach(() => { + workflowClient = new WorkflowClient('test') + }) + + it('uses tasks stop path for workflow stop', async () => { + axios.mockResolvedValue({ data: 'stopped' }) + await workflowClient.stop('task-1', 'end-user') + + expect(axios).toHaveBeenCalledWith({ + method: routes.stopWorkflow.method, + url: `${BASE_URL}${routes.stopWorkflow.url('task-1')}`, + data: { user: 'end-user' }, + params: null, + headers: { + Authorization: `Bearer ${workflowClient.apiKey}`, + 'Content-Type': 'application/json', + }, + responseType: 'json', + }) + }) }) diff --git a/sdks/nodejs-client/jest.config.cjs b/sdks/nodejs-client/jest.config.cjs new file mode 100644 index 0000000000..ea0fb34ad1 --- /dev/null +++ b/sdks/nodejs-client/jest.config.cjs @@ -0,0 +1,6 @@ +module.exports = { + testEnvironment: "node", + transform: { + "^.+\\.[tj]sx?$": "babel-jest", + }, +}; diff --git a/sdks/nodejs-client/package.json b/sdks/nodejs-client/package.json index cd3bcc4bce..c6bb0a9c1f 100644 --- a/sdks/nodejs-client/package.json +++ b/sdks/nodejs-client/package.json @@ -18,11 +18,6 @@ "scripts": { "test": "jest" }, - "jest": { - "transform": { - "^.+\\.[t|j]sx?$": "babel-jest" - } - }, "dependencies": { "axios": "^1.3.5" },