@amritk/nish-x86_64-linux 0.28.0 → 0.30.0

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/std/net/http1.ts CHANGED
@@ -81,6 +81,7 @@
81
81
  * Written from RFC 9112 and RFC 9110, not ported from another implementation.
82
82
  */
83
83
  import { copyBytes } from "nish/bytes"
84
+ import { extract, loadU8x16, u8x16 } from "nish:simd"
84
85
 
85
86
  /** `next()`: the input so far holds no further event; feed more. */
86
87
  export const HTTP1_NEED_MORE: i32 = 0
@@ -88,7 +89,12 @@ export const HTTP1_NEED_MORE: i32 = 0
88
89
  export const HTTP1_HEAD: i32 = 1
89
90
  /** `next()`: `bodyLen` body bytes are at `body[bodyOff ..]`, valid until the next `feed`. */
90
91
  export const HTTP1_BODY: i32 = 2
91
- /** `next()`: the request's body is complete. */
92
+ /**
93
+ * `next()`: the request's body is complete. Its head (method, target,
94
+ * version, fields, `keepAlive`) still reads as this request's until the next
95
+ * call to `next()`, so a response written at `HTTP1_END` sees the request it
96
+ * answers and never the pipelined one behind it.
97
+ */
92
98
  export const HTTP1_END: i32 = 3
93
99
  /** `next()`, from now on: the request switched protocols; see `upgradeBytes()`. */
94
100
  export const HTTP1_UPGRADE: i32 = 4
