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