إرسال مصفوفة طويلة عشوائية من الملفات في طلب HTTP عبر node

وصف المشكلة/الخطأ/السؤال

أحتاج إلى تنفيذ طلب HTTP حيث يتوقع الخادم مني تقديم ملفات متعددة تحت نفس حقل البيانات. عدد الملفات ليس ثابتًا، لذا لا يمكنني ببساطة تعيين كل ملف إلى حقل في عقدة n8n. هل هذا ممكن حتى عبر عقدة HTTP أم يجب أن أستخدم عقدة الكود؟

ما هي رسالة الخطأ (إن وجدت)؟

حاولت بهذه الطريقة لكن من الواضح أنني إذا كان لدي أكثر من ملف واحد، أحصل على خطأ يفيد بأن الملف الثنائي برقم المعرف لا يوجد.

يرجى مشاركة سير العمل الخاص بك

معلومات حول إعداد n8n الخاص بك

  • إصدار n8n: 2.21.7
  • قاعدة البيانات (الافتراضي: SQLite): postgres
  • إعداد n8n EXECUTIONS_PROCESS (الافتراضي: own, main): own
  • تشغيل n8n عبر (Docker, npm, n8n cloud, desktop app): docker
  • نظام التشغيل: macOS

مرحباً @Vidomina

جرب هذا

