feat(fp): WP-21 — resilience seams (correlation-id, idempotency, retry)

Correlation id becomes real ASP.NET Core middleware instead of a per-endpoint
read: every request gets one (client-supplied or generated), it's echoed as
an X-Correlation-Id response header, and pushed into the logging scope so
every log line for that request carries it — not just the Submit helper's,
verified against LogBrief which never threads it explicitly.

Idempotency-Key moves from per-HTTP-attempt (defeating its own purpose) to
per-logical-submit: runSubmit mints one key and threads it through a small
bridge (withIdempotencyKey/currentIdempotencyKey) since the NSwag-generated
client has no per-call header hook. Backend gains an IdempotencyStore that
short-circuits a replayed key to the first call's result instead of minting
a second reference — scoped to the Submit-helper endpoints per the WP's own
decision.

GET requests now retry transient failures (rxjs retry({count:2, delay:500}));
writes never auto-retry. Proven with a fake-HttpClient spec
(api-client.provider.spec.ts) rather than a manual network-tab check — the
WP's suggested `?scenario=error` check turned out not to exercise a real
network call at all (the interceptor throws before calling next()), so the
automated test is the actual proof.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-04 20:03:41 +02:00
parent e276629107
commit 40dbcb2606
8 changed files with 322 additions and 44 deletions

View File

@@ -1,6 +1,6 @@
# WP-21 — Resilience seams (correlation-id, idempotency, retry)
Status: todo
Status: done (pending commit)
Phase: 5 — productie-volwassenheid
## Why
@@ -98,22 +98,41 @@ delay })` in `httpClientFetch`'s pipe, per the existing header comment's own
## Acceptance criteria
- [ ] Every backend log line for a given request shares one correlation id (not
- [x] Every backend log line for a given request shares one correlation id (not
just lines inside `Submit`); the id is echoed in the response headers.
- [ ] Replaying a submit with the same `Idempotency-Key` returns the same result
without minting a second reference (backend test proves this).
- [ ] A logical wizard submit generates exactly one `Idempotency-Key`, reused across
Verified manually: `LogBrief`'s log line — which never interpolates a `Cid`
itself — now prints `=> CorrelationId:scope-check-5` from the middleware's
`BeginScope`, and `EndpointTests.Correlation_id_supplied_by_the_caller_is_echoed_back`
/ `..._is_generated_when_the_caller_omits_it` cover the response header.
- [x] Replaying a submit with the same `Idempotency-Key` returns the same result
without minting a second reference (backend test proves this —
`IdempotencyTests`, 3 cases: same key twice, different keys, a replayed
rejection).
- [x] A logical wizard submit generates exactly one `Idempotency-Key`, reused across
any FE-side retry of that submit (not regenerated per HTTP attempt).
- [ ] GET requests retry on transient failure (e.g. simulated via `?scenario=slow`
or a forced 5xx); POST/PUT/DELETE never auto-retry.
`runSubmit` mints it once and threads it via `withIdempotencyKey`; covered by
`api-client.provider.spec.ts`.
- [x] GET requests retry on transient failure (e.g. simulated via `?scenario=slow`
or a forced 5xx); POST/PUT/DELETE never auto-retry. Covered by
`api-client.provider.spec.ts` (3 retries on GET, 1 attempt on POST).
## Verification
GREEN + `cd backend && dotnet test`. Manual: `?scenario=error` on a GET-backed page,
confirm a retry attempt happens (network tab shows 2 requests) before the error
state renders; submit a wizard twice with a manually replayed idempotency key (e.g.
via curl against the backend directly) and confirm the second call doesn't create a
duplicate application.
GREEN + `cd backend && dotnet test` (84 passing) — done. Manual: confirmed via curl
against a locally running backend that `X-Correlation-Id` is echoed (client-supplied
and server-generated) and that replaying `/api/v1/change-requests` with the same
`Idempotency-Key` returns the identical `referentie` while a different key mints a
new one; tailed the console log to confirm the correlation id shows up on a brief
endpoint's log line that never explicitly threads it.
**Deviation**: the `?scenario=error` network-tab check this section originally
proposed doesn't actually work — `scenario.interceptor.ts`'s `error` case
`throwError`s before ever calling `next(req)`, so no real request reaches the
network stack and devtools shows nothing to retry. Automated tests
(`api-client.provider.spec.ts`, using a fake `HttpClient`-shaped `.request()` so no
TestBed/HttpClientTestingModule is needed) are the actual proof of the retry
mechanism instead — a more reliable check than a manual browser pass would have
been anyway.
## Out of scope