@constructive-io/knative-job-fn 0.2.7 → 0.3.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/CHANGELOG.md +8 -0
- package/dist/index.d.ts +9 -3
- package/dist/index.js +137 -127
- package/package.json +3 -2
- package/src/index.ts +150 -131
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,14 @@
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
5
|
|
|
6
|
+
# [0.3.0](https://github.com/constructive-io/jobs/compare/@constructive-io/knative-job-fn@0.2.8...@constructive-io/knative-job-fn@0.3.0) (2026-01-18)
|
|
7
|
+
|
|
8
|
+
**Note:** Version bump only for package @constructive-io/knative-job-fn
|
|
9
|
+
|
|
10
|
+
## [0.2.8](https://github.com/constructive-io/jobs/compare/@constructive-io/knative-job-fn@0.2.7...@constructive-io/knative-job-fn@0.2.8) (2026-01-18)
|
|
11
|
+
|
|
12
|
+
**Note:** Version bump only for package @constructive-io/knative-job-fn
|
|
13
|
+
|
|
6
14
|
## [0.2.7](https://github.com/constructive-io/jobs/compare/@constructive-io/knative-job-fn@0.2.6...@constructive-io/knative-job-fn@0.2.7) (2025-12-25)
|
|
7
15
|
|
|
8
16
|
**Note:** Version bump only for package @constructive-io/knative-job-fn
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
|
-
|
|
1
|
+
import type { Server as HttpServer } from 'http';
|
|
2
|
+
declare const createJobApp: () => {
|
|
2
3
|
post: (...args: any[]) => any;
|
|
3
|
-
listen: (port: any, cb?: () => void) =>
|
|
4
|
+
listen: (port: any, hostOrCb?: string | (() => void), cb?: () => void) => HttpServer;
|
|
4
5
|
};
|
|
5
|
-
|
|
6
|
+
declare const defaultApp: {
|
|
7
|
+
post: (...args: any[]) => any;
|
|
8
|
+
listen: (port: any, hostOrCb?: string | (() => void), cb?: () => void) => HttpServer;
|
|
9
|
+
};
|
|
10
|
+
export { createJobApp };
|
|
11
|
+
export default defaultApp;
|
package/dist/index.js
CHANGED
|
@@ -3,11 +3,13 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.createJobApp = void 0;
|
|
6
7
|
const express_1 = __importDefault(require("express"));
|
|
7
8
|
const body_parser_1 = __importDefault(require("body-parser"));
|
|
8
9
|
const node_http_1 = __importDefault(require("node:http"));
|
|
9
10
|
const node_https_1 = __importDefault(require("node:https"));
|
|
10
11
|
const node_url_1 = require("node:url");
|
|
12
|
+
const logger_1 = require("@pgpmjs/logger");
|
|
11
13
|
function getHeaders(req) {
|
|
12
14
|
return {
|
|
13
15
|
'x-worker-id': req.get('X-Worker-Id'),
|
|
@@ -16,48 +18,6 @@ function getHeaders(req) {
|
|
|
16
18
|
'x-callback-url': req.get('X-Callback-Url')
|
|
17
19
|
};
|
|
18
20
|
}
|
|
19
|
-
const app = (0, express_1.default)();
|
|
20
|
-
app.use(body_parser_1.default.json());
|
|
21
|
-
// Basic request logging for all incoming job invocations.
|
|
22
|
-
app.use((req, res, next) => {
|
|
23
|
-
try {
|
|
24
|
-
// Log only the headers we care about plus a shallow body snapshot
|
|
25
|
-
const headers = getHeaders(req);
|
|
26
|
-
let body;
|
|
27
|
-
if (req.body && typeof req.body === 'object') {
|
|
28
|
-
// Only log top-level keys to avoid exposing sensitive body contents.
|
|
29
|
-
body = { keys: Object.keys(req.body) };
|
|
30
|
-
}
|
|
31
|
-
else if (typeof req.body === 'string') {
|
|
32
|
-
// For string bodies, log only the length.
|
|
33
|
-
body = { length: req.body.length };
|
|
34
|
-
}
|
|
35
|
-
else {
|
|
36
|
-
body = undefined;
|
|
37
|
-
}
|
|
38
|
-
// eslint-disable-next-line no-console
|
|
39
|
-
console.log('[knative-job-fn] Incoming job request', {
|
|
40
|
-
method: req.method,
|
|
41
|
-
path: req.originalUrl || req.url,
|
|
42
|
-
headers,
|
|
43
|
-
body
|
|
44
|
-
});
|
|
45
|
-
}
|
|
46
|
-
catch {
|
|
47
|
-
// best-effort logging; never block the request
|
|
48
|
-
}
|
|
49
|
-
next();
|
|
50
|
-
});
|
|
51
|
-
// Echo job headers back on responses for debugging/traceability.
|
|
52
|
-
app.use((req, res, next) => {
|
|
53
|
-
res.set({
|
|
54
|
-
'Content-Type': 'application/json',
|
|
55
|
-
'X-Worker-Id': req.get('X-Worker-Id'),
|
|
56
|
-
'X-Database-Id': req.get('X-Database-Id'),
|
|
57
|
-
'X-Job-Id': req.get('X-Job-Id')
|
|
58
|
-
});
|
|
59
|
-
next();
|
|
60
|
-
});
|
|
61
21
|
// Normalize callback URL so it always points at the /callback endpoint.
|
|
62
22
|
const normalizeCallbackUrl = (rawUrl) => {
|
|
63
23
|
try {
|
|
@@ -123,8 +83,7 @@ const sendJobCallback = async (ctx, status, errorMessage) => {
|
|
|
123
83
|
body.error = errorMessage || 'ERROR';
|
|
124
84
|
}
|
|
125
85
|
try {
|
|
126
|
-
|
|
127
|
-
console.log('[knative-job-fn] Sending job callback', {
|
|
86
|
+
logger.info('Sending job callback', {
|
|
128
87
|
status,
|
|
129
88
|
target: normalizeCallbackUrl(callbackUrl),
|
|
130
89
|
workerId,
|
|
@@ -134,99 +93,150 @@ const sendJobCallback = async (ctx, status, errorMessage) => {
|
|
|
134
93
|
await postJson(target, headers, body);
|
|
135
94
|
}
|
|
136
95
|
catch (err) {
|
|
137
|
-
|
|
138
|
-
console.error('[knative-job-fn] Failed to POST job callback', {
|
|
96
|
+
logger.error('Failed to POST job callback', {
|
|
139
97
|
target,
|
|
140
98
|
status,
|
|
141
99
|
err
|
|
142
100
|
});
|
|
143
101
|
}
|
|
144
102
|
};
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
const
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
103
|
+
const logger = (0, logger_1.createLogger)('knative-job-fn');
|
|
104
|
+
const createJobApp = () => {
|
|
105
|
+
const app = (0, express_1.default)();
|
|
106
|
+
app.use(body_parser_1.default.json());
|
|
107
|
+
// Basic request logging for all incoming job invocations.
|
|
108
|
+
app.use((req, res, next) => {
|
|
109
|
+
try {
|
|
110
|
+
// Log only the headers we care about plus a shallow body snapshot
|
|
111
|
+
const headers = getHeaders(req);
|
|
112
|
+
let body;
|
|
113
|
+
if (req.body && typeof req.body === 'object') {
|
|
114
|
+
// Only log top-level keys to avoid exposing sensitive body contents.
|
|
115
|
+
body = { keys: Object.keys(req.body) };
|
|
116
|
+
}
|
|
117
|
+
else if (typeof req.body === 'string') {
|
|
118
|
+
// For string bodies, log only the length.
|
|
119
|
+
body = { length: req.body.length };
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
body = undefined;
|
|
123
|
+
}
|
|
124
|
+
logger.info('Incoming job request', {
|
|
125
|
+
method: req.method,
|
|
126
|
+
path: req.originalUrl || req.url,
|
|
127
|
+
headers,
|
|
128
|
+
body
|
|
169
129
|
});
|
|
170
|
-
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
// best-effort logging; never block the request
|
|
133
|
+
}
|
|
134
|
+
next();
|
|
135
|
+
});
|
|
136
|
+
// Echo job headers back on responses for debugging/traceability.
|
|
137
|
+
app.use((req, res, next) => {
|
|
138
|
+
res.set({
|
|
139
|
+
'Content-Type': 'application/json',
|
|
140
|
+
'X-Worker-Id': req.get('X-Worker-Id'),
|
|
141
|
+
'X-Database-Id': req.get('X-Database-Id'),
|
|
142
|
+
'X-Job-Id': req.get('X-Job-Id')
|
|
171
143
|
});
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
//
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
144
|
+
next();
|
|
145
|
+
});
|
|
146
|
+
// Attach per-request context and a finish hook to send success callbacks.
|
|
147
|
+
app.use((req, res, next) => {
|
|
148
|
+
const ctx = {
|
|
149
|
+
callbackUrl: req.get('X-Callback-Url'),
|
|
150
|
+
workerId: req.get('X-Worker-Id'),
|
|
151
|
+
jobId: req.get('X-Job-Id'),
|
|
152
|
+
databaseId: req.get('X-Database-Id')
|
|
153
|
+
};
|
|
154
|
+
// Store on res.locals so the error middleware can also mark callbacks as sent.
|
|
155
|
+
res.locals = res.locals || {};
|
|
156
|
+
res.locals.jobContext = ctx;
|
|
157
|
+
res.locals.jobCallbackSent = false;
|
|
158
|
+
if (ctx.callbackUrl && ctx.workerId && ctx.jobId) {
|
|
159
|
+
res.on('finish', () => {
|
|
160
|
+
// If an error handler already sent a callback, skip.
|
|
161
|
+
if (res.locals.jobCallbackSent)
|
|
162
|
+
return;
|
|
163
|
+
res.locals.jobCallbackSent = true;
|
|
164
|
+
logger.info('Function completed', {
|
|
165
|
+
workerId: ctx.workerId,
|
|
166
|
+
jobId: ctx.jobId,
|
|
167
|
+
databaseId: ctx.databaseId,
|
|
168
|
+
statusCode: res.statusCode
|
|
169
|
+
});
|
|
170
|
+
void sendJobCallback(ctx, 'success');
|
|
188
171
|
});
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
172
|
+
}
|
|
173
|
+
next();
|
|
174
|
+
});
|
|
175
|
+
return {
|
|
176
|
+
post: function (...args) {
|
|
177
|
+
return app.post.apply(app, args);
|
|
178
|
+
},
|
|
179
|
+
listen: (port, hostOrCb, cb = () => { }) => {
|
|
180
|
+
// NOTE Remember that Express middleware executes in order.
|
|
181
|
+
// You should define error handlers last, after all other middleware.
|
|
182
|
+
// Otherwise, your error handler won't get called
|
|
183
|
+
// eslint-disable-next-line no-unused-vars
|
|
184
|
+
app.use(async (error, req, res, next) => {
|
|
185
|
+
res.set({
|
|
186
|
+
'Content-Type': 'application/json',
|
|
187
|
+
'X-Job-Error': true
|
|
188
|
+
});
|
|
189
|
+
// Mark job as having errored via callback, if available.
|
|
190
|
+
try {
|
|
191
|
+
const ctx = res.locals?.jobContext;
|
|
192
|
+
if (ctx && !res.locals.jobCallbackSent) {
|
|
193
|
+
res.locals.jobCallbackSent = true;
|
|
194
|
+
await sendJobCallback(ctx, 'error', error?.message);
|
|
195
|
+
}
|
|
195
196
|
}
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
name: error?.name,
|
|
208
|
-
stack: error?.stack
|
|
209
|
-
};
|
|
210
|
-
if (error?.response) {
|
|
211
|
-
errorDetails.response = {
|
|
212
|
-
status: error.response.status,
|
|
213
|
-
statusText: error.response.statusText,
|
|
214
|
-
errors: error.response.errors,
|
|
215
|
-
data: error.response.data
|
|
197
|
+
catch (err) {
|
|
198
|
+
logger.error('Failed to send error callback', err);
|
|
199
|
+
}
|
|
200
|
+
// Log the full error context for debugging.
|
|
201
|
+
try {
|
|
202
|
+
const headers = getHeaders(req);
|
|
203
|
+
// Some error types (e.g. GraphQL ClientError) expose response info.
|
|
204
|
+
const errorDetails = {
|
|
205
|
+
message: error?.message,
|
|
206
|
+
name: error?.name,
|
|
207
|
+
stack: error?.stack
|
|
216
208
|
};
|
|
209
|
+
if (error?.response) {
|
|
210
|
+
errorDetails.response = {
|
|
211
|
+
status: error.response.status,
|
|
212
|
+
statusText: error.response.statusText,
|
|
213
|
+
errors: error.response.errors,
|
|
214
|
+
data: error.response.data
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
logger.error('Function error', {
|
|
218
|
+
headers,
|
|
219
|
+
path: req.originalUrl || req.url,
|
|
220
|
+
error: errorDetails
|
|
221
|
+
});
|
|
217
222
|
}
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
223
|
+
catch {
|
|
224
|
+
// never throw from the error logger
|
|
225
|
+
}
|
|
226
|
+
res.status(200).json({ message: error.message });
|
|
227
|
+
});
|
|
228
|
+
const host = typeof hostOrCb === 'string' ? hostOrCb : undefined;
|
|
229
|
+
const callback = typeof hostOrCb === 'function' ? hostOrCb : cb;
|
|
230
|
+
const onListen = () => {
|
|
231
|
+
callback();
|
|
232
|
+
};
|
|
233
|
+
const server = host
|
|
234
|
+
? app.listen(port, host, onListen)
|
|
235
|
+
: app.listen(port, onListen);
|
|
236
|
+
return server;
|
|
237
|
+
}
|
|
238
|
+
};
|
|
232
239
|
};
|
|
240
|
+
exports.createJobApp = createJobApp;
|
|
241
|
+
const defaultApp = createJobApp();
|
|
242
|
+
exports.default = defaultApp;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@constructive-io/knative-job-fn",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "knative job fn",
|
|
5
5
|
"author": "Constructive <developers@constructive.io>",
|
|
6
6
|
"homepage": "https://github.com/constructive-io/jobs/tree/master/packages/knative-job-fn#readme",
|
|
@@ -28,8 +28,9 @@
|
|
|
28
28
|
"url": "https://github.com/constructive-io/jobs/issues"
|
|
29
29
|
},
|
|
30
30
|
"dependencies": {
|
|
31
|
+
"@pgpmjs/logger": "^1.4.0",
|
|
31
32
|
"body-parser": "1.19.0",
|
|
32
33
|
"express": "5.2.1"
|
|
33
34
|
},
|
|
34
|
-
"gitHead": "
|
|
35
|
+
"gitHead": "481b3a50b4eec2da6b376c4cd1868065e1e28edb"
|
|
35
36
|
}
|
package/src/index.ts
CHANGED
|
@@ -3,6 +3,8 @@ import bodyParser from 'body-parser';
|
|
|
3
3
|
import http from 'node:http';
|
|
4
4
|
import https from 'node:https';
|
|
5
5
|
import { URL } from 'node:url';
|
|
6
|
+
import type { Server as HttpServer } from 'http';
|
|
7
|
+
import { createLogger } from '@pgpmjs/logger';
|
|
6
8
|
|
|
7
9
|
type JobCallbackStatus = 'success' | 'error';
|
|
8
10
|
|
|
@@ -22,51 +24,6 @@ function getHeaders(req: any) {
|
|
|
22
24
|
};
|
|
23
25
|
}
|
|
24
26
|
|
|
25
|
-
const app: any = express();
|
|
26
|
-
|
|
27
|
-
app.use(bodyParser.json());
|
|
28
|
-
|
|
29
|
-
// Basic request logging for all incoming job invocations.
|
|
30
|
-
app.use((req: any, res: any, next: any) => {
|
|
31
|
-
try {
|
|
32
|
-
// Log only the headers we care about plus a shallow body snapshot
|
|
33
|
-
const headers = getHeaders(req);
|
|
34
|
-
|
|
35
|
-
let body: any;
|
|
36
|
-
if (req.body && typeof req.body === 'object') {
|
|
37
|
-
// Only log top-level keys to avoid exposing sensitive body contents.
|
|
38
|
-
body = { keys: Object.keys(req.body) };
|
|
39
|
-
} else if (typeof req.body === 'string') {
|
|
40
|
-
// For string bodies, log only the length.
|
|
41
|
-
body = { length: req.body.length };
|
|
42
|
-
} else {
|
|
43
|
-
body = undefined;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
// eslint-disable-next-line no-console
|
|
47
|
-
console.log('[knative-job-fn] Incoming job request', {
|
|
48
|
-
method: req.method,
|
|
49
|
-
path: req.originalUrl || req.url,
|
|
50
|
-
headers,
|
|
51
|
-
body
|
|
52
|
-
});
|
|
53
|
-
} catch {
|
|
54
|
-
// best-effort logging; never block the request
|
|
55
|
-
}
|
|
56
|
-
next();
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
// Echo job headers back on responses for debugging/traceability.
|
|
60
|
-
app.use((req: any, res: any, next: any) => {
|
|
61
|
-
res.set({
|
|
62
|
-
'Content-Type': 'application/json',
|
|
63
|
-
'X-Worker-Id': req.get('X-Worker-Id'),
|
|
64
|
-
'X-Database-Id': req.get('X-Database-Id'),
|
|
65
|
-
'X-Job-Id': req.get('X-Job-Id')
|
|
66
|
-
});
|
|
67
|
-
next();
|
|
68
|
-
});
|
|
69
|
-
|
|
70
27
|
// Normalize callback URL so it always points at the /callback endpoint.
|
|
71
28
|
const normalizeCallbackUrl = (rawUrl: string): string => {
|
|
72
29
|
try {
|
|
@@ -152,8 +109,7 @@ const sendJobCallback = async (
|
|
|
152
109
|
}
|
|
153
110
|
|
|
154
111
|
try {
|
|
155
|
-
|
|
156
|
-
console.log('[knative-job-fn] Sending job callback', {
|
|
112
|
+
logger.info('Sending job callback', {
|
|
157
113
|
status,
|
|
158
114
|
target: normalizeCallbackUrl(callbackUrl),
|
|
159
115
|
workerId,
|
|
@@ -162,8 +118,7 @@ const sendJobCallback = async (
|
|
|
162
118
|
});
|
|
163
119
|
await postJson(target, headers, body);
|
|
164
120
|
} catch (err) {
|
|
165
|
-
|
|
166
|
-
console.error('[knative-job-fn] Failed to POST job callback', {
|
|
121
|
+
logger.error('Failed to POST job callback', {
|
|
167
122
|
target,
|
|
168
123
|
status,
|
|
169
124
|
err
|
|
@@ -171,98 +126,162 @@ const sendJobCallback = async (
|
|
|
171
126
|
}
|
|
172
127
|
};
|
|
173
128
|
|
|
174
|
-
|
|
175
|
-
app.use((req: any, res: any, next: any) => {
|
|
176
|
-
const ctx: JobContext = {
|
|
177
|
-
callbackUrl: req.get('X-Callback-Url'),
|
|
178
|
-
workerId: req.get('X-Worker-Id'),
|
|
179
|
-
jobId: req.get('X-Job-Id'),
|
|
180
|
-
databaseId: req.get('X-Database-Id')
|
|
181
|
-
};
|
|
129
|
+
const logger = createLogger('knative-job-fn');
|
|
182
130
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
131
|
+
const createJobApp = () => {
|
|
132
|
+
const app: any = express();
|
|
133
|
+
|
|
134
|
+
app.use(bodyParser.json());
|
|
135
|
+
|
|
136
|
+
// Basic request logging for all incoming job invocations.
|
|
137
|
+
app.use((req: any, res: any, next: any) => {
|
|
138
|
+
try {
|
|
139
|
+
// Log only the headers we care about plus a shallow body snapshot
|
|
140
|
+
const headers = getHeaders(req);
|
|
141
|
+
|
|
142
|
+
let body: any;
|
|
143
|
+
if (req.body && typeof req.body === 'object') {
|
|
144
|
+
// Only log top-level keys to avoid exposing sensitive body contents.
|
|
145
|
+
body = { keys: Object.keys(req.body) };
|
|
146
|
+
} else if (typeof req.body === 'string') {
|
|
147
|
+
// For string bodies, log only the length.
|
|
148
|
+
body = { length: req.body.length };
|
|
149
|
+
} else {
|
|
150
|
+
body = undefined;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
logger.info('Incoming job request', {
|
|
154
|
+
method: req.method,
|
|
155
|
+
path: req.originalUrl || req.url,
|
|
156
|
+
headers,
|
|
157
|
+
body
|
|
199
158
|
});
|
|
200
|
-
|
|
159
|
+
} catch {
|
|
160
|
+
// best-effort logging; never block the request
|
|
161
|
+
}
|
|
162
|
+
next();
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
// Echo job headers back on responses for debugging/traceability.
|
|
166
|
+
app.use((req: any, res: any, next: any) => {
|
|
167
|
+
res.set({
|
|
168
|
+
'Content-Type': 'application/json',
|
|
169
|
+
'X-Worker-Id': req.get('X-Worker-Id'),
|
|
170
|
+
'X-Database-Id': req.get('X-Database-Id'),
|
|
171
|
+
'X-Job-Id': req.get('X-Job-Id')
|
|
201
172
|
});
|
|
202
|
-
|
|
173
|
+
next();
|
|
174
|
+
});
|
|
203
175
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
//
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
176
|
+
// Attach per-request context and a finish hook to send success callbacks.
|
|
177
|
+
app.use((req: any, res: any, next: any) => {
|
|
178
|
+
const ctx: JobContext = {
|
|
179
|
+
callbackUrl: req.get('X-Callback-Url'),
|
|
180
|
+
workerId: req.get('X-Worker-Id'),
|
|
181
|
+
jobId: req.get('X-Job-Id'),
|
|
182
|
+
databaseId: req.get('X-Database-Id')
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
// Store on res.locals so the error middleware can also mark callbacks as sent.
|
|
186
|
+
res.locals = res.locals || {};
|
|
187
|
+
res.locals.jobContext = ctx;
|
|
188
|
+
res.locals.jobCallbackSent = false;
|
|
189
|
+
|
|
190
|
+
if (ctx.callbackUrl && ctx.workerId && ctx.jobId) {
|
|
191
|
+
res.on('finish', () => {
|
|
192
|
+
// If an error handler already sent a callback, skip.
|
|
193
|
+
if (res.locals.jobCallbackSent) return;
|
|
194
|
+
res.locals.jobCallbackSent = true;
|
|
195
|
+
logger.info('Function completed', {
|
|
196
|
+
workerId: ctx.workerId,
|
|
197
|
+
jobId: ctx.jobId,
|
|
198
|
+
databaseId: ctx.databaseId,
|
|
199
|
+
statusCode: res.statusCode
|
|
200
|
+
});
|
|
201
|
+
void sendJobCallback(ctx, 'success');
|
|
220
202
|
});
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
next();
|
|
206
|
+
});
|
|
221
207
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
208
|
+
return {
|
|
209
|
+
post: function (...args: any[]) {
|
|
210
|
+
return app.post.apply(app, args as any);
|
|
211
|
+
},
|
|
212
|
+
listen: (
|
|
213
|
+
port: any,
|
|
214
|
+
hostOrCb?: string | (() => void),
|
|
215
|
+
cb: () => void = () => {}
|
|
216
|
+
): HttpServer => {
|
|
217
|
+
// NOTE Remember that Express middleware executes in order.
|
|
218
|
+
// You should define error handlers last, after all other middleware.
|
|
219
|
+
// Otherwise, your error handler won't get called
|
|
220
|
+
// eslint-disable-next-line no-unused-vars
|
|
221
|
+
app.use(async (error: any, req: any, res: any, next: any) => {
|
|
222
|
+
res.set({
|
|
223
|
+
'Content-Type': 'application/json',
|
|
224
|
+
'X-Job-Error': true
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
// Mark job as having errored via callback, if available.
|
|
228
|
+
try {
|
|
229
|
+
const ctx: JobContext | undefined = res.locals?.jobContext;
|
|
230
|
+
if (ctx && !res.locals.jobCallbackSent) {
|
|
231
|
+
res.locals.jobCallbackSent = true;
|
|
232
|
+
await sendJobCallback(ctx, 'error', error?.message);
|
|
233
|
+
}
|
|
234
|
+
} catch (err) {
|
|
235
|
+
logger.error('Failed to send error callback', err);
|
|
228
236
|
}
|
|
229
|
-
} catch (err) {
|
|
230
|
-
// eslint-disable-next-line no-console
|
|
231
|
-
console.error('[knative-job-fn] Failed to send error callback', err);
|
|
232
|
-
}
|
|
233
237
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
};
|
|
244
|
-
|
|
245
|
-
if (error?.response) {
|
|
246
|
-
errorDetails.response = {
|
|
247
|
-
status: error.response.status,
|
|
248
|
-
statusText: error.response.statusText,
|
|
249
|
-
errors: error.response.errors,
|
|
250
|
-
data: error.response.data
|
|
238
|
+
// Log the full error context for debugging.
|
|
239
|
+
try {
|
|
240
|
+
const headers = getHeaders(req);
|
|
241
|
+
|
|
242
|
+
// Some error types (e.g. GraphQL ClientError) expose response info.
|
|
243
|
+
const errorDetails: any = {
|
|
244
|
+
message: error?.message,
|
|
245
|
+
name: error?.name,
|
|
246
|
+
stack: error?.stack
|
|
251
247
|
};
|
|
248
|
+
|
|
249
|
+
if (error?.response) {
|
|
250
|
+
errorDetails.response = {
|
|
251
|
+
status: error.response.status,
|
|
252
|
+
statusText: error.response.statusText,
|
|
253
|
+
errors: error.response.errors,
|
|
254
|
+
data: error.response.data
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
logger.error('Function error', {
|
|
259
|
+
headers,
|
|
260
|
+
path: req.originalUrl || req.url,
|
|
261
|
+
error: errorDetails
|
|
262
|
+
});
|
|
263
|
+
} catch {
|
|
264
|
+
// never throw from the error logger
|
|
252
265
|
}
|
|
253
266
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
headers,
|
|
257
|
-
path: req.originalUrl || req.url,
|
|
258
|
-
error: errorDetails
|
|
259
|
-
});
|
|
260
|
-
} catch {
|
|
261
|
-
// never throw from the error logger
|
|
262
|
-
}
|
|
267
|
+
res.status(200).json({ message: error.message });
|
|
268
|
+
});
|
|
263
269
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
270
|
+
const host = typeof hostOrCb === 'string' ? hostOrCb : undefined;
|
|
271
|
+
const callback = typeof hostOrCb === 'function' ? hostOrCb : cb;
|
|
272
|
+
const onListen = () => {
|
|
273
|
+
callback();
|
|
274
|
+
};
|
|
275
|
+
const server = host
|
|
276
|
+
? app.listen(port, host, onListen)
|
|
277
|
+
: app.listen(port, onListen);
|
|
278
|
+
|
|
279
|
+
return server;
|
|
280
|
+
}
|
|
281
|
+
};
|
|
268
282
|
};
|
|
283
|
+
|
|
284
|
+
const defaultApp = createJobApp();
|
|
285
|
+
|
|
286
|
+
export { createJobApp };
|
|
287
|
+
export default defaultApp;
|