@flink-app/flink 0.14.0 → 0.14.2
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 +12 -0
- package/dist/src/FlinkApp.d.ts +48 -1
- package/dist/src/FlinkApp.js +38 -13
- package/package.json +1 -1
- package/spec/FlinkApp.onError.spec.ts +80 -0
- package/src/FlinkApp.ts +81 -6
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# @flink-app/flink
|
|
2
2
|
|
|
3
|
+
## 0.14.2
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 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.
|
|
8
|
+
|
|
9
|
+
## 0.14.1
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- fix: restore compatibility between core framework and plugins after 0.14.0 release
|
|
14
|
+
|
|
3
15
|
## 0.14.0
|
|
4
16
|
|
|
5
17
|
### Minor 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:
|
|
@@ -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)));
|
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/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() {
|
|
@@ -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)) {
|