- Nx 22.7 monorepo (pnpm 11.1, TypeScript 5.9, Node 24) - apps/api: NestJS 11 (CJS conforme CODING-RULES.md PGD-DB-004) - apps/web: React 19 + Vite 8 (ESM) - libs/shared/api-interface: Zod contract base - Docker Compose dev: Postgres 18, Valkey 8, MinIO, Mailpit - WDS artifacts: - design-artifacts/A-Product-Brief/ (5 docs canônicos + 16 dialogs) - design-artifacts/B-Trigger-Map/ (hub + 4 personas + feature impact) - Stack canon: STACK.md v2.2 + CODING-RULES.md v2.0 + brand.md - AGENTS.md + README.md como entrada para devs/agentes Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5.6 KiB
WebhookTimeoutError and Debugging
Principle
WebhookTimeoutError is thrown when waitFor or waitForCount does not find a matching webhook within the configured timeout. It carries a snapshot of received webhooks from the last polling cycle — truncated to the last 10 entries — so you can inspect what arrived vs. what was expected. The full count of all received webhooks is available in totalReceived.
Error Properties
class WebhookTimeoutError extends Error {
readonly name = 'WebhookTimeoutError';
readonly templateName: string; // from webhookTemplate('...')
readonly timeoutMs: number; // the timeout that was exceeded
readonly totalReceived: number; // total webhooks seen in polling window
readonly receivedWebhooks: ReceivedWebhook[]; // last ≤10 received webhooks
readonly matcherDetails: string[]; // human-readable matcher summary
toJSON(): Record<string, unknown>; // serialize all fields for CI logs
}
receivedWebhooks is capped at the last 10 entries. If more than 10 webhooks arrived, totalReceived shows the full count but receivedWebhooks contains only the most recent 10.
Reading the Error
The error message format:
Webhook "movie.deleted" not received within 15000ms.
3 webhook(s) were received but none matched.
Matchers: field(event="movie.deleted"), field(data.id=42).
Use matcherDetails to confirm the matchers were configured correctly. Use receivedWebhooks to inspect actual payloads — compare field paths and values against what the matchers expect.
Validating the Error Shape in Tests
import { WebhookTimeoutError, webhookTemplate } from '@seontechnologies/playwright-utils/webhook';
const neverArrivingTemplate = webhookTemplate('never.arrives')
.matchField('event', 'event.that.never.happens')
.withTimeout(500)
.withInterval(100)
.build();
const [waitResult] = await Promise.allSettled([webhookRegistry.waitFor(neverArrivingTemplate)]);
expect(waitResult.status).toBe('rejected');
if (waitResult.status !== 'rejected') {
throw new Error('Expected webhook wait to reject with WebhookTimeoutError');
}
const error = waitResult.reason as WebhookTimeoutError;
expect(error).toBeInstanceOf(WebhookTimeoutError);
expect(error.templateName).toBe('never.arrives');
expect(error.timeoutMs).toBe(500);
expect(error.toJSON()).toMatchObject({
name: 'WebhookTimeoutError',
templateName: 'never.arrives',
timeoutMs: 500,
totalReceived: expect.any(Number),
matcherDetails: ['field(event="event.that.never.happens")'],
});
Inspecting receivedWebhooks
When a webhook arrives but doesn't match, receivedWebhooks shows you what actually came in:
// Wait for create webhook first — puts it in the journal
await webhookRegistry.waitFor(movieCreated(movieId));
// Wait for delete webhook that will never arrive — no delete was called
const undeliveredDelete = webhookTemplate<{
event: string;
data: { id: number };
}>('movie.deleted.not.delivered')
.matchField('event', 'movie.deleted')
.matchField('data.id', movieId)
.withTimeout(2_000)
.withInterval(200)
.build();
const [waitResult] = await Promise.allSettled([webhookRegistry.waitFor(undeliveredDelete)]);
expect(waitResult.status).toBe('rejected');
if (waitResult.status !== 'rejected') {
throw new Error('Expected webhook wait to reject with WebhookTimeoutError');
}
const error = waitResult.reason as WebhookTimeoutError;
expect(error).toBeInstanceOf(WebhookTimeoutError);
expect(error.totalReceived).toBeGreaterThanOrEqual(1);
// The movie.created webhook that did arrive is visible in the error
const createdWebhook = error.receivedWebhooks.find((w) => (w.body as { data: { id: number } }).data.id === movieId);
expect(createdWebhook).toBeDefined();
expect((createdWebhook!.body as { event: string }).event).toBe('movie.created');
Common Failure Patterns
| What you see | Likely cause | Fix |
|---|---|---|
totalReceived: 0 |
Webhook not delivered; wrong URL or event not firing | Check application event publishing and webhook routing |
totalReceived > 0, none match |
Webhooks arriving but matchers not matching | Inspect receivedWebhooks[0].body — check field paths and values |
matcherDetails shows wrong path |
Template factory misconfigured | Print error.toJSON() and compare paths against actual payload |
totalReceived: 0 with matched-only |
Another worker claimed and deleted the webhook first | Ensure template is scoped by entity ID |
| Parse error in body | Webhook body is not valid JSON | Check receivedWebhooks[n].parseError and rawBody |
matcherDetails Format per Matcher Type
| Matcher | matcherDetails string |
|---|---|
matchField('event', 'x') |
field(event="x") |
matchPartial({ a: 1 }) |
partial({"a":1}) |
matchPredicate('my desc', fn) |
predicate(my desc) |
Import
import { WebhookTimeoutError } from '@seontechnologies/playwright-utils/webhook';
Related Fragments
webhook-template-matchers.md— matcherDetails string format per matcher typewebhook-waiting-querying.md— waitFor and waitForCount throw this error on timeout