@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.
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,17 @@ 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
+ // Use pre-parsed query if provided, otherwise parse from URL
36
+ if (options.query) {
37
+ this.query = options.query;
38
+ }
39
+ else {
40
+ const queryIndex = options.url.indexOf("?");
41
+ if (queryIndex !== -1) {
42
+ const queryString = options.url.slice(queryIndex + 1);
43
+ this.query = parseQueryString(queryString);
44
+ }
45
+ }
35
46
  // Store Lambda context
36
47
  this._lambdaContext = options.lambdaContext;
37
48
  this._lambdaEvent = options.lambdaEvent;
@@ -42,6 +53,18 @@ class LambdaRequest extends Readable {
42
53
  remoteAddress: options.remoteAddress,
43
54
  };
44
55
  this.connection = this.socket;
56
+ // Schedule body push for next tick to ensure stream is ready
57
+ // This is needed for body parsers that consume the stream
58
+ if (this.bodyBuffer && this.bodyBuffer.length > 0) {
59
+ process.nextTick(() => {
60
+ if (!this.bodyPushed) {
61
+ this.push(this.bodyBuffer);
62
+ this.push(null);
63
+ this.bodyPushed = true;
64
+ this.complete = true;
65
+ }
66
+ });
67
+ }
45
68
  }
46
69
  //
47
70
  // Readable stream implementation
@@ -80,6 +103,66 @@ class LambdaRequest extends Readable {
80
103
  }
81
104
  //
82
105
  //
106
+ // Helper Functions
107
+ //
108
+ /**
109
+ * Normalize bracket notation in query parameter key.
110
+ * Removes trailing `[]` from keys (e.g., `filterByStatus[]` → `filterByStatus`).
111
+ */
112
+ function normalizeQueryKey(key) {
113
+ return key.endsWith("[]") ? key.slice(0, -2) : key;
114
+ }
115
+ /**
116
+ * Parse a query string into a record with proper array handling.
117
+ * Handles bracket notation (e.g., `param[]`) and multi-value parameters.
118
+ */
119
+ function parseQueryString(queryString) {
120
+ const result = {};
121
+ const params = new URLSearchParams(queryString);
122
+ for (const [rawKey, value] of params) {
123
+ const key = normalizeQueryKey(rawKey);
124
+ const existing = result[key];
125
+ if (existing === undefined) {
126
+ // First occurrence - check if it's bracket notation to determine if it should be an array
127
+ result[key] = rawKey.endsWith("[]") ? [value] : value;
128
+ }
129
+ else if (Array.isArray(existing)) {
130
+ existing.push(value);
131
+ }
132
+ else {
133
+ // Convert to array when we encounter a second value
134
+ result[key] = [existing, value];
135
+ }
136
+ }
137
+ return result;
138
+ }
139
+ /**
140
+ * Build query object from API Gateway v1 multiValueQueryStringParameters.
141
+ * Normalizes bracket notation and preserves array values.
142
+ */
143
+ function buildQueryFromMultiValue(multiValueParams) {
144
+ const result = {};
145
+ if (!multiValueParams)
146
+ return result;
147
+ for (const [rawKey, values] of Object.entries(multiValueParams)) {
148
+ const key = normalizeQueryKey(rawKey);
149
+ const existingValues = result[key];
150
+ if (existingValues === undefined) {
151
+ // First occurrence - use array if multiple values or bracket notation
152
+ result[key] = values.length === 1 && !rawKey.endsWith("[]") ? values[0] : values;
153
+ }
154
+ else if (Array.isArray(existingValues)) {
155
+ existingValues.push(...values);
156
+ }
157
+ else {
158
+ // Convert to array and merge
159
+ result[key] = [existingValues, ...values];
160
+ }
161
+ }
162
+ return result;
163
+ }
164
+ //
165
+ //
83
166
  // Type Guards
84
167
  //
