@flink-app/flink 2.0.0-alpha.98 → 2.0.0-alpha.99
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 +8 -0
- package/dist/src/FlinkApp.d.ts +27 -9
- package/dist/src/FlinkApp.js +56 -41
- package/package.json +1 -1
- package/spec/FlinkApp.onError.invocation.spec.ts +151 -0
- package/src/FlinkApp.ts +72 -36
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# @flink-app/flink
|
|
2
2
|
|
|
3
|
+
## 2.0.0-alpha.99
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- ed9cd2c: Extend `onError` callback to also fire for request validation (400 Bad request) and response validation (500 Bad response) failures, in addition to handler-thrown errors. Streaming handler errors continue to be delivered via `stream.error()` and do not trigger `onError`.
|
|
8
|
+
|
|
9
|
+
The callback context now also includes the app context as `ctx`, so handlers can use repos/plugins (e.g. `context.ctx.repos.errorLogRepo`) from within `onError`. `FlinkOptions` is now generic over the app context (`FlinkOptions<C>`) with a default of `FlinkContext`, keeping existing usages backward compatible.
|
|
10
|
+
|
|
3
11
|
## 2.0.0-alpha.98
|
|
4
12
|
|
|
5
13
|
### Minor Changes
|
package/dist/src/FlinkApp.d.ts
CHANGED
|
@@ -64,7 +64,7 @@ export declare const autoRegisteredServices: {
|
|
|
64
64
|
serviceInstanceName: string;
|
|
65
65
|
Service: any;
|
|
66
66
|
}[];
|
|
67
|
-
export interface FlinkOptions {
|
|
67
|
+
export interface FlinkOptions<C extends FlinkContext = FlinkContext> {
|
|
68
68
|
/**
|
|
69
69
|
* Name of application, will only show in logs and in HTTP header.
|
|
70
70
|
*/
|
|
@@ -215,17 +215,26 @@ export interface FlinkOptions {
|
|
|
215
215
|
format?: string;
|
|
216
216
|
};
|
|
217
217
|
/**
|
|
218
|
-
* Optional callback invoked when an error occurs
|
|
218
|
+
* Optional callback invoked when an error occurs while serving a request.
|
|
219
219
|
* The error response and request context are passed for custom
|
|
220
220
|
* error logging or monitoring. This is a side-effect only and
|
|
221
221
|
* will not modify the response flow.
|
|
222
222
|
*
|
|
223
|
+
* Invoked for:
|
|
224
|
+
* - Handler-thrown errors (FlinkErrors and unhandled exceptions)
|
|
225
|
+
* - Request validation failures (400 Bad request)
|
|
226
|
+
* - Response validation failures (500 Bad response)
|
|
227
|
+
*
|
|
228
|
+
* Not invoked for errors in streaming (SSE/NDJSON) handlers, which are
|
|
229
|
+
* delivered to the client via `stream.error()` instead.
|
|
230
|
+
*
|
|
223
231
|
* Supports both synchronous and asynchronous callbacks. Any errors
|
|
224
232
|
* thrown or rejected by the callback will be caught and logged
|
|
225
|
-
* without affecting the error response to the client.
|
|
233
|
+
* without affecting the error response to the client. Async callbacks
|
|
234
|
+
* are fire-and-forget — they are not awaited before the response is sent.
|
|
226
235
|
*
|
|
227
236
|
* @param error - The error response with status and error details
|
|
228
|
-
* @param context - Request context including method, path, and
|
|
237
|
+
* @param context - Request context including method, path, request ID and the app context
|
|
229
238
|
*
|
|
230
239
|
* @example
|
|
231
240
|
* ```ts
|
|
@@ -241,12 +250,14 @@ export interface FlinkOptions {
|
|
|
241
250
|
* }
|
|
242
251
|
* }
|
|
243
252
|
*
|
|
244
|
-
* // Asynchronous callback
|
|
253
|
+
* // Asynchronous callback using the app context
|
|
245
254
|
* onError: async (error, context) => {
|
|
246
255
|
* if (error.status >= 500) {
|
|
247
|
-
* await
|
|
248
|
-
* error,
|
|
249
|
-
*
|
|
256
|
+
* await context.ctx.repos.errorLogRepo.create({
|
|
257
|
+
* status: error.status,
|
|
258
|
+
* detail: error.error?.detail,
|
|
259
|
+
* route: `${context.method} ${context.path}`,
|
|
260
|
+
* reqId: context.reqId,
|
|
250
261
|
* });
|
|
251
262
|
* }
|
|
252
263
|
* }
|
|
@@ -257,6 +268,7 @@ export interface FlinkOptions {
|
|
|
257
268
|
method: HttpMethod;
|
|
258
269
|
path: string;
|
|
259
270
|
reqId: string;
|
|
271
|
+
ctx: C;
|
|
260
272
|
}) => void | Promise<void>;
|
|
261
273
|
}
|
|
262
274
|
export interface HandlerConfig {
|
|
@@ -317,7 +329,7 @@ export declare class FlinkApp<C extends FlinkContext> {
|
|
|
317
329
|
private allInstanceScheduler?;
|
|
318
330
|
private leaderElection?;
|
|
319
331
|
private accessLog;
|
|
320
|
-
constructor(opts: FlinkOptions);
|
|
332
|
+
constructor(opts: FlinkOptions<C>);
|
|
321
333
|
get ctx(): C;
|
|
322
334
|
start(): Promise<this>;
|
|
323
335
|
stop(): Promise<void>;
|
|
@@ -400,6 +412,12 @@ export declare class FlinkApp<C extends FlinkContext> {
|
|
|
400
412
|
*/
|
|
401
413
|
private initPluginDb;
|
|
402
414
|
private authenticate;
|
|
415
|
+
/**
|
|
416
|
+
* Invokes the optional onError callback in a fire-and-forget manner.
|
|
417
|
+
* Any error thrown or rejected by the callback is caught and logged so
|
|
418
|
+
* it never affects the error response sent to the client.
|
|
419
|
+
*/
|
|
420
|
+
private invokeOnError;
|
|
403
421
|
getRegisteredRoutes(): string[];
|
|
404
422
|
private get isSchedulingEnabled();
|
|
405
423
|
private get leaderElectionConfig();
|
package/dist/src/FlinkApp.js
CHANGED
|
@@ -451,7 +451,7 @@ var FlinkApp = /** @class */ (function () {
|
|
|
451
451
|
}
|
|
452
452
|
}
|
|
453
453
|
this.expressApp[method](routeProps.path, function (req, res) { return __awaiter(_this, void 0, void 0, function () {
|
|
454
|
-
var valid, formattedErrors, data, normalizedQuery, _i, _a, _b, key, value, stream, handlerRes, flinkReq_1, err_1, errorResponse,
|
|
454
|
+
var valid, formattedErrors, errorResponse, data, normalizedQuery, _i, _a, _b, key, value, stream, handlerRes, flinkReq_1, err_1, errorResponse, detail, errorResponse, valid, formattedErrors, errorResponse;
|
|
455
455
|
var _this = this;
|
|
456
456
|
var _c;
|
|
457
457
|
return __generator(this, function (_d) {
|
|
@@ -470,14 +470,16 @@ var FlinkApp = /** @class */ (function () {
|
|
|
470
470
|
if (!valid) {
|
|
471
471
|
formattedErrors = (0, utils_1.formatValidationErrors)(validateReq_1.errors, req.body);
|
|
472
472
|
FlinkLog_1.log.warn("[".concat(req.reqId, "] ").concat(methodAndRoute_1, ": Bad request\n").concat(formattedErrors));
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
473
|
+
errorResponse = {
|
|
474
|
+
status: 400,
|
|
475
|
+
error: {
|
|
476
|
+
id: (0, uuid_1.v4)(),
|
|
477
|
+
title: "Bad request",
|
|
478
|
+
detail: formattedErrors,
|
|
479
|
+
},
|
|
480
|
+
};
|
|
481
|
+
this.invokeOnError(errorResponse, req, method, routeProps.path);
|
|
482
|
+
return [2 /*return*/, res.status(400).json(errorResponse)];
|
|
481
483
|
}
|
|
482
484
|
}
|
|
483
485
|
// Skip mock API for streaming handlers
|
|
@@ -568,26 +570,7 @@ var FlinkApp = /** @class */ (function () {
|
|
|
568
570
|
console.error(err_1);
|
|
569
571
|
errorResponse = (0, FlinkErrors_1.internalServerError)(err_1);
|
|
570
572
|
}
|
|
571
|
-
|
|
572
|
-
if (this.onError) {
|
|
573
|
-
try {
|
|
574
|
-
result = this.onError(errorResponse, {
|
|
575
|
-
req: req,
|
|
576
|
-
method: method,
|
|
577
|
-
path: routeProps.path,
|
|
578
|
-
reqId: req.reqId,
|
|
579
|
-
});
|
|
580
|
-
// Handle async callbacks - don't wait for them
|
|
581
|
-
if (result instanceof Promise) {
|
|
582
|
-
result.catch(function (callbackErr) {
|
|
583
|
-
FlinkLog_1.log.error("onError callback rejected with: ".concat(callbackErr));
|
|
584
|
-
});
|
|
585
|
-
}
|
|
586
|
-
}
|
|
587
|
-
catch (callbackErr) {
|
|
588
|
-
FlinkLog_1.log.error("onError callback threw an exception: ".concat(callbackErr));
|
|
589
|
-
}
|
|
590
|
-
}
|
|
573
|
+
this.invokeOnError(errorResponse, req, method, routeProps.path);
|
|
591
574
|
return [2 /*return*/, res.status(errorResponse.status || 500).json(errorResponse)];
|
|
592
575
|
case 6:
|
|
593
576
|
// Skip response handling for streaming handlers (stream controls response lifecycle)
|
|
@@ -603,10 +586,12 @@ var FlinkApp = /** @class */ (function () {
|
|
|
603
586
|
if (handlerRes.status !== 204) {
|
|
604
587
|
detail = "Response schema is defined but handler returned no data";
|
|
605
588
|
FlinkLog_1.log.warn("[".concat(req.reqId, "] ").concat(methodAndRoute_1, ": Bad response - ").concat(detail));
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
589
|
+
errorResponse = {
|
|
590
|
+
status: 500,
|
|
591
|
+
error: { id: (0, uuid_1.v4)(), title: "Bad response", detail: detail },
|
|
592
|
+
};
|
|
593
|
+
this.invokeOnError(errorResponse, req, method, routeProps.path);
|
|
594
|
+
return [2 /*return*/, res.status(500).json(errorResponse)];
|
|
610
595
|
}
|
|
611
596
|
}
|
|
612
597
|
else {
|
|
@@ -614,14 +599,16 @@ var FlinkApp = /** @class */ (function () {
|
|
|
614
599
|
if (!valid) {
|
|
615
600
|
formattedErrors = (0, utils_1.formatValidationErrors)(validateRes_1.errors, handlerRes.data);
|
|
616
601
|
FlinkLog_1.log.warn("[".concat(req.reqId, "] ").concat(methodAndRoute_1, ": Bad response\n").concat(formattedErrors));
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
602
|
+
errorResponse = {
|
|
603
|
+
status: 500,
|
|
604
|
+
error: {
|
|
605
|
+
id: (0, uuid_1.v4)(),
|
|
606
|
+
title: "Bad response",
|
|
607
|
+
detail: formattedErrors,
|
|
608
|
+
},
|
|
609
|
+
};
|
|
610
|
+
this.invokeOnError(errorResponse, req, method, routeProps.path);
|
|
611
|
+
return [2 /*return*/, res.status(500).json(errorResponse)];
|
|
625
612
|
}
|
|
626
613
|
}
|
|
627
614
|
}
|
|
@@ -1306,6 +1293,34 @@ var FlinkApp = /** @class */ (function () {
|
|
|
1306
1293
|
});
|
|
1307
1294
|
});
|
|
1308
1295
|
};
|
|
1296
|
+
/**
|
|
1297
|
+
* Invokes the optional onError callback in a fire-and-forget manner.
|
|
1298
|
+
* Any error thrown or rejected by the callback is caught and logged so
|
|
1299
|
+
* it never affects the error response sent to the client.
|
|
1300
|
+
*/
|
|
1301
|
+
FlinkApp.prototype.invokeOnError = function (errorResponse, req, method, path) {
|
|
1302
|
+
if (!this.onError) {
|
|
1303
|
+
return;
|
|
1304
|
+
}
|
|
1305
|
+
try {
|
|
1306
|
+
var result = this.onError(errorResponse, {
|
|
1307
|
+
req: req,
|
|
1308
|
+
method: method,
|
|
1309
|
+
path: path,
|
|
1310
|
+
reqId: req.reqId,
|
|
1311
|
+
ctx: this.ctx,
|
|
1312
|
+
});
|
|
1313
|
+
// Handle async callbacks - don't wait for them
|
|
1314
|
+
if (result instanceof Promise) {
|
|
1315
|
+
result.catch(function (callbackErr) {
|
|
1316
|
+
FlinkLog_1.log.error("onError callback rejected with: ".concat(callbackErr));
|
|
1317
|
+
});
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
catch (callbackErr) {
|
|
1321
|
+
FlinkLog_1.log.error("onError callback threw an exception: ".concat(callbackErr));
|
|
1322
|
+
}
|
|
1323
|
+
};
|
|
1309
1324
|
FlinkApp.prototype.getRegisteredRoutes = function () {
|
|
1310
1325
|
return Array.from(this.handlerRouteCache.values());
|
|
1311
1326
|
};
|
package/package.json
CHANGED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { FlinkApp } from "../src/FlinkApp";
|
|
2
|
+
import { FlinkContext } from "../src/FlinkContext";
|
|
3
|
+
import { FlinkError } from "../src/FlinkErrors";
|
|
4
|
+
import { GetHandler, Handler, HttpMethod } from "../src/FlinkHttpHandler";
|
|
5
|
+
import { FlinkResponse } from "../src/FlinkResponse";
|
|
6
|
+
|
|
7
|
+
const request = require("supertest");
|
|
8
|
+
|
|
9
|
+
interface TestContext extends FlinkContext {}
|
|
10
|
+
|
|
11
|
+
const reqSchema = {
|
|
12
|
+
type: "object",
|
|
13
|
+
properties: {
|
|
14
|
+
name: { type: "string" },
|
|
15
|
+
},
|
|
16
|
+
required: ["name"],
|
|
17
|
+
additionalProperties: false,
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const resSchema = {
|
|
21
|
+
type: "object",
|
|
22
|
+
properties: {
|
|
23
|
+
id: { type: "string" },
|
|
24
|
+
},
|
|
25
|
+
required: ["id"],
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
describe("FlinkApp onError invocation", () => {
|
|
29
|
+
let app: FlinkApp<TestContext>;
|
|
30
|
+
let calls: { error: FlinkResponse<FlinkError>; context: any }[];
|
|
31
|
+
|
|
32
|
+
const onError = (error: FlinkResponse<FlinkError>, context: any) => {
|
|
33
|
+
calls.push({ error, context });
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
beforeEach(() => {
|
|
37
|
+
calls = [];
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
afterEach(async () => {
|
|
41
|
+
if (app && app.started) {
|
|
42
|
+
await app.stop();
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("should invoke onError for request validation 400s", async () => {
|
|
47
|
+
const handler: Handler<TestContext, any, any> = async () => {
|
|
48
|
+
return { data: { id: "ok" } };
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
app = new FlinkApp<TestContext>({ name: "test-onerror-req", port: 4210, onError });
|
|
52
|
+
await app.start();
|
|
53
|
+
|
|
54
|
+
app.addHandler({
|
|
55
|
+
default: handler,
|
|
56
|
+
Route: { method: HttpMethod.post, path: "/test", reqSchema },
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
const response = await request(app.expressApp).post("/test").send({ wrong: "field" });
|
|
60
|
+
|
|
61
|
+
expect(response.status).toBe(400);
|
|
62
|
+
expect(calls.length).toBe(1);
|
|
63
|
+
expect(calls[0].error.status).toBe(400);
|
|
64
|
+
expect(calls[0].error.error?.title).toBe("Bad request");
|
|
65
|
+
expect(calls[0].context.method).toBe(HttpMethod.post);
|
|
66
|
+
expect(calls[0].context.path).toBe("/test");
|
|
67
|
+
expect(calls[0].context.reqId).toBeDefined();
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("should invoke onError for response validation 500s (invalid shape)", async () => {
|
|
71
|
+
const handler: GetHandler<TestContext, any> = async () => {
|
|
72
|
+
return { data: { wrongField: 123 } }; // missing required "id"
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
app = new FlinkApp<TestContext>({ name: "test-onerror-res", port: 4211, onError });
|
|
76
|
+
await app.start();
|
|
77
|
+
|
|
78
|
+
app.addHandler({
|
|
79
|
+
default: handler,
|
|
80
|
+
Route: { method: HttpMethod.get, path: "/test", resSchema },
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
const response = await request(app.expressApp).get("/test");
|
|
84
|
+
|
|
85
|
+
expect(response.status).toBe(500);
|
|
86
|
+
expect(calls.length).toBe(1);
|
|
87
|
+
expect(calls[0].error.status).toBe(500);
|
|
88
|
+
expect(calls[0].error.error?.title).toBe("Bad response");
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("should invoke onError for response validation 500s (no data)", async () => {
|
|
92
|
+
const handler: GetHandler<TestContext, any> = async () => {
|
|
93
|
+
return { status: 200 } as any; // no data
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
app = new FlinkApp<TestContext>({ name: "test-onerror-nodata", port: 4212, onError });
|
|
97
|
+
await app.start();
|
|
98
|
+
|
|
99
|
+
app.addHandler({
|
|
100
|
+
default: handler,
|
|
101
|
+
Route: { method: HttpMethod.get, path: "/test", resSchema },
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
const response = await request(app.expressApp).get("/test");
|
|
105
|
+
|
|
106
|
+
expect(response.status).toBe(500);
|
|
107
|
+
expect(calls.length).toBe(1);
|
|
108
|
+
expect(calls[0].error.status).toBe(500);
|
|
109
|
+
expect(calls[0].error.error?.title).toBe("Bad response");
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("should still invoke onError for handler-thrown errors", async () => {
|
|
113
|
+
const handler: GetHandler<TestContext, any> = async () => {
|
|
114
|
+
throw new Error("boom");
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
app = new FlinkApp<TestContext>({ name: "test-onerror-throw", port: 4213, onError });
|
|
118
|
+
await app.start();
|
|
119
|
+
|
|
120
|
+
app.addHandler({
|
|
121
|
+
default: handler,
|
|
122
|
+
Route: { method: HttpMethod.get, path: "/test" },
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
const response = await request(app.expressApp).get("/test");
|
|
126
|
+
|
|
127
|
+
expect(response.status).toBe(500);
|
|
128
|
+
expect(calls.length).toBe(1);
|
|
129
|
+
expect(calls[0].error.status).toBe(500);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it("should pass the app context (ctx) to the callback", async () => {
|
|
133
|
+
const handler: GetHandler<TestContext, any> = async () => {
|
|
134
|
+
throw new Error("boom");
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
app = new FlinkApp<TestContext>({ name: "test-onerror-ctx", port: 4214, onError });
|
|
138
|
+
await app.start();
|
|
139
|
+
|
|
140
|
+
app.addHandler({
|
|
141
|
+
default: handler,
|
|
142
|
+
Route: { method: HttpMethod.get, path: "/test" },
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
await request(app.expressApp).get("/test");
|
|
146
|
+
|
|
147
|
+
expect(calls.length).toBe(1);
|
|
148
|
+
expect(calls[0].context.ctx).toBeDefined();
|
|
149
|
+
expect(calls[0].context.ctx).toBe(app.ctx);
|
|
150
|
+
});
|
|
151
|
+
});
|
package/src/FlinkApp.ts
CHANGED
|
@@ -99,7 +99,7 @@ export const autoRegisteredServices: {
|
|
|
99
99
|
Service: any;
|
|
100
100
|
}[] = [];
|
|
101
101
|
|
|
102
|
-
export interface FlinkOptions {
|
|
102
|
+
export interface FlinkOptions<C extends FlinkContext = FlinkContext> {
|
|
103
103
|
/**
|
|
104
104
|
* Name of application, will only show in logs and in HTTP header.
|
|
105
105
|
*/
|
|
@@ -269,17 +269,26 @@ export interface FlinkOptions {
|
|
|
269
269
|
};
|
|
270
270
|
|
|
271
271
|
/**
|
|
272
|
-
* Optional callback invoked when an error occurs
|
|
272
|
+
* Optional callback invoked when an error occurs while serving a request.
|
|
273
273
|
* The error response and request context are passed for custom
|
|
274
274
|
* error logging or monitoring. This is a side-effect only and
|
|
275
275
|
* will not modify the response flow.
|
|
276
276
|
*
|
|
277
|
+
* Invoked for:
|
|
278
|
+
* - Handler-thrown errors (FlinkErrors and unhandled exceptions)
|
|
279
|
+
* - Request validation failures (400 Bad request)
|
|
280
|
+
* - Response validation failures (500 Bad response)
|
|
281
|
+
*
|
|
282
|
+
* Not invoked for errors in streaming (SSE/NDJSON) handlers, which are
|
|
283
|
+
* delivered to the client via `stream.error()` instead.
|
|
284
|
+
*
|
|
277
285
|
* Supports both synchronous and asynchronous callbacks. Any errors
|
|
278
286
|
* thrown or rejected by the callback will be caught and logged
|
|
279
|
-
* without affecting the error response to the client.
|
|
287
|
+
* without affecting the error response to the client. Async callbacks
|
|
288
|
+
* are fire-and-forget — they are not awaited before the response is sent.
|
|
280
289
|
*
|
|
281
290
|
* @param error - The error response with status and error details
|
|
282
|
-
* @param context - Request context including method, path, and
|
|
291
|
+
* @param context - Request context including method, path, request ID and the app context
|
|
283
292
|
*
|
|
284
293
|
* @example
|
|
285
294
|
* ```ts
|
|
@@ -295,12 +304,14 @@ export interface FlinkOptions {
|
|
|
295
304
|
* }
|
|
296
305
|
* }
|
|
297
306
|
*
|
|
298
|
-
* // Asynchronous callback
|
|
307
|
+
* // Asynchronous callback using the app context
|
|
299
308
|
* onError: async (error, context) => {
|
|
300
309
|
* if (error.status >= 500) {
|
|
301
|
-
* await
|
|
302
|
-
* error,
|
|
303
|
-
*
|
|
310
|
+
* await context.ctx.repos.errorLogRepo.create({
|
|
311
|
+
* status: error.status,
|
|
312
|
+
* detail: error.error?.detail,
|
|
313
|
+
* route: `${context.method} ${context.path}`,
|
|
314
|
+
* reqId: context.reqId,
|
|
304
315
|
* });
|
|
305
316
|
* }
|
|
306
317
|
* }
|
|
@@ -313,6 +324,7 @@ export interface FlinkOptions {
|
|
|
313
324
|
method: HttpMethod;
|
|
314
325
|
path: string;
|
|
315
326
|
reqId: string;
|
|
327
|
+
ctx: C;
|
|
316
328
|
}
|
|
317
329
|
) => void | Promise<void>;
|
|
318
330
|
}
|
|
@@ -362,7 +374,7 @@ export class FlinkApp<C extends FlinkContext> {
|
|
|
362
374
|
private schedulingOptions?: FlinkOptions["scheduling"];
|
|
363
375
|
private disableHttpServer = false;
|
|
364
376
|
private expressServer: any; // for simplicity, we don't want to import types from express/node here
|
|
365
|
-
private onError?: FlinkOptions["onError"];
|
|
377
|
+
private onError?: FlinkOptions<C>["onError"];
|
|
366
378
|
|
|
367
379
|
private repos: { [x: string]: FlinkRepo<C, any> } = {};
|
|
368
380
|
private services: { [x: string]: FlinkService<C> } = {};
|
|
@@ -383,7 +395,7 @@ export class FlinkApp<C extends FlinkContext> {
|
|
|
383
395
|
|
|
384
396
|
private accessLog: { enabled: boolean; format: string };
|
|
385
397
|
|
|
386
|
-
constructor(opts: FlinkOptions) {
|
|
398
|
+
constructor(opts: FlinkOptions<C>) {
|
|
387
399
|
// Load config file and initialize logging
|
|
388
400
|
const { loadFlinkConfig } = require("./utils/loadFlinkConfig");
|
|
389
401
|
const flinkConfig = loadFlinkConfig();
|
|
@@ -713,14 +725,18 @@ export class FlinkApp<C extends FlinkContext> {
|
|
|
713
725
|
const formattedErrors = formatValidationErrors(validateReq.errors, req.body);
|
|
714
726
|
log.warn(`[${req.reqId}] ${methodAndRoute}: Bad request\n${formattedErrors}`);
|
|
715
727
|
|
|
716
|
-
|
|
728
|
+
const errorResponse: FlinkResponse<FlinkError> = {
|
|
717
729
|
status: 400,
|
|
718
730
|
error: {
|
|
719
731
|
id: v4(),
|
|
720
732
|
title: "Bad request",
|
|
721
733
|
detail: formattedErrors,
|
|
722
734
|
},
|
|
723
|
-
}
|
|
735
|
+
};
|
|
736
|
+
|
|
737
|
+
this.invokeOnError(errorResponse, req as FlinkRequest, method!, routeProps.path);
|
|
738
|
+
|
|
739
|
+
return res.status(400).json(errorResponse);
|
|
724
740
|
}
|
|
725
741
|
}
|
|
726
742
|
|
|
@@ -813,26 +829,7 @@ export class FlinkApp<C extends FlinkContext> {
|
|
|
813
829
|
errorResponse = internalServerError(err as any);
|
|
814
830
|
}
|
|
815
831
|
|
|
816
|
-
|
|
817
|
-
if (this.onError) {
|
|
818
|
-
try {
|
|
819
|
-
const result = this.onError(errorResponse, {
|
|
820
|
-
req: req as FlinkRequest,
|
|
821
|
-
method: method!,
|
|
822
|
-
path: routeProps.path,
|
|
823
|
-
reqId: req.reqId,
|
|
824
|
-
});
|
|
825
|
-
|
|
826
|
-
// Handle async callbacks - don't wait for them
|
|
827
|
-
if (result instanceof Promise) {
|
|
828
|
-
result.catch((callbackErr) => {
|
|
829
|
-
log.error(`onError callback rejected with: ${callbackErr}`);
|
|
830
|
-
});
|
|
831
|
-
}
|
|
832
|
-
} catch (callbackErr) {
|
|
833
|
-
log.error(`onError callback threw an exception: ${callbackErr}`);
|
|
834
|
-
}
|
|
835
|
-
}
|
|
832
|
+
this.invokeOnError(errorResponse, req as FlinkRequest, method!, routeProps.path);
|
|
836
833
|
|
|
837
834
|
return res.status(errorResponse.status || 500).json(errorResponse);
|
|
838
835
|
}
|
|
@@ -853,10 +850,15 @@ export class FlinkApp<C extends FlinkContext> {
|
|
|
853
850
|
const detail =
|
|
854
851
|
"Response schema is defined but handler returned no data";
|
|
855
852
|
log.warn(`[${req.reqId}] ${methodAndRoute}: Bad response - ${detail}`);
|
|
856
|
-
|
|
853
|
+
|
|
854
|
+
const errorResponse: FlinkResponse<FlinkError> = {
|
|
857
855
|
status: 500,
|
|
858
856
|
error: { id: v4(), title: "Bad response", detail },
|
|
859
|
-
}
|
|
857
|
+
};
|
|
858
|
+
|
|
859
|
+
this.invokeOnError(errorResponse, req as FlinkRequest, method!, routeProps.path);
|
|
860
|
+
|
|
861
|
+
return res.status(500).json(errorResponse);
|
|
860
862
|
}
|
|
861
863
|
} else {
|
|
862
864
|
const valid = validateRes(JSON.parse(JSON.stringify(handlerRes.data)));
|
|
@@ -865,14 +867,18 @@ export class FlinkApp<C extends FlinkContext> {
|
|
|
865
867
|
const formattedErrors = formatValidationErrors(validateRes.errors, handlerRes.data);
|
|
866
868
|
log.warn(`[${req.reqId}] ${methodAndRoute}: Bad response\n${formattedErrors}`);
|
|
867
869
|
|
|
868
|
-
|
|
870
|
+
const errorResponse: FlinkResponse<FlinkError> = {
|
|
869
871
|
status: 500,
|
|
870
872
|
error: {
|
|
871
873
|
id: v4(),
|
|
872
874
|
title: "Bad response",
|
|
873
875
|
detail: formattedErrors,
|
|
874
876
|
},
|
|
875
|
-
}
|
|
877
|
+
};
|
|
878
|
+
|
|
879
|
+
this.invokeOnError(errorResponse, req as FlinkRequest, method!, routeProps.path);
|
|
880
|
+
|
|
881
|
+
return res.status(500).json(errorResponse);
|
|
876
882
|
}
|
|
877
883
|
}
|
|
878
884
|
}
|
|
@@ -1583,6 +1589,36 @@ export class FlinkApp<C extends FlinkContext> {
|
|
|
1583
1589
|
return await this.auth.authenticateRequest(req as FlinkRequest, permissions, this._ctx);
|
|
1584
1590
|
}
|
|
1585
1591
|
|
|
1592
|
+
/**
|
|
1593
|
+
* Invokes the optional onError callback in a fire-and-forget manner.
|
|
1594
|
+
* Any error thrown or rejected by the callback is caught and logged so
|
|
1595
|
+
* it never affects the error response sent to the client.
|
|
1596
|
+
*/
|
|
1597
|
+
private invokeOnError(errorResponse: FlinkResponse<FlinkError>, req: FlinkRequest, method: HttpMethod, path: string) {
|
|
1598
|
+
if (!this.onError) {
|
|
1599
|
+
return;
|
|
1600
|
+
}
|
|
1601
|
+
|
|
1602
|
+
try {
|
|
1603
|
+
const result = this.onError(errorResponse, {
|
|
1604
|
+
req,
|
|
1605
|
+
method,
|
|
1606
|
+
path,
|
|
1607
|
+
reqId: req.reqId,
|
|
1608
|
+
ctx: this.ctx,
|
|
1609
|
+
});
|
|
1610
|
+
|
|
1611
|
+
// Handle async callbacks - don't wait for them
|
|
1612
|
+
if (result instanceof Promise) {
|
|
1613
|
+
result.catch((callbackErr) => {
|
|
1614
|
+
log.error(`onError callback rejected with: ${callbackErr}`);
|
|
1615
|
+
});
|
|
1616
|
+
}
|
|
1617
|
+
} catch (callbackErr) {
|
|
1618
|
+
log.error(`onError callback threw an exception: ${callbackErr}`);
|
|
1619
|
+
}
|
|
1620
|
+
}
|
|
1621
|
+
|
|
1586
1622
|
public getRegisteredRoutes() {
|
|
1587
1623
|
return Array.from(this.handlerRouteCache.values());
|
|
1588
1624
|
}
|