@jaypie/express 1.2.4-rc9 → 1.2.5

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