Чтобы протестировать веб-перехватчики 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 и зарегистрируйте конечную точку.
- Беги
npm run devи подтвердите, что приложение прослушивает порт 3000. - Старт
npx portpreview 3000в другом терминале. - В Resend создайте веб-перехватчик, конечная точка которого
https://YOUR-TUNNEL.portpreview.dev/api/webhooks/resend. - Выберите только те события электронной почты, которые обрабатывает ваше приложение.
- Скопируйте секрет подписи конечной точки в
RESEND_WEBHOOK_SECRETи перезапустите Next.js, чтобы он загрузил переменную. - Отправьте сообщение, используя проверенный домен, и проверьте события, которые достигают локального маршрута.
Поддерживайте стабильность общедоступного 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 веб-перехватчик: руководство по локальному хосту.
