@jskit-ai/kernel 0.1.185 → 0.1.187
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/package.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ActionRuntimeError } from "../../shared/actions/actionDefinitions.js";
|
|
2
|
-
import { normalizeOpaqueId } from "../../shared/support/normalize.js";
|
|
2
|
+
import { normalizeOpaqueId, normalizeTransactionOutcome } from "../../shared/support/normalize.js";
|
|
3
3
|
import { resolveDefaultSurfaceId } from "../support/appConfig.js";
|
|
4
4
|
|
|
5
5
|
const JSON_API_CONTENT_TYPE = "application/vnd.api+json";
|
|
@@ -255,7 +255,9 @@ function resolveRequestRouteTransport(request) {
|
|
|
255
255
|
|
|
256
256
|
function applyRouteTransportErrorResponse(reply, request, error, {
|
|
257
257
|
statusCode = 500,
|
|
258
|
-
normalizedErrorCode = ""
|
|
258
|
+
normalizedErrorCode = "",
|
|
259
|
+
message = error?.message,
|
|
260
|
+
exposeDetails = true
|
|
259
261
|
} = {}) {
|
|
260
262
|
const transport = resolveRequestRouteTransport(request);
|
|
261
263
|
const errorSerializer = transport && typeof transport.error === "function" ? transport.error : null;
|
|
@@ -267,7 +269,9 @@ function applyRouteTransportErrorResponse(reply, request, error, {
|
|
|
267
269
|
request,
|
|
268
270
|
reply,
|
|
269
271
|
statusCode,
|
|
270
|
-
code: normalizedErrorCode
|
|
272
|
+
code: normalizedErrorCode,
|
|
273
|
+
message,
|
|
274
|
+
exposeDetails
|
|
271
275
|
});
|
|
272
276
|
|
|
273
277
|
if (payload && typeof payload.then === "function") {
|
|
@@ -282,6 +286,36 @@ function applyRouteTransportErrorResponse(reply, request, error, {
|
|
|
282
286
|
return true;
|
|
283
287
|
}
|
|
284
288
|
|
|
289
|
+
function resolveApiErrorStatus(error, code) {
|
|
290
|
+
// JSON REST's public error codes need HTTP classification even without status hints.
|
|
291
|
+
switch (code) {
|
|
292
|
+
case "REST_API_VALIDATION":
|
|
293
|
+
return 422;
|
|
294
|
+
case "REST_API_RESOURCE":
|
|
295
|
+
switch (error?.subtype) {
|
|
296
|
+
case "not_found": return 404;
|
|
297
|
+
case "forbidden": return 403;
|
|
298
|
+
case "conflict": return 409;
|
|
299
|
+
default: return 400;
|
|
300
|
+
}
|
|
301
|
+
case "REST_API_VERSION_CONFLICT":
|
|
302
|
+
return 409;
|
|
303
|
+
case "REST_API_PRECONDITION_FAILED":
|
|
304
|
+
return 412;
|
|
305
|
+
case "REST_API_FIELDSET_INVALID":
|
|
306
|
+
case "REST_API_INCLUDE_INVALID":
|
|
307
|
+
return 400;
|
|
308
|
+
case "REST_API_PAYLOAD":
|
|
309
|
+
return error?.statusCode === 413 ? 413 : 400;
|
|
310
|
+
case "REST_API_TEMPORAL_DATA_INVALID":
|
|
311
|
+
return 500;
|
|
312
|
+
default: {
|
|
313
|
+
const status = Number(error?.statusCode || error?.status);
|
|
314
|
+
return Number.isInteger(status) && status >= 400 && status <= 599 ? status : 500;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
285
319
|
function registerApiErrorHandler(
|
|
286
320
|
app,
|
|
287
321
|
{
|
|
@@ -308,10 +342,10 @@ function registerApiErrorHandler(
|
|
|
308
342
|
|
|
309
343
|
app.setErrorHandler((error, request, reply) => {
|
|
310
344
|
const normalizedErrorCode = String(error?.code || "").trim();
|
|
345
|
+
const transactionOutcome = normalizeTransactionOutcome(error?.transactionOutcome);
|
|
346
|
+
const outcomeFields = transactionOutcome ? { transactionOutcome } : {};
|
|
311
347
|
const isCsrfErrorCode = normalizedErrorCode.startsWith("FST_CSRF_");
|
|
312
|
-
const
|
|
313
|
-
const statusCode =
|
|
314
|
-
Number.isInteger(statusFromError) && statusFromError >= 400 && statusFromError <= 599 ? statusFromError : 500;
|
|
348
|
+
const statusCode = resolveApiErrorStatus(error, normalizedErrorCode);
|
|
315
349
|
|
|
316
350
|
if (Array.isArray(error?.validation)) {
|
|
317
351
|
const fieldErrors = resolveValidationFieldErrors(error);
|
|
@@ -325,6 +359,7 @@ function registerApiErrorHandler(
|
|
|
325
359
|
reply.code(400).send({
|
|
326
360
|
error: "Validation failed.",
|
|
327
361
|
code: validationErrorCode,
|
|
362
|
+
...outcomeFields,
|
|
328
363
|
fieldErrors,
|
|
329
364
|
details: {
|
|
330
365
|
fieldErrors
|
|
@@ -336,7 +371,8 @@ function registerApiErrorHandler(
|
|
|
336
371
|
if (isAppError(error) || error instanceof ActionRuntimeError) {
|
|
337
372
|
if (applyRouteTransportErrorResponse(reply, request, error, {
|
|
338
373
|
statusCode: error.status,
|
|
339
|
-
normalizedErrorCode: normalizedErrorCode || "app_error"
|
|
374
|
+
normalizedErrorCode: normalizedErrorCode || "app_error",
|
|
375
|
+
exposeDetails: shouldExposeAppErrorDetails(normalizedErrorCode)
|
|
340
376
|
})) {
|
|
341
377
|
return;
|
|
342
378
|
}
|
|
@@ -349,7 +385,8 @@ function registerApiErrorHandler(
|
|
|
349
385
|
const appErrorCode = normalizedErrorCode || "app_error";
|
|
350
386
|
const payload = {
|
|
351
387
|
error: error.message,
|
|
352
|
-
code: appErrorCode
|
|
388
|
+
code: appErrorCode,
|
|
389
|
+
...outcomeFields
|
|
353
390
|
};
|
|
354
391
|
if (error.details && shouldExposeAppErrorDetails(appErrorCode)) {
|
|
355
392
|
payload.details = error.details;
|
|
@@ -385,7 +422,8 @@ function registerApiErrorHandler(
|
|
|
385
422
|
normalizedErrorCode || (statusCode >= 500 ? "internal_server_error" : "request_failed");
|
|
386
423
|
const payload = {
|
|
387
424
|
error: message,
|
|
388
|
-
code: fallbackErrorCode
|
|
425
|
+
code: fallbackErrorCode,
|
|
426
|
+
...outcomeFields
|
|
389
427
|
};
|
|
390
428
|
if (isCsrfErrorCode) {
|
|
391
429
|
payload.details = {
|
|
@@ -394,7 +432,9 @@ function registerApiErrorHandler(
|
|
|
394
432
|
}
|
|
395
433
|
if (applyRouteTransportErrorResponse(reply, request, error, {
|
|
396
434
|
statusCode,
|
|
397
|
-
normalizedErrorCode: fallbackErrorCode
|
|
435
|
+
normalizedErrorCode: fallbackErrorCode,
|
|
436
|
+
message,
|
|
437
|
+
exposeDetails: statusCode < 500
|
|
398
438
|
})) {
|
|
399
439
|
return;
|
|
400
440
|
}
|
|
@@ -155,6 +155,44 @@ test("registerApiErrorHandler includes internal_server_error code for unhandled
|
|
|
155
155
|
assert.equal(reply.payload.error, "Internal server error.");
|
|
156
156
|
});
|
|
157
157
|
|
|
158
|
+
test("registerApiErrorHandler exposes only allowlisted transaction outcomes", () => {
|
|
159
|
+
const fastify = createFastifyStub();
|
|
160
|
+
registerApiErrorHandler(fastify, { isAppError });
|
|
161
|
+
for (const transactionOutcome of ["none", "pending", "committed", "rolledBack", "unknown"]) {
|
|
162
|
+
const error = Object.assign(new Error("Private SQL failure"), {
|
|
163
|
+
transactionOutcome,
|
|
164
|
+
cause: { password: "private" },
|
|
165
|
+
cleanupErrors: [{ message: "private cleanup" }]
|
|
166
|
+
});
|
|
167
|
+
const reply = createReplyStub();
|
|
168
|
+
fastify.errorHandler(error, {}, reply);
|
|
169
|
+
assert.equal(reply.statusCode, 500);
|
|
170
|
+
assert.deepEqual(reply.payload, {
|
|
171
|
+
error: "Internal server error.",
|
|
172
|
+
code: "internal_server_error",
|
|
173
|
+
transactionOutcome
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
for (const transactionOutcome of [undefined, "success", { secret: "private" }]) {
|
|
177
|
+
const reply = createReplyStub();
|
|
178
|
+
fastify.errorHandler(Object.assign(new Error("private"), { transactionOutcome }), {}, reply);
|
|
179
|
+
assert.equal(Object.hasOwn(reply.payload, "transactionOutcome"), false);
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
test("AppError and validation payloads retain transaction outcomes", () => {
|
|
184
|
+
const fastify = createFastifyStub();
|
|
185
|
+
registerApiErrorHandler(fastify, { isAppError });
|
|
186
|
+
for (const error of [
|
|
187
|
+
Object.assign(new AppError(409, "Conflict."), { transactionOutcome: "rolledBack" }),
|
|
188
|
+
{ validation: [{ instancePath: "/title", message: "Required" }], transactionOutcome: "none" }
|
|
189
|
+
]) {
|
|
190
|
+
const reply = createReplyStub();
|
|
191
|
+
fastify.errorHandler(error, {}, reply);
|
|
192
|
+
assert.equal(reply.payload.transactionOutcome, error.transactionOutcome);
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
|
|
158
196
|
test("registerApiErrorHandler keeps known error code for non-app errors", () => {
|
|
159
197
|
const fastify = createFastifyStub();
|
|
160
198
|
registerApiErrorHandler(fastify, { isAppError });
|
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
const TRANSACTION_OUTCOMES = Object.freeze(["none", "pending", "committed", "rolledBack", "unknown"]);
|
|
2
|
+
|
|
3
|
+
function normalizeTransactionOutcome(value) {
|
|
4
|
+
return TRANSACTION_OUTCOMES.includes(value) ? value : "";
|
|
5
|
+
}
|
|
6
|
+
|
|
1
7
|
function normalizeText(value, { fallback = "" } = {}) {
|
|
2
8
|
const normalized = String(value || "").trim();
|
|
3
9
|
return normalized || fallback;
|
|
@@ -351,6 +357,8 @@ function ensureNonEmptyText(value, label = "value") {
|
|
|
351
357
|
}
|
|
352
358
|
|
|
353
359
|
export {
|
|
360
|
+
TRANSACTION_OUTCOMES,
|
|
361
|
+
normalizeTransactionOutcome,
|
|
354
362
|
normalizeText,
|
|
355
363
|
hasValue,
|
|
356
364
|
normalizeBoolean,
|
|
@@ -17,9 +17,19 @@ import {
|
|
|
17
17
|
normalizeOneOf,
|
|
18
18
|
normalizeQueryToken,
|
|
19
19
|
normalizeText,
|
|
20
|
+
normalizeTransactionOutcome,
|
|
20
21
|
normalizeUniqueTextList
|
|
21
22
|
} from "./normalize.js";
|
|
22
23
|
|
|
24
|
+
test("normalizeTransactionOutcome accepts only the documented literal outcomes", () => {
|
|
25
|
+
for (const value of ["none", "pending", "committed", "rolledBack", "unknown"]) {
|
|
26
|
+
assert.equal(normalizeTransactionOutcome(value), value);
|
|
27
|
+
}
|
|
28
|
+
for (const value of [undefined, null, false, 0, "", "rolledback", " committed ", { toString: () => "committed" }]) {
|
|
29
|
+
assert.equal(normalizeTransactionOutcome(value), "");
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
|
|
23
33
|
test("hasValue returns false for nullish and blank text, true otherwise", () => {
|
|
24
34
|
assert.equal(hasValue(null), false);
|
|
25
35
|
assert.equal(hasValue(undefined), false);
|