|
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130 |
- # 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
-
- ```typescript
- 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
-
- ```typescript
- 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:
-
- ```typescript
- // 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
-
- ```typescript
- import { WebhookTimeoutError } from '@seontechnologies/playwright-utils/webhook';
- ```
-
- ## Related Fragments
-
- - `webhook-template-matchers.md` — matcherDetails string format per matcher type
- - `webhook-waiting-querying.md` — waitFor and waitForCount throw this error on timeout
|