Senden eines beliebig langen Arrays von Dateien in einer HTTP-Request-Node

Beschreibe das Problem/Fehler/Frage

Ich muss eine HTTP-Anfrage ausführen, bei der der Server erwartet, dass ich mehrere Dateien unter demselben Feld bereitstelle. Die Anzahl der Dateien ist nicht statisch, daher kann ich jede Datei nicht einfach einem Feld im n8n-Knoten zuordnen. Ist das über den HTTP-Knoten überhaupt möglich oder sollte ich einen Code-Knoten verwenden?

Welche ist die Fehlermeldung (falls vorhanden)?

Ich habe es so versucht, aber offensichtlich erhalte ich einen Fehler, dass die Binärdatei mit der ID nicht existiert, wenn ich mehr als 1 Datei habe.

Bitte teile deinen Workflow

Informationen zu deinem n8n-Setup

  • n8n-Version: 2.21.7
  • Datenbank (Standard: SQLite): postgres
  • n8n EXECUTIONS_PROCESS-Einstellung (Standard: own, main): own
  • n8n wird ausgeführt über (Docker, npm, n8n cloud, Desktop-App): docker
  • Betriebssystem: macOS

Hi @Vidomina

Versuchen Sie das

  1. Kopieren Sie den obigen JSON-Block.
  2. Öffnen Sie Ihren n8n-Editor.
  3. Drücken Sie Strg+V (oder Cmd+V auf Mac), um die Knoten direkt auf die Canvas einzufügen.
  4. Zum Testen: Klicken Sie auf „Execute Workflow

Hallo @kjooleng, danke für den Vorschlag!

Ich bin auf 2 Probleme gestoßen: Zum einen funktioniert dein Construct Multipart Body gut für Text, aber Dateien wie PDF und DOCX werden durch diesen Ansatz absolut verstümmelt.

Ich habe es modifiziert zu:

{
"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"
}
}

Das zweite Problem ist, dass ich bei der Anfrage an den Service jetzt eine Fehlermeldung erhalte:
Content-Type is not supported

Ich habe das noch nicht zu debuggen angefangen.

Hier ist das geänderte JSON

Dies ermöglicht es dir zu überprüfen, ob die Logik sowohl für Metadaten (JSON) als auch für Binärinhalte (mit simulierten PDFs/Docs) funktioniert, ohne deine echten Dateien vorliegen zu haben.

Danke für den bearbeiteten Mock-Datengenerator.
Ich habe untersucht, warum der Dienst
Content-Type is not supported zurückgab, und es sieht so aus, als würde der HTTP-Knoten nichts senden.

Ich habe versucht, es auf n8n-binary + Feldname multipartBody zu setzen, aber ich bekam einen Fehler „source.on is not a function

Verwenden Sie diesen spezifischen Code Node und diese exakten HTTP-Request-Einstellungen

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. Erstellen Sie einen eindeutigen Boundary
    const boundary = `----n8nboundary${Math.random().toString(36).slice(2, 11)}`;
    const chunks = [];

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

    // 2. FÜGEN SIE DAS FELD 'aggregate' MANUELL HINZU
    // Da wir im 'Binary'-Modus sind, MUSS dies im Buffer sein
    pushText(`--${boundary}${CRLF}`);
    pushText(`Content-Disposition: form-data; name="aggregate"${CRLF}`);
    pushText(`${CRLF}true${CRLF}`);

    // 3. Verarbeiten Sie die 'params'-Datei
    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. Verarbeiten Sie alle anderen Dateien (zugeordnet zu '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. Schließen Sie den Boundary
    pushText(`--${boundary}--${CRLF}`);

    const body = Buffer.concat(chunks);
    
    // Der exakte Header, den der Server sehen muss
    const contentTypeHeader = `multipart/form-data; boundary=${boundary}`;

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

  return result;
})();

Fügen Sie nichts zu „Body Parameters

Welche Version von n8n verwendest du? Ich habe mehrere Versionen ausprobiert und konnte es nur konsistent auf 1.123.65 zum Laufen bringen. Leider versuchte n8n, wenn ich es zum Laufen brachte, den Body hochzuladen, indem es jedes einzelne Bit als eigene Form-Data sendete:

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
 
<viele Zeilen übersprungen>

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

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

Bei jeder 2.xx-Version, die ich ausprobiert habe, bekam ich einfach nur source.on is not a function als Fehlermeldung.

Hilft das weiter?

  1. Löschen Sie den HTTP Request-Knoten.

  2. Fügen Sie diesen Code in einen Code Node ein.

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

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

// 1. 'aggregate' hinzufügen
form.append('aggregate', 'true');

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

// 3. Alle anderen Dateien zu 'data' hinzufügen
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. Anfrage direkt ausführen
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. Gehen Sie zu Ihren Settings (oder Environment variables) und stellen Sie sicher, dass NODE_FUNCTION_ALLOW_EXTERNAL axios und form-data enthält, falls Ihre spezifische Instanz diese einschränkt (normalerweise sind sie jedoch gebündelt). Bei n8n Cloud oder Standard Docker ist axios integriert.

Das funktionierte großartig, bis der Endpoint eine Authentifizierung erforderte – Custom-Code-Knoten haben keine Berechtigung, mit gespeicherten Anmeldedaten zu arbeiten.

Nach einem Brainstorming habe ich am Ende einen benutzerdefinierten Knoten erstellt. Die aktuelle Implementierung ist schnell und schmutzig, aber wenn ich irgendwann Zeit habe, werde ich sie auf Standard bringen und als Community-Knoten posten.

Danke dafür