Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,11 @@ Consumer territories must apply per-call timeouts at instantiation OR rely on th
- **Success path** (request/response loops): a throw turns a resolved 200 into a **rejected** promise.
- **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.

**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.
**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.

**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.
**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.

**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.
**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.

## Packages (12)

Expand Down
111 changes: 65 additions & 46 deletions docs/packages/http.md

Large diffs are not rendered by default.

26 changes: 13 additions & 13 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions packages/adapter-store/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@script-development/fs-adapter-store",
"version": "0.3.1",
"version": "0.3.2",
"description": "Reactive adapter-store pattern with domain state management and CRUD resource adapters",
"homepage": "https://packages.script.nl/packages/adapter-store",
"license": "MIT",
Expand Down Expand Up @@ -43,15 +43,15 @@
},
"devDependencies": {
"@script-development/fs-helpers": "^0.1.0",
"@script-development/fs-http": "^0.1.0 || ^0.2.0 || ^0.3.0 || ^0.4.0 || ^0.5.0",
"@script-development/fs-http": "^0.1.0 || ^0.2.0 || ^0.3.0 || ^0.4.0 || ^0.5.0 || ^0.6.0",
"@script-development/fs-loading": "^0.1.0",
"@script-development/fs-storage": "^0.1.0",
"happy-dom": "^20.10.3",
"vue": "^3.5.39"
},
"peerDependencies": {
"@script-development/fs-helpers": "^0.1.0",
"@script-development/fs-http": "^0.1.0 || ^0.2.0 || ^0.3.0 || ^0.4.0 || ^0.5.0",
"@script-development/fs-http": "^0.1.0 || ^0.2.0 || ^0.3.0 || ^0.4.0 || ^0.5.0 || ^0.6.0",
"@script-development/fs-loading": "^0.1.0",
"@script-development/fs-storage": "^0.1.0",
"vue": "^3.5.39"
Expand Down
6 changes: 3 additions & 3 deletions packages/cached-adapter-store/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@script-development/fs-cached-adapter-store",
"version": "0.2.4",
"version": "0.2.5",
"description": "Higher-order factory wrapping @script-development/fs-adapter-store with hash-bumping cache-check that suppresses redundant retrieveAll GETs at source",
"homepage": "https://packages.script.nl/packages/cached-adapter-store",
"license": "MIT",
Expand Down Expand Up @@ -43,15 +43,15 @@
},
"devDependencies": {
"@script-development/fs-adapter-store": "^0.1.0 || ^0.2.0 || ^0.3.0",
"@script-development/fs-http": "^0.1.0 || ^0.2.0 || ^0.3.0 || ^0.4.0 || ^0.5.0",
"@script-development/fs-http": "^0.1.0 || ^0.2.0 || ^0.3.0 || ^0.4.0 || ^0.5.0 || ^0.6.0",
"@script-development/fs-storage": "^0.1.0",
"axios": "^1.18.1",
"happy-dom": "^20.10.3",
"vue": "^3.5.39"
},
"peerDependencies": {
"@script-development/fs-adapter-store": "^0.1.0 || ^0.2.0 || ^0.3.0",
"@script-development/fs-http": "^0.1.0 || ^0.2.0 || ^0.3.0 || ^0.4.0 || ^0.5.0",
"@script-development/fs-http": "^0.1.0 || ^0.2.0 || ^0.3.0 || ^0.4.0 || ^0.5.0 || ^0.6.0",
"@script-development/fs-storage": "^0.1.0",
"vue": "^3.5.39"
},
Expand Down
6 changes: 3 additions & 3 deletions packages/form/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@script-development/fs-form",
"version": "0.1.0",
"version": "0.1.1",
"description": "Reactive form-submit helpers: double-submit guard plus 422 validation-error binding for fs-http",
"homepage": "https://packages.script.nl/packages/form",
"license": "MIT",
Expand Down Expand Up @@ -42,14 +42,14 @@
"typecheck": "tsc --noEmit"
},
"devDependencies": {
"@script-development/fs-http": "^0.5.0",
"@script-development/fs-http": "^0.5.0 || ^0.6.0",
"@vue/test-utils": "^2.4.11",
"axios": "^1.18.1",
"happy-dom": "^20.10.3",
"vue": "^3.5.39"
},
"peerDependencies": {
"@script-development/fs-http": "^0.5.0",
"@script-development/fs-http": "^0.5.0 || ^0.6.0",
"vue": "^3.5.39"
},
"engines": {
Expand Down
11 changes: 11 additions & 0 deletions packages/http/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
# @script-development/fs-http

## 0.6.0 — 2026-07-09

### Minor Changes

- **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).
- **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.
- **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.
- `guarded()` remains a public export for the `{ guard: false }` + manual-wrap case and for consumers on older fs-http.
- New exported type: `RegisterMiddlewareOptions`.
- Existing explicit `guarded()` wraps at consumers become redundant-but-harmless double-wraps (inner catches, outer never fires) — strip in a later, unhurried cleanup.

## 0.5.0 — 2026-07-02

### Minor Changes
Expand Down
10 changes: 7 additions & 3 deletions packages/http/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ Creates a new HTTP service instance.
- `withXSRFToken` — Include XSRF token header (default: `false`)
- `smartCredentials` — Auto-toggle `withCredentials` based on request host matching base URL host (default: `false`)
- `timeout` — Request timeout in milliseconds (default: `30000`). Pass `0` to disable; pass any positive number to override.
- `onMiddlewareError` — Handler (`GuardedMiddlewareErrorHandler`) for a throw from any auto-guarded middleware on this service (default: a loud `console.error`). Must not re-throw.

### Timeout

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

### Middleware

