Все статьи
Конверты событий доставки, возврата и жалобы на письмо проходят через подписанный туннель к маршруту Next.js на localhost.
ResendNext.jsemail webhookslocalhost

Локальное тестирование вебхуков Resend в Next.js

Чтобы протестировать веб-перехватчики Resend локально в Next.js, создайте POST-маршрут App Router, который считывает необработанное тело, сверьте его заголовки Svix с секретом подписи Resend и зарегистрируйте URL-адрес туннеля HTTPS на панели управления Resend. Отправьте электронное письмо через Resend, а затем обработайте реальные email.sent, email.delivered, email.bounced, или email.complained события на локальном хосте.

Что вебхук Resend сообщает вашему приложению

Ответ API о том, что электронное письмо было принято, не является доказательством того, что оно дошло до получателя. Доставка происходит асинхронно. Веб-перехватчики Resend позволяют вашему приложению обновлять состояние сообщения, подавлять неверные адреса, сообщать о недоставках и реагировать на жалобы после завершения исходного запроса на отправку. официальная документация по веб-перехватчику Resend перечисляет типы событий и настройки информационной панели.

Проверка локального веб-перехватчика должна охватывать весь конечный автомат, а не только то, достигает ли POST вашего маршрута. Сопоставьте идентификатор электронной почты каждого события с записью, созданной при отправке. Рассматривайте состояния как переходы: принято, отправлено, доставлено, задержано, возвращено, жалоба, открыто или нажато, где это применимо. Более поздний дубликат не должен перезаписывать более полезное состояние или вызывать одно и то же оповещение дважды.

Создайте маршрут Next.js App Router.

Установите верификатор, поддерживаемый для формата подписи:

npm install svix

Затем создайте маршрут времени выполнения Node. Resend подписывает исходное тело, поэтому используйте request.text() ровно один раз перед разбором.

// app/api/webhooks/resend/route.ts
import { Webhook } from 'svix';

export const runtime = 'nodejs';

export async function POST(request: Request) {
  const payload = await request.text();
  const headers = {
    'svix-id': request.headers.get('svix-id') ?? '',
    'svix-timestamp': request.headers.get('svix-timestamp') ?? '',
    'svix-signature': request.headers.get('svix-signature') ?? '',
  };

  let event: ResendEvent;
  try {
    const webhook = new Webhook(process.env.RESEND_WEBHOOK_SECRET!);
    event = webhook.verify(payload, headers) as ResendEvent;
  } catch {
    return Response.json({ error: 'Invalid webhook signature' }, { status: 400 });
  }

  await enqueueResendEvent({
    deliveryId: headers['svix-id'],
    event,
  });
  return Response.json({ received: true });
}

Секрет подписи принадлежит этой конечной точке веб-перехватчика и обычно начинается с префикса, специфичного для поставщика. Скопируйте его из настроек веб-перехватчика Resend в игнорируемый файл локальной среды, например .env.local. Это не ключ API Resend, используемый для отправки электронной почты.

Почему важны три заголовка Svix

  • svix-id однозначно идентифицирует доставку и является лучшим ключом идемпотентности.
  • svix-timestamp привязывает подпись ко времени, позволяя проверяющему устройству отклонять устаревшие запросы, выходящие за пределы его допуска.
  • svix-signature может содержать одну или несколько версионных подписей, используемых для аутентификации тела.

Не реализуйте этот протокол путем разделения строк заголовков, если у вас нет веской причины. SDK обрабатывает кодирование, проверку нескольких подписей и меток времени. Resend явно рекомендует использовать секрет подписи и эти заголовки для проверки. Чем глубже руководство по проверке подписи объясняет, почему важны необработанные байты и проверки с соблюдением тайминга.

