@jaypie/express 1.2.4-rc2 → 1.2.4-rc21
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 +34 -5
- package/dist/cjs/adapter/LambdaResponseStreaming.d.ts +30 -2
- package/dist/cjs/adapter/__tests__/debug-harness.d.ts +1 -0
- package/dist/cjs/index.cjs +587 -28
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/esm/adapter/LambdaResponseBuffered.d.ts +34 -5
- package/dist/esm/adapter/LambdaResponseStreaming.d.ts +30 -2
- package/dist/esm/adapter/__tests__/debug-harness.d.ts +1 -0
- package/dist/esm/index.js +587 -28
- package/dist/esm/index.js.map +1 -1
- package/package.json +10 -1
package/dist/esm/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Readable, Writable } from 'node:stream';
|
|
2
|
+
import { ServerResponse } from 'node:http';
|
|
2
3
|
import { CorsError, BadRequestError, UnhandledError, GatewayTimeoutError, UnavailableError, BadGatewayError, InternalError, TeapotError, GoneError, MethodNotAllowedError, NotFoundError, ForbiddenError, UnauthorizedError, NotImplementedError } from '@jaypie/errors';
|
|
3
4
|
import { force, envBoolean, JAYPIE, HTTP, getHeaderFrom, jaypieHandler } from '@jaypie/kit';
|
|
4
5
|
import expressCors from 'cors';
|
|
@@ -31,6 +32,15 @@ class LambdaRequest extends Readable {
|
|
|
31
32
|
this.path = options.url.split("?")[0];
|
|
32
33
|
this.headers = this.normalizeHeaders(options.headers);
|
|
33
34
|
this.bodyBuffer = options.body ?? null;
|
|
35
|
+
// Parse query string from URL
|
|
36
|
+
const queryIndex = options.url.indexOf("?");
|
|
37
|
+
if (queryIndex !== -1) {
|
|
38
|
+
const queryString = options.url.slice(queryIndex + 1);
|
|
39
|
+
const params = new URLSearchParams(queryString);
|
|
40
|
+
for (const [key, value] of params) {
|
|
41
|
+
this.query[key] = value;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
34
44
|
// Store Lambda context
|
|
35
45
|
this._lambdaContext = options.lambdaContext;
|
|
36
46
|
this._lambdaEvent = options.lambdaEvent;
|
|
@@ -41,6 +51,18 @@ class LambdaRequest extends Readable {
|
|
|
41
51
|
remoteAddress: options.remoteAddress,
|
|
42
52
|
};
|
|
43
53
|
this.connection = this.socket;
|
|
54
|
+
// Schedule body push for next tick to ensure stream is ready
|
|
55
|
+
// This is needed for body parsers that consume the stream
|
|
56
|
+
if (this.bodyBuffer && this.bodyBuffer.length > 0) {
|
|
57
|
+
process.nextTick(() => {
|
|
58
|
+
if (!this.bodyPushed) {
|
|
59
|
+
this.push(this.bodyBuffer);
|
|
60
|
+
this.push(null);
|
|
61
|
+
this.bodyPushed = true;
|
|
62
|
+
this.complete = true;
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
}
|
|
44
66
|
}
|
|
45
67
|
//
|
|
46
68
|
// Readable stream implementation
|
|
@@ -142,6 +164,11 @@ function createLambdaRequest(event, context) {
|
|
|
142
164
|
body = event.isBase64Encoded
|
|
143
165
|
? Buffer.from(event.body, "base64")
|
|
144
166
|
: Buffer.from(event.body, "utf8");
|
|
167
|
+
// Add content-length header if not present (required for body parsers)
|
|
168
|
+
const hasContentLength = Object.keys(headers).some((k) => k.toLowerCase() === "content-length");
|
|
169
|
+
if (!hasContentLength) {
|
|
170
|
+
headers["content-length"] = String(body.length);
|
|
171
|
+
}
|
|
145
172
|
}
|
|
146
173
|
return new LambdaRequest({
|
|
147
174
|
body,
|
|
@@ -159,6 +186,10 @@ function createLambdaRequest(event, context) {
|
|
|
159
186
|
//
|
|
160
187
|
// Constants
|
|
161
188
|
//
|
|
189
|
+
// Get Node's internal kOutHeaders symbol from ServerResponse prototype.
|
|
190
|
+
// This is needed for compatibility with Datadog dd-trace instrumentation,
|
|
191
|
+
// which patches HTTP methods and expects this internal state to exist.
|
|
192
|
+
const kOutHeaders$1 = Object.getOwnPropertySymbols(ServerResponse.prototype).find((s) => s.toString() === "Symbol(kOutHeaders)");
|
|
162
193
|
const BINARY_CONTENT_TYPE_PATTERNS = [
|
|
163
194
|
/^application\/octet-stream$/,
|
|
164
195
|
/^application\/pdf$/,
|
|
@@ -185,17 +216,88 @@ class LambdaResponseBuffered extends Writable {
|
|
|
185
216
|
this.socket = {
|
|
186
217
|
remoteAddress: "127.0.0.1",
|
|
187
218
|
};
|
|
219
|
+
// Internal state exposed for direct manipulation by safe response methods
|
|
220
|
+
// that need to bypass dd-trace interception of stream methods
|
|
188
221
|
this._chunks = [];
|
|
222
|
+
this._ended = false; // Track ended state since writableEnded is lost after prototype change
|
|
189
223
|
this._headers = new Map();
|
|
190
224
|
this._headersSent = false;
|
|
191
225
|
this._resolve = null;
|
|
226
|
+
// Initialize Node's internal kOutHeaders for dd-trace compatibility.
|
|
227
|
+
// dd-trace patches HTTP methods and expects this internal state.
|
|
228
|
+
if (kOutHeaders$1) {
|
|
229
|
+
this[kOutHeaders$1] = Object.create(null);
|
|
230
|
+
}
|
|
231
|
+
// CRITICAL: Define key methods as instance properties to survive Express's
|
|
232
|
+
// setPrototypeOf(res, app.response) in middleware/init.js which would
|
|
233
|
+
// otherwise replace our prototype with ServerResponse.prototype.
|
|
234
|
+
// Instance properties take precedence over prototype properties.
|
|
235
|
+
this.getHeader = this.getHeader.bind(this);
|
|
236
|
+
this.setHeader = this.setHeader.bind(this);
|
|
237
|
+
this.removeHeader = this.removeHeader.bind(this);
|
|
238
|
+
this.hasHeader = this.hasHeader.bind(this);
|
|
239
|
+
this.getHeaders = this.getHeaders.bind(this);
|
|
240
|
+
this.getHeaderNames = this.getHeaderNames.bind(this);
|
|
241
|
+
this.writeHead = this.writeHead.bind(this);
|
|
242
|
+
this.get = this.get.bind(this);
|
|
243
|
+
this.set = this.set.bind(this);
|
|
244
|
+
this.status = this.status.bind(this);
|
|
245
|
+
this.json = this.json.bind(this);
|
|
246
|
+
this.send = this.send.bind(this);
|
|
247
|
+
this.vary = this.vary.bind(this);
|
|
248
|
+
this.end = this.end.bind(this);
|
|
249
|
+
this.write = this.write.bind(this);
|
|
250
|
+
// Also bind internal Writable methods that are called via prototype chain
|
|
251
|
+
this._write = this._write.bind(this);
|
|
252
|
+
this._final = this._final.bind(this);
|
|
253
|
+
// Bind result-building methods
|
|
254
|
+
this.getResult = this.getResult.bind(this);
|
|
255
|
+
this.buildResult = this.buildResult.bind(this);
|
|
256
|
+
this.isBinaryContentType = this.isBinaryContentType.bind(this);
|
|
257
|
+
}
|
|
258
|
+
//
|
|
259
|
+
// Internal bypass methods - completely avoid prototype chain lookup
|
|
260
|
+
// These directly access _headers Map, safe from dd-trace interception
|
|
261
|
+
//
|
|
262
|
+
_internalGetHeader(name) {
|
|
263
|
+
const value = this._headers.get(name.toLowerCase());
|
|
264
|
+
return value ? String(value) : undefined;
|
|
265
|
+
}
|
|
266
|
+
_internalSetHeader(name, value) {
|
|
267
|
+
if (!this._headersSent) {
|
|
268
|
+
const lowerName = name.toLowerCase();
|
|
269
|
+
this._headers.set(lowerName, value);
|
|
270
|
+
// Also sync kOutHeaders for any code that expects it
|
|
271
|
+
if (kOutHeaders$1) {
|
|
272
|
+
const outHeaders = this[kOutHeaders$1];
|
|
273
|
+
if (outHeaders) {
|
|
274
|
+
outHeaders[lowerName] = [name, value];
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
_internalHasHeader(name) {
|
|
280
|
+
return this._headers.has(name.toLowerCase());
|
|
281
|
+
}
|
|
282
|
+
_internalRemoveHeader(name) {
|
|
283
|
+
if (!this._headersSent) {
|
|
284
|
+
const lowerName = name.toLowerCase();
|
|
285
|
+
this._headers.delete(lowerName);
|
|
286
|
+
if (kOutHeaders$1) {
|
|
287
|
+
const outHeaders = this[kOutHeaders$1];
|
|
288
|
+
if (outHeaders) {
|
|
289
|
+
delete outHeaders[lowerName];
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
192
293
|
}
|
|
193
294
|
//
|
|
194
295
|
// Promise-based API for getting final result
|
|
195
296
|
//
|
|
196
297
|
getResult() {
|
|
197
298
|
return new Promise((resolve) => {
|
|
198
|
-
|
|
299
|
+
// Use _ended instead of writableEnded since Express's setPrototypeOf breaks the getter
|
|
300
|
+
if (this._ended) {
|
|
199
301
|
resolve(this.buildResult());
|
|
200
302
|
}
|
|
201
303
|
else {
|
|
@@ -211,14 +313,31 @@ class LambdaResponseBuffered extends Writable {
|
|
|
211
313
|
// In production, log warning but don't throw to match Express behavior
|
|
212
314
|
return this;
|
|
213
315
|
}
|
|
214
|
-
|
|
316
|
+
const lowerName = name.toLowerCase();
|
|
317
|
+
this._headers.set(lowerName, String(value));
|
|
318
|
+
// Sync with kOutHeaders for dd-trace compatibility
|
|
319
|
+
// Node stores as { 'header-name': ['Header-Name', value] }
|
|
320
|
+
if (kOutHeaders$1) {
|
|
321
|
+
const outHeaders = this[kOutHeaders$1];
|
|
322
|
+
if (outHeaders) {
|
|
323
|
+
outHeaders[lowerName] = [name, String(value)];
|
|
324
|
+
}
|
|
325
|
+
}
|
|
215
326
|
return this;
|
|
216
327
|
}
|
|
217
328
|
getHeader(name) {
|
|
218
329
|
return this._headers.get(name.toLowerCase());
|
|
219
330
|
}
|
|
220
331
|
removeHeader(name) {
|
|
221
|
-
|
|
332
|
+
const lowerName = name.toLowerCase();
|
|
333
|
+
this._headers.delete(lowerName);
|
|
334
|
+
// Sync with kOutHeaders for dd-trace compatibility
|
|
335
|
+
if (kOutHeaders$1) {
|
|
336
|
+
const outHeaders = this[kOutHeaders$1];
|
|
337
|
+
if (outHeaders) {
|
|
338
|
+
delete outHeaders[lowerName];
|
|
339
|
+
}
|
|
340
|
+
}
|
|
222
341
|
}
|
|
223
342
|
getHeaders() {
|
|
224
343
|
const headers = {};
|
|
@@ -233,6 +352,47 @@ class LambdaResponseBuffered extends Writable {
|
|
|
233
352
|
getHeaderNames() {
|
|
234
353
|
return Array.from(this._headers.keys());
|
|
235
354
|
}
|
|
355
|
+
/**
|
|
356
|
+
* Proxy for direct header access (e.g., res.headers['content-type']).
|
|
357
|
+
* Required for compatibility with middleware like helmet that access headers directly.
|
|
358
|
+
* Uses direct _headers access to bypass dd-trace interception.
|
|
359
|
+
*/
|
|
360
|
+
get headers() {
|
|
361
|
+
return new Proxy({}, {
|
|
362
|
+
deleteProperty: (_target, prop) => {
|
|
363
|
+
this._headers.delete(String(prop).toLowerCase());
|
|
364
|
+
return true;
|
|
365
|
+
},
|
|
366
|
+
get: (_target, prop) => {
|
|
367
|
+
if (typeof prop === "symbol")
|
|
368
|
+
return undefined;
|
|
369
|
+
return this._headers.get(String(prop).toLowerCase());
|
|
370
|
+
},
|
|
371
|
+
getOwnPropertyDescriptor: (_target, prop) => {
|
|
372
|
+
const lowerProp = String(prop).toLowerCase();
|
|
373
|
+
if (this._headers.has(lowerProp)) {
|
|
374
|
+
return {
|
|
375
|
+
configurable: true,
|
|
376
|
+
enumerable: true,
|
|
377
|
+
value: this._headers.get(lowerProp),
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
return undefined;
|
|
381
|
+
},
|
|
382
|
+
has: (_target, prop) => {
|
|
383
|
+
return this._headers.has(String(prop).toLowerCase());
|
|
384
|
+
},
|
|
385
|
+
ownKeys: () => {
|
|
386
|
+
return Array.from(this._headers.keys());
|
|
387
|
+
},
|
|
388
|
+
set: (_target, prop, value) => {
|
|
389
|
+
if (!this._headersSent) {
|
|
390
|
+
this._headers.set(String(prop).toLowerCase(), value);
|
|
391
|
+
}
|
|
392
|
+
return true;
|
|
393
|
+
},
|
|
394
|
+
});
|
|
395
|
+
}
|
|
236
396
|
writeHead(statusCode, statusMessageOrHeaders, headers) {
|
|
237
397
|
this.statusCode = statusCode;
|
|
238
398
|
let headersToSet;
|
|
@@ -245,9 +405,10 @@ class LambdaResponseBuffered extends Writable {
|
|
|
245
405
|
headersToSet = statusMessageOrHeaders;
|
|
246
406
|
}
|
|
247
407
|
if (headersToSet) {
|
|
408
|
+
// Use direct _headers access to bypass dd-trace interception
|
|
248
409
|
for (const [key, value] of Object.entries(headersToSet)) {
|
|
249
410
|
if (value !== undefined) {
|
|
250
|
-
this.
|
|
411
|
+
this._headers.set(key.toLowerCase(), String(value));
|
|
251
412
|
}
|
|
252
413
|
}
|
|
253
414
|
}
|
|
@@ -260,12 +421,32 @@ class LambdaResponseBuffered extends Writable {
|
|
|
260
421
|
//
|
|
261
422
|
// Express compatibility methods
|
|
262
423
|
//
|
|
424
|
+
/**
|
|
425
|
+
* Express-style alias for getHeader().
|
|
426
|
+
* Used by middleware like decorateResponse that use res.get().
|
|
427
|
+
* Note: Directly accesses _headers to avoid prototype chain issues with bundled code.
|
|
428
|
+
*/
|
|
429
|
+
get(name) {
|
|
430
|
+
return this._headers.get(name.toLowerCase());
|
|
431
|
+
}
|
|
432
|
+
/**
|
|
433
|
+
* Express-style alias for setHeader().
|
|
434
|
+
* Used by middleware like decorateResponse that use res.set().
|
|
435
|
+
* Note: Directly accesses _headers to avoid prototype chain issues with bundled code.
|
|
436
|
+
*/
|
|
437
|
+
set(name, value) {
|
|
438
|
+
if (!this._headersSent) {
|
|
439
|
+
this._headers.set(name.toLowerCase(), String(value));
|
|
440
|
+
}
|
|
441
|
+
return this;
|
|
442
|
+
}
|
|
263
443
|
status(code) {
|
|
264
444
|
this.statusCode = code;
|
|
265
445
|
return this;
|
|
266
446
|
}
|
|
267
447
|
json(data) {
|
|
268
|
-
|
|
448
|
+
// Use direct _headers access to bypass dd-trace interception
|
|
449
|
+
this._headers.set("content-type", "application/json");
|
|
269
450
|
this.end(JSON.stringify(data));
|
|
270
451
|
return this;
|
|
271
452
|
}
|
|
@@ -276,6 +457,27 @@ class LambdaResponseBuffered extends Writable {
|
|
|
276
457
|
this.end(body);
|
|
277
458
|
return this;
|
|
278
459
|
}
|
|
460
|
+
/**
|
|
461
|
+
* Add a field to the Vary response header.
|
|
462
|
+
* Used by CORS middleware to indicate response varies by Origin.
|
|
463
|
+
* Uses direct _headers access to bypass dd-trace interception.
|
|
464
|
+
*/
|
|
465
|
+
vary(field) {
|
|
466
|
+
const existing = this._headers.get("vary");
|
|
467
|
+
if (!existing) {
|
|
468
|
+
this._headers.set("vary", field);
|
|
469
|
+
}
|
|
470
|
+
else {
|
|
471
|
+
// Append to existing Vary header if field not already present
|
|
472
|
+
const fields = String(existing)
|
|
473
|
+
.split(",")
|
|
474
|
+
.map((f) => f.trim().toLowerCase());
|
|
475
|
+
if (!fields.includes(field.toLowerCase())) {
|
|
476
|
+
this._headers.set("vary", `${existing}, ${field}`);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
return this;
|
|
480
|
+
}
|
|
279
481
|
//
|
|
280
482
|
// Writable stream implementation
|
|
281
483
|
//
|
|
@@ -289,6 +491,7 @@ class LambdaResponseBuffered extends Writable {
|
|
|
289
491
|
callback();
|
|
290
492
|
}
|
|
291
493
|
_final(callback) {
|
|
494
|
+
this._ended = true;
|
|
292
495
|
if (this._resolve) {
|
|
293
496
|
this._resolve(this.buildResult());
|
|
294
497
|
}
|
|
@@ -299,7 +502,8 @@ class LambdaResponseBuffered extends Writable {
|
|
|
299
502
|
//
|
|
300
503
|
buildResult() {
|
|
301
504
|
const body = Buffer.concat(this._chunks);
|
|
302
|
-
|
|
505
|
+
// Use direct _headers access to bypass dd-trace interception
|
|
506
|
+
const contentType = this._headers.get("content-type") || "";
|
|
303
507
|
// Determine if response should be base64 encoded
|
|
304
508
|
const isBase64Encoded = this.isBinaryContentType(contentType);
|
|
305
509
|
// Build headers object
|
|
@@ -336,6 +540,14 @@ class LambdaResponseBuffered extends Writable {
|
|
|
336
540
|
}
|
|
337
541
|
}
|
|
338
542
|
|
|
543
|
+
//
|
|
544
|
+
//
|
|
545
|
+
// Constants
|
|
546
|
+
//
|
|
547
|
+
// Get Node's internal kOutHeaders symbol from ServerResponse prototype.
|
|
548
|
+
// This is needed for compatibility with Datadog dd-trace instrumentation,
|
|
549
|
+
// which patches HTTP methods and expects this internal state to exist.
|
|
550
|
+
const kOutHeaders = Object.getOwnPropertySymbols(ServerResponse.prototype).find((s) => s.toString() === "Symbol(kOutHeaders)");
|
|
339
551
|
//
|
|
340
552
|
//
|
|
341
553
|
// LambdaResponseStreaming Class
|
|
@@ -353,11 +565,77 @@ class LambdaResponseStreaming extends Writable {
|
|
|
353
565
|
this.socket = {
|
|
354
566
|
remoteAddress: "127.0.0.1",
|
|
355
567
|
};
|
|
568
|
+
// Internal state exposed for direct manipulation by safe response methods
|
|
569
|
+
// that need to bypass dd-trace interception
|
|
356
570
|
this._headers = new Map();
|
|
357
571
|
this._headersSent = false;
|
|
358
572
|
this._pendingWrites = [];
|
|
359
573
|
this._wrappedStream = null;
|
|
360
574
|
this._responseStream = responseStream;
|
|
575
|
+
// Initialize Node's internal kOutHeaders for dd-trace compatibility.
|
|
576
|
+
// dd-trace patches HTTP methods and expects this internal state.
|
|
577
|
+
if (kOutHeaders) {
|
|
578
|
+
this[kOutHeaders] = Object.create(null);
|
|
579
|
+
}
|
|
580
|
+
// CRITICAL: Define key methods as instance properties to survive Express's
|
|
581
|
+
// setPrototypeOf(res, app.response) in middleware/init.js which would
|
|
582
|
+
// otherwise replace our prototype with ServerResponse.prototype.
|
|
583
|
+
// Instance properties take precedence over prototype properties.
|
|
584
|
+
this.getHeader = this.getHeader.bind(this);
|
|
585
|
+
this.setHeader = this.setHeader.bind(this);
|
|
586
|
+
this.removeHeader = this.removeHeader.bind(this);
|
|
587
|
+
this.hasHeader = this.hasHeader.bind(this);
|
|
588
|
+
this.getHeaders = this.getHeaders.bind(this);
|
|
589
|
+
this.getHeaderNames = this.getHeaderNames.bind(this);
|
|
590
|
+
this.writeHead = this.writeHead.bind(this);
|
|
591
|
+
this.flushHeaders = this.flushHeaders.bind(this);
|
|
592
|
+
this.get = this.get.bind(this);
|
|
593
|
+
this.set = this.set.bind(this);
|
|
594
|
+
this.status = this.status.bind(this);
|
|
595
|
+
this.json = this.json.bind(this);
|
|
596
|
+
this.send = this.send.bind(this);
|
|
597
|
+
this.vary = this.vary.bind(this);
|
|
598
|
+
this.end = this.end.bind(this);
|
|
599
|
+
this.write = this.write.bind(this);
|
|
600
|
+
// Also bind internal Writable methods that are called via prototype chain
|
|
601
|
+
this._write = this._write.bind(this);
|
|
602
|
+
this._final = this._final.bind(this);
|
|
603
|
+
}
|
|
604
|
+
//
|
|
605
|
+
// Internal bypass methods - completely avoid prototype chain lookup
|
|
606
|
+
// These directly access _headers Map, safe from dd-trace interception
|
|
607
|
+
//
|
|
608
|
+
_internalGetHeader(name) {
|
|
609
|
+
const value = this._headers.get(name.toLowerCase());
|
|
610
|
+
return value ? String(value) : undefined;
|
|
611
|
+
}
|
|
612
|
+
_internalSetHeader(name, value) {
|
|
613
|
+
if (!this._headersSent) {
|
|
614
|
+
const lowerName = name.toLowerCase();
|
|
615
|
+
this._headers.set(lowerName, value);
|
|
616
|
+
// Also sync kOutHeaders for any code that expects it
|
|
617
|
+
if (kOutHeaders) {
|
|
618
|
+
const outHeaders = this[kOutHeaders];
|
|
619
|
+
if (outHeaders) {
|
|
620
|
+
outHeaders[lowerName] = [name, value];
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
_internalHasHeader(name) {
|
|
626
|
+
return this._headers.has(name.toLowerCase());
|
|
627
|
+
}
|
|
628
|
+
_internalRemoveHeader(name) {
|
|
629
|
+
if (!this._headersSent) {
|
|
630
|
+
const lowerName = name.toLowerCase();
|
|
631
|
+
this._headers.delete(lowerName);
|
|
632
|
+
if (kOutHeaders) {
|
|
633
|
+
const outHeaders = this[kOutHeaders];
|
|
634
|
+
if (outHeaders) {
|
|
635
|
+
delete outHeaders[lowerName];
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
}
|
|
361
639
|
}
|
|
362
640
|
//
|
|
363
641
|
// Header management
|
|
@@ -368,7 +646,16 @@ class LambdaResponseStreaming extends Writable {
|
|
|
368
646
|
// Headers cannot be changed after body starts
|
|
369
647
|
return this;
|
|
370
648
|
}
|
|
371
|
-
|
|
649
|
+
const lowerName = name.toLowerCase();
|
|
650
|
+
this._headers.set(lowerName, String(value));
|
|
651
|
+
// Sync with kOutHeaders for dd-trace compatibility
|
|
652
|
+
// Node stores as { 'header-name': ['Header-Name', value] }
|
|
653
|
+
if (kOutHeaders) {
|
|
654
|
+
const outHeaders = this[kOutHeaders];
|
|
655
|
+
if (outHeaders) {
|
|
656
|
+
outHeaders[lowerName] = [name, String(value)];
|
|
657
|
+
}
|
|
658
|
+
}
|
|
372
659
|
return this;
|
|
373
660
|
}
|
|
374
661
|
getHeader(name) {
|
|
@@ -376,7 +663,15 @@ class LambdaResponseStreaming extends Writable {
|
|
|
376
663
|
}
|
|
377
664
|
removeHeader(name) {
|
|
378
665
|
if (!this._headersSent) {
|
|
379
|
-
|
|
666
|
+
const lowerName = name.toLowerCase();
|
|
667
|
+
this._headers.delete(lowerName);
|
|
668
|
+
// Sync with kOutHeaders for dd-trace compatibility
|
|
669
|
+
if (kOutHeaders) {
|
|
670
|
+
const outHeaders = this[kOutHeaders];
|
|
671
|
+
if (outHeaders) {
|
|
672
|
+
delete outHeaders[lowerName];
|
|
673
|
+
}
|
|
674
|
+
}
|
|
380
675
|
}
|
|
381
676
|
}
|
|
382
677
|
getHeaders() {
|
|
@@ -392,6 +687,49 @@ class LambdaResponseStreaming extends Writable {
|
|
|
392
687
|
getHeaderNames() {
|
|
393
688
|
return Array.from(this._headers.keys());
|
|
394
689
|
}
|
|
690
|
+
/**
|
|
691
|
+
* Proxy for direct header access (e.g., res.headers['content-type']).
|
|
692
|
+
* Required for compatibility with middleware like helmet that access headers directly.
|
|
693
|
+
* Uses direct _headers access to bypass dd-trace interception.
|
|
694
|
+
*/
|
|
695
|
+
get headers() {
|
|
696
|
+
return new Proxy({}, {
|
|
697
|
+
deleteProperty: (_target, prop) => {
|
|
698
|
+
if (!this._headersSent) {
|
|
699
|
+
this._headers.delete(String(prop).toLowerCase());
|
|
700
|
+
}
|
|
701
|
+
return true;
|
|
702
|
+
},
|
|
703
|
+
get: (_target, prop) => {
|
|
704
|
+
if (typeof prop === "symbol")
|
|
705
|
+
return undefined;
|
|
706
|
+
return this._headers.get(String(prop).toLowerCase());
|
|
707
|
+
},
|
|
708
|
+
getOwnPropertyDescriptor: (_target, prop) => {
|
|
709
|
+
const lowerProp = String(prop).toLowerCase();
|
|
710
|
+
if (this._headers.has(lowerProp)) {
|
|
711
|
+
return {
|
|
712
|
+
configurable: true,
|
|
713
|
+
enumerable: true,
|
|
714
|
+
value: this._headers.get(lowerProp),
|
|
715
|
+
};
|
|
716
|
+
}
|
|
717
|
+
return undefined;
|
|
718
|
+
},
|
|
719
|
+
has: (_target, prop) => {
|
|
720
|
+
return this._headers.has(String(prop).toLowerCase());
|
|
721
|
+
},
|
|
722
|
+
ownKeys: () => {
|
|
723
|
+
return Array.from(this._headers.keys());
|
|
724
|
+
},
|
|
725
|
+
set: (_target, prop, value) => {
|
|
726
|
+
if (!this._headersSent) {
|
|
727
|
+
this._headers.set(String(prop).toLowerCase(), value);
|
|
728
|
+
}
|
|
729
|
+
return true;
|
|
730
|
+
},
|
|
731
|
+
});
|
|
732
|
+
}
|
|
395
733
|
writeHead(statusCode, statusMessageOrHeaders, headers) {
|
|
396
734
|
if (this._headersSent) {
|
|
397
735
|
return this;
|
|
@@ -407,9 +745,10 @@ class LambdaResponseStreaming extends Writable {
|
|
|
407
745
|
headersToSet = statusMessageOrHeaders;
|
|
408
746
|
}
|
|
409
747
|
if (headersToSet) {
|
|
748
|
+
// Use direct _headers access to bypass dd-trace interception
|
|
410
749
|
for (const [key, value] of Object.entries(headersToSet)) {
|
|
411
750
|
if (value !== undefined) {
|
|
412
|
-
this.
|
|
751
|
+
this._headers.set(key.toLowerCase(), String(value));
|
|
413
752
|
}
|
|
414
753
|
}
|
|
415
754
|
}
|
|
@@ -443,12 +782,32 @@ class LambdaResponseStreaming extends Writable {
|
|
|
443
782
|
//
|
|
444
783
|
// Express compatibility methods
|
|
445
784
|
//
|
|
785
|
+
/**
|
|
786
|
+
* Express-style alias for getHeader().
|
|
787
|
+
* Used by middleware like decorateResponse that use res.get().
|
|
788
|
+
* Note: Directly accesses _headers to avoid prototype chain issues with bundled code.
|
|
789
|
+
*/
|
|
790
|
+
get(name) {
|
|
791
|
+
return this._headers.get(name.toLowerCase());
|
|
792
|
+
}
|
|
793
|
+
/**
|
|
794
|
+
* Express-style alias for setHeader().
|
|
795
|
+
* Used by middleware like decorateResponse that use res.set().
|
|
796
|
+
* Note: Directly accesses _headers to avoid prototype chain issues with bundled code.
|
|
797
|
+
*/
|
|
798
|
+
set(name, value) {
|
|
799
|
+
if (!this._headersSent) {
|
|
800
|
+
this._headers.set(name.toLowerCase(), String(value));
|
|
801
|
+
}
|
|
802
|
+
return this;
|
|
803
|
+
}
|
|
446
804
|
status(code) {
|
|
447
805
|
this.statusCode = code;
|
|
448
806
|
return this;
|
|
449
807
|
}
|
|
450
808
|
json(data) {
|
|
451
|
-
|
|
809
|
+
// Use direct _headers access to bypass dd-trace interception
|
|
810
|
+
this._headers.set("content-type", "application/json");
|
|
452
811
|
this.end(JSON.stringify(data));
|
|
453
812
|
return this;
|
|
454
813
|
}
|
|
@@ -459,6 +818,27 @@ class LambdaResponseStreaming extends Writable {
|
|
|
459
818
|
this.end(body);
|
|
460
819
|
return this;
|
|
461
820
|
}
|
|
821
|
+
/**
|
|
822
|
+
* Add a field to the Vary response header.
|
|
823
|
+
* Used by CORS middleware to indicate response varies by Origin.
|
|
824
|
+
* Uses direct _headers access to bypass dd-trace interception.
|
|
825
|
+
*/
|
|
826
|
+
vary(field) {
|
|
827
|
+
const existing = this._headers.get("vary");
|
|
828
|
+
if (!existing) {
|
|
829
|
+
this._headers.set("vary", field);
|
|
830
|
+
}
|
|
831
|
+
else {
|
|
832
|
+
// Append to existing Vary header if field not already present
|
|
833
|
+
const fields = String(existing)
|
|
834
|
+
.split(",")
|
|
835
|
+
.map((f) => f.trim().toLowerCase());
|
|
836
|
+
if (!fields.includes(field.toLowerCase())) {
|
|
837
|
+
this._headers.set("vary", `${existing}, ${field}`);
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
return this;
|
|
841
|
+
}
|
|
462
842
|
//
|
|
463
843
|
// Writable stream implementation
|
|
464
844
|
//
|
|
@@ -552,6 +932,7 @@ function runExpressApp(app, req, res) {
|
|
|
552
932
|
*/
|
|
553
933
|
function createLambdaHandler(app, _options) {
|
|
554
934
|
return async (event, context) => {
|
|
935
|
+
let result;
|
|
555
936
|
try {
|
|
556
937
|
// Set current invoke for getCurrentInvokeUuid
|
|
557
938
|
setCurrentInvoke(event, context);
|
|
@@ -561,8 +942,38 @@ function createLambdaHandler(app, _options) {
|
|
|
561
942
|
const res = new LambdaResponseBuffered();
|
|
562
943
|
// Run Express app
|
|
563
944
|
await runExpressApp(app, req, res);
|
|
564
|
-
//
|
|
565
|
-
|
|
945
|
+
// Get Lambda response - await explicitly to ensure we have the result
|
|
946
|
+
result = await res.getResult();
|
|
947
|
+
// Debug: Log the response before returning
|
|
948
|
+
console.log("[createLambdaHandler] Returning response:", JSON.stringify({
|
|
949
|
+
statusCode: result.statusCode,
|
|
950
|
+
headers: result.headers,
|
|
951
|
+
bodyLength: result.body?.length,
|
|
952
|
+
isBase64Encoded: result.isBase64Encoded,
|
|
953
|
+
}));
|
|
954
|
+
return result;
|
|
955
|
+
}
|
|
956
|
+
catch (error) {
|
|
957
|
+
// Log any unhandled errors
|
|
958
|
+
console.error("[createLambdaHandler] Unhandled error:", error);
|
|
959
|
+
if (error instanceof Error) {
|
|
960
|
+
console.error("[createLambdaHandler] Stack:", error.stack);
|
|
961
|
+
}
|
|
962
|
+
// Return a proper error response instead of throwing
|
|
963
|
+
return {
|
|
964
|
+
statusCode: 500,
|
|
965
|
+
headers: { "content-type": "application/json" },
|
|
966
|
+
body: JSON.stringify({
|
|
967
|
+
errors: [
|
|
968
|
+
{
|
|
969
|
+
status: 500,
|
|
970
|
+
title: "Internal Server Error",
|
|
971
|
+
detail: error instanceof Error ? error.message : "Unknown error occurred",
|
|
972
|
+
},
|
|
973
|
+
],
|
|
974
|
+
}),
|
|
975
|
+
isBase64Encoded: false,
|
|
976
|
+
};
|
|
566
977
|
}
|
|
567
978
|
finally {
|
|
568
979
|
// Clear current invoke context
|
|
@@ -915,6 +1326,73 @@ function getCurrentInvokeUuid(req) {
|
|
|
915
1326
|
return getWebAdapterUuid();
|
|
916
1327
|
}
|
|
917
1328
|
|
|
1329
|
+
//
|
|
1330
|
+
//
|
|
1331
|
+
// Helpers
|
|
1332
|
+
//
|
|
1333
|
+
/**
|
|
1334
|
+
* Safely get a header value from response.
|
|
1335
|
+
* Handles both Express Response and Lambda adapter responses.
|
|
1336
|
+
* Defensive against dd-trace instrumentation issues.
|
|
1337
|
+
*/
|
|
1338
|
+
function safeGetHeader(res, name) {
|
|
1339
|
+
try {
|
|
1340
|
+
// Try internal method first (completely bypasses dd-trace)
|
|
1341
|
+
if (typeof res._internalGetHeader === "function") {
|
|
1342
|
+
return res._internalGetHeader(name);
|
|
1343
|
+
}
|
|
1344
|
+
// Fall back to _headers Map access (Lambda adapter, avoids dd-trace)
|
|
1345
|
+
if (res._headers instanceof Map) {
|
|
1346
|
+
const value = res._headers.get(name.toLowerCase());
|
|
1347
|
+
return value ? String(value) : undefined;
|
|
1348
|
+
}
|
|
1349
|
+
// Fall back to getHeader (more standard than get)
|
|
1350
|
+
if (typeof res.getHeader === "function") {
|
|
1351
|
+
const value = res.getHeader(name);
|
|
1352
|
+
return value ? String(value) : undefined;
|
|
1353
|
+
}
|
|
1354
|
+
// Last resort: try get
|
|
1355
|
+
if (typeof res.get === "function") {
|
|
1356
|
+
const value = res.get(name);
|
|
1357
|
+
return value ? String(value) : undefined;
|
|
1358
|
+
}
|
|
1359
|
+
}
|
|
1360
|
+
catch {
|
|
1361
|
+
// Silently fail - caller will handle missing value
|
|
1362
|
+
}
|
|
1363
|
+
return undefined;
|
|
1364
|
+
}
|
|
1365
|
+
/**
|
|
1366
|
+
* Safely set a header value on response.
|
|
1367
|
+
* Handles both Express Response and Lambda adapter responses.
|
|
1368
|
+
* Defensive against dd-trace instrumentation issues.
|
|
1369
|
+
*/
|
|
1370
|
+
function safeSetHeader(res, name, value) {
|
|
1371
|
+
try {
|
|
1372
|
+
// Try internal method first (completely bypasses dd-trace)
|
|
1373
|
+
if (typeof res._internalSetHeader === "function") {
|
|
1374
|
+
res._internalSetHeader(name, value);
|
|
1375
|
+
return;
|
|
1376
|
+
}
|
|
1377
|
+
// Fall back to _headers Map access (Lambda adapter, avoids dd-trace)
|
|
1378
|
+
if (res._headers instanceof Map) {
|
|
1379
|
+
res._headers.set(name.toLowerCase(), value);
|
|
1380
|
+
return;
|
|
1381
|
+
}
|
|
1382
|
+
// Fall back to setHeader (more standard than set)
|
|
1383
|
+
if (typeof res.setHeader === "function") {
|
|
1384
|
+
res.setHeader(name, value);
|
|
1385
|
+
return;
|
|
1386
|
+
}
|
|
1387
|
+
// Last resort: try set
|
|
1388
|
+
if (typeof res.set === "function") {
|
|
1389
|
+
res.set(name, value);
|
|
1390
|
+
}
|
|
1391
|
+
}
|
|
1392
|
+
catch {
|
|
1393
|
+
// Silently fail - header just won't be set
|
|
1394
|
+
}
|
|
1395
|
+
}
|
|
918
1396
|
//
|
|
919
1397
|
//
|
|
920
1398
|
// Main
|
|
@@ -931,36 +1409,37 @@ const decorateResponse = (res, { handler = "", version = process.env.PROJECT_VER
|
|
|
931
1409
|
log$1.warn("decorateResponse called but response is not an object");
|
|
932
1410
|
return;
|
|
933
1411
|
}
|
|
1412
|
+
const extRes = res;
|
|
934
1413
|
try {
|
|
935
1414
|
//
|
|
936
1415
|
//
|
|
937
1416
|
// Decorate Headers
|
|
938
1417
|
//
|
|
939
1418
|
// X-Powered-By, override "Express" but nothing else
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
1419
|
+
const currentPoweredBy = safeGetHeader(extRes, HTTP.HEADER.POWERED_BY);
|
|
1420
|
+
if (!currentPoweredBy || currentPoweredBy === "Express") {
|
|
1421
|
+
safeSetHeader(extRes, HTTP.HEADER.POWERED_BY, JAYPIE.LIB.EXPRESS);
|
|
943
1422
|
}
|
|
944
1423
|
// X-Project-Environment
|
|
945
1424
|
if (process.env.PROJECT_ENV) {
|
|
946
|
-
|
|
1425
|
+
safeSetHeader(extRes, HTTP.HEADER.PROJECT.ENVIRONMENT, process.env.PROJECT_ENV);
|
|
947
1426
|
}
|
|
948
1427
|
// X-Project-Handler
|
|
949
1428
|
if (handler) {
|
|
950
|
-
|
|
1429
|
+
safeSetHeader(extRes, HTTP.HEADER.PROJECT.HANDLER, handler);
|
|
951
1430
|
}
|
|
952
1431
|
// X-Project-Invocation
|
|
953
1432
|
const currentInvoke = getCurrentInvokeUuid();
|
|
954
1433
|
if (currentInvoke) {
|
|
955
|
-
|
|
1434
|
+
safeSetHeader(extRes, HTTP.HEADER.PROJECT.INVOCATION, currentInvoke);
|
|
956
1435
|
}
|
|
957
1436
|
// X-Project-Key
|
|
958
1437
|
if (process.env.PROJECT_KEY) {
|
|
959
|
-
|
|
1438
|
+
safeSetHeader(extRes, HTTP.HEADER.PROJECT.KEY, process.env.PROJECT_KEY);
|
|
960
1439
|
}
|
|
961
1440
|
// X-Project-Version
|
|
962
1441
|
if (version) {
|
|
963
|
-
|
|
1442
|
+
safeSetHeader(extRes, HTTP.HEADER.PROJECT.VERSION, version);
|
|
964
1443
|
}
|
|
965
1444
|
//
|
|
966
1445
|
//
|
|
@@ -1020,6 +1499,82 @@ function summarizeResponse(res, extras) {
|
|
|
1020
1499
|
|
|
1021
1500
|
// Cast logger to extended interface for runtime features not in type definitions
|
|
1022
1501
|
const logger$1 = log;
|
|
1502
|
+
//
|
|
1503
|
+
//
|
|
1504
|
+
// Helpers - Safe response methods to bypass dd-trace interception
|
|
1505
|
+
//
|
|
1506
|
+
/**
|
|
1507
|
+
* Check if response is a Lambda mock response with direct internal access.
|
|
1508
|
+
*/
|
|
1509
|
+
function isLambdaMockResponse(res) {
|
|
1510
|
+
const mock = res;
|
|
1511
|
+
return (mock._headers instanceof Map &&
|
|
1512
|
+
Array.isArray(mock._chunks) &&
|
|
1513
|
+
typeof mock.buildResult === "function");
|
|
1514
|
+
}
|
|
1515
|
+
/**
|
|
1516
|
+
* Safely send a JSON response, avoiding dd-trace interception.
|
|
1517
|
+
* For Lambda mock responses, directly manipulates internal state instead of
|
|
1518
|
+
* using stream methods (write/end) which dd-trace intercepts.
|
|
1519
|
+
*/
|
|
1520
|
+
function safeSendJson(res, statusCode, data) {
|
|
1521
|
+
if (isLambdaMockResponse(res)) {
|
|
1522
|
+
// Use internal method to set header (completely bypasses dd-trace)
|
|
1523
|
+
if (typeof res._internalSetHeader === "function") {
|
|
1524
|
+
res._internalSetHeader("content-type", "application/json");
|
|
1525
|
+
}
|
|
1526
|
+
else {
|
|
1527
|
+
// Fall back to direct _headers manipulation
|
|
1528
|
+
res._headers.set("content-type", "application/json");
|
|
1529
|
+
}
|
|
1530
|
+
res.statusCode = statusCode;
|
|
1531
|
+
// Directly push to chunks array instead of using stream write/end
|
|
1532
|
+
const chunk = Buffer.from(JSON.stringify(data));
|
|
1533
|
+
res._chunks.push(chunk);
|
|
1534
|
+
res._headersSent = true;
|
|
1535
|
+
// Signal completion if a promise is waiting
|
|
1536
|
+
if (res._resolve) {
|
|
1537
|
+
res._resolve(res.buildResult());
|
|
1538
|
+
}
|
|
1539
|
+
// Emit "finish" event so runExpressApp's promise resolves
|
|
1540
|
+
console.log("[safeSendJson] Emitting finish event");
|
|
1541
|
+
res.emit("finish");
|
|
1542
|
+
return;
|
|
1543
|
+
}
|
|
1544
|
+
// Fall back to standard Express methods for real responses
|
|
1545
|
+
res.status(statusCode).json(data);
|
|
1546
|
+
}
|
|
1547
|
+
/**
|
|
1548
|
+
* Safely send a response body, avoiding dd-trace interception.
|
|
1549
|
+
* For Lambda mock responses, directly manipulates internal state instead of
|
|
1550
|
+
* using stream methods (write/end) which dd-trace intercepts.
|
|
1551
|
+
*/
|
|
1552
|
+
function safeSend(res, statusCode, body) {
|
|
1553
|
+
if (isLambdaMockResponse(res)) {
|
|
1554
|
+
// Direct internal state manipulation - bypasses dd-trace completely
|
|
1555
|
+
res.statusCode = statusCode;
|
|
1556
|
+
if (body !== undefined) {
|
|
1557
|
+
const chunk = Buffer.from(body);
|
|
1558
|
+
res._chunks.push(chunk);
|
|
1559
|
+
}
|
|
1560
|
+
res._headersSent = true;
|
|
1561
|
+
// Signal completion if a promise is waiting
|
|
1562
|
+
if (res._resolve) {
|
|
1563
|
+
res._resolve(res.buildResult());
|
|
1564
|
+
}
|
|
1565
|
+
// Emit "finish" event so runExpressApp's promise resolves
|
|
1566
|
+
console.log("[safeSend] Emitting finish event");
|
|
1567
|
+
res.emit("finish");
|
|
1568
|
+
return;
|
|
1569
|
+
}
|
|
1570
|
+
// Fall back to standard Express methods for real responses
|
|
1571
|
+
if (body !== undefined) {
|
|
1572
|
+
res.status(statusCode).send(body);
|
|
1573
|
+
}
|
|
1574
|
+
else {
|
|
1575
|
+
res.status(statusCode).send();
|
|
1576
|
+
}
|
|
1577
|
+
}
|
|
1023
1578
|
function expressHandler(handlerOrOptions, optionsOrHandler) {
|
|
1024
1579
|
/* eslint-enable no-redeclare */
|
|
1025
1580
|
let handler;
|
|
@@ -1236,30 +1791,30 @@ function expressHandler(handlerOrOptions, optionsOrHandler) {
|
|
|
1236
1791
|
if (typeof response === "object") {
|
|
1237
1792
|
if (typeof response.json ===
|
|
1238
1793
|
"function") {
|
|
1239
|
-
res
|
|
1794
|
+
safeSendJson(res, status, response.json());
|
|
1240
1795
|
}
|
|
1241
1796
|
else {
|
|
1242
|
-
res
|
|
1797
|
+
safeSendJson(res, status, response);
|
|
1243
1798
|
}
|
|
1244
1799
|
}
|
|
1245
1800
|
else if (typeof response === "string") {
|
|
1246
1801
|
try {
|
|
1247
|
-
res
|
|
1802
|
+
safeSendJson(res, status, JSON.parse(response));
|
|
1248
1803
|
}
|
|
1249
1804
|
catch {
|
|
1250
|
-
res
|
|
1805
|
+
safeSend(res, status, response);
|
|
1251
1806
|
}
|
|
1252
1807
|
}
|
|
1253
1808
|
else if (response === true) {
|
|
1254
|
-
res
|
|
1809
|
+
safeSend(res, HTTP.CODE.CREATED);
|
|
1255
1810
|
}
|
|
1256
1811
|
else {
|
|
1257
|
-
res
|
|
1812
|
+
safeSend(res, status, response);
|
|
1258
1813
|
}
|
|
1259
1814
|
}
|
|
1260
1815
|
else {
|
|
1261
1816
|
// No response
|
|
1262
|
-
res
|
|
1817
|
+
safeSend(res, HTTP.CODE.NO_CONTENT);
|
|
1263
1818
|
}
|
|
1264
1819
|
}
|
|
1265
1820
|
else {
|
|
@@ -1276,7 +1831,11 @@ function expressHandler(handlerOrOptions, optionsOrHandler) {
|
|
|
1276
1831
|
}
|
|
1277
1832
|
}
|
|
1278
1833
|
catch (error) {
|
|
1279
|
-
|
|
1834
|
+
// Use console.error for raw stack trace to ensure it appears in CloudWatch
|
|
1835
|
+
if (error instanceof Error) {
|
|
1836
|
+
console.error("Express response error stack trace:", error.stack);
|
|
1837
|
+
}
|
|
1838
|
+
log.fatal(`Express encountered an error while sending the response: ${error instanceof Error ? error.message : String(error)}`);
|
|
1280
1839
|
log.var({ responseError: error });
|
|
1281
1840
|
}
|
|
1282
1841
|
// Log response
|