@jaypie/express 1.2.4-rc8 → 1.2.4

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
@@ -3,8 +3,8 @@ import { ServerResponse } from 'node:http';
3
3
  import { CorsError, BadRequestError, UnhandledError, GatewayTimeoutError, UnavailableError, BadGatewayError, InternalError, TeapotError, GoneError, MethodNotAllowedError, NotFoundError, ForbiddenError, UnauthorizedError, NotImplementedError } from '@jaypie/errors';
4
4
  import { force, envBoolean, JAYPIE, HTTP, getHeaderFrom, jaypieHandler } from '@jaypie/kit';
5
5
  import expressCors from 'cors';
6
+ import { loadEnvSecrets, getContentTypeForFormat, formatStreamError } from '@jaypie/aws';
6
7
  import { log } from '@jaypie/logger';
7
- import { loadEnvSecrets } from '@jaypie/aws';
8
8
  import { hasDatadogEnv, submitMetric, DATADOG } from '@jaypie/datadog';
9
9
 
10
10
  //
@@ -32,6 +32,15 @@ class LambdaRequest extends Readable {
32
32
  this.path = options.url.split("?")[0];
33
33
  this.headers = this.normalizeHeaders(options.headers);
34
34
  this.bodyBuffer = options.body ?? null;
35
+ // 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
+ }
35
44
  // Store Lambda context
36
45
  this._lambdaContext = options.lambdaContext;
37
46
  this._lambdaEvent = options.lambdaEvent;
@@ -42,6 +51,18 @@ class LambdaRequest extends Readable {
42
51
  remoteAddress: options.remoteAddress,
43
52
  };
44
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
+ }
45
66
  }
46
67
  //
47
68
  // Readable stream implementation
@@ -143,6 +164,11 @@ function createLambdaRequest(event, context) {
143
164
  body = event.isBase64Encoded
144
165
  ? Buffer.from(event.body, "base64")
145
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
+ }
146
172
  }
