@jaypie/express 1.2.4-rc2 → 1.2.4-rc21

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