h3 1.6.6 → 1.7.1
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/README.md +43 -19
- package/dist/index.cjs +77 -38
- package/dist/index.d.ts +34 -23
- package/dist/index.mjs +77 -38
- package/package.json +22 -22
package/README.md
CHANGED
|
@@ -68,7 +68,7 @@ listen(toNodeListener(app));
|
|
|
68
68
|
|
|
69
69
|
## Router
|
|
70
70
|
|
|
71
|
-
The `app` instance created by `h3` uses a middleware stack (see [how it works](
|
|
71
|
+
The `app` instance created by `h3` uses a middleware stack (see [how it works](./src/app.ts)) with the ability to match route prefix and apply matched middleware.
|
|
72
72
|
|
|
73
73
|
To opt-in using a more advanced and convenient routing system, we can create a router instance and register it to app instance.
|
|
74
74
|
|
|
@@ -98,36 +98,60 @@ Routes are internally stored in a [Radix Tree](https://en.wikipedia.org/wiki/Rad
|
|
|
98
98
|
|
|
99
99
|
```js
|
|
100
100
|
// Handle can directly return object or Promise<object> for JSON response
|
|
101
|
-
app.use(
|
|
101
|
+
app.use(
|
|
102
|
+
"/api",
|
|
103
|
+
eventHandler((event) => ({ url: event.node.req.url }))
|
|
104
|
+
);
|
|
102
105
|
|
|
103
106
|
// We can have better matching other than quick prefix match
|
|
104
|
-
app.use(
|
|
107
|
+
app.use(
|
|
108
|
+
"/odd",
|
|
109
|
+
eventHandler(() => "Is odd!"),
|
|
110
|
+
{ match: (url) => url.substr(1) % 2 }
|
|
111
|
+
);
|
|
105
112
|
|
|
106
113
|
// Handle can directly return string for HTML response
|
|
107
|
-
app.use(eventHandler(() =>
|
|
114
|
+
app.use(eventHandler(() => "<h1>Hello world!</h1>"));
|
|
108
115
|
|
|
109
116
|
// We can chain calls to .use()
|
|
110
|
-
app
|
|
111
|
-
|
|
117
|
+
app
|
|
118
|
+
.use(
|
|
119
|
+
"/1",
|
|
120
|
+
eventHandler(() => "<h1>Hello world!</h1>")
|
|
121
|
+
)
|
|
122
|
+
.use(
|
|
123
|
+
"/2",
|
|
124
|
+
eventHandler(() => "<h1>Goodbye!</h1>")
|
|
125
|
+
);
|
|
112
126
|
|
|
113
127
|
// We can proxy requests and rewrite cookie's domain and path
|
|
114
|
-
app.use(
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
128
|
+
app.use(
|
|
129
|
+
"/api",
|
|
130
|
+
eventHandler((event) =>
|
|
131
|
+
proxyRequest(event, "https://example.com", {
|
|
132
|
+
// f.e. keep one domain unchanged, rewrite one domain and remove other domains
|
|
133
|
+
cookieDomainRewrite: {
|
|
134
|
+
"example.com": "example.com",
|
|
135
|
+
"example.com": "somecompany.co.uk",
|
|
136
|
+
"*": "",
|
|
137
|
+
},
|
|
138
|
+
cookiePathRewrite: {
|
|
139
|
+
"/": "/api",
|
|
140
|
+
},
|
|
141
|
+
})
|
|
142
|
+
)
|
|
143
|
+
);
|
|
125
144
|
|
|
126
145
|
// Legacy middleware with 3rd argument are automatically promisified
|
|
127
|
-
app.use(
|
|
146
|
+
app.use(
|
|
147
|
+
fromNodeMiddleware((req, res, next) => {
|
|
148
|
+
req.setHeader("x-foo", "bar");
|
|
149
|
+
next();
|
|
150
|
+
})
|
|
151
|
+
);
|
|
128
152
|
|
|
129
153
|
// Lazy loaded routes using { lazy: true }
|
|
130
|
-
app.use(
|
|
154
|
+
app.use("/big", () => import("./big-handler"), { lazy: true });
|
|
131
155
|
```
|
|
132
156
|
|
|
133
157
|
## Utilities
|
package/dist/index.cjs
CHANGED
|
@@ -108,7 +108,6 @@ class H3Error extends Error {
|
|
|
108
108
|
this.statusCode = 500;
|
|
109
109
|
this.fatal = false;
|
|
110
110
|
this.unhandled = false;
|
|
111
|
-
this.statusMessage = void 0;
|
|
112
111
|
}
|
|
113
112
|
toJSON() {
|
|
114
113
|
const obj = {
|
|
@@ -133,8 +132,8 @@ function createError(input) {
|
|
|
133
132
|
return input;
|
|
134
133
|
}
|
|
135
134
|
const err = new H3Error(
|
|
136
|
-
input.message ?? input.statusMessage,
|
|
137
|
-
// @ts-ignore
|
|
135
|
+
input.message ?? input.statusMessage ?? "",
|
|
136
|
+
// @ts-ignore https://v8.dev/features/error-cause
|
|
138
137
|
input.cause ? { cause: input.cause } : void 0
|
|
139
138
|
);
|
|
140
139
|
if ("stack" in input) {
|
|
@@ -169,7 +168,7 @@ function createError(input) {
|
|
|
169
168
|
const sanitizedMessage = sanitizeStatusMessage(err.statusMessage);
|
|
170
169
|
if (sanitizedMessage !== originalMessage) {
|
|
171
170
|
console.warn(
|
|
172
|
-
"[h3] Please prefer using `message` for longer error messages instead of `statusMessage`. In the future `statusMessage` will be sanitized by default."
|
|
171
|
+
"[h3] Please prefer using `message` for longer error messages instead of `statusMessage`. In the future, `statusMessage` will be sanitized by default."
|
|
173
172
|
);
|
|
174
173
|
}
|
|
175
174
|
}
|
|
@@ -182,7 +181,7 @@ function createError(input) {
|
|
|
182
181
|
return err;
|
|
183
182
|
}
|
|
184
183
|
function sendError(event, error, debug) {
|
|
185
|
-
if (event.
|
|
184
|
+
if (event.handled) {
|
|
186
185
|
return;
|
|
187
186
|
}
|
|
188
187
|
const h3Error = isError(error) ? error : createError(error);
|
|
@@ -195,7 +194,7 @@ function sendError(event, error, debug) {
|
|
|
195
194
|
if (debug) {
|
|
196
195
|
responseBody.stack = (h3Error.stack || "").split("\n").map((l) => l.trim());
|
|
197
196
|
}
|
|
198
|
-
if (event.
|
|
197
|
+
if (event.handled) {
|
|
199
198
|
return;
|
|
200
199
|
}
|
|
201
200
|
const _code = Number.parseInt(h3Error.statusCode);
|
|
@@ -291,9 +290,15 @@ function readRawBody(event, encoding = "utf8") {
|
|
|
291
290
|
assertMethod(event, PayloadMethods$1);
|
|
292
291
|
const _rawBody = event.node.req[RawBodySymbol] || event.node.req.body;
|
|
293
292
|
if (_rawBody) {
|
|
294
|
-
const promise2 = Promise.resolve(_rawBody).then(
|
|
295
|
-
(
|
|
296
|
-
|
|
293
|
+
const promise2 = Promise.resolve(_rawBody).then((_resolved) => {
|
|
294
|
+
if (Buffer.isBuffer(_resolved)) {
|
|
295
|
+
return _resolved;
|
|
296
|
+
}
|
|
297
|
+
if (_resolved.constructor === Object) {
|
|
298
|
+
return Buffer.from(JSON.stringify(_resolved));
|
|
299
|
+
}
|
|
300
|
+
return Buffer.from(_resolved);
|
|
301
|
+
});
|
|
297
302
|
return encoding ? promise2.then((buff) => buff.toString(encoding)) : promise2;
|
|
298
303
|
}
|
|
299
304
|
if (!Number.parseInt(event.node.req.headers["content-length"] || "")) {
|
|
@@ -378,7 +383,9 @@ function handleCacheHeaders(event, opts) {
|
|
|
378
383
|
event.node.res.setHeader("cache-control", cacheControls.join(", "));
|
|
379
384
|
if (cacheMatched) {
|
|
380
385
|
event.node.res.statusCode = 304;
|
|
381
|
-
event.
|
|
386
|
+
if (!event.handled) {
|
|
387
|
+
event.node.res.end();
|
|
388
|
+
}
|
|
382
389
|
return true;
|
|
383
390
|
}
|
|
384
391
|
return false;
|
|
@@ -527,6 +534,7 @@ async function sendProxy(event, target, opts = {}) {
|
|
|
527
534
|
event.node.res.statusCode
|
|
528
535
|
);
|
|
529
536
|
event.node.res.statusMessage = sanitizeStatusMessage(response.statusText);
|
|
537
|
+
const cookies = [];
|
|
530
538
|
for (const [key, value] of response.headers.entries()) {
|
|
531
539
|
if (key === "content-encoding") {
|
|
532
540
|
continue;
|
|
@@ -535,7 +543,15 @@ async function sendProxy(event, target, opts = {}) {
|
|
|
535
543
|
continue;
|
|
536
544
|
}
|
|
537
545
|
if (key === "set-cookie") {
|
|
538
|
-
|
|
546
|
+
cookies.push(...splitCookiesString(value));
|
|
547
|
+
continue;
|
|
548
|
+
}
|
|
549
|
+
event.node.res.setHeader(key, value);
|
|
550
|
+
}
|
|
551
|
+
if (cookies.length > 0) {
|
|
552
|
+
event.node.res.setHeader(
|
|
553
|
+
"set-cookie",
|
|
554
|
+
cookies.map((cookie) => {
|
|
539
555
|
if (opts.cookieDomainRewrite) {
|
|
540
556
|
cookie = rewriteCookieProperty(
|
|
541
557
|
cookie,
|
|
@@ -551,15 +567,18 @@ async function sendProxy(event, target, opts = {}) {
|
|
|
551
567
|
);
|
|
552
568
|
}
|
|
553
569
|
return cookie;
|
|
554
|
-
})
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
570
|
+
})
|
|
571
|
+
);
|
|
572
|
+
}
|
|
573
|
+
if (opts.onResponse) {
|
|
574
|
+
await opts.onResponse(event, response);
|
|
559
575
|
}
|
|
560
576
|
if (response._data !== void 0) {
|
|
561
577
|
return response._data;
|
|
562
578
|
}
|
|
579
|
+
if (event.handled) {
|
|
580
|
+
return;
|
|
581
|
+
}
|
|
563
582
|
if (opts.sendStream === false) {
|
|
564
583
|
const data = new Uint8Array(await response.arrayBuffer());
|
|
565
584
|
return event.node.res.end(data);
|
|
@@ -618,14 +637,16 @@ function rewriteCookieProperty(header, map, property) {
|
|
|
618
637
|
);
|
|
619
638
|
}
|
|
620
639
|
|
|
621
|
-
const defer = typeof setImmediate
|
|
640
|
+
const defer = typeof setImmediate === "undefined" ? (fn) => fn() : setImmediate;
|
|
622
641
|
function send(event, data, type) {
|
|
623
642
|
if (type) {
|
|
624
643
|
defaultContentType(event, type);
|
|
625
644
|
}
|
|
626
645
|
return new Promise((resolve) => {
|
|
627
646
|
defer(() => {
|
|
628
|
-
event.
|
|
647
|
+
if (!event.handled) {
|
|
648
|
+
event.node.res.end(data);
|
|
649
|
+
}
|
|
629
650
|
resolve();
|
|
630
651
|
});
|
|
631
652
|
});
|
|
@@ -635,7 +656,9 @@ function sendNoContent(event, code = 204) {
|
|
|
635
656
|
if (event.node.res.statusCode === 204) {
|
|
636
657
|
event.node.res.removeHeader("content-length");
|
|
637
658
|
}
|
|
638
|
-
event.
|
|
659
|
+
if (!event.handled) {
|
|
660
|
+
event.node.res.end();
|
|
661
|
+
}
|
|
639
662
|
}
|
|
640
663
|
function setResponseStatus(event, code, text) {
|
|
641
664
|
if (code) {
|
|
@@ -818,7 +841,7 @@ async function getSession(event, config) {
|
|
|
818
841
|
Object.assign(session, unsealed);
|
|
819
842
|
}
|
|
820
843
|
if (!session.id) {
|
|
821
|
-
session.id = (config.crypto || crypto__default).randomUUID();
|
|
844
|
+
session.id = config.generateId?.() ?? (config.crypto || crypto__default).randomUUID();
|
|
822
845
|
session.createdAt = Date.now();
|
|
823
846
|
await updateSession(event, config);
|
|
824
847
|
}
|
|
@@ -1089,12 +1112,16 @@ class H3Response {
|
|
|
1089
1112
|
class H3Event {
|
|
1090
1113
|
constructor(req, res) {
|
|
1091
1114
|
this["__is_event__"] = true;
|
|
1115
|
+
this._handled = false;
|
|
1092
1116
|
this.context = {};
|
|
1093
1117
|
this.node = { req, res };
|
|
1094
1118
|
}
|
|
1095
1119
|
get path() {
|
|
1096
1120
|
return getRequestPath(this);
|
|
1097
1121
|
}
|
|
1122
|
+
get handled() {
|
|
1123
|
+
return this._handled || this.node.res.writableEnded || this.node.res.headersSent;
|
|
1124
|
+
}
|
|
1098
1125
|
/** @deprecated Please use `event.node.req` instead. **/
|
|
1099
1126
|
get req() {
|
|
1100
1127
|
return this.node.req;
|
|
@@ -1106,35 +1133,37 @@ class H3Event {
|
|
|
1106
1133
|
// Implementation of FetchEvent
|
|
1107
1134
|
respondWith(r) {
|
|
1108
1135
|
Promise.resolve(r).then((_response) => {
|
|
1109
|
-
if (this.
|
|
1136
|
+
if (this.handled) {
|
|
1110
1137
|
return;
|
|
1111
1138
|
}
|
|
1112
1139
|
const response = _response instanceof H3Response ? _response : new H3Response(_response);
|
|
1113
1140
|
for (const [key, value] of response.headers.entries()) {
|
|
1114
|
-
this.res.setHeader(key, value);
|
|
1141
|
+
this.node.res.setHeader(key, value);
|
|
1115
1142
|
}
|
|
1116
1143
|
if (response.status) {
|
|
1117
|
-
this.res.statusCode = sanitizeStatusCode(
|
|
1144
|
+
this.node.res.statusCode = sanitizeStatusCode(
|
|
1118
1145
|
response.status,
|
|
1119
|
-
this.res.statusCode
|
|
1146
|
+
this.node.res.statusCode
|
|
1120
1147
|
);
|
|
1121
1148
|
}
|
|
1122
1149
|
if (response.statusText) {
|
|
1123
|
-
this.res.statusMessage = sanitizeStatusMessage(
|
|
1150
|
+
this.node.res.statusMessage = sanitizeStatusMessage(
|
|
1151
|
+
response.statusText
|
|
1152
|
+
);
|
|
1124
1153
|
}
|
|
1125
1154
|
if (response.redirected) {
|
|
1126
|
-
this.res.setHeader("location", response.url);
|
|
1155
|
+
this.node.res.setHeader("location", response.url);
|
|
1127
1156
|
}
|
|
1128
1157
|
if (!response._body) {
|
|
1129
|
-
return this.res.end();
|
|
1158
|
+
return this.node.res.end();
|
|
1130
1159
|
}
|
|
1131
1160
|
if (typeof response._body === "string" || "buffer" in response._body || "byteLength" in response._body) {
|
|
1132
|
-
return this.res.end(response._body);
|
|
1161
|
+
return this.node.res.end(response._body);
|
|
1133
1162
|
}
|
|
1134
1163
|
if (!response.headers.has("content-type")) {
|
|
1135
1164
|
response.headers.set("content-type", MIMES.json);
|
|
1136
1165
|
}
|
|
1137
|
-
this.res.end(JSON.stringify(response._body));
|
|
1166
|
+
this.node.res.end(JSON.stringify(response._body));
|
|
1138
1167
|
});
|
|
1139
1168
|
}
|
|
1140
1169
|
}
|
|
@@ -1260,7 +1289,7 @@ function createAppEventHandler(stack, options) {
|
|
|
1260
1289
|
continue;
|
|
1261
1290
|
}
|
|
1262
1291
|
const val = await layer.handler(event);
|
|
1263
|
-
if (event.
|
|
1292
|
+
if (event.handled) {
|
|
1264
1293
|
return;
|
|
1265
1294
|
}
|
|
1266
1295
|
const type = typeof val;
|
|
@@ -1285,10 +1314,10 @@ function createAppEventHandler(stack, options) {
|
|
|
1285
1314
|
}
|
|
1286
1315
|
}
|
|
1287
1316
|
}
|
|
1288
|
-
if (!event.
|
|
1317
|
+
if (!event.handled) {
|
|
1289
1318
|
throw createError({
|
|
1290
1319
|
statusCode: 404,
|
|
1291
|
-
statusMessage: `Cannot find any
|
|
1320
|
+
statusMessage: `Cannot find any path matching ${event.node.req.url || "/"}.`
|
|
1292
1321
|
});
|
|
1293
1322
|
}
|
|
1294
1323
|
});
|
|
@@ -1436,15 +1465,25 @@ function createRouter(opts = {}) {
|
|
|
1436
1465
|
const method = (event.node.req.method || "get").toLowerCase();
|
|
1437
1466
|
const handler = matched.handlers[method] || matched.handlers.all;
|
|
1438
1467
|
if (!handler) {
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1468
|
+
if (opts.preemptive || opts.preemtive) {
|
|
1469
|
+
throw createError({
|
|
1470
|
+
statusCode: 405,
|
|
1471
|
+
name: "Method Not Allowed",
|
|
1472
|
+
statusMessage: `Method ${method} is not allowed on this route.`
|
|
1473
|
+
});
|
|
1474
|
+
} else {
|
|
1475
|
+
return;
|
|
1476
|
+
}
|
|
1444
1477
|
}
|
|
1445
1478
|
const params = matched.params || {};
|
|
1446
1479
|
event.context.params = params;
|
|
1447
|
-
return handler(event)
|
|
1480
|
+
return Promise.resolve(handler(event)).then((res) => {
|
|
1481
|
+
if (res === void 0 && (opts.preemptive || opts.preemtive)) {
|
|
1482
|
+
setResponseStatus(event, 204);
|
|
1483
|
+
return "";
|
|
1484
|
+
}
|
|
1485
|
+
return res;
|
|
1486
|
+
});
|
|
1448
1487
|
});
|
|
1449
1488
|
return router;
|
|
1450
1489
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -24,6 +24,8 @@ interface SessionConfig {
|
|
|
24
24
|
sessionHeader?: false | string;
|
|
25
25
|
seal?: SealOptions;
|
|
26
26
|
crypto?: Crypto;
|
|
27
|
+
/** Default is Crypto.randomUUID */
|
|
28
|
+
generateId?: () => string;
|
|
27
29
|
}
|
|
28
30
|
declare function useSession<T extends SessionDataT = SessionDataT>(event: H3Event, config: SessionConfig): Promise<{
|
|
29
31
|
readonly id: string | undefined;
|
|
@@ -105,10 +107,12 @@ interface NodeEventContext {
|
|
|
105
107
|
}
|
|
106
108
|
declare class H3Event implements Pick<FetchEvent, "respondWith"> {
|
|
107
109
|
"__is_event__": boolean;
|
|
110
|
+
_handled: boolean;
|
|
108
111
|
node: NodeEventContext;
|
|
109
112
|
context: H3EventContext;
|
|
110
113
|
constructor(req: IncomingMessage, res: ServerResponse);
|
|
111
114
|
get path(): string;
|
|
115
|
+
get handled(): boolean;
|
|
112
116
|
/** @deprecated Please use `event.node.req` instead. **/
|
|
113
117
|
get req(): IncomingMessage;
|
|
114
118
|
/** @deprecated Please use `event.node.res` instead. **/
|
|
@@ -166,44 +170,50 @@ declare function createAppEventHandler(stack: Stack, options: AppOptions): Event
|
|
|
166
170
|
* H3 Runtime Error
|
|
167
171
|
* @class
|
|
168
172
|
* @extends Error
|
|
169
|
-
* @property {
|
|
170
|
-
* @property {
|
|
171
|
-
* @property {
|
|
172
|
-
* @property {
|
|
173
|
-
* @property {
|
|
174
|
-
*
|
|
175
|
-
* @property {
|
|
173
|
+
* @property {number} statusCode - An integer indicating the HTTP response status code.
|
|
174
|
+
* @property {string} statusMessage - A string representing the HTTP status message.
|
|
175
|
+
* @property {boolean} fatal - Indicates if the error is a fatal error.
|
|
176
|
+
* @property {boolean} unhandled - Indicates if the error was unhandled and auto captured.
|
|
177
|
+
* @property {any} data - An extra data that will be included in the response.
|
|
178
|
+
* This can be used to pass additional information about the error.
|
|
179
|
+
* @property {boolean} internal - Setting this property to `true` will mark the error as an internal error.
|
|
176
180
|
*/
|
|
177
181
|
declare class H3Error extends Error {
|
|
178
182
|
static __h3_error__: boolean;
|
|
179
|
-
toJSON(): Pick<H3Error, "data" | "statusCode" | "statusMessage" | "message">;
|
|
180
183
|
statusCode: number;
|
|
181
184
|
fatal: boolean;
|
|
182
185
|
unhandled: boolean;
|
|
183
186
|
statusMessage?: string;
|
|
184
187
|
data?: any;
|
|
188
|
+
toJSON(): Pick<H3Error, "data" | "statusCode" | "statusMessage" | "message">;
|
|
185
189
|
}
|
|
186
190
|
/**
|
|
187
|
-
* Creates new `Error` that can be used to handle both internal and runtime errors.
|
|
191
|
+
* Creates a new `Error` that can be used to handle both internal and runtime errors.
|
|
188
192
|
*
|
|
189
|
-
* @param input {Partial<H3Error>}
|
|
190
|
-
* @return {H3Error} An instance of
|
|
193
|
+
* @param input {string | (Partial<H3Error> & { status?: number; statusText?: string })} - The error message or an object containing error properties.
|
|
194
|
+
* @return {H3Error} - An instance of H3Error.
|
|
191
195
|
*/
|
|
192
196
|
declare function createError(input: string | (Partial<H3Error> & {
|
|
193
197
|
status?: number;
|
|
194
198
|
statusText?: string;
|
|
195
199
|
})): H3Error;
|
|
196
200
|
/**
|
|
197
|
-
*
|
|
198
|
-
*
|
|
199
|
-
*
|
|
201
|
+
* Receives an error and returns the corresponding response.
|
|
202
|
+
* H3 internally uses this function to handle unhandled errors.
|
|
203
|
+
* Note that calling this function will close the connection and no other data will be sent to the client afterwards.
|
|
200
204
|
*
|
|
201
|
-
@param event {H3Event} H3 event or req passed by h3 handler
|
|
202
|
-
* @param error {H3Error
|
|
203
|
-
* @param debug {
|
|
204
|
-
*
|
|
205
|
+
* @param event {H3Event} - H3 event or req passed by h3 handler.
|
|
206
|
+
* @param error {Error | H3Error} - The raised error.
|
|
207
|
+
* @param debug {boolean} - Whether the application is in debug mode.
|
|
208
|
+
* In the debug mode, the stack trace of errors will be returned in the response.
|
|
205
209
|
*/
|
|
206
210
|
declare function sendError(event: H3Event, error: Error | H3Error, debug?: boolean): void;
|
|
211
|
+
/**
|
|
212
|
+
* Checks if the given input is an instance of H3Error.
|
|
213
|
+
*
|
|
214
|
+
* @param input {*} - The input to check.
|
|
215
|
+
* @return {boolean} - Returns true if the input is an instance of H3Error, false otherwise.
|
|
216
|
+
*/
|
|
207
217
|
declare function isError(input: any): input is H3Error;
|
|
208
218
|
|
|
209
219
|
declare function useBase(base: string, handler: EventHandler): EventHandler;
|
|
@@ -313,19 +323,20 @@ interface ProxyOptions {
|
|
|
313
323
|
sendStream?: boolean;
|
|
314
324
|
cookieDomainRewrite?: string | Record<string, string>;
|
|
315
325
|
cookiePathRewrite?: string | Record<string, string>;
|
|
326
|
+
onResponse?: (event: H3Event, response: Response) => void;
|
|
316
327
|
}
|
|
317
328
|
declare function proxyRequest(event: H3Event, target: string, opts?: ProxyOptions): Promise<any>;
|
|
318
329
|
declare function sendProxy(event: H3Event, target: string, opts?: ProxyOptions): Promise<any>;
|
|
319
330
|
declare function getProxyRequestHeaders(event: H3Event): any;
|
|
320
|
-
declare function fetchWithEvent(event: H3Event, req: RequestInfo | URL, init?: RequestInit & {
|
|
331
|
+
declare function fetchWithEvent<T = unknown, _R = any, F extends (req: RequestInfo | URL, opts?: any) => any = typeof fetch>(event: H3Event, req: RequestInfo | URL, init?: RequestInit & {
|
|
321
332
|
context?: H3EventContext;
|
|
322
333
|
}, options?: {
|
|
323
|
-
fetch:
|
|
324
|
-
}):
|
|
334
|
+
fetch: F;
|
|
335
|
+
}): unknown extends T ? ReturnType<F> : T;
|
|
325
336
|
|
|
326
337
|
declare function getQuery(event: H3Event): ufo.QueryObject;
|
|
327
|
-
declare function getRouterParams(event: H3Event): H3Event["context"]
|
|
328
|
-
declare function getRouterParam(event: H3Event, name: string):
|
|
338
|
+
declare function getRouterParams(event: H3Event): NonNullable<H3Event["context"]["params"]>;
|
|
339
|
+
declare function getRouterParam(event: H3Event, name: string): string | undefined;
|
|
329
340
|
declare function getMethod(event: H3Event, defaultMethod?: HTTPMethod): HTTPMethod;
|
|
330
341
|
declare function isMethod(event: H3Event, expected: HTTPMethod | HTTPMethod[], allowHead?: boolean): boolean;
|
|
331
342
|
declare function assertMethod(event: H3Event, expected: HTTPMethod | HTTPMethod[], allowHead?: boolean): void;
|
package/dist/index.mjs
CHANGED
|
@@ -101,7 +101,6 @@ class H3Error extends Error {
|
|
|
101
101
|
this.statusCode = 500;
|
|
102
102
|
this.fatal = false;
|
|
103
103
|
this.unhandled = false;
|
|
104
|
-
this.statusMessage = void 0;
|
|
105
104
|
}
|
|
106
105
|
toJSON() {
|
|
107
106
|
const obj = {
|
|
@@ -126,8 +125,8 @@ function createError(input) {
|
|
|
126
125
|
return input;
|
|
127
126
|
}
|
|
128
127
|
const err = new H3Error(
|
|
129
|
-
input.message ?? input.statusMessage,
|
|
130
|
-
// @ts-ignore
|
|
128
|
+
input.message ?? input.statusMessage ?? "",
|
|
129
|
+
// @ts-ignore https://v8.dev/features/error-cause
|
|
131
130
|
input.cause ? { cause: input.cause } : void 0
|
|
132
131
|
);
|
|
133
132
|
if ("stack" in input) {
|
|
@@ -162,7 +161,7 @@ function createError(input) {
|
|
|
162
161
|
const sanitizedMessage = sanitizeStatusMessage(err.statusMessage);
|
|
163
162
|
if (sanitizedMessage !== originalMessage) {
|
|
164
163
|
console.warn(
|
|
165
|
-
"[h3] Please prefer using `message` for longer error messages instead of `statusMessage`. In the future `statusMessage` will be sanitized by default."
|
|
164
|
+
"[h3] Please prefer using `message` for longer error messages instead of `statusMessage`. In the future, `statusMessage` will be sanitized by default."
|
|
166
165
|
);
|
|
167
166
|
}
|
|
168
167
|
}
|
|
@@ -175,7 +174,7 @@ function createError(input) {
|
|
|
175
174
|
return err;
|
|
176
175
|
}
|
|
177
176
|
function sendError(event, error, debug) {
|
|
178
|
-
if (event.
|
|
177
|
+
if (event.handled) {
|
|
179
178
|
return;
|
|
180
179
|
}
|
|
181
180
|
const h3Error = isError(error) ? error : createError(error);
|
|
@@ -188,7 +187,7 @@ function sendError(event, error, debug) {
|
|
|
188
187
|
if (debug) {
|
|
189
188
|
responseBody.stack = (h3Error.stack || "").split("\n").map((l) => l.trim());
|
|
190
189
|
}
|
|
191
|
-
if (event.
|
|
190
|
+
if (event.handled) {
|
|
192
191
|
return;
|
|
193
192
|
}
|
|
194
193
|
const _code = Number.parseInt(h3Error.statusCode);
|
|
@@ -284,9 +283,15 @@ function readRawBody(event, encoding = "utf8") {
|
|
|
284
283
|
assertMethod(event, PayloadMethods$1);
|
|
285
284
|
const _rawBody = event.node.req[RawBodySymbol] || event.node.req.body;
|
|
286
285
|
if (_rawBody) {
|
|
287
|
-
const promise2 = Promise.resolve(_rawBody).then(
|
|
288
|
-
(
|
|
289
|
-
|
|
286
|
+
const promise2 = Promise.resolve(_rawBody).then((_resolved) => {
|
|
287
|
+
if (Buffer.isBuffer(_resolved)) {
|
|
288
|
+
return _resolved;
|
|
289
|
+
}
|
|
290
|
+
if (_resolved.constructor === Object) {
|
|
291
|
+
return Buffer.from(JSON.stringify(_resolved));
|
|
292
|
+
}
|
|
293
|
+
return Buffer.from(_resolved);
|
|
294
|
+
});
|
|
290
295
|
return encoding ? promise2.then((buff) => buff.toString(encoding)) : promise2;
|
|
291
296
|
}
|
|
292
297
|
if (!Number.parseInt(event.node.req.headers["content-length"] || "")) {
|
|
@@ -371,7 +376,9 @@ function handleCacheHeaders(event, opts) {
|
|
|
371
376
|
event.node.res.setHeader("cache-control", cacheControls.join(", "));
|
|
372
377
|
if (cacheMatched) {
|
|
373
378
|
event.node.res.statusCode = 304;
|
|
374
|
-
event.
|
|
379
|
+
if (!event.handled) {
|
|
380
|
+
event.node.res.end();
|
|
381
|
+
}
|
|
375
382
|
return true;
|
|
376
383
|
}
|
|
377
384
|
return false;
|
|
@@ -520,6 +527,7 @@ async function sendProxy(event, target, opts = {}) {
|
|
|
520
527
|
event.node.res.statusCode
|
|
521
528
|
);
|
|
522
529
|
event.node.res.statusMessage = sanitizeStatusMessage(response.statusText);
|
|
530
|
+
const cookies = [];
|
|
523
531
|
for (const [key, value] of response.headers.entries()) {
|
|
524
532
|
if (key === "content-encoding") {
|
|
525
533
|
continue;
|
|
@@ -528,7 +536,15 @@ async function sendProxy(event, target, opts = {}) {
|
|
|
528
536
|
continue;
|
|
529
537
|
}
|
|
530
538
|
if (key === "set-cookie") {
|
|
531
|
-
|
|
539
|
+
cookies.push(...splitCookiesString(value));
|
|
540
|
+
continue;
|
|
541
|
+
}
|
|
542
|
+
event.node.res.setHeader(key, value);
|
|
543
|
+
}
|
|
544
|
+
if (cookies.length > 0) {
|
|
545
|
+
event.node.res.setHeader(
|
|
546
|
+
"set-cookie",
|
|
547
|
+
cookies.map((cookie) => {
|
|
532
548
|
if (opts.cookieDomainRewrite) {
|
|
533
549
|
cookie = rewriteCookieProperty(
|
|
534
550
|
cookie,
|
|
@@ -544,15 +560,18 @@ async function sendProxy(event, target, opts = {}) {
|
|
|
544
560
|
);
|
|
545
561
|
}
|
|
546
562
|
return cookie;
|
|
547
|
-
})
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
563
|
+
})
|
|
564
|
+
);
|
|
565
|
+
}
|
|
566
|
+
if (opts.onResponse) {
|
|
567
|
+
await opts.onResponse(event, response);
|
|
552
568
|
}
|
|
553
569
|
if (response._data !== void 0) {
|
|
554
570
|
return response._data;
|
|
555
571
|
}
|
|
572
|
+
if (event.handled) {
|
|
573
|
+
return;
|
|
574
|
+
}
|
|
556
575
|
if (opts.sendStream === false) {
|
|
557
576
|
const data = new Uint8Array(await response.arrayBuffer());
|
|
558
577
|
return event.node.res.end(data);
|
|
@@ -611,14 +630,16 @@ function rewriteCookieProperty(header, map, property) {
|
|
|
611
630
|
);
|
|
612
631
|
}
|
|
613
632
|
|
|
614
|
-
const defer = typeof setImmediate
|
|
633
|
+
const defer = typeof setImmediate === "undefined" ? (fn) => fn() : setImmediate;
|
|
615
634
|
function send(event, data, type) {
|
|
616
635
|
if (type) {
|
|
617
636
|
defaultContentType(event, type);
|
|
618
637
|
}
|
|
619
638
|
return new Promise((resolve) => {
|
|
620
639
|
defer(() => {
|
|
621
|
-
event.
|
|
640
|
+
if (!event.handled) {
|
|
641
|
+
event.node.res.end(data);
|
|
642
|
+
}
|
|
622
643
|
resolve();
|
|
623
644
|
});
|
|
624
645
|
});
|
|
@@ -628,7 +649,9 @@ function sendNoContent(event, code = 204) {
|
|
|
628
649
|
if (event.node.res.statusCode === 204) {
|
|
629
650
|
event.node.res.removeHeader("content-length");
|
|
630
651
|
}
|
|
631
|
-
event.
|
|
652
|
+
if (!event.handled) {
|
|
653
|
+
event.node.res.end();
|
|
654
|
+
}
|
|
632
655
|
}
|
|
633
656
|
function setResponseStatus(event, code, text) {
|
|
634
657
|
if (code) {
|
|
@@ -811,7 +834,7 @@ async function getSession(event, config) {
|
|
|
811
834
|
Object.assign(session, unsealed);
|
|
812
835
|
}
|
|
813
836
|
if (!session.id) {
|
|
814
|
-
session.id = (config.crypto || crypto).randomUUID();
|
|
837
|
+
session.id = config.generateId?.() ?? (config.crypto || crypto).randomUUID();
|
|
815
838
|
session.createdAt = Date.now();
|
|
816
839
|
await updateSession(event, config);
|
|
817
840
|
}
|
|
@@ -1082,12 +1105,16 @@ class H3Response {
|
|
|
1082
1105
|
class H3Event {
|
|
1083
1106
|
constructor(req, res) {
|
|
1084
1107
|
this["__is_event__"] = true;
|
|
1108
|
+
this._handled = false;
|
|
1085
1109
|
this.context = {};
|
|
1086
1110
|
this.node = { req, res };
|
|
1087
1111
|
}
|
|
1088
1112
|
get path() {
|
|
1089
1113
|
return getRequestPath(this);
|
|
1090
1114
|
}
|
|
1115
|
+
get handled() {
|
|
1116
|
+
return this._handled || this.node.res.writableEnded || this.node.res.headersSent;
|
|
1117
|
+
}
|
|
1091
1118
|
/** @deprecated Please use `event.node.req` instead. **/
|
|
1092
1119
|
get req() {
|
|
1093
1120
|
return this.node.req;
|
|
@@ -1099,35 +1126,37 @@ class H3Event {
|
|
|
1099
1126
|
// Implementation of FetchEvent
|
|
1100
1127
|
respondWith(r) {
|
|
1101
1128
|
Promise.resolve(r).then((_response) => {
|
|
1102
|
-
if (this.
|
|
1129
|
+
if (this.handled) {
|
|
1103
1130
|
return;
|
|
1104
1131
|
}
|
|
1105
1132
|
const response = _response instanceof H3Response ? _response : new H3Response(_response);
|
|
1106
1133
|
for (const [key, value] of response.headers.entries()) {
|
|
1107
|
-
this.res.setHeader(key, value);
|
|
1134
|
+
this.node.res.setHeader(key, value);
|
|
1108
1135
|
}
|
|
1109
1136
|
if (response.status) {
|
|
1110
|
-
this.res.statusCode = sanitizeStatusCode(
|
|
1137
|
+
this.node.res.statusCode = sanitizeStatusCode(
|
|
1111
1138
|
response.status,
|
|
1112
|
-
this.res.statusCode
|
|
1139
|
+
this.node.res.statusCode
|
|
1113
1140
|
);
|
|
1114
1141
|
}
|
|
1115
1142
|
if (response.statusText) {
|
|
1116
|
-
this.res.statusMessage = sanitizeStatusMessage(
|
|
1143
|
+
this.node.res.statusMessage = sanitizeStatusMessage(
|
|
1144
|
+
response.statusText
|
|
1145
|
+
);
|
|
1117
1146
|
}
|
|
1118
1147
|
if (response.redirected) {
|
|
1119
|
-
this.res.setHeader("location", response.url);
|
|
1148
|
+
this.node.res.setHeader("location", response.url);
|
|
1120
1149
|
}
|
|
1121
1150
|
if (!response._body) {
|
|
1122
|
-
return this.res.end();
|
|
1151
|
+
return this.node.res.end();
|
|
1123
1152
|
}
|
|
1124
1153
|
if (typeof response._body === "string" || "buffer" in response._body || "byteLength" in response._body) {
|
|
1125
|
-
return this.res.end(response._body);
|
|
1154
|
+
return this.node.res.end(response._body);
|
|
1126
1155
|
}
|
|
1127
1156
|
if (!response.headers.has("content-type")) {
|
|
1128
1157
|
response.headers.set("content-type", MIMES.json);
|
|
1129
1158
|
}
|
|
1130
|
-
this.res.end(JSON.stringify(response._body));
|
|
1159
|
+
this.node.res.end(JSON.stringify(response._body));
|
|
1131
1160
|
});
|
|
1132
1161
|
}
|
|
1133
1162
|
}
|
|
@@ -1253,7 +1282,7 @@ function createAppEventHandler(stack, options) {
|
|
|
1253
1282
|
continue;
|
|
1254
1283
|
}
|
|
1255
1284
|
const val = await layer.handler(event);
|
|
1256
|
-
if (event.
|
|
1285
|
+
if (event.handled) {
|
|
1257
1286
|
return;
|
|
1258
1287
|
}
|
|
1259
1288
|
const type = typeof val;
|
|
@@ -1278,10 +1307,10 @@ function createAppEventHandler(stack, options) {
|
|
|
1278
1307
|
}
|
|
1279
1308
|
}
|
|
1280
1309
|
}
|
|
1281
|
-
if (!event.
|
|
1310
|
+
if (!event.handled) {
|
|
1282
1311
|
throw createError({
|
|
1283
1312
|
statusCode: 404,
|
|
1284
|
-
statusMessage: `Cannot find any
|
|
1313
|
+
statusMessage: `Cannot find any path matching ${event.node.req.url || "/"}.`
|
|
1285
1314
|
});
|
|
1286
1315
|
}
|
|
1287
1316
|
});
|
|
@@ -1429,15 +1458,25 @@ function createRouter(opts = {}) {
|
|
|
1429
1458
|
const method = (event.node.req.method || "get").toLowerCase();
|
|
1430
1459
|
const handler = matched.handlers[method] || matched.handlers.all;
|
|
1431
1460
|
if (!handler) {
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1461
|
+
if (opts.preemptive || opts.preemtive) {
|
|
1462
|
+
throw createError({
|
|
1463
|
+
statusCode: 405,
|
|
1464
|
+
name: "Method Not Allowed",
|
|
1465
|
+
statusMessage: `Method ${method} is not allowed on this route.`
|
|
1466
|
+
});
|
|
1467
|
+
} else {
|
|
1468
|
+
return;
|
|
1469
|
+
}
|
|
1437
1470
|
}
|
|
1438
1471
|
const params = matched.params || {};
|
|
1439
1472
|
event.context.params = params;
|
|
1440
|
-
return handler(event)
|
|
1473
|
+
return Promise.resolve(handler(event)).then((res) => {
|
|
1474
|
+
if (res === void 0 && (opts.preemptive || opts.preemtive)) {
|
|
1475
|
+
setResponseStatus(event, 204);
|
|
1476
|
+
return "";
|
|
1477
|
+
}
|
|
1478
|
+
return res;
|
|
1479
|
+
});
|
|
1441
1480
|
});
|
|
1442
1481
|
return router;
|
|
1443
1482
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "h3",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.7.1",
|
|
4
4
|
"description": "Tiny JavaScript Server",
|
|
5
5
|
"repository": "unjs/h3",
|
|
6
6
|
"license": "MIT",
|
|
@@ -19,46 +19,46 @@
|
|
|
19
19
|
"files": [
|
|
20
20
|
"dist"
|
|
21
21
|
],
|
|
22
|
-
"scripts": {
|
|
23
|
-
"build": "unbuild",
|
|
24
|
-
"dev": "vitest",
|
|
25
|
-
"lint": "eslint --cache --ext .ts,.js,.mjs,.cjs . && prettier -c src test playground",
|
|
26
|
-
"lint:fix": "eslint --cache --ext .ts,.js,.mjs,.cjs . --fix && prettier -c src test playground -w",
|
|
27
|
-
"play": "jiti ./playground/index.ts",
|
|
28
|
-
"profile": "0x -o -D .profile -P 'autocannon -c 100 -p 10 -d 40 http://localhost:$PORT' ./playground/server.cjs",
|
|
29
|
-
"release": "pnpm test && pnpm build && changelogen --release && pnpm publish && git push --follow-tags",
|
|
30
|
-
"test": "pnpm lint && vitest run --coverage"
|
|
31
|
-
},
|
|
32
22
|
"dependencies": {
|
|
33
23
|
"cookie-es": "^1.0.0",
|
|
34
24
|
"defu": "^6.1.2",
|
|
35
|
-
"destr": "^
|
|
25
|
+
"destr": "^2.0.0",
|
|
36
26
|
"iron-webcrypto": "^0.7.0",
|
|
37
27
|
"radix3": "^1.0.1",
|
|
38
28
|
"ufo": "^1.1.2",
|
|
39
|
-
"uncrypto": "^0.1.
|
|
29
|
+
"uncrypto": "^0.1.3"
|
|
40
30
|
},
|
|
41
31
|
"devDependencies": {
|
|
42
32
|
"0x": "^5.5.0",
|
|
43
33
|
"@types/express": "^4.17.17",
|
|
44
|
-
"@types/node": "^20.1
|
|
34
|
+
"@types/node": "^20.3.1",
|
|
45
35
|
"@types/supertest": "^2.0.12",
|
|
46
|
-
"@vitest/coverage-
|
|
36
|
+
"@vitest/coverage-v8": "^0.32.2",
|
|
47
37
|
"autocannon": "^7.11.0",
|
|
48
38
|
"changelogen": "^0.5.3",
|
|
49
39
|
"connect": "^3.7.0",
|
|
50
|
-
"eslint": "^8.
|
|
51
|
-
"eslint-config-unjs": "^0.1
|
|
40
|
+
"eslint": "^8.43.0",
|
|
41
|
+
"eslint-config-unjs": "^0.2.1",
|
|
52
42
|
"express": "^4.18.2",
|
|
53
|
-
"get-port": "^
|
|
43
|
+
"get-port": "^7.0.0",
|
|
54
44
|
"jiti": "^1.18.2",
|
|
55
45
|
"listhen": "^1.0.4",
|
|
56
|
-
"node-fetch-native": "^1.
|
|
46
|
+
"node-fetch-native": "^1.2.0",
|
|
57
47
|
"prettier": "^2.8.8",
|
|
58
48
|
"supertest": "^6.3.3",
|
|
59
|
-
"typescript": "^5.
|
|
49
|
+
"typescript": "^5.1.3",
|
|
60
50
|
"unbuild": "^1.2.1",
|
|
61
|
-
"vitest": "^0.
|
|
51
|
+
"vitest": "^0.32.2"
|
|
62
52
|
},
|
|
63
|
-
"packageManager": "pnpm@8.
|
|
53
|
+
"packageManager": "pnpm@8.6.3",
|
|
54
|
+
"scripts": {
|
|
55
|
+
"build": "unbuild",
|
|
56
|
+
"dev": "vitest",
|
|
57
|
+
"lint": "eslint --cache --ext .ts,.js,.mjs,.cjs . && prettier -c src test playground",
|
|
58
|
+
"lint:fix": "eslint --cache --ext .ts,.js,.mjs,.cjs . --fix && prettier -c src test playground -w",
|
|
59
|
+
"play": "jiti ./playground/index.ts",
|
|
60
|
+
"profile": "0x -o -D .profile -P 'autocannon -c 100 -p 10 -d 40 http://localhost:$PORT' ./playground/server.cjs",
|
|
61
|
+
"release": "pnpm test && pnpm build && changelogen --release && pnpm publish && git push --follow-tags",
|
|
62
|
+
"test": "pnpm lint && vitest run --coverage"
|
|
63
|
+
}
|
|
64
64
|
}
|