@wooksjs/event-http 0.7.26 → 0.7.28
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/dist/index.cjs +457 -81
- package/dist/index.d.ts +168 -6
- package/dist/index.mjs +456 -83
- package/package.json +6 -6
package/dist/index.mjs
CHANGED
|
@@ -2,11 +2,12 @@ import { EventContext, cached, cachedBy, createEventContext, current, defineEven
|
|
|
2
2
|
import { Buffer as Buffer$1 } from "buffer";
|
|
3
3
|
import { Readable, pipeline } from "node:stream";
|
|
4
4
|
import { promisify } from "node:util";
|
|
5
|
-
import { createBrotliCompress, createBrotliDecompress, createDeflate, createGunzip, createGzip, createInflate } from "node:zlib";
|
|
5
|
+
import { brotliCompress, constants, createBrotliCompress, createBrotliDecompress, createDeflate, createGunzip, createGzip, createInflate, gzip } from "node:zlib";
|
|
6
6
|
import { URLSearchParams } from "url";
|
|
7
7
|
import http, { IncomingMessage, ServerResponse } from "http";
|
|
8
8
|
import { Duplex, Readable as Readable$1, pipeline as pipeline$1 } from "stream";
|
|
9
9
|
import { WooksAdapterBase } from "wooks";
|
|
10
|
+
import { createHash } from "crypto";
|
|
10
11
|
import { Socket } from "net";
|
|
11
12
|
|
|
12
13
|
//#region packages/event-http/src/http-kind.ts
|
|
@@ -67,18 +68,22 @@ const useCookies = defineWook((ctx) => ({
|
|
|
67
68
|
}));
|
|
68
69
|
|
|
69
70
|
//#endregion
|
|
70
|
-
//#region packages/event-http/src/
|
|
71
|
+
//#region packages/event-http/src/utils/accept.ts
|
|
72
|
+
/** Short Accept names mapped to their MIME types. */
|
|
71
73
|
const ACCEPT_TYPE_MAP = {
|
|
72
74
|
json: "application/json",
|
|
73
75
|
html: "text/html",
|
|
74
76
|
xml: "application/xml",
|
|
75
77
|
text: "text/plain"
|
|
76
78
|
};
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
79
|
+
/** Whether an `Accept` header value accepts `type` (a short name from {@link ACCEPT_TYPE_MAP} or a MIME type). */
|
|
80
|
+
function acceptHeaderHas(accept, type) {
|
|
81
|
+
return !!(accept && (accept === "*/*" || accept.includes(ACCEPT_TYPE_MAP[type] || type)));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
//#endregion
|
|
85
|
+
//#region packages/event-http/src/composables/header-accept.ts
|
|
86
|
+
const acceptsMime = cachedBy((type, ctx) => acceptHeaderHas(ctx.get(httpKind.keys.req).headers.accept, type));
|
|
82
87
|
/** Provides helpers to check the request's Accept header for supported MIME types. */
|
|
83
88
|
const useAccept = defineWook((ctx) => {
|
|
84
89
|
return {
|
|
@@ -138,6 +143,19 @@ const useAuthorization = defineWook((ctx) => {
|
|
|
138
143
|
};
|
|
139
144
|
});
|
|
140
145
|
|
|
146
|
+
//#endregion
|
|
147
|
+
//#region packages/event-http/src/composables/headers.ts
|
|
148
|
+
/**
|
|
149
|
+
* Returns the incoming request headers.
|
|
150
|
+
* @example
|
|
151
|
+
* ```ts
|
|
152
|
+
* const { host, authorization } = useHeaders()
|
|
153
|
+
* ```
|
|
154
|
+
*/
|
|
155
|
+
function useHeaders(ctx) {
|
|
156
|
+
return (ctx ?? current()).get(httpKind.keys.req).headers;
|
|
157
|
+
}
|
|
158
|
+
|
|
141
159
|
//#endregion
|
|
142
160
|
//#region packages/event-http/src/compressor/body-compressor.ts
|
|
143
161
|
const compressors = { identity: {
|
|
@@ -387,18 +405,72 @@ const DEFAULT_LIMITS = {
|
|
|
387
405
|
maxRatio: 100,
|
|
388
406
|
readTimeoutMs: 1e4
|
|
389
407
|
};
|
|
408
|
+
const NO_ENCODINGS = Object.freeze([]);
|
|
390
409
|
const contentEncodingsSlot = cached((ctx) => {
|
|
391
|
-
|
|
410
|
+
const contentEncoding = ctx.get(httpKind.keys.req).headers["content-encoding"];
|
|
411
|
+
if (!contentEncoding) return NO_ENCODINGS;
|
|
412
|
+
return contentEncoding.split(",").map((p) => p.trim()).filter((p) => !!p);
|
|
392
413
|
});
|
|
393
414
|
const isCompressedSlot = cached((ctx) => {
|
|
394
|
-
const
|
|
395
|
-
for (const p of parts) if ([
|
|
396
|
-
"deflate",
|
|
397
|
-
"gzip",
|
|
398
|
-
"br"
|
|
399
|
-
].includes(p)) return true;
|
|
415
|
+
for (const p of ctx.get(contentEncodingsSlot)) if (p === "deflate" || p === "gzip" || p === "br") return true;
|
|
400
416
|
return false;
|
|
401
417
|
});
|
|
418
|
+
/**
|
|
419
|
+
* Reads the whole request body with plain stream events (one listener set, one timer).
|
|
420
|
+
* Raw bytes are checked against `upfrontLimit` first, then against `maxInflated`
|
|
421
|
+
* (the same order as the streaming path). A stall longer than `timeoutMs` destroys the
|
|
422
|
+
* request; a premature close or stream error becomes 408. The request is destroyed
|
|
423
|
+
* once reading settles, as async iteration did.
|
|
424
|
+
*/
|
|
425
|
+
function readRawBody(req, upfrontLimit, maxInflated, timeoutMs) {
|
|
426
|
+
return new Promise((resolve, reject) => {
|
|
427
|
+
const chunks = [];
|
|
428
|
+
let bytes = 0;
|
|
429
|
+
if (req.readableEnded) {
|
|
430
|
+
req.destroy();
|
|
431
|
+
resolve(Buffer$1.alloc(0));
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
if (req.destroyed) {
|
|
435
|
+
reject(new HttpError(408, "Request body timeout"));
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
let done = false;
|
|
439
|
+
const timer = timeoutMs === 0 ? null : setTimeout(() => req.destroy(), timeoutMs);
|
|
440
|
+
const finish = (error) => {
|
|
441
|
+
if (done) return;
|
|
442
|
+
done = true;
|
|
443
|
+
if (timer) clearTimeout(timer);
|
|
444
|
+
req.off("data", onData);
|
|
445
|
+
req.off("end", onEnd);
|
|
446
|
+
req.off("error", onAbort);
|
|
447
|
+
req.off("close", onAbort);
|
|
448
|
+
req.destroy();
|
|
449
|
+
if (error) reject(error);
|
|
450
|
+
else resolve(chunks.length === 1 ? chunks[0] : Buffer$1.concat(chunks, bytes));
|
|
451
|
+
};
|
|
452
|
+
const onData = (chunk) => {
|
|
453
|
+
bytes += chunk.length;
|
|
454
|
+
if (bytes > upfrontLimit) {
|
|
455
|
+
finish(new HttpError(413, "Payload Too Large"));
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
if (bytes > maxInflated) {
|
|
459
|
+
finish(new HttpError(413, "Inflated body too large"));
|
|
460
|
+
return;
|
|
461
|
+
}
|
|
462
|
+
timer?.refresh();
|
|
463
|
+
chunks.push(chunk);
|
|
464
|
+
};
|
|
465
|
+
const onEnd = () => finish();
|
|
466
|
+
const onAbort = () => finish(new HttpError(408, "Request body timeout"));
|
|
467
|
+
req.on("data", onData);
|
|
468
|
+
req.on("end", onEnd);
|
|
469
|
+
req.on("error", onAbort);
|
|
470
|
+
req.on("close", onAbort);
|
|
471
|
+
req.resume();
|
|
472
|
+
});
|
|
473
|
+
}
|
|
402
474
|
/** @internal Exported for test pre-seeding — use {@link seedRawBody} instead. */
|
|
403
475
|
const rawBodySlot = cached(async (ctx) => {
|
|
404
476
|
const req = ctx.get(httpKind.keys.req);
|
|
@@ -414,6 +486,14 @@ const rawBodySlot = cached(async (ctx) => {
|
|
|
414
486
|
const upfrontLimit = isZip ? maxCompressed : maxInflated;
|
|
415
487
|
if (cl && cl > upfrontLimit) throw new HttpError(413, "Payload Too Large");
|
|
416
488
|
for (const enc of encs) if (!compressors[enc]) throw new HttpError(415, `Unsupported Content-Encoding "${enc}"`);
|
|
489
|
+
if (!streamable) {
|
|
490
|
+
const raw = await readRawBody(req, upfrontLimit, maxInflated, timeoutMs);
|
|
491
|
+
if (!isZip) return raw;
|
|
492
|
+
const body = await uncompressBody(encs, raw);
|
|
493
|
+
if (body.byteLength > maxInflated) throw new HttpError(413, "Inflated body too large");
|
|
494
|
+
if (raw.byteLength > 0 && body.byteLength / raw.byteLength > maxRatio) throw new HttpError(413, "Compression ratio too high");
|
|
495
|
+
return body;
|
|
496
|
+
}
|
|
417
497
|
let timer = null;
|
|
418
498
|
function resetTimer() {
|
|
419
499
|
if (timeoutMs === 0) return;
|
|
@@ -446,8 +526,7 @@ const rawBodySlot = cached(async (ctx) => {
|
|
|
446
526
|
clearTimer();
|
|
447
527
|
}
|
|
448
528
|
}
|
|
449
|
-
|
|
450
|
-
if (streamable) stream = await uncompressBodyStream(encs, stream);
|
|
529
|
+
const stream = await uncompressBodyStream(encs, limitedCompressed());
|
|
451
530
|
const chunks = [];
|
|
452
531
|
let inflatedBytes = 0;
|
|
453
532
|
try {
|
|
@@ -460,14 +539,8 @@ const rawBodySlot = cached(async (ctx) => {
|
|
|
460
539
|
if (error instanceof HttpError) throw error;
|
|
461
540
|
throw new HttpError(408, "Request body timeout");
|
|
462
541
|
}
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
body = await uncompressBody(encs, body);
|
|
466
|
-
inflatedBytes = body.byteLength;
|
|
467
|
-
if (inflatedBytes > maxInflated) throw new HttpError(413, "Inflated body too large");
|
|
468
|
-
}
|
|
469
|
-
if (isZip && rawBytes > 0 && inflatedBytes / rawBytes > maxRatio) throw new HttpError(413, "Compression ratio too high");
|
|
470
|
-
return body;
|
|
542
|
+
if (rawBytes > 0 && inflatedBytes / rawBytes > maxRatio) throw new HttpError(413, "Compression ratio too high");
|
|
543
|
+
return Buffer$1.concat(chunks);
|
|
471
544
|
});
|
|
472
545
|
const forwardedIpSlot = cached((ctx) => {
|
|
473
546
|
const req = ctx.get(httpKind.keys.req);
|
|
@@ -563,19 +636,6 @@ function seedRawBody(ctx, raw) {
|
|
|
563
636
|
if (ctx.parent) ctx.getOwn(useRequest._slot);
|
|
564
637
|
}
|
|
565
638
|
|
|
566
|
-
//#endregion
|
|
567
|
-
//#region packages/event-http/src/composables/headers.ts
|
|
568
|
-
/**
|
|
569
|
-
* Returns the incoming request headers.
|
|
570
|
-
* @example
|
|
571
|
-
* ```ts
|
|
572
|
-
* const { host, authorization } = useHeaders()
|
|
573
|
-
* ```
|
|
574
|
-
*/
|
|
575
|
-
function useHeaders(ctx) {
|
|
576
|
-
return useRequest(ctx).headers;
|
|
577
|
-
}
|
|
578
|
-
|
|
579
639
|
//#endregion
|
|
580
640
|
//#region packages/event-http/src/composables/response.ts
|
|
581
641
|
/**
|
|
@@ -658,6 +718,120 @@ function useHttpContext(ctx) {
|
|
|
658
718
|
return ctx ?? current();
|
|
659
719
|
}
|
|
660
720
|
|
|
721
|
+
//#endregion
|
|
722
|
+
//#region packages/event-http/src/response/compression.ts
|
|
723
|
+
const COMPRESSIBLE_TYPE = /^(?:text\/(?!event-stream)|image\/svg\+xml|application\/(?:json|javascript|x-javascript|ecmascript|xml|xhtml\+xml|graphql|x-ndjson|ndjson|wasm|x-www-form-urlencoded)\b)|\+(?:json|xml)\b/i;
|
|
724
|
+
/**
|
|
725
|
+
* Default compression filter: `text/*` (except `text/event-stream`), JSON and `+json`, XML and
|
|
726
|
+
* `+xml`, JavaScript, SVG, NDJSON, WASM and form-urlencoded content types.
|
|
727
|
+
*/
|
|
728
|
+
function isCompressibleType(contentType) {
|
|
729
|
+
return COMPRESSIBLE_TYPE.test(contentType);
|
|
730
|
+
}
|
|
731
|
+
const DEFAULTS = {
|
|
732
|
+
threshold: 1024,
|
|
733
|
+
encodings: ["br", "gzip"],
|
|
734
|
+
brotliQuality: 4,
|
|
735
|
+
gzipLevel: 6,
|
|
736
|
+
filter: isCompressibleType
|
|
737
|
+
};
|
|
738
|
+
/**
|
|
739
|
+
* @internal Resolves the `compression` option. `base` supplies the values `value` does not set
|
|
740
|
+
* (the app settings for a per-response override). Returns `undefined` when compression is off.
|
|
741
|
+
*/
|
|
742
|
+
function resolveCompression(value, base = DEFAULTS) {
|
|
743
|
+
if (!value) return;
|
|
744
|
+
if (value === true) return base;
|
|
745
|
+
return {
|
|
746
|
+
threshold: value.threshold ?? base.threshold,
|
|
747
|
+
encodings: value.encodings ?? base.encodings,
|
|
748
|
+
brotliQuality: value.brotliQuality ?? base.brotliQuality,
|
|
749
|
+
gzipLevel: value.gzipLevel ?? base.gzipLevel,
|
|
750
|
+
filter: value.filter ?? base.filter
|
|
751
|
+
};
|
|
752
|
+
}
|
|
753
|
+
/**
|
|
754
|
+
* Picks the content coding for a response from the request's `Accept-Encoding` header.
|
|
755
|
+
*
|
|
756
|
+
* The coding with the highest client q-value wins; `supported` order breaks ties. `q=0`
|
|
757
|
+
* excludes a coding, `*` matches every coding not listed explicitly. Returns `undefined` when
|
|
758
|
+
* the header is missing or none of `supported` is acceptable — send the body uncompressed then.
|
|
759
|
+
*
|
|
760
|
+
* @example
|
|
761
|
+
* ```ts
|
|
762
|
+
* negotiateEncoding('gzip, deflate, br', ['br', 'gzip']) // 'br'
|
|
763
|
+
* negotiateEncoding('br;q=0.5, gzip', ['br', 'gzip']) // 'gzip'
|
|
764
|
+
* negotiateEncoding('*;q=0, identity', ['br', 'gzip']) // undefined
|
|
765
|
+
* ```
|
|
766
|
+
*/
|
|
767
|
+
function negotiateEncoding(acceptEncoding, supported) {
|
|
768
|
+
if (!acceptEncoding) return;
|
|
769
|
+
const header = Array.isArray(acceptEncoding) ? acceptEncoding.join(",") : acceptEncoding;
|
|
770
|
+
const listed = /* @__PURE__ */ new Map();
|
|
771
|
+
let wildcard;
|
|
772
|
+
for (const part of header.split(",")) {
|
|
773
|
+
const semi = part.indexOf(";");
|
|
774
|
+
const name = (semi === -1 ? part : part.slice(0, semi)).trim().toLowerCase();
|
|
775
|
+
if (!name) continue;
|
|
776
|
+
const q = semi === -1 ? 1 : parseQ(part.slice(semi + 1));
|
|
777
|
+
if (name === "*") wildcard = q;
|
|
778
|
+
else listed.set(name, q);
|
|
779
|
+
}
|
|
780
|
+
let best;
|
|
781
|
+
let bestQ = 0;
|
|
782
|
+
for (const enc of supported) {
|
|
783
|
+
const q = listed.get(enc) ?? wildcard ?? 0;
|
|
784
|
+
if (q > bestQ) {
|
|
785
|
+
best = enc;
|
|
786
|
+
bestQ = q;
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
return best;
|
|
790
|
+
}
|
|
791
|
+
function parseQ(params) {
|
|
792
|
+
for (const param of params.split(";")) {
|
|
793
|
+
const eq = param.indexOf("=");
|
|
794
|
+
if (eq !== -1 && param.slice(0, eq).trim().toLowerCase() === "q") {
|
|
795
|
+
const q = Number(param.slice(eq + 1).trim());
|
|
796
|
+
return Number.isFinite(q) && q > 0 ? Math.min(q, 1) : 0;
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
return 1;
|
|
800
|
+
}
|
|
801
|
+
/** @internal Compresses a rendered body of `size` bytes with the given coding (async, libuv threadpool). */
|
|
802
|
+
function compressResponseBody(body, size, encoding, opts) {
|
|
803
|
+
return new Promise((resolve, reject) => {
|
|
804
|
+
const done = (error, result) => {
|
|
805
|
+
if (error) reject(error);
|
|
806
|
+
else resolve(result);
|
|
807
|
+
};
|
|
808
|
+
if (encoding === "br") brotliCompress(body, { params: {
|
|
809
|
+
[constants.BROTLI_PARAM_QUALITY]: opts.brotliQuality,
|
|
810
|
+
[constants.BROTLI_PARAM_MODE]: constants.BROTLI_MODE_TEXT,
|
|
811
|
+
[constants.BROTLI_PARAM_SIZE_HINT]: size
|
|
812
|
+
} }, done);
|
|
813
|
+
else gzip(body, { level: opts.gzipLevel }, done);
|
|
814
|
+
});
|
|
815
|
+
}
|
|
816
|
+
/** @internal Cache key for compressed bytes of a prerendered body (coding + level). */
|
|
817
|
+
function compressionCacheKey(encoding, opts) {
|
|
818
|
+
return encoding === "br" ? `br${opts.brotliQuality}` : `gzip${opts.gzipLevel}`;
|
|
819
|
+
}
|
|
820
|
+
/**
|
|
821
|
+
* @internal Appends `token` to a `Vary` header value unless it (or `*`) is already listed.
|
|
822
|
+
* Returns `undefined` when nothing has to change.
|
|
823
|
+
*/
|
|
824
|
+
function mergeVary(existing, token) {
|
|
825
|
+
if (!existing || existing.length === 0) return token;
|
|
826
|
+
const value = Array.isArray(existing) ? existing.join(", ") : existing;
|
|
827
|
+
const lower = token.toLowerCase();
|
|
828
|
+
for (const part of value.split(",")) {
|
|
829
|
+
const name = part.trim().toLowerCase();
|
|
830
|
+
if (name === lower || name === "*") return;
|
|
831
|
+
}
|
|
832
|
+
return value.trim() ? `${value}, ${token}` : token;
|
|
833
|
+
}
|
|
834
|
+
|
|
661
835
|
//#endregion
|
|
662
836
|
//#region packages/event-http/src/utils/time.ts
|
|
663
837
|
function convertTime(time, unit = "ms") {
|
|
@@ -734,6 +908,70 @@ const cookieAttrFunc = {
|
|
|
734
908
|
sameSite: (v) => v ? `SameSite=${typeof v === "string" ? v : "Strict"}` : ""
|
|
735
909
|
};
|
|
736
910
|
|
|
911
|
+
//#endregion
|
|
912
|
+
//#region packages/event-http/src/response/prerender.ts
|
|
913
|
+
const registry = /* @__PURE__ */ new WeakMap();
|
|
914
|
+
function weakEtag(json) {
|
|
915
|
+
return `W/"${createHash("sha256").update(json).digest("base64url").slice(0, 22)}"`;
|
|
916
|
+
}
|
|
917
|
+
/**
|
|
918
|
+
* Serializes `obj` to JSON once and remembers the string by object identity. Whenever a handler
|
|
919
|
+
* (or an interceptor) responds with that same object, the response reuses the stored JSON instead
|
|
920
|
+
* of calling `JSON.stringify` again. Use it for large, long-lived response objects that many
|
|
921
|
+
* requests return as is — e.g. a schema or metadata envelope built once and cached.
|
|
922
|
+
*
|
|
923
|
+
* With `{ etag: true }` a weak `ETag` is derived from the serialized bytes; a `GET`/`HEAD`
|
|
924
|
+
* that responds `200` with the object sets the `ETag` header and answers a matching
|
|
925
|
+
* `If-None-Match` with `304 Not Modified` (no body; other headers such as `Cache-Control`
|
|
926
|
+
* and `Vary` are kept). Error responses never become `304`. An `ETag` header set explicitly
|
|
927
|
+
* on the response wins and disables the `304` handling for that response.
|
|
928
|
+
*
|
|
929
|
+
* **Contract: never mutate a registered object (or anything it references).** The stored JSON
|
|
930
|
+
* is not refreshed, so a mutation would keep serving the old bytes. Build a new object instead —
|
|
931
|
+
* freezing registered objects (`Object.freeze`, deeply) in development makes violations throw.
|
|
932
|
+
*
|
|
933
|
+
* Calling it again for the same object is a no-op (it adds the `ETag` if it was not requested
|
|
934
|
+
* the first time). Entries are held weakly and disappear with the object.
|
|
935
|
+
*
|
|
936
|
+
* @param obj - A JSON-serializable object or array
|
|
937
|
+
* @param options - `{ etag: true }` to also compute a weak ETag
|
|
938
|
+
* @returns The same `obj`, so it can wrap a return value
|
|
939
|
+
*
|
|
940
|
+
* @example
|
|
941
|
+
* ```ts
|
|
942
|
+
* const envelope = prerenderJson(Object.freeze(buildMeta()), { etag: true })
|
|
943
|
+
* app.get('/meta', () => {
|
|
944
|
+
* useResponse().setHeader('cache-control', 'private, no-cache')
|
|
945
|
+
* return envelope // sent without re-serializing; 304 on a matching If-None-Match
|
|
946
|
+
* })
|
|
947
|
+
* ```
|
|
948
|
+
*/
|
|
949
|
+
function prerenderJson(obj, options) {
|
|
950
|
+
let entry = registry.get(obj);
|
|
951
|
+
if (!entry) {
|
|
952
|
+
const json = JSON.stringify(obj);
|
|
953
|
+
if (typeof json !== "string") throw new TypeError("prerenderJson: value does not serialize to JSON");
|
|
954
|
+
entry = { json };
|
|
955
|
+
registry.set(obj, entry);
|
|
956
|
+
}
|
|
957
|
+
if (options?.etag && !entry.etag) entry.etag = weakEtag(entry.json);
|
|
958
|
+
return obj;
|
|
959
|
+
}
|
|
960
|
+
/** @internal Returns the pre-serialized JSON (and weak ETag) registered for `obj`, if any. */
|
|
961
|
+
function getPrerenderedJson(obj) {
|
|
962
|
+
return registry.get(obj);
|
|
963
|
+
}
|
|
964
|
+
/** @internal Weak comparison of `etag` against an `If-None-Match` header value. */
|
|
965
|
+
function ifNoneMatchHas(header, etag) {
|
|
966
|
+
if (!header) return false;
|
|
967
|
+
const opaque = etag.startsWith("W/") ? etag.slice(2) : etag;
|
|
968
|
+
for (const part of header.split(",")) {
|
|
969
|
+
const tag = part.trim();
|
|
970
|
+
if (tag === "*" || (tag.startsWith("W/") ? tag.slice(2) : tag) === opaque) return true;
|
|
971
|
+
}
|
|
972
|
+
return false;
|
|
973
|
+
}
|
|
974
|
+
|
|
737
975
|
//#endregion
|
|
738
976
|
//#region packages/event-http/src/response/http-response.ts
|
|
739
977
|
const hasFetchResponse = typeof globalThis.Response === "function";
|
|
@@ -763,8 +1001,10 @@ var HttpResponse = class {
|
|
|
763
1001
|
* @param _req - The underlying Node.js `IncomingMessage`.
|
|
764
1002
|
* @param _logger - Logger instance for error reporting.
|
|
765
1003
|
* @param defaultHeaders - Optional headers to pre-populate on this response (e.g. from `securityHeaders()`).
|
|
1004
|
+
* @param _captureMode - Finalize state on `send()` without writing to `_res` (programmatic fetch).
|
|
1005
|
+
* @param compression - App-level response compression settings (`undefined` = off).
|
|
766
1006
|
*/
|
|
767
|
-
constructor(_res, _req, _logger, defaultHeaders, _captureMode = false) {
|
|
1007
|
+
constructor(_res, _req, _logger, defaultHeaders, _captureMode = false, compression) {
|
|
768
1008
|
this._res = _res;
|
|
769
1009
|
this._req = _req;
|
|
770
1010
|
this._logger = _logger;
|
|
@@ -772,10 +1012,9 @@ var HttpResponse = class {
|
|
|
772
1012
|
this._status = 0;
|
|
773
1013
|
this._body = void 0;
|
|
774
1014
|
this._headers = {};
|
|
775
|
-
this._cookies = {};
|
|
776
|
-
this._rawCookies = [];
|
|
777
1015
|
this._hasCookies = false;
|
|
778
1016
|
this._responded = false;
|
|
1017
|
+
this._compression = compression;
|
|
779
1018
|
if (defaultHeaders) for (const key in defaultHeaders) this._headers[key] = defaultHeaders[key];
|
|
780
1019
|
}
|
|
781
1020
|
/** The HTTP status code. If not set, it is inferred automatically when `send()` is called. */
|
|
@@ -841,7 +1080,7 @@ var HttpResponse = class {
|
|
|
841
1080
|
}
|
|
842
1081
|
/** Sets an outgoing `Set-Cookie` header with optional attributes (chainable). */
|
|
843
1082
|
setCookie(name, value, attrs) {
|
|
844
|
-
this._cookies[name] = {
|
|
1083
|
+
(this._cookies ??= {})[name] = {
|
|
845
1084
|
value,
|
|
846
1085
|
attrs: attrs || {}
|
|
847
1086
|
};
|
|
@@ -850,23 +1089,23 @@ var HttpResponse = class {
|
|
|
850
1089
|
}
|
|
851
1090
|
/** Returns a previously set cookie's data, or `undefined` if not set. */
|
|
852
1091
|
getCookie(name) {
|
|
853
|
-
return this._cookies[name];
|
|
1092
|
+
return this._cookies?.[name];
|
|
854
1093
|
}
|
|
855
1094
|
/** Removes a cookie from the outgoing set list (chainable). */
|
|
856
1095
|
removeCookie(name) {
|
|
857
|
-
delete this._cookies[name];
|
|
1096
|
+
if (this._cookies) delete this._cookies[name];
|
|
858
1097
|
return this;
|
|
859
1098
|
}
|
|
860
1099
|
/** Removes all outgoing cookies (chainable). */
|
|
861
1100
|
clearCookies() {
|
|
862
|
-
this._cookies =
|
|
863
|
-
this._rawCookies =
|
|
1101
|
+
this._cookies = void 0;
|
|
1102
|
+
this._rawCookies = void 0;
|
|
864
1103
|
this._hasCookies = false;
|
|
865
1104
|
return this;
|
|
866
1105
|
}
|
|
867
1106
|
/** Appends a raw `Set-Cookie` header string (chainable). Use when you need full control over the cookie format. */
|
|
868
1107
|
setCookieRaw(rawValue) {
|
|
869
|
-
this._rawCookies.push(rawValue);
|
|
1108
|
+
(this._rawCookies ??= []).push(rawValue);
|
|
870
1109
|
this._hasCookies = true;
|
|
871
1110
|
return this;
|
|
872
1111
|
}
|
|
@@ -881,8 +1120,10 @@ var HttpResponse = class {
|
|
|
881
1120
|
*/
|
|
882
1121
|
getSetCookieStrings() {
|
|
883
1122
|
const rendered = [];
|
|
884
|
-
|
|
885
|
-
|
|
1123
|
+
if (this._cookies) {
|
|
1124
|
+
for (const [name, data] of Object.entries(this._cookies)) if (data) rendered.push(renderCookie(name, data));
|
|
1125
|
+
}
|
|
1126
|
+
if (this._rawCookies) rendered.push(...this._rawCookies);
|
|
886
1127
|
return rendered;
|
|
887
1128
|
}
|
|
888
1129
|
/** Sets the `Cache-Control` header from a directive object (chainable). */
|
|
@@ -906,6 +1147,24 @@ var HttpResponse = class {
|
|
|
906
1147
|
return this;
|
|
907
1148
|
}
|
|
908
1149
|
/**
|
|
1150
|
+
* Overrides response compression for this response (chainable).
|
|
1151
|
+
*
|
|
1152
|
+
* - `false` — never compress this response (e.g. a body that mixes secrets with reflected input).
|
|
1153
|
+
* - `true` — compress with the app settings, or the defaults when the app has compression off.
|
|
1154
|
+
* - an options object — compress with these settings over the app settings (or the defaults).
|
|
1155
|
+
*
|
|
1156
|
+
* Only regular bodies (strings, numbers, booleans, objects, `Uint8Array`) are compressed; streams
|
|
1157
|
+
* and fetch `Response` bodies are sent as is.
|
|
1158
|
+
*/
|
|
1159
|
+
setCompression(value) {
|
|
1160
|
+
this._compression = value === true ? this._compression ?? resolveCompression(true) : resolveCompression(value, this._compression);
|
|
1161
|
+
return this;
|
|
1162
|
+
}
|
|
1163
|
+
/** Effective compression settings for this response, or `false` when compression is off. */
|
|
1164
|
+
get compression() {
|
|
1165
|
+
return this._compression ?? false;
|
|
1166
|
+
}
|
|
1167
|
+
/**
|
|
909
1168
|
* Returns the underlying Node.js `ServerResponse`.
|
|
910
1169
|
* @param passthrough - If `true`, the framework still manages the response lifecycle. If `false` (default), the response is marked as "responded" and the framework will not touch it.
|
|
911
1170
|
*/
|
|
@@ -943,11 +1202,13 @@ var HttpResponse = class {
|
|
|
943
1202
|
});
|
|
944
1203
|
}
|
|
945
1204
|
const rendered = this.renderBody();
|
|
1205
|
+
const prerendered = this.takePrerendered();
|
|
946
1206
|
this.autoStatus(!!rendered);
|
|
947
|
-
if (
|
|
948
|
-
|
|
949
|
-
this.
|
|
950
|
-
}
|
|
1207
|
+
if (prerendered?.etag && this.applyPrerenderedEtag(prerendered.etag)) return new globalThis.Response(null, {
|
|
1208
|
+
status: this._status,
|
|
1209
|
+
headers: this._buildWebHeaders()
|
|
1210
|
+
});
|
|
1211
|
+
if (rendered) this._headers["content-length"] = renderedSize(rendered, prerendered).toString();
|
|
951
1212
|
const webBody = method === "HEAD" ? null : rendered instanceof Uint8Array ? rendered.buffer : rendered || null;
|
|
952
1213
|
const webResponse = new globalThis.Response(webBody, {
|
|
953
1214
|
status: this._status,
|
|
@@ -992,6 +1253,11 @@ var HttpResponse = class {
|
|
|
992
1253
|
if (body instanceof Uint8Array) return body;
|
|
993
1254
|
if (typeof body === "object") {
|
|
994
1255
|
if (!this._headers["content-type"]) this._headers["content-type"] = "application/json";
|
|
1256
|
+
const prerendered = getPrerenderedJson(body);
|
|
1257
|
+
if (prerendered) {
|
|
1258
|
+
this._prerendered = prerendered;
|
|
1259
|
+
return prerendered.json;
|
|
1260
|
+
}
|
|
995
1261
|
return JSON.stringify(body);
|
|
996
1262
|
}
|
|
997
1263
|
throw new Error(`Unsupported body format "${typeof body}"`);
|
|
@@ -1012,6 +1278,7 @@ var HttpResponse = class {
|
|
|
1012
1278
|
*
|
|
1013
1279
|
* Flushes all accumulated headers (including cookies) in a single `writeHead()` call,
|
|
1014
1280
|
* then writes the body. Supports `Readable` streams, `fetch` `Response` objects, and regular values.
|
|
1281
|
+
* Returns a Promise (which never rejects) for streamed bodies and compressed regular bodies.
|
|
1015
1282
|
*
|
|
1016
1283
|
* @throws Error if the response was already sent.
|
|
1017
1284
|
*/
|
|
@@ -1031,7 +1298,7 @@ var HttpResponse = class {
|
|
|
1031
1298
|
const method = this._req.method;
|
|
1032
1299
|
if (body instanceof Readable$1) return this.sendStream(body, method);
|
|
1033
1300
|
if (hasFetchResponse && body instanceof Response) return this.sendFetchResponse(body, method);
|
|
1034
|
-
this.sendRegular(method);
|
|
1301
|
+
return this.sendRegular(method);
|
|
1035
1302
|
}
|
|
1036
1303
|
finalizeCookies() {
|
|
1037
1304
|
if (!this._hasCookies) return;
|
|
@@ -1043,6 +1310,35 @@ var HttpResponse = class {
|
|
|
1043
1310
|
}
|
|
1044
1311
|
this._hasCookies = false;
|
|
1045
1312
|
}
|
|
1313
|
+
/**
|
|
1314
|
+
* For a body registered with `prerenderJson(obj, { etag: true })`: sets the `ETag` header on a
|
|
1315
|
+
* GET/HEAD 2xx response (unless one was set explicitly) and turns a `200` whose `If-None-Match`
|
|
1316
|
+
* matches into a bodiless `304`. Returns `true` when the response became `304`.
|
|
1317
|
+
* Other methods get no ETag (a validator on e.g. a PUT response would describe the stored resource).
|
|
1318
|
+
*/
|
|
1319
|
+
applyPrerenderedEtag(etag) {
|
|
1320
|
+
const method = this._req.method;
|
|
1321
|
+
if (method !== "GET" && method !== "HEAD" || this._status < 200 || this._status > 299 || this.headerKeyIgnoreCase("etag") !== void 0) return false;
|
|
1322
|
+
this._headers.etag = etag;
|
|
1323
|
+
if (this._status !== EHttpStatusCode.OK || !ifNoneMatchHas(this._req.headers["if-none-match"], etag)) return false;
|
|
1324
|
+
this._status = EHttpStatusCode.NotModified;
|
|
1325
|
+
for (const key in this._headers) {
|
|
1326
|
+
const lower = key.toLowerCase();
|
|
1327
|
+
if (lower === "content-type" || lower === "content-length") delete this._headers[key];
|
|
1328
|
+
}
|
|
1329
|
+
return true;
|
|
1330
|
+
}
|
|
1331
|
+
/** Returns the stored key of header `name` (lower-case) in whatever casing it was set with. */
|
|
1332
|
+
headerKeyIgnoreCase(name) {
|
|
1333
|
+
if (name in this._headers) return name;
|
|
1334
|
+
for (const key in this._headers) if (key.toLowerCase() === name) return key;
|
|
1335
|
+
}
|
|
1336
|
+
/** Returns and clears the prerender entry picked by the last `renderBody()`. */
|
|
1337
|
+
takePrerendered() {
|
|
1338
|
+
const prerendered = this._prerendered;
|
|
1339
|
+
this._prerendered = void 0;
|
|
1340
|
+
return prerendered;
|
|
1341
|
+
}
|
|
1046
1342
|
autoStatus(hasBody) {
|
|
1047
1343
|
if (this._status) return;
|
|
1048
1344
|
if (!hasBody) {
|
|
@@ -1088,13 +1384,101 @@ var HttpResponse = class {
|
|
|
1088
1384
|
return this.pipeToResponse(Readable$1.fromWeb(fetchResponse.body), "Error streaming fetch response body");
|
|
1089
1385
|
}
|
|
1090
1386
|
sendRegular(method) {
|
|
1091
|
-
const
|
|
1092
|
-
this.
|
|
1093
|
-
const
|
|
1094
|
-
this.
|
|
1095
|
-
this.
|
|
1387
|
+
const body = this.renderBody();
|
|
1388
|
+
const prerendered = this.takePrerendered();
|
|
1389
|
+
const size = renderedSize(body, prerendered);
|
|
1390
|
+
this.autoStatus(!!body);
|
|
1391
|
+
const compression = this._compression && this.isCompressible(size, this._compression) ? this._compression : void 0;
|
|
1392
|
+
if (compression) this.appendVary("Accept-Encoding");
|
|
1393
|
+
if (prerendered?.etag && this.applyPrerenderedEtag(prerendered.etag)) {
|
|
1394
|
+
this._res.writeHead(this._status, this._headers).end();
|
|
1395
|
+
return;
|
|
1396
|
+
}
|
|
1397
|
+
if (compression && method !== "HEAD") {
|
|
1398
|
+
const encoding = negotiateEncoding(this._req.headers["accept-encoding"], compression.encodings);
|
|
1399
|
+
if (encoding) return this.sendCompressed(body, size, encoding, compression, prerendered);
|
|
1400
|
+
}
|
|
1401
|
+
this._headers["content-length"] = size.toString();
|
|
1402
|
+
this._res.writeHead(this._status, this._headers).end(method === "HEAD" ? "" : body);
|
|
1403
|
+
}
|
|
1404
|
+
/**
|
|
1405
|
+
* Whether a rendered regular body of `size` bytes may be compressed — independent of the
|
|
1406
|
+
* request's `Accept-Encoding` (and of HEAD, which is answered uncompressed).
|
|
1407
|
+
*/
|
|
1408
|
+
isCompressible(size, opts) {
|
|
1409
|
+
const status = this._status;
|
|
1410
|
+
if (!size || size < opts.threshold || status < 200 || status === 204 || status === 206 || status === 304) return false;
|
|
1411
|
+
let contentType;
|
|
1412
|
+
for (const key in this._headers) {
|
|
1413
|
+
const lower = key.toLowerCase();
|
|
1414
|
+
if (lower === "content-type") contentType = this._headers[key];
|
|
1415
|
+
else if (lower === "content-encoding") return false;
|
|
1416
|
+
else if (lower === "cache-control" && /no-transform/i.test(String(this._headers[key]))) return false;
|
|
1417
|
+
}
|
|
1418
|
+
return typeof contentType === "string" && opts.filter(contentType, this);
|
|
1419
|
+
}
|
|
1420
|
+
/** Adds `token` to the `Vary` header (case-insensitive merge, keeps existing entries). */
|
|
1421
|
+
appendVary(token) {
|
|
1422
|
+
const key = this.headerKeyIgnoreCase("vary") ?? "vary";
|
|
1423
|
+
const merged = mergeVary(this._headers[key], token);
|
|
1424
|
+
if (merged !== void 0) this._headers[key] = merged;
|
|
1425
|
+
}
|
|
1426
|
+
/**
|
|
1427
|
+
* Sends the body compressed with `encoding`. Prerendered bodies are compressed once per coding
|
|
1428
|
+
* and level and the bytes reused (sent synchronously once ready). A compressor failure is
|
|
1429
|
+
* logged and the body is sent uncompressed — headers are not written yet, so that is safe.
|
|
1430
|
+
* Never rejects: a sync handler's `send()` is not awaited.
|
|
1431
|
+
*/
|
|
1432
|
+
sendCompressed(body, size, encoding, opts, prerendered) {
|
|
1433
|
+
let pending;
|
|
1434
|
+
if (prerendered?.json === body) {
|
|
1435
|
+
const cache = prerendered.compressed ??= {};
|
|
1436
|
+
const cacheKey = compressionCacheKey(encoding, opts);
|
|
1437
|
+
let entry = cache[cacheKey];
|
|
1438
|
+
if (!entry) {
|
|
1439
|
+
const promise = compressResponseBody(body, size, encoding, opts);
|
|
1440
|
+
promise.then((bytes) => cache[cacheKey] = bytes, () => delete cache[cacheKey]);
|
|
1441
|
+
entry = cache[cacheKey] = promise;
|
|
1442
|
+
}
|
|
1443
|
+
pending = entry;
|
|
1444
|
+
} else pending = compressResponseBody(body, size, encoding, opts);
|
|
1445
|
+
if (Buffer.isBuffer(pending)) {
|
|
1446
|
+
this.writeDeferred(pending, pending.byteLength, encoding);
|
|
1447
|
+
return;
|
|
1448
|
+
}
|
|
1449
|
+
return pending.then((bytes) => this.writeDeferred(bytes, bytes.byteLength, encoding), (error) => {
|
|
1450
|
+
this._logger.error("Response compression failed, sending uncompressed body", error);
|
|
1451
|
+
this.writeDeferred(body, size);
|
|
1452
|
+
}).catch((error) => {
|
|
1453
|
+
this._logger.error("Failed to send compressed response", error);
|
|
1454
|
+
});
|
|
1455
|
+
}
|
|
1456
|
+
/**
|
|
1457
|
+
* Writes the body picked by `sendCompressed()` — the `encoding`-compressed bytes, or the identity
|
|
1458
|
+
* body after a compressor failure — unless the client went away meanwhile.
|
|
1459
|
+
*/
|
|
1460
|
+
writeDeferred(body, length, encoding) {
|
|
1461
|
+
if (this._res.destroyed) return;
|
|
1462
|
+
if (encoding) {
|
|
1463
|
+
this._headers["content-encoding"] = encoding;
|
|
1464
|
+
for (const key in this._headers) {
|
|
1465
|
+
const lower = key.toLowerCase();
|
|
1466
|
+
if (lower === "content-length" && key !== lower) delete this._headers[key];
|
|
1467
|
+
else if (lower === "etag") {
|
|
1468
|
+
const etag = this._headers[key];
|
|
1469
|
+
if (typeof etag === "string" && etag && !etag.startsWith("W/")) this._headers[key] = `W/${etag}`;
|
|
1470
|
+
}
|
|
1471
|
+
}
|
|
1472
|
+
}
|
|
1473
|
+
this._headers["content-length"] = length.toString();
|
|
1474
|
+
this._res.writeHead(this._status, this._headers).end(body);
|
|
1096
1475
|
}
|
|
1097
1476
|
};
|
|
1477
|
+
/** UTF-8 size of a rendered body — computed once per registration for a prerendered body. */
|
|
1478
|
+
function renderedSize(body, prerendered) {
|
|
1479
|
+
if (typeof body !== "string") return body.byteLength;
|
|
1480
|
+
return prerendered?.json === body ? prerendered.size ??= Buffer.byteLength(body) : Buffer.byteLength(body);
|
|
1481
|
+
}
|
|
1098
1482
|
/** Converts a Record of headers to a Web Standard `Headers` object. */
|
|
1099
1483
|
function recordToWebHeaders(record) {
|
|
1100
1484
|
const headers = new Headers();
|
|
@@ -1468,7 +1852,7 @@ function escapeHtml(value) {
|
|
|
1468
1852
|
//#endregion
|
|
1469
1853
|
//#region packages/event-http/src/response/wooks-http-response.ts
|
|
1470
1854
|
let framework = {
|
|
1471
|
-
version: "0.7.
|
|
1855
|
+
version: "0.7.28",
|
|
1472
1856
|
poweredBy: "wooksjs",
|
|
1473
1857
|
link: "https://wooks.moost.org/",
|
|
1474
1858
|
image: "https://wooks.moost.org/wooks-full-logo.svg"
|
|
@@ -1492,7 +1876,8 @@ var WooksHttpResponse = class extends HttpResponse {
|
|
|
1492
1876
|
}
|
|
1493
1877
|
renderError(data, ctx) {
|
|
1494
1878
|
this._status = data.statusCode || 500;
|
|
1495
|
-
const
|
|
1879
|
+
const accept = ctx.get(httpKind.keys.req).headers.accept;
|
|
1880
|
+
const has = (type) => acceptHeaderHas(accept, type);
|
|
1496
1881
|
if (has("json")) {
|
|
1497
1882
|
this._headers["content-type"] = "application/json";
|
|
1498
1883
|
this._body = JSON.stringify(data);
|
|
@@ -1563,6 +1948,7 @@ var WooksHttp = class extends WooksAdapterBase {
|
|
|
1563
1948
|
this.logger = opts?.logger || this.getLogger(`[96m[wooks-http]`);
|
|
1564
1949
|
this.ResponseClass = opts?.responseClass ?? WooksHttpResponse;
|
|
1565
1950
|
this.eventContextOptions = this.getEventContextOptions();
|
|
1951
|
+
this.compression = resolveCompression(opts?.compression);
|
|
1566
1952
|
}
|
|
1567
1953
|
/** Registers a handler for all HTTP methods on the given path. */
|
|
1568
1954
|
all(path, handler) {
|
|
@@ -1685,8 +2071,9 @@ var WooksHttp = class extends WooksAdapterBase {
|
|
|
1685
2071
|
const RequestLimits = this.opts?.requestLimits;
|
|
1686
2072
|
const notFoundHandler = this.opts?.onNotFound;
|
|
1687
2073
|
const defaultHeaders = this.opts?.defaultHeaders;
|
|
2074
|
+
const compression = this.compression;
|
|
1688
2075
|
return (req, res) => {
|
|
1689
|
-
const response = new this.ResponseClass(res, req, ctxOptions.logger, defaultHeaders);
|
|
2076
|
+
const response = new this.ResponseClass(res, req, ctxOptions.logger, defaultHeaders, false, compression);
|
|
1690
2077
|
const method = req.method || "";
|
|
1691
2078
|
const url = req.url || "";
|
|
1692
2079
|
createHttpContext(ctxOptions, {
|
|
@@ -1788,33 +2175,19 @@ var WooksHttp = class extends WooksAdapterBase {
|
|
|
1788
2175
|
}
|
|
1789
2176
|
}
|
|
1790
2177
|
async processAsyncResult(promise, handlers, startIndex, ctx, response) {
|
|
1791
|
-
try {
|
|
1792
|
-
const result = await promise;
|
|
1793
|
-
|
|
2178
|
+
for (let i = startIndex; i < handlers.length; i++) try {
|
|
2179
|
+
const result = await (i === startIndex ? promise : handlers[i]());
|
|
2180
|
+
const sent = this.respond(result, response, ctx);
|
|
2181
|
+
if (sent) await sent;
|
|
1794
2182
|
return result;
|
|
1795
2183
|
} catch (error) {
|
|
1796
|
-
const isLastHandler = startIndex === handlers.length - 1;
|
|
1797
2184
|
if (!(error instanceof HttpError)) this.logger.error(`Uncaught route handler exception: ${ctx.get(httpKind.keys.req)?.url || ""}`, error);
|
|
1798
|
-
if (
|
|
1799
|
-
|
|
2185
|
+
if (i === handlers.length - 1) {
|
|
2186
|
+
const sent = this.respond(error, response, ctx);
|
|
2187
|
+
if (sent) await sent;
|
|
1800
2188
|
return error;
|
|
1801
2189
|
}
|
|
1802
2190
|
}
|
|
1803
|
-
for (let i = startIndex + 1; i < handlers.length; i++) {
|
|
1804
|
-
const handler = handlers[i];
|
|
1805
|
-
const isLastHandler = i === handlers.length - 1;
|
|
1806
|
-
try {
|
|
1807
|
-
const result = await handler();
|
|
1808
|
-
await this.respond(result, response, ctx);
|
|
1809
|
-
return result;
|
|
1810
|
-
} catch (error) {
|
|
1811
|
-
if (!(error instanceof HttpError)) this.logger.error(`Uncaught route handler exception: ${ctx.get(httpKind.keys.req)?.url || ""}`, error);
|
|
1812
|
-
if (isLastHandler) {
|
|
1813
|
-
await this.respond(error, response, ctx);
|
|
1814
|
-
return error;
|
|
1815
|
-
}
|
|
1816
|
-
}
|
|
1817
|
-
}
|
|
1818
2191
|
}
|
|
1819
2192
|
/**
|
|
1820
2193
|
* Programmatic route invocation using the Web Standard fetch API.
|
|
@@ -2087,4 +2460,4 @@ function prepareTestHttpContext(options) {
|
|
|
2087
2460
|
}
|
|
2088
2461
|
|
|
2089
2462
|
//#endregion
|
|
2090
|
-
export { DEFAULT_FORWARD_HEADERS, DEFAULT_LIMITS, EHttpStatusCode, HttpError, HttpResponse, WooksHttp, WooksHttpResponse, WooksURLSearchParams, createHttpApp, createHttpContext, httpKind, httpStatusCodes, prepareTestHttpContext, rawBodySlot, recordToWebHeaders, renderCacheControl, securityHeaders, seedRawBody, useAccept, useAuthorization, useCookies, useHeaders, useHttpContext, useLogger, useRequest, useResponse, useRouteParams, useUrlParams };
|
|
2463
|
+
export { DEFAULT_FORWARD_HEADERS, DEFAULT_LIMITS, EHttpStatusCode, HttpError, HttpResponse, WooksHttp, WooksHttpResponse, WooksURLSearchParams, createHttpApp, createHttpContext, httpKind, httpStatusCodes, isCompressibleType, negotiateEncoding, prepareTestHttpContext, prerenderJson, rawBodySlot, recordToWebHeaders, renderCacheControl, securityHeaders, seedRawBody, useAccept, useAuthorization, useCookies, useHeaders, useHttpContext, useLogger, useRequest, useResponse, useRouteParams, useUrlParams };
|