85
168
  /**
@@ -105,6 +188,7 @@ function createLambdaRequest(event, context) {
105
188
  let url;
106
189
  let method;
107
190
  let protocol;
191
+ let query;
108
192
  let remoteAddress;
109
193
  const headers = { ...event.headers };
110
194
  if (isFunctionUrlEvent(event)) {
@@ -115,6 +199,10 @@ function createLambdaRequest(event, context) {
115
199
  method = event.requestContext.http.method;
116
200
  protocol = event.requestContext.http.protocol.split("/")[0].toLowerCase();
117
201
  remoteAddress = event.requestContext.http.sourceIp;
202
+ // Parse query string with proper multi-value and bracket notation support
203
+ if (event.rawQueryString) {
204
+ query = parseQueryString(event.rawQueryString);
205
+ }
118
206
  // Normalize cookies into Cookie header if not already present
119
207
  if (event.cookies && event.cookies.length > 0 && !headers.cookie) {
120
208
  headers.cookie = event.cookies.join("; ");
@@ -122,7 +210,13 @@ function createLambdaRequest(event, context) {
122
210
  }
123
211
  else if (isApiGatewayV1Event(event)) {
124
212
  // API Gateway REST API v1 format
213
+ // Use multiValueQueryStringParameters for proper array support
214
+ const multiValueParams = event.multiValueQueryStringParameters;
125
215
  const queryParams = event.queryStringParameters;
216
+ if (multiValueParams && Object.keys(multiValueParams).length > 0) {
217
+ query = buildQueryFromMultiValue(multiValueParams);
218
+ }
219
+ // Build URL with query string
126
220
  if (queryParams && Object.keys(queryParams).length > 0) {
127
221
  const queryString = new URLSearchParams(queryParams).toString();
128
222
  url = `${event.path}?${queryString}`;
@@ -143,6 +237,11 @@ function createLambdaRequest(event, context) {
143
237
  body = event.isBase64Encoded
144
238
  ? Buffer.from(event.body, "base64")
145
239
  : Buffer.from(event.body, "utf8");
240
+ // Add content-length header if not present (required for body parsers)
241
+ const hasContentLength = Object.keys(headers).some((k) => k.toLowerCase() === "content-length");
242
+ if (!hasContentLength) {
243
+ headers["content-length"] = String(body.length);
244
+ }
146
245
  }
147
246
  return new LambdaRequest({
148
247
  body,
@@ -151,6 +250,7 @@ function createLambdaRequest(event, context) {
151
250
  lambdaEvent: event,
152
251
  method,
153
252
  protocol,
253
+ query,
154
254
  remoteAddress,
155
255
  url,
156
256
  });
@@ -160,6 +260,9 @@ function createLambdaRequest(event, context) {
160
260
  //
161
261
  // Constants
162
262
  //
263
+ // Symbol to identify Lambda mock responses. Uses Symbol.for() to ensure
264
+ // the same symbol is used across bundles/realms. Survives prototype manipulation.
265
+ const JAYPIE_LAMBDA_MOCK = Symbol.for("@jaypie/express/LambdaMock");
163
266
  // Get Node's internal kOutHeaders symbol from ServerResponse prototype.
164
267
  // This is needed for compatibility with Datadog dd-trace instrumentation,
165
268
  // which patches HTTP methods and expects this internal state to exist.
@@ -193,21 +296,87 @@ class LambdaResponseBuffered extends Writable {
193
296
  // Internal state exposed for direct manipulation by safe response methods
194
297
  // that need to bypass dd-trace interception of stream methods
195
298
  this._chunks = [];
299
+ this._ended = false; // Track ended state since writableEnded is lost after prototype change
196
300
  this._headers = new Map();
197
301
  this._headersSent = false;
198
302
  this._resolve = null;
303
+ // Mark as Lambda mock response for identification in expressHandler
304
+ this[JAYPIE_LAMBDA_MOCK] = true;
199
305
  // Initialize Node's internal kOutHeaders for dd-trace compatibility.
200
306
  // dd-trace patches HTTP methods and expects this internal state.
201
307
  if (kOutHeaders$1) {
202
308
  this[kOutHeaders$1] = Object.create(null);
203
309
  }
310
+ // CRITICAL: Define key methods as instance properties to survive Express's
311
+ // setPrototypeOf(res, app.response) in middleware/init.js which would
312
+ // otherwise replace our prototype with ServerResponse.prototype.
313
+ // Instance properties take precedence over prototype properties.
314
+ this.getHeader = this.getHeader.bind(this);
315
+ this.setHeader = this.setHeader.bind(this);
316
+ this.removeHeader = this.removeHeader.bind(this);
317
+ this.hasHeader = this.hasHeader.bind(this);
318
+ this.getHeaders = this.getHeaders.bind(this);
319
+ this.getHeaderNames = this.getHeaderNames.bind(this);
320
+ this.writeHead = this.writeHead.bind(this);
321
+ this.get = this.get.bind(this);
322
+ this.set = this.set.bind(this);
323
+ this.status = this.status.bind(this);
324
+ this.json = this.json.bind(this);
325
+ this.send = this.send.bind(this);
326
+ this.vary = this.vary.bind(this);
327
+ this.end = this.end.bind(this);
328
+ this.write = this.write.bind(this);
329
+ // Also bind internal Writable methods that are called via prototype chain
330
+ this._write = this._write.bind(this);
331
+ this._final = this._final.bind(this);
332
+ // Bind result-building methods
333
+ this.getResult = this.getResult.bind(this);
334
+ this.buildResult = this.buildResult.bind(this);
335
+ this.isBinaryContentType = this.isBinaryContentType.bind(this);
336
+ }
337
+ //
338
+ // Internal bypass methods - completely avoid prototype chain lookup
339
+ // These directly access _headers Map, safe from dd-trace interception
340
+ //
341
+ _internalGetHeader(name) {
342
+ const value = this._headers.get(name.toLowerCase());
343
+ return value ? String(value) : undefined;
344
+ }
345
+ _internalSetHeader(name, value) {
346
+ if (!this._headersSent) {
347
+ const lowerName = name.toLowerCase();
348
+ this._headers.set(lowerName, value);
349
+ // Also sync kOutHeaders for any code that expects it
350
+ if (kOutHeaders$1) {
351
+ const outHeaders = this[kOutHeaders$1];
352
+ if (outHeaders) {
353
+ outHeaders[lowerName] = [name, value];
354
+ }
355
+ }
356
+ }
357
+ }
358
+ _internalHasHeader(name) {
359
+ return this._headers.has(name.toLowerCase());
360
+ }
361
+ _internalRemoveHeader(name) {
362
+ if (!this._headersSent) {
363
+ const lowerName = name.toLowerCase();
364
+ this._headers.delete(lowerName);
365
+ if (kOutHeaders$1) {
366
+ const outHeaders = this[kOutHeaders$1];
367
+ if (outHeaders) {
368
+ delete outHeaders[lowerName];
369
+ }
370
+ }
371
+ }
204
372
  }
205
373
  //
206
374
  // Promise-based API for getting final result
207
375
  //
208
376
  getResult() {
209
377
  return new Promise((resolve) => {
210
- if (this.writableEnded) {
378
+ // Use _ended instead of writableEnded since Express's setPrototypeOf breaks the getter
379
+ if (this._ended) {
211
380
  resolve(this.buildResult());
212
381
  }
213
382
  else {
@@ -265,36 +434,40 @@ class LambdaResponseBuffered extends Writable {
265
434
  /**
266
435
  * Proxy for direct header access (e.g., res.headers['content-type']).
267
436
  * Required for compatibility with middleware like helmet that access headers directly.
437
+ * Uses direct _headers access to bypass dd-trace interception.
268
438
  */