- `registerRequestMiddleware(fn)` — Returns unregister function
- `registerResponseMiddleware(fn)` — Returns unregister function
- `registerResponseErrorMiddleware(fn)` — Returns unregister function
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})`.

- `registerRequestMiddleware(fn, opts?)` — Returns unregister function
- `registerResponseMiddleware(fn, opts?)` — Returns unregister function
- `registerResponseErrorMiddleware(fn, opts?)` — Returns unregister function
- `guarded(fn, onError?)` — Manual middleware-body guard; still exported for the `{guard: false}` + manual-wrap case

### Utilities

Expand Down
2 changes: 1 addition & 1 deletion packages/http/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@script-development/fs-http",
"version": "0.5.0",
"version": "0.6.0",
"description": "Framework-agnostic HTTP service factory with middleware architecture",
"homepage": "https://packages.script.nl/packages/http",
"license": "MIT",
Expand Down
9 changes: 6 additions & 3 deletions packages/http/src/guarded.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,12 @@ const defaultOnError: GuardedMiddlewareErrorHandler = (error) => {
* cannot reject a resolved 200 nor mask the real API error on the error path.
*
* `fs-http` invokes middleware synchronously and un-caught **by design** (the
* library stays loud; the 2026-05-13 rejection of library-side try/catch holds).
* `guarded` is the **consumer-side, opt-in** defense: apply it at the
* registration site. Loud library, defensive consumer.
* library stays loud; the loops are never awaited). Since ADR-0037 the
* `register*Middleware` functions apply `guarded` **by default** — every
* registered body is loud-swallow-protected without the consumer doing
* anything, and a consumer opts *out* per call with `{guard: false}`. This
* export remains public for that opt-out escape hatch, for manual wrapping, and
* for consumers on older fs-http where guarding was opt-in.
*
* All three middleware types (`RequestMiddlewareFunc`, `ResponseMiddlewareFunc`,
* `ResponseErrorMiddlewareFunc`) share the `(arg) => void` shape, so this one
Expand Down
46 changes: 35 additions & 11 deletions packages/http/src/http.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,15 @@ import axios from 'axios';
import type {
HttpService,
HttpServiceOptions,
RegisterMiddlewareOptions,
RequestMiddlewareFunc,
ResponseErrorMiddlewareFunc,
ResponseMiddlewareFunc,
UnregisterMiddleware,
AxiosResponseError,
} from './types';

import {guarded} from './guarded';
import {isAxiosError} from './utils';

/**
Expand Down Expand Up @@ -51,6 +53,11 @@ const parseBaseURL = (baseURL: string): URL => {
export const createHttpService = (baseURL: string, options?: HttpServiceOptions): HttpService => {
const apiUrl = parseBaseURL(baseURL);

// Service-level handler for auto-guarded middleware throws (ADR-0037).
// Passed to guarded() for every registered middleware; undefined ⇒ guarded()'s
// default loud console.error.
const onMiddlewareError = options?.onMiddlewareError;

const http = axios.create({
baseURL: apiUrl.toString(),
withCredentials: options?.withCredentials ?? true,
Expand Down Expand Up @@ -123,23 +130,40 @@ export const createHttpService = (baseURL: string, options?: HttpServiceOptions)
const previewRequest = (endpoint: string, options?: AxiosRequestConfig) =>
http.get<Blob>(endpoint, {...options, responseType: 'blob'});

// Middleware registration
const registerRequestMiddleware = (fn: RequestMiddlewareFunc): UnregisterMiddleware => {
requestMiddleware.push(fn);

return unregister(requestMiddleware, fn);
// Middleware registration. Bodies are wrapped in guarded() by default
// (ADR-0037) so a side-effect throw cannot reject a resolved 200 nor mask
// the real API error. Pass `{guard: false}` to register the raw body
// unguarded (throws propagate — the deliberate escape hatch). The stored
// (possibly wrapped) function is what's pushed AND what unregister removes,
// so reference identity holds.
const registerRequestMiddleware = (
fn: RequestMiddlewareFunc,
opts?: RegisterMiddlewareOptions,
): UnregisterMiddleware => {
const middleware = opts?.guard === false ? fn : guarded(fn, onMiddlewareError);
requestMiddleware.push(middleware);

return unregister(requestMiddleware, middleware);
};

const registerResponseMiddleware = (fn: ResponseMiddlewareFunc): UnregisterMiddleware => {
responseMiddleware.push(fn);
const registerResponseMiddleware = (
fn: ResponseMiddlewareFunc,
opts?: RegisterMiddlewareOptions,
): UnregisterMiddleware => {
const middleware = opts?.guard === false ? fn : guarded(fn, onMiddlewareError);
responseMiddleware.push(middleware);

return unregister(responseMiddleware, fn);
return unregister(responseMiddleware, middleware);
};

const registerResponseErrorMiddleware = (fn: ResponseErrorMiddlewareFunc): UnregisterMiddleware => {
responseErrorMiddleware.push(fn);
const registerResponseErrorMiddleware = (
fn: ResponseErrorMiddlewareFunc,
opts?: RegisterMiddlewareOptions,
): UnregisterMiddleware => {
const middleware = opts?.guard === false ? fn : guarded(fn, onMiddlewareError);
responseErrorMiddleware.push(middleware);

return unregister(responseErrorMiddleware, fn);
return unregister(responseErrorMiddleware, middleware);
};

return {
Expand Down
1 change: 1 addition & 0 deletions packages/http/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export type {GuardedMiddlewareErrorHandler} from './guarded';
export type {
HttpService,
HttpServiceOptions,
RegisterMiddlewareOptions,
RequestMiddlewareFunc,
ResponseMiddlewareFunc,
ResponseErrorMiddlewareFunc,
Expand Down
Loading