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
59 changes: 59 additions & 0 deletions docs/packages/http.md
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,58 @@ const unregister = http.registerResponseErrorMiddleware((error) => {
Other packages hook into these middleware points. `fs-loading` registers request + response + error middleware to track loading state. `fs-dialog` can register error middleware to show error dialogs. You can stack as many middleware handlers as you need — they all run independently.
:::

## Middleware guard (`guarded`)

`fs-http` invokes middleware **synchronously and un-caught, by design** — the library stays loud so a bug in a middleware body is never silently eaten inside the transport layer. The consequence: if a middleware body throws (a toast that blows up, a store write, a router push, a `JSON.parse` of a cache hash), that throw escapes into the interceptor chain. On the response path it **rejects a resolved 200**; on the error path it **replaces the original `AxiosError` with the middleware's throw**, masking the real API failure.

`guarded()` is the **consumer-side, opt-in** defense. Wrap a middleware body at its registration site and a throw from the body is caught, reported, and swallowed — the interceptor chain is never corrupted. Loud library, defensive consumer.

```typescript
import {createHttpService, guarded} from '@script-development/fs-http';

const http = createHttpService(`${location.origin}/api`);

// A throwing response body would otherwise reject a resolved 200 — guarded()
// contains it so the successful response still resolves.
http.registerResponseMiddleware(
guarded((response) => {
showToast(`Loaded ${response.config.url}`); // may throw — no longer fatal
}),
);

// A throwing error body would otherwise mask the real AxiosError — guarded()
// contains it so the caller still rejects with the original error.
http.registerResponseErrorMiddleware(
guarded((error) => {
openErrorDialog(error); // may throw — the 500 still surfaces to the caller
}),
);
```

All three middleware types share the `(arg) => void` shape, so one generic wraps any of them and stays assignable to the source type with **zero casts** — `guarded(reqBody)`, `guarded(resBody)`, and `guarded(errBody)` each infer their argument type from the body you pass.

### Custom error handling

By default a swallowed throw is logged loudly via `console.error` (visible to any error tracker that hooks `console`). It is never re-thrown — re-throwing would re-open the exact failure `guarded()` closes. Pass a custom `GuardedMiddlewareErrorHandler` to route the failure elsewhere:

```typescript
import {guarded, type GuardedMiddlewareErrorHandler} from '@script-development/fs-http';

const reportToTracker: GuardedMiddlewareErrorHandler = (error) => {
errorTracker.capture(error); // must not re-throw
};

http.registerResponseMiddleware(
guarded((response) => {
analytics.record(response.status);
}, reportToTracker),
);
```

::: tip Why opt-in, not library-side
Wrapping the interceptor loops in try/catch inside `fs-http` was rejected (2026-05-13): it would swallow every consumer's middleware bug silently, at the library layer, with no way to opt back into loud behaviour. `guarded()` inverts that — the library stays loud, and each consumer decides, explicitly at each registration site, which bodies to protect.
:::

## File Operations

`downloadRequest` and `previewRequest` are **transport-only** — they GET an endpoint as a `Blob` and return the full `AxiosResponse<Blob>`. Neither touches the DOM. There is no browser save dialog and no object-URL management inside fs-http; the consumer owns that orchestration (fs-packages issue #59). The two names share identical transport (`responseType: 'blob'`); the separate name communicates intent (download = save-to-disk, preview = inline-display).
Expand Down Expand Up @@ -263,3 +315,10 @@ try {
| `registerRequestMiddleware(fn)` | `UnregisterMiddleware` |
| `registerResponseMiddleware(fn)` | `UnregisterMiddleware` |
| `registerResponseErrorMiddleware(fn)` | `UnregisterMiddleware` |

### Middleware Guard

| Export | Type | Description |
| ------------------------------- | --------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `guarded(fn, onError?)` | `<T>(fn: (arg: T) => void, onError?) => (arg: T) => void` | Wraps a middleware body so a throw is caught, reported, and swallowed instead of corrupting the interceptor chain. See [Middleware guard](#middleware-guard-guarded). |
| `GuardedMiddlewareErrorHandler` | `(error: unknown) => void` | Handler type for `guarded`'s optional second argument; defaults to a loud `console.error`. Must not re-throw. |
20 changes: 10 additions & 10 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.0",
"version": "0.3.1",
"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",
"@script-development/fs-http": "^0.1.0 || ^0.2.0 || ^0.3.0 || ^0.4.0 || ^0.5.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",
"@script-development/fs-http": "^0.1.0 || ^0.2.0 || ^0.3.0 || ^0.4.0 || ^0.5.0",
"@script-development/fs-loading": "^0.1.0",
"@script-development/fs-storage": "^0.1.0",
"vue": "^3.5.39"
Expand Down
6 changes: 6 additions & 0 deletions packages/cached-adapter-store/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# @script-development/fs-cached-adapter-store

## 0.2.4 — 2026-07-02

### Patch Changes

- **Peer-range widening for `@script-development/fs-http` `^0.5.0`.** `fs-http` published a minor (0.5.0, the additive `guarded()` middleware guard). Pre-1.0 caret semantics require every `fs-http` consumer to widen its accepted range; no behavioural change. Mechanical cascade per fs-packages `CLAUDE.md` § Versioning Discipline.

## 0.2.3 — 2026-06-29

### Patch Changes
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.3",
"version": "0.2.4",
"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",
"@script-development/fs-http": "^0.1.0 || ^0.2.0 || ^0.3.0 || ^0.4.0 || ^0.5.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",
"@script-development/fs-http": "^0.1.0 || ^0.2.0 || ^0.3.0 || ^0.4.0 || ^0.5.0",
"@script-development/fs-storage": "^0.1.0",
"vue": "^3.5.39"
},
Expand Down
10 changes: 10 additions & 0 deletions packages/http/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
# @script-development/fs-http

## 0.5.0 — 2026-07-02

### Minor Changes

- **New export: `guarded()` — consumer-side middleware guard wrapper.** A higher-order function that wraps an `fs-http` middleware body in try/catch so a side-effect throw (toast, store write, router push, cache-hash parse) cannot corrupt the interceptor chain — it can no longer reject a resolved 200 nor mask the original API error on the error path. All three middleware types (`RequestMiddlewareFunc`, `ResponseMiddlewareFunc`, `ResponseErrorMiddlewareFunc`) share the `(arg) => void` shape, so one generic wraps any of them and stays assignable to the source type with zero casts: `service.registerResponseMiddleware(guarded((response) => { ... }))`.
- The library stays **sync-only and loud** — `createHttpService` and the interceptor loops are unchanged (the 2026-05-13 rejection of library-side try/catch holds). `guarded()` is **opt-in at the consumer's registration site**: loud library, defensive consumer.
- The default error handler surfaces the swallowed failure via `console.error` (visible to error trackers) and never re-throws. Pass a custom `GuardedMiddlewareErrorHandler` to route the failure elsewhere.
- Also exports the `GuardedMiddlewareErrorHandler` type.
- Additive and non-breaking — no existing API changed.

## 0.4.1 — 2026-05-29

### Patch Changes
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.4.1",
"version": "0.5.0",
"description": "Framework-agnostic HTTP service factory with middleware architecture",
"homepage": "https://packages.script.nl/packages/http",
"license": "MIT",
Expand Down
53 changes: 53 additions & 0 deletions packages/http/src/guarded.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/**
* Error handler invoked when a middleware body wrapped by {@link guarded} throws.
* Receives the thrown value (typed `unknown`, since a throw can be anything).
* Must not re-throw — doing so re-opens the exact failure `guarded` closes.
*/
export type GuardedMiddlewareErrorHandler = (error: unknown) => void;

/**
* Default handler: surface the swallowed failure loudly (visible to `console`
* and any error tracker that hooks it) without letting it propagate. Loud, not
* silent — a swallowed middleware throw is still a bug the consumer should see.
*/
const defaultOnError: GuardedMiddlewareErrorHandler = (error) => {
console.error('[fs-http] middleware body threw and was swallowed by guarded():', error);
};

/**
* Wrap an `fs-http` middleware body so a side-effect throw (toast, store write,
* router push, cache-hash parse) cannot corrupt the interceptor chain — i.e.
* 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.
*
* All three middleware types (`RequestMiddlewareFunc`, `ResponseMiddlewareFunc`,
* `ResponseErrorMiddlewareFunc`) share the `(arg) => void` shape, so this one
* generic wraps any of them and stays assignable to the source type with zero
* casts:
*
* ```ts
* service.registerResponseMiddleware(guarded((response) => { ...may throw... }));
* ```
*
* @param fn the middleware body to protect.
* @param onError handler for a thrown value; defaults to a loud `console.error`.
* Pass a custom handler to route the failure elsewhere (e.g. an
* error tracker). Do not re-throw from it.
* @returns a middleware function of the same shape that never throws.
*/
export const guarded = <T>(
fn: (arg: T) => void,
onError: GuardedMiddlewareErrorHandler = defaultOnError,
): ((arg: T) => void) => {
return (arg: T) => {
try {
fn(arg);
} catch (error) {
onError(error);
}
};
};
2 changes: 2 additions & 0 deletions packages/http/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
export {DEFAULT_TIMEOUT_MS, createHttpService} from './http';
export {guarded} from './guarded';
export type {GuardedMiddlewareErrorHandler} from './guarded';
export type {
HttpService,
HttpServiceOptions,
Expand Down
Loading