Skip to content

Commit 774ef58

Browse files
Goosterhofclaude
andcommitted
feat(http): guard middleware by default (ADR-0037) — fs-http 0.6.0
register{Request,Response,ResponseError}Middleware now wrap the supplied body in guarded() internally, so a side-effect throw cannot reject a resolved 200 nor mask the real API error — without the consumer doing anything. Per-call {guard:false} opt-out registers the raw body (throws propagate); createHttpService(url, {onMiddlewareError}) routes the loud signal for every auto-guarded middleware on the service. The stored (possibly wrapped) fn is both pushed and what unregister removes (reference identity). Interceptor loops stay sync-only and un-awaited. Supersedes the consumer-side guard obligation in Architectural Principle #8 (loud-swallow serves "be loud, never silent" strictly better than a loud throw, which still corrupts the request outcome). - fs-http 0.5.0 -> 0.6.0 (minor) - peer cascade widened + patch-bumped: fs-loading 0.1.5->0.1.6, fs-adapter-store 0.3.1->0.3.2, fs-cached-adapter-store 0.2.4->0.2.5, fs-form 0.1.0->0.1.1 (all add ^0.6.0) - new export: RegisterMiddlewareOptions; onMiddlewareError service option - doctrine: fs-packages CLAUDE.md § Middleware Sync Contract flipped to guard-by-default; http docs + README + CHANGELOG updated - tests: default-guard swallow+surface, onMiddlewareError routing, {guard:false} propagation, explicit {guard:true}, unregister reference-identity (guarded + raw). 100% coverage, 97.47% mutation. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016K2oPf27cMgbUXWd48fmKX
1 parent b57f6bc commit 774ef58

16 files changed

Lines changed: 354 additions & 100 deletions

File tree

CLAUDE.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,11 +38,11 @@ Consumer territories must apply per-call timeouts at instantiation OR rely on th
3838
- **Success path** (request/response loops): a throw turns a resolved 200 into a **rejected** promise.
3939
- **Error path** (response-error loop): a throw at `:93` propagates *before* the `Promise.reject(error)` at `:95`, **masking the real API error** with the middleware's own throw.
4040

41-
**Design stance — the library stays sync-only and loud.** A library-side try/catch was **rejected by the Commander 2026-05-13** (silent middleware failure is worse than a loud throw) and the rejection **HELD at n=2 on 2026-06-15**. fs-http does not swallow middleware throws and does not await middleware returns — by deliberate disposition, not omission.
41+
**Design stance — the library stays sync-only and loud, and now guards by default (ADR-0037).** A library-side try/catch that *silently* swallowed was **rejected by the Commander 2026-05-13** (silent middleware failure is worse than a loud throw) and the rejection **HELD at n=2 on 2026-06-15**. ADR-0037 (Accepted 2026-07-08, shipped in fs-http **0.6.0**) is **not** a reversal of that value: `guarded()` is *loud swallow* — it surfaces the failure (`console.error` / a service handler) *and* lets the request complete correctly. That third option did not exist in 2026-05-13; a loud throw still corrupts the request outcome, so loud-swallow serves the "be loud, never silent" value strictly better. The interceptor loops remain **sync-only and un-awaited** — async middleware stays out of contract; `guarded()` is sync.
4242

43-
**Consumer contract — the guard lives at the consumer boundary.** Every fs-http consumer **MUST** wrap its own middleware bodies (try/catch + fail-safe swallow) so a side-effect throw — a toast, a store write, a cache-hash parse — cannot reject the request promise or mask an API error. Loud library, defensive consumer.
43+
**Contract — the library guards middleware by default; consumers opt *out*.** `registerRequestMiddleware` / `registerResponseMiddleware` / `registerResponseErrorMiddleware` wrap the supplied body in `guarded()` internally, so a side-effect throw — a toast, a store write, a cache-hash parse — cannot reject the request promise or mask an API error, with the consumer doing nothing. A consumer opts out per call with `register*Middleware(fn, {guard: false})` (raw body, throws propagate — the deliberate escape hatch, no known consumer today). Route the loud signal via `createHttpService(url, {onMiddlewareError})` (unset ⇒ the default loud `console.error`). `guarded()` stays exported for the `{guard: false}` + manual-wrap case and for consumers on older fs-http.
4444

