@jaypie/express 1.2.4-rc9 → 1.2.5
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/README.md +1 -0
- package/dist/cjs/adapter/LambdaRequest.d.ts +1 -0
- package/dist/cjs/adapter/LambdaResponseBuffered.d.ts +8 -0
- package/dist/cjs/adapter/LambdaResponseStreaming.d.ts +8 -2
- package/dist/cjs/adapter/__tests__/debug-harness.d.ts +1 -0
- package/dist/cjs/expressStreamHandler.d.ts +2 -0
- package/dist/cjs/getCurrentInvokeUuid.adapter.d.ts +2 -2
- package/dist/cjs/index.cjs +372 -208
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/cjs/index.d.ts +0 -2
- package/dist/esm/adapter/LambdaRequest.d.ts +1 -0
- package/dist/esm/adapter/LambdaResponseBuffered.d.ts +8 -0
- package/dist/esm/adapter/LambdaResponseStreaming.d.ts +8 -2
- package/dist/esm/adapter/__tests__/debug-harness.d.ts +1 -0
- package/dist/esm/expressStreamHandler.d.ts +2 -0
- package/dist/esm/getCurrentInvokeUuid.adapter.d.ts +2 -2
- package/dist/esm/index.d.ts +0 -2
- package/dist/esm/index.js +372 -207
- package/dist/esm/index.js.map +1 -1
- package/package.json +17 -21
- package/dist/cjs/createServer.d.ts +0 -60
- package/dist/cjs/getCurrentInvokeUuid.webadapter.d.ts +0 -12
- package/dist/esm/createServer.d.ts +0 -60
- package/dist/esm/getCurrentInvokeUuid.webadapter.d.ts +0 -12
package/dist/esm/index.js
CHANGED
|
@@ -3,8 +3,8 @@ import { ServerResponse } from 'node:http';
|
|
|
3
3
|
import { CorsError, BadRequestError, UnhandledError, GatewayTimeoutError, UnavailableError, BadGatewayError, InternalError, TeapotError, GoneError, MethodNotAllowedError, NotFoundError, ForbiddenError, UnauthorizedError, NotImplementedError } from '@jaypie/errors';
|
|
4
4
|
import { force, envBoolean, JAYPIE, HTTP, getHeaderFrom, jaypieHandler } from '@jaypie/kit';
|
|
5
5
|
import expressCors from 'cors';
|
|
6
|
+
import { loadEnvSecrets, getContentTypeForFormat, formatStreamError } from '@jaypie/aws';
|
|
6
7
|
import { log } from '@jaypie/logger';
|
|
7
|
-
import { loadEnvSecrets } from '@jaypie/aws';
|
|
8
8
|
import { hasDatadogEnv, submitMetric, DATADOG } from '@jaypie/datadog';
|
|
9
9
|
|
|
10
10
|
//
|
|
@@ -32,6 +32,17 @@ class LambdaRequest extends Readable {
|
|
|
32
32
|
this.path = options.url.split("?")[0];
|
|
33
33
|
this.headers = this.normalizeHeaders(options.headers);
|
|
34
34
|
this.bodyBuffer = options.body ?? null;
|
|
35
|
+
// Use pre-parsed query if provided, otherwise parse from URL
|
|
36
|
+
if (options.query) {
|
|
37
|
+
this.query = options.query;
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
const queryIndex = options.url.indexOf("?");
|
|
41
|
+
if (queryIndex !== -1) {
|
|
42
|
+
const queryString = options.url.slice(queryIndex + 1);
|
|
43
|
+
this.query = parseQueryString(queryString);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
35
46
|
// Store Lambda context
|
|
36
47
|
this._lambdaContext = options.lambdaContext;
|
|
37
48
|
this._lambdaEvent = options.lambdaEvent;
|
|
@@ -42,6 +53,18 @@ class LambdaRequest extends Readable {
|
|
|
42
53
|
remoteAddress: options.remoteAddress,
|
|
43
54
|
};
|
|
44
55
|
this.connection = this.socket;
|
|
56
|
+
// Schedule body push for next tick to ensure stream is ready
|
|
57
|
+
// This is needed for body parsers that consume the stream
|
|
58
|
+
if (this.bodyBuffer && this.bodyBuffer.length > 0) {
|
|
59
|
+
process.nextTick(() => {
|
|
60
|
+
if (!this.bodyPushed) {
|
|
61
|
+
this.push(this.bodyBuffer);
|
|
62
|
+
this.push(null);
|
|
63
|
+
this.bodyPushed = true;
|
|
64
|
+
this.complete = true;
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
}
|
|
45
68
|
}
|
|
46
69
|
//
|
|
47
70
|
// Readable stream implementation
|
|
@@ -80,6 +103,66 @@ class LambdaRequest extends Readable {
|
|
|
80
103
|
}
|
|
81
104
|
//
|
|
82
105
|
//
|
|
106
|
+
// Helper Functions
|
|
107
|
+
//
|
|
108
|
+
/**
|
|
109
|
+
* Normalize bracket notation in query parameter key.
|
|
110
|
+
* Removes trailing `[]` from keys (e.g., `filterByStatus[]` → `filterByStatus`).
|
|
111
|
+
*/
|
|
112
|
+
function normalizeQueryKey(key) {
|
|
113
|
+
return key.endsWith("[]") ? key.slice(0, -2) : key;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Parse a query string into a record with proper array handling.
|
|
117
|
+
* Handles bracket notation (e.g., `param[]`) and multi-value parameters.
|
|
118
|
+
*/
|
|
119
|
+
function parseQueryString(queryString) {
|
|
120
|
+
const result = {};
|
|
121
|
+
const params = new URLSearchParams(queryString);
|
|
122
|
+
for (const [rawKey, value] of params) {
|
|
123
|
+
const key = normalizeQueryKey(rawKey);
|
|
124
|
+
const existing = result[key];
|
|
125
|
+
if (existing === undefined) {
|
|
126
|
+
// First occurrence - check if it's bracket notation to determine if it should be an array
|
|
127
|
+
result[key] = rawKey.endsWith("[]") ? [value] : value;
|
|
128
|
+
}
|
|
129
|
+
else if (Array.isArray(existing)) {
|
|
130
|
+
existing.push(value);
|
|
131
|
+
}
|
|
132
|
+
else {
|
|
133
|
+
// Convert to array when we encounter a second value
|
|
134
|
+
result[key] = [existing, value];
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return result;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Build query object from API Gateway v1 multiValueQueryStringParameters.
|
|
141
|
+
* Normalizes bracket notation and preserves array values.
|
|
142
|
+
*/
|
|
143
|
+
function buildQueryFromMultiValue(multiValueParams) {
|
|
144
|
+
const result = {};
|
|
145
|
+
if (!multiValueParams)
|
|
146
|
+
return result;
|
|
147
|
+
for (const [rawKey, values] of Object.entries(multiValueParams)) {
|
|
148
|
+
const key = normalizeQueryKey(rawKey);
|
|
149
|
+
const existingValues = result[key];
|
|
150
|
+
if (existingValues === undefined) {
|
|
151
|
+
// First occurrence - use array if multiple values or bracket notation
|
|
152
|
+
result[key] = values.length === 1 && !rawKey.endsWith("[]") ? values[0] : values;
|
|
153
|
+
}
|
|
154
|
+
else if (Array.isArray(existingValues)) {
|
|
155
|
+
existingValues.push(...values);
|
|
156
|
+
}
|
|
157
|
+
else {
|
|
158
|
+
// Convert to array and merge
|
|
159
|
+
result[key] = [existingValues, ...values];
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return result;
|
|
163
|
+
}
|
|
164
|
+
//
|
|
165
|
+
//
|
|
83
166
|
// Type Guards
|
|
84
167
|
//
|
|
85
168
|
/**
|
|
@@ -105,6 +188,7 @@ function createLambdaRequest(event, context) {
|
|
|
105
188
|
let url;
|
|
106
189
|
let method;
|
|
107
190
|
let protocol;
|
|
191
|
+
let query;
|
|
108
192
|
let remoteAddress;
|
|
109
193
|
const headers = { ...event.headers };
|
|
110
194
|
if (isFunctionUrlEvent(event)) {
|
|
@@ -115,6 +199,10 @@ function createLambdaRequest(event, context) {
|
|
|
115
199
|
method = event.requestContext.http.method;
|
|
116
200
|
protocol = event.requestContext.http.protocol.split("/")[0].toLowerCase();
|
|
117
201
|
remoteAddress = event.requestContext.http.sourceIp;
|
|
202
|
+
// Parse query string with proper multi-value and bracket notation support
|
|
203
|
+
if (event.rawQueryString) {
|
|
204
|
+
query = parseQueryString(event.rawQueryString);
|
|
205
|
+
}
|
|
118
206
|
// Normalize cookies into Cookie header if not already present
|
|
119
207
|
if (event.cookies && event.cookies.length > 0 && !headers.cookie) {
|
|
120
208
|
headers.cookie = event.cookies.join("; ");
|
|
@@ -122,7 +210,13 @@ function createLambdaRequest(event, context) {
|
|
|
122
210
|
}
|
|
123
211
|
else if (isApiGatewayV1Event(event)) {
|
|
124
212
|
// API Gateway REST API v1 format
|
|
213
|
+
// Use multiValueQueryStringParameters for proper array support
|
|
214
|
+
const multiValueParams = event.multiValueQueryStringParameters;
|
|
125
215
|
const queryParams = event.queryStringParameters;
|
|
216
|
+
if (multiValueParams && Object.keys(multiValueParams).length > 0) {
|
|
217
|
+
query = buildQueryFromMultiValue(multiValueParams);
|
|
218
|
+
}
|
|
219
|
+
// Build URL with query string
|
|
126
220
|
if (queryParams && Object.keys(queryParams).length > 0) {
|
|
127
221
|
const queryString = new URLSearchParams(queryParams).toString();
|
|
128
222
|
url = `${event.path}?${queryString}`;
|
|
@@ -143,6 +237,11 @@ function createLambdaRequest(event, context) {
|
|
|
143
237
|
body = event.isBase64Encoded
|
|
144
238
|
? Buffer.from(event.body, "base64")
|
|
145
239
|
: Buffer.from(event.body, "utf8");
|
|
240
|
+
// Add content-length header if not present (required for body parsers)
|
|
241
|
+
const hasContentLength = Object.keys(headers).some((k) => k.toLowerCase() === "content-length");
|
|
242
|
+
if (!hasContentLength) {
|
|
243
|
+
headers["content-length"] = String(body.length);
|
|
244
|
+
}
|
|
146
245
|
}
|
|
147
246
|
return new LambdaRequest({
|
|
148
247
|
body,
|
|
@@ -151,6 +250,7 @@ function createLambdaRequest(event, context) {
|
|
|
151
250
|
lambdaEvent: event,
|
|
152
251
|
method,
|
|
153
252
|
protocol,
|
|
253
|
+
query,
|
|
154
254
|
remoteAddress,
|
|
155
255
|
url,
|
|
156
256
|
});
|
|
@@ -160,6 +260,9 @@ function createLambdaRequest(event, context) {
|
|
|
160
260
|
//
|
|
161
261
|
// Constants
|
|
162
262
|
//
|
|
263
|
+
// Symbol to identify Lambda mock responses. Uses Symbol.for() to ensure
|
|
264
|
+
// the same symbol is used across bundles/realms. Survives prototype manipulation.
|
|
265
|
+
const JAYPIE_LAMBDA_MOCK = Symbol.for("@jaypie/express/LambdaMock");
|
|
163
266
|
// Get Node's internal kOutHeaders symbol from ServerResponse prototype.
|
|
164
267
|
// This is needed for compatibility with Datadog dd-trace instrumentation,
|
|
165
268
|
// which patches HTTP methods and expects this internal state to exist.
|
|
@@ -193,21 +296,87 @@ class LambdaResponseBuffered extends Writable {
|
|
|
193
296
|
// Internal state exposed for direct manipulation by safe response methods
|
|
194
297
|
// that need to bypass dd-trace interception of stream methods
|
|
195
298
|
this._chunks = [];
|
|
299
|
+
this._ended = false; // Track ended state since writableEnded is lost after prototype change
|
|
196
300
|
this._headers = new Map();
|
|
197
301
|
this._headersSent = false;
|
|
198
302
|
this._resolve = null;
|
|
303
|
+
// Mark as Lambda mock response for identification in expressHandler
|
|
304
|
+
this[JAYPIE_LAMBDA_MOCK] = true;
|
|
199
305
|
// Initialize Node's internal kOutHeaders for dd-trace compatibility.
|
|
200
306
|
// dd-trace patches HTTP methods and expects this internal state.
|
|
201
307
|
if (kOutHeaders$1) {
|
|
202
308
|
this[kOutHeaders$1] = Object.create(null);
|
|
203
309
|
}
|
|
310
|
+
// CRITICAL: Define key methods as instance properties to survive Express's
|
|
311
|
+
// setPrototypeOf(res, app.response) in middleware/init.js which would
|
|
312
|
+
// otherwise replace our prototype with ServerResponse.prototype.
|
|
313
|
+
// Instance properties take precedence over prototype properties.
|
|
314
|
+
this.getHeader = this.getHeader.bind(this);
|
|
315
|
+
this.setHeader = this.setHeader.bind(this);
|
|
316
|
+
this.removeHeader = this.removeHeader.bind(this);
|
|
317
|
+
this.hasHeader = this.hasHeader.bind(this);
|
|
318
|
+
this.getHeaders = this.getHeaders.bind(this);
|
|
319
|
+
this.getHeaderNames = this.getHeaderNames.bind(this);
|
|
320
|
+
this.writeHead = this.writeHead.bind(this);
|
|
321
|
+
this.get = this.get.bind(this);
|
|
322
|
+
this.set = this.set.bind(this);
|
|
323
|
+
this.status = this.status.bind(this);
|
|
324
|
+
this.json = this.json.bind(this);
|
|
325
|
+
this.send = this.send.bind(this);
|
|
326
|
+
this.vary = this.vary.bind(this);
|
|
327
|
+
this.end = this.end.bind(this);
|
|
328
|
+
this.write = this.write.bind(this);
|
|
329
|
+
// Also bind internal Writable methods that are called via prototype chain
|
|
330
|
+
this._write = this._write.bind(this);
|
|
331
|
+
this._final = this._final.bind(this);
|
|
332
|
+
// Bind result-building methods
|
|
333
|
+
this.getResult = this.getResult.bind(this);
|
|
334
|
+
this.buildResult = this.buildResult.bind(this);
|
|
335
|
+
this.isBinaryContentType = this.isBinaryContentType.bind(this);
|
|
336
|
+
}
|
|
337
|
+
//
|
|
338
|
+
// Internal bypass methods - completely avoid prototype chain lookup
|
|
339
|
+
// These directly access _headers Map, safe from dd-trace interception
|
|
340
|
+
//
|
|
341
|
+
_internalGetHeader(name) {
|
|
342
|
+
const value = this._headers.get(name.toLowerCase());
|
|
343
|
+
return value ? String(value) : undefined;
|
|
344
|
+
}
|
|
345
|
+
_internalSetHeader(name, value) {
|
|
346
|
+
if (!this._headersSent) {
|
|
347
|
+
const lowerName = name.toLowerCase();
|
|
348
|
+
this._headers.set(lowerName, value);
|
|
349
|
+
// Also sync kOutHeaders for any code that expects it
|
|
350
|
+
if (kOutHeaders$1) {
|
|
351
|
+
const outHeaders = this[kOutHeaders$1];
|
|
352
|
+
if (outHeaders) {
|
|
353
|
+
outHeaders[lowerName] = [name, value];
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
_internalHasHeader(name) {
|
|
359
|
+
return this._headers.has(name.toLowerCase());
|
|
360
|
+
}
|
|
361
|
+
_internalRemoveHeader(name) {
|
|
362
|
+
if (!this._headersSent) {
|
|
363
|
+
const lowerName = name.toLowerCase();
|
|
364
|
+
this._headers.delete(lowerName);
|
|
365
|
+
if (kOutHeaders$1) {
|
|
366
|
+
const outHeaders = this[kOutHeaders$1];
|
|
367
|
+
if (outHeaders) {
|
|
368
|
+
delete outHeaders[lowerName];
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
204
372
|
}
|
|
205
373
|
//
|
|
206
374
|
// Promise-based API for getting final result
|
|
207
375
|
//
|
|
208
376
|
getResult() {
|
|
209
377
|
return new Promise((resolve) => {
|
|
210
|
-
|
|
378
|
+
// Use _ended instead of writableEnded since Express's setPrototypeOf breaks the getter
|
|
379
|
+
if (this._ended) {
|
|
211
380
|
resolve(this.buildResult());
|
|
212
381
|
}
|
|
213
382
|
else {
|
|
@@ -265,36 +434,40 @@ class LambdaResponseBuffered extends Writable {
|
|
|
265
434
|
/**
|
|
266
435
|
* Proxy for direct header access (e.g., res.headers['content-type']).
|
|
267
436
|
* Required for compatibility with middleware like helmet that access headers directly.
|
|
437
|
+
* Uses direct _headers access to bypass dd-trace interception.
|
|
268
438
|
*/
|
|
269
439
|
get headers() {
|
|
270
440
|
return new Proxy({}, {
|
|
271
441
|
deleteProperty: (_target, prop) => {
|
|
272
|
-
this.
|
|
442
|
+
this._headers.delete(String(prop).toLowerCase());
|
|
273
443
|
return true;
|
|
274
444
|
},
|
|
275
445
|
get: (_target, prop) => {
|
|
276
446
|
if (typeof prop === "symbol")
|
|
277
447
|
return undefined;
|
|
278
|
-
return this.
|
|
448
|
+
return this._headers.get(String(prop).toLowerCase());
|
|
279
449
|
},
|
|
280
450
|
getOwnPropertyDescriptor: (_target, prop) => {
|
|
281
|
-
|
|
451
|
+
const lowerProp = String(prop).toLowerCase();
|
|
452
|
+
if (this._headers.has(lowerProp)) {
|
|
282
453
|
return {
|
|
283
454
|
configurable: true,
|
|
284
455
|
enumerable: true,
|
|
285
|
-
value: this.
|
|
456
|
+
value: this._headers.get(lowerProp),
|
|
286
457
|
};
|
|
287
458
|
}
|
|
288
459
|
return undefined;
|
|
289
460
|
},
|
|
290
461
|
has: (_target, prop) => {
|
|
291
|
-
return this.
|
|
462
|
+
return this._headers.has(String(prop).toLowerCase());
|
|
292
463
|
},
|
|
293
464
|
ownKeys: () => {
|
|
294
|
-
return this.
|
|
465
|
+
return Array.from(this._headers.keys());
|
|
295
466
|
},
|
|
296
467
|
set: (_target, prop, value) => {
|
|
297
|
-
this.
|
|
468
|
+
if (!this._headersSent) {
|
|
469
|
+
this._headers.set(String(prop).toLowerCase(), value);
|
|
470
|
+
}
|
|
298
471
|
return true;
|
|
299
472
|
},
|
|
300
473
|
});
|
|
@@ -311,9 +484,10 @@ class LambdaResponseBuffered extends Writable {
|
|
|
311
484
|
headersToSet = statusMessageOrHeaders;
|
|
312
485
|
}
|
|
313
486
|
if (headersToSet) {
|
|
487
|
+
// Use direct _headers access to bypass dd-trace interception
|
|
314
488
|
for (const [key, value] of Object.entries(headersToSet)) {
|
|
315
489
|
if (value !== undefined) {
|
|
316
|
-
this.
|
|
490
|
+
this._headers.set(key.toLowerCase(), String(value));
|
|
317
491
|
}
|
|
318
492
|
}
|
|
319
493
|
}
|
|
@@ -350,7 +524,8 @@ class LambdaResponseBuffered extends Writable {
|
|
|
350
524
|
return this;
|
|
351
525
|
}
|
|
352
526
|
json(data) {
|
|
353
|
-
|
|
527
|
+
// Use direct _headers access to bypass dd-trace interception
|
|
528
|
+
this._headers.set("content-type", "application/json");
|
|
354
529
|
this.end(JSON.stringify(data));
|
|
355
530
|
return this;
|
|
356
531
|
}
|
|
@@ -364,11 +539,12 @@ class LambdaResponseBuffered extends Writable {
|
|
|
364
539
|
/**
|
|
365
540
|
* Add a field to the Vary response header.
|
|
366
541
|
* Used by CORS middleware to indicate response varies by Origin.
|
|
542
|
+
* Uses direct _headers access to bypass dd-trace interception.
|
|
367
543
|
*/
|
|
368
544
|
vary(field) {
|
|
369
|
-
const existing = this.
|
|
545
|
+
const existing = this._headers.get("vary");
|
|
370
546
|
if (!existing) {
|
|
371
|
-
this.
|
|
547
|
+
this._headers.set("vary", field);
|
|
372
548
|
}
|
|
373
549
|
else {
|
|
374
550
|
// Append to existing Vary header if field not already present
|
|
@@ -376,7 +552,7 @@ class LambdaResponseBuffered extends Writable {
|
|
|
376
552
|
.split(",")
|
|
377
553
|
.map((f) => f.trim().toLowerCase());
|
|
378
554
|
if (!fields.includes(field.toLowerCase())) {
|
|
379
|
-
this.
|
|
555
|
+
this._headers.set("vary", `${existing}, ${field}`);
|
|
380
556
|
}
|
|
381
557
|
}
|
|
382
558
|
return this;
|
|
@@ -394,6 +570,7 @@ class LambdaResponseBuffered extends Writable {
|
|
|
394
570
|
callback();
|
|
395
571
|
}
|
|
396
572
|
_final(callback) {
|
|
573
|
+
this._ended = true;
|
|
397
574
|
if (this._resolve) {
|
|
398
575
|
this._resolve(this.buildResult());
|
|
399
576
|
}
|
|
@@ -404,7 +581,8 @@ class LambdaResponseBuffered extends Writable {
|
|
|
404
581
|
//
|
|
405
582
|
buildResult() {
|
|
406
583
|
const body = Buffer.concat(this._chunks);
|
|
407
|
-
|
|
584
|
+
// Use direct _headers access to bypass dd-trace interception
|
|
585
|
+
const contentType = this._headers.get("content-type") || "";
|
|
408
586
|
// Determine if response should be base64 encoded
|
|
409
587
|
const isBase64Encoded = this.isBinaryContentType(contentType);
|
|
410
588
|
// Build headers object
|
|
@@ -466,6 +644,8 @@ class LambdaResponseStreaming extends Writable {
|
|
|
466
644
|
this.socket = {
|
|
467
645
|
remoteAddress: "127.0.0.1",
|
|
468
646
|
};
|
|
647
|
+
// Internal state exposed for direct manipulation by safe response methods
|
|
648
|
+
// that need to bypass dd-trace interception
|
|
469
649
|
this._headers = new Map();
|
|
470
650
|
this._headersSent = false;
|
|
471
651
|
this._pendingWrites = [];
|
|
@@ -476,6 +656,65 @@ class LambdaResponseStreaming extends Writable {
|
|
|
476
656
|
if (kOutHeaders) {
|
|
477
657
|
this[kOutHeaders] = Object.create(null);
|
|
478
658
|
}
|
|
659
|
+
// CRITICAL: Define key methods as instance properties to survive Express's
|
|
660
|
+
// setPrototypeOf(res, app.response) in middleware/init.js which would
|
|
661
|
+
// otherwise replace our prototype with ServerResponse.prototype.
|
|
662
|
+
// Instance properties take precedence over prototype properties.
|
|
663
|
+
this.getHeader = this.getHeader.bind(this);
|
|
664
|
+
this.setHeader = this.setHeader.bind(this);
|
|
665
|
+
this.removeHeader = this.removeHeader.bind(this);
|
|
666
|
+
this.hasHeader = this.hasHeader.bind(this);
|
|
667
|
+
this.getHeaders = this.getHeaders.bind(this);
|
|
668
|
+
this.getHeaderNames = this.getHeaderNames.bind(this);
|
|
669
|
+
this.writeHead = this.writeHead.bind(this);
|
|
670
|
+
this.flushHeaders = this.flushHeaders.bind(this);
|
|
671
|
+
this.get = this.get.bind(this);
|
|
672
|
+
this.set = this.set.bind(this);
|
|
673
|
+
this.status = this.status.bind(this);
|
|
674
|
+
this.json = this.json.bind(this);
|
|
675
|
+
this.send = this.send.bind(this);
|
|
676
|
+
this.vary = this.vary.bind(this);
|
|
677
|
+
this.end = this.end.bind(this);
|
|
678
|
+
this.write = this.write.bind(this);
|
|
679
|
+
// Also bind internal Writable methods that are called via prototype chain
|
|
680
|
+
this._write = this._write.bind(this);
|
|
681
|
+
this._final = this._final.bind(this);
|
|
682
|
+
}
|
|
683
|
+
//
|
|
684
|
+
// Internal bypass methods - completely avoid prototype chain lookup
|
|
685
|
+
// These directly access _headers Map, safe from dd-trace interception
|
|
686
|
+
//
|
|
687
|
+
_internalGetHeader(name) {
|
|
688
|
+
const value = this._headers.get(name.toLowerCase());
|
|
689
|
+
return value ? String(value) : undefined;
|
|
690
|
+
}
|
|
691
|
+
_internalSetHeader(name, value) {
|
|
692
|
+
if (!this._headersSent) {
|
|
693
|
+
const lowerName = name.toLowerCase();
|
|
694
|
+
this._headers.set(lowerName, value);
|
|
695
|
+
// Also sync kOutHeaders for any code that expects it
|
|
696
|
+
if (kOutHeaders) {
|
|
697
|
+
const outHeaders = this[kOutHeaders];
|
|
698
|
+
if (outHeaders) {
|
|
699
|
+
outHeaders[lowerName] = [name, value];
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
_internalHasHeader(name) {
|
|
705
|
+
return this._headers.has(name.toLowerCase());
|
|
706
|
+
}
|
|
707
|
+
_internalRemoveHeader(name) {
|
|
708
|
+
if (!this._headersSent) {
|
|
709
|
+
const lowerName = name.toLowerCase();
|
|
710
|
+
this._headers.delete(lowerName);
|
|
711
|
+
if (kOutHeaders) {
|
|
712
|
+
const outHeaders = this[kOutHeaders];
|
|
713
|
+
if (outHeaders) {
|
|
714
|
+
delete outHeaders[lowerName];
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
}
|
|
479
718
|
}
|
|
480
719
|
//
|
|
481
720
|
// Header management
|
|
@@ -530,36 +769,42 @@ class LambdaResponseStreaming extends Writable {
|
|
|
530
769
|
/**
|
|
531
770
|
* Proxy for direct header access (e.g., res.headers['content-type']).
|
|
532
771
|
* Required for compatibility with middleware like helmet that access headers directly.
|
|
772
|
+
* Uses direct _headers access to bypass dd-trace interception.
|
|
533
773
|
*/
|
|
534
774
|
get headers() {
|
|
535
775
|
return new Proxy({}, {
|
|
536
776
|
deleteProperty: (_target, prop) => {
|
|
537
|
-
this.
|
|
777
|
+
if (!this._headersSent) {
|
|
778
|
+
this._headers.delete(String(prop).toLowerCase());
|
|
779
|
+
}
|
|
538
780
|
return true;
|
|
539
781
|
},
|
|
540
782
|
get: (_target, prop) => {
|
|
541
783
|
if (typeof prop === "symbol")
|
|
542
784
|
return undefined;
|
|
543
|
-
return this.
|
|
785
|
+
return this._headers.get(String(prop).toLowerCase());
|
|
544
786
|
},
|
|
545
787
|
getOwnPropertyDescriptor: (_target, prop) => {
|
|
546
|
-
|
|
788
|
+
const lowerProp = String(prop).toLowerCase();
|
|
789
|
+
if (this._headers.has(lowerProp)) {
|
|
547
790
|
return {
|
|
548
791
|
configurable: true,
|
|
549
792
|
enumerable: true,
|
|
550
|
-
value: this.
|
|
793
|
+
value: this._headers.get(lowerProp),
|
|
551
794
|
};
|
|
552
795
|
}
|
|
553
796
|
return undefined;
|
|
554
797
|
},
|
|
555
798
|
has: (_target, prop) => {
|
|
556
|
-
return this.
|
|
799
|
+
return this._headers.has(String(prop).toLowerCase());
|
|
557
800
|
},
|
|
558
801
|
ownKeys: () => {
|
|
559
|
-
return this.
|
|
802
|
+
return Array.from(this._headers.keys());
|
|
560
803
|
},
|
|
561
804
|
set: (_target, prop, value) => {
|
|
562
|
-
this.
|
|
805
|
+
if (!this._headersSent) {
|
|
806
|
+
this._headers.set(String(prop).toLowerCase(), value);
|
|
807
|
+
}
|
|
563
808
|
return true;
|
|
564
809
|
},
|
|
565
810
|
});
|
|
@@ -579,9 +824,10 @@ class LambdaResponseStreaming extends Writable {
|
|
|
579
824
|
headersToSet = statusMessageOrHeaders;
|
|
580
825
|
}
|
|
581
826
|
if (headersToSet) {
|
|
827
|
+
// Use direct _headers access to bypass dd-trace interception
|
|
582
828
|
for (const [key, value] of Object.entries(headersToSet)) {
|
|
583
829
|
if (value !== undefined) {
|
|
584
|
-
this.
|
|
830
|
+
this._headers.set(key.toLowerCase(), String(value));
|
|
585
831
|
}
|
|
586
832
|
}
|
|
587
833
|
}
|
|
@@ -639,7 +885,8 @@ class LambdaResponseStreaming extends Writable {
|
|
|
639
885
|
return this;
|
|
640
886
|
}
|
|
641
887
|
json(data) {
|
|
642
|
-
|
|
888
|
+
// Use direct _headers access to bypass dd-trace interception
|
|
889
|
+
this._headers.set("content-type", "application/json");
|
|
643
890
|
this.end(JSON.stringify(data));
|
|
644
891
|
return this;
|
|
645
892
|
}
|
|
@@ -653,11 +900,12 @@ class LambdaResponseStreaming extends Writable {
|
|
|
653
900
|
/**
|
|
654
901
|
* Add a field to the Vary response header.
|
|
655
902
|
* Used by CORS middleware to indicate response varies by Origin.
|
|
903
|
+
* Uses direct _headers access to bypass dd-trace interception.
|
|
656
904
|
*/
|
|
657
905
|
vary(field) {
|
|
658
|
-
const existing = this.
|
|
906
|
+
const existing = this._headers.get("vary");
|
|
659
907
|
if (!existing) {
|
|
660
|
-
this.
|
|
908
|
+
this._headers.set("vary", field);
|
|
661
909
|
}
|
|
662
910
|
else {
|
|
663
911
|
// Append to existing Vary header if field not already present
|
|
@@ -665,7 +913,7 @@ class LambdaResponseStreaming extends Writable {
|
|
|
665
913
|
.split(",")
|
|
666
914
|
.map((f) => f.trim().toLowerCase());
|
|
667
915
|
if (!fields.includes(field.toLowerCase())) {
|
|
668
|
-
this.
|
|
916
|
+
this._headers.set("vary", `${existing}, ${field}`);
|
|
669
917
|
}
|
|
670
918
|
}
|
|
671
919
|
return this;
|
|
@@ -763,6 +1011,7 @@ function runExpressApp(app, req, res) {
|
|
|
763
1011
|
*/
|
|
764
1012
|
function createLambdaHandler(app, _options) {
|
|
765
1013
|
return async (event, context) => {
|
|
1014
|
+
let result;
|
|
766
1015
|
try {
|
|
767
1016
|
// Set current invoke for getCurrentInvokeUuid
|
|
768
1017
|
setCurrentInvoke(event, context);
|
|
@@ -772,8 +1021,38 @@ function createLambdaHandler(app, _options) {
|
|
|
772
1021
|
const res = new LambdaResponseBuffered();
|
|
773
1022
|
// Run Express app
|
|
774
1023
|
await runExpressApp(app, req, res);
|
|
775
|
-
//
|
|
776
|
-
|
|
1024
|
+
// Get Lambda response - await explicitly to ensure we have the result
|
|
1025
|
+
result = await res.getResult();
|
|
1026
|
+
// Debug: Log the response before returning
|
|
1027
|
+
console.log("[createLambdaHandler] Returning response:", JSON.stringify({
|
|
1028
|
+
statusCode: result.statusCode,
|
|
1029
|
+
headers: result.headers,
|
|
1030
|
+
bodyLength: result.body?.length,
|
|
1031
|
+
isBase64Encoded: result.isBase64Encoded,
|
|
1032
|
+
}));
|
|
1033
|
+
return result;
|
|
1034
|
+
}
|
|
1035
|
+
catch (error) {
|
|
1036
|
+
// Log any unhandled errors
|
|
1037
|
+
console.error("[createLambdaHandler] Unhandled error:", error);
|
|
1038
|
+
if (error instanceof Error) {
|
|
1039
|
+
console.error("[createLambdaHandler] Stack:", error.stack);
|
|
1040
|
+
}
|
|
1041
|
+
// Return a proper error response instead of throwing
|
|
1042
|
+
return {
|
|
1043
|
+
statusCode: 500,
|
|
1044
|
+
headers: { "content-type": "application/json" },
|
|
1045
|
+
body: JSON.stringify({
|
|
1046
|
+
errors: [
|
|
1047
|
+
{
|
|
1048
|
+
status: 500,
|
|
1049
|
+
title: "Internal Server Error",
|
|
1050
|
+
detail: error instanceof Error ? error.message : "Unknown error occurred",
|
|
1051
|
+
},
|
|
1052
|
+
],
|
|
1053
|
+
}),
|
|
1054
|
+
isBase64Encoded: false,
|
|
1055
|
+
};
|
|
777
1056
|
}
|
|
778
1057
|
finally {
|
|
779
1058
|
// Clear current invoke context
|
|
@@ -911,7 +1190,7 @@ const corsHelper = (config = {}) => {
|
|
|
911
1190
|
};
|
|
912
1191
|
return expressCors(options);
|
|
913
1192
|
};
|
|
914
|
-
var
|
|
1193
|
+
var cors_helper = (config) => {
|
|
915
1194
|
const cors = corsHelper(config);
|
|
916
1195
|
return (req, res, next) => {
|
|
917
1196
|
cors(req, res, (error) => {
|
|
@@ -926,154 +1205,10 @@ var cors = (config) => {
|
|
|
926
1205
|
};
|
|
927
1206
|
};
|
|
928
1207
|
|
|
929
|
-
//
|
|
930
|
-
//
|
|
931
|
-
// Constants
|
|
932
|
-
//
|
|
933
|
-
const DEFAULT_PORT = 8080;
|
|
934
|
-
//
|
|
935
|
-
//
|
|
936
|
-
// Main
|
|
937
|
-
//
|
|
938
|
-
/**
|
|
939
|
-
* Creates and starts an Express server with standard Jaypie middleware.
|
|
940
|
-
*
|
|
941
|
-
* Features:
|
|
942
|
-
* - CORS handling (configurable)
|
|
943
|
-
* - JSON body parsing
|
|
944
|
-
* - Listens on PORT env var (default 8080)
|
|
945
|
-
*
|
|
946
|
-
* Usage:
|
|
947
|
-
* ```ts
|
|
948
|
-
* import express from "express";
|
|
949
|
-
* import { createServer, expressHandler } from "@jaypie/express";
|
|
950
|
-
*
|
|
951
|
-
* const app = express();
|
|
952
|
-
*
|
|
953
|
-
* app.get("/", expressHandler(async (req, res) => {
|
|
954
|
-
* return { message: "Hello World" };
|
|
955
|
-
* }));
|
|
956
|
-
*
|
|
957
|
-
* const { server, port } = await createServer(app);
|
|
958
|
-
* console.log(`Server running on port ${port}`);
|
|
959
|
-
* ```
|
|
960
|
-
*
|
|
961
|
-
* @param app - Express application instance
|
|
962
|
-
* @param options - Server configuration options
|
|
963
|
-
* @returns Promise resolving to server instance and port
|
|
964
|
-
*/
|
|
965
|
-
async function createServer(app, options = {}) {
|
|
966
|
-
const { cors: corsConfig, jsonLimit = "1mb", middleware = [], port: portOption, } = options;
|
|
967
|
-
// Determine port
|
|
968
|
-
const port = typeof portOption === "string"
|
|
969
|
-
? parseInt(portOption, 10)
|
|
970
|
-
: (portOption ?? parseInt(process.env.PORT || String(DEFAULT_PORT), 10));
|
|
971
|
-
// Apply CORS middleware (unless explicitly disabled)
|
|
972
|
-
if (corsConfig !== false) {
|
|
973
|
-
app.use(cors(corsConfig));
|
|
974
|
-
}
|
|
975
|
-
// Apply JSON body parser
|
|
976
|
-
// Note: We use dynamic import to avoid requiring express as a direct dependency
|
|
977
|
-
const express = await import('express');
|
|
978
|
-
app.use(express.json({ limit: jsonLimit }));
|
|
979
|
-
// Apply additional middleware
|
|
980
|
-
for (const mw of middleware) {
|
|
981
|
-
app.use(mw);
|
|
982
|
-
}
|
|
983
|
-
// Start server
|
|
984
|
-
return new Promise((resolve, reject) => {
|
|
985
|
-
try {
|
|
986
|
-
const server = app.listen(port, () => {
|
|
987
|
-
// Get the actual port (important when port 0 is passed to get an ephemeral port)
|
|
988
|
-
const address = server.address();
|
|
989
|
-
const actualPort = address?.port ?? port;
|
|
990
|
-
log.info(`Server listening on port ${actualPort}`);
|
|
991
|
-
resolve({ port: actualPort, server });
|
|
992
|
-
});
|
|
993
|
-
server.on("error", (error) => {
|
|
994
|
-
log.error("Server error", { error });
|
|
995
|
-
reject(error);
|
|
996
|
-
});
|
|
997
|
-
}
|
|
998
|
-
catch (error) {
|
|
999
|
-
reject(error);
|
|
1000
|
-
}
|
|
1001
|
-
});
|
|
1002
|
-
}
|
|
1003
|
-
|
|
1004
|
-
//
|
|
1005
|
-
//
|
|
1006
|
-
// Constants
|
|
1007
|
-
//
|
|
1008
|
-
const HEADER_AMZN_REQUEST_ID$1 = "x-amzn-request-id";
|
|
1009
|
-
const ENV_AMZN_TRACE_ID = "_X_AMZN_TRACE_ID";
|
|
1010
|
-
//
|
|
1011
|
-
//
|
|
1012
|
-
// Helper Functions
|
|
1013
|
-
//
|
|
1014
|
-
/**
|
|
1015
|
-
* Extract request ID from X-Ray trace ID environment variable
|
|
1016
|
-
* Format: Root=1-5e6b4a90-example;Parent=example;Sampled=1
|
|
1017
|
-
* We extract the trace ID from the Root segment
|
|
1018
|
-
*/
|
|
1019
|
-
function parseTraceId(traceId) {
|
|
1020
|
-
if (!traceId)
|
|
1021
|
-
return undefined;
|
|
1022
|
-
// Extract the Root segment (format: Root=1-{timestamp}-{uuid})
|
|
1023
|
-
const rootMatch = traceId.match(/Root=([^;]+)/);
|
|
1024
|
-
if (rootMatch && rootMatch[1]) {
|
|
1025
|
-
return rootMatch[1];
|
|
1026
|
-
}
|
|
1027
|
-
return undefined;
|
|
1028
|
-
}
|
|
1029
|
-
//
|
|
1030
|
-
//
|
|
1031
|
-
// Main
|
|
1032
|
-
//
|
|
1033
|
-
/**
|
|
1034
|
-
* Get the current invoke UUID from Lambda Web Adapter context.
|
|
1035
|
-
* This function extracts the request ID from either:
|
|
1036
|
-
* 1. The x-amzn-request-id header (set by Lambda Web Adapter)
|
|
1037
|
-
* 2. The _X_AMZN_TRACE_ID environment variable (set by Lambda runtime)
|
|
1038
|
-
*
|
|
1039
|
-
* @param req - Optional Express request object to extract headers from
|
|
1040
|
-
* @returns The AWS request ID or undefined if not in Lambda context
|
|
1041
|
-
*/
|
|
1042
|
-
function getWebAdapterUuid(req) {
|
|
1043
|
-
// First, try to get from request headers
|
|
1044
|
-
if (req && req.headers) {
|
|
1045
|
-
const headerValue = req.headers[HEADER_AMZN_REQUEST_ID$1];
|
|
1046
|
-
if (headerValue) {
|
|
1047
|
-
return Array.isArray(headerValue) ? headerValue[0] : headerValue;
|
|
1048
|
-
}
|
|
1049
|
-
}
|
|
1050
|
-
// Fall back to environment variable (X-Ray trace ID)
|
|
1051
|
-
const traceId = process.env[ENV_AMZN_TRACE_ID];
|
|
1052
|
-
if (traceId) {
|
|
1053
|
-
return parseTraceId(traceId);
|
|
1054
|
-
}
|
|
1055
|
-
return undefined;
|
|
1056
|
-
}
|
|
1057
|
-
|
|
1058
|
-
//
|
|
1059
|
-
//
|
|
1060
|
-
// Constants
|
|
1061
|
-
//
|
|
1062
|
-
const HEADER_AMZN_REQUEST_ID = "x-amzn-request-id";
|
|
1063
1208
|
//
|
|
1064
1209
|
//
|
|
1065
1210
|
// Helper Functions
|
|
1066
1211
|
//
|
|
1067
|
-
/**
|
|
1068
|
-
* Detect if we're running in Lambda Web Adapter mode.
|
|
1069
|
-
* Web Adapter sets the x-amzn-request-id header on requests.
|
|
1070
|
-
*/
|
|
1071
|
-
function isWebAdapterMode(req) {
|
|
1072
|
-
if (req && req.headers && req.headers[HEADER_AMZN_REQUEST_ID]) {
|
|
1073
|
-
return true;
|
|
1074
|
-
}
|
|
1075
|
-
return false;
|
|
1076
|
-
}
|
|
1077
1212
|
/**
|
|
1078
1213
|
* Get UUID from Jaypie Lambda adapter context.
|
|
1079
1214
|
* This is set by createLambdaHandler/createLambdaStreamHandler.
|
|
@@ -1090,8 +1225,12 @@ function getJaypieAdapterUuid() {
|
|
|
1090
1225
|
* The Jaypie adapter attaches _lambdaContext to the request.
|
|
1091
1226
|
*/
|
|
1092
1227
|
function getRequestContextUuid(req) {
|
|
1093
|
-
if (req && req._lambdaContext
|
|
1094
|
-
|
|
1228
|
+
if (req && req._lambdaContext) {
|
|
1229
|
+
const lambdaContext = req
|
|
1230
|
+
._lambdaContext;
|
|
1231
|
+
if (lambdaContext.awsRequestId) {
|
|
1232
|
+
return lambdaContext.awsRequestId;
|
|
1233
|
+
}
|
|
1095
1234
|
}
|
|
1096
1235
|
return undefined;
|
|
1097
1236
|
}
|
|
@@ -1101,29 +1240,20 @@ function getRequestContextUuid(req) {
|
|
|
1101
1240
|
//
|
|
1102
1241
|
/**
|
|
1103
1242
|
* Get the current invoke UUID from Lambda context.
|
|
1104
|
-
* Works with Jaypie Lambda adapter
|
|
1243
|
+
* Works with Jaypie Lambda adapter (createLambdaHandler/createLambdaStreamHandler).
|
|
1105
1244
|
*
|
|
1106
1245
|
* @param req - Optional Express request object. Used to extract context
|
|
1107
|
-
* from
|
|
1246
|
+
* from Jaypie adapter's _lambdaContext.
|
|
1108
1247
|
* @returns The AWS request ID or undefined if not in Lambda context
|
|
1109
1248
|
*/
|
|
1110
1249
|
function getCurrentInvokeUuid(req) {
|
|
1111
|
-
// Priority 1:
|
|
1112
|
-
if (isWebAdapterMode(req)) {
|
|
1113
|
-
return getWebAdapterUuid(req);
|
|
1114
|
-
}
|
|
1115
|
-
// Priority 2: Request has Lambda context attached (Jaypie adapter)
|
|
1250
|
+
// Priority 1: Request has Lambda context attached (Jaypie adapter)
|
|
1116
1251
|
const requestContextUuid = getRequestContextUuid(req);
|
|
1117
1252
|
if (requestContextUuid) {
|
|
1118
1253
|
return requestContextUuid;
|
|
1119
1254
|
}
|
|
1120
|
-
// Priority
|
|
1121
|
-
|
|
1122
|
-
if (jaypieAdapterUuid) {
|
|
1123
|
-
return jaypieAdapterUuid;
|
|
1124
|
-
}
|
|
1125
|
-
// Fallback: Web Adapter env var
|
|
1126
|
-
return getWebAdapterUuid();
|
|
1255
|
+
// Priority 2: Global context from Jaypie adapter
|
|
1256
|
+
return getJaypieAdapterUuid();
|
|
1127
1257
|
}
|
|
1128
1258
|
|
|
1129
1259
|
//
|
|
@@ -1137,7 +1267,11 @@ function getCurrentInvokeUuid(req) {
|
|
|
1137
1267
|
*/
|
|
1138
1268
|
function safeGetHeader(res, name) {
|
|
1139
1269
|
try {
|
|
1140
|
-
// Try
|
|
1270
|
+
// Try internal method first (completely bypasses dd-trace)
|
|
1271
|
+
if (typeof res._internalGetHeader === "function") {
|
|
1272
|
+
return res._internalGetHeader(name);
|
|
1273
|
+
}
|
|
1274
|
+
// Fall back to _headers Map access (Lambda adapter, avoids dd-trace)
|
|
1141
1275
|
if (res._headers instanceof Map) {
|
|
1142
1276
|
const value = res._headers.get(name.toLowerCase());
|
|
1143
1277
|
return value ? String(value) : undefined;
|
|
@@ -1165,7 +1299,12 @@ function safeGetHeader(res, name) {
|
|
|
1165
1299
|
*/
|
|
1166
1300
|
function safeSetHeader(res, name, value) {
|
|
1167
1301
|
try {
|
|
1168
|
-
// Try
|
|
1302
|
+
// Try internal method first (completely bypasses dd-trace)
|
|
1303
|
+
if (typeof res._internalSetHeader === "function") {
|
|
1304
|
+
res._internalSetHeader(name, value);
|
|
1305
|
+
return;
|
|
1306
|
+
}
|
|
1307
|
+
// Fall back to _headers Map access (Lambda adapter, avoids dd-trace)
|
|
1169
1308
|
if (res._headers instanceof Map) {
|
|
1170
1309
|
res._headers.set(name.toLowerCase(), value);
|
|
1171
1310
|
return;
|
|
@@ -1296,12 +1435,10 @@ const logger$1 = log;
|
|
|
1296
1435
|
//
|
|
1297
1436
|
/**
|
|
1298
1437
|
* Check if response is a Lambda mock response with direct internal access.
|
|
1438
|
+
* Uses Symbol marker to survive prototype chain modifications from Express and dd-trace.
|
|
1299
1439
|
*/
|
|
1300
1440
|
function isLambdaMockResponse(res) {
|
|
1301
|
-
|
|
1302
|
-
return (mock._headers instanceof Map &&
|
|
1303
|
-
Array.isArray(mock._chunks) &&
|
|
1304
|
-
typeof mock.buildResult === "function");
|
|
1441
|
+
return res[JAYPIE_LAMBDA_MOCK] === true;
|
|
1305
1442
|
}
|
|
1306
1443
|
/**
|
|
1307
1444
|
* Safely send a JSON response, avoiding dd-trace interception.
|
|
@@ -1310,17 +1447,28 @@ function isLambdaMockResponse(res) {
|
|
|
1310
1447
|
*/
|
|
1311
1448
|
function safeSendJson(res, statusCode, data) {
|
|
1312
1449
|
if (isLambdaMockResponse(res)) {
|
|
1313
|
-
//
|
|
1314
|
-
res.
|
|
1450
|
+
// Use internal method to set header (completely bypasses dd-trace)
|
|
1451
|
+
if (typeof res._internalSetHeader === "function") {
|
|
1452
|
+
res._internalSetHeader("content-type", "application/json");
|
|
1453
|
+
}
|
|
1454
|
+
else {
|
|
1455
|
+
// Fall back to direct _headers manipulation
|
|
1456
|
+
res._headers.set("content-type", "application/json");
|
|
1457
|
+
}
|
|
1315
1458
|
res.statusCode = statusCode;
|
|
1316
1459
|
// Directly push to chunks array instead of using stream write/end
|
|
1317
1460
|
const chunk = Buffer.from(JSON.stringify(data));
|
|
1318
1461
|
res._chunks.push(chunk);
|
|
1319
1462
|
res._headersSent = true;
|
|
1463
|
+
// Mark as ended so getResult() resolves immediately
|
|
1464
|
+
res._ended = true;
|
|
1320
1465
|
// Signal completion if a promise is waiting
|
|
1321
1466
|
if (res._resolve) {
|
|
1322
1467
|
res._resolve(res.buildResult());
|
|
1323
1468
|
}
|
|
1469
|
+
// Emit "finish" event so runExpressApp's promise resolves
|
|
1470
|
+
console.log("[safeSendJson] Emitting finish event");
|
|
1471
|
+
res.emit("finish");
|
|
1324
1472
|
return;
|
|
1325
1473
|
}
|
|
1326
1474
|
// Fall back to standard Express methods for real responses
|
|
@@ -1340,10 +1488,15 @@ function safeSend(res, statusCode, body) {
|
|
|
1340
1488
|
res._chunks.push(chunk);
|
|
1341
1489
|
}
|
|
1342
1490
|
res._headersSent = true;
|
|
1491
|
+
// Mark as ended so getResult() resolves immediately
|
|
1492
|
+
res._ended = true;
|
|
1343
1493
|
// Signal completion if a promise is waiting
|
|
1344
1494
|
if (res._resolve) {
|
|
1345
1495
|
res._resolve(res.buildResult());
|
|
1346
1496
|
}
|
|
1497
|
+
// Emit "finish" event so runExpressApp's promise resolves
|
|
1498
|
+
console.log("[safeSend] Emitting finish event");
|
|
1499
|
+
res.emit("finish");
|
|
1347
1500
|
return;
|
|
1348
1501
|
}
|
|
1349
1502
|
// Fall back to standard Express methods for real responses
|
|
@@ -1610,7 +1763,18 @@ function expressHandler(handlerOrOptions, optionsOrHandler) {
|
|
|
1610
1763
|
}
|
|
1611
1764
|
}
|
|
1612
1765
|
catch (error) {
|
|
1613
|
-
|
|
1766
|
+
// Use console.error for raw stack trace to ensure it appears in CloudWatch
|
|
1767
|
+
// Handle both Error objects and plain thrown values
|
|
1768
|
+
const errorMessage = error instanceof Error
|
|
1769
|
+
? error.message
|
|
1770
|
+
: typeof error === "object" && error !== null
|
|
1771
|
+
? JSON.stringify(error)
|
|
1772
|
+
: String(error);
|
|
1773
|
+
const errorStack = error instanceof Error
|
|
1774
|
+
? error.stack
|
|
1775
|
+
: new Error("Stack trace").stack?.replace("Error: Stack trace", `Error: ${errorMessage}`);
|
|
1776
|
+
console.error("Express response error stack trace:", errorStack);
|
|
1777
|
+
log.fatal(`Express encountered an error while sending the response: ${errorMessage}`);
|
|
1614
1778
|
log.var({ responseError: error });
|
|
1615
1779
|
}
|
|
1616
1780
|
// Log response
|
|
@@ -1710,14 +1874,13 @@ const logger = log;
|
|
|
1710
1874
|
// Helper
|
|
1711
1875
|
//
|
|
1712
1876
|
/**
|
|
1713
|
-
*
|
|
1877
|
+
* Get error body from an error
|
|
1714
1878
|
*/
|
|
1715
|
-
function
|
|
1879
|
+
function getErrorBody(error) {
|
|
1716
1880
|
const isJaypieError = error.isProjectError;
|
|
1717
|
-
|
|
1881
|
+
return isJaypieError
|
|
1718
1882
|
? (error.body?.() ?? { error: error.message })
|
|
1719
1883
|
: new UnhandledError().body();
|
|
1720
|
-
return `event: error\ndata: ${JSON.stringify(body)}\n\n`;
|
|
1721
1884
|
}
|
|
1722
1885
|
function expressStreamHandler(handlerOrOptions, optionsOrHandler) {
|
|
1723
1886
|
/* eslint-enable no-redeclare */
|
|
@@ -1737,7 +1900,8 @@ function expressStreamHandler(handlerOrOptions, optionsOrHandler) {
|
|
|
1737
1900
|
//
|
|
1738
1901
|
// Validate
|
|
1739
1902
|
//
|
|
1740
|
-
|
|
1903
|
+
const format = options.format ?? "sse";
|
|
1904
|
+
let { chaos, contentType = getContentTypeForFormat(format), locals, name, secrets, setup = [], teardown = [], unavailable, validate, } = options;
|
|
1741
1905
|
if (typeof handler !== "function") {
|
|
1742
1906
|
throw new BadRequestError(`Argument "${handler}" doesn't match type "function"`);
|
|
1743
1907
|
}
|
|
@@ -1875,9 +2039,10 @@ function expressStreamHandler(handlerOrOptions, optionsOrHandler) {
|
|
|
1875
2039
|
log.fatal("Caught unhandled error in stream handler");
|
|
1876
2040
|
log.info.var({ unhandledError: error.message });
|
|
1877
2041
|
}
|
|
1878
|
-
// Write error
|
|
2042
|
+
// Write error in the appropriate format
|
|
1879
2043
|
try {
|
|
1880
|
-
|
|
2044
|
+
const errorBody = getErrorBody(error);
|
|
2045
|
+
res.write(formatStreamError(errorBody, format));
|
|
1881
2046
|
}
|
|
1882
2047
|
catch {
|
|
1883
2048
|
// Response may already be closed
|
|
@@ -1986,5 +2151,5 @@ const noContentRoute = routes.noContentRoute;
|
|
|
1986
2151
|
const notFoundRoute = routes.notFoundRoute;
|
|
1987
2152
|
const notImplementedRoute = routes.notImplementedRoute;
|
|
1988
2153
|
|
|
1989
|
-
export { EXPRESS, LambdaRequest, LambdaResponseBuffered, LambdaResponseStreaming, badRequestRoute, cors, createLambdaHandler, createLambdaStreamHandler,
|
|
2154
|
+
export { EXPRESS, LambdaRequest, LambdaResponseBuffered, LambdaResponseStreaming, badRequestRoute, cors_helper as cors, createLambdaHandler, createLambdaStreamHandler, echoRoute, expressHandler, httpHandler as expressHttpCodeHandler, expressStreamHandler, forbiddenRoute, getCurrentInvoke, getCurrentInvokeUuid, goneRoute, methodNotAllowedRoute, noContentRoute, notFoundRoute, notImplementedRoute };
|
|
1990
2155
|
//# sourceMappingURL=index.js.map
|