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