Откройте Next.js и зарегистрируйте конечную точку.

  1. Беги npm run dev и подтвердите, что приложение прослушивает порт 3000.
  2. Старт npx portpreview 3000 в другом терминале.
  3. В Resend создайте веб-перехватчик, конечная точка которого https://YOUR-TUNNEL.portpreview.dev/api/webhooks/resend.
  4. Выберите только те события электронной почты, которые обрабатывает ваше приложение.
  5. Скопируйте секрет подписи конечной точки в RESEND_WEBHOOK_SECRET и перезапустите Next.js, чтобы он загрузил переменную.
  6. Отправьте сообщение, используя проверенный домен, и проверьте события, которые достигают локального маршрута.

Поддерживайте стабильность общедоступного URL-адреса на протяжении сеанса. Если источник туннеля изменится, отредактируйте конечную точку Resend перед повторным тестированием. Конечная точка, настроенная со старым URL-адресом, не может связаться с вашим новым процессом, даже если сам localhost исправен.

Используйте типизированный диспетчер событий

Полезные нагрузки вебхука должны поступать в один узкий диспетчер. Проверяйте обязательные поля и делайте невидимые типы событий видимыми, не рассматривая их как сбои сервера.

async function processEvent(event: ResendEvent) {
  switch (event.type) {
    case 'email.delivered':
      await markDelivered(event.data.email_id, event.created_at);
      break;
    case 'email.bounced':
      await markBounced(event.data.email_id, event.data.bounce?.message);
      await suppressIfPermanent(event.data);
      break;
    case 'email.complained':
      await suppressRecipients(event.data.to);
      await alertCompliance(event.data.email_id);
      break;
    default:
      await recordUnhandledEvent(event);
  }
}

Сохраняйте типы полезных данных в соответствии с текущей схемой Resend, а не предполагайте, что каждое событие имеет идентичные данные. Например, сведения о возвратах и ​​списки получателей могут быть актуальны только для определенных событий. Сохраните тип события, идентификатор электронной почты поставщика, метку времени события и минимальную отредактированную полезную нагрузку для расследований службы поддержки.

Сделайте обработку идемпотентной перед повторными попытками тестирования

Системы Webhook на практике обеспечивают доставку хотя бы один раз. Тайм-аут может произойти после фиксации базы данных, но до того, как поставщик получит ваш ответ 200. Затем поставщик повторяет запрос, который вы уже подали. Использование svix-id в качестве уникального ключа доставки и вставьте его в ту же транзакцию, что и изменение состояния.

await db.transaction(async (tx) => {
  const inserted = await tx.webhookDelivery.insertOnce({
    provider: 'resend',
    deliveryId,
  });
  if (!inserted) return;
  await applyEmailEvent(tx, event);
});

Не выполняйте дедупликацию исключительно по идентификатору электронной почты, поскольку одно электронное письмо законно получает несколько типов событий. В зависимости от вашей модели данных сохраните как уникальный ключ уровня доставки, так и правила перехода состояний. Читать Повторные попытки веб-перехватчика и шаблоны идемпотентности перед подключением событий к выставлению счетов, подавлению или уведомлениям клиентов.

Вернитесь быстро, не теряя события

Проверка подписи уместна в пути запроса; медленная деловая работа — нет. Сохраните или поставьте в очередь проверенное событие, а затем верните 2xx. Если вы вернетесь до начала устойчивой записи, сбой процесса может привести к потере события. Если вы подождете несколько удаленных API, ваша конечная точка может истечь и предложить повторные попытки. Таблица входящих сообщений базы данных часто представляет собой простейшую локальную и производственную структуру.

Генерируйте полезные тестовые события

Отправлено и доставлено

Отправьте на адрес, который вы контролируете, с подтвержденного домена. Запишите идентификатор электронной почты, возвращенный API отправки, и подтвердите, что входящие события обновляют ту же строку. Время доставки зависит от сервера-получателя, поэтому не предполагайте, что события происходят немедленно или в упрощенной последовательности.

Отскакивает

Используйте задокументированные тестовые адреса или функции тестирования Resend, а не изобретайте трафик для несвязанных доменов. Убедитесь, что постоянные сбои подавляют будущую почту, а временные условия соответствуют вашей политике повторных попыток. Не подавляйте автоматически каждое отложенное событие.

Жалобы

