@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/cjs/adapter/LambdaResponseBuffered.d.ts +8 -0
- package/dist/cjs/adapter/LambdaResponseStreaming.d.ts +8 -2
- package/dist/cjs/adapter/__tests__/debug-harness.d.ts +1 -0
- package/dist/cjs/expressStreamHandler.d.ts +2 -0
- package/dist/cjs/getCurrentInvokeUuid.adapter.d.ts +2 -2
- package/dist/cjs/index.cjs +298 -208
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/cjs/index.d.ts +0 -2
- package/dist/esm/adapter/LambdaResponseBuffered.d.ts +8 -0
- package/dist/esm/adapter/LambdaResponseStreaming.d.ts +8 -2
- package/dist/esm/adapter/__tests__/debug-harness.d.ts +1 -0
- package/dist/esm/expressStreamHandler.d.ts +2 -0
- package/dist/esm/getCurrentInvokeUuid.adapter.d.ts +2 -2
- package/dist/esm/index.d.ts +0 -2
- package/dist/esm/index.js +298 -207
- package/dist/esm/index.js.map +1 -1
- package/package.json +12 -16
- package/dist/cjs/createServer.d.ts +0 -60
- package/dist/cjs/getCurrentInvokeUuid.webadapter.d.ts +0 -12
- package/dist/esm/createServer.d.ts +0 -60
- package/dist/esm/getCurrentInvokeUuid.webadapter.d.ts +0 -12
package/dist/cjs/index.cjs
CHANGED
|
@@ -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,15 @@ 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
|
+
// Parse query string from URL
|
|
38
|
+
const queryIndex = options.url.indexOf("?");
|
|
39
|
+
if (queryIndex !== -1) {
|
|
40
|
+
const queryString = options.url.slice(queryIndex + 1);
|
|
41
|
+
const params = new URLSearchParams(queryString);
|
|
42
|
+
for (const [key, value] of params) {
|
|
43
|
+
this.query[key] = value;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
37
46
|
// Store Lambda context
|
|
38
47
|
this._lambdaContext = options.lambdaContext;
|
|
39
48
|
this._lambdaEvent = options.lambdaEvent;
|
|
@@ -44,6 +53,18 @@ class LambdaRequest extends node_stream.Readable {
|
|
|
44
53
|
remoteAddress: options.remoteAddress,
|
|
45
54
|
};
|
|
46
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
|
+
}
|
|
47
68
|
}
|
|
48
69
|
//
|
|
49
70
|
// Readable stream implementation
|
|
@@ -145,6 +166,11 @@ function createLambdaRequest(event, context) {
|
|
|
145
166
|
body = event.isBase64Encoded
|
|
146
167
|
? Buffer.from(event.body, "base64")
|
|
147
168
|
: Buffer.from(event.body, "utf8");
|
|
169
|
+
// Add content-length header if not present (required for body parsers)
|
|
170
|
+
const hasContentLength = Object.keys(headers).some((k) => k.toLowerCase() === "content-length");
|
|
171
|
+
if (!hasContentLength) {
|
|
172
|
+
headers["content-length"] = String(body.length);
|
|
173
|
+
}
|
|
148
174
|
}
|
|
149
175
|
return new LambdaRequest({
|
|
150
176
|
body,
|
|
@@ -162,6 +188,9 @@ function createLambdaRequest(event, context) {
|
|
|
162
188
|
//
|
|
163
189
|
// Constants
|
|
164
190
|
//
|
|
191
|
+
// Symbol to identify Lambda mock responses. Uses Symbol.for() to ensure
|
|
192
|
+
// the same symbol is used across bundles/realms. Survives prototype manipulation.
|
|
193
|
+
const JAYPIE_LAMBDA_MOCK = Symbol.for("@jaypie/express/LambdaMock");
|
|
165
194
|
// Get Node's internal kOutHeaders symbol from ServerResponse prototype.
|
|
166
195
|
// This is needed for compatibility with Datadog dd-trace instrumentation,
|
|
167
196
|
// which patches HTTP methods and expects this internal state to exist.
|
|
@@ -195,21 +224,87 @@ class LambdaResponseBuffered extends node_stream.Writable {
|
|
|
195
224
|
// Internal state exposed for direct manipulation by safe response methods
|
|
196
225
|
// that need to bypass dd-trace interception of stream methods
|
|
197
226
|
this._chunks = [];
|
|
227
|
+
this._ended = false; // Track ended state since writableEnded is lost after prototype change
|
|
198
228
|
this._headers = new Map();
|
|
199
229
|
this._headersSent = false;
|
|
200
230
|
this._resolve = null;
|
|
231
|
+
// Mark as Lambda mock response for identification in expressHandler
|
|
232
|
+
this[JAYPIE_LAMBDA_MOCK] = true;
|
|
201
233
|
// Initialize Node's internal kOutHeaders for dd-trace compatibility.
|
|
202
234
|
// dd-trace patches HTTP methods and expects this internal state.
|
|
203
235
|
if (kOutHeaders$1) {
|
|
204
236
|
this[kOutHeaders$1] = Object.create(null);
|
|
205
237
|
}
|
|
238
|
+
// CRITICAL: Define key methods as instance properties to survive Express's
|
|
239
|
+
// setPrototypeOf(res, app.response) in middleware/init.js which would
|
|
240
|
+
// otherwise replace our prototype with ServerResponse.prototype.
|
|
241
|
+
// Instance properties take precedence over prototype properties.
|
|
242
|
+
this.getHeader = this.getHeader.bind(this);
|
|
243
|
+
this.setHeader = this.setHeader.bind(this);
|
|
244
|
+
this.removeHeader = this.removeHeader.bind(this);
|
|
245
|
+
this.hasHeader = this.hasHeader.bind(this);
|
|
246
|
+
this.getHeaders = this.getHeaders.bind(this);
|
|
247
|
+
this.getHeaderNames = this.getHeaderNames.bind(this);
|
|
248
|
+
this.writeHead = this.writeHead.bind(this);
|
|
249
|
+
this.get = this.get.bind(this);
|
|
250
|
+
this.set = this.set.bind(this);
|
|
251
|
+
this.status = this.status.bind(this);
|
|
252
|
+
this.json = this.json.bind(this);
|
|
253
|
+
this.send = this.send.bind(this);
|
|
254
|
+
this.vary = this.vary.bind(this);
|
|
255
|
+
this.end = this.end.bind(this);
|
|
256
|
+
this.write = this.write.bind(this);
|
|
257
|
+
// Also bind internal Writable methods that are called via prototype chain
|
|
258
|
+
this._write = this._write.bind(this);
|
|
259
|
+
this._final = this._final.bind(this);
|
|
260
|
+
// Bind result-building methods
|
|
261
|
+
this.getResult = this.getResult.bind(this);
|
|
262
|
+
this.buildResult = this.buildResult.bind(this);
|
|
263
|
+
this.isBinaryContentType = this.isBinaryContentType.bind(this);
|
|
264
|
+
}
|
|
265
|
+
//
|
|
266
|
+
// Internal bypass methods - completely avoid prototype chain lookup
|
|
267
|
+
// These directly access _headers Map, safe from dd-trace interception
|
|
268
|
+
//
|
|
269
|
+
_internalGetHeader(name) {
|
|
270
|
+
const value = this._headers.get(name.toLowerCase());
|
|
271
|
+
return value ? String(value) : undefined;
|
|
272
|
+
}
|
|
273
|
+
_internalSetHeader(name, value) {
|
|
274
|
+
if (!this._headersSent) {
|
|
275
|
+
const lowerName = name.toLowerCase();
|
|
276
|
+
this._headers.set(lowerName, value);
|
|
277
|
+
// Also sync kOutHeaders for any code that expects it
|
|
278
|
+
if (kOutHeaders$1) {
|
|
279
|
+
const outHeaders = this[kOutHeaders$1];
|
|
280
|
+
if (outHeaders) {
|
|
281
|
+
outHeaders[lowerName] = [name, value];
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
_internalHasHeader(name) {
|
|
287
|
+
return this._headers.has(name.toLowerCase());
|
|
288
|
+
}
|
|
289
|
+
_internalRemoveHeader(name) {
|
|
290
|
+
if (!this._headersSent) {
|
|
291
|
+
const lowerName = name.toLowerCase();
|
|
292
|
+
this._headers.delete(lowerName);
|
|
293
|
+
if (kOutHeaders$1) {
|
|
294
|
+
const outHeaders = this[kOutHeaders$1];
|
|
295
|
+
if (outHeaders) {
|
|
296
|
+
delete outHeaders[lowerName];
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
206
300
|
}
|
|
207
301
|
//
|
|
208
302
|
// Promise-based API for getting final result
|
|
209
303
|
//
|
|
210
304
|
getResult() {
|
|
211
305
|
return new Promise((resolve) => {
|
|
212
|
-
|
|
306
|
+
// Use _ended instead of writableEnded since Express's setPrototypeOf breaks the getter
|
|
307
|
+
if (this._ended) {
|
|
213
308
|
resolve(this.buildResult());
|
|
214
309
|
}
|
|
215
310
|
else {
|
|
@@ -267,36 +362,40 @@ class LambdaResponseBuffered extends node_stream.Writable {
|
|
|
267
362
|
/**
|
|
268
363
|
* Proxy for direct header access (e.g., res.headers['content-type']).
|
|
269
364
|
* Required for compatibility with middleware like helmet that access headers directly.
|
|
365
|
+
* Uses direct _headers access to bypass dd-trace interception.
|
|
270
366
|
*/
|
|
271
367
|
get headers() {
|
|
272
368
|
return new Proxy({}, {
|
|
273
369
|
deleteProperty: (_target, prop) => {
|
|
274
|
-
this.
|
|
370
|
+
this._headers.delete(String(prop).toLowerCase());
|
|
275
371
|
return true;
|
|
276
372
|
},
|
|
277
373
|
get: (_target, prop) => {
|
|
278
374
|
if (typeof prop === "symbol")
|
|
279
375
|
return undefined;
|
|
280
|
-
return this.
|
|
376
|
+
return this._headers.get(String(prop).toLowerCase());
|
|
281
377
|
},
|
|
282
378
|
getOwnPropertyDescriptor: (_target, prop) => {
|
|
283
|
-
|
|
379
|
+
const lowerProp = String(prop).toLowerCase();
|
|
380
|
+
if (this._headers.has(lowerProp)) {
|
|
284
381
|
return {
|
|
285
382
|
configurable: true,
|
|
286
383
|
enumerable: true,
|
|
287
|
-
value: this.
|
|
384
|
+
value: this._headers.get(lowerProp),
|
|
288
385
|
};
|
|
289
386
|
}
|
|
290
387
|
return undefined;
|
|
291
388
|
},
|
|
292
389
|
has: (_target, prop) => {
|
|
293
|
-
return this.
|
|
390
|
+
return this._headers.has(String(prop).toLowerCase());
|
|
294
391
|
},
|
|
295
392
|
ownKeys: () => {
|
|
296
|
-
return this.
|
|
393
|
+
return Array.from(this._headers.keys());
|
|
297
394
|
},
|
|
298
395
|
set: (_target, prop, value) => {
|
|
299
|
-
this.
|
|
396
|
+
if (!this._headersSent) {
|
|
397
|
+
this._headers.set(String(prop).toLowerCase(), value);
|
|
398
|
+
}
|
|
300
399
|
return true;
|
|
301
400
|
},
|
|
302
401
|
});
|
|
@@ -313,9 +412,10 @@ class LambdaResponseBuffered extends node_stream.Writable {
|
|
|
313
412
|
headersToSet = statusMessageOrHeaders;
|
|
314
413
|
}
|
|
315
414
|
if (headersToSet) {
|
|
415
|
+
// Use direct _headers access to bypass dd-trace interception
|
|
316
416
|
for (const [key, value] of Object.entries(headersToSet)) {
|
|
317
417
|
if (value !== undefined) {
|
|
318
|
-
this.
|
|
418
|
+
this._headers.set(key.toLowerCase(), String(value));
|
|
319
419
|
}
|
|
320
420
|
}
|
|
321
421
|
}
|
|
@@ -352,7 +452,8 @@ class LambdaResponseBuffered extends node_stream.Writable {
|
|
|
352
452
|
return this;
|
|
353
453
|
}
|
|
354
454
|
json(data) {
|
|
355
|
-
|
|
455
|
+
// Use direct _headers access to bypass dd-trace interception
|
|
456
|
+
this._headers.set("content-type", "application/json");
|
|
356
457
|
this.end(JSON.stringify(data));
|
|
357
458
|
return this;
|
|
358
459
|
}
|
|
@@ -366,11 +467,12 @@ class LambdaResponseBuffered extends node_stream.Writable {
|
|
|
366
467
|
/**
|
|
367
468
|
* Add a field to the Vary response header.
|
|
368
469
|
* Used by CORS middleware to indicate response varies by Origin.
|
|
470
|
+
* Uses direct _headers access to bypass dd-trace interception.
|
|
369
471
|
*/
|
|
370
472
|
vary(field) {
|
|
371
|
-
const existing = this.
|
|
473
|
+
const existing = this._headers.get("vary");
|
|
372
474
|
if (!existing) {
|
|
373
|
-
this.
|
|
475
|
+
this._headers.set("vary", field);
|
|
374
476
|
}
|
|
375
477
|
else {
|
|
376
478
|
// Append to existing Vary header if field not already present
|
|
@@ -378,7 +480,7 @@ class LambdaResponseBuffered extends node_stream.Writable {
|
|
|
378
480
|
.split(",")
|
|
379
481
|
.map((f) => f.trim().toLowerCase());
|
|
380
482
|
if (!fields.includes(field.toLowerCase())) {
|
|
381
|
-
this.
|
|
483
|
+
this._headers.set("vary", `${existing}, ${field}`);
|
|
382
484
|
}
|
|
383
485
|
}
|
|
384
486
|
return this;
|
|
@@ -396,6 +498,7 @@ class LambdaResponseBuffered extends node_stream.Writable {
|
|
|
396
498
|
callback();
|
|
397
499
|
}
|
|
398
500
|
_final(callback) {
|
|
501
|
+
this._ended = true;
|
|
399
502
|
if (this._resolve) {
|
|
400
503
|
this._resolve(this.buildResult());
|
|
401
504
|
}
|
|
@@ -406,7 +509,8 @@ class LambdaResponseBuffered extends node_stream.Writable {
|
|
|
406
509
|
//
|
|
407
510
|
buildResult() {
|
|
408
511
|
const body = Buffer.concat(this._chunks);
|
|
409
|
-
|
|
512
|
+
// Use direct _headers access to bypass dd-trace interception
|
|
513
|
+
const contentType = this._headers.get("content-type") || "";
|
|
410
514
|
// Determine if response should be base64 encoded
|
|
411
515
|
const isBase64Encoded = this.isBinaryContentType(contentType);
|
|
412
516
|
// Build headers object
|
|
@@ -468,6 +572,8 @@ class LambdaResponseStreaming extends node_stream.Writable {
|
|
|
468
572
|
this.socket = {
|
|
469
573
|
remoteAddress: "127.0.0.1",
|
|
470
574
|
};
|
|
575
|
+
// Internal state exposed for direct manipulation by safe response methods
|
|
576
|
+
// that need to bypass dd-trace interception
|
|
471
577
|
this._headers = new Map();
|
|
472
578
|
this._headersSent = false;
|
|
473
579
|
this._pendingWrites = [];
|
|
@@ -478,6 +584,65 @@ class LambdaResponseStreaming extends node_stream.Writable {
|
|
|
478
584
|
if (kOutHeaders) {
|
|
479
585
|
this[kOutHeaders] = Object.create(null);
|
|
480
586
|
}
|
|
587
|
+
// CRITICAL: Define key methods as instance properties to survive Express's
|
|
588
|
+
// setPrototypeOf(res, app.response) in middleware/init.js which would
|
|
589
|
+
// otherwise replace our prototype with ServerResponse.prototype.
|
|
590
|
+
// Instance properties take precedence over prototype properties.
|
|
591
|
+
this.getHeader = this.getHeader.bind(this);
|
|
592
|
+
this.setHeader = this.setHeader.bind(this);
|
|
593
|
+
this.removeHeader = this.removeHeader.bind(this);
|
|
594
|
+
this.hasHeader = this.hasHeader.bind(this);
|
|
595
|
+
this.getHeaders = this.getHeaders.bind(this);
|
|
596
|
+
this.getHeaderNames = this.getHeaderNames.bind(this);
|
|
597
|
+
this.writeHead = this.writeHead.bind(this);
|
|
598
|
+
this.flushHeaders = this.flushHeaders.bind(this);
|
|
599
|
+
this.get = this.get.bind(this);
|
|
600
|
+
this.set = this.set.bind(this);
|
|
601
|
+
this.status = this.status.bind(this);
|
|
602
|
+
this.json = this.json.bind(this);
|
|
603
|
+
this.send = this.send.bind(this);
|
|
604
|
+
this.vary = this.vary.bind(this);
|
|
605
|
+
this.end = this.end.bind(this);
|
|
606
|
+
this.write = this.write.bind(this);
|
|
607
|
+
// Also bind internal Writable methods that are called via prototype chain
|
|
608
|
+
this._write = this._write.bind(this);
|
|
609
|
+
this._final = this._final.bind(this);
|
|
610
|
+
}
|
|
611
|
+
//
|
|
612
|
+
// Internal bypass methods - completely avoid prototype chain lookup
|
|
613
|
+
// These directly access _headers Map, safe from dd-trace interception
|
|
614
|
+
//
|
|
615
|
+
_internalGetHeader(name) {
|
|
616
|
+
const value = this._headers.get(name.toLowerCase());
|
|
617
|
+
return value ? String(value) : undefined;
|
|
618
|
+
}
|
|
619
|
+
_internalSetHeader(name, value) {
|
|
620
|
+
if (!this._headersSent) {
|
|
621
|
+
const lowerName = name.toLowerCase();
|
|
622
|
+
this._headers.set(lowerName, value);
|
|
623
|
+
// Also sync kOutHeaders for any code that expects it
|
|
624
|
+
if (kOutHeaders) {
|
|
625
|
+
const outHeaders = this[kOutHeaders];
|
|
626
|
+
if (outHeaders) {
|
|
627
|
+
outHeaders[lowerName] = [name, value];
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
_internalHasHeader(name) {
|
|
633
|
+
return this._headers.has(name.toLowerCase());
|
|
634
|
+
}
|
|
635
|
+
_internalRemoveHeader(name) {
|
|
636
|
+
if (!this._headersSent) {
|
|
637
|
+
const lowerName = name.toLowerCase();
|
|
638
|
+
this._headers.delete(lowerName);
|
|
639
|
+
if (kOutHeaders) {
|
|
640
|
+
const outHeaders = this[kOutHeaders];
|
|
641
|
+
if (outHeaders) {
|
|
642
|
+
delete outHeaders[lowerName];
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
}
|
|
481
646
|
}
|
|
482
647
|
//
|
|
483
648
|
// Header management
|
|
@@ -532,36 +697,42 @@ class LambdaResponseStreaming extends node_stream.Writable {
|
|
|
532
697
|
/**
|
|
533
698
|
* Proxy for direct header access (e.g., res.headers['content-type']).
|
|
534
699
|
* Required for compatibility with middleware like helmet that access headers directly.
|
|
700
|
+
* Uses direct _headers access to bypass dd-trace interception.
|
|
535
701
|
*/
|
|
536
702
|
get headers() {
|
|
537
703
|
return new Proxy({}, {
|
|
538
704
|
deleteProperty: (_target, prop) => {
|
|
539
|
-
this.
|
|
705
|
+
if (!this._headersSent) {
|
|
706
|
+
this._headers.delete(String(prop).toLowerCase());
|
|
707
|
+
}
|
|
540
708
|
return true;
|
|
541
709
|
},
|
|
542
710
|
get: (_target, prop) => {
|
|
543
711
|
if (typeof prop === "symbol")
|
|
544
712
|
return undefined;
|
|
545
|
-
return this.
|
|
713
|
+
return this._headers.get(String(prop).toLowerCase());
|
|
546
714
|
},
|
|
547
715
|
getOwnPropertyDescriptor: (_target, prop) => {
|
|
548
|
-
|
|
716
|
+
const lowerProp = String(prop).toLowerCase();
|
|
717
|
+
if (this._headers.has(lowerProp)) {
|
|
549
718
|
return {
|
|
550
719
|
configurable: true,
|
|
551
720
|
enumerable: true,
|
|
552
|
-
value: this.
|
|
721
|
+
value: this._headers.get(lowerProp),
|
|
553
722
|
};
|
|
554
723
|
}
|
|
555
724
|
return undefined;
|
|
556
725
|
},
|
|
557
726
|
has: (_target, prop) => {
|
|
558
|
-
return this.
|
|
727
|
+
return this._headers.has(String(prop).toLowerCase());
|
|
559
728
|
},
|
|
560
729
|
ownKeys: () => {
|
|
561
|
-
return this.
|
|
730
|
+
return Array.from(this._headers.keys());
|
|
562
731
|
},
|
|
563
732
|
set: (_target, prop, value) => {
|
|
564
|
-
this.
|
|
733
|
+
if (!this._headersSent) {
|
|
734
|
+
this._headers.set(String(prop).toLowerCase(), value);
|
|
735
|
+
}
|
|
565
736
|
return true;
|
|
566
737
|
},
|
|
567
738
|
});
|
|
@@ -581,9 +752,10 @@ class LambdaResponseStreaming extends node_stream.Writable {
|
|
|
581
752
|
headersToSet = statusMessageOrHeaders;
|
|
582
753
|
}
|
|
583
754
|
if (headersToSet) {
|
|
755
|
+
// Use direct _headers access to bypass dd-trace interception
|
|
584
756
|
for (const [key, value] of Object.entries(headersToSet)) {
|
|
585
757
|
if (value !== undefined) {
|
|
586
|
-
this.
|
|
758
|
+
this._headers.set(key.toLowerCase(), String(value));
|
|
587
759
|
}
|
|
588
760
|
}
|
|
589
761
|
}
|
|
@@ -641,7 +813,8 @@ class LambdaResponseStreaming extends node_stream.Writable {
|
|
|
641
813
|
return this;
|
|
642
814
|
}
|
|
643
815
|
json(data) {
|
|
644
|
-
|
|
816
|
+
// Use direct _headers access to bypass dd-trace interception
|
|
817
|
+
this._headers.set("content-type", "application/json");
|
|
645
818
|
this.end(JSON.stringify(data));
|
|
646
819
|
return this;
|
|
647
820
|
}
|
|
@@ -655,11 +828,12 @@ class LambdaResponseStreaming extends node_stream.Writable {
|
|
|
655
828
|
/**
|
|
656
829
|
* Add a field to the Vary response header.
|
|
657
830
|
* Used by CORS middleware to indicate response varies by Origin.
|
|
831
|
+
* Uses direct _headers access to bypass dd-trace interception.
|
|
658
832
|
*/
|
|
659
833
|
vary(field) {
|
|
660
|
-
const existing = this.
|
|
834
|
+
const existing = this._headers.get("vary");
|
|
661
835
|
if (!existing) {
|
|
662
|
-
this.
|
|
836
|
+
this._headers.set("vary", field);
|
|
663
837
|
}
|
|
664
838
|
else {
|
|
665
839
|
// Append to existing Vary header if field not already present
|
|
@@ -667,7 +841,7 @@ class LambdaResponseStreaming extends node_stream.Writable {
|
|
|
667
841
|
.split(",")
|
|
668
842
|
.map((f) => f.trim().toLowerCase());
|
|
669
843
|
if (!fields.includes(field.toLowerCase())) {
|
|
670
|
-
this.
|
|
844
|
+
this._headers.set("vary", `${existing}, ${field}`);
|
|
671
845
|
}
|
|
672
846
|
}
|
|
673
847
|
return this;
|
|
@@ -765,6 +939,7 @@ function runExpressApp(app, req, res) {
|
|
|
765
939
|
*/
|
|
766
940
|
function createLambdaHandler(app, _options) {
|
|
767
941
|
return async (event, context) => {
|
|
942
|
+
let result;
|
|
768
943
|
try {
|
|
769
944
|
// Set current invoke for getCurrentInvokeUuid
|
|
770
945
|
setCurrentInvoke(event, context);
|
|
@@ -774,8 +949,38 @@ function createLambdaHandler(app, _options) {
|
|
|
774
949
|
const res = new LambdaResponseBuffered();
|
|
775
950
|
// Run Express app
|
|
776
951
|
await runExpressApp(app, req, res);
|
|
777
|
-
//
|
|
778
|
-
|
|
952
|
+
// Get Lambda response - await explicitly to ensure we have the result
|
|
953
|
+
result = await res.getResult();
|
|
954
|
+
// Debug: Log the response before returning
|
|
955
|
+
console.log("[createLambdaHandler] Returning response:", JSON.stringify({
|
|
956
|
+
statusCode: result.statusCode,
|
|
957
|
+
headers: result.headers,
|
|
958
|
+
bodyLength: result.body?.length,
|
|
959
|
+
isBase64Encoded: result.isBase64Encoded,
|
|
960
|
+
}));
|
|
961
|
+
return result;
|
|
962
|
+
}
|
|
963
|
+
catch (error) {
|
|
964
|
+
// Log any unhandled errors
|
|
965
|
+
console.error("[createLambdaHandler] Unhandled error:", error);
|
|
966
|
+
if (error instanceof Error) {
|
|
967
|
+
console.error("[createLambdaHandler] Stack:", error.stack);
|
|
968
|
+
}
|
|
969
|
+
// Return a proper error response instead of throwing
|
|
970
|
+
return {
|
|
971
|
+
statusCode: 500,
|
|
972
|
+
headers: { "content-type": "application/json" },
|
|
973
|
+
body: JSON.stringify({
|
|
974
|
+
errors: [
|
|
975
|
+
{
|
|
976
|
+
status: 500,
|
|
977
|
+
title: "Internal Server Error",
|
|
978
|
+
detail: error instanceof Error ? error.message : "Unknown error occurred",
|
|
979
|
+
},
|
|
980
|
+
],
|
|
981
|
+
}),
|
|
982
|
+
isBase64Encoded: false,
|
|
983
|
+
};
|
|
779
984
|
}
|
|
780
985
|
finally {
|
|
781
986
|
// Clear current invoke context
|
|
@@ -913,7 +1118,7 @@ const corsHelper = (config = {}) => {
|
|
|
913
1118
|
};
|
|
914
1119
|
return expressCors(options);
|
|
915
1120
|
};
|
|
916
|
-
var
|
|
1121
|
+
var cors_helper = (config) => {
|
|
917
1122
|
const cors = corsHelper(config);
|
|
918
1123
|
return (req, res, next) => {
|
|
919
1124
|
cors(req, res, (error) => {
|
|
@@ -928,154 +1133,10 @@ var cors = (config) => {
|
|
|
928
1133
|
};
|
|
929
1134
|
};
|
|
930
1135
|
|
|
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
|
-
//
|
|
1013
|
-
//
|
|
1014
|
-
// Helper Functions
|
|
1015
|
-
//
|
|
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
1136
|
//
|
|
1066
1137
|
//
|
|
1067
1138
|
// Helper Functions
|
|
1068
1139
|
//
|
|
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
1140
|
/**
|
|
1080
1141
|
* Get UUID from Jaypie Lambda adapter context.
|
|
1081
1142
|
* This is set by createLambdaHandler/createLambdaStreamHandler.
|
|
@@ -1092,8 +1153,12 @@ function getJaypieAdapterUuid() {
|
|
|
1092
1153
|
* The Jaypie adapter attaches _lambdaContext to the request.
|
|
1093
1154
|
*/
|
|
1094
1155
|
function getRequestContextUuid(req) {
|
|
1095
|
-
if (req && req._lambdaContext
|
|
1096
|
-
|
|
1156
|
+
if (req && req._lambdaContext) {
|
|
1157
|
+
const lambdaContext = req
|
|
1158
|
+
._lambdaContext;
|
|
1159
|
+
if (lambdaContext.awsRequestId) {
|
|
1160
|
+
return lambdaContext.awsRequestId;
|
|
1161
|
+
}
|
|
1097
1162
|
}
|
|
1098
1163
|
return undefined;
|
|
1099
1164
|
}
|
|
@@ -1103,29 +1168,20 @@ function getRequestContextUuid(req) {
|
|
|
1103
1168
|
//
|
|
1104
1169
|
/**
|
|
1105
1170
|
* Get the current invoke UUID from Lambda context.
|
|
1106
|
-
* Works with Jaypie Lambda adapter
|
|
1171
|
+
* Works with Jaypie Lambda adapter (createLambdaHandler/createLambdaStreamHandler).
|
|
1107
1172
|
*
|
|
1108
1173
|
* @param req - Optional Express request object. Used to extract context
|
|
1109
|
-
* from
|
|
1174
|
+
* from Jaypie adapter's _lambdaContext.
|
|
1110
1175
|
* @returns The AWS request ID or undefined if not in Lambda context
|
|
1111
1176
|
*/
|
|
1112
1177
|
function getCurrentInvokeUuid(req) {
|
|
1113
|
-
// Priority 1:
|
|
1114
|
-
if (isWebAdapterMode(req)) {
|
|
1115
|
-
return getWebAdapterUuid(req);
|
|
1116
|
-
}
|
|
1117
|
-
// Priority 2: Request has Lambda context attached (Jaypie adapter)
|
|
1178
|
+
// Priority 1: Request has Lambda context attached (Jaypie adapter)
|
|
1118
1179
|
const requestContextUuid = getRequestContextUuid(req);
|
|
1119
1180
|
if (requestContextUuid) {
|
|
1120
1181
|
return requestContextUuid;
|
|
1121
1182
|
}
|
|
1122
|
-
// Priority
|
|
1123
|
-
|
|
1124
|
-
if (jaypieAdapterUuid) {
|
|
1125
|
-
return jaypieAdapterUuid;
|
|
1126
|
-
}
|
|
1127
|
-
// Fallback: Web Adapter env var
|
|
1128
|
-
return getWebAdapterUuid();
|
|
1183
|
+
// Priority 2: Global context from Jaypie adapter
|
|
1184
|
+
return getJaypieAdapterUuid();
|
|
1129
1185
|
}
|
|
1130
1186
|
|
|
1131
1187
|
//
|
|
@@ -1139,7 +1195,11 @@ function getCurrentInvokeUuid(req) {
|
|
|
1139
1195
|
*/
|
|
1140
1196
|
function safeGetHeader(res, name) {
|
|
1141
1197
|
try {
|
|
1142
|
-
// Try
|
|
1198
|
+
// Try internal method first (completely bypasses dd-trace)
|
|
1199
|
+
if (typeof res._internalGetHeader === "function") {
|
|
1200
|
+
return res._internalGetHeader(name);
|
|
1201
|
+
}
|
|
1202
|
+
// Fall back to _headers Map access (Lambda adapter, avoids dd-trace)
|
|
1143
1203
|
if (res._headers instanceof Map) {
|
|
1144
1204
|
const value = res._headers.get(name.toLowerCase());
|
|
1145
1205
|
return value ? String(value) : undefined;
|
|
@@ -1167,7 +1227,12 @@ function safeGetHeader(res, name) {
|
|
|
1167
1227
|
*/
|
|
1168
1228
|
function safeSetHeader(res, name, value) {
|
|
1169
1229
|
try {
|
|
1170
|
-
// Try
|
|
1230
|
+
// Try internal method first (completely bypasses dd-trace)
|
|
1231
|
+
if (typeof res._internalSetHeader === "function") {
|
|
1232
|
+
res._internalSetHeader(name, value);
|
|
1233
|
+
return;
|
|
1234
|
+
}
|
|
1235
|
+
// Fall back to _headers Map access (Lambda adapter, avoids dd-trace)
|
|
1171
1236
|
if (res._headers instanceof Map) {
|
|
1172
1237
|
res._headers.set(name.toLowerCase(), value);
|
|
1173
1238
|
return;
|
|
@@ -1298,12 +1363,10 @@ const logger$1 = logger$2.log;
|
|
|
1298
1363
|
//
|
|
1299
1364
|
/**
|
|
1300
1365
|
* Check if response is a Lambda mock response with direct internal access.
|
|
1366
|
+
* Uses Symbol marker to survive prototype chain modifications from Express and dd-trace.
|
|
1301
1367
|
*/
|
|
1302
1368
|
function isLambdaMockResponse(res) {
|
|
1303
|
-
|
|
1304
|
-
return (mock._headers instanceof Map &&
|
|
1305
|
-
Array.isArray(mock._chunks) &&
|
|
1306
|
-
typeof mock.buildResult === "function");
|
|
1369
|
+
return res[JAYPIE_LAMBDA_MOCK] === true;
|
|
1307
1370
|
}
|
|
1308
1371
|
/**
|
|
1309
1372
|
* Safely send a JSON response, avoiding dd-trace interception.
|
|
@@ -1312,17 +1375,28 @@ function isLambdaMockResponse(res) {
|
|
|
1312
1375
|
*/
|
|
1313
1376
|
function safeSendJson(res, statusCode, data) {
|
|
1314
1377
|
if (isLambdaMockResponse(res)) {
|
|
1315
|
-
//
|
|
1316
|
-
res.
|
|
1378
|
+
// Use internal method to set header (completely bypasses dd-trace)
|
|
1379
|
+
if (typeof res._internalSetHeader === "function") {
|
|
1380
|
+
res._internalSetHeader("content-type", "application/json");
|
|
1381
|
+
}
|
|
1382
|
+
else {
|
|
1383
|
+
// Fall back to direct _headers manipulation
|
|
1384
|
+
res._headers.set("content-type", "application/json");
|
|
1385
|
+
}
|
|
1317
1386
|
res.statusCode = statusCode;
|
|
1318
1387
|
// Directly push to chunks array instead of using stream write/end
|
|
1319
1388
|
const chunk = Buffer.from(JSON.stringify(data));
|
|
1320
1389
|
res._chunks.push(chunk);
|
|
1321
1390
|
res._headersSent = true;
|
|
1391
|
+
// Mark as ended so getResult() resolves immediately
|
|
1392
|
+
res._ended = true;
|
|
1322
1393
|
// Signal completion if a promise is waiting
|
|
1323
1394
|
if (res._resolve) {
|
|
1324
1395
|
res._resolve(res.buildResult());
|
|
1325
1396
|
}
|
|
1397
|
+
// Emit "finish" event so runExpressApp's promise resolves
|
|
1398
|
+
console.log("[safeSendJson] Emitting finish event");
|
|
1399
|
+
res.emit("finish");
|
|
1326
1400
|
return;
|
|
1327
1401
|
}
|
|
1328
1402
|
// Fall back to standard Express methods for real responses
|
|
@@ -1342,10 +1416,15 @@ function safeSend(res, statusCode, body) {
|
|
|
1342
1416
|
res._chunks.push(chunk);
|
|
1343
1417
|
}
|
|
1344
1418
|
res._headersSent = true;
|
|
1419
|
+
// Mark as ended so getResult() resolves immediately
|
|
1420
|
+
res._ended = true;
|
|
1345
1421
|
// Signal completion if a promise is waiting
|
|
1346
1422
|
if (res._resolve) {
|
|
1347
1423
|
res._resolve(res.buildResult());
|
|
1348
1424
|
}
|
|
1425
|
+
// Emit "finish" event so runExpressApp's promise resolves
|
|
1426
|
+
console.log("[safeSend] Emitting finish event");
|
|
1427
|
+
res.emit("finish");
|
|
1349
1428
|
return;
|
|
1350
1429
|
}
|
|
1351
1430
|
// Fall back to standard Express methods for real responses
|
|
@@ -1612,7 +1691,18 @@ function expressHandler(handlerOrOptions, optionsOrHandler) {
|
|
|
1612
1691
|
}
|
|
1613
1692
|
}
|
|
1614
1693
|
catch (error) {
|
|
1615
|
-
|
|
1694
|
+
// Use console.error for raw stack trace to ensure it appears in CloudWatch
|
|
1695
|
+
// Handle both Error objects and plain thrown values
|
|
1696
|
+
const errorMessage = error instanceof Error
|
|
1697
|
+
? error.message
|
|
1698
|
+
: typeof error === "object" && error !== null
|
|
1699
|
+
? JSON.stringify(error)
|
|
1700
|
+
: String(error);
|
|
1701
|
+
const errorStack = error instanceof Error
|
|
1702
|
+
? error.stack
|
|
1703
|
+
: new Error("Stack trace").stack?.replace("Error: Stack trace", `Error: ${errorMessage}`);
|
|
1704
|
+
console.error("Express response error stack trace:", errorStack);
|
|
1705
|
+
log.fatal(`Express encountered an error while sending the response: ${errorMessage}`);
|
|
1616
1706
|
log.var({ responseError: error });
|
|
1617
1707
|
}
|
|
1618
1708
|
// Log response
|
|
@@ -1712,14 +1802,13 @@ const logger = logger$2.log;
|
|
|
1712
1802
|
// Helper
|
|
1713
1803
|
//
|
|
1714
1804
|
/**
|
|
1715
|
-
*
|
|
1805
|
+
* Get error body from an error
|
|
1716
1806
|
*/
|
|
1717
|
-
function
|
|
1807
|
+
function getErrorBody(error) {
|
|
1718
1808
|
const isJaypieError = error.isProjectError;
|
|
1719
|
-
|
|
1809
|
+
return isJaypieError
|
|
1720
1810
|
? (error.body?.() ?? { error: error.message })
|
|
1721
1811
|
: new errors.UnhandledError().body();
|
|
1722
|
-
return `event: error\ndata: ${JSON.stringify(body)}\n\n`;
|
|
1723
1812
|
}
|
|
1724
1813
|
function expressStreamHandler(handlerOrOptions, optionsOrHandler) {
|
|
1725
1814
|
/* eslint-enable no-redeclare */
|
|
@@ -1739,7 +1828,8 @@ function expressStreamHandler(handlerOrOptions, optionsOrHandler) {
|
|
|
1739
1828
|
//
|
|
1740
1829
|
// Validate
|
|
1741
1830
|
//
|
|
1742
|
-
|
|
1831
|
+
const format = options.format ?? "sse";
|
|
1832
|
+
let { chaos, contentType = aws.getContentTypeForFormat(format), locals, name, secrets, setup = [], teardown = [], unavailable, validate, } = options;
|
|
1743
1833
|
if (typeof handler !== "function") {
|
|
1744
1834
|
throw new errors.BadRequestError(`Argument "${handler}" doesn't match type "function"`);
|
|
1745
1835
|
}
|
|
@@ -1877,9 +1967,10 @@ function expressStreamHandler(handlerOrOptions, optionsOrHandler) {
|
|
|
1877
1967
|
log.fatal("Caught unhandled error in stream handler");
|
|
1878
1968
|
log.info.var({ unhandledError: error.message });
|
|
1879
1969
|
}
|
|
1880
|
-
// Write error
|
|
1970
|
+
// Write error in the appropriate format
|
|
1881
1971
|
try {
|
|
1882
|
-
|
|
1972
|
+
const errorBody = getErrorBody(error);
|
|
1973
|
+
res.write(aws.formatStreamError(errorBody, format));
|
|
1883
1974
|
}
|
|
1884
1975
|
catch {
|
|
1885
1976
|
// Response may already be closed
|
|
@@ -1993,10 +2084,9 @@ exports.LambdaRequest = LambdaRequest;
|
|
|
1993
2084
|
exports.LambdaResponseBuffered = LambdaResponseBuffered;
|
|
1994
2085
|
exports.LambdaResponseStreaming = LambdaResponseStreaming;
|
|
1995
2086
|
exports.badRequestRoute = badRequestRoute;
|
|
1996
|
-
exports.cors =
|
|
2087
|
+
exports.cors = cors_helper;
|
|
1997
2088
|
exports.createLambdaHandler = createLambdaHandler;
|
|
1998
2089
|
exports.createLambdaStreamHandler = createLambdaStreamHandler;
|
|
1999
|
-
exports.createServer = createServer;
|
|
2000
2090
|
exports.echoRoute = echoRoute;
|
|
2001
2091
|
exports.expressHandler = expressHandler;
|
|
2002
2092
|
exports.expressHttpCodeHandler = httpHandler;
|