Files
sar/.claude/skills/bmad-testarch-trace/resources/knowledge/webhook-timeout-error.md
julian 17c08e6392 chore: initial monorepo scaffold + WDS Phase 1+2 artifacts
- 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>
2026-05-27 14:34:20 +00:00

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