@cedvict/http-guardian 0.0.1-next.10 → 0.0.1-next.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +6 -0
- package/README.md +36 -0
- package/dist/cache/key.d.ts +1 -0
- package/dist/cache/key.d.ts.map +1 -1
- package/dist/client/httpClient.d.ts.map +1 -1
- package/dist/client/types.d.ts +2 -0
- package/dist/client/types.d.ts.map +1 -1
- package/dist/index.cjs +31 -9
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +31 -9
- package/dist/index.js.map +1 -1
- package/package.json +7 -3
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,12 @@ The format is based on *Keep a Changelog*, and this project adheres to *Semantic
|
|
|
6
6
|
|
|
7
7
|
## Unreleased
|
|
8
8
|
|
|
9
|
+
### Binary responses
|
|
10
|
+
|
|
11
|
+
- Add explicit `blob` and `arrayBuffer` response types for lossless downloads.
|
|
12
|
+
- Keep structured JSON error envelopes readable on binary requests.
|
|
13
|
+
- Separate cache and in-flight deduplication keys by response type.
|
|
14
|
+
|
|
9
15
|
### Contract (security)
|
|
10
16
|
|
|
11
17
|
- Refresh single-flight: concurrent 401s trigger only one refresh request.
|
package/README.md
CHANGED
|
@@ -30,6 +30,30 @@ if (res.ok) console.log(res.data.name);
|
|
|
30
30
|
else console.error(res.errors);
|
|
31
31
|
```
|
|
32
32
|
|
|
33
|
+
## Binary responses
|
|
34
|
+
|
|
35
|
+
Use an explicit response type for downloads. Successful responses bypass the
|
|
36
|
+
JSON envelope parser so their bytes stay intact. Error responses still use the
|
|
37
|
+
normal content-type decoder, which preserves structured API errors.
|
|
38
|
+
|
|
39
|
+
```ts
|
|
40
|
+
const report = await api.get<Blob>("/reports/monthly", {
|
|
41
|
+
responseType: "blob",
|
|
42
|
+
headers: { accept: "application/pdf" },
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
if (report.ok) {
|
|
46
|
+
const url = URL.createObjectURL(report.data);
|
|
47
|
+
// Open or download the URL, then call URL.revokeObjectURL(url).
|
|
48
|
+
} else {
|
|
49
|
+
console.error(report.errors);
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
`responseType: "arrayBuffer"` is also available for byte-oriented processing.
|
|
54
|
+
Binary requests do not receive the default `Accept: application/json` header,
|
|
55
|
+
and cache/deduplication keys keep `blob` and `arrayBuffer` responses separate.
|
|
56
|
+
|
|
33
57
|
## Presets
|
|
34
58
|
- `createApiParserRestClassic()`
|
|
35
59
|
- `createApiParserLaravel()`
|
|
@@ -100,6 +124,18 @@ entre retries** (mémorisé sur le ctx). Si la réponse contient le même header
|
|
|
100
124
|
il est copié dans `ctx.meta.correlationId` pour les plugins downstream
|
|
101
125
|
(logger, sentry, otel).
|
|
102
126
|
|
|
127
|
+
> 💡 **Pattern "X-Request-Id"** : si ton backend (ops, ELK, Datadog…) utilise
|
|
128
|
+
> le header `X-Request-Id` plutôt que `X-Correlation-Id` pour stitcher les
|
|
129
|
+
> logs front/back, change juste `headerName` :
|
|
130
|
+
>
|
|
131
|
+
> ```ts
|
|
132
|
+
> correlationIdGuard({ headerName: "X-Request-Id" })
|
|
133
|
+
> ```
|
|
134
|
+
>
|
|
135
|
+
> Pas besoin d'OpenTelemetry côté front, ni d'un second guard : c'est le même
|
|
136
|
+
> mécanisme avec un nom différent. Combinable avec un OTel `traceparent` :
|
|
137
|
+
> les deux headers coexistent sans conflit.
|
|
138
|
+
|
|
103
139
|
### `idempotencyGuard({ headerName?, applyTo?, respectExisting? })`
|
|
104
140
|
|
|
105
141
|
Injecte un header `X-Idempotency-Key` (UUID par défaut) sur `POST`/`PUT`/`PATCH`.
|
package/dist/cache/key.d.ts
CHANGED
package/dist/cache/key.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"key.d.ts","sourceRoot":"","sources":["../../src/cache/key.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,gCAAgC,CAAC;AAEtE,wBAAgB,YAAY,CAAC,KAAK,EAAE;IAClC,MAAM,EAAE,MAAM,CAAC;IACf,GAAG,EAAE,MAAM,CAAC;IAGZ,OAAO,CAAC,EAAE,OAAO,GAAG,eAAe,CAAC;IACpC,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB,GAAG,MAAM,
|
|
1
|
+
{"version":3,"file":"key.d.ts","sourceRoot":"","sources":["../../src/cache/key.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,gCAAgC,CAAC;AAEtE,wBAAgB,YAAY,CAAC,KAAK,EAAE;IAClC,MAAM,EAAE,MAAM,CAAC;IACf,GAAG,EAAE,MAAM,CAAC;IAGZ,OAAO,CAAC,EAAE,OAAO,GAAG,eAAe,CAAC;IACpC,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB,GAAG,MAAM,CAmBT"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"httpClient.d.ts","sourceRoot":"","sources":["../../src/client/httpClient.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,cAAc,EAAE,YAAY,
|
|
1
|
+
{"version":3,"file":"httpClient.d.ts","sourceRoot":"","sources":["../../src/client/httpClient.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,cAAc,EAAE,YAAY,EAA4B,MAAM,YAAY,CAAC;AA2C5G,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,iBAAiB;UAuOjD,CAAC,QAAQ,MAAM,OAAO,cAAc,CAAC,CAAC,CAAC;WACtC,CAAC,QAAQ,MAAM,OAAO,cAAc,CAAC,CAAC,CAAC;UACxC,CAAC,QAAQ,MAAM,OAAO,cAAc,CAAC,CAAC,CAAC;YACrC,CAAC,QAAQ,MAAM,OAAO,cAAc,CAAC,CAAC,CAAC;aACtC,CAAC,QAAQ,MAAM,OAAO,cAAc,CAAC,CAAC,CAAC;;+BAChB,MAAM;;;EAEzC"}
|
package/dist/client/types.d.ts
CHANGED
|
@@ -8,6 +8,7 @@ import type { AppError } from "./errors.js";
|
|
|
8
8
|
import type { HttpPlugin } from "../plugins/types.js";
|
|
9
9
|
export type HttpMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
|
|
10
10
|
export type RedirectMode = "follow" | "manual" | "error";
|
|
11
|
+
export type ResponseType = "auto" | "blob" | "arrayBuffer";
|
|
11
12
|
export type RedirectPolicy = {
|
|
12
13
|
mode: RedirectMode;
|
|
13
14
|
maxHops?: number;
|
|
@@ -43,6 +44,7 @@ export type RequestOptions<T> = {
|
|
|
43
44
|
timeoutMs?: number;
|
|
44
45
|
cache?: CacheOptions;
|
|
45
46
|
dataSchema?: Schema<T>;
|
|
47
|
+
responseType?: ResponseType;
|
|
46
48
|
notifier?: Notifier;
|
|
47
49
|
noAuth?: boolean;
|
|
48
50
|
idempotencyKey?: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/client/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AACxD,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,oBAAoB,CAAC;AAChD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AACpD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,2BAA2B,CAAC;AAC7D,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,6BAA6B,CAAC;AAC1D,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AACnD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAC5C,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAC;AAEtD,MAAM,MAAM,UAAU,GAAG,KAAK,GAAG,MAAM,GAAG,KAAK,GAAG,OAAO,GAAG,QAAQ,CAAC;AACrE,MAAM,MAAM,YAAY,GAAG,QAAQ,GAAG,QAAQ,GAAG,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/client/types.ts"],"names":[],"mappings":"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"}
|
package/dist/index.cjs
CHANGED
|
@@ -225,7 +225,8 @@ function makeCacheKey(parts) {
|
|
|
225
225
|
u: parts.url,
|
|
226
226
|
h: headerObj,
|
|
227
227
|
b: parts.body ?? null,
|
|
228
|
-
i: parts.idempotencyKey ?? null
|
|
228
|
+
i: parts.idempotencyKey ?? null,
|
|
229
|
+
r: parts.responseType ?? "auto"
|
|
229
230
|
});
|
|
230
231
|
}
|
|
231
232
|
|
|
@@ -375,6 +376,8 @@ function createHttpClient(options) {
|
|
|
375
376
|
const pipeline = composeGuards(baseGuards, terminal);
|
|
376
377
|
async function request(method, path, ro) {
|
|
377
378
|
const requestUrl = withQuery(joinUrl(draft.baseUrl, path), ro?.query);
|
|
379
|
+
const responseType = ro?.responseType ?? "auto";
|
|
380
|
+
const expectsBinary = isBinaryResponseType(responseType);
|
|
378
381
|
const headers = createHeaders();
|
|
379
382
|
if (draft.defaults?.headers) for (const [k, v] of Object.entries(draft.defaults.headers)) headers.set(k, v);
|
|
380
383
|
if (ro?.headers) {
|
|
@@ -384,15 +387,15 @@ function createHttpClient(options) {
|
|
|
384
387
|
let bodyForKey = void 0;
|
|
385
388
|
if (ro?.json !== void 0) {
|
|
386
389
|
if (!headers.has("content-type")) headers.set("content-type", "application/json");
|
|
387
|
-
if (!headers.has("accept")) headers.set("accept", "application/json");
|
|
390
|
+
if (!headers.has("accept") && !expectsBinary) headers.set("accept", "application/json");
|
|
388
391
|
body = JSON.stringify(ro.json);
|
|
389
392
|
bodyForKey = ro.json;
|
|
390
393
|
} else if (ro?.body !== void 0) {
|
|
391
394
|
body = ro.body;
|
|
392
395
|
bodyForKey = "[body]";
|
|
393
|
-
if (!headers.has("accept")) headers.set("accept", "application/json");
|
|
396
|
+
if (!headers.has("accept") && !expectsBinary) headers.set("accept", "application/json");
|
|
394
397
|
} else {
|
|
395
|
-
if (!headers.has("accept")) headers.set("accept", "application/json");
|
|
398
|
+
if (!headers.has("accept") && !expectsBinary) headers.set("accept", "application/json");
|
|
396
399
|
}
|
|
397
400
|
const timeoutMs = ro?.timeoutMs ?? draft.defaults?.timeoutMs;
|
|
398
401
|
const notifier = ro?.notifier ?? defaultNotifier;
|
|
@@ -411,6 +414,7 @@ function createHttpClient(options) {
|
|
|
411
414
|
headers,
|
|
412
415
|
...method === "GET" ? {} : { body: bodyForKey },
|
|
413
416
|
...ro?.idempotencyKey !== void 0 ? { idempotencyKey: ro.idempotencyKey } : {},
|
|
417
|
+
responseType,
|
|
414
418
|
...cacheOpts?.key !== void 0 ? { keyOverride: cacheOpts.key } : {}
|
|
415
419
|
}) : void 0;
|
|
416
420
|
if (inflightKey) {
|
|
@@ -451,7 +455,7 @@ function createHttpClient(options) {
|
|
|
451
455
|
...timeoutMs !== void 0 ? { timeoutMs } : {},
|
|
452
456
|
...ro?.noAuth ? { noAuth: true } : {}
|
|
453
457
|
});
|
|
454
|
-
const raw = await decodeResponse(res);
|
|
458
|
+
const raw = await decodeResponse(res, responseType);
|
|
455
459
|
const envelope = { kind: "network", url: res.url || requestUrl, status: res.status, headers: res.headers, raw };
|
|
456
460
|
if (wantsCache && cacheOpts && res.ok) {
|
|
457
461
|
const key = inflightKey;
|
|
@@ -477,7 +481,7 @@ function createHttpClient(options) {
|
|
|
477
481
|
...ro?.noAuth ? { noAuth: true } : {}
|
|
478
482
|
});
|
|
479
483
|
if (!res2.ok) return;
|
|
480
|
-
const raw2 = await decodeResponse(res2);
|
|
484
|
+
const raw2 = await decodeResponse(res2, responseType);
|
|
481
485
|
cache.set(key, {
|
|
482
486
|
expiresAt: Date.now() + cacheOpts.ttlMs,
|
|
483
487
|
value: raw2,
|
|
@@ -605,8 +609,25 @@ function makeCtx(args, redirect) {
|
|
|
605
609
|
_fetch: args.resolvedFetch
|
|
606
610
|
};
|
|
607
611
|
}
|
|
608
|
-
|
|
612
|
+
function isBinaryResponseType(responseType) {
|
|
613
|
+
return responseType === "blob" || responseType === "arrayBuffer";
|
|
614
|
+
}
|
|
615
|
+
async function decodeResponse(res, responseType = "auto") {
|
|
609
616
|
if (res.status === 204) return null;
|
|
617
|
+
if (res.ok && responseType === "blob") {
|
|
618
|
+
try {
|
|
619
|
+
return await res.blob();
|
|
620
|
+
} catch (e) {
|
|
621
|
+
throw new ParseError("Failed to read Blob response", void 0, e);
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
if (res.ok && responseType === "arrayBuffer") {
|
|
625
|
+
try {
|
|
626
|
+
return await res.arrayBuffer();
|
|
627
|
+
} catch (e) {
|
|
628
|
+
throw new ParseError("Failed to read ArrayBuffer response", void 0, e);
|
|
629
|
+
}
|
|
630
|
+
}
|
|
610
631
|
const ct = res.headers.get("content-type") ?? "";
|
|
611
632
|
if (ct.includes("application/json")) {
|
|
612
633
|
try {
|
|
@@ -631,9 +652,10 @@ function asClientResult(envelope, requestUrl, opts, ro, notifier, shouldNotify,
|
|
|
631
652
|
const raw = envelope.raw;
|
|
632
653
|
const resHeaders = envelope.headers ?? new Headers();
|
|
633
654
|
const finalUrl = envelope.url ?? requestUrl;
|
|
634
|
-
const
|
|
655
|
+
const binaryResponse = isBinaryResponseType(ro?.responseType);
|
|
656
|
+
const okByParser = binaryResponse ? status >= 200 && status < 400 : opts.parser.isSuccess(raw, status);
|
|
635
657
|
if (okByParser && status >= 200 && status < 400) {
|
|
636
|
-
const dataRaw = opts.parser.getData(raw);
|
|
658
|
+
const dataRaw = binaryResponse ? raw : opts.parser.getData(raw);
|
|
637
659
|
try {
|
|
638
660
|
const data = ro?.dataSchema ? ro.dataSchema.parse(dataRaw) : dataRaw;
|
|
639
661
|
return { ok: true, status, headers: resHeaders, data, raw, url: finalUrl };
|