- 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>
131 lines
5.6 KiB
Markdown
131 lines
5.6 KiB
Markdown
# 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
|