@@ -144,7 +150,7 @@ export class Http1Parser {
144
150
  // The fields are declared widest first, references before numbers before
145
151
  // flags, so the struct carries no padding; the groups say what each is.
146
152
 
147
- // --- the request: valid from its `HTTP1_HEAD` until the next request's -----
153
+ // --- the request: valid from its `HTTP1_HEAD` until the `next()` after its `HTTP1_END`
148
154
  method: string = ""
149
155
  target: string = ""
150
156
  /** The field names, in lowercase, in the order they arrived; `values` beside them. */
@@ -213,11 +219,22 @@ export class Http1Parser {
213
219
  /** The line `http1TakeLine` found is `buf[lineStart .. lineEnd)`, its CRLF dropped. */
214
220
  lineStart: i32 = 0
215
221
  lineEnd: i32 = 0
216
-
222
+ /**
223
+ * Whether the line being scanned holds a byte a field value may not: a
224
+ * control byte other than HTAB, or DEL (RFC 9110 §5.5). `http1TakeLine`
225
+ * sets it as it looks for the line's end, so a field line's bytes are read
226
+ * once, not once for the LF and again for the check.
227
+ */
228
+ lineControl: boolean = false
217
229
  chunked: boolean = false
218
230
  keepAlive: boolean = false
219
231
  /** Whether the request asked to switch protocols (`upgrade` is its value when `keepText`). */
220
232
  upgrading: boolean = false
233
+ /**
234
+ * Whether a kept-alive request ended and its head is still held for the
235
+ * response: the next `next()` clears it before it reads a byte more.
236
+ */
237
+ headHeld: boolean = false
221
238
  /**
222
239
  * Whether `method`, `target`, `names`, `values` and `upgrade` are filled as
223
240
  * strings. A server that must allocate nothing per request turns it off and
@@ -361,7 +378,7 @@ export class Http1Parser {
361
378
  * next feed and head make or the caller puts back first.
362
379
  */
363
380
  resting(): boolean {
364
- return this.state === HTTP1_S_REQUEST_LINE && this.end === this.start
381
+ return this.state === HTTP1_S_REQUEST_LINE && this.end === this.start && !this.headHeld
365
382
  }
366
383
 
367
384
  /** How many bytes were fed and not consumed yet: what a caller bounds its reads by. */
@@ -485,6 +502,61 @@ const http1StartRequest = (p: Http1Parser): void => {
485
502
  p.bodyTotal = 0
486
503
  p.sectionBytes = 0
487
504
  p.fieldCount = 0
505
+ p.headHeld = false
506
+ }
507
+
508
+ /**
509
+ * Whether any of the eight bytes of `x` is below 32 or is 127: a control
510
+ * byte or DEL, which `http1TakeLine` stops at. Two of the bit tricks of
511
+ * "Bit Twiddling Hacks" (S. E. Anderson, public domain): a byte below `n`
512
+ * leaves its high bit set in `(x - n * ones) & ~x`, for `n` up to 128, and
513
+ * `127` is the byte that is zero in `x ^ 127 * ones`. A byte from 128 up
514
+ * has its high bit set in `x`, so `~x` clears it: obs-text never matches.
515
+ * The answer is exact for the word, though not for which byte. Each
516
+ * subtraction is masked to 64 bits because a `u64` is a BigInt under Node,
517
+ * which does not wrap; natively the mask is a no-op.
518
+ */
519
+ const http1WordHasControl = (x: u64): boolean => {
520
+ const ones32: u64 = toU64(16843009)
521
+ const ones: u64 = (ones32 << toU64(32)) | ones32
522
+ const low32: u64 = 0xffffffff
523
+ const all: u64 = (low32 << toU64(32)) | low32
524
+ const high: u64 = ones * toU64(128)
525
+ const below: u64 = (x - ones * toU64(32)) & all & ~x & high
526
+ const y: u64 = x ^ (ones * toU64(127))
527
+ const del: u64 = (y - ones) & all & ~y & high
528
+ return (below | del) !== toU64(0)
529
+ }
530
+
531
+ /**
532
+ * Lanes `at` to `at + 7` of `v`, the first the lowest byte: eight bytes of a
533
+ * sixteen-byte load as one word, which `opt` folds to a bitcast of the
534
+ * vector's half. One load is one bounds check for sixteen bytes, where eight
535
+ * reads of `buf[i + k]` would be eight.
536
+ */
537
+ const http1Lanes = (v: u8x16, at: i32): u64 => {
538
+ if (at === 0) {
539
+ return (
540
+ toU64(extract(v, 0)) |
541
+ (toU64(extract(v, 1)) << toU64(8)) |
542
+ (toU64(extract(v, 2)) << toU64(16)) |
543
+ (toU64(extract(v, 3)) << toU64(24)) |
544
+ (toU64(extract(v, 4)) << toU64(32)) |
545
+ (toU64(extract(v, 5)) << toU64(40)) |
546
+ (toU64(extract(v, 6)) << toU64(48)) |
547
+ (toU64(extract(v, 7)) << toU64(56))
548
+ )
549
+ }
550
+ return (
551
+ toU64(extract(v, 8)) |
552
+ (toU64(extract(v, 9)) << toU64(8)) |
553
+ (toU64(extract(v, 10)) << toU64(16)) |
554
+ (toU64(extract(v, 11)) << toU64(24)) |
555
+ (toU64(extract(v, 12)) << toU64(32)) |
556
+ (toU64(extract(v, 13)) << toU64(40)) |
557
+ (toU64(extract(v, 14)) << toU64(48)) |
558
+ (toU64(extract(v, 15)) << toU64(56))
559
+ )
488
560
  }
489
561
 
490
562
  /** `http1TakeLine`'s answer when a whole line is ready; never an event. */
@@ -494,14 +566,42 @@ const HTTP1_LINE: i32 = -1
494
566
  * Finds the next line: answers `HTTP1_LINE` with it in `lineStart ..
495
567
  * lineEnd`, `HTTP1_NEED_MORE` when no LF has arrived yet, or the refusal. A line ends in CRLF and in
496
568
  * nothing else; a LF without its CR is a 400. A line that has not ended by
497
- * `limit` bytes is refused with `overStatus`.
569
+ * `limit` bytes is refused with `overStatus`. On the way it sets
570
+ * `lineControl` when the line holds a byte no field value may, which
571
+ * `http1FieldLine` reads instead of scanning the value again.
498
572
  */
499
573
  const http1TakeLine = (p: Http1Parser, limit: i32, overStatus: i32): i32 => {
500
574
  const buf: u8[] = p.buf
501
575
  const end: i32 = p.end
502
- let i: i32 = p.scan > p.start ? p.scan : p.start
503
- while (i >= 0 && i < end && i < toI32(buf.length)) {
504
- if (buf[i] === 10) {
576
+ let i: i32 = p.start
577
+ if (p.scan > p.start) {
578
+ i = p.scan
579
+ } else {
580
+ p.lineControl = false
581
+ }
582
+ const stop: i32 = end < toI32(buf.length) ? end : toI32(buf.length)
583
+ if (i < 0) {
584
+ i = 0
585
+ }
586
+ while (i < stop) {
587
+ // Most bytes are printable: sixteen at a time while a whole vector is,
588
+ // then one at a time, with no test in either loop but the bytes' and the
589
+ // bound's.
590
+ while (i >= 0 && i <= stop - 16 && i <= toI32(buf.length) - 16) {
591
+ const v: u8x16 = loadU8x16(buf, i)
592
+ if (http1WordHasControl(http1Lanes(v, 0)) || http1WordHasControl(http1Lanes(v, 8))) {
593
+ break
594
+ }
595
+ i += 16
596
+ }
597
+ while (i >= 0 && i < stop && i < toI32(buf.length) && buf[i] >= 32 && buf[i] !== 127) {
598
+ i += 1
599
+ }
600
+ if (i < 0 || i >= stop || i >= toI32(buf.length)) {
601
+ break
602
+ }
603
+ const c: i32 = toI32(buf[i])
604
+ if (c === 10) {
505
605
  if (i === p.start || buf[i - 1] !== 13) {
506
606
  return http1Fail(p, 400, "a line ended in a bare LF")
507
607
  }
@@ -514,31 +614,49 @@ const http1TakeLine = (p: Http1Parser, limit: i32, overStatus: i32): i32 => {
514
614
  p.scan = i + 1
515
615
  return HTTP1_LINE
516
616
  }
617
+ if (c === 13 && i + 1 >= stop) {
618
+ // A CR at the end of the bytes so far: whether it ends the line is
619
+ // the next byte's to say, so the next call looks at it again.
620
+ break
621
+ }
622
+ if (c !== 9 && !(c === 13 && i + 1 < toI32(buf.length) && buf[i + 1] === 10)) {
623
+ p.lineControl = true
624
+ }
517
625
  i += 1
518
626
  }
519
- p.scan = end
627
+ p.scan = i
520
628
  if (end - p.start > limit + 1) {
521
629
  return http1Fail(p, overStatus, "a line over its limit")
522
630
  }
523
631
  return HTTP1_NEED_MORE
524
632
  }
525
633
 
634
+ /**
635
+ * The tchars of RFC 9110 §5.6.2 as three 32-bit masks, bit `c - 32`,
636
+ * `c - 64` and `c - 96` of each: `!` `#`-`'` `*` `+` `-` `.` and the digits
637
+ * in the first (0x03ff6cfa); the upper-case letters, `^` and `_` in the
638
+ * second (0xc7fffffe); `` ` ``, the lower-case letters, `|` and `~` in the
639
+ * third (0x57ffffff). No byte below 33 or above 126 is a tchar. One shift
640
+ * and mask replaces a chain of fourteen comparisons on the parser's hottest
641
+ * path (`bench/techempower`).
642
+ */
643
+ const HTTP1_TCHAR_32: i32 = 67071226
644
+ const HTTP1_TCHAR_64: i32 = -939524098
645
+ const HTTP1_TCHAR_96: i32 = 1476395007
646
+
526
647
  /** Whether `c` is a tchar of RFC 9110 §5.6.2, the bytes a token is made of. */
527
- export const http1IsTokenByte = (c: i32): boolean =>
528
- (c >= 97 && c <= 122) ||
529
- (c >= 65 && c <= 90) ||
530
- (c >= 48 && c <= 57) ||
531
- c === 33 ||
532
- (c >= 35 && c <= 39) ||
533
- c === 42 ||
534
- c === 43 ||
535
- c === 45 ||
536
- c === 46 ||
537
- c === 94 ||
538
- c === 95 ||
539
- c === 96 ||
540
- c === 124 ||
541
- c === 126
648
+ export const http1IsTokenByte = (c: i32): boolean => {
649
+ if (c < 32 || c > 127) {
650
+ return false
651
+ }
652
+ let mask: i32 = HTTP1_TCHAR_96
653
+ if (c < 64) {
654
+ mask = HTTP1_TCHAR_32
655
+ } else if (c < 96) {
656
+ mask = HTTP1_TCHAR_64
657
+ }
658
+ return ((mask >> (c & 31)) & 1) !== 0
659
+ }
542
660
 
543
661
  /** Whether `c` may appear in a field value (RFC 9110 §5.5): HTAB, SP, VCHAR or obs-text. */
544
662
  export const http1IsValueByte = (c: i32): boolean => c === 9 || (c >= 32 && c !== 127)
@@ -709,6 +827,57 @@ const http1Tally = (p: Http1Parser, name: string, word: string): Http1ListTally
709
827
  return tally
710
828
  }
711
829
 
830
+ /** `http1ConnectionOptions`: the Connection list names `close`. */
831
+ const HTTP1_CONNECTION_CLOSE: i32 = 1
832
+ /** `http1ConnectionOptions`: the Connection list names `keep-alive`. */
833
+ const HTTP1_CONNECTION_KEEP_ALIVE: i32 = 2
834
+ /** `http1ConnectionOptions`: the Connection list names `upgrade`. */
835
+ const HTTP1_CONNECTION_UPGRADE: i32 = 4
836
+
837
+ /**
838
+ * Which of `close`, `keep-alive` and `upgrade` the Connection fields name,
839
+ * in any case, as `HTTP1_CONNECTION_` bits: one walk of every Connection
840
+ * field's list, where three `http1Tally` walks would each read it all. An
841
+ * element is trimmed of OWS and compared whole, as `http1TallyRun` compares.
842
+ */
843
+ const http1ConnectionOptions = (p: Http1Parser): i32 => {
844
+ const spans: i32[] = p.spans
845
+ const head: u8[] = p.head
846
+ let found: i32 = 0
847
+ for (let i: i32 = 0; i < p.headerCount && 4 * i + 3 < toI32(spans.length); i += 1) {
848
+ if (!http1HeadIs(head, spans[4 * i], spans[4 * i + 1], "connection", false)) {
849
+ continue
850
+ }
851
+ const to: i32 = spans[4 * i + 3]
852
+ let start: i32 = spans[4 * i + 2]
853
+ for (let k: i32 = start; k <= to; k += 1) {
854
+ const c: i32 = k < to && k >= 0 && k < toI32(head.length) ? toI32(head[k]) : 44
855
+ if (c !== 44) {
856
+ continue
857
+ }
858
+ let a: i32 = start
859
+ let b: i32 = k
860
+ while (a >= 0 && a < b && a < toI32(head.length) && http1IsOws(toI32(head[a]))) {
861
+ a += 1
862
+ }
863
+ while (b > a && b - 1 >= 0 && b - 1 < toI32(head.length) && http1IsOws(toI32(head[b - 1]))) {
864
+ b -= 1
865
+ }
866
+ // The length picks the one word an element could be.
867
+ const n: i32 = b - a
868
+ if (n === 5 && http1HeadIs(head, a, b, "close", true)) {
869
+ found = found | HTTP1_CONNECTION_CLOSE
870
+ } else if (n === 10 && http1HeadIs(head, a, b, "keep-alive", true)) {
871
+ found = found | HTTP1_CONNECTION_KEEP_ALIVE
872
+ } else if (n === 7 && http1HeadIs(head, a, b, "upgrade", true)) {
873
+ found = found | HTTP1_CONNECTION_UPGRADE
874
+ }
875
+ start = k + 1
876
+ }
877
+ }
878
+ return found
879
+ }
880
+
712
881
  /**
713
882
  * Whether the buffered start of a request line holds more than
714
883
  * `HTTP1_MAX_METHOD` bytes before its first space (or line end).
@@ -817,10 +986,10 @@ const http1FieldLine = (p: Http1Parser, keep: boolean): i32 => {
817
986
  return http1Fail(p, 400, "a malformed field name")
818
987
  }
819
988
  const nameEnd: i32 = i
820
- for (let k: i32 = nameEnd + 1; k >= 0 && k < b && k < toI32(buf.length); k += 1) {
821
- if (!http1IsValueByte(toI32(buf[k]))) {
822
- return http1Fail(p, 400, "a control character in a field value")
823
- }
989
+ // The name is tchars and the colon is field text, so a byte no value may
990
+ // hold anywhere on the line is one in the value.
991
+ if (p.lineControl) {
992
+ return http1Fail(p, 400, "a control character in a field value")
824
993
  }
825
994
  p.fieldCount += 1
826
995
  if (p.fieldCount > p.maxHeaders) {
@@ -845,9 +1014,13 @@ const http1FieldLine = (p: Http1Parser, keep: boolean): i32 => {
845
1014
  }
846
1015
  const base: i32 = 4 * p.headerCount
847
1016
  http1SpanSet(p, base + 3, at + (valueEnd - a))
848
- http1SpanSet(p, base, at)
849
- http1SpanSet(p, base + 1, at + (nameEnd - a))
850
- http1SpanSet(p, base + 2, at + (valueStart - a))
1017
+ // The last of the four is set first, so the array has room for them all.
1018
+ const spans: i32[] = p.spans
1019
+ if (base >= 0 && base + 2 < toI32(spans.length)) {
1020
+ spans[base] = at
1021
+ spans[base + 1] = at + (nameEnd - a)
1022
+ spans[base + 2] = at + (valueStart - a)
1023
+ }
851
1024
  p.headerCount += 1
852
1025
  if (p.keepText) {
853
1026
  p.names.push(http1Text(buf, a, nameEnd, true))
@@ -927,12 +1100,12 @@ const http1EndOfHead = (p: Http1Parser): i32 => {
927
1100
  }
928
1101
  p.contentLength = size
929
1102
  }
930
- // Each tally is read before the next one reuses it, and none is walked
931
- // for a request without a Connection field, which is most of them.
932
- const listed: boolean = connectionFields > 0
933
- const close: boolean = listed && http1Tally(p, "connection", "close").matches > 0
934
- const keep: boolean = listed && http1Tally(p, "connection", "keep-alive").matches > 0
935
- const upgradeAsked: boolean = listed && http1Tally(p, "connection", "upgrade").matches > 0
1103
+ // One walk of the Connection list finds all three options, and none is
1104
+ // made for a request without a Connection field.
1105
+ const options: i32 = connectionFields > 0 ? http1ConnectionOptions(p) : 0
1106
+ const close: boolean = (options & HTTP1_CONNECTION_CLOSE) !== 0
1107
+ const keep: boolean = (options & HTTP1_CONNECTION_KEEP_ALIVE) !== 0
1108
+ const upgradeAsked: boolean = (options & HTTP1_CONNECTION_UPGRADE) !== 0
936
1109
  p.keepAlive = p.minor === 1 ? !close : keep && !close
937
1110
  // The fields of one name joined with ", " are empty only when there is one
938
1111
  // field and it is empty: the same test, without the join.
@@ -1092,6 +1265,11 @@ const http1Next = (p: Http1Parser): i32 => {
1092
1265
  while (true) {
1093
1266
  switch (p.state) {
1094
1267
  case HTTP1_S_REQUEST_LINE: {
1268
+ // The request that ended was answered while its head was held; the
1269
+ // pipelined bytes behind it are only read once it is cleared.
1270
+ if (p.headHeld) {
1271
+ http1StartRequest(p)
1272
+ }
1095
1273
  // A method too long is a 400, decided before the line's length can
1096
1274
  // call it a long target and answer 414.
1097
1275
  if (http1MethodOverrun(p)) {
@@ -1162,7 +1340,11 @@ const http1Next = (p: Http1Parser): i32 => {
1162
1340
  } else if (!p.keepAlive) {
1163
1341
  p.state = HTTP1_S_CLOSED
1164
1342
  } else {
1165
- http1StartRequest(p)
1343
+ // The head stays this request's until the next call, which clears
1344
+ // it before reading on: a response written at HTTP1_END reads its
1345
+ // own method, version and fields, and no copy is made.
1346
+ p.state = HTTP1_S_REQUEST_LINE
1347
+ p.headHeld = true
1166
1348
  }
1167
1349
  return HTTP1_END
1168
1350
  }
@@ -1385,9 +1567,18 @@ export const http1BytesAre = (bytes: u8[], word: string): boolean =>
1385
1567
 
1386
1568
  /** How many decimal digits `n`, which is not negative, takes. */
1387
1569
  const http1DecimalDigits = (n: i32): i32 => {
1388
- let digits: i32 = 1
1389
- let v: i32 = n
1390
- while (v >= 10) {
1570
+ if (n < 10) {
1571
+ return 1
1572
+ }
1573
+ if (n < 100) {
1574
+ return 2
1575
+ }
1576
+ if (n < 1000) {
1577
+ return 3
1578
+ }
1579
+ let digits: i32 = 4
1580
+ let v: i32 = n / 10000
1581
+ while (v > 0) {
1391
1582
  v = v / 10
1392
1583
  digits += 1
1393
1584
  }
@@ -1405,9 +1596,31 @@ export const http1PutText = (out: u8[], at: i32, text: string): i32 => {
1405
1596
  return at + n
1406
1597
  }
1407
1598
 
1408
- /** Writes `bytes` at `out[at ..)`, likewise. */
1599
+ /** Writes CR LF at `out[at ..)`, likewise: two stores, where `http1PutText` walks a string. */
1600
+ const http1PutCrlf = (out: u8[], at: i32): i32 => {
1601
+ if (at >= 0 && at < toI32(out.length) - 1) {
1602
+ out[at] = 13
1603
+ out[at + 1] = 10
1604
+ }
1605
+ return at + 2
1606
+ }
1607
+
1608
+ /** Writes `: ` at `out[at ..)`, likewise. */
1609
+ const http1PutColonSpace = (out: u8[], at: i32): i32 => {
1610
+ if (at >= 0 && at < toI32(out.length) - 1) {
1611
+ out[at] = 58
1612
+ out[at + 1] = 32
1613
+ }
1614
+ return at + 2
1615
+ }
1616
+
1617
+ /** Writes `bytes` at `out[at ..)`, likewise, as one copy when it fits. */
1409
1618
  const http1PutBytes = (out: u8[], at: i32, bytes: u8[]): i32 => {
1410
1619
  const n: i32 = toI32(bytes.length)
1620
+ if (at >= 0 && at <= toI32(out.length) - n) {
1621
+ copyBytes(out, at, bytes, 0, n)
1622
+ return at + n
1623
+ }
1411
1624
  for (let k: i32 = 0; k < n && at + k < toI32(out.length); k += 1) {
1412
1625
  if (at + k >= 0) {
1413
1626
  out[at + k] = bytes[k]
@@ -1505,32 +1718,42 @@ export const http1WriteResponseHead = (
1505
1718
  return HTTP1_NO_ROOM
1506
1719
  }
1507
1720
  let end: i32 = http1PutText(out, at, "HTTP/1.1 ")
1508
- end = http1PutDecimal(out, end, status)
1509
- end = http1PutText(out, end, " ")
1721
+ // A status is three digits, checked above: no division loop for them.
1722
+ if (end >= 0 && end < toI32(out.length) - 3) {
1723
+ const hundreds: i32 = status / 100
1724
+ const tens: i32 = status / 10
1725
+ const tensDigit: i32 = tens % 10
1726
+ const ones: i32 = status % 10
1727
+ out[end] = toU8(48 + hundreds)
1728
+ out[end + 1] = toU8(48 + tensDigit)
1729
+ out[end + 2] = toU8(48 + ones)
1730
+ out[end + 3] = 32
1731
+ }
1732
+ end = end + 4
1510
1733
  end = http1PutText(out, end, reason)
1511
- end = http1PutText(out, end, "\r\n")
1734
+ end = http1PutCrlf(out, end)
1512
1735
  for (let i: i32 = 0; i < count && i < toI32(values.length); i += 1) {
1513
1736
  end = http1PutBytes(out, end, names[i])
1514
- end = http1PutText(out, end, ": ")
1737
+ end = http1PutColonSpace(out, end)
1515
1738
  end = http1PutBytes(out, end, values[i])
1516
- end = http1PutText(out, end, "\r\n")
1739
+ end = http1PutCrlf(out, end)
1517
1740
  }
1518
1741
  if (sendDate) {
1519
1742
  end = http1PutText(out, end, "Date: ")
1520
1743
  end = http1PutBytes(out, end, date)
1521
- end = http1PutText(out, end, "\r\n")
1744
+ end = http1PutCrlf(out, end)
1522
1745
  }
1523
1746
  if (bodyLength >= 0) {
1524
1747
  end = http1PutText(out, end, "Content-Length: ")
1525
1748
  end = http1PutDecimal(out, end, bodyLength)
1526
- end = http1PutText(out, end, "\r\n")
1749
+ end = http1PutCrlf(out, end)
1527
1750
  } else if (bodyLength === HTTP1_CHUNKED) {
1528
1751
  end = http1PutText(out, end, "Transfer-Encoding: chunked\r\n")
1529
1752
  }
1530
1753
  if (toI32(connection.length) > 0) {
1531
1754
  end = http1PutText(out, end, "Connection: ")
1532
1755
  end = http1PutText(out, end, connection)
1533
- end = http1PutText(out, end, "\r\n")
1756
+ end = http1PutCrlf(out, end)
1534
1757
  }
1535
- return http1PutText(out, end, "\r\n")
1758
+ return http1PutCrlf(out, end)
1536
1759
  }