45-
**Precedent + latency.** kendo WR-0078 (PR [#1538](https://github.com/script-development/kendo/pull/1538)) independently re-derived this mechanism against fs-http source and guarded **both** kendo central + tenant middleware (try/catch + fail-safe swallow, 100% coverage, 2026-06-15). See the war-room `deferred.md [adr] fs-packages-fs-http-async-aware-middleware-rejection-doctrine` entry (promoted, n=2). **entreezuil / ublgenie / emmie / BIO carry the latent exposure** until their middleware is likewise guarded.
45+
**Precedent + residue.** The prior consumer-side obligation ("every consumer MUST wrap its own middleware bodies") is **superseded** by the default guard. kendo WR-0078 (PR [#1538](https://github.com/script-development/kendo/pull/1538), 2026-06-15) and the WR-0290 fleet wave hand-wrapped consumer middleware under the old contract; those explicit `guarded()`/try-catch wraps (and fs-form's own `guarded()`) become **redundant-but-harmless double-wraps** (inner catches, outer auto-guard never fires) — strip in a later, unhurried cleanup, not a blocker. See the war-room `deferred.md [adr] fs-packages-fs-http-async-aware-middleware-rejection-doctrine` entry and ADR-0037.
4646

4747
## Packages (12)
4848

docs/packages/http.md

Lines changed: 65 additions & 46 deletions
Large diffs are not rendered by default.

package-lock.json

Lines changed: 13 additions & 13 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/adapter-store/package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@script-development/fs-adapter-store",
3-
"version": "0.3.1",
3+
"version": "0.3.2",
44
"description": "Reactive adapter-store pattern with domain state management and CRUD resource adapters",
55
"homepage": "https://packages.script.nl/packages/adapter-store",
66
"license": "MIT",
@@ -43,15 +43,15 @@
4343
},
4444
"devDependencies": {
4545
"@script-development/fs-helpers": "^0.1.0",
46-
"@script-development/fs-http": "^0.1.0 || ^0.2.0 || ^0.3.0 || ^0.4.0 || ^0.5.0",
46+
"@script-development/fs-http": "^0.1.0 || ^0.2.0 || ^0.3.0 || ^0.4.0 || ^0.5.0 || ^0.6.0",
4747
"@script-development/fs-loading": "^0.1.0",
4848
"@script-development/fs-storage": "^0.1.0",
4949
"happy-dom": "^20.10.3",
5050
"vue": "^3.5.39"
5151
},
5252
"peerDependencies": {
5353
"@script-development/fs-helpers": "^0.1.0",
54-
"@script-development/fs-http": "^0.1.0 || ^0.2.0 || ^0.3.0 || ^0.4.0 || ^0.5.0",
54+
"@script-development/fs-http": "^0.1.0 || ^0.2.0 || ^0.3.0 || ^0.4.0 || ^0.5.0 || ^0.6.0",
5555
"@script-development/fs-loading": "^0.1.0",
5656
"@script-development/fs-storage": "^0.1.0",
5757
"vue": "^3.5.39"

packages/cached-adapter-store/package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@script-development/fs-cached-adapter-store",
3-
"version": "0.2.4",
3+
"version": "0.2.5",
44
"description": "Higher-order factory wrapping @script-development/fs-adapter-store with hash-bumping cache-check that suppresses redundant retrieveAll GETs at source",
55
"homepage": "https://packages.script.nl/packages/cached-adapter-store",
66
"license": "MIT",
@@ -43,15 +43,15 @@
4343
},
4444
"devDependencies": {
4545
"@script-development/fs-adapter-store": "^0.1.0 || ^0.2.0 || ^0.3.0",
46-
"@script-development/fs-http": "^0.1.0 || ^0.2.0 || ^0.3.0 || ^0.4.0 || ^0.5.0",
46+
"@script-development/fs-http": "^0.1.0 || ^0.2.0 || ^0.3.0 || ^0.4.0 || ^0.5.0 || ^0.6.0",
4747
"@script-development/fs-storage": "^0.1.0",
4848
"axios": "^1.18.1",
4949
"happy-dom": "^20.10.3",
5050
"vue": "^3.5.39"
5151
},
5252
"peerDependencies": {
5353
"@script-development/fs-adapter-store": "^0.1.0 || ^0.2.0 || ^0.3.0",
54-
"@script-development/fs-http": "^0.1.0 || ^0.2.0 || ^0.3.0 || ^0.4.0 || ^0.5.0",
54+
"@script-development/fs-http": "^0.1.0 || ^0.2.0 || ^0.3.0 || ^0.4.0 || ^0.5.0 || ^0.6.0",
5555
"@script-development/fs-storage": "^0.1.0",
5656
"vue": "^3.5.39"
5757
},

packages/form/package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@script-development/fs-form",
3-
"version": "0.1.0",
3+
"version": "0.1.1",
44
"description": "Reactive form-submit helpers: double-submit guard plus 422 validation-error binding for fs-http",
55
"homepage": "https://packages.script.nl/packages/form",
66
"license": "MIT",
@@ -42,14 +42,14 @@
4242
"typecheck": "tsc --noEmit"
4343
},
4444
"devDependencies": {
45-
"@script-development/fs-http": "^0.5.0",
45+
"@script-development/fs-http": "^0.5.0 || ^0.6.0",
4646
"@vue/test-utils": "^2.4.11",
4747
"axios": "^1.18.1",
4848
"happy-dom": "^20.10.3",
4949
"vue": "^3.5.39"
5050
},
5151
"peerDependencies": {
52-
"@script-development/fs-http": "^0.5.0",
52+
"@script-development/fs-http": "^0.5.0 || ^0.6.0",
5353
"vue": "^3.5.39"
5454
},
5555
"engines": {

packages/http/CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,16 @@
11
# @script-development/fs-http
22

3+
## 0.6.0 — 2026-07-09
4+
5+
### Minor Changes
6+
7+
- **Middleware is now guarded by default (ADR-0037).** `registerRequestMiddleware`, `registerResponseMiddleware`, and `registerResponseErrorMiddleware` wrap the supplied body in `guarded()` internally, so a side-effect throw (toast, store write, router push, cache-hash parse) can no longer reject a resolved 200 nor mask the real API error on the error path — without the consumer doing anything. This supersedes the consumer-side obligation to hand-wrap every middleware body (war-room Architectural Principle #8, "every consumer MUST wrap its own middleware bodies"). The library stays **loud** (surfaces via `console.error` or a service-level handler, never silent) and **sync-only** (the interceptor loops are unchanged and un-awaited; async middleware remains out of contract).
8+
- **Per-call opt-out:** `register*Middleware(fn, { guard: false })` registers the raw body unguarded (throws propagate) — the deliberate escape hatch. No known consumer today; kept as insurance against a one-way door.
9+
- **Service-level error routing:** `createHttpService(baseURL, { onMiddlewareError })` sets the handler passed to the internal `guarded()` for every middleware on that service, so a consumer can route the swallowed failure to an error tracker instead of `console.error`. Unset ⇒ the default loud `console.error` stands.
10+
- `guarded()` remains a public export for the `{ guard: false }` + manual-wrap case and for consumers on older fs-http.
11+
- New exported type: `RegisterMiddlewareOptions`.
12+
- Existing explicit `guarded()` wraps at consumers become redundant-but-harmless double-wraps (inner catches, outer never fires) — strip in a later, unhurried cleanup.
13+
314
## 0.5.0 — 2026-07-02
415

516
### Minor Changes

packages/http/README.md

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ Creates a new HTTP service instance.
4141
- `withXSRFToken` — Include XSRF token header (default: `false`)
4242
- `smartCredentials` — Auto-toggle `withCredentials` based on request host matching base URL host (default: `false`)
4343
- `timeout` — Request timeout in milliseconds (default: `30000`). Pass `0` to disable; pass any positive number to override.
44+
- `onMiddlewareError` — Handler (`GuardedMiddlewareErrorHandler`) for a throw from any auto-guarded middleware on this service (default: a loud `console.error`). Must not re-throw.
4445

4546
### Timeout
4647

@@ -62,9 +63,12 @@ For Laravel Sanctum SPA consumers, `withXSRFToken: true` is required to avoid HT
6263

6364
### Middleware
6465

65-
- `registerRequestMiddleware(fn)` — Returns unregister function
66-
- `registerResponseMiddleware(fn)` — Returns unregister function
67-
- `registerResponseErrorMiddleware(fn)` — Returns unregister function
66+
Every registered middleware body is wrapped in `guarded()` **by default** (ADR-0037, since 0.6.0) so a side-effect throw cannot reject a resolved 200 nor mask the real API error. Pass `{guard: false}` as the second argument to register the raw body unguarded (throws propagate). Route the loud signal via `createHttpService(url, {onMiddlewareError})`.
67+
68+
- `registerRequestMiddleware(fn, opts?)` — Returns unregister function
69+
- `registerResponseMiddleware(fn, opts?)` — Returns unregister function
70+
- `registerResponseErrorMiddleware(fn, opts?)` — Returns unregister function
71+
- `guarded(fn, onError?)` — Manual middleware-body guard; still exported for the `{guard: false}` + manual-wrap case
6872

6973
### Utilities
7074

packages/http/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@script-development/fs-http",
3-
"version": "0.5.0",
3+
"version": "0.6.0",
44
"description": "Framework-agnostic HTTP service factory with middleware architecture",
55
"homepage": "https://packages.script.nl/packages/http",
66
"license": "MIT",

packages/http/src/guarded.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,12 @@ const defaultOnError: GuardedMiddlewareErrorHandler = (error) => {
2020
* cannot reject a resolved 200 nor mask the real API error on the error path.
2121
*
2222
* `fs-http` invokes middleware synchronously and un-caught **by design** (the
23-
* library stays loud; the 2026-05-13 rejection of library-side try/catch holds).
24-
* `guarded` is the **consumer-side, opt-in** defense: apply it at the
25-
* registration site. Loud library, defensive consumer.
23+
* library stays loud; the loops are never awaited). Since ADR-0037 the
24+
* `register*Middleware` functions apply `guarded` **by default** — every
25+
* registered body is loud-swallow-protected without the consumer doing
26+
* anything, and a consumer opts *out* per call with `{guard: false}`. This
27+
* export remains public for that opt-out escape hatch, for manual wrapping, and
28+
* for consumers on older fs-http where guarding was opt-in.
2629
*
2730
* All three middleware types (`RequestMiddlewareFunc`, `ResponseMiddlewareFunc`,
2831
* `ResponseErrorMiddlewareFunc`) share the `(arg) => void` shape, so this one

0 commit comments

Comments
 (0)