@web-ts-toolkit/express-response-handler 0.31.5 → 0.33.0

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/index.js CHANGED
@@ -45,7 +45,8 @@ __export(index_exports, {
45
45
  ResetContent: () => ResetContent,
46
46
  Response: () => Response,
47
47
  createHandler: () => createHandler,
48
- default: () => index_default
48
+ default: () => index_default,
49
+ handleResponse: () => handleResponse
49
50
  });
50
51
  module.exports = __toCommonJS(index_exports);
51
52
 
@@ -56,6 +57,41 @@ var import_utils3 = require("@web-ts-toolkit/utils");
56
57
  // src/responses/csv.ts
57
58
  var import_format = require("@fast-csv/format");
58
59
  var import_utils = require("@web-ts-toolkit/utils");
60
+ var import_events = require("events");
61
+ var CONTROL_CHARACTER_PATTERN = /[\u0000-\u001f\u007f]/u;
62
+ var isIterable = (value) => typeof value === "object" && value !== null && Symbol.iterator in value;
63
+ var isAsyncIterable = (value) => typeof value === "object" && value !== null && Symbol.asyncIterator in value;
64
+ var isLazyCsvSource = (value) => !Array.isArray(value) && (isIterable(value) || isAsyncIterable(value));
65
+ var createAsyncIterator = (source) => {
66
+ if (isAsyncIterable(source)) {
67
+ return source[Symbol.asyncIterator]();
68
+ }
69
+ const iterator = (isIterable(source) ? source : (0, import_utils.castArray)(source))[Symbol.iterator]();
70
+ return {
71
+ next: () => Promise.resolve(iterator.next()),
72
+ return: (value) => {
73
+ if (iterator.return) {
74
+ return Promise.resolve(iterator.return(value));
75
+ }
76
+ return Promise.resolve({ done: true, value });
77
+ }
78
+ };
79
+ };
80
+ var encodeRFC5987Value = (value) => encodeURIComponent(value).replace(/[!'()*]/g, (char) => `%${char.charCodeAt(0).toString(16).toUpperCase()}`);
81
+ var createQuotedFilenameFallback = (filename) => {
82
+ const fallback = Array.from(filename.replace(/[\\/]/g, "_"), (char) => {
83
+ const codePoint = char.codePointAt(0) ?? 0;
84
+ return codePoint >= 32 && codePoint <= 126 ? char : "_";
85
+ }).join("");
86
+ return fallback.replace(/(["\\])/g, "\\$1");
87
+ };
88
+ var createAttachmentContentDisposition = (filename) => {
89
+ if (CONTROL_CHARACTER_PATTERN.test(filename)) {
90
+ throw new TypeError("CSV filename cannot contain control characters");
91
+ }
92
+ const fallback = createQuotedFilenameFallback(filename);
93
+ return `attachment; filename="${fallback}"; filename*=UTF-8''${encodeRFC5987Value(filename)}`;
94
+ };
59
95
  var csvResponseBrand = /* @__PURE__ */ Symbol.for("@web-ts-toolkit/express-response-handler.csv-response");
60
96
  var isCSVResponse = (value) => {
61
97
  if (value instanceof CSVResponse) {
@@ -72,35 +108,127 @@ _a = csvResponseBrand;
72
108
  var CSVResponse = class {
73
109
  constructor(dataset = [], options = {}) {
74
110
  this[_a] = true;
75
- this.dataset = (0, import_utils.castArray)(dataset);
111
+ this.dataset = dataset;
76
112
  this.filename = options.filename || "download.csv";
77
113
  this.processor = options.processor || ((value) => value);
78
- if ((0, import_utils.isBoolean)(options.headers)) {
114
+ if ((0, import_utils.isBoolean)(options.headers) || Array.isArray(options.headers)) {
79
115
  this.headers = options.headers;
80
- } else if (this.dataset.length > 0) {
116
+ } else if (Array.isArray(this.dataset) && this.dataset.length > 0) {
81
117
  this.headers = (0, import_utils.isPlainObject)(this.dataset[0]);
118
+ } else if (isLazyCsvSource(this.dataset)) {
119
+ throw new TypeError("CSV lazy sources require an explicit headers option");
120
+ } else {
121
+ this.headers = (0, import_utils.isPlainObject)(this.dataset);
82
122
  }
83
123
  }
84
- streamCsv(res) {
124
+ streamCsv(res, onBeforeOutputError) {
85
125
  const stream = (0, import_format.format)({ headers: this.headers });
86
- res.set("Content-Type", "text/csv");
87
- res.set("Content-Disposition", `attachment;filename=${this.filename}`);
88
- stream.pipe(res);
89
- stream.on("error", (error) => {
126
+ let outputStarted = false;
127
+ let failed = false;
128
+ let finished = false;
129
+ let activeIterator = null;
130
+ let abortStreaming = () => void 0;
131
+ const abortSignal = new Promise((_, reject) => {
132
+ abortStreaming = reject;
133
+ });
134
+ abortSignal.catch(() => void 0);
135
+ const cleanup = () => {
136
+ stream.off("error", fail);
137
+ res.off?.("error", fail);
138
+ res.off?.("close", abort);
139
+ res.off?.("finish", markFinished);
140
+ };
141
+ const closeActiveIterator = () => {
142
+ const iterator = activeIterator;
143
+ activeIterator = null;
144
+ if (iterator?.return) {
145
+ void Promise.resolve(iterator.return()).catch(() => void 0);
146
+ }
147
+ };
148
+ const closeResponse = (error) => {
90
149
  if (res.destroy) {
91
150
  res.destroy(error);
92
151
  return;
93
152
  }
94
153
  res.end();
95
- });
96
- stream.on("end", () => res.end());
97
- this.dataset.forEach((value) => stream.write(this.processor(value)));
98
- stream.end();
154
+ };
155
+ const fail = (error) => {
156
+ if (failed) {
157
+ return;
158
+ }
159
+ failed = true;
160
+ cleanup();
161
+ const normalizedError = error instanceof Error ? error : new Error("CSV response streaming failed");
162
+ abortStreaming(normalizedError);
163
+ closeActiveIterator();
164
+ if (!outputStarted && !res.headersSent) {
165
+ onBeforeOutputError?.(normalizedError);
166
+ return;
167
+ }
168
+ stream.destroy();
169
+ closeResponse(normalizedError);
170
+ };
171
+ const abort = () => {
172
+ if (!finished) {
173
+ fail(new Error("CSV response streaming aborted"));
174
+ }
175
+ };
176
+ const markFinished = () => {
177
+ finished = true;
178
+ };
179
+ const writeRow = async (row) => {
180
+ outputStarted = true;
181
+ if (!stream.write(row)) {
182
+ await Promise.race([(0, import_events.once)(stream, "drain"), abortSignal]);
183
+ }
184
+ };
185
+ stream.on("error", fail);
186
+ stream.on("end", cleanup);
187
+ res.on?.("error", fail);
188
+ res.on?.("close", abort);
189
+ res.on?.("finish", markFinished);
190
+ const pump = async () => {
191
+ try {
192
+ const iterator = createAsyncIterator(this.dataset);
193
+ activeIterator = iterator;
194
+ const first = await iterator.next();
195
+ const firstRow = first.done ? void 0 : this.processor(first.value);
196
+ const contentDisposition = createAttachmentContentDisposition(this.filename);
197
+ res.set("Content-Type", "text/csv");
198
+ res.set("Content-Disposition", contentDisposition);
199
+ stream.pipe(res);
200
+ if (!first.done) {
201
+ await writeRow(firstRow);
202
+ }
203
+ while (!failed) {
204
+ const row = await Promise.race([iterator.next(), abortSignal]);
205
+ if (row.done) {
206
+ break;
207
+ }
208
+ await writeRow(this.processor(row.value));
209
+ }
210
+ if (!failed) {
211
+ finished = true;
212
+ stream.end();
213
+ }
214
+ } catch (error) {
215
+ fail(error);
216
+ } finally {
217
+ activeIterator = null;
218
+ }
219
+ };
220
+ void pump();
99
221
  }
100
222
  };
101
223
 
102
224
  // src/responses/index.ts
103
225
  var responseBrand = /* @__PURE__ */ Symbol.for("@web-ts-toolkit/express-response-handler.response");
226
+ var validateHttpStatusCode = (statusCode) => {
227
+ if (typeof statusCode !== "number" || !Number.isFinite(statusCode) || !Number.isInteger(statusCode) || statusCode < 100 || statusCode > 599) {
228
+ throw new TypeError("statusCode must be an integer HTTP status code between 100 and 599");
229
+ }
230
+ return statusCode;
231
+ };
104
232
  var isResponse = (value) => {
105
233
  if (value instanceof Response) {
106
234
  return true;
@@ -116,7 +244,7 @@ _a2 = responseBrand;
116
244
  var Response = class {
117
245
  constructor(statusCode = 200, data) {
118
246
  this[_a2] = true;
119
- this.statusCode = statusCode;
247
+ this.statusCode = validateHttpStatusCode(statusCode);
120
248
  this.data = data;
121
249
  }
122
250
  };
@@ -220,11 +348,17 @@ var HttpResponse = {
220
348
  // src/error-format.ts
221
349
  var import_http_errors2 = require("@web-ts-toolkit/http-errors");
222
350
  var import_utils2 = require("@web-ts-toolkit/utils");
223
- var toStatusCode = (status, code, fallbackStatusCode) => {
224
- if (typeof status === "number") {
225
- return status;
351
+ var FALLBACK_ERROR_STATUS = 500;
352
+ var FALLBACK_ERROR_MESSAGE = "Internal Server Error";
353
+ var validateErrorStatusCode = (statusCode, source) => {
354
+ return (0, import_http_errors2.validateHttpErrorStatusCode)(statusCode, source);
355
+ };
356
+ var toErrorStatusCode = (status, code, fallbackStatusCode = FALLBACK_ERROR_STATUS) => {
357
+ const statusCode = typeof status === "number" ? status : code;
358
+ if (statusCode === void 0) {
359
+ return fallbackStatusCode;
226
360
  }
227
- return typeof code === "number" ? code : fallbackStatusCode;
361
+ return validateErrorStatusCode(statusCode, "error status");
228
362
  };
229
363
  var toOptionalString = (value) => typeof value === "string" ? value : void 0;
230
364
  var toMetadata = (value) => {
@@ -236,6 +370,41 @@ var toArray = (value) => {
236
370
  }
237
371
  return (0, import_utils2.isArray)(value) ? value : [value];
238
372
  };
373
+ var describeThrown = (value) => {
374
+ try {
375
+ if (value === null) {
376
+ return "null";
377
+ }
378
+ if (value === void 0) {
379
+ return "undefined";
380
+ }
381
+ if (typeof value === "symbol") {
382
+ return value.toString();
383
+ }
384
+ const stringified = typeof value === "object" && value !== null ? value.message : void 0;
385
+ if (typeof stringified === "string" && stringified.length > 0) {
386
+ return stringified;
387
+ }
388
+ return String(value);
389
+ } catch {
390
+ return FALLBACK_ERROR_MESSAGE;
391
+ }
392
+ };
393
+ var normalizeThrownError = (value) => {
394
+ if (value instanceof Error) {
395
+ return value;
396
+ }
397
+ const message = describeThrown(value);
398
+ if ((0, import_utils2.isPlainObject)(value)) {
399
+ const record = value;
400
+ const normalized = {
401
+ ...record,
402
+ message
403
+ };
404
+ return normalized;
405
+ }
406
+ return { message };
407
+ };
239
408
  var toHttpErrorShape = (error, fallbackDomain) => ({
240
409
  statusCode: error.statusCode ?? 500,
241
410
  status: error.status,
@@ -256,8 +425,8 @@ var toProblemDetailsSource = (result) => {
256
425
  return (0, import_utils2.isPlainObject)(result) ? result : void 0;
257
426
  };
258
427
  var defaultErrorMessageProvider = function(error) {
259
- const errorLike = error;
260
- return errorLike.message ?? errorLike._message ?? String(error);
428
+ void error;
429
+ return FALLBACK_ERROR_MESSAGE;
261
430
  };
262
431
  var toSimpleErrorPayload = (result) => (0, import_utils2.isString)(result) ? { message: result } : { ...result };
263
432
  var toStructuredHttpErrorPayload = (error, errorDomain) => (0, import_http_errors2.toAip193ErrorPayload)(toHttpErrorShape(error, errorDomain), errorDomain);
@@ -265,7 +434,7 @@ var toRfc9457HttpErrorPayload = (error, errorDomain) => (0, import_http_errors2.
265
434
  var toStructuredGenericErrorPayload = (result, errorDomain) => {
266
435
  if ((0, import_utils2.isPlainObject)(result) && (0, import_utils2.isPlainObject)(result.error)) {
267
436
  const error = result.error;
268
- const statusCode = typeof error.code === "number" ? error.code : 422;
437
+ const statusCode = toErrorStatusCode(error.code, void 0);
269
438
  const status = typeof error.status === "string" ? error.status : (0, import_http_errors2.getCanonicalStatus)(statusCode);
270
439
  const message = typeof error.message === "string" ? error.message : "";
271
440
  const details = toArray(error.details);
@@ -279,7 +448,7 @@ var toStructuredGenericErrorPayload = (result, errorDomain) => {
279
448
  };
280
449
  }
281
450
  if ((0, import_utils2.isPlainObject)(result)) {
282
- const statusCode = typeof result.code === "number" ? result.code : 422;
451
+ const statusCode = toErrorStatusCode(result.code, void 0);
283
452
  const status = typeof result.status === "string" ? result.status : (0, import_http_errors2.getCanonicalStatus)(statusCode);
284
453
  const message = typeof result.message === "string" ? result.message : "";
285
454
  const details = toArray(result.details) || [(0, import_http_errors2.createAip193ErrorInfoDetail)(status, errorDomain)];
@@ -294,17 +463,17 @@ var toStructuredGenericErrorPayload = (result, errorDomain) => {
294
463
  }
295
464
  return {
296
465
  error: {
297
- code: 422,
298
- status: (0, import_http_errors2.getCanonicalStatus)(422),
466
+ code: FALLBACK_ERROR_STATUS,
467
+ status: (0, import_http_errors2.getCanonicalStatus)(FALLBACK_ERROR_STATUS),
299
468
  message: String(result),
300
- details: [(0, import_http_errors2.createAip193ErrorInfoDetail)((0, import_http_errors2.getCanonicalStatus)(422), errorDomain)]
469
+ details: [(0, import_http_errors2.createAip193ErrorInfoDetail)((0, import_http_errors2.getCanonicalStatus)(FALLBACK_ERROR_STATUS), errorDomain)]
301
470
  }
302
471
  };
303
472
  };
304
473
  var toRfc9457GenericErrorPayload = (result) => {
305
474
  const problem = toProblemDetailsSource(result);
306
475
  if (problem) {
307
- const statusCode = toStatusCode(problem.status, problem.code, 422);
476
+ const statusCode = toErrorStatusCode(problem.status, problem.code);
308
477
  return (0, import_http_errors2.toRfc9457ErrorPayload)({
309
478
  statusCode,
310
479
  message: toOptionalString(problem.detail) ?? toOptionalString(problem.message) ?? "",
@@ -315,7 +484,7 @@ var toRfc9457GenericErrorPayload = (result) => {
315
484
  });
316
485
  }
317
486
  return (0, import_http_errors2.toRfc9457ErrorPayload)({
318
- statusCode: 422,
487
+ statusCode: FALLBACK_ERROR_STATUS,
319
488
  message: String(result)
320
489
  });
321
490
  };
@@ -328,11 +497,9 @@ var ErrorFormats = {
328
497
  };
329
498
 
330
499
  // src/create-handler.ts
331
- var promisify = (fn) => (value) => Promise.resolve().then(() => fn(value));
332
- var invokePostHook = (hook, value) => {
333
- void hook(value).catch(() => void 0);
334
- };
500
+ var promisify = (fn) => (value) => Promise.resolve().then(() => fn(value)).then(() => void 0);
335
501
  var RFC_9457_CONTENT_TYPE = "application/problem+json";
502
+ var SUPPORTED_RFC_9457_CONTENT_TYPES = /* @__PURE__ */ new Set(["application/problem+json", "application/json"]);
336
503
  var shouldSkipResponse = (res, event) => res.headersSent || event.canceled;
337
504
  var sendProblemJson = (res, statusCode, payload, contentType) => {
338
505
  res.status(statusCode);
@@ -341,30 +508,34 @@ var sendProblemJson = (res, statusCode, payload, contentType) => {
341
508
  };
342
509
  var sendHttpErrorByFormat = {
343
510
  [ErrorFormats.simple]: (res, error) => {
511
+ const statusCode = validateErrorStatusCode(error.statusCode ?? FALLBACK_ERROR_STATUS, "error.statusCode");
344
512
  const payload = { message: error.message ?? "" };
345
513
  if (error.errors !== void 0) {
346
514
  payload.errors = error.errors;
347
515
  }
348
- res.status(error.statusCode ?? 500).send(payload);
516
+ res.status(statusCode).send(payload);
349
517
  },
350
518
  [ErrorFormats.aip193]: (res, error, domain) => {
351
- res.status(error.statusCode ?? 500).send(toStructuredHttpErrorPayload(error, domain));
519
+ const statusCode = validateErrorStatusCode(error.statusCode ?? FALLBACK_ERROR_STATUS, "error.statusCode");
520
+ res.status(statusCode).send(toStructuredHttpErrorPayload(error, domain));
352
521
  },
353
522
  [ErrorFormats.rfc9457]: (res, error, domain) => {
354
- sendProblemJson(res, error.statusCode ?? 500, toRfc9457HttpErrorPayload(error, domain), RFC_9457_CONTENT_TYPE);
523
+ const statusCode = validateErrorStatusCode(error.statusCode ?? FALLBACK_ERROR_STATUS, "error.statusCode");
524
+ sendProblemJson(res, statusCode, toRfc9457HttpErrorPayload(error, domain), RFC_9457_CONTENT_TYPE);
355
525
  }
356
526
  };
357
527
  var sendGenericErrorByFormat = {
358
528
  [ErrorFormats.simple]: (res, result) => {
359
- res.status(422).send(toSimpleErrorPayload(result));
529
+ res.status(FALLBACK_ERROR_STATUS).send(toSimpleErrorPayload(result));
360
530
  },
361
531
  [ErrorFormats.aip193]: (res, result, domain) => {
362
532
  const payload = toStructuredGenericErrorPayload(result, domain);
363
- res.status(payload.error.code).send(payload);
533
+ res.status(validateErrorStatusCode(payload.error.code, "error.code")).send(payload);
364
534
  },
365
535
  [ErrorFormats.rfc9457]: (res, result) => {
366
536
  const payload = toRfc9457GenericErrorPayload(result);
367
- sendProblemJson(res, payload.status ?? 422, payload, RFC_9457_CONTENT_TYPE);
537
+ const statusCode = payload.status ?? FALLBACK_ERROR_STATUS;
538
+ sendProblemJson(res, validateErrorStatusCode(statusCode, "problem.status"), payload, RFC_9457_CONTENT_TYPE);
368
539
  }
369
540
  };
370
541
  var assertMiddleware = (fn) => {
@@ -384,10 +555,23 @@ var normalizeMiddlewareList = (fns) => {
384
555
  assertMiddleware(fns[0]);
385
556
  return [fns[0]];
386
557
  };
387
- function createHandler(options = {}) {
558
+ var validateHandlerConfig = (options) => {
388
559
  const errorFormat = options.errorFormat ?? ErrorFormats.simple;
389
560
  const errorDomain = options.errorDomain ?? "express-response-handler";
390
561
  const rfc9457ContentType = options.rfc9457ContentType ?? RFC_9457_CONTENT_TYPE;
562
+ import_assert.default.ok(
563
+ Object.values(ErrorFormats).includes(errorFormat),
564
+ `errorFormat must be one of: ${Object.values(ErrorFormats).join(", ")}`
565
+ );
566
+ import_assert.default.ok(typeof errorDomain === "string" && errorDomain.length > 0, "errorDomain must be a non-empty string");
567
+ import_assert.default.ok(
568
+ SUPPORTED_RFC_9457_CONTENT_TYPES.has(rfc9457ContentType),
569
+ "rfc9457ContentType must be one of: application/problem+json, application/json"
570
+ );
571
+ return Object.freeze({ errorFormat, errorDomain, rfc9457ContentType });
572
+ };
573
+ function createHandler(options = {}) {
574
+ const config = validateHandlerConfig(options);
391
575
  let errorMessageProvider = defaultErrorMessageProvider;
392
576
  let preJson = null;
393
577
  let postJson = null;
@@ -407,153 +591,202 @@ function createHandler(options = {}) {
407
591
  setState(fn, promisify(fn));
408
592
  rebuild();
409
593
  };
410
- const sendBaseJson = function(res, data, event) {
594
+ const noopRebuild = () => {
595
+ };
596
+ const sendBaseJson = function(res, data, event, onBeforeOutputError) {
411
597
  if (shouldSkipResponse(res, event)) {
412
- return;
598
+ return false;
413
599
  }
414
600
  if (data === void 0) {
415
- return;
601
+ return false;
416
602
  }
417
603
  if (isResponse(data)) {
418
- res.status(data.statusCode).json(data.data);
419
- return;
604
+ res.status(validateHttpStatusCode(data.statusCode)).json(data.data);
605
+ return true;
420
606
  }
421
607
  if (isCSVResponse(data)) {
422
- data.streamCsv(res);
423
- return;
608
+ data.streamCsv(res, onBeforeOutputError);
609
+ return true;
424
610
  }
425
611
  res.json(data);
612
+ return true;
426
613
  };
427
614
  const sendBaseError = function(res, err, event) {
428
615
  if (shouldSkipResponse(res, event)) {
429
- return;
616
+ return false;
430
617
  }
431
- const error = err;
432
- if (error.statusCode) {
433
- if (errorFormat === ErrorFormats.rfc9457) {
618
+ const error = normalizeThrownError(err);
619
+ if (error.statusCode !== void 0) {
620
+ const statusCode = validateErrorStatusCode(error.statusCode, "error.statusCode");
621
+ if (config.errorFormat === ErrorFormats.rfc9457) {
434
622
  sendProblemJson(
435
623
  res,
436
- error.statusCode ?? 500,
437
- toRfc9457HttpErrorPayload(error, errorDomain),
438
- rfc9457ContentType
624
+ statusCode,
625
+ toRfc9457HttpErrorPayload(error, config.errorDomain),
626
+ config.rfc9457ContentType
439
627
  );
440
- return;
628
+ return true;
441
629
  }
442
- sendHttpErrorByFormat[errorFormat](res, error, errorDomain);
443
- return;
630
+ sendHttpErrorByFormat[config.errorFormat](res, error, config.errorDomain);
631
+ return true;
444
632
  }
445
633
  const result = errorMessageProvider(err);
446
- if (errorFormat === ErrorFormats.rfc9457) {
634
+ if (config.errorFormat === ErrorFormats.rfc9457) {
447
635
  const payload = toRfc9457GenericErrorPayload(result);
448
- sendProblemJson(res, payload.status ?? 422, payload, rfc9457ContentType);
449
- return;
636
+ const statusCode = payload.status ?? FALLBACK_ERROR_STATUS;
637
+ sendProblemJson(res, validateErrorStatusCode(statusCode, "problem.status"), payload, config.rfc9457ContentType);
638
+ return true;
450
639
  }
451
- sendGenericErrorByFormat[errorFormat](res, result, errorDomain);
640
+ sendGenericErrorByFormat[config.errorFormat](res, result, config.errorDomain);
641
+ return true;
642
+ };
643
+ const invokePostHook = (hook, value, onFailure) => {
644
+ hook(value).then(
645
+ () => void 0,
646
+ (err) => onFailure(err)
647
+ );
452
648
  };
453
- let sendJson = sendBaseJson;
454
- let sendError = sendBaseError;
455
- const rebuildSendJson = () => {
456
- if (!preJsonHook && !postJsonHook) {
457
- sendJson = sendBaseJson;
649
+ const invokePostHookOnFinish = (res, hook, value, onFailure) => {
650
+ if ((0, import_utils3.isFunction)(res.once)) {
651
+ res.once("finish", () => invokePostHook(hook, value, onFailure));
458
652
  return;
459
653
  }
460
- sendJson = function(res, data, event) {
461
- const finalize = () => {
462
- try {
463
- sendBaseJson(res, data, event);
464
- } catch (err) {
465
- sendError(res, err, event);
466
- return;
467
- }
468
- if (postJsonHook) {
469
- invokePostHook(postJsonHook, data);
470
- }
471
- };
472
- if (preJsonHook) {
473
- preJsonHook(data).then(finalize, (err) => sendError(res, err, event));
474
- return;
475
- }
476
- finalize();
477
- };
654
+ invokePostHook(hook, value, onFailure);
478
655
  };
479
- const rebuildSendError = () => {
480
- if (!preErrorHook && !postErrorHook) {
481
- sendError = sendBaseError;
656
+ const terminalErrorBoundary = (res, next, terminalError, event) => {
657
+ if (event.canceled) {
482
658
  return;
483
659
  }
484
- sendError = function(res, err, event) {
485
- const finalize = () => {
486
- sendBaseError(res, err, event);
487
- if (postErrorHook) {
488
- invokePostHook(postErrorHook, err);
489
- }
490
- };
491
- if (preErrorHook) {
492
- preErrorHook(err).then(finalize, (hookErr) => sendBaseError(res, hookErr, event));
493
- return;
494
- }
495
- finalize();
496
- };
660
+ next(terminalError);
497
661
  };
498
- const handlePromise = function(res, promise, event) {
499
- Promise.resolve(promise).then((data) => {
500
- if (event.nextError) {
501
- sendError(res, event.nextError, event);
662
+ const dispatchError = (res, next, err, event) => {
663
+ const reportFailure = (failure) => {
664
+ terminalErrorBoundary(res, next, failure, event);
665
+ };
666
+ const sendFormatted = (failure) => {
667
+ let didSend;
668
+ try {
669
+ didSend = sendBaseError(res, failure, event);
670
+ } catch (senderFailure) {
671
+ reportFailure(senderFailure === void 0 ? failure : senderFailure);
502
672
  return;
503
673
  }
504
- sendJson(res, data, event);
505
- }).catch((err) => sendError(res, err, event));
506
- };
507
- const handleResult = function(res, result, event) {
508
- if (shouldSkipResponse(res, event)) {
674
+ if (didSend && postErrorHook) {
675
+ invokePostHookOnFinish(res, postErrorHook, failure, reportFailure);
676
+ }
677
+ };
678
+ if (event.canceled) {
509
679
  return;
510
680
  }
511
- if (event.nextError) {
512
- sendError(res, event.nextError, event);
681
+ const runSender = () => sendFormatted(err);
682
+ if (preErrorHook) {
683
+ preErrorHook(err).then(
684
+ () => sendFormatted(err),
685
+ (hookErr) => sendFormatted(hookErr === void 0 ? err : hookErr)
686
+ );
513
687
  return;
514
688
  }
515
- if ((0, import_utils3.isPromise)(result)) {
516
- handlePromise(res, result, event);
689
+ runSender();
690
+ };
691
+ const dispatchValue = (res, next, data, event, onSerialized) => {
692
+ const reportFailure = (failure) => {
693
+ terminalErrorBoundary(res, next, failure, event);
694
+ };
695
+ const sendFormattedError = (failure) => {
696
+ try {
697
+ sendBaseError(res, failure, event);
698
+ } catch (senderFailure) {
699
+ reportFailure(senderFailure === void 0 ? failure : senderFailure);
700
+ }
701
+ };
702
+ const runSender = () => {
703
+ let didSend;
704
+ try {
705
+ didSend = sendBaseJson(res, data, event, sendFormattedError);
706
+ } catch (senderFailure) {
707
+ const failure = senderFailure === void 0 ? new Error("response serialization failed") : senderFailure;
708
+ if (res.headersSent) {
709
+ reportFailure(failure);
710
+ return;
711
+ }
712
+ sendFormattedError(failure);
713
+ return;
714
+ }
715
+ if (didSend && postJsonHook) {
716
+ invokePostHookOnFinish(res, postJsonHook, data, reportFailure);
717
+ }
718
+ onSerialized();
719
+ };
720
+ if (preJsonHook) {
721
+ preJsonHook(data).then(
722
+ runSender,
723
+ (hookErr) => sendFormattedError(hookErr === void 0 ? new Error("pre-json hook failed") : hookErr)
724
+ );
517
725
  return;
518
726
  }
519
- sendJson(res, result, event);
727
+ runSender();
520
728
  };
521
729
  const nextFn = function(event, next) {
522
- return function(...args) {
523
- if (args.length === 0) {
730
+ return function(error) {
731
+ if (event.canceled) {
732
+ return;
733
+ }
734
+ if (error === void 0) {
524
735
  event.canceled = true;
525
736
  next();
526
737
  return;
527
738
  }
528
- if (args[0] instanceof Error) {
529
- event.nextError = args[0];
739
+ event.canceled = true;
740
+ next(error);
741
+ };
742
+ };
743
+ const runLifecycle = (res, next, result, event) => {
744
+ const finalize = (resolved) => {
745
+ if (event.canceled) {
746
+ return;
747
+ }
748
+ if (event.nextError) {
749
+ dispatchError(res, next, event.nextError, event);
530
750
  return;
531
751
  }
532
- event.nextError = new TypeError("next(value) is not supported; return a value instead");
752
+ dispatchValue(res, next, resolved, event, () => void 0);
533
753
  };
754
+ if ((0, import_utils3.isPromise)(result)) {
755
+ Promise.resolve(result).then(
756
+ (resolved) => finalize(resolved),
757
+ (err) => {
758
+ if (res.headersSent) {
759
+ next(err);
760
+ return;
761
+ }
762
+ dispatchError(res, next, err, event);
763
+ }
764
+ );
765
+ return;
766
+ }
767
+ finalize(result);
534
768
  };
535
769
  const routerFn = function(fn) {
536
770
  return function(req, res, next) {
537
771
  const event = { canceled: false, nextError: null };
538
772
  try {
539
773
  const result = fn(req, res, nextFn(event, next));
540
- handleResult(res, result, event);
774
+ runLifecycle(res, next, result, event);
541
775
  } catch (err) {
542
776
  if (res.headersSent) {
777
+ next(err);
543
778
  return;
544
779
  }
545
- sendError(res, err, event);
780
+ dispatchError(res, next, err, event);
546
781
  }
547
782
  };
548
783
  };
549
- const handleResponse = function(...fns) {
784
+ const handleResponse2 = function(...fns) {
550
785
  const middlewares = normalizeMiddlewareList(fns);
551
786
  return middlewares.length === 1 ? routerFn(middlewares[0]) : middlewares.map(routerFn);
552
787
  };
553
- return {
554
- handleResponse,
555
- handleResult,
556
- handlePromise,
788
+ const handler = {
789
+ handleResponse: handleResponse2,
557
790
  HttpResponse,
558
791
  createHandler,
559
792
  get errorMessageProvider() {
@@ -574,7 +807,7 @@ function createHandler(options = {}) {
574
807
  preJson = syncHook;
575
808
  preJsonHook = asyncHook;
576
809
  },
577
- rebuildSendJson
810
+ noopRebuild
578
811
  );
579
812
  },
580
813
  get postJson() {
@@ -588,7 +821,7 @@ function createHandler(options = {}) {
588
821
  postJson = syncHook;
589
822
  postJsonHook = asyncHook;
590
823
  },
591
- rebuildSendJson
824
+ noopRebuild
592
825
  );
593
826
  },
594
827
  get preError() {
@@ -602,7 +835,7 @@ function createHandler(options = {}) {
602
835
  preError = syncHook;
603
836
  preErrorHook = asyncHook;
604
837
  },
605
- rebuildSendError
838
+ noopRebuild
606
839
  );
607
840
  },
608
841
  get postError() {
@@ -616,14 +849,16 @@ function createHandler(options = {}) {
616
849
  postError = syncHook;
617
850
  postErrorHook = asyncHook;
618
851
  },
619
- rebuildSendError
852
+ noopRebuild
620
853
  );
621
854
  }
622
855
  };
856
+ return handler;
623
857
  }
624
858
 
625
859
  // src/index.ts
626
860
  var apiHandler = createHandler();
861
+ var { handleResponse } = apiHandler;
627
862
  var index_default = apiHandler;
628
863
  // Annotate the CommonJS export names for ESM import in node:
629
864
  0 && (module.exports = {
@@ -641,5 +876,6 @@ var index_default = apiHandler;
641
876
  PartialContent,
642
877
  ResetContent,
643
878
  Response,
644
- createHandler
879
+ createHandler,
880
+ handleResponse
645
881
  });