@wooksjs/event-http 0.7.25 → 0.7.27
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 +240 -69
- package/dist/index.d.ts +81 -6
- package/dist/index.mjs +239 -70
- package/package.json +6 -6
package/dist/index.cjs
CHANGED
|
@@ -36,6 +36,7 @@ let http = require("http");
|
|
|
36
36
|
http = __toESM(http);
|
|
37
37
|
let stream = require("stream");
|
|
38
38
|
let wooks = require("wooks");
|
|
39
|
+
let crypto = require("crypto");
|
|
39
40
|
let net = require("net");
|
|
40
41
|
|
|
41
42
|
//#region packages/event-http/src/http-kind.ts
|
|
@@ -96,18 +97,22 @@ const useCookies = (0, _wooksjs_event_core.defineWook)((ctx) => ({
|
|
|
96
97
|
}));
|
|
97
98
|
|
|
98
99
|
//#endregion
|
|
99
|
-
//#region packages/event-http/src/
|
|
100
|
+
//#region packages/event-http/src/utils/accept.ts
|
|
101
|
+
/** Short Accept names mapped to their MIME types. */
|
|
100
102
|
const ACCEPT_TYPE_MAP = {
|
|
101
103
|
json: "application/json",
|
|
102
104
|
html: "text/html",
|
|
103
105
|
xml: "application/xml",
|
|
104
106
|
text: "text/plain"
|
|
105
107
|
};
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
108
|
+
/** Whether an `Accept` header value accepts `type` (a short name from {@link ACCEPT_TYPE_MAP} or a MIME type). */
|
|
109
|
+
function acceptHeaderHas(accept, type) {
|
|
110
|
+
return !!(accept && (accept === "*/*" || accept.includes(ACCEPT_TYPE_MAP[type] || type)));
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
//#endregion
|
|
114
|
+
//#region packages/event-http/src/composables/header-accept.ts
|
|
115
|
+
const acceptsMime = (0, _wooksjs_event_core.cachedBy)((type, ctx) => acceptHeaderHas(ctx.get(httpKind.keys.req).headers.accept, type));
|
|
111
116
|
/** Provides helpers to check the request's Accept header for supported MIME types. */
|
|
112
117
|
const useAccept = (0, _wooksjs_event_core.defineWook)((ctx) => {
|
|
113
118
|
return {
|
|
@@ -167,6 +172,19 @@ const useAuthorization = (0, _wooksjs_event_core.defineWook)((ctx) => {
|
|
|
167
172
|
};
|
|
168
173
|
});
|
|
169
174
|
|
|
175
|
+
//#endregion
|
|
176
|
+
//#region packages/event-http/src/composables/headers.ts
|
|
177
|
+
/**
|
|
178
|
+
* Returns the incoming request headers.
|
|
179
|
+
* @example
|
|
180
|
+
* ```ts
|
|
181
|
+
* const { host, authorization } = useHeaders()
|
|
182
|
+
* ```
|
|
183
|
+
*/
|
|
184
|
+
function useHeaders(ctx) {
|
|
185
|
+
return (ctx ?? (0, _wooksjs_event_core.current)()).get(httpKind.keys.req).headers;
|
|
186
|
+
}
|
|
187
|
+
|
|
170
188
|
//#endregion
|
|
171
189
|
//#region packages/event-http/src/compressor/body-compressor.ts
|
|
172
190
|
const compressors = { identity: {
|
|
@@ -416,19 +434,73 @@ const DEFAULT_LIMITS = {
|
|
|
416
434
|
maxRatio: 100,
|
|
417
435
|
readTimeoutMs: 1e4
|
|
418
436
|
};
|
|
437
|
+
const NO_ENCODINGS = Object.freeze([]);
|
|
419
438
|
const contentEncodingsSlot = (0, _wooksjs_event_core.cached)((ctx) => {
|
|
420
|
-
|
|
439
|
+
const contentEncoding = ctx.get(httpKind.keys.req).headers["content-encoding"];
|
|
440
|
+
if (!contentEncoding) return NO_ENCODINGS;
|
|
441
|
+
return contentEncoding.split(",").map((p) => p.trim()).filter((p) => !!p);
|
|
421
442
|
});
|
|
422
443
|
const isCompressedSlot = (0, _wooksjs_event_core.cached)((ctx) => {
|
|
423
|
-
const
|
|
424
|
-
for (const p of parts) if ([
|
|
425
|
-
"deflate",
|
|
426
|
-
"gzip",
|
|
427
|
-
"br"
|
|
428
|
-
].includes(p)) return true;
|
|
444
|
+
for (const p of ctx.get(contentEncodingsSlot)) if (p === "deflate" || p === "gzip" || p === "br") return true;
|
|
429
445
|
return false;
|
|
430
446
|
});
|
|
431
|
-
/**
|
|
447
|
+
/**
|
|
448
|
+
* Reads the whole request body with plain stream events (one listener set, one timer).
|
|
449
|
+
* Raw bytes are checked against `upfrontLimit` first, then against `maxInflated`
|
|
450
|
+
* (the same order as the streaming path). A stall longer than `timeoutMs` destroys the
|
|
451
|
+
* request; a premature close or stream error becomes 408. The request is destroyed
|
|
452
|
+
* once reading settles, as async iteration did.
|
|
453
|
+
*/
|
|
454
|
+
function readRawBody(req, upfrontLimit, maxInflated, timeoutMs) {
|
|
455
|
+
return new Promise((resolve, reject) => {
|
|
456
|
+
const chunks = [];
|
|
457
|
+
let bytes = 0;
|
|
458
|
+
if (req.readableEnded) {
|
|
459
|
+
req.destroy();
|
|
460
|
+
resolve(buffer.Buffer.alloc(0));
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
if (req.destroyed) {
|
|
464
|
+
reject(new HttpError(408, "Request body timeout"));
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
let done = false;
|
|
468
|
+
const timer = timeoutMs === 0 ? null : setTimeout(() => req.destroy(), timeoutMs);
|
|
469
|
+
const finish = (error) => {
|
|
470
|
+
if (done) return;
|
|
471
|
+
done = true;
|
|
472
|
+
if (timer) clearTimeout(timer);
|
|
473
|
+
req.off("data", onData);
|
|
474
|
+
req.off("end", onEnd);
|
|
475
|
+
req.off("error", onAbort);
|
|
476
|
+
req.off("close", onAbort);
|
|
477
|
+
req.destroy();
|
|
478
|
+
if (error) reject(error);
|
|
479
|
+
else resolve(chunks.length === 1 ? chunks[0] : buffer.Buffer.concat(chunks, bytes));
|
|
480
|
+
};
|
|
481
|
+
const onData = (chunk) => {
|
|
482
|
+
bytes += chunk.length;
|
|
483
|
+
if (bytes > upfrontLimit) {
|
|
484
|
+
finish(new HttpError(413, "Payload Too Large"));
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
if (bytes > maxInflated) {
|
|
488
|
+
finish(new HttpError(413, "Inflated body too large"));
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
timer?.refresh();
|
|
492
|
+
chunks.push(chunk);
|
|
493
|
+
};
|
|
494
|
+
const onEnd = () => finish();
|
|
495
|
+
const onAbort = () => finish(new HttpError(408, "Request body timeout"));
|
|
496
|
+
req.on("data", onData);
|
|
497
|
+
req.on("end", onEnd);
|
|
498
|
+
req.on("error", onAbort);
|
|
499
|
+
req.on("close", onAbort);
|
|
500
|
+
req.resume();
|
|
501
|
+
});
|
|
502
|
+
}
|
|
503
|
+
/** @internal Exported for test pre-seeding — use {@link seedRawBody} instead. */
|
|
432
504
|
const rawBodySlot = (0, _wooksjs_event_core.cached)(async (ctx) => {
|
|
433
505
|
const req = ctx.get(httpKind.keys.req);
|
|
434
506
|
const encs = ctx.get(contentEncodingsSlot);
|
|
@@ -443,6 +515,14 @@ const rawBodySlot = (0, _wooksjs_event_core.cached)(async (ctx) => {
|
|
|
443
515
|
const upfrontLimit = isZip ? maxCompressed : maxInflated;
|
|
444
516
|
if (cl && cl > upfrontLimit) throw new HttpError(413, "Payload Too Large");
|
|
445
517
|
for (const enc of encs) if (!compressors[enc]) throw new HttpError(415, `Unsupported Content-Encoding "${enc}"`);
|
|
518
|
+
if (!streamable) {
|
|
519
|
+
const raw = await readRawBody(req, upfrontLimit, maxInflated, timeoutMs);
|
|
520
|
+
if (!isZip) return raw;
|
|
521
|
+
const body = await uncompressBody(encs, raw);
|
|
522
|
+
if (body.byteLength > maxInflated) throw new HttpError(413, "Inflated body too large");
|
|
523
|
+
if (raw.byteLength > 0 && body.byteLength / raw.byteLength > maxRatio) throw new HttpError(413, "Compression ratio too high");
|
|
524
|
+
return body;
|
|
525
|
+
}
|
|
446
526
|
let timer = null;
|
|
447
527
|
function resetTimer() {
|
|
448
528
|
if (timeoutMs === 0) return;
|
|
@@ -475,8 +555,7 @@ const rawBodySlot = (0, _wooksjs_event_core.cached)(async (ctx) => {
|
|
|
475
555
|
clearTimer();
|
|
476
556
|
}
|
|
477
557
|
}
|
|
478
|
-
|
|
479
|
-
if (streamable) stream = await uncompressBodyStream(encs, stream);
|
|
558
|
+
const stream = await uncompressBodyStream(encs, limitedCompressed());
|
|
480
559
|
const chunks = [];
|
|
481
560
|
let inflatedBytes = 0;
|
|
482
561
|
try {
|
|
@@ -489,14 +568,8 @@ const rawBodySlot = (0, _wooksjs_event_core.cached)(async (ctx) => {
|
|
|
489
568
|
if (error instanceof HttpError) throw error;
|
|
490
569
|
throw new HttpError(408, "Request body timeout");
|
|
491
570
|
}
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
body = await uncompressBody(encs, body);
|
|
495
|
-
inflatedBytes = body.byteLength;
|
|
496
|
-
if (inflatedBytes > maxInflated) throw new HttpError(413, "Inflated body too large");
|
|
497
|
-
}
|
|
498
|
-
if (isZip && rawBytes > 0 && inflatedBytes / rawBytes > maxRatio) throw new HttpError(413, "Compression ratio too high");
|
|
499
|
-
return body;
|
|
571
|
+
if (rawBytes > 0 && inflatedBytes / rawBytes > maxRatio) throw new HttpError(413, "Compression ratio too high");
|
|
572
|
+
return buffer.Buffer.concat(chunks);
|
|
500
573
|
});
|
|
501
574
|
const forwardedIpSlot = (0, _wooksjs_event_core.cached)((ctx) => {
|
|
502
575
|
const req = ctx.get(httpKind.keys.req);
|
|
@@ -568,18 +641,28 @@ const useRequest = (0, _wooksjs_event_core.defineWook)((ctx) => {
|
|
|
568
641
|
setMaxRatio
|
|
569
642
|
};
|
|
570
643
|
});
|
|
571
|
-
|
|
572
|
-
//#endregion
|
|
573
|
-
//#region packages/event-http/src/composables/headers.ts
|
|
574
644
|
/**
|
|
575
|
-
*
|
|
645
|
+
* Seeds the raw request body of `ctx`: `useRequest(ctx).rawBody()` resolves to `raw` instead of
|
|
646
|
+
* reading the incoming request stream. To seed what `useBody()` (`@wooksjs/http-body`) parses,
|
|
647
|
+
* use its `seedBody()` — a parent's already parsed body is otherwise read through.
|
|
648
|
+
*
|
|
649
|
+
* Use it for a child event context (`new EventContext({ logger, parent })`) that runs a handler
|
|
650
|
+
* with its own payload: the child still reads everything else (request, headers, auth) through
|
|
651
|
+
* its parent, but never the parent's body. Call it before anything in the child reads the body.
|
|
652
|
+
*
|
|
653
|
+
* @param ctx - The context to seed — usually a child of the current HTTP event
|
|
654
|
+
* @param raw - The body bytes (a string is encoded as UTF-8)
|
|
655
|
+
*
|
|
576
656
|
* @example
|
|
577
657
|
* ```ts
|
|
578
|
-
* const {
|
|
658
|
+
* const child = new EventContext({ logger: parent.logger, parent })
|
|
659
|
+
* seedRawBody(child, JSON.stringify({ ids: [1, 2] }))
|
|
660
|
+
* await run(child, () => useRequest().rawBody()) // Buffer of '{"ids":[1,2]}'
|
|
579
661
|
* ```
|
|
580
662
|
*/
|
|
581
|
-
function
|
|
582
|
-
|
|
663
|
+
function seedRawBody(ctx, raw) {
|
|
664
|
+
ctx.setOwn(rawBodySlot, Promise.resolve(buffer.Buffer.isBuffer(raw) ? raw : buffer.Buffer.from(raw)));
|
|
665
|
+
if (ctx.parent) ctx.getOwn(useRequest._slot);
|
|
583
666
|
}
|
|
584
667
|
|
|
585
668
|
//#endregion
|
|
@@ -740,6 +823,70 @@ const cookieAttrFunc = {
|
|
|
740
823
|
sameSite: (v) => v ? `SameSite=${typeof v === "string" ? v : "Strict"}` : ""
|
|
741
824
|
};
|
|
742
825
|
|
|
826
|
+
//#endregion
|
|
827
|
+
//#region packages/event-http/src/response/prerender.ts
|
|
828
|
+
const registry = /* @__PURE__ */ new WeakMap();
|
|
829
|
+
function weakEtag(json) {
|
|
830
|
+
return `W/"${(0, crypto.createHash)("sha256").update(json).digest("base64url").slice(0, 22)}"`;
|
|
831
|
+
}
|
|
832
|
+
/**
|
|
833
|
+
* Serializes `obj` to JSON once and remembers the string by object identity. Whenever a handler
|
|
834
|
+
* (or an interceptor) responds with that same object, the response reuses the stored JSON instead
|
|
835
|
+
* of calling `JSON.stringify` again. Use it for large, long-lived response objects that many
|
|
836
|
+
* requests return as is — e.g. a schema or metadata envelope built once and cached.
|
|
837
|
+
*
|
|
838
|
+
* With `{ etag: true }` a weak `ETag` is derived from the serialized bytes; a `GET`/`HEAD`
|
|
839
|
+
* that responds `200` with the object sets the `ETag` header and answers a matching
|
|
840
|
+
* `If-None-Match` with `304 Not Modified` (no body; other headers such as `Cache-Control`
|
|
841
|
+
* and `Vary` are kept). Error responses never become `304`. An `ETag` header set explicitly
|
|
842
|
+
* on the response wins and disables the `304` handling for that response.
|
|
843
|
+
*
|
|
844
|
+
* **Contract: never mutate a registered object (or anything it references).** The stored JSON
|
|
845
|
+
* is not refreshed, so a mutation would keep serving the old bytes. Build a new object instead —
|
|
846
|
+
* freezing registered objects (`Object.freeze`, deeply) in development makes violations throw.
|
|
847
|
+
*
|
|
848
|
+
* Calling it again for the same object is a no-op (it adds the `ETag` if it was not requested
|
|
849
|
+
* the first time). Entries are held weakly and disappear with the object.
|
|
850
|
+
*
|
|
851
|
+
* @param obj - A JSON-serializable object or array
|
|
852
|
+
* @param options - `{ etag: true }` to also compute a weak ETag
|
|
853
|
+
* @returns The same `obj`, so it can wrap a return value
|
|
854
|
+
*
|
|
855
|
+
* @example
|
|
856
|
+
* ```ts
|
|
857
|
+
* const envelope = prerenderJson(Object.freeze(buildMeta()), { etag: true })
|
|
858
|
+
* app.get('/meta', () => {
|
|
859
|
+
* useResponse().setHeader('cache-control', 'private, no-cache')
|
|
860
|
+
* return envelope // sent without re-serializing; 304 on a matching If-None-Match
|
|
861
|
+
* })
|
|
862
|
+
* ```
|
|
863
|
+
*/
|
|
864
|
+
function prerenderJson(obj, options) {
|
|
865
|
+
let entry = registry.get(obj);
|
|
866
|
+
if (!entry) {
|
|
867
|
+
const json = JSON.stringify(obj);
|
|
868
|
+
if (typeof json !== "string") throw new TypeError("prerenderJson: value does not serialize to JSON");
|
|
869
|
+
entry = { json };
|
|
870
|
+
registry.set(obj, entry);
|
|
871
|
+
}
|
|
872
|
+
if (options?.etag && !entry.etag) entry.etag = weakEtag(entry.json);
|
|
873
|
+
return obj;
|
|
874
|
+
}
|
|
875
|
+
/** @internal Returns the pre-serialized JSON (and weak ETag) registered for `obj`, if any. */
|
|
876
|
+
function getPrerenderedJson(obj) {
|
|
877
|
+
return registry.get(obj);
|
|
878
|
+
}
|
|
879
|
+
/** @internal Weak comparison of `etag` against an `If-None-Match` header value. */
|
|
880
|
+
function ifNoneMatchHas(header, etag) {
|
|
881
|
+
if (!header) return false;
|
|
882
|
+
const opaque = etag.startsWith("W/") ? etag.slice(2) : etag;
|
|
883
|
+
for (const part of header.split(",")) {
|
|
884
|
+
const tag = part.trim();
|
|
885
|
+
if (tag === "*" || (tag.startsWith("W/") ? tag.slice(2) : tag) === opaque) return true;
|
|
886
|
+
}
|
|
887
|
+
return false;
|
|
888
|
+
}
|
|
889
|
+
|
|
743
890
|
//#endregion
|
|
744
891
|
//#region packages/event-http/src/response/http-response.ts
|
|
745
892
|
const hasFetchResponse = typeof globalThis.Response === "function";
|
|
@@ -778,8 +925,6 @@ var HttpResponse = class {
|
|
|
778
925
|
this._status = 0;
|
|
779
926
|
this._body = void 0;
|
|
780
927
|
this._headers = {};
|
|
781
|
-
this._cookies = {};
|
|
782
|
-
this._rawCookies = [];
|
|
783
928
|
this._hasCookies = false;
|
|
784
929
|
this._responded = false;
|
|
785
930
|
if (defaultHeaders) for (const key in defaultHeaders) this._headers[key] = defaultHeaders[key];
|
|
@@ -847,7 +992,7 @@ var HttpResponse = class {
|
|
|
847
992
|
}
|
|
848
993
|
/** Sets an outgoing `Set-Cookie` header with optional attributes (chainable). */
|
|
849
994
|
setCookie(name, value, attrs) {
|
|
850
|
-
this._cookies[name] = {
|
|
995
|
+
(this._cookies ??= {})[name] = {
|
|
851
996
|
value,
|
|
852
997
|
attrs: attrs || {}
|
|
853
998
|
};
|
|
@@ -856,23 +1001,23 @@ var HttpResponse = class {
|
|
|
856
1001
|
}
|
|
857
1002
|
/** Returns a previously set cookie's data, or `undefined` if not set. */
|
|
858
1003
|
getCookie(name) {
|
|
859
|
-
return this._cookies[name];
|
|
1004
|
+
return this._cookies?.[name];
|
|
860
1005
|
}
|
|
861
1006
|
/** Removes a cookie from the outgoing set list (chainable). */
|
|
862
1007
|
removeCookie(name) {
|
|
863
|
-
delete this._cookies[name];
|
|
1008
|
+
if (this._cookies) delete this._cookies[name];
|
|
864
1009
|
return this;
|
|
865
1010
|
}
|
|
866
1011
|
/** Removes all outgoing cookies (chainable). */
|
|
867
1012
|
clearCookies() {
|
|
868
|
-
this._cookies =
|
|
869
|
-
this._rawCookies =
|
|
1013
|
+
this._cookies = void 0;
|
|
1014
|
+
this._rawCookies = void 0;
|
|
870
1015
|
this._hasCookies = false;
|
|
871
1016
|
return this;
|
|
872
1017
|
}
|
|
873
1018
|
/** Appends a raw `Set-Cookie` header string (chainable). Use when you need full control over the cookie format. */
|
|
874
1019
|
setCookieRaw(rawValue) {
|
|
875
|
-
this._rawCookies.push(rawValue);
|
|
1020
|
+
(this._rawCookies ??= []).push(rawValue);
|
|
876
1021
|
this._hasCookies = true;
|
|
877
1022
|
return this;
|
|
878
1023
|
}
|
|
@@ -887,8 +1032,10 @@ var HttpResponse = class {
|
|
|
887
1032
|
*/
|
|
888
1033
|
getSetCookieStrings() {
|
|
889
1034
|
const rendered = [];
|
|
890
|
-
|
|
891
|
-
|
|
1035
|
+
if (this._cookies) {
|
|
1036
|
+
for (const [name, data] of Object.entries(this._cookies)) if (data) rendered.push(renderCookie(name, data));
|
|
1037
|
+
}
|
|
1038
|
+
if (this._rawCookies) rendered.push(...this._rawCookies);
|
|
892
1039
|
return rendered;
|
|
893
1040
|
}
|
|
894
1041
|
/** Sets the `Cache-Control` header from a directive object (chainable). */
|
|
@@ -950,6 +1097,10 @@ var HttpResponse = class {
|
|
|
950
1097
|
}
|
|
951
1098
|
const rendered = this.renderBody();
|
|
952
1099
|
this.autoStatus(!!rendered);
|
|
1100
|
+
if (this.applyPrerenderedEtag()) return new globalThis.Response(null, {
|
|
1101
|
+
status: this._status,
|
|
1102
|
+
headers: this._buildWebHeaders()
|
|
1103
|
+
});
|
|
953
1104
|
if (rendered) {
|
|
954
1105
|
const contentLength = typeof rendered === "string" ? Buffer.byteLength(rendered) : rendered.byteLength;
|
|
955
1106
|
this._headers["content-length"] = contentLength.toString();
|
|
@@ -998,6 +1149,11 @@ var HttpResponse = class {
|
|
|
998
1149
|
if (body instanceof Uint8Array) return body;
|
|
999
1150
|
if (typeof body === "object") {
|
|
1000
1151
|
if (!this._headers["content-type"]) this._headers["content-type"] = "application/json";
|
|
1152
|
+
const prerendered = getPrerenderedJson(body);
|
|
1153
|
+
if (prerendered) {
|
|
1154
|
+
this._prerenderedEtag = prerendered.etag;
|
|
1155
|
+
return prerendered.json;
|
|
1156
|
+
}
|
|
1001
1157
|
return JSON.stringify(body);
|
|
1002
1158
|
}
|
|
1003
1159
|
throw new Error(`Unsupported body format "${typeof body}"`);
|
|
@@ -1049,6 +1205,31 @@ var HttpResponse = class {
|
|
|
1049
1205
|
}
|
|
1050
1206
|
this._hasCookies = false;
|
|
1051
1207
|
}
|
|
1208
|
+
/**
|
|
1209
|
+
* For a body registered with `prerenderJson(obj, { etag: true })`: sets the `ETag` header on a
|
|
1210
|
+
* GET/HEAD 2xx response (unless one was set explicitly) and turns a `200` whose `If-None-Match`
|
|
1211
|
+
* matches into a bodiless `304`. Returns `true` when the response became `304`.
|
|
1212
|
+
* Other methods get no ETag (a validator on e.g. a PUT response would describe the stored resource).
|
|
1213
|
+
*/
|
|
1214
|
+
applyPrerenderedEtag() {
|
|
1215
|
+
const etag = this._prerenderedEtag;
|
|
1216
|
+
if (!etag) return false;
|
|
1217
|
+
this._prerenderedEtag = void 0;
|
|
1218
|
+
const method = this._req.method;
|
|
1219
|
+
if (method !== "GET" && method !== "HEAD" || this._status < 200 || this._status > 299 || this.hasHeaderIgnoreCase("etag")) return false;
|
|
1220
|
+
this._headers.etag = etag;
|
|
1221
|
+
if (this._status !== EHttpStatusCode.OK || !ifNoneMatchHas(this._req.headers["if-none-match"], etag)) return false;
|
|
1222
|
+
this._status = EHttpStatusCode.NotModified;
|
|
1223
|
+
for (const key in this._headers) {
|
|
1224
|
+
const lower = key.toLowerCase();
|
|
1225
|
+
if (lower === "content-type" || lower === "content-length") delete this._headers[key];
|
|
1226
|
+
}
|
|
1227
|
+
return true;
|
|
1228
|
+
}
|
|
1229
|
+
hasHeaderIgnoreCase(name) {
|
|
1230
|
+
for (const key in this._headers) if (key.toLowerCase() === name) return true;
|
|
1231
|
+
return false;
|
|
1232
|
+
}
|
|
1052
1233
|
autoStatus(hasBody) {
|
|
1053
1234
|
if (this._status) return;
|
|
1054
1235
|
if (!hasBody) {
|
|
@@ -1096,6 +1277,10 @@ var HttpResponse = class {
|
|
|
1096
1277
|
sendRegular(method) {
|
|
1097
1278
|
const renderedBody = this.renderBody();
|
|
1098
1279
|
this.autoStatus(!!renderedBody);
|
|
1280
|
+
if (this.applyPrerenderedEtag()) {
|
|
1281
|
+
this._res.writeHead(this._status, this._headers).end();
|
|
1282
|
+
return;
|
|
1283
|
+
}
|
|
1099
1284
|
const contentLength = typeof renderedBody === "string" ? Buffer.byteLength(renderedBody) : renderedBody.byteLength;
|
|
1100
1285
|
this._headers["content-length"] = contentLength.toString();
|
|
1101
1286
|
this._res.writeHead(this._status, this._headers).end(method === "HEAD" ? "" : renderedBody);
|
|
@@ -1474,7 +1659,7 @@ function escapeHtml(value) {
|
|
|
1474
1659
|
//#endregion
|
|
1475
1660
|
//#region packages/event-http/src/response/wooks-http-response.ts
|
|
1476
1661
|
let framework = {
|
|
1477
|
-
version: "0.7.
|
|
1662
|
+
version: "0.7.27",
|
|
1478
1663
|
poweredBy: "wooksjs",
|
|
1479
1664
|
link: "https://wooks.moost.org/",
|
|
1480
1665
|
image: "https://wooks.moost.org/wooks-full-logo.svg"
|
|
@@ -1498,7 +1683,8 @@ var WooksHttpResponse = class extends HttpResponse {
|
|
|
1498
1683
|
}
|
|
1499
1684
|
renderError(data, ctx) {
|
|
1500
1685
|
this._status = data.statusCode || 500;
|
|
1501
|
-
const
|
|
1686
|
+
const accept = ctx.get(httpKind.keys.req).headers.accept;
|
|
1687
|
+
const has = (type) => acceptHeaderHas(accept, type);
|
|
1502
1688
|
if (has("json")) {
|
|
1503
1689
|
this._headers["content-type"] = "application/json";
|
|
1504
1690
|
this._body = JSON.stringify(data);
|
|
@@ -1794,33 +1980,19 @@ var WooksHttp = class extends wooks.WooksAdapterBase {
|
|
|
1794
1980
|
}
|
|
1795
1981
|
}
|
|
1796
1982
|
async processAsyncResult(promise, handlers, startIndex, ctx, response) {
|
|
1797
|
-
try {
|
|
1798
|
-
const result = await promise;
|
|
1799
|
-
|
|
1983
|
+
for (let i = startIndex; i < handlers.length; i++) try {
|
|
1984
|
+
const result = await (i === startIndex ? promise : handlers[i]());
|
|
1985
|
+
const sent = this.respond(result, response, ctx);
|
|
1986
|
+
if (sent) await sent;
|
|
1800
1987
|
return result;
|
|
1801
1988
|
} catch (error) {
|
|
1802
|
-
const isLastHandler = startIndex === handlers.length - 1;
|
|
1803
1989
|
if (!(error instanceof HttpError)) this.logger.error(`Uncaught route handler exception: ${ctx.get(httpKind.keys.req)?.url || ""}`, error);
|
|
1804
|
-
if (
|
|
1805
|
-
|
|
1990
|
+
if (i === handlers.length - 1) {
|
|
1991
|
+
const sent = this.respond(error, response, ctx);
|
|
1992
|
+
if (sent) await sent;
|
|
1806
1993
|
return error;
|
|
1807
1994
|
}
|
|
1808
1995
|
}
|
|
1809
|
-
for (let i = startIndex + 1; i < handlers.length; i++) {
|
|
1810
|
-
const handler = handlers[i];
|
|
1811
|
-
const isLastHandler = i === handlers.length - 1;
|
|
1812
|
-
try {
|
|
1813
|
-
const result = await handler();
|
|
1814
|
-
await this.respond(result, response, ctx);
|
|
1815
|
-
return result;
|
|
1816
|
-
} catch (error) {
|
|
1817
|
-
if (!(error instanceof HttpError)) this.logger.error(`Uncaught route handler exception: ${ctx.get(httpKind.keys.req)?.url || ""}`, error);
|
|
1818
|
-
if (isLastHandler) {
|
|
1819
|
-
await this.respond(error, response, ctx);
|
|
1820
|
-
return error;
|
|
1821
|
-
}
|
|
1822
|
-
}
|
|
1823
|
-
}
|
|
1824
1996
|
}
|
|
1825
1997
|
/**
|
|
1826
1998
|
* Programmatic route invocation using the Web Standard fetch API.
|
|
@@ -1884,7 +2056,7 @@ var WooksHttp = class extends wooks.WooksAdapterBase {
|
|
|
1884
2056
|
requestLimits
|
|
1885
2057
|
}, async () => {
|
|
1886
2058
|
const ctx = (0, _wooksjs_event_core.current)();
|
|
1887
|
-
if (bodyBuffer) ctx
|
|
2059
|
+
if (bodyBuffer) seedRawBody(ctx, bodyBuffer);
|
|
1888
2060
|
try {
|
|
1889
2061
|
const handlers = this.wooks.lookupHandlers(method, pathname, ctx);
|
|
1890
2062
|
if (handlers) {
|
|
@@ -2088,10 +2260,7 @@ function prepareTestHttpContext(options) {
|
|
|
2088
2260
|
requestLimits: options.requestLimits
|
|
2089
2261
|
});
|
|
2090
2262
|
if (options.params) ctx.set(_wooksjs_event_core.routeParamsKey, options.params);
|
|
2091
|
-
if (options.rawBody !== void 0)
|
|
2092
|
-
const buf = buffer.Buffer.isBuffer(options.rawBody) ? options.rawBody : buffer.Buffer.from(options.rawBody);
|
|
2093
|
-
ctx.set(rawBodySlot, Promise.resolve(buf));
|
|
2094
|
-
}
|
|
2263
|
+
if (options.rawBody !== void 0) seedRawBody(ctx, options.rawBody);
|
|
2095
2264
|
return (cb) => (0, _wooksjs_event_core.run)(ctx, cb);
|
|
2096
2265
|
}
|
|
2097
2266
|
|
|
@@ -2109,10 +2278,12 @@ exports.createHttpContext = createHttpContext;
|
|
|
2109
2278
|
exports.httpKind = httpKind;
|
|
2110
2279
|
exports.httpStatusCodes = httpStatusCodes;
|
|
2111
2280
|
exports.prepareTestHttpContext = prepareTestHttpContext;
|
|
2281
|
+
exports.prerenderJson = prerenderJson;
|
|
2112
2282
|
exports.rawBodySlot = rawBodySlot;
|
|
2113
2283
|
exports.recordToWebHeaders = recordToWebHeaders;
|
|
2114
2284
|
exports.renderCacheControl = renderCacheControl;
|
|
2115
2285
|
exports.securityHeaders = securityHeaders;
|
|
2286
|
+
exports.seedRawBody = seedRawBody;
|
|
2116
2287
|
exports.useAccept = useAccept;
|
|
2117
2288
|
exports.useAuthorization = useAuthorization;
|
|
2118
2289
|
exports.useCookies = useCookies;
|
package/dist/index.d.ts
CHANGED
|
@@ -69,7 +69,7 @@ declare const DEFAULT_LIMITS: {
|
|
|
69
69
|
readonly maxRatio: 100;
|
|
70
70
|
readonly readTimeoutMs: 10000;
|
|
71
71
|
};
|
|
72
|
-
/** @internal Exported for test pre-seeding
|
|
72
|
+
/** @internal Exported for test pre-seeding — use {@link seedRawBody} instead. */
|
|
73
73
|
declare const rawBodySlot: _wooksjs_event_core.Cached<Promise<Buffer<ArrayBufferLike>>>;
|
|
74
74
|
/**
|
|
75
75
|
* Provides access to the incoming HTTP request (method, url, headers, body, IP).
|
|
@@ -80,7 +80,7 @@ declare const rawBodySlot: _wooksjs_event_core.Cached<Promise<Buffer<ArrayBuffer
|
|
|
80
80
|
* ```
|
|
81
81
|
*/
|
|
82
82
|
declare const useRequest: _wooksjs_event_core.WookComposable<{
|
|
83
|
-
raw:
|
|
83
|
+
raw: IncomingMessage;
|
|
84
84
|
url: string | undefined;
|
|
85
85
|
method: string | undefined;
|
|
86
86
|
headers: http.IncomingHttpHeaders;
|
|
@@ -103,6 +103,26 @@ declare const useRequest: _wooksjs_event_core.WookComposable<{
|
|
|
103
103
|
getMaxRatio: () => number;
|
|
104
104
|
setMaxRatio: (limit: number) => void;
|
|
105
105
|
}>;
|
|
106
|
+
/**
|
|
107
|
+
* Seeds the raw request body of `ctx`: `useRequest(ctx).rawBody()` resolves to `raw` instead of
|
|
108
|
+
* reading the incoming request stream. To seed what `useBody()` (`@wooksjs/http-body`) parses,
|
|
109
|
+
* use its `seedBody()` — a parent's already parsed body is otherwise read through.
|
|
110
|
+
*
|
|
111
|
+
* Use it for a child event context (`new EventContext({ logger, parent })`) that runs a handler
|
|
112
|
+
* with its own payload: the child still reads everything else (request, headers, auth) through
|
|
113
|
+
* its parent, but never the parent's body. Call it before anything in the child reads the body.
|
|
114
|
+
*
|
|
115
|
+
* @param ctx - The context to seed — usually a child of the current HTTP event
|
|
116
|
+
* @param raw - The body bytes (a string is encoded as UTF-8)
|
|
117
|
+
*
|
|
118
|
+
* @example
|
|
119
|
+
* ```ts
|
|
120
|
+
* const child = new EventContext({ logger: parent.logger, parent })
|
|
121
|
+
* seedRawBody(child, JSON.stringify({ ids: [1, 2] }))
|
|
122
|
+
* await run(child, () => useRequest().rawBody()) // Buffer of '{"ids":[1,2]}'
|
|
123
|
+
* ```
|
|
124
|
+
*/
|
|
125
|
+
declare function seedRawBody(ctx: EventContext, raw: Buffer | string): void;
|
|
106
126
|
|
|
107
127
|
/** Maps numeric HTTP status codes to their human-readable descriptions. */
|
|
108
128
|
declare const httpStatusCodes: {
|
|
@@ -355,10 +375,14 @@ declare class HttpResponse {
|
|
|
355
375
|
protected _status: EHttpStatusCode;
|
|
356
376
|
protected _body: unknown;
|
|
357
377
|
protected _headers: Record<string, string | string[]>;
|
|
358
|
-
|
|
359
|
-
protected
|
|
378
|
+
/** Outgoing named cookies — allocated on the first `setCookie()`. */
|
|
379
|
+
protected _cookies?: Record<string, TSetCookieData>;
|
|
380
|
+
/** Outgoing raw `Set-Cookie` strings — allocated on the first `setCookieRaw()`. */
|
|
381
|
+
protected _rawCookies?: string[];
|
|
360
382
|
protected _hasCookies: boolean;
|
|
361
383
|
protected _responded: boolean;
|
|
384
|
+
/** Weak ETag of the prerendered body picked by the last `renderBody()` (see `prerenderJson`). */
|
|
385
|
+
private _prerenderedEtag?;
|
|
362
386
|
/** The HTTP status code. If not set, it is inferred automatically when `send()` is called. */
|
|
363
387
|
get status(): EHttpStatusCode;
|
|
364
388
|
set status(value: EHttpStatusCode);
|
|
@@ -448,6 +472,14 @@ declare class HttpResponse {
|
|
|
448
472
|
*/
|
|
449
473
|
send(): void | Promise<void>;
|
|
450
474
|
private finalizeCookies;
|
|
475
|
+
/**
|
|
476
|
+
* For a body registered with `prerenderJson(obj, { etag: true })`: sets the `ETag` header on a
|
|
477
|
+
* GET/HEAD 2xx response (unless one was set explicitly) and turns a `200` whose `If-None-Match`
|
|
478
|
+
* matches into a bodiless `304`. Returns `true` when the response became `304`.
|
|
479
|
+
* Other methods get no ETag (a validator on e.g. a PUT response would describe the stored resource).
|
|
480
|
+
*/
|
|
481
|
+
private applyPrerenderedEtag;
|
|
482
|
+
private hasHeaderIgnoreCase;
|
|
451
483
|
private autoStatus;
|
|
452
484
|
private sendStream;
|
|
453
485
|
/**
|
|
@@ -709,6 +741,49 @@ declare class WooksHttp extends WooksAdapterBase {
|
|
|
709
741
|
*/
|
|
710
742
|
declare function createHttpApp(opts?: TWooksHttpOptions, wooks?: Wooks | WooksAdapterBase): WooksHttp;
|
|
711
743
|
|
|
744
|
+
/** Options for {@link prerenderJson}. */
|
|
745
|
+
interface TPrerenderJsonOptions {
|
|
746
|
+
/**
|
|
747
|
+
* Also compute a weak `ETag` from the serialized bytes. Responses that send the object then
|
|
748
|
+
* carry the `ETag` header and answer a matching `If-None-Match` (GET/HEAD, status 200) with
|
|
749
|
+
* `304 Not Modified`.
|
|
750
|
+
*/
|
|
751
|
+
etag?: boolean;
|
|
752
|
+
}
|
|
753
|
+
/**
|
|
754
|
+
* Serializes `obj` to JSON once and remembers the string by object identity. Whenever a handler
|
|
755
|
+
* (or an interceptor) responds with that same object, the response reuses the stored JSON instead
|
|
756
|
+
* of calling `JSON.stringify` again. Use it for large, long-lived response objects that many
|
|
757
|
+
* requests return as is — e.g. a schema or metadata envelope built once and cached.
|
|
758
|
+
*
|
|
759
|
+
* With `{ etag: true }` a weak `ETag` is derived from the serialized bytes; a `GET`/`HEAD`
|
|
760
|
+
* that responds `200` with the object sets the `ETag` header and answers a matching
|
|
761
|
+
* `If-None-Match` with `304 Not Modified` (no body; other headers such as `Cache-Control`
|
|
762
|
+
* and `Vary` are kept). Error responses never become `304`. An `ETag` header set explicitly
|
|
763
|
+
* on the response wins and disables the `304` handling for that response.
|
|
764
|
+
*
|
|
765
|
+
* **Contract: never mutate a registered object (or anything it references).** The stored JSON
|
|
766
|
+
* is not refreshed, so a mutation would keep serving the old bytes. Build a new object instead —
|
|
767
|
+
* freezing registered objects (`Object.freeze`, deeply) in development makes violations throw.
|
|
768
|
+
*
|
|
769
|
+
* Calling it again for the same object is a no-op (it adds the `ETag` if it was not requested
|
|
770
|
+
* the first time). Entries are held weakly and disappear with the object.
|
|
771
|
+
*
|
|
772
|
+
* @param obj - A JSON-serializable object or array
|
|
773
|
+
* @param options - `{ etag: true }` to also compute a weak ETag
|
|
774
|
+
* @returns The same `obj`, so it can wrap a return value
|
|
775
|
+
*
|
|
776
|
+
* @example
|
|
777
|
+
* ```ts
|
|
778
|
+
* const envelope = prerenderJson(Object.freeze(buildMeta()), { etag: true })
|
|
779
|
+
* app.get('/meta', () => {
|
|
780
|
+
* useResponse().setHeader('cache-control', 'private, no-cache')
|
|
781
|
+
* return envelope // sent without re-serializing; 304 on a matching If-None-Match
|
|
782
|
+
* })
|
|
783
|
+
* ```
|
|
784
|
+
*/
|
|
785
|
+
declare function prerenderJson<T extends object>(obj: T, options?: TPrerenderJsonOptions): T;
|
|
786
|
+
|
|
712
787
|
/**
|
|
713
788
|
* Configuration for `securityHeaders()`. Each option accepts a `string` (override value),
|
|
714
789
|
* `false` (disable), or `undefined` (use default). `strictTransportSecurity` has no default (opt-in only).
|
|
@@ -773,5 +848,5 @@ interface TTestHttpContext {
|
|
|
773
848
|
*/
|
|
774
849
|
declare function prepareTestHttpContext(options: TTestHttpContext): <T>(cb: (...a: any[]) => T) => T;
|
|
775
850
|
|
|
776
|
-
export { DEFAULT_FORWARD_HEADERS, DEFAULT_LIMITS, EHttpStatusCode, HttpError, HttpResponse, WooksHttp, WooksHttpResponse, WooksURLSearchParams, createHttpApp, createHttpContext, httpKind, httpStatusCodes, prepareTestHttpContext, rawBodySlot, recordToWebHeaders, renderCacheControl, securityHeaders, useAccept, useAuthorization, useCookies, useHeaders, useHttpContext, useRequest, useResponse, useUrlParams };
|
|
777
|
-
export type { KnownAcceptType, KnownAuthType, SecurityHeadersOptions, TCacheControl, TCookieAttributes, TCookieAttributesInput, THttpEventData, TRequestLimits, TSetCookieData, TTestHttpContext, TWooksErrorBody, TWooksErrorBodyExt, TWooksHttpOptions };
|
|
851
|
+
export { DEFAULT_FORWARD_HEADERS, DEFAULT_LIMITS, EHttpStatusCode, HttpError, HttpResponse, WooksHttp, WooksHttpResponse, WooksURLSearchParams, createHttpApp, createHttpContext, httpKind, httpStatusCodes, prepareTestHttpContext, prerenderJson, rawBodySlot, recordToWebHeaders, renderCacheControl, securityHeaders, seedRawBody, useAccept, useAuthorization, useCookies, useHeaders, useHttpContext, useRequest, useResponse, useUrlParams };
|
|
852
|
+
export type { KnownAcceptType, KnownAuthType, SecurityHeadersOptions, TCacheControl, TCookieAttributes, TCookieAttributesInput, THttpEventData, TPrerenderJsonOptions, TRequestLimits, TSetCookieData, TTestHttpContext, TWooksErrorBody, TWooksErrorBodyExt, TWooksHttpOptions };
|
package/dist/index.mjs
CHANGED
|
@@ -7,6 +7,7 @@ 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,19 +405,73 @@ 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
|
});
|
|
402
|
-
/**
|
|
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
|
+
}
|
|
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);
|
|
405
477
|
const encs = ctx.get(contentEncodingsSlot);
|
|
@@ -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);
|
|
@@ -539,18 +612,28 @@ const useRequest = defineWook((ctx) => {
|
|
|
539
612
|
setMaxRatio
|
|
540
613
|
};
|
|
541
614
|
});
|
|
542
|
-
|
|
543
|
-
//#endregion
|
|
544
|
-
//#region packages/event-http/src/composables/headers.ts
|
|
545
615
|
/**
|
|
546
|
-
*
|
|
616
|
+
* Seeds the raw request body of `ctx`: `useRequest(ctx).rawBody()` resolves to `raw` instead of
|
|
617
|
+
* reading the incoming request stream. To seed what `useBody()` (`@wooksjs/http-body`) parses,
|
|
618
|
+
* use its `seedBody()` — a parent's already parsed body is otherwise read through.
|
|
619
|
+
*
|
|
620
|
+
* Use it for a child event context (`new EventContext({ logger, parent })`) that runs a handler
|
|
621
|
+
* with its own payload: the child still reads everything else (request, headers, auth) through
|
|
622
|
+
* its parent, but never the parent's body. Call it before anything in the child reads the body.
|
|
623
|
+
*
|
|
624
|
+
* @param ctx - The context to seed — usually a child of the current HTTP event
|
|
625
|
+
* @param raw - The body bytes (a string is encoded as UTF-8)
|
|
626
|
+
*
|
|
547
627
|
* @example
|
|
548
628
|
* ```ts
|
|
549
|
-
* const {
|
|
629
|
+
* const child = new EventContext({ logger: parent.logger, parent })
|
|
630
|
+
* seedRawBody(child, JSON.stringify({ ids: [1, 2] }))
|
|
631
|
+
* await run(child, () => useRequest().rawBody()) // Buffer of '{"ids":[1,2]}'
|
|
550
632
|
* ```
|
|
551
633
|
*/
|
|
552
|
-
function
|
|
553
|
-
|
|
634
|
+
function seedRawBody(ctx, raw) {
|
|
635
|
+
ctx.setOwn(rawBodySlot, Promise.resolve(Buffer$1.isBuffer(raw) ? raw : Buffer$1.from(raw)));
|
|
636
|
+
if (ctx.parent) ctx.getOwn(useRequest._slot);
|
|
554
637
|
}
|
|
555
638
|
|
|
556
639
|
//#endregion
|
|
@@ -711,6 +794,70 @@ const cookieAttrFunc = {
|
|
|
711
794
|
sameSite: (v) => v ? `SameSite=${typeof v === "string" ? v : "Strict"}` : ""
|
|
712
795
|
};
|
|
713
796
|
|
|
797
|
+
//#endregion
|
|
798
|
+
//#region packages/event-http/src/response/prerender.ts
|
|
799
|
+
const registry = /* @__PURE__ */ new WeakMap();
|
|
800
|
+
function weakEtag(json) {
|
|
801
|
+
return `W/"${createHash("sha256").update(json).digest("base64url").slice(0, 22)}"`;
|
|
802
|
+
}
|
|
803
|
+
/**
|
|
804
|
+
* Serializes `obj` to JSON once and remembers the string by object identity. Whenever a handler
|
|
805
|
+
* (or an interceptor) responds with that same object, the response reuses the stored JSON instead
|
|
806
|
+
* of calling `JSON.stringify` again. Use it for large, long-lived response objects that many
|
|
807
|
+
* requests return as is — e.g. a schema or metadata envelope built once and cached.
|
|
808
|
+
*
|
|
809
|
+
* With `{ etag: true }` a weak `ETag` is derived from the serialized bytes; a `GET`/`HEAD`
|
|
810
|
+
* that responds `200` with the object sets the `ETag` header and answers a matching
|
|
811
|
+
* `If-None-Match` with `304 Not Modified` (no body; other headers such as `Cache-Control`
|
|
812
|
+
* and `Vary` are kept). Error responses never become `304`. An `ETag` header set explicitly
|
|
813
|
+
* on the response wins and disables the `304` handling for that response.
|
|
814
|
+
*
|
|
815
|
+
* **Contract: never mutate a registered object (or anything it references).** The stored JSON
|
|
816
|
+
* is not refreshed, so a mutation would keep serving the old bytes. Build a new object instead —
|
|
817
|
+
* freezing registered objects (`Object.freeze`, deeply) in development makes violations throw.
|
|
818
|
+
*
|
|
819
|
+
* Calling it again for the same object is a no-op (it adds the `ETag` if it was not requested
|
|
820
|
+
* the first time). Entries are held weakly and disappear with the object.
|
|
821
|
+
*
|
|
822
|
+
* @param obj - A JSON-serializable object or array
|
|
823
|
+
* @param options - `{ etag: true }` to also compute a weak ETag
|
|
824
|
+
* @returns The same `obj`, so it can wrap a return value
|
|
825
|
+
*
|
|
826
|
+
* @example
|
|
827
|
+
* ```ts
|
|
828
|
+
* const envelope = prerenderJson(Object.freeze(buildMeta()), { etag: true })
|
|
829
|
+
* app.get('/meta', () => {
|
|
830
|
+
* useResponse().setHeader('cache-control', 'private, no-cache')
|
|
831
|
+
* return envelope // sent without re-serializing; 304 on a matching If-None-Match
|
|
832
|
+
* })
|
|
833
|
+
* ```
|
|
834
|
+
*/
|
|
835
|
+
function prerenderJson(obj, options) {
|
|
836
|
+
let entry = registry.get(obj);
|
|
837
|
+
if (!entry) {
|
|
838
|
+
const json = JSON.stringify(obj);
|
|
839
|
+
if (typeof json !== "string") throw new TypeError("prerenderJson: value does not serialize to JSON");
|
|
840
|
+
entry = { json };
|
|
841
|
+
registry.set(obj, entry);
|
|
842
|
+
}
|
|
843
|
+
if (options?.etag && !entry.etag) entry.etag = weakEtag(entry.json);
|
|
844
|
+
return obj;
|
|
845
|
+
}
|
|
846
|
+
/** @internal Returns the pre-serialized JSON (and weak ETag) registered for `obj`, if any. */
|
|
847
|
+
function getPrerenderedJson(obj) {
|
|
848
|
+
return registry.get(obj);
|
|
849
|
+
}
|
|
850
|
+
/** @internal Weak comparison of `etag` against an `If-None-Match` header value. */
|
|
851
|
+
function ifNoneMatchHas(header, etag) {
|
|
852
|
+
if (!header) return false;
|
|
853
|
+
const opaque = etag.startsWith("W/") ? etag.slice(2) : etag;
|
|
854
|
+
for (const part of header.split(",")) {
|
|
855
|
+
const tag = part.trim();
|
|
856
|
+
if (tag === "*" || (tag.startsWith("W/") ? tag.slice(2) : tag) === opaque) return true;
|
|
857
|
+
}
|
|
858
|
+
return false;
|
|
859
|
+
}
|
|
860
|
+
|
|
714
861
|
//#endregion
|
|
715
862
|
//#region packages/event-http/src/response/http-response.ts
|
|
716
863
|
const hasFetchResponse = typeof globalThis.Response === "function";
|
|
@@ -749,8 +896,6 @@ var HttpResponse = class {
|
|
|
749
896
|
this._status = 0;
|
|
750
897
|
this._body = void 0;
|
|
751
898
|
this._headers = {};
|
|
752
|
-
this._cookies = {};
|
|
753
|
-
this._rawCookies = [];
|
|
754
899
|
this._hasCookies = false;
|
|
755
900
|
this._responded = false;
|
|
756
901
|
if (defaultHeaders) for (const key in defaultHeaders) this._headers[key] = defaultHeaders[key];
|
|
@@ -818,7 +963,7 @@ var HttpResponse = class {
|
|
|
818
963
|
}
|
|
819
964
|
/** Sets an outgoing `Set-Cookie` header with optional attributes (chainable). */
|
|
820
965
|
setCookie(name, value, attrs) {
|
|
821
|
-
this._cookies[name] = {
|
|
966
|
+
(this._cookies ??= {})[name] = {
|
|
822
967
|
value,
|
|
823
968
|
attrs: attrs || {}
|
|
824
969
|
};
|
|
@@ -827,23 +972,23 @@ var HttpResponse = class {
|
|
|
827
972
|
}
|
|
828
973
|
/** Returns a previously set cookie's data, or `undefined` if not set. */
|
|
829
974
|
getCookie(name) {
|
|
830
|
-
return this._cookies[name];
|
|
975
|
+
return this._cookies?.[name];
|
|
831
976
|
}
|
|
832
977
|
/** Removes a cookie from the outgoing set list (chainable). */
|
|
833
978
|
removeCookie(name) {
|
|
834
|
-
delete this._cookies[name];
|
|
979
|
+
if (this._cookies) delete this._cookies[name];
|
|
835
980
|
return this;
|
|
836
981
|
}
|
|
837
982
|
/** Removes all outgoing cookies (chainable). */
|
|
838
983
|
clearCookies() {
|
|
839
|
-
this._cookies =
|
|
840
|
-
this._rawCookies =
|
|
984
|
+
this._cookies = void 0;
|
|
985
|
+
this._rawCookies = void 0;
|
|
841
986
|
this._hasCookies = false;
|
|
842
987
|
return this;
|
|
843
988
|
}
|
|
844
989
|
/** Appends a raw `Set-Cookie` header string (chainable). Use when you need full control over the cookie format. */
|
|
845
990
|
setCookieRaw(rawValue) {
|
|
846
|
-
this._rawCookies.push(rawValue);
|
|
991
|
+
(this._rawCookies ??= []).push(rawValue);
|
|
847
992
|
this._hasCookies = true;
|
|
848
993
|
return this;
|
|
849
994
|
}
|
|
@@ -858,8 +1003,10 @@ var HttpResponse = class {
|
|
|
858
1003
|
*/
|
|
859
1004
|
getSetCookieStrings() {
|
|
860
1005
|
const rendered = [];
|
|
861
|
-
|
|
862
|
-
|
|
1006
|
+
if (this._cookies) {
|
|
1007
|
+
for (const [name, data] of Object.entries(this._cookies)) if (data) rendered.push(renderCookie(name, data));
|
|
1008
|
+
}
|
|
1009
|
+
if (this._rawCookies) rendered.push(...this._rawCookies);
|
|
863
1010
|
return rendered;
|
|
864
1011
|
}
|
|
865
1012
|
/** Sets the `Cache-Control` header from a directive object (chainable). */
|
|
@@ -921,6 +1068,10 @@ var HttpResponse = class {
|
|
|
921
1068
|
}
|
|
922
1069
|
const rendered = this.renderBody();
|
|
923
1070
|
this.autoStatus(!!rendered);
|
|
1071
|
+
if (this.applyPrerenderedEtag()) return new globalThis.Response(null, {
|
|
1072
|
+
status: this._status,
|
|
1073
|
+
headers: this._buildWebHeaders()
|
|
1074
|
+
});
|
|
924
1075
|
if (rendered) {
|
|
925
1076
|
const contentLength = typeof rendered === "string" ? Buffer.byteLength(rendered) : rendered.byteLength;
|
|
926
1077
|
this._headers["content-length"] = contentLength.toString();
|
|
@@ -969,6 +1120,11 @@ var HttpResponse = class {
|
|
|
969
1120
|
if (body instanceof Uint8Array) return body;
|
|
970
1121
|
if (typeof body === "object") {
|
|
971
1122
|
if (!this._headers["content-type"]) this._headers["content-type"] = "application/json";
|
|
1123
|
+
const prerendered = getPrerenderedJson(body);
|
|
1124
|
+
if (prerendered) {
|
|
1125
|
+
this._prerenderedEtag = prerendered.etag;
|
|
1126
|
+
return prerendered.json;
|
|
1127
|
+
}
|
|
972
1128
|
return JSON.stringify(body);
|
|
973
1129
|
}
|
|
974
1130
|
throw new Error(`Unsupported body format "${typeof body}"`);
|
|
@@ -1020,6 +1176,31 @@ var HttpResponse = class {
|
|
|
1020
1176
|
}
|
|
1021
1177
|
this._hasCookies = false;
|
|
1022
1178
|
}
|
|
1179
|
+
/**
|
|
1180
|
+
* For a body registered with `prerenderJson(obj, { etag: true })`: sets the `ETag` header on a
|
|
1181
|
+
* GET/HEAD 2xx response (unless one was set explicitly) and turns a `200` whose `If-None-Match`
|
|
1182
|
+
* matches into a bodiless `304`. Returns `true` when the response became `304`.
|
|
1183
|
+
* Other methods get no ETag (a validator on e.g. a PUT response would describe the stored resource).
|
|
1184
|
+
*/
|
|
1185
|
+
applyPrerenderedEtag() {
|
|
1186
|
+
const etag = this._prerenderedEtag;
|
|
1187
|
+
if (!etag) return false;
|
|
1188
|
+
this._prerenderedEtag = void 0;
|
|
1189
|
+
const method = this._req.method;
|
|
1190
|
+
if (method !== "GET" && method !== "HEAD" || this._status < 200 || this._status > 299 || this.hasHeaderIgnoreCase("etag")) return false;
|
|
1191
|
+
this._headers.etag = etag;
|
|
1192
|
+
if (this._status !== EHttpStatusCode.OK || !ifNoneMatchHas(this._req.headers["if-none-match"], etag)) return false;
|
|
1193
|
+
this._status = EHttpStatusCode.NotModified;
|
|
1194
|
+
for (const key in this._headers) {
|
|
1195
|
+
const lower = key.toLowerCase();
|
|
1196
|
+
if (lower === "content-type" || lower === "content-length") delete this._headers[key];
|
|
1197
|
+
}
|
|
1198
|
+
return true;
|
|
1199
|
+
}
|
|
1200
|
+
hasHeaderIgnoreCase(name) {
|
|
1201
|
+
for (const key in this._headers) if (key.toLowerCase() === name) return true;
|
|
1202
|
+
return false;
|
|
1203
|
+
}
|
|
1023
1204
|
autoStatus(hasBody) {
|
|
1024
1205
|
if (this._status) return;
|
|
1025
1206
|
if (!hasBody) {
|
|
@@ -1067,6 +1248,10 @@ var HttpResponse = class {
|
|
|
1067
1248
|
sendRegular(method) {
|
|
1068
1249
|
const renderedBody = this.renderBody();
|
|
1069
1250
|
this.autoStatus(!!renderedBody);
|
|
1251
|
+
if (this.applyPrerenderedEtag()) {
|
|
1252
|
+
this._res.writeHead(this._status, this._headers).end();
|
|
1253
|
+
return;
|
|
1254
|
+
}
|
|
1070
1255
|
const contentLength = typeof renderedBody === "string" ? Buffer.byteLength(renderedBody) : renderedBody.byteLength;
|
|
1071
1256
|
this._headers["content-length"] = contentLength.toString();
|
|
1072
1257
|
this._res.writeHead(this._status, this._headers).end(method === "HEAD" ? "" : renderedBody);
|
|
@@ -1445,7 +1630,7 @@ function escapeHtml(value) {
|
|
|
1445
1630
|
//#endregion
|
|
1446
1631
|
//#region packages/event-http/src/response/wooks-http-response.ts
|
|
1447
1632
|
let framework = {
|
|
1448
|
-
version: "0.7.
|
|
1633
|
+
version: "0.7.27",
|
|
1449
1634
|
poweredBy: "wooksjs",
|
|
1450
1635
|
link: "https://wooks.moost.org/",
|
|
1451
1636
|
image: "https://wooks.moost.org/wooks-full-logo.svg"
|
|
@@ -1469,7 +1654,8 @@ var WooksHttpResponse = class extends HttpResponse {
|
|
|
1469
1654
|
}
|
|
1470
1655
|
renderError(data, ctx) {
|
|
1471
1656
|
this._status = data.statusCode || 500;
|
|
1472
|
-
const
|
|
1657
|
+
const accept = ctx.get(httpKind.keys.req).headers.accept;
|
|
1658
|
+
const has = (type) => acceptHeaderHas(accept, type);
|
|
1473
1659
|
if (has("json")) {
|
|
1474
1660
|
this._headers["content-type"] = "application/json";
|
|
1475
1661
|
this._body = JSON.stringify(data);
|
|
@@ -1765,33 +1951,19 @@ var WooksHttp = class extends WooksAdapterBase {
|
|
|
1765
1951
|
}
|
|
1766
1952
|
}
|
|
1767
1953
|
async processAsyncResult(promise, handlers, startIndex, ctx, response) {
|
|
1768
|
-
try {
|
|
1769
|
-
const result = await promise;
|
|
1770
|
-
|
|
1954
|
+
for (let i = startIndex; i < handlers.length; i++) try {
|
|
1955
|
+
const result = await (i === startIndex ? promise : handlers[i]());
|
|
1956
|
+
const sent = this.respond(result, response, ctx);
|
|
1957
|
+
if (sent) await sent;
|
|
1771
1958
|
return result;
|
|
1772
1959
|
} catch (error) {
|
|
1773
|
-
const isLastHandler = startIndex === handlers.length - 1;
|
|
1774
1960
|
if (!(error instanceof HttpError)) this.logger.error(`Uncaught route handler exception: ${ctx.get(httpKind.keys.req)?.url || ""}`, error);
|
|
1775
|
-
if (
|
|
1776
|
-
|
|
1961
|
+
if (i === handlers.length - 1) {
|
|
1962
|
+
const sent = this.respond(error, response, ctx);
|
|
1963
|
+
if (sent) await sent;
|
|
1777
1964
|
return error;
|
|
1778
1965
|
}
|
|
1779
1966
|
}
|
|
1780
|
-
for (let i = startIndex + 1; i < handlers.length; i++) {
|
|
1781
|
-
const handler = handlers[i];
|
|
1782
|
-
const isLastHandler = i === handlers.length - 1;
|
|
1783
|
-
try {
|
|
1784
|
-
const result = await handler();
|
|
1785
|
-
await this.respond(result, response, ctx);
|
|
1786
|
-
return result;
|
|
1787
|
-
} catch (error) {
|
|
1788
|
-
if (!(error instanceof HttpError)) this.logger.error(`Uncaught route handler exception: ${ctx.get(httpKind.keys.req)?.url || ""}`, error);
|
|
1789
|
-
if (isLastHandler) {
|
|
1790
|
-
await this.respond(error, response, ctx);
|
|
1791
|
-
return error;
|
|
1792
|
-
}
|
|
1793
|
-
}
|
|
1794
|
-
}
|
|
1795
1967
|
}
|
|
1796
1968
|
/**
|
|
1797
1969
|
* Programmatic route invocation using the Web Standard fetch API.
|
|
@@ -1855,7 +2027,7 @@ var WooksHttp = class extends WooksAdapterBase {
|
|
|
1855
2027
|
requestLimits
|
|
1856
2028
|
}, async () => {
|
|
1857
2029
|
const ctx = current();
|
|
1858
|
-
if (bodyBuffer) ctx
|
|
2030
|
+
if (bodyBuffer) seedRawBody(ctx, bodyBuffer);
|
|
1859
2031
|
try {
|
|
1860
2032
|
const handlers = this.wooks.lookupHandlers(method, pathname, ctx);
|
|
1861
2033
|
if (handlers) {
|
|
@@ -2059,12 +2231,9 @@ function prepareTestHttpContext(options) {
|
|
|
2059
2231
|
requestLimits: options.requestLimits
|
|
2060
2232
|
});
|
|
2061
2233
|
if (options.params) ctx.set(routeParamsKey, options.params);
|
|
2062
|
-
if (options.rawBody !== void 0)
|
|
2063
|
-
const buf = Buffer$1.isBuffer(options.rawBody) ? options.rawBody : Buffer$1.from(options.rawBody);
|
|
2064
|
-
ctx.set(rawBodySlot, Promise.resolve(buf));
|
|
2065
|
-
}
|
|
2234
|
+
if (options.rawBody !== void 0) seedRawBody(ctx, options.rawBody);
|
|
2066
2235
|
return (cb) => run(ctx, cb);
|
|
2067
2236
|
}
|
|
2068
2237
|
|
|
2069
2238
|
//#endregion
|
|
2070
|
-
export { DEFAULT_FORWARD_HEADERS, DEFAULT_LIMITS, EHttpStatusCode, HttpError, HttpResponse, WooksHttp, WooksHttpResponse, WooksURLSearchParams, createHttpApp, createHttpContext, httpKind, httpStatusCodes, prepareTestHttpContext, rawBodySlot, recordToWebHeaders, renderCacheControl, securityHeaders, useAccept, useAuthorization, useCookies, useHeaders, useHttpContext, useLogger, useRequest, useResponse, useRouteParams, useUrlParams };
|
|
2239
|
+
export { DEFAULT_FORWARD_HEADERS, DEFAULT_LIMITS, EHttpStatusCode, HttpError, HttpResponse, WooksHttp, WooksHttpResponse, WooksURLSearchParams, createHttpApp, createHttpContext, httpKind, httpStatusCodes, prepareTestHttpContext, prerenderJson, rawBodySlot, recordToWebHeaders, renderCacheControl, securityHeaders, seedRawBody, useAccept, useAuthorization, useCookies, useHeaders, useHttpContext, useLogger, useRequest, useResponse, useRouteParams, useUrlParams };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wooksjs/event-http",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.27",
|
|
4
4
|
"description": "@wooksjs/event-http",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"api",
|
|
@@ -42,14 +42,14 @@
|
|
|
42
42
|
"devDependencies": {
|
|
43
43
|
"typescript": "^5.9.3",
|
|
44
44
|
"vitest": "^3.2.7",
|
|
45
|
-
"@wooksjs/event-core": "^0.7.
|
|
46
|
-
"wooks": "^0.7.
|
|
45
|
+
"@wooksjs/event-core": "^0.7.27",
|
|
46
|
+
"wooks": "^0.7.27"
|
|
47
47
|
},
|
|
48
48
|
"peerDependencies": {
|
|
49
49
|
"@prostojs/logger": "^0.4.3",
|
|
50
|
-
"@prostojs/router": "^0.3.
|
|
51
|
-
"
|
|
52
|
-
"
|
|
50
|
+
"@prostojs/router": "^0.3.5",
|
|
51
|
+
"wooks": "^0.7.27",
|
|
52
|
+
"@wooksjs/event-core": "^0.7.27"
|
|
53
53
|
},
|
|
54
54
|
"scripts": {
|
|
55
55
|
"build": "rolldown -c ../../rolldown.config.mjs"
|