@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.
- package/README.md +50 -2
- package/create-handler.d.mts +2 -1
- package/create-handler.d.ts +2 -1
- package/create-handler.js +358 -125
- package/create-handler.mjs +361 -126
- package/error-format.d.mts +10 -3
- package/error-format.d.ts +10 -3
- package/error-format.js +69 -15
- package/error-format.mjs +65 -15
- package/http-response.d.mts +2 -6
- package/http-response.d.ts +2 -6
- package/http-response.js +140 -13
- package/http-response.mjs +140 -13
- package/index.d.mts +5 -3
- package/index.d.ts +5 -3
- package/index.js +365 -129
- package/index.mjs +366 -129
- package/llms.txt +6 -0
- package/package.json +8 -4
- package/public-types.d.mts +13 -17
- package/public-types.d.ts +13 -17
- package/responses/csv.d.mts +12 -6
- package/responses/csv.d.ts +12 -6
- package/responses/csv.js +133 -12
- package/responses/csv.mjs +133 -12
- package/responses/index.d.mts +2 -1
- package/responses/index.d.ts +2 -1
- package/responses/index.js +11 -3
- package/responses/index.mjs +9 -2
- package/responses/success.js +7 -1
- package/responses/success.mjs +7 -1
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 =
|
|
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
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
|
207
|
-
|
|
208
|
-
|
|
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
|
|
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
|
-
|
|
243
|
-
return
|
|
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 =
|
|
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 =
|
|
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:
|
|
281
|
-
status: (0, import_http_errors2.getCanonicalStatus)(
|
|
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)(
|
|
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 =
|
|
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:
|
|
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(
|
|
498
|
+
res.status(statusCode).send(payload);
|
|
332
499
|
},
|
|
333
500
|
[ErrorFormats.aip193]: (res, error, domain) => {
|
|
334
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
432
|
-
|
|
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
|
-
|
|
437
|
-
|
|
438
|
-
|
|
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
|
-
|
|
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
|
|
463
|
-
if (
|
|
464
|
-
sendError = sendBaseError;
|
|
638
|
+
const terminalErrorBoundary = (res, next, terminalError, event) => {
|
|
639
|
+
if (event.canceled) {
|
|
465
640
|
return;
|
|
466
641
|
}
|
|
467
|
-
|
|
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
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
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
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
if (
|
|
656
|
+
if (didSend && postErrorHook) {
|
|
657
|
+
invokePostHookOnFinish(res, postErrorHook, failure, reportFailure);
|
|
658
|
+
}
|
|
659
|
+
};
|
|
660
|
+
if (event.canceled) {
|
|
492
661
|
return;
|
|
493
662
|
}
|
|
494
|
-
|
|
495
|
-
|
|
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
|
-
|
|
499
|
-
|
|
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
|
-
|
|
709
|
+
runSender();
|
|
503
710
|
};
|
|
504
711
|
const nextFn = function(event, next) {
|
|
505
|
-
return function(
|
|
506
|
-
if (
|
|
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
|
-
|
|
512
|
-
|
|
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
|
-
|
|
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
|
-
|
|
756
|
+
runLifecycle(res, next, result, event);
|
|
524
757
|
} catch (err) {
|
|
525
758
|
if (res.headersSent) {
|
|
759
|
+
next(err);
|
|
526
760
|
return;
|
|
527
761
|
}
|
|
528
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 = {
|