@jaypie/express 1.2.32 → 1.2.34

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.
@@ -36,6 +36,13 @@ export declare class LambdaResponseBuffered extends Writable {
36
36
  */
37
37
  get headers(): Record<string, string | string[] | undefined>;
38
38
  writeHead(statusCode: number, statusMessageOrHeaders?: OutgoingHttpHeaders | string, headers?: OutgoingHttpHeaders): this;
39
+ /**
40
+ * Node's implicit-header hook, called before the first body byte commits.
41
+ * Routes through this.writeHead resolved at call time so wrappers installed
42
+ * mid-request (on-headers, and through it express-session, morgan,
43
+ * compression, express-openid-connect) run their listeners.
44
+ */
45
+ _implicitHeader(): void;
39
46
  get headersSent(): boolean;
40
47
  /**
41
48
  * Express-style alias for getHeader().
@@ -36,8 +36,16 @@ export declare class LambdaResponseStreaming extends Writable {
36
36
  */
37
37
  get headers(): Record<string, string | string[] | undefined>;
38
38
  writeHead(statusCode: number, statusMessageOrHeaders?: OutgoingHttpHeaders | string, headers?: OutgoingHttpHeaders): this;
39
+ /**
40
+ * Node's implicit-header hook, called before the first body byte commits.
41
+ * Routes through this.writeHead resolved at call time so wrappers installed
42
+ * mid-request (on-headers, and through it express-session, morgan,
43
+ * compression, express-openid-connect) run their listeners.
44
+ */
45
+ _implicitHeader(): void;
39
46
  get headersSent(): boolean;
40
47
  flushHeaders(): void;
48
+ private _commitHeaders;
41
49
  /**
42
50
  * Express-style alias for getHeader().
43
51
  * Used by middleware like decorateResponse that use res.get().
@@ -0,0 +1,15 @@
1
+ export declare const SET_COOKIE = "set-cookie";
2
+ /**
3
+ * Normalize a header value for storage, preserving arrays.
4
+ * Node's ServerResponse keeps multi-value headers as arrays; stringifying
5
+ * here would fold them into a single comma-separated value.
6
+ */
7
+ export declare function normalizeHeaderValue(value: number | string | string[]): string | string[];
8
+ /**
9
+ * Split stored headers into a single-value header record and a cookies
10
+ * array, matching the Lambda Function URL (v2) response shape.
11
+ */
12
+ export declare function splitCookieHeaders(source: Map<string, string | string[]>): {
13
+ cookies: string[];
14
+ headers: Record<string, string>;
15
+ };
@@ -82,6 +82,7 @@ export interface ResponseStream {
82
82
  write(chunk: string | Uint8Array): void;
83
83
  }
84
84
  export interface HttpResponseStreamMetadata {
85
+ cookies?: string[];
85
86
  headers: Record<string, string>;
86
87
  statusCode: number;
87
88
  }
@@ -278,6 +278,50 @@ function createLambdaRequest(event, context) {
278
278
  });
279
279
  }
280
280
 
281
+ //
282
+ //
283
+ // Constants
284
+ //
285
+ // Set-Cookie is the one header that must never be comma-folded: cookie
286
+ // expiry dates contain commas, so a folded value is unparseable. Lambda
287
+ // carries these out-of-band in the `cookies` field of the v2 response
288
+ // payload and of the response-streaming metadata prelude.
289
+ const SET_COOKIE = "set-cookie";
290
+ //
291
+ //
292
+ // Functions
293
+ //
294
+ /**
295
+ * Normalize a header value for storage, preserving arrays.
296
+ * Node's ServerResponse keeps multi-value headers as arrays; stringifying
297
+ * here would fold them into a single comma-separated value.
298
+ */
299
+ function normalizeHeaderValue(value) {
300
+ return Array.isArray(value) ? value.map(String) : String(value);
301
+ }
302
+ /**
303
+ * Split stored headers into a single-value header record and a cookies
304
+ * array, matching the Lambda Function URL (v2) response shape.
305
+ */
306
+ function splitCookieHeaders(source) {
307
+ const cookies = [];
308
+ const headers = {};
309
+ for (const [key, value] of source) {
310
+ if (key === SET_COOKIE) {
311
+ if (Array.isArray(value)) {
312
+ cookies.push(...value);
313
+ }
314
+ else {
315
+ cookies.push(value);
316
+ }
317
+ }
318
+ else {
319
+ headers[key] = Array.isArray(value) ? value.join(", ") : String(value);
320
+ }
321
+ }
322
+ return { cookies, headers };
323
+ }
324
+
281
325
  //
282
326
  //
283
327
  // Constants