147
173
  return new LambdaRequest({
148
174
  body,
@@ -160,6 +186,9 @@ function createLambdaRequest(event, context) {
160
186
  //
161
187
  // Constants
162
188
  //
189
+ // Symbol to identify Lambda mock responses. Uses Symbol.for() to ensure
190
+ // the same symbol is used across bundles/realms. Survives prototype manipulation.
191
+ const JAYPIE_LAMBDA_MOCK = Symbol.for("@jaypie/express/LambdaMock");
163
192
  // Get Node's internal kOutHeaders symbol from ServerResponse prototype.
164
193
  // This is needed for compatibility with Datadog dd-trace instrumentation,
165
194
  // which patches HTTP methods and expects this internal state to exist.
@@ -190,22 +219,90 @@ class LambdaResponseBuffered extends Writable {
190
219
  this.socket = {
191
220
  remoteAddress: "127.0.0.1",
192
221
  };
222
+ // Internal state exposed for direct manipulation by safe response methods
223
+ // that need to bypass dd-trace interception of stream methods
193
224
  this._chunks = [];
225
+ this._ended = false; // Track ended state since writableEnded is lost after prototype change
194
226
  this._headers = new Map();
195
227
  this._headersSent = false;
196
228
  this._resolve = null;
229
+ // Mark as Lambda mock response for identification in expressHandler
230
+ this[JAYPIE_LAMBDA_MOCK] = true;
197
231
  // Initialize Node's internal kOutHeaders for dd-trace compatibility.
198
232
  // dd-trace patches HTTP methods and expects this internal state.
199
233
  if (kOutHeaders$1) {
200
234
  this[kOutHeaders$1] = Object.create(null);
201
235
  }
236
+ // CRITICAL: Define key methods as instance properties to survive Express's
237
+ // setPrototypeOf(res, app.response) in middleware/init.js which would
238
+ // otherwise replace our prototype with ServerResponse.prototype.
239
+ // Instance properties take precedence over prototype properties.
240
+ this.getHeader = this.getHeader.bind(this);
241
+ this.setHeader = this.setHeader.bind(this);
242
+ this.removeHeader = this.removeHeader.bind(this);
243
+ this.hasHeader = this.hasHeader.bind(this);
244
+ this.getHeaders = this.getHeaders.bind(this);
245
+ this.getHeaderNames = this.getHeaderNames.bind(this);
246
+ this.writeHead = this.writeHead.bind(this);
247
+ this.get = this.get.bind(this);
248
+ this.set = this.set.bind(this);
249
+ this.status = this.status.bind(this);
250
+ this.json = this.json.bind(this);
251
+ this.send = this.send.bind(this);
252
+ this.vary = this.vary.bind(this);
253
+ this.end = this.end.bind(this);
254
+ this.write = this.write.bind(this);
255
+ // Also bind internal Writable methods that are called via prototype chain
256
+ this._write = this._write.bind(this);
257
+ this._final = this._final.bind(this);
258
+ // Bind result-building methods
259
+ this.getResult = this.getResult.bind(this);
260
+ this.buildResult = this.buildResult.bind(this);
261
+ this.isBinaryContentType = this.isBinaryContentType.bind(this);
262
+ }
263
+ //
264
+ // Internal bypass methods - completely avoid prototype chain lookup
265
+ // These directly access _headers Map, safe from dd-trace interception
266
+ //
267
+ _internalGetHeader(name) {
268
+ const value = this._headers.get(name.toLowerCase());
269
+ return value ? String(value) : undefined;
270
+ }
271
+ _internalSetHeader(name, value) {
272
+ if (!this._headersSent) {
273
+ const lowerName = name.toLowerCase();
274
+ this._headers.set(lowerName, value);
275
+ // Also sync kOutHeaders for any code that expects it
276
+ if (kOutHeaders$1) {
277
+ const outHeaders = this[kOutHeaders$1];
278
+ if (outHeaders) {
279
+ outHeaders[lowerName] = [name, value];
280
+ }
281
+ }
282
+ }
283
+ }
284
+ _internalHasHeader(name) {
285
+ return this._headers.has(name.toLowerCase());
286
+ }
287
+ _internalRemoveHeader(name) {
288
+ if (!this._headersSent) {
289
+ const lowerName = name.toLowerCase();
290
+ this._headers.delete(lowerName);
291
+ if (kOutHeaders$1) {
292
+ const outHeaders = this[kOutHeaders$1];
293
+ if (outHeaders) {
294
+ delete outHeaders[lowerName];
295
+ }
296
+ }
297
+ }
202
298
  }
203
299
  //
204
300
  // Promise-based API for getting final result
205
301
  //
206
302
  getResult() {
207
303
  return new Promise((resolve) => {
208
- if (this.writableEnded) {
304
+ // Use _ended instead of writableEnded since Express's setPrototypeOf breaks the getter
305
+ if (this._ended) {
209
306
  resolve(this.buildResult());
210
307
  }
211
308
  else {
@@ -263,36 +360,40 @@ class LambdaResponseBuffered extends Writable {
263
360
  /**
264
361
  * Proxy for direct header access (e.g., res.headers['content-type']).
265
362
  * Required for compatibility with middleware like helmet that access headers directly.
363
+ * Uses direct _headers access to bypass dd-trace interception.
266
364
  */
267
365
  get headers() {
268
366
  return new Proxy({}, {
269
367
  deleteProperty: (_target, prop) => {
270
- this.removeHeader(String(prop));
368
+ this._headers.delete(String(prop).toLowerCase());
271
369
  return true;
272
370
  },
273
371
  get: (_target, prop) => {
274
372
  if (typeof prop === "symbol")
275
373
  return undefined;
276
- return this.getHeader(String(prop));
374
+ return this._headers.get(String(prop).toLowerCase());
277
375
  },
278
376
  getOwnPropertyDescriptor: (_target, prop) => {
279
- if (this.hasHeader(String(prop))) {
377
+ const lowerProp = String(prop).toLowerCase();
378
+ if (this._headers.has(lowerProp)) {
280
379
  return {
281
380
  configurable: true,
282
381
  enumerable: true,
283
- value: this.getHeader(String(prop)),
382
+ value: this._headers.get(lowerProp),
284
383
  };
285
384
  }
286
385
  return undefined;
287
386
  },
288
387
  has: (_target, prop) => {
289
- return this.hasHeader(String(prop));
388
+ return this._headers.has(String(prop).toLowerCase());
290
389
  },
291
390
  ownKeys: () => {
292
- return this.getHeaderNames();
391
+ return Array.from(this._headers.keys());
293
392
  },
294
393
  set: (_target, prop, value) => {
295
- this.setHeader(String(prop), value);
394
+ if (!this._headersSent) {
395
+ this._headers.set(String(prop).toLowerCase(), value);
396
+ }
296
397
  return true;
297
398
  },
298
399
  });
@@ -309,9 +410,10 @@ class LambdaResponseBuffered extends Writable {
309
410
  headersToSet = statusMessageOrHeaders;
310
411
  }
311
412
  if (headersToSet) {
413
+ // Use direct _headers access to bypass dd-trace interception
312
414
  for (const [key, value] of Object.entries(headersToSet)) {
313
415
  if (value !== undefined) {
314
- this.setHeader(key, value);
416
+ this._headers.set(key.toLowerCase(), String(value));
315
417
  }
316
418
  }
317
419
  }
@@ -348,7 +450,8 @@ class LambdaResponseBuffered extends Writable {
348
450
  return this;
349
451
  }
350
452
  json(data) {
351
- this.setHeader("content-type", "application/json");
453
+ // Use direct _headers access to bypass dd-trace interception
454
+ this._headers.set("content-type", "application/json");
352
455
  this.end(JSON.stringify(data));
353
456
  return this;
354
457
  }
@@ -362,11 +465,12 @@ class LambdaResponseBuffered extends Writable {
362
465
  /**
363
466
  * Add a field to the Vary response header.
364
467
  * Used by CORS middleware to indicate response varies by Origin.
468
+ * Uses direct _headers access to bypass dd-trace interception.
365
469
  */
366
470
  vary(field) {
367
- const existing = this.getHeader("vary");
471
+ const existing = this._headers.get("vary");
368
472
  if (!existing) {
369
- this.setHeader("vary", field);
473
+ this._headers.set("vary", field);
370
474
  }
371
475
  else {
372
476
  // Append to existing Vary header if field not already present
@@ -374,7 +478,7 @@ class LambdaResponseBuffered extends Writable {
374
478
  .split(",")
375
479
  .map((f) => f.trim().toLowerCase());
376
480
  if (!fields.includes(field.toLowerCase())) {
377
- this.setHeader("vary", `${existing}, ${field}`);
481
+ this._headers.set("vary", `${existing}, ${field}`);
378
482
  }
379
483
  }
380
484
  return this;
@@ -392,6 +496,7 @@ class LambdaResponseBuffered extends Writable {
392
496
  callback();
393
497
  }
394
498
  _final(callback) {
499
+ this._ended = true;
395
500
  if (this._resolve) {
396
501
  this._resolve(this.buildResult());
397
502
  }
@@ -402,7 +507,8 @@ class LambdaResponseBuffered extends Writable {
402
507
  //
403
508
  buildResult() {
404
509
  const body = Buffer.concat(this._chunks);
405
- const contentType = this.getHeader("content-type") || "";
510
+ // Use direct _headers access to bypass dd-trace interception
511
+ const contentType = this._headers.get("content-type") || "";
406
512
  // Determine if response should be base64 encoded
407
513
  const isBase64Encoded = this.isBinaryContentType(contentType);
408
514
  // Build headers object
@@ -464,6 +570,8 @@ class LambdaResponseStreaming extends Writable {
464
570
  this.socket = {
465
571
  remoteAddress: "127.0.0.1",
466
572
  };
573
+ // Internal state exposed for direct manipulation by safe response methods
574
+ // that need to bypass dd-trace interception
467
575
  this._headers = new Map();
468
576
  this._headersSent = false;
469
577
  this._pendingWrites = [];
@@ -474,6 +582,65 @@ class LambdaResponseStreaming extends Writable {
474
582
  if (kOutHeaders) {
475
583
  this[kOutHeaders] = Object.create(null);
476
584
  }
585
+ // CRITICAL: Define key methods as instance properties to survive Express's
586
+ // setPrototypeOf(res, app.response) in middleware/init.js which would
587
+ // otherwise replace our prototype with ServerResponse.prototype.
588
+ // Instance properties take precedence over prototype properties.
589
+ this.getHeader = this.getHeader.bind(this);
590
+ this.setHeader = this.setHeader.bind(this);
591
+ this.removeHeader = this.removeHeader.bind(this);
592
+ this.hasHeader = this.hasHeader.bind(this);
593
+ this.getHeaders = this.getHeaders.bind(this);
594
+ this.getHeaderNames = this.getHeaderNames.bind(this);
595
+ this.writeHead = this.writeHead.bind(this);
596
+ this.flushHeaders = this.flushHeaders.bind(this);
597
+ this.get = this.get.bind(this);
598
+ this.set = this.set.bind(this);
599
+ this.status = this.status.bind(this);
600
+ this.json = this.json.bind(this);
601
+ this.send = this.send.bind(this);
602
+ this.vary = this.vary.bind(this);
603
+ this.end = this.end.bind(this);
604
+ this.write = this.write.bind(this);
605
+ // Also bind internal Writable methods that are called via prototype chain
606
+ this._write = this._write.bind(this);
607
+ this._final = this._final.bind(this);
608
+ }
609
+ //
610
+ // Internal bypass methods - completely avoid prototype chain lookup
611
+ // These directly access _headers Map, safe from dd-trace interception
612
+ //
613
+ _internalGetHeader(name) {
614
+ const value = this._headers.get(name.toLowerCase());
615
+ return value ? String(value) : undefined;
616
+ }
617
+ _internalSetHeader(name, value) {
618
+ if (!this._headersSent) {
619
+ const lowerName = name.toLowerCase();
620
+ this._headers.set(lowerName, value);
621
+ // Also sync kOutHeaders for any code that expects it
622
+ if (kOutHeaders) {
623
+ const outHeaders = this[kOutHeaders];
624
+ if (outHeaders) {
625
+ outHeaders[lowerName] = [name, value];
626
+ }
627
+ }
628
+ }
629
+ }
630
+ _internalHasHeader(name) {
631
+ return this._headers.has(name.toLowerCase());
632
+ }
633
+ _internalRemoveHeader(name) {
634
+ if (!this._headersSent) {
635
+ const lowerName = name.toLowerCase();
636
+ this._headers.delete(lowerName);
637
+ if (kOutHeaders) {
638
+ const outHeaders = this[kOutHeaders];
639
+ if (outHeaders) {
640
+ delete outHeaders[lowerName];
641
+ }
642
+ }
643
+ }
477
644
  }
478
645
  //
479
646
  // Header management
@@ -528,36 +695,42 @@ class LambdaResponseStreaming extends Writable {
528
695
  /**
529
696
  * Proxy for direct header access (e.g., res.headers['content-type']).
530
697
  * Required for compatibility with middleware like helmet that access headers directly.
698
+ * Uses direct _headers access to bypass dd-trace interception.
531
699
  */
532
700
  get headers() {
533
701
  return new Proxy({}, {
534
702
  deleteProperty: (_target, prop) => {
535
- this.removeHeader(String(prop));
703
+ if (!this._headersSent) {
704
+ this._headers.delete(String(prop).toLowerCase());
705
+ }
536
706
  return true;
537
707
  },
538
708
  get: (_target, prop) => {
539
709
  if (typeof prop === "symbol")
540
710
  return undefined;
541
- return this.getHeader(String(prop));
711
+ return this._headers.get(String(prop).toLowerCase());
542
712
  },
543
713
  getOwnPropertyDescriptor: (_target, prop) => {
544
- if (this.hasHeader(String(prop))) {
714
+ const lowerProp = String(prop).toLowerCase();
715
+ if (this._headers.has(lowerProp)) {
545
716
  return {
546
717
  configurable: true,
547
718
  enumerable: true,
548
- value: this.getHeader(String(prop)),
719
+ value: this._headers.get(lowerProp),
549
720
  };
550
721
  }
551
722
  return undefined;
552
723
  },
553
724
  has: (_target, prop) => {
554
- return this.hasHeader(String(prop));
725
+ return this._headers.has(String(prop).toLowerCase());
555
726
  },
556
727
  ownKeys: () => {
557
- return this.getHeaderNames();
728
+ return Array.from(this._headers.keys());
558
729
  },
559
730
  set: (_target, prop, value) => {
560
- this.setHeader(String(prop), value);
731
+ if (!this._headersSent) {
732
+ this._headers.set(String(prop).toLowerCase(), value);
733
+ }
561
734
  return true;
562
735
  },
563
736
  });
@@ -577,9 +750,10 @@ class LambdaResponseStreaming extends Writable {
577
750
  headersToSet = statusMessageOrHeaders;
578
751
  }
579
752
  if (headersToSet) {
753
+ // Use direct _headers access to bypass dd-trace interception
580
754
  for (const [key, value] of Object.entries(headersToSet)) {
581
755
  if (value !== undefined) {
582
- this.setHeader(key, value);
756
+ this._headers.set(key.toLowerCase(), String(value));
583
757
  }
584
758
  }
585
759
  }
@@ -637,7 +811,8 @@ class LambdaResponseStreaming extends Writable {
637
811
  return this;
638
812
  }
639
813
  json(data) {
640
- this.setHeader("content-type", "application/json");
814
+ // Use direct _headers access to bypass dd-trace interception
815
+ this._headers.set("content-type", "application/json");
641
816
  this.end(JSON.stringify(data));
642
817
  return this;
643
818
  }
@@ -651,11 +826,12 @@ class LambdaResponseStreaming extends Writable {
651
826
  /**
652
827
  * Add a field to the Vary response header.
653
828
  * Used by CORS middleware to indicate response varies by Origin.
829
+ * Uses direct _headers access to bypass dd-trace interception.
654
830
  */
655
831
  vary(field) {
656
- const existing = this.getHeader("vary");
832
+ const existing = this._headers.get("vary");
657
833
  if (!existing) {
658
- this.setHeader("vary", field);
834
+ this._headers.set("vary", field);
659
835
  }
660
836
  else {
661
837
  // Append to existing Vary header if field not already present
@@ -663,7 +839,7 @@ class LambdaResponseStreaming extends Writable {
663
839
  .split(",")
664
840
  .map((f) => f.trim().toLowerCase());
665
841
  if (!fields.includes(field.toLowerCase())) {
666
- this.setHeader("vary", `${existing}, ${field}`);
842
+ this._headers.set("vary", `${existing}, ${field}`);
667
843
  }
668
844
  }
669
845
  return this;
@@ -761,6 +937,7 @@ function runExpressApp(app, req, res) {
761
937
  */
762
938
  function createLambdaHandler(app, _options) {
763
939
  return async (event, context) => {
940
+ let result;
764
941
  try {
765
942
  // Set current invoke for getCurrentInvokeUuid
766
943
  setCurrentInvoke(event, context);
@@ -770,8 +947,38 @@ function createLambdaHandler(app, _options) {
770
947
  const res = new LambdaResponseBuffered();
771
948
  // Run Express app
772
949
  await runExpressApp(app, req, res);
773
- // Return Lambda response
774
- return res.getResult();
950
+ // Get Lambda response - await explicitly to ensure we have the result
951
+ result = await res.getResult();
952
+ // Debug: Log the response before returning
953
+ console.log("[createLambdaHandler] Returning response:", JSON.stringify({
954
+ statusCode: result.statusCode,
955
+ headers: result.headers,
956
+ bodyLength: result.body?.length,
957
+ isBase64Encoded: result.isBase64Encoded,
958
+ }));
959
+ return result;
960
+ }
961
+ catch (error) {
962
+ // Log any unhandled errors
963
+ console.error("[createLambdaHandler] Unhandled error:", error);
964
+ if (error instanceof Error) {
965
+ console.error("[createLambdaHandler] Stack:", error.stack);
966
+ }
967
+ // Return a proper error response instead of throwing
968
+ return {
969
+ statusCode: 500,
970
+ headers: { "content-type": "application/json" },
971
+ body: JSON.stringify({
972
+ errors: [
973
+ {
974
+ status: 500,
975
+ title: "Internal Server Error",
976
+ detail: error instanceof Error ? error.message : "Unknown error occurred",
977
+ },
978
+ ],
979
+ }),
980
+ isBase64Encoded: false,
981
+ };
775
982
  }
776
983
  finally {
777
984
  // Clear current invoke context
@@ -909,7 +1116,7 @@ const corsHelper = (config = {}) => {
909
1116
  };
910
1117
  return expressCors(options);
911
1118
  };
912
- var cors = (config) => {
1119
+ var cors_helper = (config) => {
913
1120
  const cors = corsHelper(config);
914
1121
  return (req, res, next) => {
915
1122
  cors(req, res, (error) => {
@@ -924,154 +1131,10 @@ var cors = (config) => {
924
1131
  };
925
1132
  };
926
1133
 
927
- //
928
- //
929
- // Constants
930
- //
931
- const DEFAULT_PORT = 8080;
932
- //
933
- //
934
- // Main
935
- //
936
- /**
937
- * Creates and starts an Express server with standard Jaypie middleware.
938
- *
939
- * Features:
940
- * - CORS handling (configurable)
941
- * - JSON body parsing
942
- * - Listens on PORT env var (default 8080)
943
- *
944
- * Usage:
945
- * ```ts
946
- * import express from "express";
947
- * import { createServer, expressHandler } from "@jaypie/express";
948
- *
949
- * const app = express();
950
- *
951
- * app.get("/", expressHandler(async (req, res) => {
952
- * return { message: "Hello World" };
953
- * }));
954
- *
955
- * const { server, port } = await createServer(app);
956
- * console.log(`Server running on port ${port}`);
957
- * ```
958
- *
959
- * @param app - Express application instance
960
- * @param options - Server configuration options
961
- * @returns Promise resolving to server instance and port
962
- */
963
- async function createServer(app, options = {}) {
964
- const { cors: corsConfig, jsonLimit = "1mb", middleware = [], port: portOption, } = options;
965
- // Determine port
966
- const port = typeof portOption === "string"
967
- ? parseInt(portOption, 10)
968
- : (portOption ?? parseInt(process.env.PORT || String(DEFAULT_PORT), 10));
969
- // Apply CORS middleware (unless explicitly disabled)
970
- if (corsConfig !== false) {
971
- app.use(cors(corsConfig));
972
- }
973
- // Apply JSON body parser
974
- // Note: We use dynamic import to avoid requiring express as a direct dependency
975
- const express = await import('express');
976
- app.use(express.json({ limit: jsonLimit }));
977
- // Apply additional middleware
978
- for (const mw of middleware) {
979
- app.use(mw);
980
- }
981
- // Start server
982
- return new Promise((resolve, reject) => {
983
- try {
984
- const server = app.listen(port, () => {
985
- // Get the actual port (important when port 0 is passed to get an ephemeral port)
986
- const address = server.address();
987
- const actualPort = address?.port ?? port;
988
- log.info(`Server listening on port ${actualPort}`);
989
- resolve({ port: actualPort, server });
990
- });
991
- server.on("error", (error) => {
992
- log.error("Server error", { error });
993
- reject(error);
994
- });
995
- }
996
- catch (error) {
997
- reject(error);
998
- }
999
- });
1000
- }
1001
-
1002
- //
1003
- //
1004
- // Constants
1005
- //
1006
- const HEADER_AMZN_REQUEST_ID$1 = "x-amzn-request-id";
1007
- const ENV_AMZN_TRACE_ID = "_X_AMZN_TRACE_ID";
1008
1134
  //
1009
1135
  //
1010
1136
  // Helper Functions
1011
1137
  //
1012
- /**
1013
- * Extract request ID from X-Ray trace ID environment variable
1014
- * Format: Root=1-5e6b4a90-example;Parent=example;Sampled=1
1015
- * We extract the trace ID from the Root segment
1016
- */
1017
- function parseTraceId(traceId) {
1018
- if (!traceId)
1019
- return undefined;
1020
- // Extract the Root segment (format: Root=1-{timestamp}-{uuid})
1021
- const rootMatch = traceId.match(/Root=([^;]+)/);
1022
- if (rootMatch && rootMatch[1]) {
1023
- return rootMatch[1];
1024
- }
1025
- return undefined;
1026
- }
1027
- //
1028
- //
1029
- // Main
1030
- //
1031
- /**
1032
- * Get the current invoke UUID from Lambda Web Adapter context.
1033
- * This function extracts the request ID from either:
1034
- * 1. The x-amzn-request-id header (set by Lambda Web Adapter)
1035
- * 2. The _X_AMZN_TRACE_ID environment variable (set by Lambda runtime)
1036
- *
1037
- * @param req - Optional Express request object to extract headers from
1038
- * @returns The AWS request ID or undefined if not in Lambda context
1039
- */
1040
- function getWebAdapterUuid(req) {
1041
- // First, try to get from request headers
1042
- if (req && req.headers) {
1043
- const headerValue = req.headers[HEADER_AMZN_REQUEST_ID$1];
1044
- if (headerValue) {
1045
- return Array.isArray(headerValue) ? headerValue[0] : headerValue;
1046
- }
1047
- }
1048
- // Fall back to environment variable (X-Ray trace ID)
1049
- const traceId = process.env[ENV_AMZN_TRACE_ID];
1050
- if (traceId) {
1051
- return parseTraceId(traceId);
1052
- }
1053
- return undefined;
1054
- }
1055
-
1056
- //
1057
- //
1058
- // Constants
1059
- //
1060
- const HEADER_AMZN_REQUEST_ID = "x-amzn-request-id";
1061
- //
1062
- //
1063
- // Helper Functions
1064
- //
1065
- /**
1066
- * Detect if we're running in Lambda Web Adapter mode.
1067
- * Web Adapter sets the x-amzn-request-id header on requests.
1068
- */
1069
- function isWebAdapterMode(req) {
1070
- if (req && req.headers && req.headers[HEADER_AMZN_REQUEST_ID]) {
1071
- return true;
1072
- }
1073
- return false;
1074
- }
1075
1138
  /**
1076
1139
  * Get UUID from Jaypie Lambda adapter context.
1077
1140
  * This is set by createLambdaHandler/createLambdaStreamHandler.
@@ -1088,8 +1151,12 @@ function getJaypieAdapterUuid() {
1088
1151
  * The Jaypie adapter attaches _lambdaContext to the request.
1089
1152
  */
1090
1153
  function getRequestContextUuid(req) {
1091
- if (req && req._lambdaContext?.awsRequestId) {
1092
- return req._lambdaContext.awsRequestId;
1154
+ if (req && req._lambdaContext) {
1155
+ const lambdaContext = req
1156
+ ._lambdaContext;
1157
+ if (lambdaContext.awsRequestId) {
1158
+ return lambdaContext.awsRequestId;
1159
+ }
1093
1160
  }
1094
1161
  return undefined;
1095
1162
  }
@@ -1099,29 +1166,20 @@ function getRequestContextUuid(req) {
1099
1166
  //
1100
1167
  /**
1101
1168
  * Get the current invoke UUID from Lambda context.
1102
- * Works with Jaypie Lambda adapter and Lambda Web Adapter mode.
1169
+ * Works with Jaypie Lambda adapter (createLambdaHandler/createLambdaStreamHandler).
1103
1170
  *
1104
1171
  * @param req - Optional Express request object. Used to extract context
1105
- * from Web Adapter headers or Jaypie adapter's _lambdaContext.
1172
+ * from Jaypie adapter's _lambdaContext.
1106
1173
  * @returns The AWS request ID or undefined if not in Lambda context
1107
1174
  */
1108
1175
  function getCurrentInvokeUuid(req) {
1109
- // Priority 1: Web Adapter mode (header-based)
1110
- if (isWebAdapterMode(req)) {
1111
- return getWebAdapterUuid(req);
1112
- }
1113
- // Priority 2: Request has Lambda context attached (Jaypie adapter)
1176
+ // Priority 1: Request has Lambda context attached (Jaypie adapter)
1114
1177
  const requestContextUuid = getRequestContextUuid(req);
1115
1178
  if (requestContextUuid) {
1116
1179
  return requestContextUuid;
1117
1180
  }
1118
- // Priority 3: Global context from Jaypie adapter
1119
- const jaypieAdapterUuid = getJaypieAdapterUuid();
1120
- if (jaypieAdapterUuid) {
1121
- return jaypieAdapterUuid;
1122
- }
1123
- // Fallback: Web Adapter env var
1124
- return getWebAdapterUuid();
1181
+ // Priority 2: Global context from Jaypie adapter
1182
+ return getJaypieAdapterUuid();
1125
1183
  }
1126
1184
 
1127
1185
  //
@@ -1135,7 +1193,11 @@ function getCurrentInvokeUuid(req) {
1135
1193
  */
1136
1194
  function safeGetHeader(res, name) {
1137
1195
  try {
1138
- // Try direct _headers access first (Lambda adapter, avoids dd-trace)
1196
+ // Try internal method first (completely bypasses dd-trace)
1197
+ if (typeof res._internalGetHeader === "function") {
1198
+ return res._internalGetHeader(name);
1199
+ }
1200
+ // Fall back to _headers Map access (Lambda adapter, avoids dd-trace)
1139
1201
  if (res._headers instanceof Map) {
1140
1202
  const value = res._headers.get(name.toLowerCase());
1141
1203
  return value ? String(value) : undefined;
@@ -1163,7 +1225,12 @@ function safeGetHeader(res, name) {
1163
1225
  */
1164
1226
  function safeSetHeader(res, name, value) {
1165
1227
  try {
1166
- // Try direct _headers access first (Lambda adapter, avoids dd-trace)
1228
+ // Try internal method first (completely bypasses dd-trace)
1229
+ if (typeof res._internalSetHeader === "function") {
1230
+ res._internalSetHeader(name, value);
1231
+ return;
1232
+ }
1233
+ // Fall back to _headers Map access (Lambda adapter, avoids dd-trace)
1167
1234
  if (res._headers instanceof Map) {
1168
1235
  res._headers.set(name.toLowerCase(), value);
1169
1236
  return;
@@ -1293,21 +1360,41 @@ const logger$1 = log;
1293
1360
  // Helpers - Safe response methods to bypass dd-trace interception
1294
1361
  //
1295
1362
  /**
1296
- * Check if response is a Lambda mock response with direct _headers access.
1363
+ * Check if response is a Lambda mock response with direct internal access.
1364
+ * Uses Symbol marker to survive prototype chain modifications from Express and dd-trace.
1297
1365
  */
1298
1366
  function isLambdaMockResponse(res) {
1299
- return res._headers instanceof Map;
1367
+ return res[JAYPIE_LAMBDA_MOCK] === true;
1300
1368
  }
1301
1369
  /**
1302
1370
  * Safely send a JSON response, avoiding dd-trace interception.
1303
- * For Lambda mock responses, directly sets headers and writes to stream.
1371
+ * For Lambda mock responses, directly manipulates internal state instead of
1372
+ * using stream methods (write/end) which dd-trace intercepts.
1304
1373
  */
1305
1374
  function safeSendJson(res, statusCode, data) {
1306
1375
  if (isLambdaMockResponse(res)) {
1307
- // Direct access for Lambda mock responses - bypasses dd-trace
1308
- res._headers.set("content-type", "application/json");
1376
+ // Use internal method to set header (completely bypasses dd-trace)
1377
+ if (typeof res._internalSetHeader === "function") {
1378
+ res._internalSetHeader("content-type", "application/json");
1379
+ }
1380
+ else {
1381
+ // Fall back to direct _headers manipulation
1382
+ res._headers.set("content-type", "application/json");
1383
+ }
1309
1384
  res.statusCode = statusCode;
1310
- res.end(JSON.stringify(data));
1385
+ // Directly push to chunks array instead of using stream write/end
1386
+ const chunk = Buffer.from(JSON.stringify(data));
1387
+ res._chunks.push(chunk);
1388
+ res._headersSent = true;
1389
+ // Mark as ended so getResult() resolves immediately
1390
+ res._ended = true;
1391
+ // Signal completion if a promise is waiting
1392
+ if (res._resolve) {
1393
+ res._resolve(res.buildResult());
1394
+ }
1395
+ // Emit "finish" event so runExpressApp's promise resolves
1396
+ console.log("[safeSendJson] Emitting finish event");
1397
+ res.emit("finish");
1311
1398
  return;
1312
1399
  }
1313
1400
  // Fall back to standard Express methods for real responses
@@ -1315,18 +1402,27 @@ function safeSendJson(res, statusCode, data) {
1315
1402
  }
1316
1403
  /**
1317
1404
  * Safely send a response body, avoiding dd-trace interception.
1318
- * For Lambda mock responses, directly writes to stream.
1405
+ * For Lambda mock responses, directly manipulates internal state instead of
1406
+ * using stream methods (write/end) which dd-trace intercepts.
1319
1407
  */
1320
1408
  function safeSend(res, statusCode, body) {
1321
1409
  if (isLambdaMockResponse(res)) {
1322
- // Direct access for Lambda mock responses - bypasses dd-trace
1410
+ // Direct internal state manipulation - bypasses dd-trace completely
1323
1411
  res.statusCode = statusCode;
1324
1412
  if (body !== undefined) {
1325
- res.end(body);
1326
- }
1327
- else {
1328
- res.end();
1329
- }
1413
+ const chunk = Buffer.from(body);
1414
+ res._chunks.push(chunk);
1415
+ }
1416
+ res._headersSent = true;
1417
+ // Mark as ended so getResult() resolves immediately
1418
+ res._ended = true;
1419
+ // Signal completion if a promise is waiting
1420
+ if (res._resolve) {
1421
+ res._resolve(res.buildResult());
1422
+ }
1423
+ // Emit "finish" event so runExpressApp's promise resolves
1424
+ console.log("[safeSend] Emitting finish event");
1425
+ res.emit("finish");
1330
1426
  return;
1331
1427
  }
1332
1428
  // Fall back to standard Express methods for real responses
@@ -1593,7 +1689,18 @@ function expressHandler(handlerOrOptions, optionsOrHandler) {
1593
1689
  }
1594
1690
  }
1595
1691
  catch (error) {
1596
- log.fatal("Express encountered an error while sending the response");
1692
+ // Use console.error for raw stack trace to ensure it appears in CloudWatch
1693
+ // Handle both Error objects and plain thrown values
1694
+ const errorMessage = error instanceof Error
1695
+ ? error.message
1696
+ : typeof error === "object" && error !== null
1697
+ ? JSON.stringify(error)
1698
+ : String(error);
1699
+ const errorStack = error instanceof Error
1700
+ ? error.stack
1701
+ : new Error("Stack trace").stack?.replace("Error: Stack trace", `Error: ${errorMessage}`);
1702
+ console.error("Express response error stack trace:", errorStack);
1703
+ log.fatal(`Express encountered an error while sending the response: ${errorMessage}`);
1597
1704
  log.var({ responseError: error });
1598
1705
  }
1599
1706
  // Log response
@@ -1693,14 +1800,13 @@ const logger = log;
1693
1800
  // Helper
1694
1801
  //
1695
1802
  /**
1696
- * Format an error as an SSE error event
1803
+ * Get error body from an error
1697
1804
  */
1698
- function formatErrorSSE(error) {
1805
+ function getErrorBody(error) {
1699
1806
  const isJaypieError = error.isProjectError;
1700
- const body = isJaypieError
1807
+ return isJaypieError
1701
1808
  ? (error.body?.() ?? { error: error.message })
1702
1809
  : new UnhandledError().body();
1703
- return `event: error\ndata: ${JSON.stringify(body)}\n\n`;
1704
1810
  }
1705
1811
  function expressStreamHandler(handlerOrOptions, optionsOrHandler) {
1706
1812
  /* eslint-enable no-redeclare */
@@ -1720,7 +1826,8 @@ function expressStreamHandler(handlerOrOptions, optionsOrHandler) {
1720
1826
  //
1721
1827
  // Validate
1722
1828
  //
1723
- let { chaos, contentType = "text/event-stream", locals, name, secrets, setup = [], teardown = [], unavailable, validate, } = options;
1829
+ const format = options.format ?? "sse";
1830
+ let { chaos, contentType = getContentTypeForFormat(format), locals, name, secrets, setup = [], teardown = [], unavailable, validate, } = options;
1724
1831
  if (typeof handler !== "function") {
1725
1832
  throw new BadRequestError(`Argument "${handler}" doesn't match type "function"`);
1726
1833
  }
@@ -1858,9 +1965,10 @@ function expressStreamHandler(handlerOrOptions, optionsOrHandler) {
1858
1965
  log.fatal("Caught unhandled error in stream handler");
1859
1966
  log.info.var({ unhandledError: error.message });
1860
1967
  }
1861
- // Write error as SSE event
1968
+ // Write error in the appropriate format
1862
1969
  try {
1863
- res.write(formatErrorSSE(error));
1970
+ const errorBody = getErrorBody(error);
1971
+ res.write(formatStreamError(errorBody, format));
1864
1972
  }
1865
1973
  catch {
1866
1974
  // Response may already be closed
@@ -1969,5 +2077,5 @@ const noContentRoute = routes.noContentRoute;
1969
2077
  const notFoundRoute = routes.notFoundRoute;
1970
2078
  const notImplementedRoute = routes.notImplementedRoute;
1971
2079
 
1972
- export { EXPRESS, LambdaRequest, LambdaResponseBuffered, LambdaResponseStreaming, badRequestRoute, cors, createLambdaHandler, createLambdaStreamHandler, createServer, echoRoute, expressHandler, httpHandler as expressHttpCodeHandler, expressStreamHandler, forbiddenRoute, getCurrentInvoke, getCurrentInvokeUuid, goneRoute, methodNotAllowedRoute, noContentRoute, notFoundRoute, notImplementedRoute };
2080
+ export { EXPRESS, LambdaRequest, LambdaResponseBuffered, LambdaResponseStreaming, badRequestRoute, cors_helper as cors, createLambdaHandler, createLambdaStreamHandler, echoRoute, expressHandler, httpHandler as expressHttpCodeHandler, expressStreamHandler, forbiddenRoute, getCurrentInvoke, getCurrentInvokeUuid, goneRoute, methodNotAllowedRoute, noContentRoute, notFoundRoute, notImplementedRoute };
1973
2081
  //# sourceMappingURL=index.js.map