goscript 0.2.5 → 0.2.7

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.
Files changed (264) hide show
  1. package/cmd/goscript/cmd-compile.go +7 -0
  2. package/cmd/goscript/cmd_compile_test.go +83 -0
  3. package/compiler/compile-request.go +3 -0
  4. package/compiler/compiler-cache.go +828 -0
  5. package/compiler/compiler-cache_test.go +705 -0
  6. package/compiler/config.go +2 -0
  7. package/compiler/index.test.ts +26 -1
  8. package/compiler/index.ts +5 -0
  9. package/compiler/lowered-program.go +31 -20
  10. package/compiler/lowering.go +349 -93
  11. package/compiler/lowering_bench_test.go +1 -0
  12. package/compiler/override-facts.go +309 -8
  13. package/compiler/override-parity-verifier.go +45 -1
  14. package/compiler/override-parity-verifier_test.go +100 -0
  15. package/compiler/override-registry_test.go +1 -0
  16. package/compiler/package-graph.go +40 -12
  17. package/compiler/package-graph_test.go +29 -0
  18. package/compiler/runtime-contract.go +8 -0
  19. package/compiler/service.go +98 -11
  20. package/compiler/skeleton_test.go +110 -14
  21. package/compiler/typescript-emitter.go +120 -23
  22. package/dist/compiler/index.d.ts +2 -0
  23. package/dist/compiler/index.js +3 -0
  24. package/dist/compiler/index.js.map +1 -1
  25. package/dist/gs/builtin/builtin.d.ts +24 -33
  26. package/dist/gs/builtin/builtin.js +54 -61
  27. package/dist/gs/builtin/builtin.js.map +1 -1
  28. package/dist/gs/builtin/hostio.d.ts +1 -0
  29. package/dist/gs/builtin/hostio.js +1 -1
  30. package/dist/gs/builtin/hostio.js.map +1 -1
  31. package/dist/gs/builtin/index.d.ts +1 -0
  32. package/dist/gs/builtin/index.js +1 -0
  33. package/dist/gs/builtin/index.js.map +1 -1
  34. package/dist/gs/builtin/panic.d.ts +18 -0
  35. package/dist/gs/builtin/panic.js +98 -0
  36. package/dist/gs/builtin/panic.js.map +1 -0
  37. package/dist/gs/builtin/slice.d.ts +10 -0
  38. package/dist/gs/builtin/slice.js +110 -53
  39. package/dist/gs/builtin/slice.js.map +1 -1
  40. package/dist/gs/builtin/type.js +15 -3
  41. package/dist/gs/builtin/type.js.map +1 -1
  42. package/dist/gs/builtin/varRef.d.ts +1 -1
  43. package/dist/gs/builtin/varRef.js +3 -2
  44. package/dist/gs/builtin/varRef.js.map +1 -1
  45. package/dist/gs/bytes/bytes.gs.js +51 -38
  46. package/dist/gs/bytes/bytes.gs.js.map +1 -1
  47. package/dist/gs/bytes/reader.gs.d.ts +1 -1
  48. package/dist/gs/bytes/reader.gs.js +6 -7
  49. package/dist/gs/bytes/reader.gs.js.map +1 -1
  50. package/dist/gs/cmp/index.d.ts +1 -1
  51. package/dist/gs/cmp/index.js +43 -10
  52. package/dist/gs/cmp/index.js.map +1 -1
  53. package/dist/gs/context/context.d.ts +2 -2
  54. package/dist/gs/context/context.js +1 -1
  55. package/dist/gs/context/context.js.map +1 -1
  56. package/dist/gs/embed/index.js +1 -1
  57. package/dist/gs/embed/index.js.map +1 -1
  58. package/dist/gs/encoding/binary/index.js +201 -8
  59. package/dist/gs/encoding/binary/index.js.map +1 -1
  60. package/dist/gs/encoding/json/index.d.ts +5 -0
  61. package/dist/gs/encoding/json/index.js +388 -25
  62. package/dist/gs/encoding/json/index.js.map +1 -1
  63. package/dist/gs/errors/errors.js +17 -24
  64. package/dist/gs/errors/errors.js.map +1 -1
  65. package/dist/gs/fmt/fmt.js +129 -35
  66. package/dist/gs/fmt/fmt.js.map +1 -1
  67. package/dist/gs/golang.org/x/crypto/cryptobyte/index.js +1 -1
  68. package/dist/gs/golang.org/x/crypto/cryptobyte/index.js.map +1 -1
  69. package/dist/gs/internal/bytealg/index.js +43 -8
  70. package/dist/gs/internal/bytealg/index.js.map +1 -1
  71. package/dist/gs/internal/byteorder/index.d.ts +2 -2
  72. package/dist/gs/internal/byteorder/index.js +2 -2
  73. package/dist/gs/internal/byteorder/index.js.map +1 -1
  74. package/dist/gs/io/fs/format.js +2 -2
  75. package/dist/gs/io/fs/format.js.map +1 -1
  76. package/dist/gs/io/fs/fs.d.ts +1 -1
  77. package/dist/gs/io/fs/fs.js +1 -1
  78. package/dist/gs/io/fs/fs.js.map +1 -1
  79. package/dist/gs/io/io.d.ts +21 -21
  80. package/dist/gs/io/io.js +49 -50
  81. package/dist/gs/io/io.js.map +1 -1
  82. package/dist/gs/math/bits/index.js +26 -8
  83. package/dist/gs/math/bits/index.js.map +1 -1
  84. package/dist/gs/math/copysign.gs.js +10 -17
  85. package/dist/gs/math/copysign.gs.js.map +1 -1
  86. package/dist/gs/math/pow.gs.js +5 -0
  87. package/dist/gs/math/pow.gs.js.map +1 -1
  88. package/dist/gs/math/signbit.gs.js +6 -2
  89. package/dist/gs/math/signbit.gs.js.map +1 -1
  90. package/dist/gs/mime/index.js +1 -0
  91. package/dist/gs/mime/index.js.map +1 -1
  92. package/dist/gs/net/http/index.d.ts +6 -6
  93. package/dist/gs/net/http/index.js +507 -43
  94. package/dist/gs/net/http/index.js.map +1 -1
  95. package/dist/gs/os/stat.gs.d.ts +2 -2
  96. package/dist/gs/os/types.gs.d.ts +1 -1
  97. package/dist/gs/os/types.gs.js +1 -1
  98. package/dist/gs/os/types.gs.js.map +1 -1
  99. package/dist/gs/os/types_js.gs.d.ts +1 -1
  100. package/dist/gs/os/types_js.gs.js +7 -7
  101. package/dist/gs/os/types_js.gs.js.map +1 -1
  102. package/dist/gs/os/types_unix.gs.d.ts +1 -1
  103. package/dist/gs/os/types_unix.gs.js +1 -1
  104. package/dist/gs/os/types_unix.gs.js.map +1 -1
  105. package/dist/gs/os/zero_copy_posix.gs.d.ts +1 -1
  106. package/dist/gs/os/zero_copy_posix.gs.js +1 -1
  107. package/dist/gs/os/zero_copy_posix.gs.js.map +1 -1
  108. package/dist/gs/path/filepath/match.js +8 -4
  109. package/dist/gs/path/filepath/match.js.map +1 -1
  110. package/dist/gs/path/filepath/path.js +216 -42
  111. package/dist/gs/path/filepath/path.js.map +1 -1
  112. package/dist/gs/path/match.js +6 -3
  113. package/dist/gs/path/match.js.map +1 -1
  114. package/dist/gs/reflect/type.d.ts +5 -4
  115. package/dist/gs/reflect/type.js +29 -11
  116. package/dist/gs/reflect/type.js.map +1 -1
  117. package/dist/gs/slices/slices.js +11 -11
  118. package/dist/gs/slices/slices.js.map +1 -1
  119. package/dist/gs/strconv/atof.gs.js +156 -43
  120. package/dist/gs/strconv/atof.gs.js.map +1 -1
  121. package/dist/gs/strconv/atoi.gs.d.ts +3 -2
  122. package/dist/gs/strconv/atoi.gs.js +86 -67
  123. package/dist/gs/strconv/atoi.gs.js.map +1 -1
  124. package/dist/gs/strconv/ftoa.gs.js +73 -3
  125. package/dist/gs/strconv/ftoa.gs.js.map +1 -1
  126. package/dist/gs/strconv/itoa.gs.d.ts +4 -4
  127. package/dist/gs/strconv/itoa.gs.js +5 -4
  128. package/dist/gs/strconv/itoa.gs.js.map +1 -1
  129. package/dist/gs/strconv/quote.gs.d.ts +1 -1
  130. package/dist/gs/strconv/quote.gs.js +311 -103
  131. package/dist/gs/strconv/quote.gs.js.map +1 -1
  132. package/dist/gs/strings/reader.d.ts +1 -1
  133. package/dist/gs/strings/reader.js +8 -8
  134. package/dist/gs/strings/reader.js.map +1 -1
  135. package/dist/gs/strings/strings.js +87 -61
  136. package/dist/gs/strings/strings.js.map +1 -1
  137. package/dist/gs/sync/atomic/doc_64.gs.d.ts +14 -14
  138. package/dist/gs/sync/atomic/doc_64.gs.js +10 -10
  139. package/dist/gs/sync/atomic/doc_64.gs.js.map +1 -1
  140. package/dist/gs/sync/atomic/type.gs.d.ts +22 -22
  141. package/dist/gs/sync/atomic/type.gs.js +4 -4
  142. package/dist/gs/sync/atomic/type.gs.js.map +1 -1
  143. package/dist/gs/sync/sync.js +50 -12
  144. package/dist/gs/sync/sync.js.map +1 -1
  145. package/dist/gs/syscall/fs.d.ts +6 -6
  146. package/dist/gs/syscall/fs.js +1 -1
  147. package/dist/gs/syscall/fs.js.map +1 -1
  148. package/dist/gs/time/time.d.ts +18 -18
  149. package/dist/gs/time/time.js +58 -55
  150. package/dist/gs/time/time.js.map +1 -1
  151. package/dist/gs/unicode/tables.d.ts +11 -0
  152. package/dist/gs/unicode/tables.js +635 -0
  153. package/dist/gs/unicode/tables.js.map +1 -0
  154. package/dist/gs/unicode/unicode.d.ts +58 -38
  155. package/dist/gs/unicode/unicode.js +362 -278
  156. package/dist/gs/unicode/unicode.js.map +1 -1
  157. package/go.sum +13 -0
  158. package/gs/builtin/builtin.ts +83 -93
  159. package/gs/builtin/hostio.ts +1 -1
  160. package/gs/builtin/index.ts +1 -0
  161. package/gs/builtin/panic.test.ts +189 -0
  162. package/gs/builtin/panic.ts +107 -0
  163. package/gs/builtin/runtime-contract.test.ts +5 -5
  164. package/gs/builtin/slice.test.ts +23 -0
  165. package/gs/builtin/slice.ts +133 -95
  166. package/gs/builtin/type.ts +16 -3
  167. package/gs/builtin/varRef.ts +4 -2
  168. package/gs/builtin/wide-int.test.ts +41 -0
  169. package/gs/bytes/bytes.gs.ts +54 -41
  170. package/gs/bytes/bytes.test.ts +18 -1
  171. package/gs/bytes/reader.gs.ts +7 -8
  172. package/gs/cmp/index.test.ts +55 -0
  173. package/gs/cmp/index.ts +45 -9
  174. package/gs/context/context.ts +3 -3
  175. package/gs/embed/index.ts +2 -2
  176. package/gs/encoding/binary/index.test.ts +104 -0
  177. package/gs/encoding/binary/index.ts +259 -11
  178. package/gs/encoding/json/index.test.ts +107 -0
  179. package/gs/encoding/json/index.ts +400 -29
  180. package/gs/errors/errors.test.ts +44 -1
  181. package/gs/errors/errors.ts +15 -31
  182. package/gs/fmt/fmt.test.ts +70 -2
  183. package/gs/fmt/fmt.ts +128 -34
  184. package/gs/golang.org/x/crypto/cryptobyte/index.ts +1 -1
  185. package/gs/internal/bytealg/index.test.ts +26 -1
  186. package/gs/internal/bytealg/index.ts +44 -8
  187. package/gs/internal/byteorder/index.ts +6 -4
  188. package/gs/io/fs/format.ts +2 -2
  189. package/gs/io/fs/fs.ts +2 -2
  190. package/gs/io/fs/stat.test.ts +2 -2
  191. package/gs/io/fs/sub.test.ts +2 -2
  192. package/gs/io/fs/walk.test.ts +2 -2
  193. package/gs/io/io.test.ts +47 -5
  194. package/gs/io/io.ts +73 -73
  195. package/gs/io/limit.test.ts +103 -0
  196. package/gs/math/bits/index.test.ts +128 -0
  197. package/gs/math/bits/index.ts +26 -8
  198. package/gs/math/copysign.gs.test.ts +3 -1
  199. package/gs/math/copysign.gs.ts +10 -22
  200. package/gs/math/pow.gs.test.ts +4 -5
  201. package/gs/math/pow.gs.ts +5 -0
  202. package/gs/math/signbit.gs.test.ts +2 -1
  203. package/gs/math/signbit.gs.ts +6 -3
  204. package/gs/mime/index.ts +1 -0
  205. package/gs/net/http/index.test.ts +683 -2
  206. package/gs/net/http/index.ts +598 -57
  207. package/gs/net/http/meta.json +3 -0
  208. package/gs/os/stat.gs.ts +2 -2
  209. package/gs/os/types.gs.ts +2 -2
  210. package/gs/os/types_js.gs.ts +9 -9
  211. package/gs/os/types_unix.gs.ts +2 -2
  212. package/gs/os/zero_copy_posix.gs.ts +2 -2
  213. package/gs/path/filepath/match.test.ts +16 -0
  214. package/gs/path/filepath/match.ts +8 -4
  215. package/gs/path/filepath/path.test.ts +91 -9
  216. package/gs/path/filepath/path.ts +223 -49
  217. package/gs/path/match.test.ts +32 -0
  218. package/gs/path/match.ts +6 -3
  219. package/gs/reflect/deepequal.test.ts +1 -1
  220. package/gs/reflect/field.test.ts +1 -1
  221. package/gs/reflect/function-types.test.ts +6 -6
  222. package/gs/reflect/sliceat.test.ts +13 -13
  223. package/gs/reflect/structof.test.ts +4 -4
  224. package/gs/reflect/type.ts +34 -14
  225. package/gs/reflect/typefor.test.ts +5 -5
  226. package/gs/runtime/pprof/index.test.ts +20 -0
  227. package/gs/runtime/trace/index.test.ts +3 -0
  228. package/gs/slices/slices.test.ts +31 -0
  229. package/gs/slices/slices.ts +11 -11
  230. package/gs/strconv/append.test.ts +99 -0
  231. package/gs/strconv/atof.gs.ts +156 -42
  232. package/gs/strconv/atof.test.ts +45 -0
  233. package/gs/strconv/atoi.gs.ts +87 -69
  234. package/gs/strconv/atoi.test.ts +49 -0
  235. package/gs/strconv/ftoa.gs.ts +85 -10
  236. package/gs/strconv/ftoa.test.ts +43 -0
  237. package/gs/strconv/itoa.gs.ts +10 -9
  238. package/gs/strconv/quote.gs.ts +335 -108
  239. package/gs/strconv/quote.test.ts +111 -0
  240. package/gs/strings/reader.test.ts +10 -10
  241. package/gs/strings/reader.ts +9 -9
  242. package/gs/strings/strings.test.ts +18 -5
  243. package/gs/strings/strings.ts +81 -68
  244. package/gs/sync/atomic/doc_64.gs.ts +24 -24
  245. package/gs/sync/atomic/doc_64.test.ts +5 -5
  246. package/gs/sync/atomic/type.gs.ts +28 -28
  247. package/gs/sync/sync.test.ts +109 -1
  248. package/gs/sync/sync.ts +46 -12
  249. package/gs/syscall/fs.ts +8 -8
  250. package/gs/syscall/net.test.ts +1 -1
  251. package/gs/time/parse.test.ts +45 -0
  252. package/gs/time/time.test.ts +46 -23
  253. package/gs/time/time.ts +69 -66
  254. package/gs/unicode/gen.go +198 -0
  255. package/gs/unicode/tables.ts +646 -0
  256. package/gs/unicode/unicode.test.ts +69 -0
  257. package/gs/unicode/unicode.ts +396 -312
  258. package/package.json +2 -2
  259. package/dist/gs/github.com/aperturerobotics/util/conc/index.d.ts +0 -20
  260. package/dist/gs/github.com/aperturerobotics/util/conc/index.js +0 -134
  261. package/dist/gs/github.com/aperturerobotics/util/conc/index.js.map +0 -1
  262. package/gs/github.com/aperturerobotics/util/conc/index.test.ts +0 -30
  263. package/gs/github.com/aperturerobotics/util/conc/index.ts +0 -172
  264. package/gs/github.com/aperturerobotics/util/conc/meta.json +0 -9