@@ -340,6 +384,7 @@ class LambdaResponseBuffered extends node_stream.Writable {
340
384
  this.getHeaders = this.getHeaders.bind(this);
341
385
  this.getHeaderNames = this.getHeaderNames.bind(this);
342
386
  this.writeHead = this.writeHead.bind(this);
387
+ this._implicitHeader = this._implicitHeader.bind(this);
343
388
  this.get = this.get.bind(this);
344
389
  this.set = this.set.bind(this);
345
390
  this.status = this.status.bind(this);
@@ -415,13 +460,14 @@ class LambdaResponseBuffered extends node_stream.Writable {
415
460
  return this;
416
461
  }
417
462
  const lowerName = name.toLowerCase();
418
- this._headers.set(lowerName, String(value));
463
+ const normalized = normalizeHeaderValue(value);
464
+ this._headers.set(lowerName, normalized);
419
465
  // Sync with kOutHeaders for dd-trace compatibility
420
466
  // Node stores as { 'header-name': ['Header-Name', value] }
421
467
  if (kOutHeaders$1) {
422
468
  const outHeaders = this[kOutHeaders$1];
423
469
  if (outHeaders) {
424
- outHeaders[lowerName] = [name, String(value)];
470
+ outHeaders[lowerName] = [name, normalized];
425
471
  }
426
472
  }
427
473
  return this;
@@ -495,6 +541,9 @@ class LambdaResponseBuffered extends node_stream.Writable {
495
541
  });
496
542
  }
