Урок курса
429, Retry-After и ограниченный exponential backoff
Продвинутый n8n: production-автоматизации и AI-агентыВ предыдущем уроке вы научились останавливать пагинацию по условию — когда cursor исчез или Link-заголовок пуст. Сейчас задача другая: API само говорит «подожди», и нужно не просто остановиться, а выяснить, сколько именно ждать, не нарушив при этом контракт сервера.
HTTP 429, Retry-After и нижняя граница ожидания
Статус 429 Too Many Requests означает, что сервер ограничил частоту запросов. Заголовок Retry-After задаёт момент, раньше которого повторять запрос нельзя. По RFC 9110 у него две формы: число секунд (Retry-After: 30) и HTTP-date (Retry-After: Wed, 21 Oct 2015 07:28:00 GMT).
Чтобы обработать 429 внутри workflow, в HTTP Request включите Options → Response → Include Response Headers and Status и Never Error. Тогда downstream item имеет единый envelope { body, headers, statusCode }, а non-2xx не прерывает execution до маршрутизации.
Code node разбирает обе формы и отличает отсутствующий заголовок от некорректного:
const headers = $input.first().json.headers ?? {};
const raw = headers['retry-after'] ?? headers['Retry-After'];
function parseRetryAfter(value, nowMs = Date.now()) {
if (value === undefined || value === null || String(value).trim() === '') return 0;
const text = String(value).trim();
if (/^\d+$/.test(text)) return Number(text);
const retryAt = Date.parse(text);
if (!Number.isFinite(retryAt)) throw new Error('invalid_retry_after');
return Math.max(0, Math.ceil((retryAt - nowMs) / 1000));
}
const retry_after_seconds = parseRetryAfter(raw);
Отсутствующий Retry-After даёт 0, и тогда действует локальный backoff. Некорректная непустая строка не подменяется нулём: это ошибка входного контракта, иначе workflow мог бы повторить запрос раньше разрешённого времени. Для HTTP-date используется округление вверх, чтобы не сократить server minimum на долю секунды.
retry_after_seconds — нижняя граница ожидания. Локальный cap ограничивает только собственную exponential-компоненту и не имеет права уменьшать требование сервера. Если повторить запрос раньше, следующая попытка может снова получить 429 и впустую израсходовать бюджет попыток.
Ограниченный exponential backoff со счётчиком попыток и wait budget
Локальная компонента backoff растёт по номеру уже выполненного HTTP-вызова и ограничивается exp_cap. В Plan Retry входной item уже содержит envelope ответа и контекст запроса, который понадобится следующему вызову:
const state = $input.first().json;
const attempts = Number(state.attempts ?? 1);
const max_attempts = Number(state.max_attempts);
const base = Number(state.base);
const exp_cap = Number(state.exp_cap);
const total_wait_budget = Number(state.total_wait_budget);
const elapsed_wait = Number(state.elapsed_wait ?? 0);
const exp = Math.min(exp_cap, base * 2 ** (attempts - 1));
const wait_seconds = Math.max(exp, retry_after_seconds);
const remaining_budget = total_wait_budget - elapsed_wait;
При base=2 и exp_cap=60 значения exp равны 2, 4, 8, 16, 32, 60, 60. Затем Math.max применяет server minimum: при exp=8 и Retry-After: 45 ожидание равно 45; при exp=60 и Retry-After: 10 — 60.
Initial call входит в max_attempts. attempts — номер только что завершившегося HTTP-вызова. После каждого 429 Code node сначала решает, разрешён ли ещё один вызов. Terminal result намеренно остаётся узким, а retry result становится полным состоянием следующего запроса:
if (attempts >= max_attempts) {
return [{ json: {
error_code: 'retry_attempts_exhausted',
attempts,
last_status: 429,
decision: 'exhausted',
next_request: false
}}];
}
if (wait_seconds > remaining_budget) {
return [{ json: {
error_code: 'retry_wait_budget_exceeded',
attempts,
last_status: 429,
decision: 'defer',
next_request: false
}}];
}
return [{ json: {
...state,
error_code: null,
attempts: attempts + 1,
last_status: 429,
decision: 'retry',
next_request: true,
wait_seconds,
elapsed_wait: elapsed_wait + wait_seconds
}}];
...state здесь существенно: до Wait сохраняются параметры запроса и пагинации, а attempts + 1 задаёт номер следующего HTTP-вызова. Проверки лимитов используют номер текущего неудачного вызова, и только разрешённая retry-ветка увеличивает его. Так wait budget не обрезает Retry-After: если server minimum не помещается в остаток бюджета, execution завершается controlled deferral без Wait и нового запроса. Отдельный fixture проверяет decision=defer, error_code=retry_wait_budget_exceeded, next_request=false.
Цикл retry в n8n: маршрутизация IF, Wait и контролируемое завершение через Stop And Error
Один IF не может выдать отдельные ветки для 2xx, 429 и остальных статусов: у него только outputs true и false. Здесь нужен Switch и отдельный маршрутизатор решения после Code.
Полная топология:
HTTP Request
→ Status Switch
output 0: 2xx → Success
output 1: 429 → Plan Retry → Retry Decision
fallback: → Unexpected Status Error
Retry Decision
output 0: decision=retry → Wait → HTTP Request
fallback: → Stop And Error
В Status Switch удобно выбрать Expression, задать Number of Outputs = 3 и вернуть индекс:
{{ $json.statusCode >= 200 && $json.statusCode < 300
? 0
: $json.statusCode === 429
? 1
: 2 }}
Ветка 429 идёт в Plan Retry. Этот Code node возвращает один из двух контрактов предыдущей секции: узкий terminal result либо полный retry state. Следующий Retry Decision проверяет только decision: retry уходит в Wait, любой terminal decision (defer или exhausted) — в Stop And Error. Благодаря отдельному Switch terminal item физически не может попасть в Wait.
В Wait выберите After Time Interval, единицу Seconds и значение {{ $json.wait_seconds }}. Wait не пересобирает item: после него в HTTP Request приходит сохранённый контекст с attempts, уже увеличенным на единицу, и обновлённым elapsed_wait.
Для terminal path в Stop And Error выберите Error Type = Error Object и передайте фиксированную форму:
{{ {
error_code: $json.error_code,
attempts: $json.attempts,
last_status: $json.last_status,
decision: $json.decision,
next_request: false
} }}
При recovery fixture 429, 429, 200 запросы получают attempt=1, attempt=2 и attempt=3. Always-429 fixture при max_attempts=3 также выполняет ровно три HTTP-вызова: после третьего 429 Plan Retry возвращает retry_attempts_exhausted, attempts=3, last_status=429, decision=exhausted, next_request=false, поэтому четвёртого вызова в transcript нет. Over-budget fixture завершается terminal result с retry_wait_budget_exceeded и decision=defer.
Задача 1
Зачем в retry-fixture включён Never Error у HTTP Request?
Задача 2
max_attempts равен 4, и каждый ответ — 429. Сколько HTTP-вызовов разрешено до terminal error?
Задача 3
Лаборатория: 429, Retry-After и ограниченный exponential backoff
Соберите ограниченный retry для 429: задержка учитывает Retry-After и exponential backoff, а общий wait budget имеет приоритет над очередной попыткой. Проверьте восстановление и жёсткую остановку.
Файлы лабораторной
Скачайте из блока «Файлы шага» n8n_lab_08_setup.py и n8n_lab_08_README.md. Запустите setup-скрипт в пустой папке: он восстановит рабочую структуру и создаст submission/workflow.json.
Зафиксированная среда
n8n
2.39.9; цель:either. Источник фиксации: Pinned course runtime 2.39.9; exact node typeVersions read from the supplied export on 2026-09-22.n8n-nodes-base.manualTriggertypeVersion1— Узел Manual Trigger в выданном exportn8n-nodes-base.settypeVersion3.4— Узел Scenario Config в выданном exportn8n-nodes-base.switchtypeVersion3.4— Узел Source Mode Switch в выданном exportn8n-nodes-base.httpRequesttypeVersion4.3— Узел Cursor Request в выданном exportn8n-nodes-base.codetypeVersion2— Узел Validate Body в выданном exportn8n-nodes-base.stopAndErrortypeVersion1— Узел Auth Failure в выданном exportn8n-nodes-base.waittypeVersion1.1— Узел Wait Before Retry в выданном exportn8n-nodes-base.convertToFiletypeVersion1.1— Узел Convert Result to JSON File в выданном exportn8n-nodes-base.readWriteFiletypeVersion1— Узел Write result.json в выданном export
Подготовка
- Создать рабочую копию. Выполните python assets/setup.py, затем импортируйте submission/workflow.json в n8n 2.39.9.
Ожидаемое наблюдение: Создан отдельный submission/workflow.json; исходный starter остаётся неизменным.
Запуск
Исправить workflow. Внесите требуемое lesson-specific изменение в импортированный граф и экспортируйте его поверх submission/workflow.json.
Ожидаемое наблюдение: Изменение присутствует в параметрах и connections экспортированного workflow.Проверить submission. Выполните python assets/run.py. Runner сам читает submission/workflow.json, прогоняет fixtures и перезаписывает observations/report; student_evidence не используется.
Ожидаемое наблюдение: Receipt N8N_LAB_08_OK появляется только для исправленного графа.
Матрица приёмки
two 429 then success. Runner подаёт fixture в deterministic engine после проверки текущего submission/workflow.json; ручные actual и execution_id не принимаются. Наблюдения: До успеха выполнены ровно три HTTP attempts.; Суммарное ожидание равно 5 секундам.. Зафиксируйте: lesson_id, submission_sha256, case_id, derived_actual, observation_sha256, receipt.
retry after over budget. Runner подаёт fixture в deterministic engine после проверки текущего submission/workflow.json; ручные actual и execution_id не принимаются. Наблюдения: Retry-After 12 не запускает Wait при бюджете 5.; Результат терминальный и next_request=false.. Зафиксируйте: lesson_id, submission_sha256, case_id, derived_actual, observation_sha256, receipt.
Сброс
- Сбросить derived evidence. Выполните python assets/reset.py. Чтобы вернуть starter, отдельно снова выполните python assets/setup.py.
Ожидаемое наблюдение: Удалены observations.json и verification_report.json; workflow не подменяется скрытым эталоном.
Что отправить
Сдайте receipt из assets/run.py и сохраните generated evidence/verification_report.json. Runner не читает student_evidence.
Обязательные поля: lesson_id, submission_sha256, case_id, derived_actual, observation_sha256, receipt.
В поле ответа вставьте значение receipt из отчёта runner.
Границы проверки
Runner исполняет детерминированную учебную модель узлов и state/stub-контрактов после разбора export; живой n8n 2.39.9 остаётся отдельной ручной проверкой.
Ни actual, ни execution_id, ни текстовое заявление learner_change не принимаются как доказательство.
Продолжить с сохранением прогресса
Перейдите к уроку в учебном кабинете.