@@ -420,10 +420,11 @@ function parseRequestURL(rawURL: string): [RequestURL | null, $.GoError] {
420
420
  return [null, errors.New(`parse "${rawURL}": invalid URL escape`)]
421
421
  }
422
422
  const parsed = new URL(rawURL, 'http://goscript.invalid')
423
+ const path = decodeURIComponent(parsed.pathname)
423
424
  const hasHost = /^[a-zA-Z][a-zA-Z0-9+.-]*:\/\//.test(rawURL)
424
425
  return [
425
426
  new RequestURL(
426
- parsed.pathname,
427
+ path,
427
428
  parsed.search.startsWith('?') ? parsed.search.slice(1) : parsed.search,
428
429
  hasHost ? parsed.protocol.replace(/:$/, '') : '',
429
430
  hasHost ? parsed.host : '',
@@ -1508,6 +1509,11 @@ async function readFetchBody(
1508
1509
 
1509
1510
  type maybePromise<T> = T | Promise<T>
1510
1511
 
1512
+ type httpRange = {
1513
+ start: number
1514
+ length: number
1515
+ }
1516
+
1511
1517
  export interface FileSystem {
1512
1518
  Open(name: string): maybePromise<[File | null, $.GoError]>
1513
1519
  }
@@ -1524,7 +1530,7 @@ interface fileServerFileSystem {
1524
1530
  interface fileServerFile {
1525
1531
  Close(): maybePromise<$.GoError>
1526
1532
  Read(p: $.Bytes): maybePromise<[number, $.GoError]>
1527
- Seek(offset: number, whence: number): maybePromise<[number, $.GoError]>
1533
+ Seek(offset: bigint, whence: number): maybePromise<[bigint, $.GoError]>
1528
1534
  Readdir(count: number): maybePromise<[$.Slice<fs.FileInfo> | null, $.GoError]>
1529
1535
  Stat(): maybePromise<[fs.FileInfo | null, $.GoError]>
1530
1536
  }
@@ -1554,7 +1560,7 @@ function httpFileFromFSFile(file: Exclude<fs.File, null>): File {
1554
1560
  Stat: () => file.Stat(),
1555
1561
  Seek:
1556
1562
  seek == null ?
1557
- () => [0, errors.New('net/http: file does not support seek')]
1563
+ () => [0n, errors.New('net/http: file does not support seek')]
1558
1564
  : seek.bind(file),
1559
1565
  Readdir: readdir == null ? () => [null, io.EOF] : readdir.bind(file),
1560
1566
  }
@@ -1588,28 +1594,13 @@ export function FileServer(root: fileServerFileSystem | null): Handler {
1588
1594
  NotFound(w, req)
1589
1595
  return
1590
1596
  }
1591
- const header = await w.Header()
1592
- if (Header_Get(header, 'Content-Type') === '') {
1593
- const contentType = mime.TypeByExtension(
1594
- path.Ext(info?.Name?.() || req.URL?.Path || ''),
1595
- )
1596
- if (contentType !== '') {
1597
- Header_Set(header, 'Content-Type', contentType)
1598
- }
1599
- }
1600
- if (info?.Size != null) {
1601
- Header_Set(header, 'Content-Length', String(info.Size()))
1602
- }
1603
- await w.WriteHeader(StatusOK)
1604
- if (req.Method !== MethodHead) {
1605
- const [, copyErr] = await io.Copy(
1606
- w as unknown as io.Writer,
1607
- file as io.Reader,
1608
- )
1609
- if (copyErr != null) {
1610
- return
1611
- }
1612
- }
1597
+ await serveContent(
1598
+ w,
1599
+ req,
1600
+ info?.Name?.() || req.URL?.Path || '',
1601
+ file as io.Reader,
1602
+ typeof info?.Size === 'function' ? Number(info.Size()) : null,
1603
+ )
1613
1604
  } finally {
1614
1605
  await file.Close()
1615
1606
  }
@@ -1637,13 +1628,45 @@ export function ServeFile(
1637
1628
  NotFound(w, req)
1638
1629
  }
1639
1630
 
1640
- export function ServeFileFS(
1631
+ export async function ServeFileFS(
1641
1632
  w: ResponseWriter | null,
1642
1633
  r: Request | $.VarRef<Request> | null,
1643
- _fsys: fs.FS,
1634
+ fsys: fs.FS,
1644
1635
  name: string,
1645
- ): void {
1646
- ServeFile(w, r, name)
1636
+ ): Promise<void> {
1637
+ const req = $.pointerValue<Request | null>(r)
1638
+ if (w == null || req == null) {
1639
+ return
1640
+ }
1641
+ if (req.Method !== MethodGet && req.Method !== MethodHead) {
1642
+ Error(w, 'method not allowed', StatusMethodNotAllowed)
1643
+ return
1644
+ }
1645
+ const [file, err] = (await FS(fsys).Open(name)) ?? [null, fs.ErrInvalid]
1646
+ if (err != null || file == null) {
1647
+ NotFound(w, req)
1648
+ return
1649
+ }
1650
+ try {
1651
+ const [info, statErr] = await file.Stat()
1652
+ if (statErr != null) {
1653
+ Error(w, statErr.Error(), StatusInternalServerError)
1654
+ return
1655
+ }
1656
+ if (info?.IsDir?.() === true) {
1657
+ NotFound(w, req)
1658
+ return
1659
+ }
1660
+ await serveContent(
1661
+ w,
1662
+ req,
1663
+ info?.Name?.() || name,
1664
+ file as io.Reader,
1665
+ typeof info?.Size === 'function' ? Number(info.Size()) : null,
1666
+ )
1667
+ } finally {
1668
+ await file.Close()
1669
+ }
1647
1670
  }
1648
1671
 
1649
1672
  function cleanFileServerPath(name: string): string {
@@ -2115,23 +2138,58 @@ export function HandleFunc(pattern: string, handler: HandlerFunc): void {
2115
2138
  }
2116
2139
 
2117
2140
  export function StripPrefix(prefix: string, handler: Handler | null): Handler {
2141
+ if (prefix === '') {
2142
+ return handler ?? NotFoundHandler()
2143
+ }
2118
2144
  return {
2119
2145
  ServeHTTP(w, r) {
2120
2146
  const req = $.pointerValue<Request | null>(r)
2147
+ const urlPath = req?.URL?.Path
2148
+ const rawPath = req?.URL?.RawPath ?? ''
2149
+ const strippedPath =
2150
+ typeof urlPath === 'string' && urlPath.startsWith(prefix) ?
2151
+ urlPath.slice(prefix.length)
2152
+ : urlPath
2153
+ const strippedRawPath =
2154
+ rawPath !== '' && rawPath.startsWith(prefix) ?
2155
+ rawPath.slice(prefix.length)
2156
+ : rawPath
2121
2157
  if (
2122
- req?.URL != null &&
2123
- typeof req.URL.Path === 'string' &&
2124
- req.URL.Path.startsWith(prefix)
2158
+ req != null &&
2159
+ req.URL != null &&
2160
+ typeof urlPath === 'string' &&
2161
+ strippedPath.length < urlPath.length &&
2162
+ (rawPath === '' || strippedRawPath.length < rawPath.length)
2125
2163
  ) {
2126
- req.URL = { ...req.URL, Path: req.URL.Path.slice(prefix.length) || '/' }
2164
+ const reqCopy = req.Clone(req.Context())
2165
+ reqCopy.URL = {
2166
+ ...reqCopy.URL,
2167
+ Path: strippedPath,
2168
+ RawPath: strippedRawPath,
2169
+ }
2170
+ return handler?.ServeHTTP(w, reqCopy)
2127
2171
  }
2128
- return handler?.ServeHTTP(w, req)
2172
+ NotFound(w, req)
2129
2173
  },
2130
2174
  }
2131
2175
  }
2132
2176
 
2133
2177
  export function AllowQuerySemicolons(handler: Handler | null): Handler {
2134
- return handler ?? NotFoundHandler()
2178
+ const target = handler ?? NotFoundHandler()
2179
+ return {
2180
+ ServeHTTP(w, r) {
2181
+ const req = $.pointerValue<Request | null>(r)
2182
+ if (req?.URL?.RawQuery?.includes(';') === true) {
2183
+ const reqCopy = req.Clone(req.Context())
2184
+ reqCopy.URL = {
2185
+ ...reqCopy.URL,
2186
+ RawQuery: req.URL.RawQuery.replaceAll(';', '&'),
2187
+ }
2188
+ return target.ServeHTTP(w, reqCopy)
2189
+ }
2190
+ return target.ServeHTTP(w, r)
2191
+ },
2192
+ }
2135
2193
  }
2136
2194
 
2137
2195
  export function MaxBytesHandler(handler: Handler | null, n: number): Handler {
@@ -2198,18 +2256,54 @@ export function NotFound(
2198
2256
 
2199
2257
  export async function Redirect(
2200
2258
  w: ResponseWriter | null,
2201
- _r: Request | $.VarRef<Request> | null,
2259
+ r: Request | $.VarRef<Request> | null,
2202
2260
  url: string,
2203
2261
  code: number,
2204
2262
  ): Promise<void> {
2205
2263
  if (w == null) {
2206
2264
  return
2207
2265
  }
2266
+ const req = $.pointerValue<Request | null>(r)
2267
+ url = redirectLocation(req, url)
2208
2268
  const header = await w.Header()
2269
+ const hadContentType = Header_Get(header, 'Content-Type') !== ''
2209
2270
  if (header != null) {
2210
2271
  Header_Set(header, 'Location', url)
2272
+ if (!hadContentType && req?.Method === MethodGet) {
2273
+ Header_Set(header, 'Content-Type', 'text/html; charset=utf-8')
2274
+ }
2211
2275
  }
2212
2276
  await w.WriteHeader(code)
2277
+ if (!hadContentType && req?.Method === MethodGet) {
2278
+ await w.Write(
2279
+ $.stringToBytes(`<a href="${url}">${StatusText(code)}</a>.\n\n`),
2280
+ )
2281
+ }
2282
+ }
2283
+
2284
+ function redirectLocation(req: Request | null, url: string): string {
2285
+ if (
2286
+ req?.URL == null ||
2287
+ url === '' ||
2288
+ url.startsWith('/') ||
2289
+ /^[A-Za-z][A-Za-z0-9+.-]*:/.test(url)
2290
+ ) {
2291
+ return url
2292
+ }
2293
+ const [dir] = path.Split(req.URL.Path || '/')
2294
+ let pathPart = dir + url
2295
+ let query = ''
2296
+ const queryIndex = pathPart.indexOf('?')
2297
+ if (queryIndex >= 0) {
2298
+ query = pathPart.slice(queryIndex)
2299
+ pathPart = pathPart.slice(0, queryIndex)
2300
+ }
2301
+ const trailingSlash = pathPart.endsWith('/')
2302
+ pathPart = path.Clean(pathPart)
2303
+ if (trailingSlash && !pathPart.endsWith('/')) {
2304
+ pathPart += '/'
2305
+ }
2306
+ return pathPart + query
2213
2307
  }
2214
2308
 
2215
2309
  export function ParseTime(text: string): [time.Time, $.GoError] {
@@ -2220,7 +2314,7 @@ export function ParseTime(text: string): [time.Time, $.GoError] {
2220
2314
  $.newError(`parsing time "${text}" as HTTP-date: cannot parse`),
2221
2315
  ]
2222
2316
  }
2223
- return [time.UnixMilli(date.getTime()), null]
2317
+ return [time.UnixMilli(BigInt(date.getTime())), null]
2224
2318
  }
2225
2319
 
2226
2320
  export function DetectContentType(data: $.Slice<number>): string {
@@ -2604,7 +2698,7 @@ export function ParseSetCookie(line: string): [Cookie | null, $.GoError] {
2604
2698
  if (Number.isNaN(parsed.getTime())) {
2605
2699
  break
2606
2700
  }
2607
- cookie.Expires = time.UnixMilli(parsed.getTime())
2701
+ cookie.Expires = time.UnixMilli(BigInt(parsed.getTime()))
2608
2702
  continue
2609
2703
  }
2610
2704
  case 'path':
@@ -2702,27 +2796,326 @@ export async function PostForm(
2702
2796
  return await DefaultClient.PostForm(_url, data)
2703
2797
  }
2704
2798
 
2799
+ export function ProxyURL(
2800
+ fixedURL: any,
2801
+ ): (req: Request | $.VarRef<Request> | null) => [any, $.GoError] {
2802
+ return () => [fixedURL, null]
2803
+ }
2804
+
2705
2805
  export function ProxyFromEnvironment(
2706
2806
  _req: Request | $.VarRef<Request> | null,
2707
2807
  ): [any, $.GoError] {
2708
2808
  return [null, null]
2709
2809
  }
2710
2810
 
2711
- export function ProxyURL(
2712
- fixedURL: any,
2713
- ): (req: Request | $.VarRef<Request> | null) => [any, $.GoError] {
2714
- return () => [fixedURL, null]
2811
+ export async function ReadRequest(
2812
+ reader: any,
2813
+ ): Promise<[Request | null, $.GoError]> {
2814
+ const [wire, readErr] = await readHTTPWire(reader)
2815
+ if (readErr != null) {
2816
+ return [null, readErr]
2817
+ }
2818
+ const parsed = parseHTTPWire(wire)
2819
+ if (parsed.error != null) {
2820
+ return [null, parsed.error]
2821
+ }
2822
+ const [method, requestURI, proto] = splitRequestLine(parsed.startLine)
2823
+ if (method === '' || requestURI === '' || proto === '') {
2824
+ return [null, badHTTPMessageError('malformed HTTP request')]
2825
+ }
2826
+ if (!isToken(method)) {
2827
+ return [null, badHTTPMessageError(`invalid method ${method}`)]
2828
+ }
2829
+ const [protoMajor, protoMinor, protoOK] = ParseHTTPVersion(proto)
2830
+ if (!protoOK) {
2831
+ return [null, badHTTPMessageError(`malformed HTTP version ${proto}`)]
2832
+ }
2833
+ const [url, urlErr] = parseRequestURL(requestURI)
2834
+ if (urlErr != null || url == null) {
2835
+ return [null, urlErr]
2836
+ }
2837
+ const host = Header_Get(parsed.header, 'Host')
2838
+ Header_Del(parsed.header, 'Host')
2839
+ const bodyInfo = parseHTTPBody(parsed.header, parsed.body)
2840
+ if (bodyInfo.error != null) {
2841
+ return [null, bodyInfo.error]
2842
+ }
2843
+ return [
2844
+ new Request({
2845
+ Method: method,
2846
+ URL: url,
2847
+ Proto: proto,
2848
+ ProtoMajor: protoMajor,
2849
+ ProtoMinor: protoMinor,
2850
+ Body: bodyInfo.body,
2851
+ Header: parsed.header,
2852
+ ContentLength: bodyInfo.contentLength,
2853
+ TransferEncoding: bodyInfo.transferEncoding,
2854
+ Close: shouldClose(protoMajor, protoMinor, parsed.header),
2855
+ Host: host,
2856
+ RequestURI: requestURI,
2857
+ }),
2858
+ null,
2859
+ ]
2860
+ }
2861
+
2862
+ export async function ReadResponse(
2863
+ reader: any,
2864
+ req: Request | $.VarRef<Request> | null,
2865
+ ): Promise<[Response | null, $.GoError]> {
2866
+ const [wire, readErr] = await readHTTPWire(reader)
2867
+ if (readErr != null) {
2868
+ return [null, readErr]
2869
+ }
2870
+ const parsed = parseHTTPWire(wire)
2871
+ if (parsed.error != null) {
2872
+ return [null, parsed.error]
2873
+ }
2874
+ const match = /^(HTTP\/\d+\.\d+) ([0-9]{3})(?: (.*))?$/.exec(parsed.startLine)
2875
+ if (match == null) {
2876
+ return [null, badHTTPMessageError('malformed HTTP response')]
2877
+ }
2878
+ const [, proto, statusCodeText, statusText = ''] = match
2879
+ const [protoMajor, protoMinor, protoOK] = ParseHTTPVersion(proto)
2880
+ if (!protoOK) {
2881
+ return [null, badHTTPMessageError(`malformed HTTP version ${proto}`)]
2882
+ }
2883
+ const statusCode = Number(statusCodeText)
2884
+ const noBody =
2885
+ statusCode === StatusNoContent || statusCode === StatusNotModified
2886
+ const bodyInfo =
2887
+ noBody ?
2888
+ {
2889
+ body: NoBody,
2890
+ contentLength: 0,
2891
+ transferEncoding: null,
2892
+ error: null,
2893
+ }
2894
+ : parseHTTPBody(parsed.header, parsed.body)
2895
+ if (bodyInfo.error != null) {
2896
+ return [null, bodyInfo.error]
2897
+ }
2898
+ return [
2899
+ new Response({
2900
+ Status: `${statusCodeText}${statusText === '' ? '' : ` ${statusText}`}`,
2901
+ StatusCode: statusCode,
2902
+ Proto: proto,
2903
+ ProtoMajor: protoMajor,
2904
+ ProtoMinor: protoMinor,
2905
+ Body: bodyInfo.body,
2906
+ Header: parsed.header,
2907
+ ContentLength: bodyInfo.contentLength,
2908
+ TransferEncoding: bodyInfo.transferEncoding,
2909
+ Close: shouldClose(protoMajor, protoMinor, parsed.header),
2910
+ Request: req,
2911
+ }),
2912
+ null,
2913
+ ]
2715
2914
  }
2716
2915
 
2717
- export function ReadRequest(_reader: any): [Request | null, $.GoError] {
2718
- return [null, ErrNotSupported]
2916
+ async function readHTTPWire(reader: any): Promise<[string, $.GoError]> {
2917
+ const r = $.pointerValueOrNil<any>(reader)
2918
+ if (r == null || typeof r.Read !== 'function') {
2919
+ return ['', errors.New('malformed HTTP message')]
2920
+ }
2921
+ const [data, err] = await io.ReadAll(r)
2922
+ if (err != null) {
2923
+ return ['', err]
2924
+ }
2925
+ return [$.bytesToString(data), null]
2719
2926
  }
2720
2927
 
2721
- export function ReadResponse(
2722
- _reader: any,
2723
- _req: Request | $.VarRef<Request> | null,
2724
- ): [Response | null, $.GoError] {
2725
- return [null, ErrNotSupported]
2928
+ function splitRequestLine(line: string): [string, string, string] {
2929
+ const first = line.indexOf(' ')
2930
+ const last = line.lastIndexOf(' ')
2931
+ if (first <= 0 || last <= first) {
2932
+ return ['', '', '']
2933
+ }
2934
+ return [
2935
+ line.slice(0, first),
2936
+ line.slice(first + 1, last),
2937
+ line.slice(last + 1),
2938
+ ]
2939
+ }
2940
+
2941
+ function parseHTTPWire(wire: string): {
2942
+ startLine: string
2943
+ header: Header
2944
+ body: string
2945
+ error: $.GoError
2946
+ } {
2947
+ const headerEnd = wire.indexOf('\r\n\r\n')
2948
+ const separatorLength = headerEnd >= 0 ? 4 : 2
2949
+ const fallbackHeaderEnd = headerEnd >= 0 ? headerEnd : wire.indexOf('\n\n')
2950
+ if (fallbackHeaderEnd < 0) {
2951
+ return {
2952
+ startLine: '',
2953
+ header: new Header(),
2954
+ body: '',
2955
+ error: badHTTPMessageError('malformed HTTP message'),
2956
+ }
2957
+ }
2958
+ const head = wire.slice(0, fallbackHeaderEnd).replace(/\r\n/g, '\n')
2959
+ const lines = head.split('\n')
2960
+ const startLine = lines.shift() ?? ''
2961
+ const header = new Header()
2962
+ let lastKey = ''
2963
+ for (const rawLine of lines) {
2964
+ if (rawLine === '') {
2965
+ continue
2966
+ }
2967
+ if ((rawLine[0] === ' ' || rawLine[0] === '\t') && lastKey !== '') {
2968
+ const values = Array.from(header.get(lastKey) ?? [])
2969
+ values[values.length - 1] =
2970
+ `${values[values.length - 1]} ${rawLine.trim()}`
2971
+ header.set(lastKey, $.arrayToSlice(values))
2972
+ continue
2973
+ }
2974
+ const colon = rawLine.indexOf(':')
2975
+ if (colon <= 0) {
2976
+ return {
2977
+ startLine: '',
2978
+ header: new Header(),
2979
+ body: '',
2980
+ error: badHTTPMessageError('malformed MIME header line'),
2981
+ }
2982
+ }
2983
+ lastKey = canonicalMIMEHeaderKey(rawLine.slice(0, colon).trim())
2984
+ Header_Add(header, lastKey, rawLine.slice(colon + 1).trim())
2985
+ }
2986
+ return {
2987
+ startLine,
2988
+ header,
2989
+ body: wire.slice(fallbackHeaderEnd + separatorLength),
2990
+ error: null,
2991
+ }
2992
+ }
2993
+
2994
+ function parseHTTPBody(
2995
+ header: Header,
2996
+ rawBody: string,
2997
+ ): {
2998
+ body: io.ReadCloser
2999
+ contentLength: number
3000
+ transferEncoding: $.Slice<string>
3001
+ error: $.GoError
3002
+ } {
3003
+ const transferEncoding = Header_Get(header, 'Transfer-Encoding')
3004
+ if (transferEncoding.toLowerCase() === 'chunked') {
3005
+ const chunked = decodeChunkedBody(rawBody)
3006
+ if (chunked.error != null) {
3007
+ return {
3008
+ body: NoBody,
3009
+ contentLength: -1,
3010
+ transferEncoding: $.arrayToSlice(['chunked']),
3011
+ error: chunked.error,
3012
+ }
3013
+ }
3014
+ return {
3015
+ body: bodyFromString(chunked.body),
3016
+ contentLength: -1,
3017
+ transferEncoding: $.arrayToSlice(['chunked']),
3018
+ error: null,
3019
+ }
3020
+ }
3021
+ const contentLength = Header_Get(header, 'Content-Length')
3022
+ if (contentLength !== '') {
3023
+ const parsedLength = Number.parseInt(contentLength, 10)
3024
+ if (
3025
+ !/^[0-9]+$/.test(contentLength) ||
3026
+ !Number.isSafeInteger(parsedLength)
3027
+ ) {
3028
+ return {
3029
+ body: NoBody,
3030
+ contentLength: 0,
3031
+ transferEncoding: null,
3032
+ error: badHTTPMessageError(`bad Content-Length ${contentLength}`),
3033
+ }
3034
+ }
3035
+ return {
3036
+ body: bodyFromString(rawBody.slice(0, parsedLength)),
3037
+ contentLength: parsedLength,
3038
+ transferEncoding: null,
3039
+ error: null,
3040
+ }
3041
+ }
3042
+ if (rawBody === '') {
3043
+ return {
3044
+ body: NoBody,
3045
+ contentLength: 0,
3046
+ transferEncoding: null,
3047
+ error: null,
3048
+ }
3049
+ }
3050
+ return {
3051
+ body: bodyFromString(rawBody),
3052
+ contentLength: -1,
3053
+ transferEncoding: null,
3054
+ error: null,
3055
+ }
3056
+ }
3057
+
3058
+ function decodeChunkedBody(rawBody: string): {
3059
+ body: string
3060
+ error: $.GoError
3061
+ } {
3062
+ let offset = 0
3063
+ let body = ''
3064
+ while (true) {
3065
+ const lineEnd = rawBody.indexOf('\r\n', offset)
3066
+ if (lineEnd < 0) {
3067
+ return { body: '', error: io.ErrUnexpectedEOF }
3068
+ }
3069
+ const sizeText = rawBody.slice(offset, lineEnd).split(';', 1)[0].trim()
3070
+ const size = Number.parseInt(sizeText, 16)
3071
+ if (!/^[0-9A-Fa-f]+$/.test(sizeText) || !Number.isSafeInteger(size)) {
3072
+ return { body: '', error: badHTTPMessageError('invalid chunk length') }
3073
+ }
3074
+ offset = lineEnd + 2
3075
+ if (size === 0) {
3076
+ return { body, error: null }
3077
+ }
3078
+ if (offset + size + 2 > rawBody.length) {
3079
+ return { body: '', error: io.ErrUnexpectedEOF }
3080
+ }
3081
+ body += rawBody.slice(offset, offset + size)
3082
+ offset += size
3083
+ if (rawBody.slice(offset, offset + 2) !== '\r\n') {
3084
+ return { body: '', error: badHTTPMessageError('malformed chunk') }
3085
+ }
3086
+ offset += 2
3087
+ }
3088
+ }
3089
+
3090
+ function bodyFromString(body: string): io.ReadCloser {
3091
+ return body === '' ? NoBody : new responseBody($.stringToBytes(body))
3092
+ }
3093
+
3094
+ function shouldClose(
3095
+ protoMajor: number,
3096
+ protoMinor: number,
3097
+ header: Header,
3098
+ ): boolean {
3099
+ const connection = Header_Get(header, 'Connection').toLowerCase()
3100
+ if (
3101
+ connection
3102
+ .split(',')
3103
+ .map((part) => part.trim())
3104
+ .includes('close')
3105
+ ) {
3106
+ return true
3107
+ }
3108
+ if (protoMajor < 1 || (protoMajor === 1 && protoMinor === 0)) {
3109
+ return !connection
3110
+ .split(',')
3111
+ .map((part) => part.trim())
3112
+ .includes('keep-alive')
3113
+ }
3114
+ return false
3115
+ }
3116
+
3117
+ function badHTTPMessageError(message: string): $.GoError {
3118
+ return errors.New(message)
2726
3119
  }
2727
3120
 
2728
3121
  export async function ServeContent(
@@ -2732,20 +3125,168 @@ export async function ServeContent(
2732
3125
  _modtime: time.Time,
2733
3126
  content: io.Reader | null,
2734
3127
  ): Promise<void> {
3128
+ await serveContent(w, $.pointerValueOrNil(req), _name, content, null)
3129
+ }
3130
+
3131
+ async function serveContent(
3132
+ w: ResponseWriter | null,
3133
+ req: Request | null,
3134
+ name: string,
3135
+ content: io.Reader | null,
3136
+ knownSize: number | null,
3137
+ ): Promise<void> {
3138
+ // Browser media seeks depend on FileServer and ServeContent sharing byte-range semantics.
2735
3139
  if (content == null) {
2736
- NotFound(w, req)
3140
+ NotFound(w, req as Request | null)
2737
3141
  return
2738
3142
  }
2739
- const [data, err] = await io.ReadAll(content)
2740
- if (err != null) {
2741
- Error(w, err.Error(), StatusInternalServerError)
3143
+ if (w == null) {
3144
+ return
3145
+ }
3146
+ const header = await w.Header()
3147
+ if (Header_Get(header, 'Content-Type') === '') {
3148
+ const contentType = mime.TypeByExtension(path.Ext(name))
3149
+ if (contentType !== '') {
3150
+ Header_Set(header, 'Content-Type', contentType)
3151
+ }
3152
+ }
3153
+ const rangeHeader = req?.Header == null ? '' : Header_Get(req.Header, 'Range')
3154
+ if (rangeHeader === '' && knownSize != null) {
3155
+ Header_Set(header, 'Content-Length', String(knownSize))
3156
+ await w.WriteHeader(StatusOK)
3157
+ if (req?.Method !== MethodHead) {
3158
+ await io.Copy(w as unknown as io.Writer, content)
3159
+ }
3160
+ return
3161
+ }
3162
+
3163
+ const seeker = content as io.Reader & Partial<io.Seeker>
3164
+ if (typeof seeker.Seek !== 'function') {
3165
+ const [data, err] = await io.ReadAll(content)
3166
+ if (err != null) {
3167
+ Error(w, err.Error(), StatusInternalServerError)
3168
+ return
3169
+ }
3170
+ const body = data ?? new Uint8Array(0)
3171
+ Header_Set(header, 'Content-Length', String(body.length))
3172
+ await w.WriteHeader(StatusOK)
3173
+ if (req?.Method !== MethodHead) {
3174
+ await w.Write(body)
3175
+ }
3176
+ return
3177
+ }
3178
+
3179
+ let size = knownSize
3180
+ if (size == null) {
3181
+ const [end, err] = await seeker.Seek(0n, io.SeekEnd)
3182
+ if (err != null) {
3183
+ Error(w, err.Error(), StatusInternalServerError)
3184
+ return
3185
+ }
3186
+ size = Number(end)
3187
+ }
3188
+ const [, seekErr] = await seeker.Seek(0n, io.SeekStart)
3189
+ if (seekErr != null) {
3190
+ Error(w, seekErr.Error(), StatusInternalServerError)
3191
+ return
3192
+ }
3193
+
3194
+ Header_Set(header, 'Accept-Ranges', 'bytes')
3195
+ const parsedRange =
3196
+ rangeHeader === '' ? null : parseHTTPRange(rangeHeader, size)
3197
+ if (parsedRange?.error === true) {
3198
+ Header_Set(header, 'Content-Range', `bytes */${size}`)
3199
+ Header_Set(header, 'Content-Length', '0')
3200
+ await w.WriteHeader(StatusRequestedRangeNotSatisfiable)
3201
+ return
3202
+ }
3203
+
3204
+ let status = StatusOK
3205
+ let start = 0
3206
+ let length = size
3207
+ if (parsedRange?.range != null) {
3208
+ status = StatusPartialContent
3209
+ start = parsedRange.range.start
3210
+ length = parsedRange.range.length
3211
+ Header_Set(
3212
+ header,
3213
+ 'Content-Range',
3214
+ `bytes ${start}-${start + length - 1}/${size}`,
3215
+ )
3216
+ }
3217
+ Header_Set(header, 'Content-Length', String(length))
3218
+
3219
+ const [, rangeSeekErr] = await seeker.Seek(BigInt(start), io.SeekStart)
3220
+ if (rangeSeekErr != null) {
3221
+ Error(w, rangeSeekErr.Error(), StatusInternalServerError)
2742
3222
  return
2743
3223
  }
2744
- w?.WriteHeader(StatusOK)
2745
- const request = $.pointerValueOrNil(req)
2746
- if (request?.Method !== MethodHead) {
2747
- w?.Write(data)
3224
+ await w.WriteHeader(status)
3225
+ if (req?.Method === MethodHead) {
3226
+ return
3227
+ }
3228
+ if (length === 0) {
3229
+ return
3230
+ }
3231
+ await io.CopyN(w as unknown as io.Writer, seeker, BigInt(length))
3232
+ }
3233
+
3234
+ function parseHTTPRange(
3235
+ header: string,
3236
+ size: number,
3237
+ ): { range: httpRange | null; error: boolean } {
3238
+ if (!header.startsWith('bytes=') || header.includes(',')) {
3239
+ return { range: null, error: true }
3240
+ }
3241
+ const spec = header.slice('bytes='.length).trim()
3242
+ const dash = spec.indexOf('-')
3243
+ if (dash < 0) {
3244
+ return { range: null, error: true }
3245
+ }
3246
+ const startText = spec.slice(0, dash).trim()
3247
+ const endText = spec.slice(dash + 1).trim()
3248
+ if (startText === '' && endText === '') {
3249
+ return { range: null, error: true }
3250
+ }
3251
+ if (size < 0) {
3252
+ return { range: null, error: true }
3253
+ }
3254
+ if (startText === '') {
3255
+ const suffixLength = parseHTTPRangeNumber(endText)
3256
+ if (suffixLength == null || suffixLength <= 0) {
3257
+ return { range: null, error: true }
3258
+ }
3259
+ if (size === 0) {
3260
+ return { range: null, error: true }
3261
+ }
3262
+ const length = Math.min(suffixLength, size)
3263
+ return { range: { start: size - length, length }, error: false }
3264
+ }
3265
+
3266
+ const start = parseHTTPRangeNumber(startText)
3267
+ if (start == null || start >= size) {
3268
+ return { range: null, error: true }
3269
+ }
3270
+ let end = size - 1
3271
+ if (endText !== '') {
3272
+ const parsedEnd = parseHTTPRangeNumber(endText)
3273
+ if (parsedEnd == null || parsedEnd < start) {
3274
+ return { range: null, error: true }
3275
+ }
3276
+ end = Math.min(parsedEnd, size - 1)
3277
+ }
3278
+ return { range: { start, length: end - start + 1 }, error: false }
3279
+ }
3280
+
3281
+ function parseHTTPRangeNumber(value: string): number | null {
3282
+ if (!/^[0-9]+$/.test(value)) {
3283
+ return null
3284
+ }
3285
+ const parsed = Number(value)
3286
+ if (!Number.isSafeInteger(parsed)) {
3287
+ return null
2748
3288
  }
3289
+ return parsed
2749
3290
  }
2750
3291
 
2751
3292
  function readCloserForBody(body: io.Reader | null): io.ReadCloser | null {