@jaypie/express 1.2.4-rc2 → 1.2.4-rc20
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/dist/cjs/adapter/LambdaResponseBuffered.d.ts +34 -5
- package/dist/cjs/adapter/LambdaResponseStreaming.d.ts +30 -2
- package/dist/cjs/adapter/__tests__/debug-harness.d.ts +1 -0
- package/dist/cjs/index.cjs +585 -28
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/esm/adapter/LambdaResponseBuffered.d.ts +34 -5
- package/dist/esm/adapter/LambdaResponseStreaming.d.ts +30 -2
- package/dist/esm/adapter/__tests__/debug-harness.d.ts +1 -0
- package/dist/esm/index.js +585 -28
- package/dist/esm/index.js.map +1 -1
- package/package.json +10 -1
package/dist/cjs/index.cjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
var node_stream = require('node:stream');
|
|
4
|
+
var node_http = require('node:http');
|
|
4
5
|
var errors = require('@jaypie/errors');
|
|
5
6
|
var kit = require('@jaypie/kit');
|
|
6
7
|
var expressCors = require('cors');
|
|
@@ -33,6 +34,15 @@ class LambdaRequest extends node_stream.Readable {
|
|
|
33
34
|
this.path = options.url.split("?")[0];
|
|
34
35
|
this.headers = this.normalizeHeaders(options.headers);
|
|
35
36
|
this.bodyBuffer = options.body ?? null;
|
|
37
|
+
// Parse query string from URL
|
|
38
|
+
const queryIndex = options.url.indexOf("?");
|
|
39
|
+
if (queryIndex !== -1) {
|
|
40
|
+
const queryString = options.url.slice(queryIndex + 1);
|
|
41
|
+
const params = new URLSearchParams(queryString);
|
|
42
|
+
for (const [key, value] of params) {
|
|
43
|
+
this.query[key] = value;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
36
46
|
// Store Lambda context
|
|
37
47
|
this._lambdaContext = options.lambdaContext;
|
|
38
48
|
this._lambdaEvent = options.lambdaEvent;
|
|
@@ -43,6 +53,18 @@ class LambdaRequest extends node_stream.Readable {
|
|
|
43
53
|
remoteAddress: options.remoteAddress,
|
|
44
54
|
};
|
|
45
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
|
+
}
|
|
46
68
|
}
|
|
47
69
|
//
|
|
48
70
|
// Readable stream implementation
|
|
@@ -144,6 +166,11 @@ function createLambdaRequest(event, context) {
|
|
|
144
166
|
body = event.isBase64Encoded
|
|
145
167
|
? Buffer.from(event.body, "base64")
|
|
146
168
|
: Buffer.from(event.body, "utf8");
|
|
169
|
+
// Add content-length header if not present (required for body parsers)
|
|
170
|
+
const hasContentLength = Object.keys(headers).some((k) => k.toLowerCase() === "content-length");
|
|
171
|
+
if (!hasContentLength) {
|
|
172
|
+
headers["content-length"] = String(body.length);
|
|
173
|
+
}
|
|
147
174
|
}
|
|
148
175
|
return new LambdaRequest({
|
|
149
176
|
body,
|
|
@@ -161,6 +188,10 @@ function createLambdaRequest(event, context) {
|
|
|
161
188
|
//
|
|
162
189
|
// Constants
|
|
163
190
|
//
|
|
191
|
+
// Get Node's internal kOutHeaders symbol from ServerResponse prototype.
|
|
192
|
+
// This is needed for compatibility with Datadog dd-trace instrumentation,
|
|
193
|
+
// which patches HTTP methods and expects this internal state to exist.
|
|
194
|
+
const kOutHeaders$1 = Object.getOwnPropertySymbols(node_http.ServerResponse.prototype).find((s) => s.toString() === "Symbol(kOutHeaders)");
|
|
164
195
|
const BINARY_CONTENT_TYPE_PATTERNS = [
|
|
165
196
|
/^application\/octet-stream$/,
|
|
166
197
|
/^application\/pdf$/,
|
|
@@ -187,17 +218,88 @@ class LambdaResponseBuffered extends node_stream.Writable {
|
|
|
187
218
|
this.socket = {
|
|
188
219
|
remoteAddress: "127.0.0.1",
|
|
189
220
|
};
|
|
221
|
+
// Internal state exposed for direct manipulation by safe response methods
|
|
222
|
+
// that need to bypass dd-trace interception of stream methods
|
|
190
223
|
this._chunks = [];
|
|
224
|
+
this._ended = false; // Track ended state since writableEnded is lost after prototype change
|
|
191
225
|
this._headers = new Map();
|
|
192
226
|
this._headersSent = false;
|
|
193
227
|
this._resolve = null;
|
|
228
|
+
// Initialize Node's internal kOutHeaders for dd-trace compatibility.
|
|
229
|
+
// dd-trace patches HTTP methods and expects this internal state.
|
|
230
|
+
if (kOutHeaders$1) {
|
|
231
|
+
this[kOutHeaders$1] = Object.create(null);
|
|
232
|
+
}
|
|
233
|
+
// CRITICAL: Define key methods as instance properties to survive Express's
|
|
234
|
+
// setPrototypeOf(res, app.response) in middleware/init.js which would
|
|
235
|
+
// otherwise replace our prototype with ServerResponse.prototype.
|
|
236
|
+
// Instance properties take precedence over prototype properties.
|
|
237
|
+
this.getHeader = this.getHeader.bind(this);
|
|
238
|
+
this.setHeader = this.setHeader.bind(this);
|
|
239
|
+
this.removeHeader = this.removeHeader.bind(this);
|
|
240
|
+
this.hasHeader = this.hasHeader.bind(this);
|
|
241
|
+
this.getHeaders = this.getHeaders.bind(this);
|
|
242
|
+
this.getHeaderNames = this.getHeaderNames.bind(this);
|
|
243
|
+
this.writeHead = this.writeHead.bind(this);
|
|
244
|
+
this.get = this.get.bind(this);
|
|
245
|
+
this.set = this.set.bind(this);
|
|
246
|
+
this.status = this.status.bind(this);
|
|
247
|
+
this.json = this.json.bind(this);
|
|
248
|
+
this.send = this.send.bind(this);
|
|
249
|
+
this.vary = this.vary.bind(this);
|
|
250
|
+
this.end = this.end.bind(this);
|
|
251
|
+
this.write = this.write.bind(this);
|
|
252
|
+
// Also bind internal Writable methods that are called via prototype chain
|
|
253
|
+
this._write = this._write.bind(this);
|
|
254
|
+
this._final = this._final.bind(this);
|
|
255
|
+
// Bind result-building methods
|
|
256
|
+
this.getResult = this.getResult.bind(this);
|
|
257
|
+
this.buildResult = this.buildResult.bind(this);
|
|
258
|
+
this.isBinaryContentType = this.isBinaryContentType.bind(this);
|
|
259
|
+
}
|
|
260
|
+
//
|
|
261
|
+
// Internal bypass methods - completely avoid prototype chain lookup
|
|
262
|
+
// These directly access _headers Map, safe from dd-trace interception
|
|
263
|
+
//
|
|
264
|
+
_internalGetHeader(name) {
|
|
265
|
+
const value = this._headers.get(name.toLowerCase());
|
|
266
|
+
return value ? String(value) : undefined;
|
|
267
|
+
}
|
|
268
|
+
_internalSetHeader(name, value) {
|
|
269
|
+
if (!this._headersSent) {
|
|
270
|
+
const lowerName = name.toLowerCase();
|
|
271
|
+
this._headers.set(lowerName, value);
|
|
272
|
+
// Also sync kOutHeaders for any code that expects it
|
|
273
|
+
if (kOutHeaders$1) {
|
|
274
|
+
const outHeaders = this[kOutHeaders$1];
|
|
275
|
+
if (outHeaders) {
|
|
276
|
+
outHeaders[lowerName] = [name, value];
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
_internalHasHeader(name) {
|
|
282
|
+
return this._headers.has(name.toLowerCase());
|
|
283
|
+
}
|
|
284
|
+
_internalRemoveHeader(name) {
|
|
285
|
+
if (!this._headersSent) {
|
|
286
|
+
const lowerName = name.toLowerCase();
|
|
287
|
+
this._headers.delete(lowerName);
|
|
288
|
+
if (kOutHeaders$1) {
|
|
289
|
+
const outHeaders = this[kOutHeaders$1];
|
|
290
|
+
if (outHeaders) {
|
|
291
|
+
delete outHeaders[lowerName];
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
194
295
|
}
|
|
195
296
|
//
|
|
196
297
|
// Promise-based API for getting final result
|
|
197
298
|
//
|
|
198
299
|
getResult() {
|
|
199
300
|
return new Promise((resolve) => {
|
|
200
|
-
|
|
301
|
+
// Use _ended instead of writableEnded since Express's setPrototypeOf breaks the getter
|
|
302
|
+
if (this._ended) {
|
|
201
303
|
resolve(this.buildResult());
|
|
202
304
|
}
|
|
203
305
|
else {
|
|
@@ -213,14 +315,31 @@ class LambdaResponseBuffered extends node_stream.Writable {
|
|
|
213
315
|
// In production, log warning but don't throw to match Express behavior
|
|
214
316
|
return this;
|
|
215
317
|
}
|
|
216
|
-
|
|
318
|
+
const lowerName = name.toLowerCase();
|
|
319
|
+
this._headers.set(lowerName, String(value));
|
|
320
|
+
// Sync with kOutHeaders for dd-trace compatibility
|
|
321
|
+
// Node stores as { 'header-name': ['Header-Name', value] }
|
|
322
|
+
if (kOutHeaders$1) {
|
|
323
|
+
const outHeaders = this[kOutHeaders$1];
|
|
324
|
+
if (outHeaders) {
|
|
325
|
+
outHeaders[lowerName] = [name, String(value)];
|
|
326
|
+
}
|
|
327
|
+
}
|
|
217
328
|
return this;
|
|
218
329
|
}
|
|
219
330
|
getHeader(name) {
|
|
220
331
|
return this._headers.get(name.toLowerCase());
|
|
221
332
|
}
|
|
222
333
|
removeHeader(name) {
|
|
223
|
-
|
|
334
|
+
const lowerName = name.toLowerCase();
|
|
335
|
+
this._headers.delete(lowerName);
|
|
336
|
+
// Sync with kOutHeaders for dd-trace compatibility
|
|
337
|
+
if (kOutHeaders$1) {
|
|
338
|
+
const outHeaders = this[kOutHeaders$1];
|
|
339
|
+
if (outHeaders) {
|
|
340
|
+
delete outHeaders[lowerName];
|
|
341
|
+
}
|
|
342
|
+
}
|
|
224
343
|
}
|
|
225
344
|
getHeaders() {
|
|
226
345
|
const headers = {};
|
|
@@ -235,6 +354,47 @@ class LambdaResponseBuffered extends node_stream.Writable {
|
|
|
235
354
|
getHeaderNames() {
|
|
236
355
|
return Array.from(this._headers.keys());
|
|
237
356
|
}
|
|
357
|
+
/**
|
|
358
|
+
* Proxy for direct header access (e.g., res.headers['content-type']).
|
|
359
|
+
* Required for compatibility with middleware like helmet that access headers directly.
|
|
360
|
+
* Uses direct _headers access to bypass dd-trace interception.
|
|
361
|
+
*/
|
|
362
|
+
get headers() {
|
|
363
|
+
return new Proxy({}, {
|
|
364
|
+
deleteProperty: (_target, prop) => {
|
|
365
|
+
this._headers.delete(String(prop).toLowerCase());
|
|
366
|
+
return true;
|
|
367
|
+
},
|
|
368
|
+
get: (_target, prop) => {
|
|
369
|
+
if (typeof prop === "symbol")
|
|
370
|
+
return undefined;
|
|
371
|
+
return this._headers.get(String(prop).toLowerCase());
|
|
372
|
+
},
|
|
373
|
+
getOwnPropertyDescriptor: (_target, prop) => {
|
|
374
|
+
const lowerProp = String(prop).toLowerCase();
|
|
375
|
+
if (this._headers.has(lowerProp)) {
|
|
376
|
+
return {
|
|
377
|
+
configurable: true,
|
|
378
|
+
enumerable: true,
|
|
379
|
+
value: this._headers.get(lowerProp),
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
return undefined;
|
|
383
|
+
},
|
|
384
|
+
has: (_target, prop) => {
|
|
385
|
+
return this._headers.has(String(prop).toLowerCase());
|
|
386
|
+
},
|
|
387
|
+
ownKeys: () => {
|
|
388
|
+
return Array.from(this._headers.keys());
|
|
389
|
+
},
|
|
390
|
+
set: (_target, prop, value) => {
|
|
391
|
+
if (!this._headersSent) {
|
|
392
|
+
this._headers.set(String(prop).toLowerCase(), value);
|
|
393
|
+
}
|
|
394
|
+
return true;
|
|
395
|
+
},
|
|
396
|
+
});
|
|
397
|
+
}
|
|
238
398
|
writeHead(statusCode, statusMessageOrHeaders, headers) {
|
|
239
399
|
this.statusCode = statusCode;
|
|
240
400
|
let headersToSet;
|
|
@@ -247,9 +407,10 @@ class LambdaResponseBuffered extends node_stream.Writable {
|
|
|
247
407
|
headersToSet = statusMessageOrHeaders;
|
|
248
408
|
}
|
|
249
409
|
if (headersToSet) {
|
|
410
|
+
// Use direct _headers access to bypass dd-trace interception
|
|
250
411
|
for (const [key, value] of Object.entries(headersToSet)) {
|
|
251
412
|
if (value !== undefined) {
|
|
252
|
-
this.
|
|
413
|
+
this._headers.set(key.toLowerCase(), String(value));
|
|
253
414
|
}
|
|
254
415
|
}
|
|
255
416
|
}
|
|
@@ -262,12 +423,32 @@ class LambdaResponseBuffered extends node_stream.Writable {
|
|
|
262
423
|
//
|
|
263
424
|
// Express compatibility methods
|
|
264
425
|
//
|
|
426
|
+
/**
|
|
427
|
+
* Express-style alias for getHeader().
|
|
428
|
+
* Used by middleware like decorateResponse that use res.get().
|
|
429
|
+
* Note: Directly accesses _headers to avoid prototype chain issues with bundled code.
|
|
430
|
+
*/
|
|
431
|
+
get(name) {
|
|
432
|
+
return this._headers.get(name.toLowerCase());
|
|
433
|
+
}
|
|
434
|
+
/**
|
|
435
|
+
* Express-style alias for setHeader().
|
|
436
|
+
* Used by middleware like decorateResponse that use res.set().
|
|
437
|
+
* Note: Directly accesses _headers to avoid prototype chain issues with bundled code.
|
|
438
|
+
*/
|
|
439
|
+
set(name, value) {
|
|
440
|
+
if (!this._headersSent) {
|
|
441
|
+
this._headers.set(name.toLowerCase(), String(value));
|
|
442
|
+
}
|
|
443
|
+
return this;
|
|
444
|
+
}
|
|
265
445
|
status(code) {
|
|
266
446
|
this.statusCode = code;
|
|
267
447
|
return this;
|
|
268
448
|
}
|
|
269
449
|
json(data) {
|
|
270
|
-
|
|
450
|
+
// Use direct _headers access to bypass dd-trace interception
|
|
451
|
+
this._headers.set("content-type", "application/json");
|
|
271
452
|
this.end(JSON.stringify(data));
|
|
272
453
|
return this;
|
|
273
454
|
}
|
|
@@ -278,6 +459,27 @@ class LambdaResponseBuffered extends node_stream.Writable {
|
|
|
278
459
|
this.end(body);
|
|
279
460
|
return this;
|
|
280
461
|
}
|
|
462
|
+
/**
|
|
463
|
+
* Add a field to the Vary response header.
|
|
464
|
+
* Used by CORS middleware to indicate response varies by Origin.
|
|
465
|
+
* Uses direct _headers access to bypass dd-trace interception.
|
|
466
|
+
*/
|
|
467
|
+
vary(field) {
|
|
468
|
+
const existing = this._headers.get("vary");
|
|
469
|
+
if (!existing) {
|
|
470
|
+
this._headers.set("vary", field);
|
|
471
|
+
}
|
|
472
|
+
else {
|
|
473
|
+
// Append to existing Vary header if field not already present
|
|
474
|
+
const fields = String(existing)
|
|
475
|
+
.split(",")
|
|
476
|
+
.map((f) => f.trim().toLowerCase());
|
|
477
|
+
if (!fields.includes(field.toLowerCase())) {
|
|
478
|
+
this._headers.set("vary", `${existing}, ${field}`);
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
return this;
|
|
482
|
+
}
|
|
281
483
|
//
|
|
282
484
|
// Writable stream implementation
|
|
283
485
|
//
|
|
@@ -291,6 +493,7 @@ class LambdaResponseBuffered extends node_stream.Writable {
|
|
|
291
493
|
callback();
|
|
292
494
|
}
|
|
293
495
|
_final(callback) {
|
|
496
|
+
this._ended = true;
|
|
294
497
|
if (this._resolve) {
|
|
295
498
|
this._resolve(this.buildResult());
|
|
296
499
|
}
|
|
@@ -301,7 +504,8 @@ class LambdaResponseBuffered extends node_stream.Writable {
|
|
|
301
504
|
//
|
|
302
505
|
buildResult() {
|
|
303
506
|
const body = Buffer.concat(this._chunks);
|
|
304
|
-
|
|
507
|
+
// Use direct _headers access to bypass dd-trace interception
|
|
508
|
+
const contentType = this._headers.get("content-type") || "";
|
|
305
509
|
// Determine if response should be base64 encoded
|
|
306
510
|
const isBase64Encoded = this.isBinaryContentType(contentType);
|
|
307
511
|
// Build headers object
|
|
@@ -338,6 +542,14 @@ class LambdaResponseBuffered extends node_stream.Writable {
|
|
|
338
542
|
}
|
|
339
543
|
}
|
|
340
544
|
|
|
545
|
+
//
|
|
546
|
+
//
|
|
547
|
+
// Constants
|
|
548
|
+
//
|
|
549
|
+
// Get Node's internal kOutHeaders symbol from ServerResponse prototype.
|
|
550
|
+
// This is needed for compatibility with Datadog dd-trace instrumentation,
|
|
551
|
+
// which patches HTTP methods and expects this internal state to exist.
|
|
552
|
+
const kOutHeaders = Object.getOwnPropertySymbols(node_http.ServerResponse.prototype).find((s) => s.toString() === "Symbol(kOutHeaders)");
|
|
341
553
|
//
|
|
342
554
|
//
|
|
343
555
|
// LambdaResponseStreaming Class
|
|
@@ -355,11 +567,77 @@ class LambdaResponseStreaming extends node_stream.Writable {
|
|
|
355
567
|
this.socket = {
|
|
356
568
|
remoteAddress: "127.0.0.1",
|
|
357
569
|
};
|
|
570
|
+
// Internal state exposed for direct manipulation by safe response methods
|
|
571
|
+
// that need to bypass dd-trace interception
|
|
358
572
|
this._headers = new Map();
|
|
359
573
|
this._headersSent = false;
|
|
360
574
|
this._pendingWrites = [];
|
|
361
575
|
this._wrappedStream = null;
|
|
362
576
|
this._responseStream = responseStream;
|
|
577
|
+
// Initialize Node's internal kOutHeaders for dd-trace compatibility.
|
|
578
|
+
// dd-trace patches HTTP methods and expects this internal state.
|
|
579
|
+
if (kOutHeaders) {
|
|
580
|
+
this[kOutHeaders] = Object.create(null);
|
|
581
|
+
}
|
|
582
|
+
// CRITICAL: Define key methods as instance properties to survive Express's
|
|
583
|
+
// setPrototypeOf(res, app.response) in middleware/init.js which would
|
|
584
|
+
// otherwise replace our prototype with ServerResponse.prototype.
|
|
585
|
+
// Instance properties take precedence over prototype properties.
|
|
586
|
+
this.getHeader = this.getHeader.bind(this);
|
|
587
|
+
this.setHeader = this.setHeader.bind(this);
|
|
588
|
+
this.removeHeader = this.removeHeader.bind(this);
|
|
589
|
+
this.hasHeader = this.hasHeader.bind(this);
|
|
590
|
+
this.getHeaders = this.getHeaders.bind(this);
|
|
591
|
+
this.getHeaderNames = this.getHeaderNames.bind(this);
|
|
592
|
+
this.writeHead = this.writeHead.bind(this);
|
|
593
|
+
this.flushHeaders = this.flushHeaders.bind(this);
|
|
594
|
+
this.get = this.get.bind(this);
|
|
595
|
+
this.set = this.set.bind(this);
|
|
596
|
+
this.status = this.status.bind(this);
|
|
597
|
+
this.json = this.json.bind(this);
|
|
598
|
+
this.send = this.send.bind(this);
|
|
599
|
+
this.vary = this.vary.bind(this);
|
|
600
|
+
this.end = this.end.bind(this);
|
|
601
|
+
this.write = this.write.bind(this);
|
|
602
|
+
// Also bind internal Writable methods that are called via prototype chain
|
|
603
|
+
this._write = this._write.bind(this);
|
|
604
|
+
this._final = this._final.bind(this);
|
|
605
|
+
}
|
|
606
|
+
//
|
|
607
|
+
// Internal bypass methods - completely avoid prototype chain lookup
|
|
608
|
+
// These directly access _headers Map, safe from dd-trace interception
|
|
609
|
+
//
|
|
610
|
+
_internalGetHeader(name) {
|
|
611
|
+
const value = this._headers.get(name.toLowerCase());
|
|
612
|
+
return value ? String(value) : undefined;
|
|
613
|
+
}
|
|
614
|
+
_internalSetHeader(name, value) {
|
|
615
|
+
if (!this._headersSent) {
|
|
616
|
+
const lowerName = name.toLowerCase();
|
|
617
|
+
this._headers.set(lowerName, value);
|
|
618
|
+
// Also sync kOutHeaders for any code that expects it
|
|
619
|
+
if (kOutHeaders) {
|
|
620
|
+
const outHeaders = this[kOutHeaders];
|
|
621
|
+
if (outHeaders) {
|
|
622
|
+
outHeaders[lowerName] = [name, value];
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
_internalHasHeader(name) {
|
|
628
|
+
return this._headers.has(name.toLowerCase());
|
|
629
|
+
}
|
|
630
|
+
_internalRemoveHeader(name) {
|
|
631
|
+
if (!this._headersSent) {
|
|
632
|
+
const lowerName = name.toLowerCase();
|
|
633
|
+
this._headers.delete(lowerName);
|
|
634
|
+
if (kOutHeaders) {
|
|
635
|
+
const outHeaders = this[kOutHeaders];
|
|
636
|
+
if (outHeaders) {
|
|
637
|
+
delete outHeaders[lowerName];
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
}
|
|
363
641
|
}
|
|
364
642
|
//
|
|
365
643
|
// Header management
|
|
@@ -370,7 +648,16 @@ class LambdaResponseStreaming extends node_stream.Writable {
|
|
|
370
648
|
// Headers cannot be changed after body starts
|
|
371
649
|
return this;
|
|
372
650
|
}
|
|
373
|
-
|
|
651
|
+
const lowerName = name.toLowerCase();
|
|
652
|
+
this._headers.set(lowerName, String(value));
|
|
653
|
+
// Sync with kOutHeaders for dd-trace compatibility
|
|
654
|
+
// Node stores as { 'header-name': ['Header-Name', value] }
|
|
655
|
+
if (kOutHeaders) {
|
|
656
|
+
const outHeaders = this[kOutHeaders];
|
|
657
|
+
if (outHeaders) {
|
|
658
|
+
outHeaders[lowerName] = [name, String(value)];
|
|
659
|
+
}
|
|
660
|
+
}
|
|
374
661
|
return this;
|
|
375
662
|
}
|
|
376
663
|
getHeader(name) {
|
|
@@ -378,7 +665,15 @@ class LambdaResponseStreaming extends node_stream.Writable {
|
|
|
378
665
|
}
|
|
379
666
|
removeHeader(name) {
|
|
380
667
|
if (!this._headersSent) {
|
|
381
|
-
|
|
668
|
+
const lowerName = name.toLowerCase();
|
|
669
|
+
this._headers.delete(lowerName);
|
|
670
|
+
// Sync with kOutHeaders for dd-trace compatibility
|
|
671
|
+
if (kOutHeaders) {
|
|
672
|
+
const outHeaders = this[kOutHeaders];
|
|
673
|
+
if (outHeaders) {
|
|
674
|
+
delete outHeaders[lowerName];
|
|
675
|
+
}
|
|
676
|
+
}
|
|
382
677
|
}
|
|
383
678
|
}
|
|
384
679
|
getHeaders() {
|
|
@@ -394,6 +689,49 @@ class LambdaResponseStreaming extends node_stream.Writable {
|
|
|
394
689
|
getHeaderNames() {
|
|
395
690
|
return Array.from(this._headers.keys());
|
|
396
691
|
}
|
|
692
|
+
/**
|
|
693
|
+
* Proxy for direct header access (e.g., res.headers['content-type']).
|
|
694
|
+
* Required for compatibility with middleware like helmet that access headers directly.
|
|
695
|
+
* Uses direct _headers access to bypass dd-trace interception.
|
|
696
|
+
*/
|
|
697
|
+
get headers() {
|
|
698
|
+
return new Proxy({}, {
|
|
699
|
+
deleteProperty: (_target, prop) => {
|
|
700
|
+
if (!this._headersSent) {
|
|
701
|
+
this._headers.delete(String(prop).toLowerCase());
|
|
702
|
+
}
|
|
703
|
+
return true;
|
|
704
|
+
},
|
|
705
|
+
get: (_target, prop) => {
|
|
706
|
+
if (typeof prop === "symbol")
|
|
707
|
+
return undefined;
|
|
708
|
+
return this._headers.get(String(prop).toLowerCase());
|
|
709
|
+
},
|
|
710
|
+
getOwnPropertyDescriptor: (_target, prop) => {
|
|
711
|
+
const lowerProp = String(prop).toLowerCase();
|
|
712
|
+
if (this._headers.has(lowerProp)) {
|
|
713
|
+
return {
|
|
714
|
+
configurable: true,
|
|
715
|
+
enumerable: true,
|
|
716
|
+
value: this._headers.get(lowerProp),
|
|
717
|
+
};
|
|
718
|
+
}
|
|
719
|
+
return undefined;
|
|
720
|
+
},
|
|
721
|
+
has: (_target, prop) => {
|
|
722
|
+
return this._headers.has(String(prop).toLowerCase());
|
|
723
|
+
},
|
|
724
|
+
ownKeys: () => {
|
|
725
|
+
return Array.from(this._headers.keys());
|
|
726
|
+
},
|
|
727
|
+
set: (_target, prop, value) => {
|
|
728
|
+
if (!this._headersSent) {
|
|
729
|
+
this._headers.set(String(prop).toLowerCase(), value);
|
|
730
|
+
}
|
|
731
|
+
return true;
|
|
732
|
+
},
|
|
733
|
+
});
|
|
734
|
+
}
|
|
397
735
|
writeHead(statusCode, statusMessageOrHeaders, headers) {
|
|
398
736
|
if (this._headersSent) {
|
|
399
737
|
return this;
|
|
@@ -409,9 +747,10 @@ class LambdaResponseStreaming extends node_stream.Writable {
|
|
|
409
747
|
headersToSet = statusMessageOrHeaders;
|
|
410
748
|
}
|
|
411
749
|
if (headersToSet) {
|
|
750
|
+
// Use direct _headers access to bypass dd-trace interception
|
|
412
751
|
for (const [key, value] of Object.entries(headersToSet)) {
|
|
413
752
|
if (value !== undefined) {
|
|
414
|
-
this.
|
|
753
|
+
this._headers.set(key.toLowerCase(), String(value));
|
|
415
754
|
}
|
|
416
755
|
}
|
|
417
756
|
}
|
|
@@ -445,12 +784,32 @@ class LambdaResponseStreaming extends node_stream.Writable {
|
|
|
445
784
|
//
|
|
446
785
|
// Express compatibility methods
|
|
447
786
|
//
|
|
787
|
+
/**
|
|
788
|
+
* Express-style alias for getHeader().
|
|
789
|
+
* Used by middleware like decorateResponse that use res.get().
|
|
790
|
+
* Note: Directly accesses _headers to avoid prototype chain issues with bundled code.
|
|
791
|
+
*/
|
|
792
|
+
get(name) {
|
|
793
|
+
return this._headers.get(name.toLowerCase());
|
|
794
|
+
}
|
|
795
|
+
/**
|
|
796
|
+
* Express-style alias for setHeader().
|
|
797
|
+
* Used by middleware like decorateResponse that use res.set().
|
|
798
|
+
* Note: Directly accesses _headers to avoid prototype chain issues with bundled code.
|
|
799
|
+
*/
|
|
800
|
+
set(name, value) {
|
|
801
|
+
if (!this._headersSent) {
|
|
802
|
+
this._headers.set(name.toLowerCase(), String(value));
|
|
803
|
+
}
|
|
804
|
+
return this;
|
|
805
|
+
}
|
|
448
806
|
status(code) {
|
|
449
807
|
this.statusCode = code;
|
|
450
808
|
return this;
|
|
451
809
|
}
|
|
452
810
|
json(data) {
|
|
453
|
-
|
|
811
|
+
// Use direct _headers access to bypass dd-trace interception
|
|
812
|
+
this._headers.set("content-type", "application/json");
|
|
454
813
|
this.end(JSON.stringify(data));
|
|
455
814
|
return this;
|
|
456
815
|
}
|
|
@@ -461,6 +820,27 @@ class LambdaResponseStreaming extends node_stream.Writable {
|
|
|
461
820
|
this.end(body);
|
|
462
821
|
return this;
|
|
463
822
|
}
|
|
823
|
+
/**
|
|
824
|
+
* Add a field to the Vary response header.
|
|
825
|
+
* Used by CORS middleware to indicate response varies by Origin.
|
|
826
|
+
* Uses direct _headers access to bypass dd-trace interception.
|
|
827
|
+
*/
|
|
828
|
+
vary(field) {
|
|
829
|
+
const existing = this._headers.get("vary");
|
|
830
|
+
if (!existing) {
|
|
831
|
+
this._headers.set("vary", field);
|
|
832
|
+
}
|
|
833
|
+
else {
|
|
834
|
+
// Append to existing Vary header if field not already present
|
|
835
|
+
const fields = String(existing)
|
|
836
|
+
.split(",")
|
|
837
|
+
.map((f) => f.trim().toLowerCase());
|
|
838
|
+
if (!fields.includes(field.toLowerCase())) {
|
|
839
|
+
this._headers.set("vary", `${existing}, ${field}`);
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
return this;
|
|
843
|
+
}
|
|
464
844
|
//
|
|
465
845
|
// Writable stream implementation
|
|
466
846
|
//
|
|
@@ -554,6 +934,7 @@ function runExpressApp(app, req, res) {
|
|
|
554
934
|
*/
|
|
555
935
|
function createLambdaHandler(app, _options) {
|
|
556
936
|
return async (event, context) => {
|
|
937
|
+
let result;
|
|
557
938
|
try {
|
|
558
939
|
// Set current invoke for getCurrentInvokeUuid
|
|
559
940
|
setCurrentInvoke(event, context);
|
|
@@ -563,8 +944,38 @@ function createLambdaHandler(app, _options) {
|
|
|
563
944
|
const res = new LambdaResponseBuffered();
|
|
564
945
|
// Run Express app
|
|
565
946
|
await runExpressApp(app, req, res);
|
|
566
|
-
//
|
|
567
|
-
|
|
947
|
+
// Get Lambda response - await explicitly to ensure we have the result
|
|
948
|
+
result = await res.getResult();
|
|
949
|
+
// Debug: Log the response before returning
|
|
950
|
+
console.log("[createLambdaHandler] Returning response:", JSON.stringify({
|
|
951
|
+
statusCode: result.statusCode,
|
|
952
|
+
headers: result.headers,
|
|
953
|
+
bodyLength: result.body?.length,
|
|
954
|
+
isBase64Encoded: result.isBase64Encoded,
|
|
955
|
+
}));
|
|
956
|
+
return result;
|
|
957
|
+
}
|
|
958
|
+
catch (error) {
|
|
959
|
+
// Log any unhandled errors
|
|
960
|
+
console.error("[createLambdaHandler] Unhandled error:", error);
|
|
961
|
+
if (error instanceof Error) {
|
|
962
|
+
console.error("[createLambdaHandler] Stack:", error.stack);
|
|
963
|
+
}
|
|
964
|
+
// Return a proper error response instead of throwing
|
|
965
|
+
return {
|
|
966
|
+
statusCode: 500,
|
|
967
|
+
headers: { "content-type": "application/json" },
|
|
968
|
+
body: JSON.stringify({
|
|
969
|
+
errors: [
|
|
970
|
+
{
|
|
971
|
+
status: 500,
|
|
972
|
+
title: "Internal Server Error",
|
|
973
|
+
detail: error instanceof Error ? error.message : "Unknown error occurred",
|
|
974
|
+
},
|
|
975
|
+
],
|
|
976
|
+
}),
|
|
977
|
+
isBase64Encoded: false,
|
|
978
|
+
};
|
|
568
979
|
}
|
|
569
980
|
finally {
|
|
570
981
|
// Clear current invoke context
|
|
@@ -917,6 +1328,73 @@ function getCurrentInvokeUuid(req) {
|
|
|
917
1328
|
return getWebAdapterUuid();
|
|
918
1329
|
}
|
|
919
1330
|
|
|
1331
|
+
//
|
|
1332
|
+
//
|
|
1333
|
+
// Helpers
|
|
1334
|
+
//
|
|
1335
|
+
/**
|
|
1336
|
+
* Safely get a header value from response.
|
|
1337
|
+
* Handles both Express Response and Lambda adapter responses.
|
|
1338
|
+
* Defensive against dd-trace instrumentation issues.
|
|
1339
|
+
*/
|
|
1340
|
+
function safeGetHeader(res, name) {
|
|
1341
|
+
try {
|
|
1342
|
+
// Try internal method first (completely bypasses dd-trace)
|
|
1343
|
+
if (typeof res._internalGetHeader === "function") {
|
|
1344
|
+
return res._internalGetHeader(name);
|
|
1345
|
+
}
|
|
1346
|
+
// Fall back to _headers Map access (Lambda adapter, avoids dd-trace)
|
|
1347
|
+
if (res._headers instanceof Map) {
|
|
1348
|
+
const value = res._headers.get(name.toLowerCase());
|
|
1349
|
+
return value ? String(value) : undefined;
|
|
1350
|
+
}
|
|
1351
|
+
// Fall back to getHeader (more standard than get)
|
|
1352
|
+
if (typeof res.getHeader === "function") {
|
|
1353
|
+
const value = res.getHeader(name);
|
|
1354
|
+
return value ? String(value) : undefined;
|
|
1355
|
+
}
|
|
1356
|
+
// Last resort: try get
|
|
1357
|
+
if (typeof res.get === "function") {
|
|
1358
|
+
const value = res.get(name);
|
|
1359
|
+
return value ? String(value) : undefined;
|
|
1360
|
+
}
|
|
1361
|
+
}
|
|
1362
|
+
catch {
|
|
1363
|
+
// Silently fail - caller will handle missing value
|
|
1364
|
+
}
|
|
1365
|
+
return undefined;
|
|
1366
|
+
}
|
|
1367
|
+
/**
|
|
1368
|
+
* Safely set a header value on response.
|
|
1369
|
+
* Handles both Express Response and Lambda adapter responses.
|
|
1370
|
+
* Defensive against dd-trace instrumentation issues.
|
|
1371
|
+
*/
|
|
1372
|
+
function safeSetHeader(res, name, value) {
|
|
1373
|
+
try {
|
|
1374
|
+
// Try internal method first (completely bypasses dd-trace)
|
|
1375
|
+
if (typeof res._internalSetHeader === "function") {
|
|
1376
|
+
res._internalSetHeader(name, value);
|
|
1377
|
+
return;
|
|
1378
|
+
}
|
|
1379
|
+
// Fall back to _headers Map access (Lambda adapter, avoids dd-trace)
|
|
1380
|
+
if (res._headers instanceof Map) {
|
|
1381
|
+
res._headers.set(name.toLowerCase(), value);
|
|
1382
|
+
return;
|
|
1383
|
+
}
|
|
1384
|
+
// Fall back to setHeader (more standard than set)
|
|
1385
|
+
if (typeof res.setHeader === "function") {
|
|
1386
|
+
res.setHeader(name, value);
|
|
1387
|
+
return;
|
|
1388
|
+
}
|
|
1389
|
+
// Last resort: try set
|
|
1390
|
+
if (typeof res.set === "function") {
|
|
1391
|
+
res.set(name, value);
|
|
1392
|
+
}
|
|
1393
|
+
}
|
|
1394
|
+
catch {
|
|
1395
|
+
// Silently fail - header just won't be set
|
|
1396
|
+
}
|
|
1397
|
+
}
|
|
920
1398
|
//
|
|
921
1399
|
//
|
|
922
1400
|
// Main
|
|
@@ -933,36 +1411,37 @@ const decorateResponse = (res, { handler = "", version = process.env.PROJECT_VER
|
|
|
933
1411
|
log.warn("decorateResponse called but response is not an object");
|
|
934
1412
|
return;
|
|
935
1413
|
}
|
|
1414
|
+
const extRes = res;
|
|
936
1415
|
try {
|
|
937
1416
|
//
|
|
938
1417
|
//
|
|
939
1418
|
// Decorate Headers
|
|
940
1419
|
//
|
|
941
1420
|
// X-Powered-By, override "Express" but nothing else
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
1421
|
+
const currentPoweredBy = safeGetHeader(extRes, kit.HTTP.HEADER.POWERED_BY);
|
|
1422
|
+
if (!currentPoweredBy || currentPoweredBy === "Express") {
|
|
1423
|
+
safeSetHeader(extRes, kit.HTTP.HEADER.POWERED_BY, kit.JAYPIE.LIB.EXPRESS);
|
|
945
1424
|
}
|
|
946
1425
|
// X-Project-Environment
|
|
947
1426
|
if (process.env.PROJECT_ENV) {
|
|
948
|
-
|
|
1427
|
+
safeSetHeader(extRes, kit.HTTP.HEADER.PROJECT.ENVIRONMENT, process.env.PROJECT_ENV);
|
|
949
1428
|
}
|
|
950
1429
|
// X-Project-Handler
|
|
951
1430
|
if (handler) {
|
|
952
|
-
|
|
1431
|
+
safeSetHeader(extRes, kit.HTTP.HEADER.PROJECT.HANDLER, handler);
|
|
953
1432
|
}
|
|
954
1433
|
// X-Project-Invocation
|
|
955
1434
|
const currentInvoke = getCurrentInvokeUuid();
|
|
956
1435
|
if (currentInvoke) {
|
|
957
|
-
|
|
1436
|
+
safeSetHeader(extRes, kit.HTTP.HEADER.PROJECT.INVOCATION, currentInvoke);
|
|
958
1437
|
}
|
|
959
1438
|
// X-Project-Key
|
|
960
1439
|
if (process.env.PROJECT_KEY) {
|
|
961
|
-
|
|
1440
|
+
safeSetHeader(extRes, kit.HTTP.HEADER.PROJECT.KEY, process.env.PROJECT_KEY);
|
|
962
1441
|
}
|
|
963
1442
|
// X-Project-Version
|
|
964
1443
|
if (version) {
|
|
965
|
-
|
|
1444
|
+
safeSetHeader(extRes, kit.HTTP.HEADER.PROJECT.VERSION, version);
|
|
966
1445
|
}
|
|
967
1446
|
//
|
|
968
1447
|
//
|
|
@@ -1022,6 +1501,80 @@ function summarizeResponse(res, extras) {
|
|
|
1022
1501
|
|
|
1023
1502
|
// Cast logger to extended interface for runtime features not in type definitions
|
|
1024
1503
|
const logger$1 = logger$2.log;
|
|
1504
|
+
//
|
|
1505
|
+
//
|
|
1506
|
+
// Helpers - Safe response methods to bypass dd-trace interception
|
|
1507
|
+
//
|
|
1508
|
+
/**
|
|
1509
|
+
* Check if response is a Lambda mock response with direct internal access.
|
|
1510
|
+
*/
|
|
1511
|
+
function isLambdaMockResponse(res) {
|
|
1512
|
+
const mock = res;
|
|
1513
|
+
return (mock._headers instanceof Map &&
|
|
1514
|
+
Array.isArray(mock._chunks) &&
|
|
1515
|
+
typeof mock.buildResult === "function");
|
|
1516
|
+
}
|
|
1517
|
+
/**
|
|
1518
|
+
* Safely send a JSON response, avoiding dd-trace interception.
|
|
1519
|
+
* For Lambda mock responses, directly manipulates internal state instead of
|
|
1520
|
+
* using stream methods (write/end) which dd-trace intercepts.
|
|
1521
|
+
*/
|
|
1522
|
+
function safeSendJson(res, statusCode, data) {
|
|
1523
|
+
if (isLambdaMockResponse(res)) {
|
|
1524
|
+
// Use internal method to set header (completely bypasses dd-trace)
|
|
1525
|
+
if (typeof res._internalSetHeader === "function") {
|
|
1526
|
+
res._internalSetHeader("content-type", "application/json");
|
|
1527
|
+
}
|
|
1528
|
+
else {
|
|
1529
|
+
// Fall back to direct _headers manipulation
|
|
1530
|
+
res._headers.set("content-type", "application/json");
|
|
1531
|
+
}
|
|
1532
|
+
res.statusCode = statusCode;
|
|
1533
|
+
// Directly push to chunks array instead of using stream write/end
|
|
1534
|
+
const chunk = Buffer.from(JSON.stringify(data));
|
|
1535
|
+
res._chunks.push(chunk);
|
|
1536
|
+
res._headersSent = true;
|
|
1537
|
+
// Signal completion if a promise is waiting
|
|
1538
|
+
if (res._resolve) {
|
|
1539
|
+
res._resolve(res.buildResult());
|
|
1540
|
+
}
|
|
1541
|
+
// Emit "finish" event so runExpressApp's promise resolves
|
|
1542
|
+
res.emit("finish");
|
|
1543
|
+
return;
|
|
1544
|
+
}
|
|
1545
|
+
// Fall back to standard Express methods for real responses
|
|
1546
|
+
res.status(statusCode).json(data);
|
|
1547
|
+
}
|
|
1548
|
+
/**
|
|
1549
|
+
* Safely send a response body, avoiding dd-trace interception.
|
|
1550
|
+
* For Lambda mock responses, directly manipulates internal state instead of
|
|
1551
|
+
* using stream methods (write/end) which dd-trace intercepts.
|
|
1552
|
+
*/
|
|
1553
|
+
function safeSend(res, statusCode, body) {
|
|
1554
|
+
if (isLambdaMockResponse(res)) {
|
|
1555
|
+
// Direct internal state manipulation - bypasses dd-trace completely
|
|
1556
|
+
res.statusCode = statusCode;
|
|
1557
|
+
if (body !== undefined) {
|
|
1558
|
+
const chunk = Buffer.from(body);
|
|
1559
|
+
res._chunks.push(chunk);
|
|
1560
|
+
}
|
|
1561
|
+
res._headersSent = true;
|
|
1562
|
+
// Signal completion if a promise is waiting
|
|
1563
|
+
if (res._resolve) {
|
|
1564
|
+
res._resolve(res.buildResult());
|
|
1565
|
+
}
|
|
1566
|
+
// Emit "finish" event so runExpressApp's promise resolves
|
|
1567
|
+
res.emit("finish");
|
|
1568
|
+
return;
|
|
1569
|
+
}
|
|
1570
|
+
// Fall back to standard Express methods for real responses
|
|
1571
|
+
if (body !== undefined) {
|
|
1572
|
+
res.status(statusCode).send(body);
|
|
1573
|
+
}
|
|
1574
|
+
else {
|
|
1575
|
+
res.status(statusCode).send();
|
|
1576
|
+
}
|
|
1577
|
+
}
|
|
1025
1578
|
function expressHandler(handlerOrOptions, optionsOrHandler) {
|
|
1026
1579
|
/* eslint-enable no-redeclare */
|
|
1027
1580
|
let handler;
|
|
@@ -1238,30 +1791,30 @@ function expressHandler(handlerOrOptions, optionsOrHandler) {
|
|
|
1238
1791
|
if (typeof response === "object") {
|
|
1239
1792
|
if (typeof response.json ===
|
|
1240
1793
|
"function") {
|
|
1241
|
-
res
|
|
1794
|
+
safeSendJson(res, status, response.json());
|
|
1242
1795
|
}
|
|
1243
1796
|
else {
|
|
1244
|
-
res
|
|
1797
|
+
safeSendJson(res, status, response);
|
|
1245
1798
|
}
|
|
1246
1799
|
}
|
|
1247
1800
|
else if (typeof response === "string") {
|
|
1248
1801
|
try {
|
|
1249
|
-
res
|
|
1802
|
+
safeSendJson(res, status, JSON.parse(response));
|
|
1250
1803
|
}
|
|
1251
1804
|
catch {
|
|
1252
|
-
res
|
|
1805
|
+
safeSend(res, status, response);
|
|
1253
1806
|
}
|
|
1254
1807
|
}
|
|
1255
1808
|
else if (response === true) {
|
|
1256
|
-
res
|
|
1809
|
+
safeSend(res, kit.HTTP.CODE.CREATED);
|
|
1257
1810
|
}
|
|
1258
1811
|
else {
|
|
1259
|
-
res
|
|
1812
|
+
safeSend(res, status, response);
|
|
1260
1813
|
}
|
|
1261
1814
|
}
|
|
1262
1815
|
else {
|
|
1263
1816
|
// No response
|
|
1264
|
-
res
|
|
1817
|
+
safeSend(res, kit.HTTP.CODE.NO_CONTENT);
|
|
1265
1818
|
}
|
|
1266
1819
|
}
|
|
1267
1820
|
else {
|
|
@@ -1278,7 +1831,11 @@ function expressHandler(handlerOrOptions, optionsOrHandler) {
|
|
|
1278
1831
|
}
|
|
1279
1832
|
}
|
|
1280
1833
|
catch (error) {
|
|
1281
|
-
|
|
1834
|
+
// Use console.error for raw stack trace to ensure it appears in CloudWatch
|
|
1835
|
+
if (error instanceof Error) {
|
|
1836
|
+
console.error("Express response error stack trace:", error.stack);
|
|
1837
|
+
}
|
|
1838
|
+
log.fatal(`Express encountered an error while sending the response: ${error instanceof Error ? error.message : String(error)}`);
|
|
1282
1839
|
log.var({ responseError: error });
|
|
1283
1840
|
}
|
|
1284
1841
|
// Log response
|