Обработка жалоб – это логика как доставляемости, так и соответствия требованиям. Убедитесь, что повторный вебхук не создает повторяющихся оповещений, и убедитесь, что затронутый получатель исключен из последующих кампаний в соответствии с вашей политикой.

Устранение сбоев веб-перехватчика Resend

Проверка подписи всегда терпит неудачу

Убедитесь, что секрет подписи конечной точки, а не ключ API, загружен. Использование await request.text(), не анализируйте и не преобразуйте JSON в новую строку, а передайте все три заголовка Svix с их точными значениями. Перезапустите сервер разработки после изменения .env.local.

Маршрут возвращает 404 или 405.

App Router Файлы маршрутов должны иметь имена route.ts под предполагаемыми сегментами URL и экспортировать POST. Проверьте, перезаписывает ли промежуточное программное обеспечение запрос туннеля на локаль или страницу входа. Проверьте общедоступный URL-адрес с помощью Curl и проверьте фактический ответ.

Resend показывает повторы, несмотря на успешную обработку.

Убедитесь, что каждая успешная ветвь быстро возвращает 2xx. Ошибки, возникающие после обновления базы данных, могут привести к ошибке 500 и повторной попытке. Сделайте обработку транзакционной и идемпотентной, а затем проверьте задержку ответа.

События приходят, но их нельзя связать с электронным письмом

Сохраните идентификатор электронной почты поставщика из исходного ответа на отправку Resend. Не полагайтесь на строки темы или адреса получателей в качестве идентификаторов. Эти поля не являются ни уникальными, ни достаточно стабильными для корреляции.

Воспроизведенные записи не проходят проверку временной метки

Это ожидается при воспроизведении старого подписанного запроса через обычный верификатор: его временная метка может выходить за пределы допустимого допуска. Предпочитайте повторную доставку поставщиком, если это возможно. Для изолированных тестов бизнес-логики выполните проверку один раз, сохраните очищенное устройство событий и протестируйте диспетчер отдельно. руководство по повторам объясняет эту границу.

Безопасность и конфиденциальность при тестировании событий электронной почты

  • Никогда не подвергайте RESEND_API_KEY или секрет подписи конечной точки в исходном коде, пакетах браузера, снимках экрана или журналах запросов.
  • Проверьте это перед анализом или сохранением события.
  • Редактируйте получателей, темы, заголовки и метаданные сообщений из записей общего туннеля.
  • Применять ограничения хранения к необработанным полезным нагрузкам веб-перехватчика; храните только то, что требует поддержки и соответствия требованиям.
  • Используйте отдельный секрет локальной конечной точки из рабочей среды и меняйте его при удалении тестовой конечной точки.

Окончательный дизайн должен работать одинаково после развертывания: общедоступная конечная точка HTTPS, проверка необработанного тела, устойчивая идемпотентность, быстрое подтверждение и асинхронная обработка состояний. Подробности о необработанном теле, специфичном для App Router, см. Next.js веб-перехватчик: руководство по локальному хосту.

Часто задаваемые вопросы

Как протестировать веб-перехватчики Resend локально в Next.js?
Создайте POST-маршрут App Router, проверьте необработанное тело с помощью заголовков Svix и секрета подписи конечной точки, откройте порт 3000 через HTTPS и зарегистрируйте этот общедоступный URL-адрес в Resend.
Должен ли веб-хук Resend использовать request.json() в Next.js?
Не раньше проверки. Прочитайте await request.text(), чтобы подписанные байты остались неизменными, проверьте с помощью Svix и используйте проверенное событие, возвращаемое SDK.
Является ли секрет подписи веб-перехватчика Resend тем же, что и ключ API?
Нет. Ключ API разрешает отправку запросов. Каждая конечная точка веб-перехватчика имеет секрет подписи, используемый для проверки входящих событий; храните оба отдельно.
Как предотвратить дублирование обработки веб-перехватчика Resend?
Сохраните Svix-id с уникальным ограничением и примените событие в той же транзакции. Не делайте дедупликацию только по идентификатору электронной почты, поскольку одно электронное письмо имеет несколько действительных событий.