@jaypie/express 1.2.4-rc9 → 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.
@@ -193,21 +222,87 @@ class LambdaResponseBuffered extends Writable {
193
222
  // Internal state exposed for direct manipulation by safe response methods
194
223
  // that need to bypass dd-trace interception of stream methods
195
224
  this._chunks = [];
225
+ this._ended = false; // Track ended state since writableEnded is lost after prototype change
196
226
  this._headers = new Map();
197
227
  this._headersSent = false;
198
228
  this._resolve = null;
229
+ // Mark as Lambda mock response for identification in expressHandler
230
+ this[JAYPIE_LAMBDA_MOCK] = true;
199
231
  // Initialize Node's internal kOutHeaders for dd-trace compatibility.
200
232
  // dd-trace patches HTTP methods and expects this internal state.
201
233
  if (kOutHeaders$1) {
202
234
  this[kOutHeaders$1] = Object.create(null);
203
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
+ }
204
298
  }
205
299
  //
206
300
  // Promise-based API for getting final result
207
301
  //
208
302
  getResult() {
209
303
  return new Promise((resolve) => {
210
- if (this.writableEnded) {
304
+ // Use _ended instead of writableEnded since Express's setPrototypeOf breaks the getter
305
+ if (this._ended) {
211
306
  resolve(this.buildResult());
212
307
  }
213
308
  else {
@@ -265,36 +360,40 @@ class LambdaResponseBuffered extends Writable {
265
360
  /**
266
361
  * Proxy for direct header access (e.g., res.headers['content-type']).
267
362
  * Required for compatibility with middleware like helmet that access headers directly.
363
+ * Uses direct _headers access to bypass dd-trace interception.
268
364
  */
269
365
  get headers() {
270
366
  return new Proxy({}, {
271
367
  deleteProperty: (_target, prop) => {
272
- this.removeHeader(String(prop));
368
+ this._headers.delete(String(prop).toLowerCase());
273
369
  return true;
274
370
  },
275
371
  get: (_target, prop) => {
276
372
  if (typeof prop === "symbol")
277
373
  return undefined;
278
- return this.getHeader(String(prop));
374
+ return this._headers.get(String(prop).toLowerCase());
279
375
  },
280
376
  getOwnPropertyDescriptor: (_target, prop) => {
281
- if (this.hasHeader(String(prop))) {
377
+ const lowerProp = String(prop).toLowerCase();
378
+ if (this._headers.has(lowerProp)) {
282
379
  return {
283
380
  configurable: true,
284
381
  enumerable: true,
285
- value: this.getHeader(String(prop)),
382
+ value: this._headers.get(lowerProp),
286
383
  };
287
384
  }
288
385
  return undefined;
289
386
  },
290
387
  has: (_target, prop) => {
291
- return this.hasHeader(String(prop));
388
+ return this._headers.has(String(prop).toLowerCase());
292
389
  },
293
390
  ownKeys: () => {
294
- return this.getHeaderNames();
391
+ return Array.from(this._headers.keys());
295
392
  },
296
393
  set: (_target, prop, value) => {
297
- this.setHeader(String(prop), value);
394
+ if (!this._headersSent) {
395
+ this._headers.set(String(prop).toLowerCase(), value);
396
+ }
298
397
  return true;
299
398
  },
300
399
  });
@@ -311,9 +410,10 @@ class LambdaResponseBuffered extends Writable {
311
410
  headersToSet = statusMessageOrHeaders;
312
411
  }
313
412
  if (headersToSet) {
413
+ // Use direct _headers access to bypass dd-trace interception
314
414
  for (const [key, value] of Object.entries(headersToSet)) {
315
415
  if (value !== undefined) {
316
- this.setHeader(key, value);
416
+ this._headers.set(key.toLowerCase(), String(value));
317
417
  }
318
418
  }
319
419
  }
@@ -350,7 +450,8 @@ class LambdaResponseBuffered extends Writable {
350
450
  return this;
351
451
  }
352
452
  json(data) {
353
- this.setHeader("content-type", "application/json");
453
+ // Use direct _headers access to bypass dd-trace interception
454
+ this._headers.set("content-type", "application/json");
354
455
  this.end(JSON.stringify(data));
355
456
  return this;
356
457
  }
@@ -364,11 +465,12 @@ class LambdaResponseBuffered extends Writable {
364
465
  /**
365
466
  * Add a field to the Vary response header.
366
467
  * Used by CORS middleware to indicate response varies by Origin.
468
+ * Uses direct _headers access to bypass dd-trace interception.
367
469
  */
368
470
  vary(field) {
369
- const existing = this.getHeader("vary");
471
+ const existing = this._headers.get("vary");
370
472
  if (!existing) {
371
- this.setHeader("vary", field);
473
+ this._headers.set("vary", field);
372
474
  }
373
475
  else {
374
476
  // Append to existing Vary header if field not already present
@@ -376,7 +478,7 @@ class LambdaResponseBuffered extends Writable {
376
478
  .split(",")
377
479
  .map((f) => f.trim().toLowerCase());
378
480
  if (!fields.includes(field.toLowerCase())) {
379
- this.setHeader("vary", `${existing}, ${field}`);
481
+ this._headers.set("vary", `${existing}, ${field}`);
380
482
  }
381
483
  }
382
484
  return this;
@@ -394,6 +496,7 @@ class LambdaResponseBuffered extends Writable {
394
496
  callback();
395
497
  }
396
498
  _final(callback) {
499
+ this._ended = true;
397
500
  if (this._resolve) {
398
501
  this._resolve(this.buildResult());
399
502
  }
@@ -404,7 +507,8 @@ class LambdaResponseBuffered extends Writable {
404
507
  //
405
508
  buildResult() {
406
509
  const body = Buffer.concat(this._chunks);
407
- const contentType = this.getHeader("content-type") || "";
510
+ // Use direct _headers access to bypass dd-trace interception
511
+ const contentType = this._headers.get("content-type") || "";
408
512
  // Determine if response should be base64 encoded
409
513
  const isBase64Encoded = this.isBinaryContentType(contentType);
410
514
  // Build headers object
@@ -466,6 +570,8 @@ class LambdaResponseStreaming extends Writable {
466
570
  this.socket = {
467
571
  remoteAddress: "127.0.0.1",
468
572
  };
573
+ // Internal state exposed for direct manipulation by safe response methods
574
+ // that need to bypass dd-trace interception
469
575
  this._headers = new Map();
470
576
  this._headersSent = false;
471
577
  this._pendingWrites = [];
@@ -476,6 +582,65 @@ class LambdaResponseStreaming extends Writable {
476
582
  if (kOutHeaders) {
477
583
  this[kOutHeaders] = Object.create(null);
478
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
+ }
479
644
  }
480
645
  //
481
646
  // Header management
@@ -530,36 +695,42 @@ class LambdaResponseStreaming extends Writable {
530
695
  /**
531
696
  * Proxy for direct header access (e.g., res.headers['content-type']).
532
697
  * Required for compatibility with middleware like helmet that access headers directly.
698
+ * Uses direct _headers access to bypass dd-trace interception.
533
699
  */
534
700
  get headers() {
535
701
  return new Proxy({}, {
536
702
  deleteProperty: (_target, prop) => {
537
- this.removeHeader(String(prop));
703
+ if (!this._headersSent) {
704
+ this._headers.delete(String(prop).toLowerCase());
705
+ }
538
706
  return true;
539
707
  },
540
708
  get: (_target, prop) => {
541
709
  if (typeof prop === "symbol")
542
710
  return undefined;
543
- return this.getHeader(String(prop));
711
+ return this._headers.get(String(prop).toLowerCase());
544
712
  },
545
713
  getOwnPropertyDescriptor: (_target, prop) => {
546
- if (this.hasHeader(String(prop))) {
714
+ const lowerProp = String(prop).toLowerCase();
715
+ if (this._headers.has(lowerProp)) {
547
716
  return {
548
717
  configurable: true,
549
718
  enumerable: true,
550
- value: this.getHeader(String(prop)),
719
+ value: this._headers.get(lowerProp),
551
720
  };
552
721
  }
553
722
  return undefined;
554
723
  },
555
724
  has: (_target, prop) => {
556
- return this.hasHeader(String(prop));
725
+ return this._headers.has(String(prop).toLowerCase());
557
726
  },
558
727
  ownKeys: () => {
559
- return this.getHeaderNames();
728
+ return Array.from(this._headers.keys());
560
729
  },
561
730
  set: (_target, prop, value) => {
562
- this.setHeader(String(prop), value);
731
+ if (!this._headersSent) {
732
+ this._headers.set(String(prop).toLowerCase(), value);
733
+ }
563
734
  return true;
564
735
  },
565
736
  });
@@ -579,9 +750,10 @@ class LambdaResponseStreaming extends Writable {
579
750
  headersToSet = statusMessageOrHeaders;
580
751
  }
581
752
  if (headersToSet) {
753
+ // Use direct _headers access to bypass dd-trace interception
582
754
  for (const [key, value] of Object.entries(headersToSet)) {
583
755
  if (value !== undefined) {
584
- this.setHeader(key, value);
756
+ this._headers.set(key.toLowerCase(), String(value));
585
757
  }
586
758
  }
587
759
  }
@@ -639,7 +811,8 @@ class LambdaResponseStreaming extends Writable {
639
811
  return this;
640
812
  }
641
813
  json(data) {
642
- this.setHeader("content-type", "application/json");
814
+ // Use direct _headers access to bypass dd-trace interception
815
+ this._headers.set("content-type", "application/json");
643
816
  this.end(JSON.stringify(data));
644
817
  return this;
645
818
  }
@@ -653,11 +826,12 @@ class LambdaResponseStreaming extends Writable {
653
826
  /**
654
827
  * Add a field to the Vary response header.
655
828
  * Used by CORS middleware to indicate response varies by Origin.
829
+ * Uses direct _headers access to bypass dd-trace interception.
656
830
  */
657
831
  vary(field) {
658
- const existing = this.getHeader("vary");
832
+ const existing = this._headers.get("vary");
659
833
  if (!existing) {
660
- this.setHeader("vary", field);
834
+ this._headers.set("vary", field);
661
835
  }
662
836
  else {
663
837
  // Append to existing Vary header if field not already present
@@ -665,7 +839,7 @@ class LambdaResponseStreaming extends Writable {
665
839
  .split(",")
666
840
  .map((f) => f.trim().toLowerCase());
667
841
  if (!fields.includes(field.toLowerCase())) {
668
- this.setHeader("vary", `${existing}, ${field}`);
842
+ this._headers.set("vary", `${existing}, ${field}`);
669
843
  }
670
844
  }
671
845
  return this;
@@ -763,6 +937,7 @@ function runExpressApp(app, req, res) {
763
937
  */
764
938
  function createLambdaHandler(app, _options) {
765
939
  return async (event, context) => {
940
+ let result;
766
941
  try {
767
942
  // Set current invoke for getCurrentInvokeUuid
768
943
  setCurrentInvoke(event, context);
@@ -772,8 +947,38 @@ function createLambdaHandler(app, _options) {
772
947
  const res = new LambdaResponseBuffered();
773
948
  // Run Express app
774
949
  await runExpressApp(app, req, res);
775
- // Return Lambda response
776
- 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
+ };
777
982
  }
778
983
  finally {
779
984
  // Clear current invoke context
@@ -911,7 +1116,7 @@ const corsHelper = (config = {}) => {
911
1116
  };
912
1117
  return expressCors(options);
913
1118
  };
914
- var cors = (config) => {
1119
+ var cors_helper = (config) => {
915
1120
  const cors = corsHelper(config);
916
1121
  return (req, res, next) => {
917
1122
  cors(req, res, (error) => {
@@ -926,154 +1131,10 @@ var cors = (config) => {
926
1131
  };
927
1132
  };
928
1133
 
929
- //
930
- //
931
- // Constants
932
- //
933
- const DEFAULT_PORT = 8080;
934
- //
935
- //
936
- // Main
937
- //
938
- /**
939
- * Creates and starts an Express server with standard Jaypie middleware.
940
- *
941
- * Features:
942
- * - CORS handling (configurable)
943
- * - JSON body parsing
944
- * - Listens on PORT env var (default 8080)
945
- *
946
- * Usage:
947
- * ```ts
948
- * import express from "express";
949
- * import { createServer, expressHandler } from "@jaypie/express";
950
- *
951
- * const app = express();
952
- *
953
- * app.get("/", expressHandler(async (req, res) => {
954
- * return { message: "Hello World" };
955
- * }));
956
- *
957
- * const { server, port } = await createServer(app);
958
- * console.log(`Server running on port ${port}`);
959
- * ```
960
- *
961
- * @param app - Express application instance
962
- * @param options - Server configuration options
963
- * @returns Promise resolving to server instance and port
964
- */
965
- async function createServer(app, options = {}) {
966
- const { cors: corsConfig, jsonLimit = "1mb", middleware = [], port: portOption, } = options;
967
- // Determine port
968
- const port = typeof portOption === "string"
969
- ? parseInt(portOption, 10)
970
- : (portOption ?? parseInt(process.env.PORT || String(DEFAULT_PORT), 10));
971
- // Apply CORS middleware (unless explicitly disabled)
972
- if (corsConfig !== false) {
973
- app.use(cors(corsConfig));
974
- }
975
- // Apply JSON body parser
976
- // Note: We use dynamic import to avoid requiring express as a direct dependency
977
- const express = await import('express');
978
- app.use(express.json({ limit: jsonLimit }));
979
- // Apply additional middleware
980
- for (const mw of middleware) {
981
- app.use(mw);
982
- }
983
- // Start server
984
- return new Promise((resolve, reject) => {
985
- try {
986
- const server = app.listen(port, () => {
987
- // Get the actual port (important when port 0 is passed to get an ephemeral port)
988
- const address = server.address();
989
- const actualPort = address?.port ?? port;
990
- log.info(`Server listening on port ${actualPort}`);
991
- resolve({ port: actualPort, server });
992
- });
993
- server.on("error", (error) => {
994
- log.error("Server error", { error });
995
- reject(error);
996
- });
997
- }
998
- catch (error) {
999
- reject(error);
1000
- }
1001
- });
1002
- }
1003
-
1004
- //
1005
- //
1006
- // Constants
1007
- //
1008
- const HEADER_AMZN_REQUEST_ID$1 = "x-amzn-request-id";
1009
- const ENV_AMZN_TRACE_ID = "_X_AMZN_TRACE_ID";
1010
1134
  //
1011
1135
  //
1012
1136
  // Helper Functions
1013
1137
  //
1014
- /**
1015
- * Extract request ID from X-Ray trace ID environment variable
1016
- * Format: Root=1-5e6b4a90-example;Parent=example;Sampled=1
1017
- * We extract the trace ID from the Root segment
1018
- */
1019
- function parseTraceId(traceId) {
1020
- if (!traceId)
1021
- return undefined;
1022
- // Extract the Root segment (format: Root=1-{timestamp}-{uuid})
1023
- const rootMatch = traceId.match(/Root=([^;]+)/);
1024
- if (rootMatch && rootMatch[1]) {
1025
- return rootMatch[1];
1026
- }
1027
- return undefined;
1028
- }
1029
- //
1030
- //
1031
- // Main
1032
- //
1033
- /**
1034
- * Get the current invoke UUID from Lambda Web Adapter context.
1035
- * This function extracts the request ID from either:
1036
- * 1. The x-amzn-request-id header (set by Lambda Web Adapter)
1037
- * 2. The _X_AMZN_TRACE_ID environment variable (set by Lambda runtime)
1038
- *
1039
- * @param req - Optional Express request object to extract headers from
1040
- * @returns The AWS request ID or undefined if not in Lambda context
1041
- */
1042
- function getWebAdapterUuid(req) {
1043
- // First, try to get from request headers
1044
- if (req && req.headers) {
1045
- const headerValue = req.headers[HEADER_AMZN_REQUEST_ID$1];
1046
- if (headerValue) {
1047
- return Array.isArray(headerValue) ? headerValue[0] : headerValue;
1048
- }
1049
- }
1050
- // Fall back to environment variable (X-Ray trace ID)
1051
- const traceId = process.env[ENV_AMZN_TRACE_ID];
1052
- if (traceId) {
1053
- return parseTraceId(traceId);
1054
- }
1055
- return undefined;
1056
- }
1057
-
1058
- //
1059
- //
1060
- // Constants
1061
- //
1062
- const HEADER_AMZN_REQUEST_ID = "x-amzn-request-id";
1063
- //
1064
- //
1065
- // Helper Functions
1066
- //
1067
- /**
1068
- * Detect if we're running in Lambda Web Adapter mode.
1069
- * Web Adapter sets the x-amzn-request-id header on requests.
1070
- */
1071
- function isWebAdapterMode(req) {
1072
- if (req && req.headers && req.headers[HEADER_AMZN_REQUEST_ID]) {
1073
- return true;
1074
- }
1075
- return false;
1076
- }
1077
1138
  /**
1078
1139
  * Get UUID from Jaypie Lambda adapter context.
1079
1140
  * This is set by createLambdaHandler/createLambdaStreamHandler.
@@ -1090,8 +1151,12 @@ function getJaypieAdapterUuid() {
1090
1151
  * The Jaypie adapter attaches _lambdaContext to the request.
1091
1152
  */
1092
1153
  function getRequestContextUuid(req) {
1093
- if (req && req._lambdaContext?.awsRequestId) {
1094
- return req._lambdaContext.awsRequestId;
1154
+ if (req && req._lambdaContext) {
1155
+ const lambdaContext = req
1156
+ ._lambdaContext;
1157
+ if (lambdaContext.awsRequestId) {
1158
+ return lambdaContext.awsRequestId;
1159
+ }
1095
1160
  }
1096
1161
  return undefined;
1097
1162
  }
@@ -1101,29 +1166,20 @@ function getRequestContextUuid(req) {
1101
1166
  //
1102
1167
  /**
1103
1168
  * Get the current invoke UUID from Lambda context.
1104
- * Works with Jaypie Lambda adapter and Lambda Web Adapter mode.
1169
+ * Works with Jaypie Lambda adapter (createLambdaHandler/createLambdaStreamHandler).
1105
1170
  *
1106
1171
  * @param req - Optional Express request object. Used to extract context
1107
- * from Web Adapter headers or Jaypie adapter's _lambdaContext.
1172
+ * from Jaypie adapter's _lambdaContext.
1108
1173
  * @returns The AWS request ID or undefined if not in Lambda context
1109
1174
  */
1110
1175
  function getCurrentInvokeUuid(req) {
1111
- // Priority 1: Web Adapter mode (header-based)
1112
- if (isWebAdapterMode(req)) {
1113
- return getWebAdapterUuid(req);
1114
- }
1115
- // Priority 2: Request has Lambda context attached (Jaypie adapter)
1176
+ // Priority 1: Request has Lambda context attached (Jaypie adapter)
1116
1177
  const requestContextUuid = getRequestContextUuid(req);
1117
1178
  if (requestContextUuid) {
1118
1179
  return requestContextUuid;
1119
1180
  }
1120
- // Priority 3: Global context from Jaypie adapter
1121
- const jaypieAdapterUuid = getJaypieAdapterUuid();
1122
- if (jaypieAdapterUuid) {
1123
- return jaypieAdapterUuid;
1124
- }
1125
- // Fallback: Web Adapter env var
1126
- return getWebAdapterUuid();
1181
+ // Priority 2: Global context from Jaypie adapter
1182
+ return getJaypieAdapterUuid();
1127
1183
  }
1128
1184
 
1129
1185
  //
@@ -1137,7 +1193,11 @@ function getCurrentInvokeUuid(req) {
1137
1193
  */
1138
1194
  function safeGetHeader(res, name) {
1139
1195
  try {
1140
- // 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)
1141
1201
  if (res._headers instanceof Map) {
1142
1202
  const value = res._headers.get(name.toLowerCase());
1143
1203
  return value ? String(value) : undefined;
@@ -1165,7 +1225,12 @@ function safeGetHeader(res, name) {
1165
1225
  */
1166
1226
  function safeSetHeader(res, name, value) {
1167
1227
  try {
1168
- // 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)
1169
1234
  if (res._headers instanceof Map) {
1170
1235
  res._headers.set(name.toLowerCase(), value);
1171
1236
  return;
@@ -1296,12 +1361,10 @@ const logger$1 = log;
1296
1361
  //
1297
1362
  /**
1298
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.
1299
1365
  */
1300
1366
  function isLambdaMockResponse(res) {
1301
- const mock = res;
1302
- return (mock._headers instanceof Map &&
1303
- Array.isArray(mock._chunks) &&
1304
- typeof mock.buildResult === "function");
1367
+ return res[JAYPIE_LAMBDA_MOCK] === true;
1305
1368
  }
1306
1369
  /**
1307
1370
  * Safely send a JSON response, avoiding dd-trace interception.
@@ -1310,17 +1373,28 @@ function isLambdaMockResponse(res) {
1310
1373
  */
1311
1374
  function safeSendJson(res, statusCode, data) {
1312
1375
  if (isLambdaMockResponse(res)) {
1313
- // Direct internal state manipulation - bypasses dd-trace completely
1314
- 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
+ }
1315
1384
  res.statusCode = statusCode;
1316
1385
  // Directly push to chunks array instead of using stream write/end
1317
1386
  const chunk = Buffer.from(JSON.stringify(data));
1318
1387
  res._chunks.push(chunk);
1319
1388
  res._headersSent = true;
1389
+ // Mark as ended so getResult() resolves immediately
1390
+ res._ended = true;
1320
1391
  // Signal completion if a promise is waiting
1321
1392
  if (res._resolve) {
1322
1393
  res._resolve(res.buildResult());
1323
1394
  }
1395
+ // Emit "finish" event so runExpressApp's promise resolves
1396
+ console.log("[safeSendJson] Emitting finish event");
1397
+ res.emit("finish");
1324
1398
  return;
1325
1399
  }
1326
1400
  // Fall back to standard Express methods for real responses
@@ -1340,10 +1414,15 @@ function safeSend(res, statusCode, body) {
1340
1414
  res._chunks.push(chunk);
1341
1415
  }
1342
1416
  res._headersSent = true;
1417
+ // Mark as ended so getResult() resolves immediately
1418
+ res._ended = true;
1343
1419
  // Signal completion if a promise is waiting
1344
1420
  if (res._resolve) {
1345
1421
  res._resolve(res.buildResult());
1346
1422
  }
1423
+ // Emit "finish" event so runExpressApp's promise resolves
1424
+ console.log("[safeSend] Emitting finish event");
1425
+ res.emit("finish");
1347
1426
  return;
1348
1427
  }
1349
1428
  // Fall back to standard Express methods for real responses
@@ -1610,7 +1689,18 @@ function expressHandler(handlerOrOptions, optionsOrHandler) {
1610
1689
  }
1611
1690
  }
1612
1691
  catch (error) {
1613
- 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}`);
1614
1704
  log.var({ responseError: error });
1615
1705
  }
1616
1706
  // Log response
@@ -1710,14 +1800,13 @@ const logger = log;
1710
1800
  // Helper
1711
1801
  //
1712
1802
  /**
1713
- * Format an error as an SSE error event
1803
+ * Get error body from an error
1714
1804
  */
1715
- function formatErrorSSE(error) {
1805
+ function getErrorBody(error) {
1716
1806
  const isJaypieError = error.isProjectError;
1717
- const body = isJaypieError
1807
+ return isJaypieError
1718
1808
  ? (error.body?.() ?? { error: error.message })
1719
1809
  : new UnhandledError().body();
1720
- return `event: error\ndata: ${JSON.stringify(body)}\n\n`;
1721
1810
  }
1722
1811
  function expressStreamHandler(handlerOrOptions, optionsOrHandler) {
1723
1812
  /* eslint-enable no-redeclare */
@@ -1737,7 +1826,8 @@ function expressStreamHandler(handlerOrOptions, optionsOrHandler) {
1737
1826
  //
1738
1827
  // Validate
1739
1828
  //
1740
- 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;
1741
1831
  if (typeof handler !== "function") {
1742
1832
  throw new BadRequestError(`Argument "${handler}" doesn't match type "function"`);
1743
1833
  }
@@ -1875,9 +1965,10 @@ function expressStreamHandler(handlerOrOptions, optionsOrHandler) {
1875
1965
  log.fatal("Caught unhandled error in stream handler");
1876
1966
  log.info.var({ unhandledError: error.message });
1877
1967
  }
1878
- // Write error as SSE event
1968
+ // Write error in the appropriate format
1879
1969
  try {
1880
- res.write(formatErrorSSE(error));
1970
+ const errorBody = getErrorBody(error);
1971
+ res.write(formatStreamError(errorBody, format));
1881
1972
  }
1882
1973
  catch {
1883
1974
  // Response may already be closed
@@ -1986,5 +2077,5 @@ const noContentRoute = routes.noContentRoute;
1986
2077
  const notFoundRoute = routes.notFoundRoute;
1987
2078
  const notImplementedRoute = routes.notImplementedRoute;
1988
2079
 
1989
- 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 };
1990
2081
  //# sourceMappingURL=index.js.map