497
543
  writeHead(statusCode, statusMessageOrHeaders, headers) {
544
+ if (this._headersSent) {
545
+ return this;
546
+ }
498
547
  this.statusCode = statusCode;
499
548
  let headersToSet;
500
549
  if (typeof statusMessageOrHeaders === "string") {
@@ -509,13 +558,24 @@ class LambdaResponseBuffered extends node_stream.Writable {
509
558
  // Use direct _headers access to bypass dd-trace interception
510
559
  for (const [key, value] of Object.entries(headersToSet)) {
511
560
  if (value !== undefined) {
512
- this._headers.set(key.toLowerCase(), String(value));
561
+ this._headers.set(key.toLowerCase(), normalizeHeaderValue(value));
513
562
  }
514
563
  }
515
564
  }
516
565
  this._headersSent = true;
517
566
  return this;
518
567
  }
568
+ /**
569
+ * Node's implicit-header hook, called before the first body byte commits.
570
+ * Routes through this.writeHead resolved at call time so wrappers installed
571
+ * mid-request (on-headers, and through it express-session, morgan,
572
+ * compression, express-openid-connect) run their listeners.
573
+ */
574
+ _implicitHeader() {
575
+ if (!this._headersSent) {
576
+ this.writeHead(this.statusCode);
577
+ }
578
+ }
519
579
  get headersSent() {
520
580
  return this._headersSent;
521
581
  }
@@ -537,7 +597,7 @@ class LambdaResponseBuffered extends node_stream.Writable {
537
597
  */
538
598
  set(name, value) {
539
599
  if (!this._headersSent) {
540
- this._headers.set(name.toLowerCase(), String(value));
600
+ this._headers.set(name.toLowerCase(), normalizeHeaderValue(value));
541
601
  }
542
602
  return this;
543
603
  }
@@ -587,11 +647,13 @@ class LambdaResponseBuffered extends node_stream.Writable {
587
647
  const buffer = Buffer.isBuffer(chunk)
588
648
  ? chunk
589
649
  : Buffer.from(chunk, encoding);
650
+ this._implicitHeader();
590
651
  this._chunks.push(buffer);
591
652
  this._headersSent = true;
592
653
  callback();
593
654
  }
594
655
  _final(callback) {
656
+ this._implicitHeader();
595
657
  this._ended = true;
596
658
  if (this._resolve) {
597
659
  this._resolve(this.buildResult());
@@ -607,23 +669,8 @@ class LambdaResponseBuffered extends node_stream.Writable {
607
669
  const contentType = this._headers.get("content-type") || "";
608
670
  // Determine if response should be base64 encoded
609
671
  const isBase64Encoded = this.isBinaryContentType(contentType);
610
- // Build headers object
611
- const headers = {};
612
- const cookies = [];
613
- for (const [key, value] of this._headers) {
614
- if (key === "set-cookie") {
615
- // Collect Set-Cookie headers for v2 response format
616
- if (Array.isArray(value)) {
617
- cookies.push(...value);
618
- }
619
- else {
620
- cookies.push(value);
621
- }
622
- }
623
- else {
624
- headers[key] = Array.isArray(value) ? value.join(", ") : String(value);
625
- }
626
- }
672
+ // Build headers object, carrying Set-Cookie out-of-band (v2 format)
673
+ const { cookies, headers } = splitCookieHeaders(this._headers);
627
674
  const result = {
628
675
  body: isBase64Encoded ? body.toString("base64") : body.toString("utf8"),
629
676
  headers,
@@ -698,6 +745,8 @@ class LambdaResponseStreaming extends node_stream.Writable {
698
745
  this.getHeaderNames = this.getHeaderNames.bind(this);
699
746
  this.writeHead = this.writeHead.bind(this);
700
747
  this.flushHeaders = this.flushHeaders.bind(this);
748
+ this._implicitHeader = this._implicitHeader.bind(this);
749
+ this._commitHeaders = this._commitHeaders.bind(this);
701
750
  this.get = this.get.bind(this);
702
751
  this.set = this.set.bind(this);
703
752
  this.status = this.status.bind(this);
@@ -756,13 +805,14 @@ class LambdaResponseStreaming extends node_stream.Writable {
756
805
  return this;
757
806
  }
758
807
  const lowerName = name.toLowerCase();
759
- this._headers.set(lowerName, String(value));
808
+ const normalized = normalizeHeaderValue(value);
809
+ this._headers.set(lowerName, normalized);
760
810
  // Sync with kOutHeaders for dd-trace compatibility
761
811
  // Node stores as { 'header-name': ['Header-Name', value] }
762
812
  if (kOutHeaders) {
763
813
  const outHeaders = this[kOutHeaders];
764
814
  if (outHeaders) {
765
- outHeaders[lowerName] = [name, String(value)];
815
+ outHeaders[lowerName] = [name, normalized];
766
816
  }
767
817
  }
768
818
  return this;
@@ -857,24 +907,36 @@ class LambdaResponseStreaming extends node_stream.Writable {
857
907
  // Use direct _headers access to bypass dd-trace interception
858
908
  for (const [key, value] of Object.entries(headersToSet)) {
859
909
  if (value !== undefined) {
860
- this._headers.set(key.toLowerCase(), String(value));
910
+ this._headers.set(key.toLowerCase(), normalizeHeaderValue(value));
861
911
  }
862
912
  }
863
913
  }
864
- this.flushHeaders();
914
+ this._commitHeaders();
865
915
  return this;
866
916
  }
917
+ /**
918
+ * Node's implicit-header hook, called before the first body byte commits.
919
+ * Routes through this.writeHead resolved at call time so wrappers installed
920
+ * mid-request (on-headers, and through it express-session, morgan,
921
+ * compression, express-openid-connect) run their listeners.
922
+ */
923
+ _implicitHeader() {
924
+ if (!this._headersSent) {
925
+ this.writeHead(this.statusCode);
926
+ }
927
+ }
867
928
  get headersSent() {
868
929
  return this._headersSent;
869
930
  }
870
931
  flushHeaders() {
932
+ this._implicitHeader();
933
+ }
934
+ _commitHeaders() {
871
935
  if (this._headersSent) {
872
936
  return;
873
937
  }
874
- const headers = {};
875
- for (const [key, value] of this._headers) {
876
- headers[key] = Array.isArray(value) ? value.join(", ") : String(value);
877
- }
938
+ // Set-Cookie travels in metadata.cookies, never folded into headers
939
+ const { cookies, headers } = splitCookieHeaders(this._headers);
878
940
  // Lambda streaming requires body content for metadata to be transmitted.
879
941
  // Convert 204 No Content to 200 OK with empty JSON body as workaround.
880
942
  // See: https://github.com/finlaysonstudio/jaypie/issues/178
@@ -889,6 +951,10 @@ class LambdaResponseStreaming extends node_stream.Writable {
889
951
  headers,
890
952
  statusCode,
891
953
  };
954
+ // Only include cookies if present (v2 format)
955
+ if (cookies.length > 0) {
956
+ metadata.cookies = cookies;
957
+ }
892
958
  // Create wrapped stream with metadata
893
959
  this._wrappedStream = awslambda.HttpResponseStream.from(this._responseStream, metadata);
894
960
  this._headersSent = true;
@@ -917,7 +983,7 @@ class LambdaResponseStreaming extends node_stream.Writable {
917
983
  */
918
984
  set(name, value) {
919
985
  if (!this._headersSent) {
920
- this._headers.set(name.toLowerCase(), String(value));
986
+ this._headers.set(name.toLowerCase(), normalizeHeaderValue(value));
921
987
  }
922
988
  return this;
923
989
  }
@@ -971,7 +1037,7 @@ class LambdaResponseStreaming extends node_stream.Writable {
971
1037
  // Buffer writes until headers are sent
972
1038
  this._pendingWrites.push({ callback: () => callback(), chunk: buffer });
973
1039
  // Auto-flush headers on first write
974
- this.flushHeaders();
1040
+ this._implicitHeader();
975
1041
  }
976
1042
  else {
977
1043
  this._wrappedStream.write(buffer);
@@ -979,9 +1045,7 @@ class LambdaResponseStreaming extends node_stream.Writable {
979
1045
  }
980
1046
  }
981
1047
  _final(callback) {
982
- if (!this._headersSent) {
983
- this.flushHeaders();
984
- }
1048
+ this._implicitHeader();
985
1049
  // For converted 204 responses, write empty JSON body
986
1050
  // Lambda streaming requires body content for metadata to be transmitted
987
1051
  if (this._convertedFrom204 && this._wrappedStream) {