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 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](#how-it-works)) with the ability to match route prefix and apply matched middleware.
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('/api', eventHandler((event) => ({ url: event.node.req.url })))
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('/odd', eventHandler(() => 'Is odd!'), { match: url => url.substr(1) % 2 })
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(() => '<h1>Hello world!</h1>'))
114
+ app.use(eventHandler(() => "<h1>Hello world!</h1>"));
108
115
 
109
116
  // We can chain calls to .use()
110
- app.use('/1', eventHandler(() => '<h1>Hello world!</h1>'))
111
- .use('/2', eventHandler(() => '<h1>Goodbye!</h1>'))
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('/api', eventHandler((event) => proxyRequest('https://example.com', {
115
- // f.e. keep one domain unchanged, rewrite one domain and remove other domains
116
- cookieDomainRewrite: {
117
- "example.com": "example.com",
118
- "example.com": "somecompany.co.uk",
119
- "*": "",
120
- },
121
- cookiePathRewrite: {
122
- "/": "/api"
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(fromNodeMiddleware((req, res, next) => { req.setHeader('x-foo', 'bar'); next() }))
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('/big', () => import('./big-handler'), { lazy: true })
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.node.res.writableEnded) {
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.node.res.writableEnded) {
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
- (_resolved) => Buffer.isBuffer(_resolved) ? _resolved : Buffer.from(_resolved)
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.node.res.end();
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
- const cookies = splitCookiesString(value).map((cookie) => {
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
- event.node.res.setHeader("set-cookie", cookies);
556
- continue;
557
- }
558
- event.node.res.setHeader(key, value);
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 !== "undefined" ? setImmediate : (fn) => fn();
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.node.res.end(data);
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.node.res.end();
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.res.writableEnded) {
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(response.statusText);
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.node.res.writableEnded) {
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.node.res.writableEnded) {
1317
+ if (!event.handled) {
1289
1318
  throw createError({
1290
1319
  statusCode: 404,
1291
- statusMessage: `Cannot find any route matching ${event.node.req.url || "/"}.`
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
- throw createError({
1440
- statusCode: 405,
1441
- name: "Method Not Allowed",
1442
- statusMessage: `Method ${method} is not allowed on this route.`
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 {Number} statusCode An Integer indicating the HTTP response status code.
170
- * @property {String} statusMessage A String representing the HTTP status message
171
- * @property {String} fatal Indicates if the error is a fatal error.
172
- * @property {String} unhandled Indicates if the error was unhandled and auto captured.
173
- * @property {Any} data An extra data that will includes in the response.<br>
174
- * This can be used to pass additional information about the error.
175
- * @property {Boolean} internal Setting this property to <code>true</code> will mark error as an internal error
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 the H3Error
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
- * Receive an error and return the corresponding response.<br>
198
- * H3 internally uses this function to handle unhandled errors.<br>
199
- * Note that calling this function will close the connection and no other data will be sent to client afterwards.
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|Error} Raised error
203
- * @param debug {Boolean} Whether application is in debug mode.<br>
204
- * In the debug mode the stack trace of errors will be return in response.
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: typeof fetch;
324
- }): Promise<Response>;
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): H3Event["context"][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.node.res.writableEnded) {
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.node.res.writableEnded) {
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
- (_resolved) => Buffer.isBuffer(_resolved) ? _resolved : Buffer.from(_resolved)
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.node.res.end();
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
- const cookies = splitCookiesString(value).map((cookie) => {
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
- event.node.res.setHeader("set-cookie", cookies);
549
- continue;
550
- }
551
- event.node.res.setHeader(key, value);
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 !== "undefined" ? setImmediate : (fn) => fn();
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.node.res.end(data);
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.node.res.end();
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.res.writableEnded) {
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(response.statusText);
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.node.res.writableEnded) {
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.node.res.writableEnded) {
1310
+ if (!event.handled) {
1282
1311
  throw createError({
1283
1312
  statusCode: 404,
1284
- statusMessage: `Cannot find any route matching ${event.node.req.url || "/"}.`
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
- throw createError({
1433
- statusCode: 405,
1434
- name: "Method Not Allowed",
1435
- statusMessage: `Method ${method} is not allowed on this route.`
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.6.6",
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": "^1.2.2",
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.2"
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.4",
34
+ "@types/node": "^20.3.1",
45
35
  "@types/supertest": "^2.0.12",
46
- "@vitest/coverage-c8": "^0.31.0",
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.40.0",
51
- "eslint-config-unjs": "^0.1.0",
40
+ "eslint": "^8.43.0",
41
+ "eslint-config-unjs": "^0.2.1",
52
42
  "express": "^4.18.2",
53
- "get-port": "^6.1.2",
43
+ "get-port": "^7.0.0",
54
44
  "jiti": "^1.18.2",
55
45
  "listhen": "^1.0.4",
56
- "node-fetch-native": "^1.1.1",
46
+ "node-fetch-native": "^1.2.0",
57
47
  "prettier": "^2.8.8",
58
48
  "supertest": "^6.3.3",
59
- "typescript": "^5.0.4",
49
+ "typescript": "^5.1.3",
60
50
  "unbuild": "^1.2.1",
61
- "vitest": "^0.31.0"
51
+ "vitest": "^0.32.2"
62
52
  },
63
- "packageManager": "pnpm@8.5.1"
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
  }