269
439
  get headers() {
270
440
  return new Proxy({}, {
271
441
  deleteProperty: (_target, prop) => {
272
- this.removeHeader(String(prop));
442
+ this._headers.delete(String(prop).toLowerCase());
273
443
  return true;
274
444
  },
275
445
  get: (_target, prop) => {
276
446
  if (typeof prop === "symbol")
277
447
  return undefined;
278
- return this.getHeader(String(prop));
448
+ return this._headers.get(String(prop).toLowerCase());
279
449
  },
280
450
  getOwnPropertyDescriptor: (_target, prop) => {
281
- if (this.hasHeader(String(prop))) {
451
+ const lowerProp = String(prop).toLowerCase();
452
+ if (this._headers.has(lowerProp)) {
282
453
  return {
283
454
  configurable: true,
284
455
  enumerable: true,
285
- value: this.getHeader(String(prop)),
456
+ value: this._headers.get(lowerProp),
286
457
  };
287
458
  }
288
459
  return undefined;
289
460
  },
290
461
  has: (_target, prop) => {
291
- return this.hasHeader(String(prop));
462
+ return this._headers.has(String(prop).toLowerCase());
292
463
  },
293
464
  ownKeys: () => {
294
- return this.getHeaderNames();
465
+ return Array.from(this._headers.keys());
295
466
  },
296
467
  set: (_target, prop, value) => {
297
- this.setHeader(String(prop), value);
468
+ if (!this._headersSent) {
469
+ this._headers.set(String(prop).toLowerCase(), value);
470
+ }
298
471
  return true;
299
472
  },
300
473
  });
@@ -311,9 +484,10 @@ class LambdaResponseBuffered extends Writable {
311
484
  headersToSet = statusMessageOrHeaders;
312
485
  }
313
486
  if (headersToSet) {
487
+ // Use direct _headers access to bypass dd-trace interception
314
488
  for (const [key, value] of Object.entries(headersToSet)) {
315
489
  if (value !== undefined) {
316
- this.setHeader(key, value);
490
+ this._headers.set(key.toLowerCase(), String(value));
317
491
  }
318
492
  }
319
493
  }
@@ -350,7 +524,8 @@ class LambdaResponseBuffered extends Writable {
350
524
  return this;
351
525
  }
352
526
  json(data) {
353
- this.setHeader("content-type", "application/json");
527
+ // Use direct _headers access to bypass dd-trace interception
528
+ this._headers.set("content-type", "application/json");
354
529
  this.end(JSON.stringify(data));
355
530
  return this;
356
531
  }
@@ -364,11 +539,12 @@ class LambdaResponseBuffered extends Writable {
364
539
  /**
365
540
  * Add a field to the Vary response header.
366
541
  * Used by CORS middleware to indicate response varies by Origin.
542
+ * Uses direct _headers access to bypass dd-trace interception.
367
543
  */
368
544
  vary(field) {
369
- const existing = this.getHeader("vary");
545
+ const existing = this._headers.get("vary");
370
546
  if (!existing) {
371
- this.setHeader("vary", field);
547
+ this._headers.set("vary", field);
372
548
  }
373
549
  else {
374
550
  // Append to existing Vary header if field not already present
@@ -376,7 +552,7 @@ class LambdaResponseBuffered extends Writable {
376
552
  .split(",")
377
553
  .map((f) => f.trim().toLowerCase());
378
554
  if (!fields.includes(field.toLowerCase())) {
379
- this.setHeader("vary", `${existing}, ${field}`);
555
+ this._headers.set("vary", `${existing}, ${field}`);
380
556
  }
381
557
  }
382
558
  return this;
@@ -394,6 +570,7 @@ class LambdaResponseBuffered extends Writable {
394
570
  callback();
395
571
  }
396
572
  _final(callback) {
573
+ this._ended = true;
397
574
  if (this._resolve) {
398
575
  this._resolve(this.buildResult());
399
576
  }
@@ -404,7 +581,8 @@ class LambdaResponseBuffered extends Writable {
404
581
  //
405
582
  buildResult() {
406
583
  const body = Buffer.concat(this._chunks);
407
- const contentType = this.getHeader("content-type") || "";
584
+ // Use direct _headers access to bypass dd-trace interception
585
+ const contentType = this._headers.get("content-type") || "";
408
586
  // Determine if response should be base64 encoded
409
587
  const isBase64Encoded = this.isBinaryContentType(contentType);
410
588
  // Build headers object
@@ -466,6 +644,8 @@ class LambdaResponseStreaming extends Writable {
466
644
  this.socket = {
467
645
  remoteAddress: "127.0.0.1",
468
646
  };
647
+ // Internal state exposed for direct manipulation by safe response methods
648
+ // that need to bypass dd-trace interception
469
649
  this._headers = new Map();
470
650
  this._headersSent = false;
471
651
  this._pendingWrites = [];
@@ -476,6 +656,65 @@ class LambdaResponseStreaming extends Writable {
476
656
  if (kOutHeaders) {
477
657
  this[kOutHeaders] = Object.create(null);
478
658
  }
659
+ // CRITICAL: Define key methods as instance properties to survive Express's
660
+ // setPrototypeOf(res, app.response) in middleware/init.js which would
661
+ // otherwise replace our prototype with ServerResponse.prototype.
662
+ // Instance properties take precedence over prototype properties.
663
+ this.getHeader = this.getHeader.bind(this);
664
+ this.setHeader = this.setHeader.bind(this);
665
+ this.removeHeader = this.removeHeader.bind(this);
666
+ this.hasHeader = this.hasHeader.bind(this);
667
+ this.getHeaders = this.getHeaders.bind(this);
668
+ this.getHeaderNames = this.getHeaderNames.bind(this);
669
+ this.writeHead = this.writeHead.bind(this);
670
+ this.flushHeaders = this.flushHeaders.bind(this);
671
+ this.get = this.get.bind(this);
672
+ this.set = this.set.bind(this);
673
+ this.status = this.status.bind(this);
674
+ this.json = this.json.bind(this);
675
+ this.send = this.send.bind(this);
676
+ this.vary = this.vary.bind(this);
677
+ this.end = this.end.bind(this);
678
+ this.write = this.write.bind(this);
679
+ // Also bind internal Writable methods that are called via prototype chain
680
+ this._write = this._write.bind(this);
681
+ this._final = this._final.bind(this);
682
+ }
683
+ //
684
+ // Internal bypass methods - completely avoid prototype chain lookup
685
+ // These directly access _headers Map, safe from dd-trace interception
686
+ //
687
+ _internalGetHeader(name) {
688
+ const value = this._headers.get(name.toLowerCase());
689
+ return value ? String(value) : undefined;
690
+ }
691
+ _internalSetHeader(name, value) {
692
+ if (!this._headersSent) {
693
+ const lowerName = name.toLowerCase();
694
+ this._headers.set(lowerName, value);
695
+ // Also sync kOutHeaders for any code that expects it
696
+ if (kOutHeaders) {
697
+ const outHeaders = this[kOutHeaders];
698
+ if (outHeaders) {
699
+ outHeaders[lowerName] = [name, value];
700
+ }
701
+ }
702
+ }
703
+ }
704
+ _internalHasHeader(name) {
705
+ return this._headers.has(name.toLowerCase());
706
+ }
707
+ _internalRemoveHeader(name) {
708
+ if (!this._headersSent) {
709
+ const lowerName = name.toLowerCase();
710
+ this._headers.delete(lowerName);
711
+ if (kOutHeaders) {
712
+ const outHeaders = this[kOutHeaders];
713
+ if (outHeaders) {
714
+ delete outHeaders[lowerName];
715
+ }
716
+ }
717
+ }
479
718
  }
480
719
  //
481
720
  // Header management
@@ -530,36 +769,42 @@ class LambdaResponseStreaming extends Writable {
530
769
  /**
531
770
  * Proxy for direct header access (e.g., res.headers['content-type']).
532
771
  * Required for compatibility with middleware like helmet that access headers directly.
772
+ * Uses direct _headers access to bypass dd-trace interception.
533
773
  */
534
774
  get headers() {
535
775
  return new Proxy({}, {
536
776
  deleteProperty: (_target, prop) => {
537
- this.removeHeader(String(prop));
777
+ if (!this._headersSent) {
778
+ this._headers.delete(String(prop).toLowerCase());
779
+ }
538
780
  return true;
539
781
  },
540
782
  get: (_target, prop) => {
541
783
  if (typeof prop === "symbol")
542
784
  return undefined;
543
- return this.getHeader(String(prop));
785
+ return this._headers.get(String(prop).toLowerCase());
544
786
  },
545
787
  getOwnPropertyDescriptor: (_target, prop) => {
546
- if (this.hasHeader(String(prop))) {
788
+ const lowerProp = String(prop).toLowerCase();
789
+ if (this._headers.has(lowerProp)) {
547
790
  return {
548
791
  configurable: true,
549
792
  enumerable: true,
550
- value: this.getHeader(String(prop)),
793
+ value: this._headers.get(lowerProp),
551
794
  };
552
795
  }
553
796
  return undefined;
554
797
  },
555
798
  has: (_target, prop) => {
556
- return this.hasHeader(String(prop));
799
+ return this._headers.has(String(prop).toLowerCase());
557
800
  },
558
801
  ownKeys: () => {
559
- return this.getHeaderNames();
802
+ return Array.from(this._headers.keys());
560
803
  },
561
804
  set: (_target, prop, value) => {
562
- this.setHeader(String(prop), value);
805
+ if (!this._headersSent) {
806
+ this._headers.set(String(prop).toLowerCase(), value);
807
+ }
563
808
  return true;
564
809
  },
565
810
  });
@@ -579,9 +824,10 @@ class LambdaResponseStreaming extends Writable {
579
824
  headersToSet = statusMessageOrHeaders;
580
825
  }
581
826
  if (headersToSet) {
827
+ // Use direct _headers access to bypass dd-trace interception
582
828
  for (const [key, value] of Object.entries(headersToSet)) {
583
829
  if (value !== undefined) {
584
- this.setHeader(key, value);
830
+ this._headers.set(key.toLowerCase(), String(value));
585
831
  }
586
832
  }
587
833
  }
@@ -639,7 +885,8 @@ class LambdaResponseStreaming extends Writable {
639
885
  return this;
640
886
  }
641
887
  json(data) {
642
- this.setHeader("content-type", "application/json");
888
+ // Use direct _headers access to bypass dd-trace interception
889
+ this._headers.set("content-type", "application/json");
643
890
  this.end(JSON.stringify(data));
644
891
  return this;
645
892
  }
@@ -653,11 +900,12 @@ class LambdaResponseStreaming extends Writable {
653
900
  /**
654
901
  * Add a field to the Vary response header.
655
902
  * Used by CORS middleware to indicate response varies by Origin.
903
+ * Uses direct _headers access to bypass dd-trace interception.
656
904
  */
657
905
  vary(field) {
658
- const existing = this.getHeader("vary");
906
+ const existing = this._headers.get("vary");
659
907
  if (!existing) {
660
- this.setHeader("vary", field);
908
+ this._headers.set("vary", field);
661
909
  }
662
910
  else {
663
911
  // Append to existing Vary header if field not already present
@@ -665,7 +913,7 @@ class LambdaResponseStreaming extends Writable {
665
913
  .split(",")
666
914
  .map((f) => f.trim().toLowerCase());
667
915
  if (!fields.includes(field.toLowerCase())) {
668
- this.setHeader("vary", `${existing}, ${field}`);
916
+ this._headers.set("vary", `${existing}, ${field}`);
669
917
  }
670
918
  }
671
919
  return this;
@@ -763,6 +1011,7 @@ function runExpressApp(app, req, res) {
763
1011
  */
764
1012
  function createLambdaHandler(app, _options) {
765
1013
  return async (event, context) => {
1014
+ let result;
766
1015
  try {
767
1016
  // Set current invoke for getCurrentInvokeUuid
768
1017
  setCurrentInvoke(event, context);
@@ -772,8 +1021,38 @@ function createLambdaHandler(app, _options) {
772
1021
  const res = new LambdaResponseBuffered();
773
1022
  // Run Express app
774
1023
  await runExpressApp(app, req, res);
775
- // Return Lambda response
776
- return res.getResult();
1024
+ // Get Lambda response - await explicitly to ensure we have the result
1025
+ result = await res.getResult();
1026
+ // Debug: Log the response before returning
1027
+ console.log("[createLambdaHandler] Returning response:", JSON.stringify({
1028
+ statusCode: result.statusCode,
1029
+ headers: result.headers,
1030
+ bodyLength: result.body?.length,
1031
+ isBase64Encoded: result.isBase64Encoded,
1032
+ }));
1033
+ return result;
1034
+ }
1035
+ catch (error) {
1036
+ // Log any unhandled errors
1037
+ console.error("[createLambdaHandler] Unhandled error:", error);
1038
+ if (error instanceof Error) {
1039
+ console.error("[createLambdaHandler] Stack:", error.stack);
1040
+ }
1041
+ // Return a proper error response instead of throwing
1042
+ return {
1043
+ statusCode: 500,
1044
+ headers: { "content-type": "application/json" },
1045
+ body: JSON.stringify({
1046
+ errors: [
1047
+ {
1048
+ status: 500,
1049
+ title: "Internal Server Error",
1050
+ detail: error instanceof Error ? error.message : "Unknown error occurred",
1051
+ },
1052
+ ],
1053
+ }),
1054
+ isBase64Encoded: false,
1055
+ };
777
1056
  }
778
1057
  finally {
779
1058
  // Clear current invoke context
@@ -911,7 +1190,7 @@ const corsHelper = (config = {}) => {
911
1190
  };
912
1191
  return expressCors(options);
913
1192
  };
914
- var cors = (config) => {
1193
+ var cors_helper = (config) => {
915
1194
  const cors = corsHelper(config);
916
1195
  return (req, res, next) => {
917
1196
  cors(req, res, (error) => {
@@ -926,154 +1205,10 @@ var cors = (config) => {
926
1205
  };
927
1206
  };
928
1207
 
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
- //
1011
- //
1012
- // Helper Functions
1013
- //
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
1208
  //
1064
1209
  //
1065
1210
  // Helper Functions
1066
1211
  //
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
1212
  /**
1078
1213
  * Get UUID from Jaypie Lambda adapter context.
1079
1214
  * This is set by createLambdaHandler/createLambdaStreamHandler.
@@ -1090,8 +1225,12 @@ function getJaypieAdapterUuid() {
1090
1225
  * The Jaypie adapter attaches _lambdaContext to the request.
1091
1226
  */
1092
1227
  function getRequestContextUuid(req) {
1093
- if (req && req._lambdaContext?.awsRequestId) {
1094
- return req._lambdaContext.awsRequestId;
1228
+ if (req && req._lambdaContext) {
1229
+ const lambdaContext = req
1230
+ ._lambdaContext;
1231
+ if (lambdaContext.awsRequestId) {
1232
+ return lambdaContext.awsRequestId;
1233
+ }
1095
1234
  }
1096
1235
  return undefined;
1097
1236
  }
@@ -1101,29 +1240,20 @@ function getRequestContextUuid(req) {
1101
1240
  //
1102
1241
  /**
1103
1242
  * Get the current invoke UUID from Lambda context.
1104
- * Works with Jaypie Lambda adapter and Lambda Web Adapter mode.
1243
+ * Works with Jaypie Lambda adapter (createLambdaHandler/createLambdaStreamHandler).
1105
1244
  *
1106
1245
  * @param req - Optional Express request object. Used to extract context
1107
- * from Web Adapter headers or Jaypie adapter's _lambdaContext.
1246
+ * from Jaypie adapter's _lambdaContext.
1108
1247
  * @returns The AWS request ID or undefined if not in Lambda context
1109
1248
  */
1110
1249
  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)
1250
+ // Priority 1: Request has Lambda context attached (Jaypie adapter)
1116
1251
  const requestContextUuid = getRequestContextUuid(req);
1117
1252
  if (requestContextUuid) {
1118
1253
  return requestContextUuid;
1119
1254
  }
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();
1255
+ // Priority 2: Global context from Jaypie adapter
1256
+ return getJaypieAdapterUuid();
1127
1257
  }
1128
1258
 
1129
1259
  //
@@ -1137,7 +1267,11 @@ function getCurrentInvokeUuid(req) {
1137
1267
  */
1138
1268
  function safeGetHeader(res, name) {
1139
1269
  try {
1140
- // Try direct _headers access first (Lambda adapter, avoids dd-trace)
1270
+ // Try internal method first (completely bypasses dd-trace)
1271
+ if (typeof res._internalGetHeader === "function") {
1272
+ return res._internalGetHeader(name);
1273
+ }
1274
+ // Fall back to _headers Map access (Lambda adapter, avoids dd-trace)
1141
1275
  if (res._headers instanceof Map) {
1142
1276
  const value = res._headers.get(name.toLowerCase());
1143
1277
  return value ? String(value) : undefined;
@@ -1165,7 +1299,12 @@ function safeGetHeader(res, name) {
1165
1299
  */
1166
1300
  function safeSetHeader(res, name, value) {
1167
1301
  try {
1168
- // Try direct _headers access first (Lambda adapter, avoids dd-trace)
1302
+ // Try internal method first (completely bypasses dd-trace)
1303
+ if (typeof res._internalSetHeader === "function") {
1304
+ res._internalSetHeader(name, value);
1305
+ return;
1306
+ }
1307
+ // Fall back to _headers Map access (Lambda adapter, avoids dd-trace)
1169
1308
  if (res._headers instanceof Map) {
1170
1309
  res._headers.set(name.toLowerCase(), value);
1171
1310
  return;
@@ -1296,12 +1435,10 @@ const logger$1 = log;
1296
1435
  //
1297
1436
  /**
1298
1437
  * Check if response is a Lambda mock response with direct internal access.
1438
+ * Uses Symbol marker to survive prototype chain modifications from Express and dd-trace.
1299
1439
  */
1300
1440
  function isLambdaMockResponse(res) {
1301
- const mock = res;
1302
- return (mock._headers instanceof Map &&
1303
- Array.isArray(mock._chunks) &&
1304
- typeof mock.buildResult === "function");
1441
+ return res[JAYPIE_LAMBDA_MOCK] === true;
1305
1442
  }
1306
1443
  /**
1307
1444
  * Safely send a JSON response, avoiding dd-trace interception.
@@ -1310,17 +1447,28 @@ function isLambdaMockResponse(res) {
1310
1447
  */
1311
1448
  function safeSendJson(res, statusCode, data) {
1312
1449
  if (isLambdaMockResponse(res)) {
1313
- // Direct internal state manipulation - bypasses dd-trace completely
1314
- res._headers.set("content-type", "application/json");
1450
+ // Use internal method to set header (completely bypasses dd-trace)
1451
+ if (typeof res._internalSetHeader === "function") {
1452
+ res._internalSetHeader("content-type", "application/json");
1453
+ }
1454
+ else {
1455
+ // Fall back to direct _headers manipulation
1456
+ res._headers.set("content-type", "application/json");
1457
+ }
1315
1458
  res.statusCode = statusCode;
1316
1459
  // Directly push to chunks array instead of using stream write/end
1317
1460
  const chunk = Buffer.from(JSON.stringify(data));
1318
1461
  res._chunks.push(chunk);
1319
1462
  res._headersSent = true;
1463
+ // Mark as ended so getResult() resolves immediately
1464
+ res._ended = true;
1320
1465
  // Signal completion if a promise is waiting
1321
1466
  if (res._resolve) {
1322
1467
  res._resolve(res.buildResult());
1323
1468
  }
1469
+ // Emit "finish" event so runExpressApp's promise resolves
1470
+ console.log("[safeSendJson] Emitting finish event");
1471
+ res.emit("finish");
1324
1472
  return;
1325
1473
  }
1326
1474
  // Fall back to standard Express methods for real responses
@@ -1340,10 +1488,15 @@ function safeSend(res, statusCode, body) {
1340
1488
  res._chunks.push(chunk);
1341
1489
  }
1342
1490
  res._headersSent = true;
1491
+ // Mark as ended so getResult() resolves immediately
1492
+ res._ended = true;
1343
1493
  // Signal completion if a promise is waiting
1344
1494
  if (res._resolve) {
1345
1495
  res._resolve(res.buildResult());
1346
1496
  }
1497
+ // Emit "finish" event so runExpressApp's promise resolves
1498
+ console.log("[safeSend] Emitting finish event");
1499
+ res.emit("finish");
1347
1500
  return;
1348
1501
  }
1349
1502
  // Fall back to standard Express methods for real responses
@@ -1610,7 +1763,18 @@ function expressHandler(handlerOrOptions, optionsOrHandler) {
1610
1763
  }
1611
1764
  }
1612
1765
  catch (error) {
1613
- log.fatal("Express encountered an error while sending the response");
1766
+ // Use console.error for raw stack trace to ensure it appears in CloudWatch
1767
+ // Handle both Error objects and plain thrown values
1768
+ const errorMessage = error instanceof Error
1769
+ ? error.message
1770
+ : typeof error === "object" && error !== null
1771
+ ? JSON.stringify(error)
1772
+ : String(error);
1773
+ const errorStack = error instanceof Error
1774
+ ? error.stack
1775
+ : new Error("Stack trace").stack?.replace("Error: Stack trace", `Error: ${errorMessage}`);
1776
+ console.error("Express response error stack trace:", errorStack);
1777
+ log.fatal(`Express encountered an error while sending the response: ${errorMessage}`);
1614
1778
  log.var({ responseError: error });
1615
1779
  }
1616
1780
  // Log response
@@ -1710,14 +1874,13 @@ const logger = log;
1710
1874
  // Helper
1711
1875
  //
1712
1876
  /**
1713
- * Format an error as an SSE error event
1877
+ * Get error body from an error
1714
1878
  */
1715
- function formatErrorSSE(error) {
1879
+ function getErrorBody(error) {
1716
1880
  const isJaypieError = error.isProjectError;
1717
- const body = isJaypieError
1881
+ return isJaypieError
1718
1882
  ? (error.body?.() ?? { error: error.message })
1719
1883
  : new UnhandledError().body();
1720
- return `event: error\ndata: ${JSON.stringify(body)}\n\n`;
1721
1884
  }
1722
1885
  function expressStreamHandler(handlerOrOptions, optionsOrHandler) {
1723
1886
  /* eslint-enable no-redeclare */
@@ -1737,7 +1900,8 @@ function expressStreamHandler(handlerOrOptions, optionsOrHandler) {
1737
1900
  //
1738
1901
  // Validate
1739
1902
  //
1740
- let { chaos, contentType = "text/event-stream", locals, name, secrets, setup = [], teardown = [], unavailable, validate, } = options;
1903
+ const format = options.format ?? "sse";
1904
+ let { chaos, contentType = getContentTypeForFormat(format), locals, name, secrets, setup = [], teardown = [], unavailable, validate, } = options;
1741
1905
  if (typeof handler !== "function") {
1742
1906
  throw new BadRequestError(`Argument "${handler}" doesn't match type "function"`);
1743
1907
  }
@@ -1875,9 +2039,10 @@ function expressStreamHandler(handlerOrOptions, optionsOrHandler) {
1875
2039
  log.fatal("Caught unhandled error in stream handler");
1876
2040
  log.info.var({ unhandledError: error.message });
1877
2041
  }
1878
- // Write error as SSE event
2042
+ // Write error in the appropriate format
1879
2043
  try {
1880
- res.write(formatErrorSSE(error));
2044
+ const errorBody = getErrorBody(error);
2045
+ res.write(formatStreamError(errorBody, format));
1881
2046
  }
1882
2047
  catch {
1883
2048
  // Response may already be closed
@@ -1986,5 +2151,5 @@ const noContentRoute = routes.noContentRoute;
1986
2151
  const notFoundRoute = routes.notFoundRoute;
1987
2152
  const notImplementedRoute = routes.notImplementedRoute;
1988
2153
 
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 };
2154
+ 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
2155
  //# sourceMappingURL=index.js.map