Envoyer un tableau arbitrairement long de fichiers dans une requête HTTP node

Décrivez le problème/l’erreur/la question

Je dois exécuter une requête HTTP où le serveur s’attend à ce que je fournisse plusieurs fichiers sous le même champ de données. Le nombre de fichiers n’est pas statique, je ne peux donc pas simplement mapper chaque fichier à un champ du nœud n8n. Est-ce même possible via le nœud HTTP ou dois-je utiliser un nœud de code ?

Quel est le message d’erreur (le cas échéant) ?

J’ai essayé comme ceci, mais évidemment si j’ai plus d’un fichier, j’obtiens une erreur indiquant que le binaire avec l’id n’existe pas.

Veuillez partager votre workflow

{
  "nodes": [
    {
      "parameters": {},
      "type": "n8n-nodes-base.manualTrigger",
      "typeVersion": 1,
      "position": [
        0,
        0
      ],
      "id": "fb16aeab-57ba-485f-8c6e-7bc015bd1e28",
      "name": "When clicking 'Execute workflow'"
    },
    {
      "parameters": {
        "method": "POST",
        "url": "=http://example.org/project/tag",
        "sendBody": true,
        "contentType": "multipart-form-data",
        "bodyParameters": {
          "parameters": [
            {
              "parameterType": "formBinaryData",
              "name": "data",
              "inputDataFieldName": "={{ Object.keys($binary).filter((k) =\
 > k !== 'params').join(',') }}"
            },
            {
              "parameterType": "formBinaryData",
              "name": "params",
              "inputDataFieldName": "params"
            },
            {
              "name": "aggregate",
              "value": "true"
            }
          ]
        },
        "options": {
          "response": {
            "response": {
              "fullResponse": true,
              "neverError": true
            }
          }
        }
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.3,
      "position": [
        448,
        0
      ],
      "id": "e2323076-8b22-4872-8eb3-7b1710a8a987",
      "name": "Async Tagging"
    },
    {
      "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": [
        224,
        0
      ],
      "id": "100a0875-9bce-4790-be6c-da7a31407523",
      "name": "Prepare Extraction Input"
    }
  ],
  "connections": {
    "When clicking 'Execute workflow'": {
      "main": [
        [
          {
            "node": "Prepare Extraction Input",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Prepare Extraction Input": {
      "main": [
        [
          {
            "node": "Async Tagging",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "pinData": {},
  "meta": {
    "instanceId": "982a83c4fa14b8860ea8391b8f766ad449d7431aac64126d0b9fc936877b58be"
  }
}

Informations sur votre configuration n8n

  • Version de n8n : 2.21.7
  • Base de données (par défaut : SQLite) : postgres
  • Paramètre EXECUTIONS_PROCESS de n8n (par défaut : own, main) : own
  • Exécution de n8n via (Docker, npm, cloud n8n, application de bureau) : docker
  • Système d’exploitation : macOS

Salut @Vidomina

Essaie ça

  1. Copie le bloc JSON ci-dessus.
  2. Ouvre ton éditeur n8n.
  3. Appuie sur Ctrl+V (ou Cmd+V sur Mac) pour coller les nœuds directement sur le canevas.
  4. Pour tester : Clique sur « Execute Workflow ».
  5. Pour utiliser avec tes vraies données :
    • Supprime le nœud « Mock Data Generator ».
    • Connecte ton nœud de données réel au nœud « Construct Multipart Body ».
    • Assure-toi que ton nœud de données fournit des fichiers binaires (le code trouvera automatiquement toutes les clés binaires, à l’exception de toute clé nommée params).

Bonjour @kjooleng, merci pour la suggestion !

J’ai rencontré 2 problèmes : d’abord, votre approche « Construct Multipart Body » fonctionne bien pour le texte, mais les fichiers comme les PDF et DOCX sont absolument déformés par cette approche.

Je l’ai modifié en :

{
"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 () =
  const CRLF = '\\r\\n';\\n  const items = $input.all();\\n  const result = [ ];\\n\\n  for (let itemIndex = 0; itemIndex 
    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"
}
}

Le second problème, c’est que quand j’envoie la demande au service maintenant, j’obtiens :
Content-Type is not supported

Je n’ai pas encore commencé à déboguer ça.

Voici le JSON modifié

Cela vous permet de vérifier que la logique fonctionne pour les métadonnées (JSON) et le contenu binaire (en simulant vos PDF/Docs) sans avoir besoin de vos fichiers réels.

Merci pour le générateur de données fictives modifié.
J’ai investigué sur la raison pour laquelle le service retournait
Content-Type is not supported et il semble que le nœud HTTP n’envoie rien.

J’ai essayé de le configurer avec n8n-binary + le nom du champ multipartBody mais j’ai reçu une erreur « source.on is not a function » - j’ai probablement fait une erreur dans le JavaScript car je n’ai pas beaucoup d’expérience avec les fonctions de n8n. Je vais consulter la documentation et essayer de comprendre, mais si je rate quelque chose d’évident, tout avis est le bienvenu.

Utilisez ce nœud Code spécifique et ces paramètres de requête HTTP exactement comme indiqué

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. Créer une limite unique
    const boundary = `----n8nboundary${Math.random().toString(36).slice(2, 11)}`;
    const chunks = [];

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

    // 2. AJOUTER LE CHAMP 'aggregate' MANUELLEMENT
    // Comme nous sommes en mode 'Binary', celui-ci DOIT être dans le buffer
    pushText(`--${boundary}${CRLF}`);
    pushText(`Content-Disposition: form-data; name="aggregate"${CRLF}`);
    pushText(`${CRLF}true${CRLF}`);

    // 3. Gérer le fichier '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. Gérer tous les autres fichiers (mappés sur '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. Fermer la limite
    pushText(`--${boundary}--${CRLF}`);

    const body = Buffer.concat(chunks);
    
    // L'en-tête exact que le serveur doit voir
    const contentTypeHeader = `multipart/form-data; boundary=${boundary}`;

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

  return result;
})();

N’ajoutez rien aux « Paramètres du corps ». Configurez la requête HTTP comme suit :

  • Méthode : POST
  • URL : your-api-endpoint
  • Envoyer le corps : true
  • Type de contenu du corps : BinaryC’est la partie la plus importante
  • Nom du champ de données d’entrée : multipartBody
  • Options : (Laisser vide)

Quelle version de n8n utilisez-vous ? J’ai essayé plusieurs versions et j’ai seulement pu le faire fonctionner de manière cohérente sur
1.123.65. Malheureusement, quand j’ai pu le faire fonctionner, n8n a essayé de télécharger le corps en envoyant chaque bit comme ses propres données de formulaire :

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--

Sur chaque version 2.xx que j’ai essayée, j’ai simplement obtenu l’erreur source.on is not a function

Cela va-t-il aider ?

  1. Supprimez le nœud HTTP Request.

  2. Mettez ce code dans un Code Node​.

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

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

// 1. Ajouter 'aggregate'
form.append('aggregate', 'true');

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

// 3. Ajouter tous les autres fichiers à '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. Exécuter la requête directement
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. Allez à vos Settings (ou Environment variables) et assurez-vous que NODE_FUNCTION_ALLOW_EXTERNAL inclut axios et form-data si votre instance spécifique les restreint (bien qu’ils soient généralement fournis). S’il s’agit de n8n Cloud ou d’un Docker standard, axios est intégré.

Cela a fonctionné parfaitement jusqu’au moment où le point de terminaison a nécessité une authentification — les nœuds de code personnalisé n’ont pas les permissions pour travailler avec les identifiants enregistrés.

Après pas mal de brainstorming, j’ai fini par créer un nœud personnalisé. Actuellement, l’implémentation est rapide et basique, mais si j’ai du temps un jour, je la mettrai aux normes et la publierai en tant que nœud communautaire.

Merci pour cela