{
  "nodes": [
    {
      "parameters": {},
      "id": "manual-trigger-id",
      "name": "When clicking 'Execute workflow'",
      "type": "n8n-nodes-base.manualTrigger",
      "typeVersion": 1,
      "position": [
        380,
        240
      ]
    },
    {
      "parameters": {
        "jsCode": "// تحاكي هذه العقدة إدخالك الحقيقي\n// تنشئ: ملف معاملات واحد، وملفي بيانات\n\nconst paramsPayload = {\n  projectId: [12345],\n  language: 'en',\n  filterNestedConcepts: true,\n  numberOfConcepts: 10,\n  numberOfTerms: 10,\n};\n\nconst paramsBinary = {\n  data: Buffer.from(JSON.stringify(paramsPayload), 'utf8').toString('base64'),\n  mimeType: 'application/json',\n  fileName: 'params.json',\n};\n\nconst file1Binary = {\n  data: Buffer.from('This is the content of file 1').toString('base64'),\n  mimeType: 'text/plain',\n  fileName: 'file1.txt',\n};\n\nconst file2Binary = {\n  data: Buffer.from('This is the content of file 2').toString('base64'),\n  mimeType: 'text/plain',\n  fileName: 'file2.txt',\n};\n\nreturn [{\n  json: { message: \"تم إنشاء بيانات وهمية\" },\n  binary: {\n    params: paramsBinary,\n    file_a: file1Binary, // سيتم تعيينها إلى "data" في multipart\n    file_b: file2Binary  // ستُعيّن أيضاً إلى "data" في multipart\n  }\n}];"
      },
      "id": "mock-data-id",
      "name": "Mock Data Generator",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        600,
        240
      ]
    },
    {
      "parameters": {
        "jsCode": "// --- الخيار 1: نهج MULTIPART اليدوي ---\n// تأخذ هذه النصيفة جميع الملفات الثنائية وتبني جسم multipart/form-data واحد\n\nconst items = $input.all();\nconst result = [];\n\nfor (const item of items) {\n  const boundary = `----n8nboundary${Math.random().toString(36).substring(2)}`;\n  const parts = [];\n  \n  // 1. أضف حقل نص 'aggregate'\n  parts.push(`--${boundary}`);\n  parts.push(`Content-Disposition: form-data; name=\"aggregate\"`);\n  parts.push('');\n  parts.push('true');\n\n  // 2. تعامل مع ملف 'params' (إذا كان موجوداً)\n  if (item.binary \\ item.binary.params) {\n    const params = item.binary.params;\n    parts.push(`--${boundary}`);\n    parts.push(`Content-Disposition: form-data; name=\"params\"; filename=\"${params.fileName || 'params.json'}\"`);\n    parts.push(`Content-Type: ${params.mimeType || 'application/json'}`);\n    parts.push(''); \n    parts.push(Buffer.from(params.data, 'base64').toString('binary'));\n  }\n\n  // 3. تعامل مع جميع الملفات الأخرى وعيّنها إلى حقل \"data\"\n  if (item.binary) {\n    // نستثني 'params' لأننا تعاملنا معها بالفعل\n    const keys = Object.keys(item.binary).filter(k => k !== 'params');\n    \n    for (const key of keys) {\n      const file = item.binary[key];\n      parts.push(`--${boundary}`);\n      // مهم: name=\"data\" مثبّت هنا بحيث تشترك جميع الملفات في نفس اسم الحقل\n      parts.push(`Content-Disposition: form-data; name=\"data\"; filename=\"${file.fileName || key}\"`);\n      parts.push(`Content-Type: ${file.mimeType || 'application/octet-stream'}`);\n      parts.push(''); \n      parts.push(Buffer.from(file.data, 'base64').toString('binary'));\n    }\n  }\n\n  // 4. أنهِ الحد\n  parts.push(`--${boundary}--`);\n  parts.push('');\n\n  // اجمع كل شيء باستخدام CRLF (\\r\\n) وفقاً لمواصفات HTTP\n  const finalBody = Buffer.from(parts.join('\\r\\n'), 'binary');\n\n  result.push({\n    json: item.json,\n    binary: {\n      multipartBody: {\n        data: finalBody.toString('base64'),\n        mimeType: `multipart/form-data; boundary=${boundary}`,\n        fileName: 'request.bin',\n      }\n    }\n  });\n}\n\nreturn result;"
      },
      "id": "multipart-constructor-id",
      "name": "Construct Multipart Body",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        820,
        240
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "http://example.org/project/tag",
        "sendBody": true,
        "contentType": "binary",
        "inputDataFieldName": "multipartBody",
        "options": {}
      },
      "id": "http-request-id",
      "name": "Async Tagging",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.3,
      "position": [
        1040,
        240
      ]
    }
  ],
  "connections": {
    "When clicking 'Execute workflow'": {
      "main": [
        [
          {
            "node": "Mock Data Generator",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Mock Data Generator": {
      "main": [
        [
          {
            "node": "Construct Multipart Body",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Construct Multipart Body": {
      "main": [
        [
          {
            "node": "Async Tagging",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}
  1. انسخ كتلة JSON أعلاه.
  2. افتح محرر n8n الخاص بك.
  3. اضغط على Ctrl+V (أو Cmd+V على Mac) لصق العقد مباشرة على الكانفاس.
  4. للاختبار: انقر على “Execute Workflow”.
  5. للاستخدام مع بياناتك الحقيقية:
    • احذف عقدة “Mock Data Generator”.
    • وصّل عقدة البيانات الفعلية بعقدة “Construct Multipart Body”.
    • تأكد من أن عقدة البيانات توفر ملفات ثنائية (سيعثر الكود تلقائياً على جميع المفاتيح الثنائية، باستثناء أي مفتاح باسم params).

مرحباً @kjooleng شكراً على الاقتراح!

واجهت مشكلتين: أولاً، طريقتك في إنشاء نص Multipart Body تعمل بشكل جيد للنصوص، لكن الملفات مثل pdf و docx يتم حجبها تماماً بهذه الطريقة.

قمت بتعديلها إلى:

{
"nodes": \[
{
"parameters": {},
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": \[
48,
32
\],
"id": "fb16aeab-57ba-485f-8c6e-7bc015bd1e28",
"name": "When clicking 'Execute workflow'"
},
{
"parameters": {
"jsCode": "const source = $('When Executed by Another Workflow').first();\\nconst sourceBinary = { ...(source.binary || {}) };\\nconst paramsPayload = {\\n  projectId: \[source.json.fields.projectID\],\\n  language: source.json.fields.language,\\n  filterNestedConcepts: source.json.fields.FilterNestedConcepts,\\n  numberOfConcepts: source.json.fields.LabelsPerPage,\\n  numberOfTerms: source.json.fields.LabelsPerPage,\\n};\\n\\nsourceBinary.params = {\\n  data: Buffer.from(JSON.stringify(paramsPayload), 'utf8').toString('base64'),\\n  mimeType: 'application/json',\\n  fileName: 'params.json',\\n  fileExtension: 'json',\\n};\\nreturn \[\\n  {\\n    binary: sourceBinary,\\n  },\\n\];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": \[
272,
32
\],
"id": "100a0875-9bce-4790-be6c-da7a31407523",
"name": "Prepare Extraction Input"
},
{
"parameters": {
"method": "POST",
"url": "https://service.127.0.0.1.nip.io/extractor/api/tag/async",
"authentication": "genericCredentialType",
"genericAuthType": "httpBasicAuth",
"sendBody": true,
"contentType": "binary",
"options": {
"response": {
"response": {
"fullResponse": true,
"neverError": true
}
}
}
},
"id": "ad21249d-27c4-4880-b90a-d747b712cf39",
"name": "TaggingSuggested",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.3,
"position": \[
688,
32
\],
"credentials": {
"httpBasicAuth": {
"id": "abCwyM6KmpPSdwff",
"name": "cred"
}
}
},
{
"parameters": {
"jsCode": "return (async () =\\ {\\n  const CRLF = '\\r\\n';\\n  const items = $input.all();\\n  const result = [ ];\\n\\n  for (let itemIndex = 0; itemIndex < items.length; itemIndex++) {\\n    const item = items\[itemIndex\];\\n    const boundary = `----n8n${Math.random().toString(36).slice(2, 11)}`;\\n    const chunks = [ ];\\n\\n    const pushText = (text) =\\ chunks.push(Buffer.from(text, 'utf8'));\\n\\n    pushText(`--${boundary}${CRLF}`);\\n    pushText(`Content-Disposition: form-data; name=\"aggregate\"${CRLF}${CRLF}`);\\n    pushText(`true${CRLF}`);\\n\\n    if (item.binary?.params) {\\n      const paramsMeta = item.binary.params;\\n      const paramsBuf = await this.helpers.getBinaryDataBuffer(itemIndex, 'params');\\n      const fileName = paramsMeta.fileName || 'params.json';\\n      const mimeType = paramsMeta.mimeType || 'application/json';\\n\\n      pushText(`--${boundary}${CRLF}`);\\n      pushText(`Content-Disposition: form-data; name=\"params\"; filename=\"${fileName}\"${CRLF}`);\\n      pushText(`Content-Type: ${mimeType}${CRLF}${CRLF}`);\\n      chunks.push(paramsBuf);\\n      pushText(CRLF);\\n    }\\n\\n    if (item.binary) {\\n      for (const key of Object.keys(item.binary).filter((k) =\\ k !== 'params')) {\\n        const fileMeta = item.binary\[key\];\\n        const fileBuf = await this.helpers.getBinaryDataBuffer(itemIndex, key);\\n        const fileName = fileMeta.fileName || key;\\n        const mimeType = fileMeta.mimeType || 'application/octet-stream';\\n\\n        pushText(`--${boundary}${CRLF}`);\\n        pushText(`Content-Disposition: form-data; name=\"data\"; filename=\"${fileName}\"${CRLF}`);\\n        pushText(`Content-Type: ${mimeType}${CRLF}${CRLF}`);\\n        chunks.push(fileBuf);\\n        pushText(CRLF);\\n      }\\n    }\\n\\n    pushText(`--${boundary}--${CRLF}`);\\n\\n    const body = Buffer.concat(chunks);\\n    const contentType = `multipart/form-data; boundary=${boundary}`;\\n\\n    result.push({\\n      json: {\\n        ...(item.json || {}),\\n        contentType,\\n      },\\n      binary: {\\n        multipartBody: await this.helpers.prepareBinaryData(\\n          body,\\n          'request.bin',\\n          contentType\\n        ),\\n      },\\n    });\\n  }\\n\\n  return result;\\n})();"
},
"id": "505a7281-fd86-41fa-8c28-1cc51187e8b7",
"name": "Construct Multipart Body1",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": \[
480,
32
\]
}
\],
"connections": {
"When clicking 'Execute workflow'": {
"main": \[
\[
{
"node": "Prepare Extraction Input",
"type": "main",
"index": 0
}
\]
\]
},
"Prepare Extraction Input": {
"main": \[
\[
{
"node": "Construct Multipart Body1",
"type": "main",
"index": 0
}
\]
\]
},
"Construct Multipart Body1": {
"main": \[
\[
{
"node": "TaggingSuggested",
"type": "main",
"index": 0
}
\]
\]
}
},
"pinData": {},
"meta": {
"instanceId": "982a83c4fa14b8860ea8391b8f766ad449d7431aac64126d0b9fc936877b58be"
}
}

المشكلة الثانية هي أنني عندما أرسل الطلب إلى الخدمة الآن أتلقى:
Content-Type is not supported

لم أبدأ بعد في تصحيح ذلك.

إليك ملف json المعدل

هذا يسمح لك بالتحقق من أن المنطق يعمل لكل من البيانات الوصفية (JSON) والمحتوى الثنائي (محاكاة ملفات PDF/Docs الخاصة بك) دون الحاجة إلى وجود الملفات الفعلية الخاصة بك.

شكراً على مولد البيانات الوهمية المعدل.
نظرت في سبب عودة الخدمة إلى
Content-Type is not supported ويبدو أن عقدة HTTP لا ترسل أي شيء.

حاولت ضبطها على n8n-binary + اسم الملف multipartBody لكنني حصلت على خطأ “source.on is not a function” - ربما أفسدت شيئاً ما في JavaScript لأنني لا أملك خبرة كبيرة مع وظائف n8n. سأبحث في التوثيق وأحاول معرفة ذلك، لكن إذا كنت أفتقد شيئاً واضحاً فأي مساهمة منك مرحب بها.

استخدم عقدة Code محددة وهذه إعدادات HTTP Request بالضبط

return (async () => {
  const CRLF = '\r\n';
  const items = $input.all();
  const result = [];

  for (let i = 0; i < items.length; i++) {
    const item = items[i];
    // 1. إنشاء حد فاصل فريد
    const boundary = `----n8nboundary${Math.random().toString(36).slice(2, 11)}`;
    const chunks = [];

    const pushText = (text) => chunks.push(Buffer.from(text, 'utf8'));

    // 2. أضف حقل "aggregate" يدويًا
    // بما أننا في وضع "Binary"، يجب أن يكون هذا في المخزن المؤقت
    pushText(`--${boundary}${CRLF}`);
    pushText(`Content-Disposition: form-data; name="aggregate"${CRLF}`);
    pushText(`${CRLF}true${CRLF}`);

    // 3. التعامل مع ملف "params"
    if (item.binary?.params) {
      const paramsMeta = item.binary.params;
      const paramsBuf = await this.helpers.getBinaryDataBuffer(i, 'params');
      const fileName = paramsMeta.fileName || 'params.json';
      const mimeType = paramsMeta.mimeType || 'application/json';

      pushText(`--${boundary}${CRLF}`);
      pushText(`Content-Disposition: form-data; name="params"; filename="${fileName}"${CRLF}`);
      pushText(`Content-Type: ${mimeType}${CRLF}${CRLF}`);
      chunks.push(paramsBuf);
      pushText(CRLF);
    }

    // 4. التعامل مع جميع الملفات الأخرى (مرسومة على "data")
    if (item.binary) {
      const dataKeys = Object.keys(item.binary).filter((k) => k !== 'params');
      for (const key of dataKeys) {
        const fileMeta = item.binary[key];
        const fileBuf = await this.helpers.getBinaryDataBuffer(i, key);
        const fileName = fileMeta.fileName || key;
        const mimeType = fileMeta.mimeType || 'application/octet-stream';

        pushText(`--${boundary}${CRLF}`);
        pushText(`Content-Disposition: form-data; name="data"; filename="${fileName}"${CRLF}`);
        pushText(`Content-Type: ${mimeType}${CRLF}${CRLF}`);
        chunks.push(fileBuf);
        pushText(CRLF);
      }
    }

    // 5. إغلاق الحد الفاصل
    pushText(`--${boundary}--${CRLF}`);

    const body = Buffer.concat(chunks);
    
    // رأس الجدول الدقيق الذي يحتاجه الخادم لرؤيته
    const contentTypeHeader = `multipart/form-data; boundary=${boundary}`;

    result.push({
      json: item.json,
      binary: {
        multipartBody: await this.helpers.prepareBinaryData(
          body,
          'request.bin',
          contentTypeHeader
        ),
      },
    });
  }

  return result;
})();

لا تضف أي شيء إلى “Body Parameters”. قم بتكوين طلب HTTP على النحو التالي:

  • الطريقة: POST
  • الرابط: your-api-endpoint
  • إرسال نص: true
  • نوع محتوى النص: Binaryهذا هو الجزء الأهم
  • اسم حقل بيانات الإدخال: multipartBody
  • الخيارات: (اتركها فارغة)

ما هي نسخة n8n التي تستخدمها؟ حاولت عدة نسخ ولم أتمكن من تشغيلها بشكل ثابت إلا على
1.123.65. للأسف عندما تمكنت من تشغيلها، حاول n8n في الواقع تحميل الجسم بإرسال كل جزء واحد كـ form-data منفصل:

Host: javeRequestCatcher.127.0.0.1.nip.io
Accept: application/json,text/html,application/xhtml+xml,application/xml,text/*;q=0.9, image/*;q=0.8, */*;q=0.7
Accept-Encoding: gzip, compress, deflate, br
Connection: close
Content-Length: 69898
Content-Type: multipart/form-data; boundary=--------------------------6c56103870e43e66b91b529d
User-Agent: axios/1.18.0

----------------------------6c56103870e43e66b91b529d
Content-Disposition: form-data; name="0"

45
----------------------------6c56103870e43e66b91b529d
Content-Disposition: form-data; name="1"

45
 
<skip a lot of lines>

13
----------------------------6c56103870e43e66b91b529d
Content-Disposition: form-data; name="668"

10
----------------------------6c56103870e43e66b91b529d--

في كل نسخة 2.xx التي حاولتها، حصلت فقط على خطأ source.on is not a function

هل سيساعد هذا؟

  1. احذف عقدة HTTP Request.

  2. ضع هذا الكود في عقدة Code.

const axios = require('axios');
const FormData = require('form-data');

const item = $input.first();
const form = new FormData();

// 1. أضف 'aggregate'
form.append('aggregate', 'true');

// 2. أضف 'params'
if (item.binary?.params) {
  const buf = await this.helpers.getBinaryDataBuffer(0, 'params');
  form.append('params', buf, { filename: 'params.json', contentType: 'application/json' });
}

// 3. أضف جميع الملفات الأخرى إلى 'data'
if (item.binary) {
  const keys = Object.keys(item.binary).filter(k => k !== 'params');
  for (const key of keys) {
    const buf = await this.helpers.getBinaryDataBuffer(0, key);
    const file = item.binary[key];
    form.append('data', buf, { filename: file.fileName || key, contentType: file.mimeType });
  }
}

// 4. نفذ الطلب مباشرة
try {
  const response = await axios.post('http://example.org/project/tag', form, {
    headers: form.getHeaders(),
  });
  return { json: response.data };
} catch (error) {
  return { json: { error: error.message } };
}
  1. انتقل إلى الإعدادات (أو متغيرات البيئة) وتأكد من أن NODE_FUNCTION_ALLOW_EXTERNAL تتضمن axios و form-data إذا كانت حالتك المحددة تقيدها (على الرغم من أنها عادة ما تكون مدمجة). إذا كانت n8n Cloud أو Docker القياسي، فإن axios مدمج.

كان ذلك يعمل بشكل رائع حتى اللحظة التي طلب فيها المقطع المصادقة - عُقد الكود المخصص لا تملك صلاحيات للعمل مع بيانات اعتماد محفوظة.

بعد جلسة عصف ذهني طويلة، انتهى بي الحال إلى إنشاء عقدة مخصصة. التطبيق الحالي سريع وخام، لكن إذا توفر لديّ الوقت سأرقيه إلى المعيار المطلوب وسأنشره كعقدة مجتمعية.

شكراً على هذا