Webhook может не только принять заявку, но и отдать файл в ответ на запрос. Сделаем учебную ссылку, которая собирает JSON-каталог и возвращает его как вложение. Проверим не только HTTP 200, но и заголовки, содержимое файла и одноразовый тестовый URL.
Результат инструкции
Локальный GET-запрос запустит workflow. Три вымышленные карточки товаров превратятся в один catalog-demo.json, после проверки байтов Respond to Webhook отправит файл клиенту. В ответе будут Content-Type: application/json; charset=utf-8 и Content-Disposition: attachment; filename="catalog-demo.json".
Используем Test URL и не публикуем workflow. Это учебная выгрузка с фиксированными данными: она не читает вашу базу и не зависит от параметров запроса. Нейросеть, внешние API и ключи не нужны. Производственный сервис скачивания с авторизацией и доступом к настоящим документам здесь не создаём.
Что понадобится
- Работающий локальный n8n и доступ к новому workflow. Проверено на n8n 2.42.4, Webhook 2.1, Convert to File 1.1 и Respond to Webhook 1.4.
- Code на JavaScript для учебных данных и проверок. Сам файл создаёт штатный Convert to File, отправляет Respond to Webhook.
- Браузер или curl для GET-запроса. Пример команды использует localhost:5679; подставьте фактический Test URL своего экземпляра.
Установить n8n локально через Docker
Шаг 1. Импортируйте готовую схему
Создайте отдельный workflow, выберите Import from File и загрузите JSON. В схеме шесть узлов:
Download Request → Sample Rows → Check Rows → Make JSON → Check File → Send File
Download Request — Webhook, Make JSON — Convert to File, Send File — Respond to Webhook. Остальные три узла — Code. В файле нет credentials и ID чужого workflow, состояние неактивное. Не нажимайте Publish или Activate: для примера достаточно тестового ожидания.
Шаг 2. Настройте Webhook на ответ отдельным узлом
В Download Request задайте следующие параметры:
| Поле | Значение | Зачем |
|---|---|---|
| HTTP Method | GET | Скачивание обычным запросом |
| Path | ezgpt-file-download | Путь учебного endpoint |
| Respond | Using Respond to Webhook node | Ответ сформирует Send File |
| Authentication | None | Только локальный тест с вымышленными данными |
None здесь допустим только в описанном локальном учебном сценарии. Не выносите такой endpoint с настоящими данными в интернет. Имя пути не является паролем. Не добавляйте запрос SQL, имя файла или путь к документу из query-параметра: пример возвращает строго заданный каталог.
В Webhook переключитесь на Test URL и скопируйте адрес. Пока слушатель не запущен, запрос к нему может возвращать 404. Production URL — другой адрес, он не должен работать у нашего неопубликованного workflow.
Шаг 3. Подготовьте карточки товаров
В Sample Rows выберите Mode: Run Once for All Items, Language: JavaScript. Вставьте:
const rows = [
{"id":"001","title":"Блокнот","quantity":2,"active":true,"note":null,"tags":["офис","бумага"]},
{"id":"002","title":"Чай «Утро»","quantity":0,"active":false,"note":"Проверить остаток","tags":[]},
{"id":"003","title":"Папка A4","quantity":5,"active":true,"note":"Строка 1\nСтрока 2","tags":["архив"]}
];
return rows.map(json => ({ json }));
Источник выдаёт три отдельных items. ID — строки с ведущими нулями, quantity — числа, active — boolean, note — строка либо null, tags — массив строк. Вторая карточка специально содержит 0, false и пустой массив. Данные запроса из Webhook в каталог не копируются.
Шаг 4. Проверьте записи до создания файла
В Check Rows установите Run Once for All Items, JavaScript и вставьте:
const items = $input.all();
if (items.length < 1 || items.length > 20) throw new Error('row_count_invalid');
const ids = new Set();
for (const { json: row } of items) {
if (Object.keys(row).sort().join('|') !== 'active|id|note|quantity|tags|title') {
throw new Error('row_fields_invalid');
}
if (typeof row.id !== 'string' || !/^\d{3}$/.test(row.id) || ids.has(row.id)) {
throw new Error('id_invalid_or_duplicate');
}
if (typeof row.title !== 'string' || !row.title.trim() || row.title.length > 100) {
throw new Error('title_invalid');
}
if (!Number.isSafeInteger(row.quantity) || row.quantity < 0) throw new Error('quantity_invalid');
if (typeof row.active !== 'boolean') throw new Error('active_invalid');
if (row.note !== null && (typeof row.note !== 'string' || row.note.length > 200)) {
throw new Error('note_invalid');
}
if (!Array.isArray(row.tags) || row.tags.length > 3 || row.tags.some(t => typeof t !== 'string' || !t.trim() || t.length > 30)) {
throw new Error('tags_invalid');
}
ids.add(row.id);
}
return items.map(item => ({ json: item.json }));
Проверка допускает 1–20 карточек с точными полями, уникальными строковыми ID и корректными типами. Неизвестные поля, строковое количество, повтор ID и отсутствующий note остановят выполнение. Это учебный договор, а не ограничение JSON или Webhook.
В Settings узлов оставьте On Error: Stop Workflow. Continue On Fail, Retry On Fail, Always Output Data и Execute Once не включайте. Ошибку подготовки файла нельзя превращать в успешную выгрузку с пустым объектом.
Шаг 5. Создайте один JSON-файл
В Make JSON выберите Operation: Convert to JSON, Mode: All Items to One File, Put Output File in Field: export. В Options задайте File Name: catalog-demo.json, Format: On, Encoding: utf8, Add Byte Order Mark (BOM): Off.
На выходе этого узла нужен один item с одним binary-полем export. В нём лежит общий JSON-массив из трёх карточек. JSON самого item может быть пустым — файл находится на вкладке Binary.
Не выбирайте Each Item to Separate File. Respond to Webhook не отправляет несколько отдельных файлов по одному HTTP-запросу таким способом. Если нужен набор файлов, сначала спроектируйте общий архив; наш Check File отклоняет несколько входных items.
Подробно о создании JSON-файла через Convert to File
Шаг 6. Проверьте байты перед ответом
В Check File установите Run Once for All Items, JavaScript и вставьте:
const items = $input.all();
if (items.length !== 1) throw new Error('one_file_required');
const item = items[0];
if (Object.keys(item.binary ?? {}).join('|') !== 'export') throw new Error('export_binary_required');
if (item.binary.export.fileName !== 'catalog-demo.json') throw new Error('file_name_invalid');
const bytes = await this.helpers.getBinaryDataBuffer(0, 'export');
if (bytes.length < 1 || bytes.length > 16384) throw new Error('file_size_invalid');
if (bytes[0] === 239 && bytes[1] === 187 && bytes[2] === 191) throw new Error('bom_not_expected');
const text = bytes.toString('utf8');
if (!Buffer.from(text, 'utf8').equals(bytes)) throw new Error('utf8_invalid');
const expected = $('Check Rows').all().map(i => i.json);
if (text !== JSON.stringify(expected, null, 2)) throw new Error('file_content_mismatch');
return [{ json: {}, binary: item.binary }];
Код читает реальные байты binary через helper n8n и сравнивает их с форматированным JSON из Check Rows. Проверяет один файл, имя, размер до 16384 байт, UTF-8 без BOM и точное содержимое. Затем передаёт исходный binary.export дальше.
Ограничение размера действует после создания файла. Это не лимит трафика входящего Webhook, не антивирус и не проверка фактов в карточках. Если вы меняете форматирование или кодировку, нужно одновременно пересмотреть договор проверки, а не просто удалить Check File.
Шаг 7. Настройте отправку Binary File
В Send File задайте:
| Поле | Значение | Что отправляется |
|---|---|---|
| Respond With | Binary File | Байты файла, не JSON item |
| Response Data Source | Specify Myself | Поле выбираем явно |
| Input Field Name | export | То же имя, что в Make JSON |
| Options → Response Code | 200 | Код успешного ответа |
Input Field Name — имя binary-поля export, не имя catalog-demo.json. Если написать data, Send File не найдёт нужный файл. Не выбирайте First Incoming Item: после Check File JSON пустой, поэтому вместо каталога можно получить {} с успешным статусом.
Один HTTP-запрос имеет один ответ. Не ставьте несколько Respond to Webhook в последовательности в надежде отправить каждый файл отдельно: этой схемой так не делают. Мы заранее собираем все карточки в один документ.
Шаг 8. Добавьте заголовки скачивания
В Send File откройте Options → Response Headers и добавьте три пары Name / Value:
- Content-Type: application/json; charset=utf-8
- Content-Disposition: attachment; filename="catalog-demo.json"
- Cache-Control: no-store
Content-Type описывает формат тела. Content-Disposition с attachment предлагает клиенту сохранить ответ как файл и задаёт имя. Без него тот же JSON может открыться как текст; HTTP 200 и правильные байты при этом ещё не доказывают настройку скачивания.
Имя файла здесь фиксированное и безопасное для учебного примера. Не вставляйте в заголовок произвольную строку из запроса. Cache-Control: no-store адресован HTTP-клиентам и промежуточным кэшам; он не отключает хранение executions и binary внутри n8n.
Шаг 9. Запустите ожидание тестового запроса
Сохраните workflow и нажмите Execute workflow, чтобы Download Request ждал событие. Можно использовать ожидание тестового события в самом Webhook. Убедитесь, что выбран именно Test URL и редактор показывает ожидание запроса.
Не запускайте Send File отдельно как обычный ручной узел: без входящего HTTP-запроса ему некому отправить ответ. После тестового запроса слушатель нужно запускать заново перед следующей проверкой.
Пример Test URL для локального экземпляра на порту 5679: http://localhost:5679/webhook-test/ezgpt-file-download. Если в редакторе другой порт, домен или протокол, используйте адрес из редактора. Внешний компьютер не сможет открыть ваш localhost: он указывает на него самого.
Шаг 10. Сделайте GET и сохраните ответ
Откройте скопированный Test URL в браузере: ожидается скачивание catalog-demo.json. Для проверки заголовков через Windows PowerShell выполните в отдельной учебной папке, пока слушатель ждёт запрос:
curl.exe --fail --show-error --max-time 15 --dump-header webhook-headers.txt --output catalog-demo.json "http://localhost:5679/webhook-test/ezgpt-file-download"
Команда создаёт webhook-headers.txt и catalog-demo.json в текущей папке и может перезаписать одноимённые файлы. Используйте пустую учебную папку, чтобы не принять старую копию за новый ответ. Если первым запросом уже открыли ссылку в браузере, перед curl снова включите ожидание.
Не отправляйте POST: наш Webhook настроен на GET. Не подставляйте Production URL вместо тестового адреса. Добавлять --location и отключать проверку TLS в этом примере не нужно.
Шаг 11. Сверьте заголовки и содержимое
В webhook-headers.txt ожидается успешный HTTP-статус и следующие значения заголовков. Регистр их имён может отличаться; это не полный список ответа:
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Content-Disposition: attachment; filename="catalog-demo.json"
Cache-Control: no-store
Откройте catalog-demo.json в редакторе с UTF-8. В исходном примере файл занимает 521 байт, без BOM и без дополнительного переноса в конце. Он должен содержать:
[
{
"id": "001",
"title": "Блокнот",
"quantity": 2,
"active": true,
"note": null,
"tags": [
"офис",
"бумага"
]
},
{
"id": "002",
"title": "Чай «Утро»",
"quantity": 0,
"active": false,
"note": "Проверить остаток",
"tags": []
},
{
"id": "003",
"title": "Папка A4",
"quantity": 5,
"active": true,
"note": "Строка 1\nСтрока 2",
"tags": [
"архив"
]
}
]
Скачать эталон n8n-webhook-file-demo.json
Эталон имеет другое имя, но те же байты. Если вы изменили Sample Rows, размер и содержимое закономерно изменятся. Восстановите источник для точной сверки исходного примера. Проверяйте и тело, и заголовки: пустой объект {} с кодом 200 — не наш файл.
В редакторе откройте выполненный Send File. Binary export остаётся частью данных выполнения. Результат на выходе узла в редакторе и HTTP-тело, полученное браузером, — разные представления; файл нужно проверять на стороне клиента.
Шаг 12. Проверьте повтор, неправильное поле и ошибочные данные
Сразу повторите запрос к Test URL, не включая новое ожидание. В проверенном экземпляре ожидается 404: тестовый слушатель уже использован. Снова нажмите Execute workflow, выполните GET и убедитесь, что скачивание повторяется. Это свойство тестового режима, не кэш файла.
В Send File временно измените Input Field Name на data. Запустите новое ожидание и GET: выполнение должно завершиться ошибкой, а клиент не должен получить эталонный файл. Верните export.
Замените quantity второй карточки на строку "0", снова включите ожидание и сделайте GET. Check Rows должен остановиться с quantity_invalid; в проверенном тестовом режиме ошибка до отправки ответа вернула HTTP 500, а не файл каталога. Верните число 0 и повторите основной запуск.
После восстановления должны одновременно выполняться четыре условия: запрос GET, все шесть узлов отработали, клиент получил attachment с именем catalog-demo.json, а содержимое равно трём исходным карточкам. Один зелёный узел или статус 200 не заменяет эту проверку.
Если файл не скачивается
| Симптом | Что проверить | Правильное состояние |
|---|---|---|
| 404 у Test URL | Ожидание и метод | Активный тестовый слушатель, GET |
| 404 у Production URL | Публикацию workflow | В статье production не включаем |
| HTTP 500, binary не найден | Input Field Name в Send File | export |
| HTTP 200 и пустой объект | Respond With | Binary File |
| JSON открыт текстом | Content-Disposition | attachment с фиксированным именем |
Что не подключать к этой версии
Не публикуйте учебный endpoint с персональными данными, финансовыми документами или произвольным выбором файла по query. Перед production нужны авторизация, проверка прав на конкретную выгрузку, ограничения запросов и размера, а также правила хранения и журналирования. Фиксированный путь и no-store не заменяют эти меры.
Мы не обещаем мгновенную выгрузку больших баз: клиент ждёт формирования ответа, а прокси и сервер имеют свои тайм-ауты. Для долгой подготовки файла нужен отдельный процесс с проверкой готовности, не бесконечное ожидание HTTP. Публикацию, proxy, Cloud и большие выгрузки здесь не тестировали.
Принять JSON и вернуть обычный ответ через Webhook
Скачать файл в другой workflow через HTTP Request
Что проверено редакцией
17 октября 2026 года схема проверена на n8n 2.42.4 в отдельном Docker-экземпляре с binary-режимом filesystem. Выполнены реальные GET-запросы к тестовым Webhook после регистрации слушателя через API. Все 19 проверок прошли: основной точный скачиваемый workflow, одна запись, обратный порядок, Unicode, нули, false и null; дубли, числовой ID, строковое количество, пропущенное поле, отдельные файлы вместо общего, неверное binary-поле и BOM; JSON-ответ вместо Binary File и отсутствие attachment. Проверены 404 без слушателя, неверный POST, одноразовый Test URL, неактивный Production URL и ошибка 500 при отсутствии узла ответа в выбранном режиме. У успешных файлов сверены точные байты, Content-Type, Content-Disposition и Cache-Control. Для ошибок до ответа подтверждён 500 без эталонного файла; успешный JSON-ответ без файла разобран отдельно. Проверены настройки по официальной документации и коду узлов. Учебный контейнер был доступен только на loopback и остановлен, существующие workflows не менялись. Клики редактора, ручное скачивание браузером, production-публикация, авторизация, Cloud, прокси и большие реальные выгрузки не тестировались. Обложка — редакционная AI-иллюстрация, не снимок интерфейса.
Частые вопросы
Почему Test URL сработал только один раз?
В проверенном тестовом режиме слушатель снимается после запроса. Перед следующим GET снова включите ожидание. Production URL — отдельный режим, не замена кнопке теста.
Почему вместо файла пришёл пустой объект?
Проверьте Respond With: Binary File. First Incoming Item отправит JSON входного item, а после Check File он пуст. Сам файл лежит в binary.export.
Зачем Content-Disposition, если JSON уже сформирован?
Тело ответа определяет содержимое, а attachment и filename подсказывают клиенту скачать его с заданным именем. Без заголовка клиент может показать JSON как текст.
Можно вернуть PDF или ZIP?
Respond to Webhook умеет отправлять binary-файл, но создание и проверка PDF или ZIP требуют другого workflow. Эта инструкция проверена только для маленького JSON-каталога.
no-store запрещает n8n хранить файл?
Нет. Этот HTTP-заголовок не управляет сохранением executions и binary внутри n8n. Сроки хранения и доступ к данным выполнения настраиваются отдельно.