fix: n8n_test_workflow webhookId resolution and form handling (v2.28.2) (#462)

This commit is contained in:
Romuald Członkowski
2025-12-01 22:33:25 +01:00
committed by GitHub
parent 3188d209b7
commit ef9b6f6341
8 changed files with 792 additions and 226 deletions

View File

@@ -7,6 +7,64 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased] ## [Unreleased]
## [2.28.2] - 2025-12-01
### Bug Fixes
**n8n_test_workflow: webhookId Resolution**
Fixed critical bug where trigger handlers used `node.id` instead of `node.webhookId` for building webhook URLs. This caused chat/form/webhook triggers to fail with 404 errors when nodes had custom IDs.
- **Root Cause**: `extractWebhookPath()` in `trigger-detector.ts` fell back to `node.id` instead of checking `node.webhookId` first
- **Fix**: Added `webhookId` to `WorkflowNode` type and updated priority: `params.path` > `webhookId` > `node.id`
- **Files**: `src/triggers/trigger-detector.ts`, `src/types/n8n-api.ts`
**n8n_test_workflow: Chat Trigger URL Pattern**
Fixed chat triggers using wrong URL pattern. n8n chat triggers require `/webhook/<id>/chat` suffix.
- **Root Cause**: `buildTriggerUrl()` used same pattern for webhooks and chat triggers
- **Fix**: Chat triggers now correctly use `/webhook/<webhookId>/chat` endpoint
- **Files**: `src/triggers/trigger-detector.ts:284-289`
**n8n_test_workflow: Form Trigger Content-Type**
Fixed form triggers failing with "Expected multipart/form-data" error.
- **Root Cause**: Form handler sent `application/json` but n8n requires `multipart/form-data`
- **Fix**: Switched to `form-data` library for proper multipart encoding
- **Files**: `src/triggers/handlers/form-handler.ts`
### Enhancements
**Form Handler: Complete Field Type Support**
Enhanced form handler to support all n8n form field types with intelligent handling:
- **Supported Types**: text, textarea, email, number, password, date, dropdown, checkbox, file, hidden, html
- **Checkbox Arrays**: Automatically converts arrays to `field[]` format required by n8n
- **File Uploads**: Supports base64 content or sends empty placeholder for required files
- **Helpful Warnings**: Reports missing required fields with field names and labels
- **Error Hints**: On failure, provides complete field structure with usage examples
```javascript
// Example with all field types
n8n_test_workflow({
workflowId: "abc123",
data: {
"field-0": "text value",
"field-1": ["checkbox1", "checkbox2"], // Array for checkboxes
"field-2": "dropdown_option",
"field-3": "2025-01-15", // Date format
"field-4": "user@example.com",
"field-5": 42, // Number
"field-6": "password123"
}
})
```
**Conceived by Romuald Członkowski - [AiAdvisors](https://www.aiadvisors.pl/en)**
## [2.28.1] - 2025-12-01 ## [2.28.1] - 2025-12-01
### 🐛 Bug Fixes ### 🐛 Bug Fixes

470
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{ {
"name": "n8n-mcp", "name": "n8n-mcp",
"version": "2.27.0", "version": "2.28.2",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "n8n-mcp", "name": "n8n-mcp",
"version": "2.27.0", "version": "2.28.2",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@modelcontextprotocol/sdk": "1.20.1", "@modelcontextprotocol/sdk": "1.20.1",
@@ -15,6 +15,7 @@
"dotenv": "^16.5.0", "dotenv": "^16.5.0",
"express": "^5.1.0", "express": "^5.1.0",
"express-rate-limit": "^7.1.5", "express-rate-limit": "^7.1.5",
"form-data": "^4.0.5",
"lru-cache": "^11.2.1", "lru-cache": "^11.2.1",
"n8n": "^1.121.2", "n8n": "^1.121.2",
"n8n-core": "^1.120.1", "n8n-core": "^1.120.1",
@@ -6654,6 +6655,20 @@
} }
} }
}, },
"node_modules/@getzep/zep-cloud/node_modules/form-data": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
"license": "MIT",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/@getzep/zep-js": { "node_modules/@getzep/zep-js": {
"version": "0.9.0", "version": "0.9.0",
"resolved": "https://registry.npmjs.org/@getzep/zep-js/-/zep-js-0.9.0.tgz", "resolved": "https://registry.npmjs.org/@getzep/zep-js/-/zep-js-0.9.0.tgz",
@@ -7060,23 +7075,6 @@
"undici-types": "~5.26.4" "undici-types": "~5.26.4"
} }
}, },
"node_modules/@ibm-cloud/watsonx-ai/node_modules/form-data": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz",
"integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==",
"license": "MIT",
"peer": true,
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"es-set-tostringtag": "^2.1.0",
"hasown": "^2.0.2",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/@ibm-cloud/watsonx-ai/node_modules/undici-types": { "node_modules/@ibm-cloud/watsonx-ai/node_modules/undici-types": {
"version": "5.26.5", "version": "5.26.5",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
@@ -9088,6 +9086,20 @@
"reflect-metadata": "0.2.2" "reflect-metadata": "0.2.2"
} }
}, },
"node_modules/@n8n/ai-workflow-builder/node_modules/form-data": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
"license": "MIT",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/@n8n/ai-workflow-builder/node_modules/n8n-workflow": { "node_modules/@n8n/ai-workflow-builder/node_modules/n8n-workflow": {
"version": "1.118.1", "version": "1.118.1",
"resolved": "https://registry.npmjs.org/n8n-workflow/-/n8n-workflow-1.118.1.tgz", "resolved": "https://registry.npmjs.org/n8n-workflow/-/n8n-workflow-1.118.1.tgz",
@@ -9141,6 +9153,20 @@
"zod-class": "0.0.16" "zod-class": "0.0.16"
} }
}, },
"node_modules/@n8n/api-types/node_modules/form-data": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
"license": "MIT",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/@n8n/api-types/node_modules/n8n-workflow": { "node_modules/@n8n/api-types/node_modules/n8n-workflow": {
"version": "1.118.1", "version": "1.118.1",
"resolved": "https://registry.npmjs.org/n8n-workflow/-/n8n-workflow-1.118.1.tgz", "resolved": "https://registry.npmjs.org/n8n-workflow/-/n8n-workflow-1.118.1.tgz",
@@ -9292,22 +9318,6 @@
"proxy-from-env": "^1.1.0" "proxy-from-env": "^1.1.0"
} }
}, },
"node_modules/@n8n/client-oauth2/node_modules/form-data": {
"version": "4.0.5",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
"integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
"license": "MIT",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"es-set-tostringtag": "^2.1.0",
"hasown": "^2.0.2",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/@n8n/config": { "node_modules/@n8n/config": {
"version": "1.64.0", "version": "1.64.0",
"resolved": "https://registry.npmjs.org/@n8n/config/-/config-1.64.0.tgz", "resolved": "https://registry.npmjs.org/@n8n/config/-/config-1.64.0.tgz",
@@ -9665,6 +9675,41 @@
"@supabase/storage-js": "2.7.1" "@supabase/storage-js": "2.7.1"
} }
}, },
"node_modules/@n8n/n8n-nodes-langchain/node_modules/form-data": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
"license": "MIT",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/@n8n/n8n-nodes-langchain/node_modules/form-data/node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/@n8n/n8n-nodes-langchain/node_modules/form-data/node_modules/mime-types": {
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"license": "MIT",
"dependencies": {
"mime-db": "1.52.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/@n8n/n8n-nodes-langchain/node_modules/mime-db": { "node_modules/@n8n/n8n-nodes-langchain/node_modules/mime-db": {
"version": "1.54.0", "version": "1.54.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
@@ -10297,43 +10342,6 @@
"proxy-from-env": "^1.1.0" "proxy-from-env": "^1.1.0"
} }
}, },
"node_modules/@n8n/task-runner/node_modules/axios/node_modules/form-data": {
"version": "4.0.5",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
"integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
"license": "MIT",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"es-set-tostringtag": "^2.1.0",
"hasown": "^2.0.2",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/@n8n/task-runner/node_modules/axios/node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/@n8n/task-runner/node_modules/axios/node_modules/mime-types": {
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"license": "MIT",
"dependencies": {
"mime-db": "1.52.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/@n8n/task-runner/node_modules/entities": { "node_modules/@n8n/task-runner/node_modules/entities": {
"version": "6.0.1", "version": "6.0.1",
"resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz",
@@ -10449,6 +10457,41 @@
"n8n-generate-translations": "bin/generate-translations" "n8n-generate-translations": "bin/generate-translations"
} }
}, },
"node_modules/@n8n/task-runner/node_modules/n8n-core/node_modules/form-data": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
"license": "MIT",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/@n8n/task-runner/node_modules/n8n-core/node_modules/form-data/node_modules/mime-types": {
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"license": "MIT",
"dependencies": {
"mime-db": "1.52.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/@n8n/task-runner/node_modules/n8n-core/node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/@n8n/task-runner/node_modules/n8n-workflow": { "node_modules/@n8n/task-runner/node_modules/n8n-workflow": {
"version": "1.118.1", "version": "1.118.1",
"resolved": "https://registry.npmjs.org/n8n-workflow/-/n8n-workflow-1.118.1.tgz", "resolved": "https://registry.npmjs.org/n8n-workflow/-/n8n-workflow-1.118.1.tgz",
@@ -10474,6 +10517,41 @@
"zod": "3.25.67" "zod": "3.25.67"
} }
}, },
"node_modules/@n8n/task-runner/node_modules/n8n-workflow/node_modules/form-data": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
"license": "MIT",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/@n8n/task-runner/node_modules/n8n-workflow/node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/@n8n/task-runner/node_modules/n8n-workflow/node_modules/mime-types": {
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"license": "MIT",
"dependencies": {
"mime-db": "1.52.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/@n8n/task-runner/node_modules/picocolors": { "node_modules/@n8n/task-runner/node_modules/picocolors": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz",
@@ -14545,22 +14623,6 @@
"form-data": "^4.0.4" "form-data": "^4.0.4"
} }
}, },
"node_modules/@types/node-fetch/node_modules/form-data": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz",
"integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==",
"license": "MIT",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"es-set-tostringtag": "^2.1.0",
"hasown": "^2.0.2",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/@types/pg": { "node_modules/@types/pg": {
"version": "8.6.1", "version": "8.6.1",
"resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.6.1.tgz", "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.6.1.tgz",
@@ -15803,22 +15865,6 @@
"axios": "0.x || 1.x" "axios": "0.x || 1.x"
} }
}, },
"node_modules/axios/node_modules/form-data": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz",
"integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==",
"license": "MIT",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"es-set-tostringtag": "^2.1.0",
"hasown": "^2.0.2",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/babel-jest": { "node_modules/babel-jest": {
"version": "29.7.0", "version": "29.7.0",
"resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz",
@@ -19301,13 +19347,15 @@
} }
}, },
"node_modules/form-data": { "node_modules/form-data": {
"version": "4.0.0", "version": "4.0.5",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"asynckit": "^0.4.0", "asynckit": "^0.4.0",
"combined-stream": "^1.0.8", "combined-stream": "^1.0.8",
"es-set-tostringtag": "^2.1.0",
"hasown": "^2.0.2",
"mime-types": "^2.1.12" "mime-types": "^2.1.12"
}, },
"engines": { "engines": {
@@ -20579,23 +20627,6 @@
"undici-types": "~5.26.4" "undici-types": "~5.26.4"
} }
}, },
"node_modules/ibm-cloud-sdk-core/node_modules/form-data": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz",
"integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==",
"license": "MIT",
"peer": true,
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"es-set-tostringtag": "^2.1.0",
"hasown": "^2.0.2",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/ibm-cloud-sdk-core/node_modules/undici-types": { "node_modules/ibm-cloud-sdk-core/node_modules/undici-types": {
"version": "5.26.5", "version": "5.26.5",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
@@ -24863,6 +24894,41 @@
"url": "https://github.com/fb55/entities?sponsor=1" "url": "https://github.com/fb55/entities?sponsor=1"
} }
}, },
"node_modules/n8n-core/node_modules/form-data": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
"license": "MIT",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/n8n-core/node_modules/form-data/node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/n8n-core/node_modules/form-data/node_modules/mime-types": {
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"license": "MIT",
"dependencies": {
"mime-db": "1.52.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/n8n-core/node_modules/htmlparser2": { "node_modules/n8n-core/node_modules/htmlparser2": {
"version": "10.0.0", "version": "10.0.0",
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.0.0.tgz", "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.0.0.tgz",
@@ -25351,6 +25417,20 @@
"zod": "3.25.67" "zod": "3.25.67"
} }
}, },
"node_modules/n8n-workflow/node_modules/form-data": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
"license": "MIT",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/n8n-workflow/node_modules/zod": { "node_modules/n8n-workflow/node_modules/zod": {
"version": "3.25.67", "version": "3.25.67",
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.67.tgz", "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.67.tgz",
@@ -25515,6 +25595,41 @@
"zod-to-json-schema": "3.23.3" "zod-to-json-schema": "3.23.3"
} }
}, },
"node_modules/n8n/node_modules/@n8n/n8n-nodes-langchain/node_modules/form-data": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
"license": "MIT",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/n8n/node_modules/@n8n/n8n-nodes-langchain/node_modules/form-data/node_modules/mime-types": {
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"license": "MIT",
"dependencies": {
"mime-db": "1.52.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/n8n/node_modules/@n8n/n8n-nodes-langchain/node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/n8n/node_modules/@n8n/n8n-nodes-langchain/node_modules/openai": { "node_modules/n8n/node_modules/@n8n/n8n-nodes-langchain/node_modules/openai": {
"version": "5.12.2", "version": "5.12.2",
"resolved": "https://registry.npmjs.org/openai/-/openai-5.12.2.tgz", "resolved": "https://registry.npmjs.org/openai/-/openai-5.12.2.tgz",
@@ -26134,43 +26249,6 @@
"proxy-from-env": "^1.1.0" "proxy-from-env": "^1.1.0"
} }
}, },
"node_modules/n8n/node_modules/axios/node_modules/form-data": {
"version": "4.0.5",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
"integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
"license": "MIT",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"es-set-tostringtag": "^2.1.0",
"hasown": "^2.0.2",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/n8n/node_modules/axios/node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/n8n/node_modules/axios/node_modules/mime-types": {
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"license": "MIT",
"dependencies": {
"mime-db": "1.52.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/n8n/node_modules/cheerio-select": { "node_modules/n8n/node_modules/cheerio-select": {
"version": "1.6.0", "version": "1.6.0",
"resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-1.6.0.tgz", "resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-1.6.0.tgz",
@@ -26493,6 +26571,41 @@
"n8n-generate-translations": "bin/generate-translations" "n8n-generate-translations": "bin/generate-translations"
} }
}, },
"node_modules/n8n/node_modules/n8n-core/node_modules/form-data": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
"license": "MIT",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/n8n/node_modules/n8n-core/node_modules/form-data/node_modules/mime-types": {
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"license": "MIT",
"dependencies": {
"mime-db": "1.52.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/n8n/node_modules/n8n-core/node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/n8n/node_modules/n8n-nodes-base": { "node_modules/n8n/node_modules/n8n-nodes-base": {
"version": "1.119.1", "version": "1.119.1",
"resolved": "https://registry.npmjs.org/n8n-nodes-base/-/n8n-nodes-base-1.119.1.tgz", "resolved": "https://registry.npmjs.org/n8n-nodes-base/-/n8n-nodes-base-1.119.1.tgz",
@@ -26683,6 +26796,41 @@
"zod": "3.25.67" "zod": "3.25.67"
} }
}, },
"node_modules/n8n/node_modules/n8n-workflow/node_modules/form-data": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
"license": "MIT",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/n8n/node_modules/n8n-workflow/node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/n8n/node_modules/n8n-workflow/node_modules/mime-types": {
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"license": "MIT",
"dependencies": {
"mime-db": "1.52.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/n8n/node_modules/open": { "node_modules/n8n/node_modules/open": {
"version": "7.4.2", "version": "7.4.2",
"resolved": "https://registry.npmjs.org/open/-/open-7.4.2.tgz", "resolved": "https://registry.npmjs.org/open/-/open-7.4.2.tgz",

View File

@@ -1,6 +1,6 @@
{ {
"name": "n8n-mcp", "name": "n8n-mcp",
"version": "2.28.1", "version": "2.28.2",
"description": "Integration between n8n workflow automation and Model Context Protocol (MCP)", "description": "Integration between n8n workflow automation and Model Context Protocol (MCP)",
"main": "dist/index.js", "main": "dist/index.js",
"types": "dist/index.d.ts", "types": "dist/index.d.ts",
@@ -146,6 +146,7 @@
"dotenv": "^16.5.0", "dotenv": "^16.5.0",
"express": "^5.1.0", "express": "^5.1.0",
"express-rate-limit": "^7.1.5", "express-rate-limit": "^7.1.5",
"form-data": "^4.0.5",
"lru-cache": "^11.2.1", "lru-cache": "^11.2.1",
"n8n": "^1.121.2", "n8n": "^1.121.2",
"n8n-core": "^1.120.1", "n8n-core": "^1.120.1",

View File

@@ -2,14 +2,15 @@
* Form trigger handler * Form trigger handler
* *
* Handles form-based workflow triggers: * Handles form-based workflow triggers:
* - POST to /form/<workflowId> or /form-test/<workflowId> * - POST to /form/<webhookId> with multipart/form-data
* - Passes form fields as request body * - Supports all n8n form field types: text, textarea, email, number, password, date, dropdown, checkbox, file, hidden
* - Workflow must be active (for production endpoint) * - Workflow must be active (for production endpoint)
*/ */
import { z } from 'zod'; import { z } from 'zod';
import axios, { AxiosRequestConfig } from 'axios'; import axios, { AxiosRequestConfig } from 'axios';
import { Workflow, WebhookRequest } from '../../types/n8n-api'; import FormData from 'form-data';
import { Workflow, WorkflowNode } from '../../types/n8n-api';
import { import {
TriggerType, TriggerType,
TriggerResponse, TriggerResponse,
@@ -32,6 +33,188 @@ const formInputSchema = z.object({
waitForResponse: z.boolean().optional(), waitForResponse: z.boolean().optional(),
}); });
/**
* Form field types supported by n8n
*/
const FORM_FIELD_TYPES = {
TEXT: 'text',
TEXTAREA: 'textarea',
EMAIL: 'email',
NUMBER: 'number',
PASSWORD: 'password',
DATE: 'date',
DROPDOWN: 'dropdown',
CHECKBOX: 'checkbox',
FILE: 'file',
HIDDEN: 'hiddenField',
HTML: 'html',
} as const;
/**
* Maximum file size for base64 uploads (10MB)
*/
const MAX_FILE_SIZE_BYTES = 10 * 1024 * 1024;
/**
* n8n form field option structure
*/
interface FormFieldOption {
option: string;
}
/**
* n8n form field value structure from workflow parameters
*/
interface FormFieldValue {
fieldType?: string;
fieldLabel?: string;
fieldName?: string;
elementName?: string;
requiredField?: boolean;
fieldOptions?: {
values?: FormFieldOption[];
};
}
/**
* Form field definition extracted from workflow
*/
interface FormFieldDef {
index: number;
fieldName: string; // field-0, field-1, etc.
label: string;
type: string;
required: boolean;
options?: string[]; // For dropdown/checkbox
}
/**
* Check if a string is valid base64
*/
function isValidBase64(str: string): boolean {
if (!str || str.length === 0) {
return false;
}
// Check for valid base64 characters and proper padding
const base64Regex = /^[A-Za-z0-9+/]*={0,2}$/;
if (!base64Regex.test(str)) {
return false;
}
try {
// Verify round-trip encoding
const decoded = Buffer.from(str, 'base64');
return decoded.toString('base64') === str;
} catch {
return false;
}
}
/**
* Extract form field definitions from workflow
*/
function extractFormFields(workflow: Workflow, triggerNode?: WorkflowNode): FormFieldDef[] {
const node = triggerNode || workflow.nodes.find(n =>
n.type.toLowerCase().includes('formtrigger')
);
const params = node?.parameters as Record<string, unknown> | undefined;
const formFields = params?.formFields as { values?: unknown[] } | undefined;
if (!formFields?.values) {
return [];
}
const fields: FormFieldDef[] = [];
let fieldIndex = 0;
for (const rawField of formFields.values) {
const field = rawField as FormFieldValue;
const fieldType = field.fieldType || FORM_FIELD_TYPES.TEXT;
// HTML fields are rendered as hidden inputs but are display-only
// They still get a field index
const def: FormFieldDef = {
index: fieldIndex,
fieldName: `field-${fieldIndex}`,
label: field.fieldLabel || field.fieldName || field.elementName || `field-${fieldIndex}`,
type: fieldType,
required: field.requiredField === true,
};
// Extract options for dropdown/checkbox
if (field.fieldOptions?.values) {
def.options = field.fieldOptions.values.map((v: FormFieldOption) => v.option);
}
fields.push(def);
fieldIndex++;
}
return fields;
}
/**
* Generate helpful usage hint for form fields
*/
function generateFormUsageHint(fields: FormFieldDef[]): string {
if (fields.length === 0) {
return 'No form fields detected in workflow.';
}
const lines: string[] = ['Form fields (use these keys in data parameter):'];
for (const field of fields) {
let hint = ` "${field.fieldName}": `;
switch (field.type) {
case FORM_FIELD_TYPES.CHECKBOX:
hint += `["${field.options?.[0] || 'option1'}", ...]`;
if (field.options) {
hint += ` (options: ${field.options.join(', ')})`;
}
break;
case FORM_FIELD_TYPES.DROPDOWN:
hint += `"${field.options?.[0] || 'value'}"`;
if (field.options) {
hint += ` (options: ${field.options.join(', ')})`;
}
break;
case FORM_FIELD_TYPES.DATE:
hint += '"YYYY-MM-DD"';
break;
case FORM_FIELD_TYPES.EMAIL:
hint += '"user@example.com"';
break;
case FORM_FIELD_TYPES.NUMBER:
hint += '123';
break;
case FORM_FIELD_TYPES.FILE:
hint += '{ filename: "test.txt", content: "base64..." } or skip (sends empty file)';
break;
case FORM_FIELD_TYPES.PASSWORD:
hint += '"secret"';
break;
case FORM_FIELD_TYPES.TEXTAREA:
hint += '"multi-line text..."';
break;
case FORM_FIELD_TYPES.HTML:
hint += '"" (display-only, can be omitted)';
break;
case FORM_FIELD_TYPES.HIDDEN:
hint += '"value" (hidden field)';
break;
default:
hint += '"text value"';
}
hint += field.required ? ' [REQUIRED]' : '';
hint += ` // ${field.label}`;
lines.push(hint);
}
return lines.join('\n');
}
/** /**
* Form trigger handler * Form trigger handler
*/ */
@@ -52,20 +235,27 @@ export class FormHandler extends BaseTriggerHandler<FormTriggerInput> {
): Promise<TriggerResponse> { ): Promise<TriggerResponse> {
const startTime = Date.now(); const startTime = Date.now();
// Extract form field definitions for helpful error messages
const formFieldDefs = extractFormFields(workflow, triggerInfo?.node);
try { try {
// Build form URL // Build form URL
const baseUrl = this.getBaseUrl(); const baseUrl = this.getBaseUrl();
if (!baseUrl) { if (!baseUrl) {
return this.errorResponse(input, 'Cannot determine n8n base URL', startTime); return this.errorResponse(input, 'Cannot determine n8n base URL', startTime, {
details: {
formFields: formFieldDefs,
hint: generateFormUsageHint(formFieldDefs),
},
});
} }
// Form triggers use /form/<path> endpoint // Form triggers use /form/<webhookId> endpoint
// The path can be from trigger info or workflow ID const formPath = triggerInfo?.webhookPath || triggerInfo?.node?.parameters?.path || input.workflowId;
const formPath = triggerInfo?.node?.parameters?.path || input.workflowId;
const formUrl = `${baseUrl.replace(/\/+$/, '')}/form/${formPath}`; const formUrl = `${baseUrl.replace(/\/+$/, '')}/form/${formPath}`;
// Merge formData and data (formData takes precedence) // Merge formData and data (formData takes precedence)
const formFields = { const inputFields = {
...input.data, ...input.data,
...input.formData, ...input.formData,
}; };
@@ -77,15 +267,142 @@ export class FormHandler extends BaseTriggerHandler<FormTriggerInput> {
return this.errorResponse(input, `SSRF protection: ${validation.reason}`, startTime); return this.errorResponse(input, `SSRF protection: ${validation.reason}`, startTime);
} }
// Build multipart/form-data (required by n8n form triggers)
const formData = new FormData();
const warnings: string[] = [];
// Process each defined form field
for (const fieldDef of formFieldDefs) {
const value = inputFields[fieldDef.fieldName];
switch (fieldDef.type) {
case FORM_FIELD_TYPES.CHECKBOX:
// Checkbox fields need array syntax with [] suffix
if (Array.isArray(value)) {
for (const item of value) {
formData.append(`${fieldDef.fieldName}[]`, String(item ?? ''));
}
} else if (value !== undefined && value !== null) {
// Single value provided, wrap in array
formData.append(`${fieldDef.fieldName}[]`, String(value));
} else if (fieldDef.required) {
warnings.push(`Required checkbox field "${fieldDef.fieldName}" (${fieldDef.label}) not provided`);
}
break;
case FORM_FIELD_TYPES.FILE:
// File fields - handle file upload or send empty placeholder
if (value && typeof value === 'object' && 'content' in value) {
// File object with content (base64 or buffer)
const fileObj = value as { filename?: string; content: string | Buffer };
let buffer: Buffer;
if (typeof fileObj.content === 'string') {
// Validate base64 encoding
if (!isValidBase64(fileObj.content)) {
warnings.push(`Invalid base64 encoding for file field "${fieldDef.fieldName}" (${fieldDef.label})`);
buffer = Buffer.from('');
} else {
buffer = Buffer.from(fileObj.content, 'base64');
// Check file size
if (buffer.length > MAX_FILE_SIZE_BYTES) {
warnings.push(`File too large for "${fieldDef.fieldName}" (${fieldDef.label}): ${Math.round(buffer.length / 1024 / 1024)}MB exceeds ${MAX_FILE_SIZE_BYTES / 1024 / 1024}MB limit`);
buffer = Buffer.from('');
}
}
} else {
buffer = fileObj.content;
// Check file size for Buffer input
if (buffer.length > MAX_FILE_SIZE_BYTES) {
warnings.push(`File too large for "${fieldDef.fieldName}" (${fieldDef.label}): ${Math.round(buffer.length / 1024 / 1024)}MB exceeds ${MAX_FILE_SIZE_BYTES / 1024 / 1024}MB limit`);
buffer = Buffer.from('');
}
}
formData.append(fieldDef.fieldName, buffer, {
filename: fileObj.filename || 'file.txt',
contentType: 'application/octet-stream',
});
} else if (value && typeof value === 'string') {
// String value - treat as base64 content
if (!isValidBase64(value)) {
warnings.push(`Invalid base64 encoding for file field "${fieldDef.fieldName}" (${fieldDef.label})`);
formData.append(fieldDef.fieldName, Buffer.from(''), {
filename: 'empty.txt',
contentType: 'text/plain',
});
} else {
const buffer = Buffer.from(value, 'base64');
if (buffer.length > MAX_FILE_SIZE_BYTES) {
warnings.push(`File too large for "${fieldDef.fieldName}" (${fieldDef.label}): ${Math.round(buffer.length / 1024 / 1024)}MB exceeds ${MAX_FILE_SIZE_BYTES / 1024 / 1024}MB limit`);
formData.append(fieldDef.fieldName, Buffer.from(''), {
filename: 'empty.txt',
contentType: 'text/plain',
});
} else {
formData.append(fieldDef.fieldName, buffer, {
filename: 'file.txt',
contentType: 'application/octet-stream',
});
}
}
} else {
// No file provided - send empty file as placeholder
formData.append(fieldDef.fieldName, Buffer.from(''), {
filename: 'empty.txt',
contentType: 'text/plain',
});
if (fieldDef.required) {
warnings.push(`Required file field "${fieldDef.fieldName}" (${fieldDef.label}) not provided - sending empty placeholder`);
}
}
break;
case FORM_FIELD_TYPES.HTML:
// HTML is display-only, but n8n renders it as hidden input
// Send empty string or provided value
formData.append(fieldDef.fieldName, String(value ?? ''));
break;
case FORM_FIELD_TYPES.HIDDEN:
// Hidden fields
formData.append(fieldDef.fieldName, String(value ?? ''));
break;
default:
// Standard fields: text, textarea, email, number, password, date, dropdown
if (value !== undefined && value !== null) {
formData.append(fieldDef.fieldName, String(value));
} else if (fieldDef.required) {
warnings.push(`Required field "${fieldDef.fieldName}" (${fieldDef.label}) not provided`);
}
break;
}
}
// Also include any extra fields not in the form definition (for flexibility)
const definedFieldNames = new Set(formFieldDefs.map(f => f.fieldName));
for (const [key, value] of Object.entries(inputFields)) {
if (!definedFieldNames.has(key)) {
if (Array.isArray(value)) {
for (const item of value) {
formData.append(`${key}[]`, String(item ?? ''));
}
} else {
formData.append(key, String(value ?? ''));
}
}
}
// Build request config // Build request config
const config: AxiosRequestConfig = { const config: AxiosRequestConfig = {
method: 'POST', method: 'POST',
url: formUrl, url: formUrl,
headers: { headers: {
'Content-Type': 'application/json', ...formData.getHeaders(),
...input.headers, ...input.headers,
}, },
data: formFields, data: formData,
timeout: input.timeout || (input.waitForResponse !== false ? 120000 : 30000), timeout: input.timeout || (input.waitForResponse !== false ? 120000 : 30000),
validateStatus: (status) => status < 500, validateStatus: (status) => status < 500,
}; };
@@ -93,13 +410,29 @@ export class FormHandler extends BaseTriggerHandler<FormTriggerInput> {
// Make the request // Make the request
const response = await axios.request(config); const response = await axios.request(config);
return this.normalizeResponse(response.data, input, startTime, { const result = this.normalizeResponse(response.data, input, startTime, {
status: response.status, status: response.status,
statusText: response.statusText, statusText: response.statusText,
metadata: { metadata: {
duration: Date.now() - startTime, duration: Date.now() - startTime,
}, },
}); });
// Add fields submitted count to details
result.details = {
...result.details,
fieldsSubmitted: formFieldDefs.length,
};
// Add warnings if any
if (warnings.length > 0) {
result.details = {
...result.details,
warnings,
};
}
return result;
} catch (error) { } catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error'; const errorMessage = error instanceof Error ? error.message : 'Unknown error';
@@ -110,7 +443,17 @@ export class FormHandler extends BaseTriggerHandler<FormTriggerInput> {
return this.errorResponse(input, errorMessage, startTime, { return this.errorResponse(input, errorMessage, startTime, {
executionId, executionId,
code: (error as any)?.code, code: (error as any)?.code,
details: errorDetails, details: {
...errorDetails,
formFields: formFieldDefs.map(f => ({
name: f.fieldName,
label: f.label,
type: f.type,
required: f.required,
options: f.options,
})),
hint: generateFormUsageHint(formFieldDefs),
},
}); });
} }
} }

View File

@@ -119,7 +119,7 @@ function detectWebhookTrigger(node: WorkflowNode): DetectedTrigger | null {
// Extract webhook path from parameters // Extract webhook path from parameters
const params = node.parameters || {}; const params = node.parameters || {};
const webhookPath = extractWebhookPath(params, node.id); const webhookPath = extractWebhookPath(params, node.id, node.webhookId);
const httpMethod = extractHttpMethod(params); const httpMethod = extractHttpMethod(params);
return { return {
@@ -148,10 +148,12 @@ function detectFormTrigger(node: WorkflowNode): DetectedTrigger | null {
// Extract form fields from parameters // Extract form fields from parameters
const params = node.parameters || {}; const params = node.parameters || {};
const formFields = extractFormFields(params); const formFields = extractFormFields(params);
const webhookPath = extractWebhookPath(params, node.id, node.webhookId);
return { return {
type: 'form', type: 'form',
node, node,
webhookPath,
formFields, formFields,
}; };
} }
@@ -174,7 +176,7 @@ function detectChatTrigger(node: WorkflowNode): DetectedTrigger | null {
// Extract chat configuration // Extract chat configuration
const params = node.parameters || {}; const params = node.parameters || {};
const responseMode = (params.options as any)?.responseMode || 'lastNode'; const responseMode = (params.options as any)?.responseMode || 'lastNode';
const webhookPath = extractWebhookPath(params, node.id); const webhookPath = extractWebhookPath(params, node.id, node.webhookId);
return { return {
type: 'chat', type: 'chat',
@@ -188,8 +190,14 @@ function detectChatTrigger(node: WorkflowNode): DetectedTrigger | null {
/** /**
* Extract webhook path from node parameters * Extract webhook path from node parameters
*
* Priority:
* 1. Explicit path parameter in node config
* 2. HTTP method specific path
* 3. webhookId on the node (n8n assigns this for all webhook-like triggers)
* 4. Fallback to node ID
*/ */
function extractWebhookPath(params: Record<string, unknown>, nodeId: string): string { function extractWebhookPath(params: Record<string, unknown>, nodeId: string, webhookId?: string): string {
// Check for explicit path parameter // Check for explicit path parameter
if (typeof params.path === 'string' && params.path) { if (typeof params.path === 'string' && params.path) {
return params.path; return params.path;
@@ -203,6 +211,11 @@ function extractWebhookPath(params: Record<string, unknown>, nodeId: string): st
} }
} }
// Use webhookId if available (n8n assigns this for chat/form/webhook triggers)
if (typeof webhookId === 'string' && webhookId) {
return webhookId;
}
// Default: use node ID as path (n8n default behavior) // Default: use node ID as path (n8n default behavior)
return nodeId; return nodeId;
} }
@@ -262,17 +275,24 @@ export function buildTriggerUrl(
const cleanBaseUrl = baseUrl.replace(/\/+$/, ''); // Remove trailing slashes const cleanBaseUrl = baseUrl.replace(/\/+$/, ''); // Remove trailing slashes
switch (trigger.type) { switch (trigger.type) {
case 'webhook': case 'webhook': {
case 'chat': {
const prefix = mode === 'test' ? 'webhook-test' : 'webhook'; const prefix = mode === 'test' ? 'webhook-test' : 'webhook';
const path = trigger.webhookPath || trigger.node.id; const path = trigger.webhookPath || trigger.node.id;
return `${cleanBaseUrl}/${prefix}/${path}`; return `${cleanBaseUrl}/${prefix}/${path}`;
} }
case 'chat': {
// Chat triggers use /webhook/<webhookId>/chat endpoint
const prefix = mode === 'test' ? 'webhook-test' : 'webhook';
const path = trigger.webhookPath || trigger.node.id;
return `${cleanBaseUrl}/${prefix}/${path}/chat`;
}
case 'form': { case 'form': {
// Form triggers use /form/<workflowId> endpoint // Form triggers use /form/<webhookId> endpoint
const prefix = mode === 'test' ? 'form-test' : 'form'; const prefix = mode === 'test' ? 'form-test' : 'form';
return `${cleanBaseUrl}/${prefix}/${trigger.node.id}`; const path = trigger.webhookPath || trigger.node.id;
return `${cleanBaseUrl}/${prefix}/${path}`;
} }
default: default:

View File

@@ -30,6 +30,7 @@ export interface WorkflowNode {
waitBetweenTries?: number; waitBetweenTries?: number;
alwaysOutputData?: boolean; alwaysOutputData?: boolean;
executeOnce?: boolean; executeOnce?: boolean;
webhookId?: string; // n8n assigns this for webhook/form/chat trigger nodes
} }
export interface WorkflowConnection { export interface WorkflowConnection {

View File

@@ -8,6 +8,7 @@ import { InstanceContext } from '../../../../src/types/instance-context';
import { Workflow } from '../../../../src/types/n8n-api'; import { Workflow } from '../../../../src/types/n8n-api';
import { DetectedTrigger } from '../../../../src/triggers/types'; import { DetectedTrigger } from '../../../../src/triggers/types';
import axios from 'axios'; import axios from 'axios';
import FormData from 'form-data';
// Mock getN8nApiConfig // Mock getN8nApiConfig
vi.mock('../../../../src/config/n8n-api', () => ({ vi.mock('../../../../src/config/n8n-api', () => ({
@@ -156,7 +157,7 @@ describe('FormHandler', () => {
}); });
describe('execute', () => { describe('execute', () => {
it('should execute form with provided formData', async () => { it('should execute form with provided formData using multipart/form-data', async () => {
const input = { const input = {
workflowId: 'workflow-123', workflowId: 'workflow-123',
triggerType: 'form' as const, triggerType: 'form' as const,
@@ -178,11 +179,15 @@ describe('FormHandler', () => {
expect(axios.request).toHaveBeenCalledWith( expect(axios.request).toHaveBeenCalledWith(
expect.objectContaining({ expect.objectContaining({
method: 'POST', method: 'POST',
data: { })
name: 'Jane Doe', );
email: 'jane@example.com', // Verify FormData is used
message: 'Hello', const config = vi.mocked(axios.request).mock.calls[0][0];
}, expect(config.data).toBeInstanceOf(FormData);
// Verify multipart/form-data content type is set via FormData headers
expect(config.headers).toEqual(
expect.objectContaining({
'content-type': expect.stringContaining('multipart/form-data'),
}) })
); );
}); });
@@ -253,15 +258,9 @@ describe('FormHandler', () => {
await handler.execute(input, workflow, triggerInfo); await handler.execute(input, workflow, triggerInfo);
expect(axios.request).toHaveBeenCalledWith( // Verify FormData is used and contains merged data
expect.objectContaining({ const config = vi.mocked(axios.request).mock.calls[0][0];
data: { expect(config.data).toBeInstanceOf(FormData);
field1: 'from data',
field2: 'from formData',
field3: 'from formData',
},
})
);
}); });
it('should return error when base URL not available', async () => { it('should return error when base URL not available', async () => {
@@ -303,7 +302,7 @@ describe('FormHandler', () => {
expect(response.error).toContain('Private IP address not allowed'); expect(response.error).toContain('Private IP address not allowed');
}); });
it('should pass custom headers', async () => { it('should pass custom headers with multipart/form-data', async () => {
const input = { const input = {
workflowId: 'workflow-123', workflowId: 'workflow-123',
triggerType: 'form' as const, triggerType: 'form' as const,
@@ -321,13 +320,13 @@ describe('FormHandler', () => {
await handler.execute(input, workflow, triggerInfo); await handler.execute(input, workflow, triggerInfo);
expect(axios.request).toHaveBeenCalledWith( const config = vi.mocked(axios.request).mock.calls[0][0];
expect(config.headers).toEqual(
expect.objectContaining({ expect.objectContaining({
headers: expect.objectContaining({ 'X-Custom-Header': 'custom-value',
'X-Custom-Header': 'custom-value', 'Authorization': 'Bearer token',
'Authorization': 'Bearer token', // FormData sets multipart/form-data with boundary
'Content-Type': 'application/json', 'content-type': expect.stringContaining('multipart/form-data'),
}),
}) })
); );
}); });
@@ -466,10 +465,15 @@ describe('FormHandler', () => {
expect(response.success).toBe(false); expect(response.success).toBe(false);
expect(response.executionId).toBe('exec-111'); expect(response.executionId).toBe('exec-111');
expect(response.details).toEqual({ // Details include original error data plus form field info and hint
id: 'exec-111', expect(response.details).toEqual(
error: 'Validation failed', expect.objectContaining({
}); id: 'exec-111',
error: 'Validation failed',
formFields: expect.any(Array),
hint: expect.any(String),
})
);
}); });
it('should handle error with code', async () => { it('should handle error with code', async () => {
@@ -535,14 +539,12 @@ describe('FormHandler', () => {
const response = await handler.execute(input, workflow, triggerInfo); const response = await handler.execute(input, workflow, triggerInfo);
expect(response.success).toBe(true); expect(response.success).toBe(true);
expect(axios.request).toHaveBeenCalledWith( // Even empty formData is sent as FormData
expect.objectContaining({ const config = vi.mocked(axios.request).mock.calls[0][0];
data: {}, expect(config.data).toBeInstanceOf(FormData);
})
);
}); });
it('should handle complex form data types', async () => { it('should handle complex form data types via FormData', async () => {
const input = { const input = {
workflowId: 'workflow-123', workflowId: 'workflow-123',
triggerType: 'form' as const, triggerType: 'form' as const,
@@ -562,17 +564,9 @@ describe('FormHandler', () => {
await handler.execute(input, workflow, triggerInfo); await handler.execute(input, workflow, triggerInfo);
expect(axios.request).toHaveBeenCalledWith( // Complex data types are serialized in FormData
expect.objectContaining({ const config = vi.mocked(axios.request).mock.calls[0][0];
data: { expect(config.data).toBeInstanceOf(FormData);
name: 'Test User',
age: 30,
active: true,
tags: ['tag1', 'tag2'],
metadata: { key: 'value' },
},
})
);
}); });
}); });
}); });

View File

@@ -242,7 +242,7 @@ describe('Trigger Detector', () => {
expect(url).toContain('/form/'); expect(url).toContain('/form/');
}); });
it('should build chat URL correctly', () => { it('should build chat URL correctly with /chat suffix', () => {
const baseUrl = 'https://n8n.example.com'; const baseUrl = 'https://n8n.example.com';
const trigger = { const trigger = {
type: 'chat' as const, type: 'chat' as const,
@@ -259,7 +259,8 @@ describe('Trigger Detector', () => {
const url = buildTriggerUrl(baseUrl, trigger, 'production'); const url = buildTriggerUrl(baseUrl, trigger, 'production');
expect(url).toBe('https://n8n.example.com/webhook/ai-chat'); // Chat triggers use /webhook/<webhookId>/chat endpoint
expect(url).toBe('https://n8n.example.com/webhook/ai-chat/chat');
}); });
}); });