@flink-app/flink 0.14.1 → 0.14.3
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/CHANGELOG.md +13 -0
- package/dist/src/FlinkApp.d.ts +48 -1
- package/dist/src/FlinkApp.js +40 -15
- package/dist/src/FlinkHttpHandler.d.ts +6 -1
- package/dist/src/utils.js +3 -0
- package/package.json +1 -1
- package/spec/FlinkApp.onError.spec.ts +80 -0
- package/spec/utils.spec.ts +27 -0
- package/src/FlinkApp.ts +83 -8
- package/src/FlinkHttpHandler.ts +6 -1
- package/src/utils.ts +2 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# @flink-app/flink
|
|
2
2
|
|
|
3
|
+
## 0.14.3
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 52eba74: Fix Query type constraint to allow user-defined interfaces without explicit index signatures. The Query type now uses explicit index signature syntax, making it easier for developers to define query parameter interfaces for their handlers without TypeScript compilation errors.
|
|
8
|
+
- b37faa5: Improve schema validation error messages to show specific additional property names. When validation fails due to additional properties, the error now displays which properties are not allowed instead of a generic message.
|
|
9
|
+
|
|
10
|
+
## 0.14.2
|
|
11
|
+
|
|
12
|
+
### Patch Changes
|
|
13
|
+
|
|
14
|
+
- 6d39832: Add optional onError callback to FlinkApp for custom error handling. The callback receives a properly typed FlinkResponse<FlinkError> and rich request context (including request object, HTTP method, path, and request ID) when handler execution throws an error. Supports both synchronous and asynchronous callbacks, with automatic error handling to prevent callback failures from affecting the error response flow. Enables custom logging or monitoring without modifying the response flow.
|
|
15
|
+
|
|
3
16
|
## 0.14.1
|
|
4
17
|
|
|
5
18
|
### Patch Changes
|
package/dist/src/FlinkApp.d.ts
CHANGED
|
@@ -5,10 +5,12 @@ import { Db, MongoClient } from "mongodb";
|
|
|
5
5
|
import { ToadScheduler } from "toad-scheduler";
|
|
6
6
|
import { FlinkAuthPlugin } from "./auth/FlinkAuthPlugin";
|
|
7
7
|
import { FlinkContext } from "./FlinkContext";
|
|
8
|
-
import {
|
|
8
|
+
import { FlinkError } from "./FlinkErrors";
|
|
9
|
+
import { FlinkRequest, HandlerFile, HttpMethod, QueryParamMetadata, RouteProps } from "./FlinkHttpHandler";
|
|
9
10
|
import { FlinkJobFile } from "./FlinkJob";
|
|
10
11
|
import { FlinkPlugin } from "./FlinkPlugin";
|
|
11
12
|
import { FlinkRepo } from "./FlinkRepo";
|
|
13
|
+
import { FlinkResponse } from "./FlinkResponse";
|
|
12
14
|
export type JSONSchema = JSONSchema7;
|
|
13
15
|
/**
|
|
14
16
|
* Re-export express factory function so sub packages
|
|
@@ -136,6 +138,50 @@ export interface FlinkOptions {
|
|
|
136
138
|
*/
|
|
137
139
|
format?: string;
|
|
138
140
|
};
|
|
141
|
+
/**
|
|
142
|
+
* Optional callback invoked when an error occurs in a handler.
|
|
143
|
+
* The error response and request context are passed for custom
|
|
144
|
+
* error logging or monitoring. This is a side-effect only and
|
|
145
|
+
* will not modify the response flow.
|
|
146
|
+
*
|
|
147
|
+
* Supports both synchronous and asynchronous callbacks. Any errors
|
|
148
|
+
* thrown or rejected by the callback will be caught and logged
|
|
149
|
+
* without affecting the error response to the client.
|
|
150
|
+
*
|
|
151
|
+
* @param error - The error response with status and error details
|
|
152
|
+
* @param context - Request context including method, path, and request ID
|
|
153
|
+
*
|
|
154
|
+
* @example
|
|
155
|
+
* ```ts
|
|
156
|
+
* // Synchronous callback
|
|
157
|
+
* onError: (error, context) => {
|
|
158
|
+
* if (error.status >= 500) {
|
|
159
|
+
* logger.error('Server error occurred', {
|
|
160
|
+
* status: error.status,
|
|
161
|
+
* code: error.error?.code,
|
|
162
|
+
* route: `${context.method} ${context.path}`,
|
|
163
|
+
* reqId: context.reqId
|
|
164
|
+
* });
|
|
165
|
+
* }
|
|
166
|
+
* }
|
|
167
|
+
*
|
|
168
|
+
* // Asynchronous callback
|
|
169
|
+
* onError: async (error, context) => {
|
|
170
|
+
* if (error.status >= 500) {
|
|
171
|
+
* await monitoringService.reportError({
|
|
172
|
+
* error,
|
|
173
|
+
* context
|
|
174
|
+
* });
|
|
175
|
+
* }
|
|
176
|
+
* }
|
|
177
|
+
* ```
|
|
178
|
+
*/
|
|
179
|
+
onError?: (error: FlinkResponse<FlinkError>, context: {
|
|
180
|
+
req: FlinkRequest;
|
|
181
|
+
method: HttpMethod;
|
|
182
|
+
path: string;
|
|
183
|
+
reqId: string;
|
|
184
|
+
}) => void | Promise<void>;
|
|
139
185
|
}
|
|
140
186
|
export interface HandlerConfig {
|
|
141
187
|
schema?: {
|
|
@@ -178,6 +224,7 @@ export declare class FlinkApp<C extends FlinkContext> {
|
|
|
178
224
|
private schedulingOptions?;
|
|
179
225
|
private disableHttpServer;
|
|
180
226
|
private expressServer;
|
|
227
|
+
private onError?;
|
|
181
228
|
private repos;
|
|
182
229
|
/**
|
|
183
230
|
* Internal cache used to track registered handlers and potentially any overlapping routes
|
package/dist/src/FlinkApp.js
CHANGED
|
@@ -122,6 +122,7 @@ var FlinkApp = /** @class */ (function () {
|
|
|
122
122
|
this.schedulingOptions = opts.scheduling;
|
|
123
123
|
this.disableHttpServer = !!opts.disableHttpServer;
|
|
124
124
|
this.accessLog = __assign({ enabled: true, format: "dev" }, opts.accessLog);
|
|
125
|
+
this.onError = opts.onError;
|
|
125
126
|
}
|
|
126
127
|
Object.defineProperty(FlinkApp.prototype, "ctx", {
|
|
127
128
|
get: function () {
|
|
@@ -341,7 +342,7 @@ var FlinkApp = /** @class */ (function () {
|
|
|
341
342
|
validateRes_1 = ajv.compile(schema.resSchema);
|
|
342
343
|
}
|
|
343
344
|
this.expressApp[method](routeProps.path, function (req, res) { return __awaiter(_this, void 0, void 0, function () {
|
|
344
|
-
var valid, formattedErrors, data, handlerRes, err_1, valid, formattedErrors;
|
|
345
|
+
var valid, formattedErrors, data, handlerRes, err_1, errorResponse, result, valid, formattedErrors;
|
|
345
346
|
return __generator(this, function (_a) {
|
|
346
347
|
switch (_a.label) {
|
|
347
348
|
case 0:
|
|
@@ -363,7 +364,7 @@ var FlinkApp = /** @class */ (function () {
|
|
|
363
364
|
error: {
|
|
364
365
|
id: (0, uuid_1.v4)(),
|
|
365
366
|
title: "Bad request",
|
|
366
|
-
detail:
|
|
367
|
+
detail: formattedErrors,
|
|
367
368
|
},
|
|
368
369
|
})];
|
|
369
370
|
}
|
|
@@ -391,21 +392,45 @@ var FlinkApp = /** @class */ (function () {
|
|
|
391
392
|
return [3 /*break*/, 6];
|
|
392
393
|
case 5:
|
|
393
394
|
err_1 = _a.sent();
|
|
395
|
+
errorResponse = void 0;
|
|
394
396
|
// duck typing to check if it is a FlinkError
|
|
395
397
|
if (typeof err_1.status === "number" && err_1.status >= 400 && err_1.status < 600 && err_1.error) {
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
398
|
+
errorResponse = {
|
|
399
|
+
status: err_1.status,
|
|
400
|
+
error: {
|
|
401
|
+
id: err_1.error.id || (0, uuid_1.v4)(),
|
|
402
|
+
title: err_1.error.title || "Unhandled error: ".concat(err_1.error.code || err_1.status),
|
|
403
|
+
detail: err_1.error.detail,
|
|
404
|
+
code: err_1.error.code,
|
|
405
|
+
},
|
|
406
|
+
};
|
|
405
407
|
}
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
408
|
+
else {
|
|
409
|
+
FlinkLog_1.log.warn("Handler '".concat(methodAndRoute_1, "' threw unhandled exception ").concat(err_1));
|
|
410
|
+
console.error(err_1);
|
|
411
|
+
errorResponse = (0, FlinkErrors_1.internalServerError)(err_1);
|
|
412
|
+
}
|
|
413
|
+
// Invoke onError callback if provided
|
|
414
|
+
if (this.onError) {
|
|
415
|
+
try {
|
|
416
|
+
result = this.onError(errorResponse, {
|
|
417
|
+
req: req,
|
|
418
|
+
method: method,
|
|
419
|
+
path: routeProps.path,
|
|
420
|
+
reqId: req.reqId,
|
|
421
|
+
});
|
|
422
|
+
// Handle async callbacks - don't wait for them
|
|
423
|
+
if (result instanceof Promise) {
|
|
424
|
+
result.catch(function (callbackErr) {
|
|
425
|
+
FlinkLog_1.log.error("onError callback rejected with: ".concat(callbackErr));
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
catch (callbackErr) {
|
|
430
|
+
FlinkLog_1.log.error("onError callback threw an exception: ".concat(callbackErr));
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
return [2 /*return*/, res.status(errorResponse.status || 500).json(errorResponse)];
|
|
409
434
|
case 6:
|
|
410
435
|
if (validateRes_1 && !(0, utils_1.isError)(handlerRes)) {
|
|
411
436
|
valid = validateRes_1(JSON.parse(JSON.stringify(handlerRes.data)));
|
|
@@ -417,7 +442,7 @@ var FlinkApp = /** @class */ (function () {
|
|
|
417
442
|
error: {
|
|
418
443
|
id: (0, uuid_1.v4)(),
|
|
419
444
|
title: "Bad response",
|
|
420
|
-
detail:
|
|
445
|
+
detail: formattedErrors,
|
|
421
446
|
},
|
|
422
447
|
})];
|
|
423
448
|
}
|
|
@@ -15,8 +15,13 @@ type Params = Request["params"];
|
|
|
15
15
|
* Query type for request query parameters.
|
|
16
16
|
* Does currently not allow nested objects, although
|
|
17
17
|
* underlying express Request does allow it.
|
|
18
|
+
*
|
|
19
|
+
* Uses index signature to allow both Record types and interface types
|
|
20
|
+
* to be assignable to Query without requiring explicit index signatures.
|
|
18
21
|
*/
|
|
19
|
-
type Query =
|
|
22
|
+
type Query = {
|
|
23
|
+
[x: string]: string | string[] | undefined;
|
|
24
|
+
};
|
|
20
25
|
/**
|
|
21
26
|
* Flink request extends express Request but adds reqId and user object.
|
|
22
27
|
*/
|
package/dist/src/utils.js
CHANGED
|
@@ -223,6 +223,9 @@ function formatValidationErrors(errors, data, maxDataLength) {
|
|
|
223
223
|
else if (error.keyword === "type") {
|
|
224
224
|
formatted.push(" - Invalid type at ".concat(error.schemaPath, ": expected ").concat(error.params.type, ", got ").concat(typeof dataAtPath));
|
|
225
225
|
}
|
|
226
|
+
else if (error.keyword === "additionalProperties") {
|
|
227
|
+
formatted.push(" - Additional property not allowed: ".concat(error.params.additionalProperty));
|
|
228
|
+
}
|
|
226
229
|
else if (error.keyword === "anyOf" || error.keyword === "oneOf") {
|
|
227
230
|
formatted.push(" - ".concat(error.message));
|
|
228
231
|
}
|
package/package.json
CHANGED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { FlinkApp } from "../src/FlinkApp";
|
|
2
|
+
import { FlinkContext } from "../src/FlinkContext";
|
|
3
|
+
import { FlinkResponse } from "../src/FlinkResponse";
|
|
4
|
+
import { FlinkError } from "../src/FlinkErrors";
|
|
5
|
+
import { badRequest, internalServerError, notFound } from "../src/FlinkErrors";
|
|
6
|
+
import { HttpMethod } from "../src/FlinkHttpHandler";
|
|
7
|
+
|
|
8
|
+
interface TestContext extends FlinkContext {}
|
|
9
|
+
|
|
10
|
+
describe("FlinkApp onError callback", () => {
|
|
11
|
+
let app: FlinkApp<TestContext>;
|
|
12
|
+
let capturedError: FlinkResponse<FlinkError> | null;
|
|
13
|
+
let capturedContext: any;
|
|
14
|
+
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
capturedError = null;
|
|
17
|
+
capturedContext = null;
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
afterEach(async () => {
|
|
21
|
+
if (app && app.started) {
|
|
22
|
+
await app.stop();
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("should be configurable in FlinkOptions", () => {
|
|
27
|
+
const callback = (error: FlinkResponse<FlinkError>, context: any) => {
|
|
28
|
+
capturedError = error;
|
|
29
|
+
capturedContext = context;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
app = new FlinkApp<TestContext>({
|
|
33
|
+
name: "test-app",
|
|
34
|
+
disableHttpServer: true,
|
|
35
|
+
onError: callback,
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
expect(app).toBeDefined();
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("should have proper type signature for onError callback", () => {
|
|
42
|
+
// This test verifies TypeScript compilation
|
|
43
|
+
// The callback should accept FlinkResponse<FlinkError> and context object
|
|
44
|
+
|
|
45
|
+
const syncCallback = (error: FlinkResponse<FlinkError>, context: { req: any; method: HttpMethod; path: string; reqId: string }) => {
|
|
46
|
+
console.log(error.status, context.reqId);
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const asyncCallback = async (error: FlinkResponse<FlinkError>, context: { req: any; method: HttpMethod; path: string; reqId: string }) => {
|
|
50
|
+
await Promise.resolve();
|
|
51
|
+
console.log(error.status, context.reqId);
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const app1 = new FlinkApp<TestContext>({
|
|
55
|
+
name: "test-app-1",
|
|
56
|
+
disableHttpServer: true,
|
|
57
|
+
onError: syncCallback,
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
const app2 = new FlinkApp<TestContext>({
|
|
61
|
+
name: "test-app-2",
|
|
62
|
+
disableHttpServer: true,
|
|
63
|
+
onError: asyncCallback,
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
expect(app1).toBeDefined();
|
|
67
|
+
expect(app2).toBeDefined();
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("should be optional in FlinkOptions", async () => {
|
|
71
|
+
app = new FlinkApp<TestContext>({
|
|
72
|
+
name: "test-app",
|
|
73
|
+
disableHttpServer: true,
|
|
74
|
+
// No onError callback
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
await app.start();
|
|
78
|
+
expect(app.started).toBe(true);
|
|
79
|
+
});
|
|
80
|
+
});
|
package/spec/utils.spec.ts
CHANGED
|
@@ -148,6 +148,33 @@ describe("Utils", () => {
|
|
|
148
148
|
expect(formatted).toContain('Missing required property: email');
|
|
149
149
|
expect(formatted).toContain('Missing required property: age');
|
|
150
150
|
});
|
|
151
|
+
|
|
152
|
+
it("should show specific additional property names", () => {
|
|
153
|
+
const errors = [
|
|
154
|
+
{
|
|
155
|
+
instancePath: "",
|
|
156
|
+
schemaPath: "#/additionalProperties",
|
|
157
|
+
keyword: "additionalProperties",
|
|
158
|
+
params: { additionalProperty: "extraField1" },
|
|
159
|
+
message: "must NOT have additional properties",
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
instancePath: "",
|
|
163
|
+
schemaPath: "#/additionalProperties",
|
|
164
|
+
keyword: "additionalProperties",
|
|
165
|
+
params: { additionalProperty: "extraField2" },
|
|
166
|
+
message: "must NOT have additional properties",
|
|
167
|
+
},
|
|
168
|
+
];
|
|
169
|
+
const data = { name: "John", age: 30, extraField1: "value1", extraField2: "value2" };
|
|
170
|
+
|
|
171
|
+
const formatted = formatValidationErrors(errors, data);
|
|
172
|
+
|
|
173
|
+
// This formatted output is used in HTTP error responses (400/500) in the error.detail field
|
|
174
|
+
expect(formatted).toContain('Path: /');
|
|
175
|
+
expect(formatted).toContain('Additional property not allowed: extraField1');
|
|
176
|
+
expect(formatted).toContain('Additional property not allowed: extraField2');
|
|
177
|
+
});
|
|
151
178
|
});
|
|
152
179
|
});
|
|
153
180
|
|
package/src/FlinkApp.ts
CHANGED
|
@@ -11,7 +11,7 @@ import { AsyncTask, CronJob, SimpleIntervalJob, ToadScheduler } from "toad-sched
|
|
|
11
11
|
import { v4 } from "uuid";
|
|
12
12
|
import { FlinkAuthPlugin } from "./auth/FlinkAuthPlugin";
|
|
13
13
|
import { FlinkContext } from "./FlinkContext";
|
|
14
|
-
import { internalServerError, notFound, unauthorized } from "./FlinkErrors";
|
|
14
|
+
import { FlinkError, internalServerError, notFound, unauthorized } from "./FlinkErrors";
|
|
15
15
|
import { FlinkRequest, Handler, HandlerFile, HttpMethod, QueryParamMetadata, RouteProps } from "./FlinkHttpHandler";
|
|
16
16
|
import { FlinkJobFile } from "./FlinkJob";
|
|
17
17
|
import { log } from "./FlinkLog";
|
|
@@ -197,6 +197,54 @@ export interface FlinkOptions {
|
|
|
197
197
|
*/
|
|
198
198
|
format?: string;
|
|
199
199
|
};
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Optional callback invoked when an error occurs in a handler.
|
|
203
|
+
* The error response and request context are passed for custom
|
|
204
|
+
* error logging or monitoring. This is a side-effect only and
|
|
205
|
+
* will not modify the response flow.
|
|
206
|
+
*
|
|
207
|
+
* Supports both synchronous and asynchronous callbacks. Any errors
|
|
208
|
+
* thrown or rejected by the callback will be caught and logged
|
|
209
|
+
* without affecting the error response to the client.
|
|
210
|
+
*
|
|
211
|
+
* @param error - The error response with status and error details
|
|
212
|
+
* @param context - Request context including method, path, and request ID
|
|
213
|
+
*
|
|
214
|
+
* @example
|
|
215
|
+
* ```ts
|
|
216
|
+
* // Synchronous callback
|
|
217
|
+
* onError: (error, context) => {
|
|
218
|
+
* if (error.status >= 500) {
|
|
219
|
+
* logger.error('Server error occurred', {
|
|
220
|
+
* status: error.status,
|
|
221
|
+
* code: error.error?.code,
|
|
222
|
+
* route: `${context.method} ${context.path}`,
|
|
223
|
+
* reqId: context.reqId
|
|
224
|
+
* });
|
|
225
|
+
* }
|
|
226
|
+
* }
|
|
227
|
+
*
|
|
228
|
+
* // Asynchronous callback
|
|
229
|
+
* onError: async (error, context) => {
|
|
230
|
+
* if (error.status >= 500) {
|
|
231
|
+
* await monitoringService.reportError({
|
|
232
|
+
* error,
|
|
233
|
+
* context
|
|
234
|
+
* });
|
|
235
|
+
* }
|
|
236
|
+
* }
|
|
237
|
+
* ```
|
|
238
|
+
*/
|
|
239
|
+
onError?: (
|
|
240
|
+
error: FlinkResponse<FlinkError>,
|
|
241
|
+
context: {
|
|
242
|
+
req: FlinkRequest;
|
|
243
|
+
method: HttpMethod;
|
|
244
|
+
path: string;
|
|
245
|
+
reqId: string;
|
|
246
|
+
}
|
|
247
|
+
) => void | Promise<void>;
|
|
200
248
|
}
|
|
201
249
|
|
|
202
250
|
export interface HandlerConfig {
|
|
@@ -242,6 +290,7 @@ export class FlinkApp<C extends FlinkContext> {
|
|
|
242
290
|
private schedulingOptions?: FlinkOptions["scheduling"];
|
|
243
291
|
private disableHttpServer = false;
|
|
244
292
|
private expressServer: any; // for simplicity, we don't want to import types from express/node here
|
|
293
|
+
private onError?: FlinkOptions["onError"];
|
|
245
294
|
|
|
246
295
|
private repos: { [x: string]: FlinkRepo<C, any> } = {};
|
|
247
296
|
|
|
@@ -272,6 +321,7 @@ export class FlinkApp<C extends FlinkContext> {
|
|
|
272
321
|
this.schedulingOptions = opts.scheduling;
|
|
273
322
|
this.disableHttpServer = !!opts.disableHttpServer;
|
|
274
323
|
this.accessLog = { enabled: true, format: "dev", ...opts.accessLog };
|
|
324
|
+
this.onError = opts.onError;
|
|
275
325
|
}
|
|
276
326
|
|
|
277
327
|
get ctx() {
|
|
@@ -512,7 +562,7 @@ export class FlinkApp<C extends FlinkContext> {
|
|
|
512
562
|
error: {
|
|
513
563
|
id: v4(),
|
|
514
564
|
title: "Bad request",
|
|
515
|
-
detail:
|
|
565
|
+
detail: formattedErrors,
|
|
516
566
|
},
|
|
517
567
|
});
|
|
518
568
|
}
|
|
@@ -540,9 +590,11 @@ export class FlinkApp<C extends FlinkContext> {
|
|
|
540
590
|
origin: routeProps.origin,
|
|
541
591
|
});
|
|
542
592
|
} catch (err: any) {
|
|
593
|
+
let errorResponse: FlinkResponse<FlinkError>;
|
|
594
|
+
|
|
543
595
|
// duck typing to check if it is a FlinkError
|
|
544
596
|
if (typeof err.status === "number" && err.status >= 400 && err.status < 600 && err.error) {
|
|
545
|
-
|
|
597
|
+
errorResponse = {
|
|
546
598
|
status: err.status,
|
|
547
599
|
error: {
|
|
548
600
|
id: err.error.id || v4(),
|
|
@@ -550,12 +602,35 @@ export class FlinkApp<C extends FlinkContext> {
|
|
|
550
602
|
detail: err.error.detail,
|
|
551
603
|
code: err.error.code,
|
|
552
604
|
},
|
|
553
|
-
}
|
|
605
|
+
};
|
|
606
|
+
} else {
|
|
607
|
+
log.warn(`Handler '${methodAndRoute}' threw unhandled exception ${err}`);
|
|
608
|
+
console.error(err);
|
|
609
|
+
errorResponse = internalServerError(err as any);
|
|
554
610
|
}
|
|
555
611
|
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
612
|
+
// Invoke onError callback if provided
|
|
613
|
+
if (this.onError) {
|
|
614
|
+
try {
|
|
615
|
+
const result = this.onError(errorResponse, {
|
|
616
|
+
req: req as FlinkRequest,
|
|
617
|
+
method: method!,
|
|
618
|
+
path: routeProps.path,
|
|
619
|
+
reqId: req.reqId,
|
|
620
|
+
});
|
|
621
|
+
|
|
622
|
+
// Handle async callbacks - don't wait for them
|
|
623
|
+
if (result instanceof Promise) {
|
|
624
|
+
result.catch((callbackErr) => {
|
|
625
|
+
log.error(`onError callback rejected with: ${callbackErr}`);
|
|
626
|
+
});
|
|
627
|
+
}
|
|
628
|
+
} catch (callbackErr) {
|
|
629
|
+
log.error(`onError callback threw an exception: ${callbackErr}`);
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
return res.status(errorResponse.status || 500).json(errorResponse);
|
|
559
634
|
}
|
|
560
635
|
|
|
561
636
|
if (validateRes && !isError(handlerRes)) {
|
|
@@ -570,7 +645,7 @@ export class FlinkApp<C extends FlinkContext> {
|
|
|
570
645
|
error: {
|
|
571
646
|
id: v4(),
|
|
572
647
|
title: "Bad response",
|
|
573
|
-
detail:
|
|
648
|
+
detail: formattedErrors,
|
|
574
649
|
},
|
|
575
650
|
});
|
|
576
651
|
}
|
package/src/FlinkHttpHandler.ts
CHANGED
|
@@ -18,8 +18,13 @@ type Params = Request["params"];
|
|
|
18
18
|
* Query type for request query parameters.
|
|
19
19
|
* Does currently not allow nested objects, although
|
|
20
20
|
* underlying express Request does allow it.
|
|
21
|
+
*
|
|
22
|
+
* Uses index signature to allow both Record types and interface types
|
|
23
|
+
* to be assignable to Query without requiring explicit index signatures.
|
|
21
24
|
*/
|
|
22
|
-
type Query =
|
|
25
|
+
type Query = {
|
|
26
|
+
[x: string]: string | string[] | undefined;
|
|
27
|
+
};
|
|
23
28
|
|
|
24
29
|
/**
|
|
25
30
|
* Flink request extends express Request but adds reqId and user object.
|
package/src/utils.ts
CHANGED
|
@@ -165,6 +165,8 @@ export function formatValidationErrors(errors: any[] | null | undefined, data: a
|
|
|
165
165
|
formatted.push(` - Missing required property: ${error.params.missingProperty}`);
|
|
166
166
|
} else if (error.keyword === "type") {
|
|
167
167
|
formatted.push(` - Invalid type at ${error.schemaPath}: expected ${error.params.type}, got ${typeof dataAtPath}`);
|
|
168
|
+
} else if (error.keyword === "additionalProperties") {
|
|
169
|
+
formatted.push(` - Additional property not allowed: ${error.params.additionalProperty}`);
|
|
168
170
|
} else if (error.keyword === "anyOf" || error.keyword === "oneOf") {
|
|
169
171
|
formatted.push(` - ${error.message}`);
|
|
170
172
|
} else {
|