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