acrylblends 0.6.3 → 0.6.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (269) hide show
  1. package/README.md +3 -2
  2. package/docs/framework/README.md +2 -2
  3. package/docs/framework/UPSTREAM.md +1 -1
  4. package/docs/framework/guides/jsx.md +24 -0
  5. package/docs/framework/helpers/ssg.md +3 -6
  6. package/docs/framework/middleware/builtin/jsx-renderer.md +0 -3
  7. package/docs/framework/upstream.json +2 -2
  8. package/lib/accepts.d.ts +1 -1
  9. package/lib/acryl-base.d.ts +1 -1
  10. package/lib/adapter.d.ts +1 -1
  11. package/lib/{app-wFwpg-bt.d.ts → app-D8riatvv.d.ts} +1 -1
  12. package/lib/basic-auth.d.ts +1 -1
  13. package/lib/bearer-auth.d.ts +1 -1
  14. package/lib/body-limit.d.ts +1 -1
  15. package/lib/cache.d.ts +1 -1
  16. package/lib/cache.js +4 -4
  17. package/lib/{client-BHTr318g.js → client-DDLXwhBo.js} +2 -125
  18. package/lib/client.d.ts +2 -2
  19. package/lib/client.js +2 -1
  20. package/lib/combine.d.ts +1 -1
  21. package/lib/{components-Ce5FRyHU.js → components-0o0CBYNv.js} +2 -2
  22. package/lib/{components-DYZdxJIJ.js → components-DdI7-ns7.js} +1 -1
  23. package/lib/compress.d.ts +1 -1
  24. package/lib/conninfo.d.ts +2 -2
  25. package/lib/{context-DVQBjuns.d.ts → context-DS3fYyG-.d.ts} +1 -1
  26. package/lib/context-storage.d.ts +1 -1
  27. package/lib/cookie.d.ts +1 -1
  28. package/lib/cors.d.ts +1 -1
  29. package/lib/csrf.d.ts +1 -1
  30. package/lib/dev.d.ts +2 -2
  31. package/lib/etag.d.ts +1 -1
  32. package/lib/factory.d.ts +2 -2
  33. package/lib/{hooks-Bq5q9IMl.js → hooks-BMw6BAyP.js} +5 -5
  34. package/lib/{index-CUTfU69R.d.ts → index-Bb7YJKCq.d.ts} +2 -2
  35. package/lib/{index-CsvB-F9j.d.ts → index-BvFh1Gfx.d.ts} +1 -1
  36. package/lib/{index-Cqcdluyn.d.ts → index-CTy4N7ng.d.ts} +2 -2
  37. package/lib/index.d.ts +4 -4
  38. package/lib/ip-restriction.d.ts +2 -2
  39. package/lib/jsx/dom/client.js +1 -1
  40. package/lib/jsx/dom/jsx-dev-runtime.js +1 -1
  41. package/lib/jsx/dom/jsx-runtime.js +1 -1
  42. package/lib/jsx/dom/server.js +1 -1
  43. package/lib/jsx/dom.js +3 -3
  44. package/lib/jsx/streaming.js +1 -1
  45. package/lib/{jsx-dev-runtime-kAhR_4C7.js → jsx-dev-runtime-D18Y35TI.js} +1 -1
  46. package/lib/jsx-renderer.d.ts +75 -0
  47. package/lib/jsx-renderer.js +99 -0
  48. package/lib/jsx.js +3 -3
  49. package/lib/jwk.d.ts +1 -1
  50. package/lib/jwt.d.ts +1 -1
  51. package/lib/language.d.ts +1 -1
  52. package/lib/logger.d.ts +1 -1
  53. package/lib/loopback-only.d.ts +1 -1
  54. package/lib/method-not-allowed.d.ts +2 -2
  55. package/lib/method-override.d.ts +2 -2
  56. package/lib/mount.d.ts +1 -1
  57. package/lib/node-server/conninfo.d.ts +1 -1
  58. package/lib/node-server/serve-static.d.ts +1 -1
  59. package/lib/node-server/serve-static.js +1 -1
  60. package/lib/node-server.d.ts +1 -1
  61. package/lib/node-server.js +1 -1
  62. package/lib/powered-by.d.ts +1 -1
  63. package/lib/pretty-json.d.ts +1 -1
  64. package/lib/quick.d.ts +1 -1
  65. package/lib/request-id.d.ts +1 -1
  66. package/lib/request.d.ts +1 -1
  67. package/lib/route.d.ts +1 -1
  68. package/lib/schema-validator.d.ts +1 -1
  69. package/lib/secure-headers.d.ts +1 -1
  70. package/lib/{serve-static-Cw23shXv.js → serve-static-B14MF1Va.js} +1 -0
  71. package/lib/serve-static.d.ts +1 -1
  72. package/lib/ssg-BwjcA-Ge.js +389 -0
  73. package/lib/ssg.d.ts +156 -0
  74. package/lib/ssg.js +2 -0
  75. package/lib/streaming.d.ts +1 -1
  76. package/lib/testing.d.ts +3 -3
  77. package/lib/testing.js +1 -1
  78. package/lib/timeout.d.ts +1 -1
  79. package/lib/timing.d.ts +1 -1
  80. package/lib/tiny.d.ts +1 -1
  81. package/lib/trailing-slash.d.ts +1 -1
  82. package/lib/{types-nEL8VPDC.d.ts → types-Dv_nV2cv.d.ts} +1 -1
  83. package/lib/{types-CQiGmsWW.d.ts → types-zNxyEpG2.d.ts} +2 -2
  84. package/lib/types.d.ts +1 -1
  85. package/lib/utils/body.d.ts +1 -1
  86. package/lib/utils/ipaddr.d.ts +1 -1
  87. package/lib/utils-Cx61GFMC.js +125 -0
  88. package/lib/validator.d.ts +1 -1
  89. package/lib/ws.d.ts +1 -1
  90. package/package.json +9 -1
  91. package/plugins/acryl-inspect/upstream.json +1 -1
  92. package/plugins/acryl-node-server/upstream.json +1 -1
  93. package/plugins/acryl-router/src/core/helper/ssg/index.ts +14 -0
  94. package/plugins/acryl-router/src/core/helper/ssg/middleware.ts +83 -0
  95. package/plugins/acryl-router/src/core/helper/ssg/plugins.ts +72 -0
  96. package/plugins/acryl-router/src/core/helper/ssg/ssg.ts +471 -0
  97. package/plugins/acryl-router/src/core/helper/ssg/utils.ts +118 -0
  98. package/plugins/acryl-router/src/core/jsx/dom/render.ts +6 -6
  99. package/plugins/acryl-router/src/core/middleware/cache/index.ts +4 -4
  100. package/plugins/acryl-router/src/core/middleware/jsx-renderer/index.ts +162 -0
  101. package/plugins/acryl-router/src/helpers/index.ts +1 -0
  102. package/plugins/acryl-router/src/middleware/index.ts +1 -0
  103. package/plugins/acryl-router/upstream.json +2 -2
  104. package/plugins/acryl-router-http/upstream.json +1 -1
  105. package/plugins/acryl-state/upstream.json +1 -1
  106. package/plugins/desktop/upstream.json +1 -1
  107. package/plugins/desktop-web/upstream.json +1 -1
  108. package/plugins/extensions/resolve.ts +4 -0
  109. package/shared/AGENTS.core.md +4 -0
  110. package/templates/agent/tsconfig.json +7 -1
  111. package/templates/todo/upstream.json +1 -1
  112. package/plugins/acryl-inspect/tests/report.spec.ts +0 -69
  113. package/plugins/acryl-inspect/tests/service.spec.ts +0 -121
  114. package/plugins/acryl-node-server/tests/adapter/app.ts +0 -76
  115. package/plugins/acryl-node-server/tests/adapter/assets/.static/plain.txt +0 -1
  116. package/plugins/acryl-node-server/tests/adapter/assets/favicon.ico +0 -0
  117. package/plugins/acryl-node-server/tests/adapter/assets/secret.txt +0 -1
  118. package/plugins/acryl-node-server/tests/adapter/assets/static/acryl.html +0 -1
  119. package/plugins/acryl-node-server/tests/adapter/assets/static/admin/secret.txt +0 -1
  120. package/plugins/acryl-node-server/tests/adapter/assets/static/data.json +0 -5
  121. package/plugins/acryl-node-server/tests/adapter/assets/static/extensionless +0 -1
  122. package/plugins/acryl-node-server/tests/adapter/assets/static/foo..bar.txt +0 -0
  123. package/plugins/acryl-node-server/tests/adapter/assets/static/index.html +0 -1
  124. package/plugins/acryl-node-server/tests/adapter/assets/static/plain.txt +0 -1
  125. package/plugins/acryl-node-server/tests/adapter/assets/static-with-context-path/plain.txt +0 -1
  126. package/plugins/acryl-node-server/tests/adapter/assets/static-with-precompressed/hello.bin +0 -1
  127. package/plugins/acryl-node-server/tests/adapter/assets/static-with-precompressed/hello.bin.br +0 -1
  128. package/plugins/acryl-node-server/tests/adapter/assets/static-with-precompressed/hello.txt +0 -1
  129. package/plugins/acryl-node-server/tests/adapter/assets/static-with-precompressed/hello.txt.br +0 -1
  130. package/plugins/acryl-node-server/tests/adapter/assets/static-with-precompressed/hello.txt.zst +0 -1
  131. package/plugins/acryl-node-server/tests/adapter/conninfo.test.ts +0 -34
  132. package/plugins/acryl-node-server/tests/adapter/content-length.test.ts +0 -248
  133. package/plugins/acryl-node-server/tests/adapter/early-hints.test.ts +0 -457
  134. package/plugins/acryl-node-server/tests/adapter/fixtures/keys/agent1-cert.pem +0 -23
  135. package/plugins/acryl-node-server/tests/adapter/fixtures/keys/agent1-key.pem +0 -27
  136. package/plugins/acryl-node-server/tests/adapter/headers.test.ts +0 -252
  137. package/plugins/acryl-node-server/tests/adapter/helpers/request.ts +0 -190
  138. package/plugins/acryl-node-server/tests/adapter/listener.test.ts +0 -727
  139. package/plugins/acryl-node-server/tests/adapter/request-client-disconnect.test.ts +0 -189
  140. package/plugins/acryl-node-server/tests/adapter/request.test.ts +0 -1230
  141. package/plugins/acryl-node-server/tests/adapter/response.test.ts +0 -285
  142. package/plugins/acryl-node-server/tests/adapter/serve-static.test.ts +0 -849
  143. package/plugins/acryl-node-server/tests/adapter/server-socket.test.ts +0 -790
  144. package/plugins/acryl-node-server/tests/adapter/server.test.ts +0 -1312
  145. package/plugins/acryl-node-server/tests/adapter/setup.ts +0 -16
  146. package/plugins/acryl-node-server/tests/adapter/url.test.ts +0 -76
  147. package/plugins/acryl-node-server/tests/adapter/utils/response.test.ts +0 -23
  148. package/plugins/acryl-node-server/tests/adapter/utils/stream.test.ts +0 -75
  149. package/plugins/acryl-node-server/tests/adapter/utils.test.ts +0 -235
  150. package/plugins/acryl-node-server/tests/adapter/websocket.test.ts +0 -260
  151. package/plugins/acryl-node-server/tests/server.spec.ts +0 -141
  152. package/plugins/acryl-router/src/core/app.test.ts +0 -3833
  153. package/plugins/acryl-router/src/core/client/client.test.ts +0 -2313
  154. package/plugins/acryl-router/src/core/client/types.test.ts +0 -149
  155. package/plugins/acryl-router/src/core/client/utils.test.ts +0 -405
  156. package/plugins/acryl-router/src/core/compose.test.ts +0 -751
  157. package/plugins/acryl-router/src/core/context.test.ts +0 -629
  158. package/plugins/acryl-router/src/core/helper/accepts/accepts.test.ts +0 -344
  159. package/plugins/acryl-router/src/core/helper/adapter/index.test.ts +0 -55
  160. package/plugins/acryl-router/src/core/helper/cookie/index.test.ts +0 -510
  161. package/plugins/acryl-router/src/core/helper/css/common.case.test.tsx +0 -506
  162. package/plugins/acryl-router/src/core/helper/css/index.test.tsx +0 -678
  163. package/plugins/acryl-router/src/core/helper/dev/index.test.ts +0 -181
  164. package/plugins/acryl-router/src/core/helper/factory/index.test.ts +0 -434
  165. package/plugins/acryl-router/src/core/helper/html/index.test.ts +0 -78
  166. package/plugins/acryl-router/src/core/helper/proxy/index.test.ts +0 -356
  167. package/plugins/acryl-router/src/core/helper/route/index.test.ts +0 -259
  168. package/plugins/acryl-router/src/core/helper/streaming/sse.test.tsx +0 -557
  169. package/plugins/acryl-router/src/core/helper/streaming/stream.test.ts +0 -128
  170. package/plugins/acryl-router/src/core/helper/streaming/text.test.ts +0 -34
  171. package/plugins/acryl-router/src/core/helper/testing/index.test.ts +0 -42
  172. package/plugins/acryl-router/src/core/helper/websocket/index.test.ts +0 -124
  173. package/plugins/acryl-router/src/core/http-exception.test.ts +0 -45
  174. package/plugins/acryl-router/src/core/jsx/base.test.tsx +0 -97
  175. package/plugins/acryl-router/src/core/jsx/children.test.ts +0 -52
  176. package/plugins/acryl-router/src/core/jsx/components.test.tsx +0 -936
  177. package/plugins/acryl-router/src/core/jsx/context-isolation.test.ts +0 -655
  178. package/plugins/acryl-router/src/core/jsx/dom/client.test.tsx +0 -128
  179. package/plugins/acryl-router/src/core/jsx/dom/components.test.tsx +0 -330
  180. package/plugins/acryl-router/src/core/jsx/dom/context.test.tsx +0 -184
  181. package/plugins/acryl-router/src/core/jsx/dom/css.test.tsx +0 -149
  182. package/plugins/acryl-router/src/core/jsx/dom/hooks/index.test.tsx +0 -172
  183. package/plugins/acryl-router/src/core/jsx/dom/index.test.tsx +0 -3681
  184. package/plugins/acryl-router/src/core/jsx/dom/intrinsic-element/components.test.tsx +0 -1344
  185. package/plugins/acryl-router/src/core/jsx/dom/server.test.tsx +0 -199
  186. package/plugins/acryl-router/src/core/jsx/hooks/dom.test.tsx +0 -859
  187. package/plugins/acryl-router/src/core/jsx/hooks/string.test.tsx +0 -40
  188. package/plugins/acryl-router/src/core/jsx/index.test.tsx +0 -1744
  189. package/plugins/acryl-router/src/core/jsx/intrinsic-element/components.test.tsx +0 -666
  190. package/plugins/acryl-router/src/core/jsx/jsx-runtime.test.tsx +0 -85
  191. package/plugins/acryl-router/src/core/jsx/streaming.test.tsx +0 -1281
  192. package/plugins/acryl-router/src/core/jsx/utils.test.ts +0 -366
  193. package/plugins/acryl-router/src/core/middleware/basic-auth/index.test.ts +0 -434
  194. package/plugins/acryl-router/src/core/middleware/bearer-auth/index.test.ts +0 -1018
  195. package/plugins/acryl-router/src/core/middleware/body-limit/index.test.ts +0 -273
  196. package/plugins/acryl-router/src/core/middleware/cache/index.test.ts +0 -1211
  197. package/plugins/acryl-router/src/core/middleware/combine/index.test.ts +0 -355
  198. package/plugins/acryl-router/src/core/middleware/compress/index.test.ts +0 -535
  199. package/plugins/acryl-router/src/core/middleware/context-storage/index.test.ts +0 -43
  200. package/plugins/acryl-router/src/core/middleware/cors/index.test.ts +0 -532
  201. package/plugins/acryl-router/src/core/middleware/csrf/index.test.ts +0 -534
  202. package/plugins/acryl-router/src/core/middleware/etag/digest.test.ts +0 -21
  203. package/plugins/acryl-router/src/core/middleware/etag/index.test.ts +0 -503
  204. package/plugins/acryl-router/src/core/middleware/ip-restriction/index.test.ts +0 -217
  205. package/plugins/acryl-router/src/core/middleware/jwk/index.test.ts +0 -1135
  206. package/plugins/acryl-router/src/core/middleware/jwk/keys.test.json +0 -48
  207. package/plugins/acryl-router/src/core/middleware/jwt/index.test.ts +0 -802
  208. package/plugins/acryl-router/src/core/middleware/language/index.test.ts +0 -552
  209. package/plugins/acryl-router/src/core/middleware/logger/index.test.ts +0 -204
  210. package/plugins/acryl-router/src/core/middleware/method-not-allowed/index.test.ts +0 -292
  211. package/plugins/acryl-router/src/core/middleware/method-override/index.test.ts +0 -221
  212. package/plugins/acryl-router/src/core/middleware/mount/index.test.ts +0 -334
  213. package/plugins/acryl-router/src/core/middleware/powered-by/index.test.ts +0 -37
  214. package/plugins/acryl-router/src/core/middleware/pretty-json/index.test.ts +0 -218
  215. package/plugins/acryl-router/src/core/middleware/request-id/index.test.ts +0 -168
  216. package/plugins/acryl-router/src/core/middleware/secure-headers/index.test.ts +0 -671
  217. package/plugins/acryl-router/src/core/middleware/serve-static/index.test.ts +0 -450
  218. package/plugins/acryl-router/src/core/middleware/serve-static/path.test.ts +0 -50
  219. package/plugins/acryl-router/src/core/middleware/timeout/index.test.ts +0 -70
  220. package/plugins/acryl-router/src/core/middleware/timing/index.test.ts +0 -310
  221. package/plugins/acryl-router/src/core/middleware/trailing-slash/index.test.ts +0 -381
  222. package/plugins/acryl-router/src/core/preset/quick.test.ts +0 -42
  223. package/plugins/acryl-router/src/core/preset/tiny.test.ts +0 -42
  224. package/plugins/acryl-router/src/core/request.test.ts +0 -751
  225. package/plugins/acryl-router/src/core/router/common.case.test.ts +0 -1132
  226. package/plugins/acryl-router/src/core/router/linear-router/router.test.ts +0 -80
  227. package/plugins/acryl-router/src/core/router/pattern-router/router.test.ts +0 -46
  228. package/plugins/acryl-router/src/core/router/reg-exp-router/prepared-router.test.ts +0 -154
  229. package/plugins/acryl-router/src/core/router/reg-exp-router/router.test.ts +0 -265
  230. package/plugins/acryl-router/src/core/router/smart-router/router.test.ts +0 -13
  231. package/plugins/acryl-router/src/core/router/trie-router/node.test.ts +0 -896
  232. package/plugins/acryl-router/src/core/router/trie-router/router.test.ts +0 -8
  233. package/plugins/acryl-router/src/core/types.test.ts +0 -4134
  234. package/plugins/acryl-router/src/core/utils/accept.test.ts +0 -299
  235. package/plugins/acryl-router/src/core/utils/basic-auth.test.ts +0 -56
  236. package/plugins/acryl-router/src/core/utils/body.test.ts +0 -496
  237. package/plugins/acryl-router/src/core/utils/buffer.test.ts +0 -147
  238. package/plugins/acryl-router/src/core/utils/color.test.ts +0 -23
  239. package/plugins/acryl-router/src/core/utils/concurrent.test.ts +0 -69
  240. package/plugins/acryl-router/src/core/utils/cookie.test.ts +0 -460
  241. package/plugins/acryl-router/src/core/utils/crypto.test.ts +0 -43
  242. package/plugins/acryl-router/src/core/utils/encode.test.ts +0 -61
  243. package/plugins/acryl-router/src/core/utils/filepath.test.ts +0 -78
  244. package/plugins/acryl-router/src/core/utils/html.test.ts +0 -16
  245. package/plugins/acryl-router/src/core/utils/ipaddr.test.ts +0 -149
  246. package/plugins/acryl-router/src/core/utils/jwt/jwa.test.ts +0 -86
  247. package/plugins/acryl-router/src/core/utils/jwt/jwt.test.ts +0 -1788
  248. package/plugins/acryl-router/src/core/utils/mime.test.ts +0 -64
  249. package/plugins/acryl-router/src/core/utils/stream.test.ts +0 -212
  250. package/plugins/acryl-router/src/core/utils/types.test.ts +0 -295
  251. package/plugins/acryl-router/src/core/utils/url.test.ts +0 -384
  252. package/plugins/acryl-router/src/core/validator/utils.test.ts +0 -82
  253. package/plugins/acryl-router/src/core/validator/validator.test.ts +0 -1488
  254. package/plugins/acryl-router/tests/docs-examples.spec.ts +0 -152
  255. package/plugins/acryl-router/tests/jsx.spec.tsx +0 -63
  256. package/plugins/acryl-router/tests/live-install.spec.ts +0 -64
  257. package/plugins/acryl-router/tests/loader.spec.ts +0 -72
  258. package/plugins/acryl-router/tests/loopback-only.spec.ts +0 -80
  259. package/plugins/acryl-router/tests/no-dom/consumer.ts +0 -11
  260. package/plugins/acryl-router/tests/no-dom/tsconfig.json +0 -13
  261. package/plugins/acryl-router/tests/service.spec.ts +0 -426
  262. package/plugins/acryl-router/tests/shadow-state.spec.ts +0 -79
  263. package/plugins/acryl-router-http/tests/adapter.spec.ts +0 -174
  264. package/plugins/acryl-router-http/tests/stream.spec.ts +0 -133
  265. package/plugins/acryl-router-http/tests/surfaces.spec.ts +0 -101
  266. package/plugins/acryl-state/src/react.ts +0 -11
  267. package/plugins/acryl-state/tests/cell.spec.ts +0 -71
  268. package/plugins/acryl-state/tests/react.spec.ts +0 -39
  269. package/plugins/acryl-state/tests/service.spec.ts +0 -132
@@ -1,1230 +0,0 @@
1
- import { IncomingMessage } from 'node:http'
2
- import type { ServerHttp2Stream } from 'node:http2'
3
- import { Http2ServerRequest } from 'node:http2'
4
- import { Socket } from 'node:net'
5
- import { Duplex } from 'node:stream'
6
- import {
7
- abortRequest,
8
- newRequest,
9
- Request as LightweightRequest,
10
- GlobalRequest,
11
- getAbortController,
12
- abortControllerKey,
13
- RequestError,
14
- } from '../../src/adapter/request.ts'
15
-
16
- Object.defineProperty(global, 'Request', {
17
- value: LightweightRequest,
18
- })
19
-
20
- describe('Request', () => {
21
- describe('newRequest', () => {
22
- it('Compatibility with standard Request object', async () => {
23
- const req = newRequest({
24
- method: 'GET',
25
- url: '/',
26
- headers: {
27
- host: 'localhost',
28
- },
29
- rawHeaders: ['host', 'localhost'],
30
- } as IncomingMessage)
31
-
32
- expect(req).toBeInstanceOf(global.Request)
33
- expect(req.method).toBe('GET')
34
- expect(req.url).toBe('http://localhost/')
35
- expect(req.headers.get('host')).toBe('localhost')
36
- expect(req.keepalive).toBe(false)
37
- })
38
-
39
- it('Should not generate GlobalRequest when accessing method, url or headers', async () => {
40
- const req = newRequest({
41
- method: 'GET',
42
- url: '/',
43
- headers: {
44
- host: 'localhost',
45
- },
46
- rawHeaders: ['host', 'localhost'],
47
- } as IncomingMessage)
48
-
49
- // keep lightweight request even if accessing method, url or headers
50
- expect(req.method).toBe('GET')
51
- expect(req.url).toBe('http://localhost/')
52
- expect(req.headers.get('host')).toBe('localhost')
53
- expect(req[abortControllerKey]).toBeUndefined()
54
-
55
- // generate GlobalRequest
56
- expect(req.keepalive).toBe(false)
57
- expect(req[abortControllerKey]).toBeDefined()
58
- })
59
-
60
- it('Should resolve double dots in URL', async () => {
61
- const req = newRequest({
62
- headers: {
63
- host: 'localhost',
64
- },
65
- url: '/static/../foo.txt',
66
- } as IncomingMessage)
67
- expect(req).toBeInstanceOf(global.Request)
68
- expect(req.url).toBe('http://localhost/foo.txt')
69
- })
70
-
71
- it('Should accept hostname and port in host header', async () => {
72
- const req = newRequest({
73
- headers: {
74
- host: 'localhost:8080',
75
- },
76
- url: '/static/../foo.txt',
77
- } as IncomingMessage)
78
- expect(req).toBeInstanceOf(global.Request)
79
- expect(req.url).toBe('http://localhost:8080/foo.txt')
80
- })
81
-
82
- it('should generate only one `AbortController` per `Request` object created', async () => {
83
- const req = newRequest({
84
- headers: {
85
- host: 'localhost',
86
- },
87
- rawHeaders: ['host', 'localhost'],
88
- url: '/foo.txt',
89
- } as IncomingMessage)
90
- const req2 = newRequest({
91
- headers: {
92
- host: 'localhost',
93
- },
94
- rawHeaders: ['host', 'localhost'],
95
- url: '/foo.txt',
96
- } as IncomingMessage)
97
-
98
- const x = req[getAbortController]()
99
- const y = req[getAbortController]()
100
- const z = req2[getAbortController]()
101
-
102
- expect(x).toBeInstanceOf(AbortController)
103
- expect(y).toBeInstanceOf(AbortController)
104
- expect(z).toBeInstanceOf(AbortController)
105
- expect(x).toBe(y)
106
- expect(z).not.toBe(x)
107
- expect(z).not.toBe(y)
108
- })
109
-
110
- it('should be able to safely check if an AbortController has been initialized by referencing the abortControllerKey', async () => {
111
- const req = newRequest({
112
- headers: {
113
- host: 'localhost',
114
- },
115
- rawHeaders: ['host', 'localhost'],
116
- url: '/foo.txt',
117
- } as IncomingMessage)
118
-
119
- expect(req[abortControllerKey]).toBeUndefined() // not initialized, do not initialize internal request object automatically
120
-
121
- expect(req[getAbortController]()).toBeDefined()
122
- expect(req[abortControllerKey]).toBeDefined() // initialized
123
- })
124
-
125
- it('should apply abort state lazily when signal is accessed after json() body read', async () => {
126
- const req = newRequest({
127
- method: 'POST',
128
- headers: {
129
- host: 'localhost',
130
- 'content-type': 'application/json',
131
- },
132
- rawHeaders: ['host', 'localhost', 'content-type', 'application/json'],
133
- rawBody: Buffer.from('{"foo":"bar"}'),
134
- url: '/foo.txt',
135
- } as IncomingMessage & { rawBody: Buffer })
136
-
137
- expect(await req.json()).toEqual({ foo: 'bar' })
138
- expect(req[abortControllerKey]).toBeUndefined()
139
-
140
- req[abortRequest]('Client connection prematurely closed.')
141
- expect(req[abortControllerKey]).toBeUndefined()
142
-
143
- expect(req.signal.aborted).toBe(true)
144
- })
145
-
146
- it('should preserve an application error when a complete stream is destroyed mid-read', async () => {
147
- const socket = new Socket()
148
- const incomingMessage = new IncomingMessage(socket)
149
- incomingMessage.method = 'POST'
150
- incomingMessage.headers = {
151
- host: 'localhost',
152
- }
153
- incomingMessage.rawHeaders = ['host', 'localhost']
154
- incomingMessage.url = '/foo.txt'
155
- const req = newRequest(incomingMessage)
156
-
157
- const textPromise = req.json()
158
- const error = new Error('Payload too large')
159
- incomingMessage.complete = true
160
- incomingMessage.destroy(error)
161
-
162
- await expect(textPromise).rejects.toBe(error)
163
- })
164
-
165
- it.each([
166
- ['without a stream error', undefined],
167
- [
168
- 'with a client disconnect error',
169
- Object.assign(new Error('aborted'), { code: 'ECONNRESET' }),
170
- ],
171
- ])(
172
- 'should preserve a complete body when the stream closes mid-read %s',
173
- async (_label, destroyError) => {
174
- const socket = new Socket()
175
- const incomingMessage = new IncomingMessage(socket)
176
- incomingMessage.method = 'POST'
177
- incomingMessage.headers = {
178
- host: 'localhost',
179
- 'content-length': '6',
180
- }
181
- incomingMessage.rawHeaders = ['host', 'localhost', 'content-length', '6']
182
- incomingMessage.url = '/foo.txt'
183
- const req = newRequest(incomingMessage)
184
-
185
- const textPromise = req.text()
186
- incomingMessage.push('foo')
187
- incomingMessage.pause()
188
- incomingMessage.push('bar')
189
- incomingMessage.complete = true
190
- incomingMessage.destroy(destroyError)
191
-
192
- await expect(textPromise).resolves.toBe('foobar')
193
- }
194
- )
195
-
196
- it('should reject direct body read when incoming stream is destroyed without error', async () => {
197
- const socket = new Socket()
198
- const incomingMessage = new IncomingMessage(socket)
199
- incomingMessage.method = 'POST'
200
- incomingMessage.headers = {
201
- host: 'localhost',
202
- }
203
- incomingMessage.rawHeaders = ['host', 'localhost']
204
- incomingMessage.url = '/foo.txt'
205
- const req = newRequest(incomingMessage)
206
-
207
- const textPromise = req.text()
208
- incomingMessage.destroy()
209
-
210
- await expect(textPromise).rejects.toBeInstanceOf(Error)
211
- })
212
-
213
- it('should preserve the connection error when an incomplete stream was destroyed before reading', async () => {
214
- const socket = new Socket()
215
- const incomingMessage = new IncomingMessage(socket)
216
- incomingMessage.method = 'POST'
217
- incomingMessage.headers = {
218
- host: 'localhost',
219
- 'content-length': '6',
220
- }
221
- incomingMessage.rawHeaders = ['host', 'localhost', 'content-length', '6']
222
- incomingMessage.url = '/foo.txt'
223
- incomingMessage.push('foo')
224
- incomingMessage.on('error', () => {})
225
- const req = newRequest(incomingMessage)
226
-
227
- const closed = new Promise<void>((resolve) => {
228
- incomingMessage.once('close', resolve)
229
- })
230
- incomingMessage.destroy(new Error('aborted'))
231
- await closed
232
-
233
- await expect(req.text()).rejects.toThrow('aborted')
234
- })
235
-
236
- const newDestroyedIncomingWithCompleteBody = async (
237
- body: string,
238
- headers: Record<string, string> = {},
239
- destroyError?: Error
240
- ): Promise<IncomingMessage> => {
241
- const socket = new Socket()
242
- const incomingMessage = new IncomingMessage(socket)
243
- incomingMessage.method = 'POST'
244
- incomingMessage.headers = { host: 'localhost', ...headers }
245
- incomingMessage.rawHeaders = ['host', 'localhost', ...Object.entries(headers).flat()]
246
- incomingMessage.url = '/foo.txt'
247
- incomingMessage.push(body)
248
- incomingMessage.complete = true
249
- incomingMessage.on('error', () => {})
250
-
251
- const closed = new Promise<void>((resolve) => {
252
- incomingMessage.once('close', resolve)
253
- })
254
- incomingMessage.destroy(destroyError)
255
- await closed
256
- return incomingMessage
257
- }
258
-
259
- it('should preserve the complete buffered body when the stream was destroyed before reading', async () => {
260
- const incomingMessage = await newDestroyedIncomingWithCompleteBody('foo', {
261
- 'content-length': '3',
262
- })
263
- const req = newRequest(incomingMessage)
264
-
265
- await expect(req.text()).resolves.toBe('foo')
266
- })
267
-
268
- it('should reject when the buffered body does not match content-length', async () => {
269
- const incomingMessage = await newDestroyedIncomingWithCompleteBody('foo', {
270
- 'content-length': '6',
271
- })
272
- const req = newRequest(incomingMessage)
273
-
274
- await expect(req.text()).rejects.toThrow('Body is unusable')
275
- })
276
-
277
- it('should preserve the complete buffered body for standard path reads like formData()', async () => {
278
- const incomingMessage = await newDestroyedIncomingWithCompleteBody('hello=world&foo=bar', {
279
- 'content-type': 'application/x-www-form-urlencoded',
280
- 'content-length': '19',
281
- })
282
- const req = newRequest(incomingMessage)
283
-
284
- const formData = await req.formData()
285
- expect(Object.fromEntries(formData.entries())).toEqual({ hello: 'world', foo: 'bar' })
286
- })
287
-
288
- it('should preserve the complete buffered body when read through the body stream', async () => {
289
- const incomingMessage = await newDestroyedIncomingWithCompleteBody('foo', {
290
- 'content-length': '3',
291
- })
292
- const req = newRequest(incomingMessage)
293
-
294
- await expect(new Response(req.body).text()).resolves.toBe('foo')
295
- })
296
-
297
- it('should not recover only the remaining chunked body after a partial read', async () => {
298
- const socket = new Socket()
299
- const incomingMessage = new IncomingMessage(socket)
300
- incomingMessage.method = 'POST'
301
- incomingMessage.headers = { host: 'localhost', 'transfer-encoding': 'chunked' }
302
- incomingMessage.rawHeaders = ['host', 'localhost', 'transfer-encoding', 'chunked']
303
- incomingMessage.url = '/foo.txt'
304
- incomingMessage.push('first')
305
- expect(incomingMessage.read()).toEqual(Buffer.from('first'))
306
- expect(incomingMessage.readableDidRead).toBe(true)
307
- incomingMessage.push('second')
308
- incomingMessage.complete = true
309
- incomingMessage.on('error', () => {})
310
-
311
- const closed = new Promise<void>((resolve) => {
312
- incomingMessage.once('close', resolve)
313
- })
314
- incomingMessage.destroy()
315
- await closed
316
-
317
- const req = newRequest(incomingMessage)
318
- await expect(async () => new Response(req.body).text()).rejects.toThrow()
319
- })
320
-
321
- it('should reject with the underlying error when a complete stream was destroyed with an application error', async () => {
322
- const incomingMessage = await newDestroyedIncomingWithCompleteBody(
323
- 'foo',
324
- { 'content-length': '3' },
325
- new Error('Payload too large')
326
- )
327
- const req = newRequest(incomingMessage)
328
-
329
- await expect(req.text()).rejects.toThrow('Payload too large')
330
- })
331
-
332
- it('should reject standard path reads when a complete stream was destroyed with an application error', async () => {
333
- const incomingMessage = await newDestroyedIncomingWithCompleteBody(
334
- 'hello=world',
335
- {
336
- 'content-type': 'application/x-www-form-urlencoded',
337
- 'content-length': '11',
338
- },
339
- new Error('Payload too large')
340
- )
341
- const req = newRequest(incomingMessage)
342
-
343
- await expect(req.formData()).rejects.toThrow('Payload too large')
344
- })
345
-
346
- it('should preserve the complete buffered body when destroyed by a client disconnect error', async () => {
347
- const incomingMessage = await newDestroyedIncomingWithCompleteBody(
348
- 'foo',
349
- { 'content-length': '3' },
350
- Object.assign(new Error('aborted'), { code: 'ECONNRESET' })
351
- )
352
- const req = newRequest(incomingMessage)
353
-
354
- await expect(req.text()).resolves.toBe('foo')
355
- })
356
-
357
- it('should preserve the complete buffered body when request properties were accessed after disconnect', async () => {
358
- const incomingMessage = await newDestroyedIncomingWithCompleteBody('foo', {
359
- 'content-length': '3',
360
- })
361
- const req = newRequest(incomingMessage)
362
-
363
- // creates the internal native Request cache before the body is read
364
- void req.cache
365
-
366
- await expect(req.text()).resolves.toBe('foo')
367
- })
368
-
369
- it('should skip content-length validation for non-canonical header values', async () => {
370
- // The HTTP parser guarantees digit-only content-length on real
371
- // connections; synthetic values must not be coerced into a bogus length.
372
- for (const contentLength of ['0x10', '10, 10', '-3']) {
373
- const incomingMessage = await newDestroyedIncomingWithCompleteBody('foo', {
374
- 'content-length': contentLength,
375
- })
376
- const req = newRequest(incomingMessage)
377
-
378
- await expect(req.text()).resolves.toBe('foo')
379
- }
380
- })
381
-
382
- it('should reconstruct the original bytes when setEncoding() was used before disconnect', async () => {
383
- const socket = new Socket()
384
- const incomingMessage = new IncomingMessage(socket)
385
- incomingMessage.method = 'POST'
386
- incomingMessage.headers = { host: 'localhost' }
387
- incomingMessage.rawHeaders = ['host', 'localhost']
388
- incomingMessage.url = '/foo.txt'
389
- incomingMessage.setEncoding('latin1')
390
- incomingMessage.push(Buffer.from('café', 'utf8'))
391
- incomingMessage.complete = true
392
- incomingMessage.on('error', () => {})
393
-
394
- const closed = new Promise<void>((resolve) => {
395
- incomingMessage.once('close', resolve)
396
- })
397
- incomingMessage.destroy()
398
- await closed
399
-
400
- const req = newRequest(incomingMessage)
401
- await expect(req.text()).resolves.toBe('café')
402
- })
403
-
404
- it('should not recover a body buffered with a lossy encoding', async () => {
405
- for (const encoding of ['ascii', 'utf8'] as const) {
406
- const socket = new Socket()
407
- const incomingMessage = new IncomingMessage(socket)
408
- incomingMessage.method = 'POST'
409
- incomingMessage.headers = { host: 'localhost' }
410
- incomingMessage.rawHeaders = ['host', 'localhost']
411
- incomingMessage.url = '/foo.txt'
412
- incomingMessage.setEncoding(encoding)
413
- incomingMessage.push(Buffer.from([0x61, 0xe9, 0x62]))
414
- incomingMessage.complete = true
415
- incomingMessage.on('error', () => {})
416
-
417
- const closed = new Promise<void>((resolve) => {
418
- incomingMessage.once('close', resolve)
419
- })
420
- incomingMessage.destroy()
421
- await closed
422
-
423
- const req = newRequest(incomingMessage)
424
- await expect(req.text()).rejects.toThrow('Client connection prematurely closed.')
425
- }
426
- })
427
-
428
- it('should reconstruct the original bytes when setEncoding() was used on a streamed body', async () => {
429
- const socket = new Socket()
430
- const incomingMessage = new IncomingMessage(socket)
431
- incomingMessage.method = 'POST'
432
- incomingMessage.headers = { host: 'localhost' }
433
- incomingMessage.rawHeaders = ['host', 'localhost']
434
- incomingMessage.url = '/foo.txt'
435
- incomingMessage.setEncoding('latin1')
436
-
437
- const req = newRequest(incomingMessage)
438
- const textPromise = req.text()
439
- incomingMessage.push(Buffer.from('café', 'utf8'))
440
- incomingMessage.push(null)
441
-
442
- await expect(textPromise).resolves.toBe('café')
443
- })
444
-
445
- it('should reject direct body read for unsupported methods like native Request', async () => {
446
- for (const method of ['CONNECT', 'TRACK']) {
447
- const req = newRequest({
448
- method,
449
- headers: {
450
- host: 'localhost',
451
- 'content-type': 'text/plain',
452
- },
453
- rawHeaders: ['host', 'localhost', 'content-type', 'text/plain'],
454
- rawBody: Buffer.from('foo'),
455
- url: '/foo.txt',
456
- } as IncomingMessage & { rawBody: Buffer })
457
-
458
- await expect(req.text()).rejects.toBeInstanceOf(TypeError)
459
- }
460
- })
461
-
462
- it('should allow direct body read even when aborted before rawBody read', async () => {
463
- const req = newRequest({
464
- method: 'POST',
465
- headers: {
466
- host: 'localhost',
467
- 'content-type': 'application/json',
468
- },
469
- rawHeaders: ['host', 'localhost', 'content-type', 'application/json'],
470
- rawBody: Buffer.from('{"foo":"bar"}'),
471
- url: '/foo.txt',
472
- } as IncomingMessage & { rawBody: Buffer })
473
-
474
- req[abortRequest]('Client connection prematurely closed.')
475
-
476
- await expect(req.json()).resolves.toEqual({ foo: 'bar' })
477
- })
478
-
479
- it('should allow json() read even after signal is accessed first and then aborted', async () => {
480
- const req = newRequest({
481
- method: 'POST',
482
- headers: {
483
- host: 'localhost',
484
- 'content-type': 'application/json',
485
- },
486
- rawHeaders: ['host', 'localhost', 'content-type', 'application/json'],
487
- rawBody: Buffer.from('{"foo":"bar"}'),
488
- url: '/foo.txt',
489
- } as IncomingMessage & { rawBody: Buffer })
490
-
491
- req[abortRequest]('Client connection prematurely closed.')
492
- expect(req.signal.aborted).toBe(true)
493
-
494
- await expect(req.json()).resolves.toEqual({ foo: 'bar' })
495
- })
496
-
497
- it('should keep bodyUsed consistent after aborted read regardless of signal access order', async () => {
498
- const reqWithoutSignal = newRequest({
499
- method: 'POST',
500
- headers: {
501
- host: 'localhost',
502
- 'content-type': 'application/json',
503
- },
504
- rawHeaders: ['host', 'localhost', 'content-type', 'application/json'],
505
- rawBody: Buffer.from('{"foo":"bar"}'),
506
- url: '/foo.txt',
507
- } as IncomingMessage & { rawBody: Buffer })
508
-
509
- reqWithoutSignal[abortRequest]('Client connection prematurely closed.')
510
- await expect(reqWithoutSignal.json()).resolves.toEqual({ foo: 'bar' })
511
- expect(reqWithoutSignal.bodyUsed).toBe(true)
512
-
513
- const reqWithSignal = newRequest({
514
- method: 'POST',
515
- headers: {
516
- host: 'localhost',
517
- 'content-type': 'application/json',
518
- },
519
- rawHeaders: ['host', 'localhost', 'content-type', 'application/json'],
520
- rawBody: Buffer.from('{"foo":"bar"}'),
521
- url: '/foo.txt',
522
- } as IncomingMessage & { rawBody: Buffer })
523
-
524
- expect(reqWithSignal.signal.aborted).toBe(false)
525
- reqWithSignal[abortRequest]('Client connection prematurely closed.')
526
- expect(reqWithSignal.signal.aborted).toBe(true)
527
- await expect(reqWithSignal.json()).resolves.toEqual({ foo: 'bar' })
528
- expect(reqWithSignal.bodyUsed).toBe(true)
529
- })
530
-
531
- it('should allow clone() and formData() after abort like native Request', async () => {
532
- const reqForClone = newRequest({
533
- method: 'POST',
534
- headers: {
535
- host: 'localhost',
536
- 'content-type': 'text/plain',
537
- },
538
- rawHeaders: ['host', 'localhost', 'content-type', 'text/plain'],
539
- rawBody: Buffer.from('foo'),
540
- url: '/foo.txt',
541
- } as IncomingMessage & { rawBody: Buffer })
542
-
543
- reqForClone[abortRequest]('Client connection prematurely closed.')
544
- const cloned = reqForClone.clone()
545
- await expect(cloned.text()).resolves.toBe('foo')
546
-
547
- const reqForFormData = newRequest({
548
- method: 'POST',
549
- headers: {
550
- host: 'localhost',
551
- 'content-type': 'application/x-www-form-urlencoded',
552
- },
553
- rawHeaders: ['host', 'localhost', 'content-type', 'application/x-www-form-urlencoded'],
554
- rawBody: Buffer.from('a=1&b=2'),
555
- url: '/foo.txt',
556
- } as IncomingMessage & { rawBody: Buffer })
557
-
558
- reqForFormData[abortRequest]('Client connection prematurely closed.')
559
- const formData = await reqForFormData.formData()
560
- expect(formData.get('a')).toBe('1')
561
- expect(formData.get('b')).toBe('2')
562
- })
563
-
564
- it('should reject direct body read when incoming stream has already been consumed', async () => {
565
- const socket = new Socket()
566
- const incomingMessage = new IncomingMessage(socket)
567
- incomingMessage.method = 'POST'
568
- incomingMessage.headers = {
569
- host: 'localhost',
570
- }
571
- incomingMessage.rawHeaders = ['host', 'localhost']
572
- incomingMessage.url = '/foo.txt'
573
- incomingMessage.push('foo')
574
- incomingMessage.push(null)
575
-
576
- for await (const chunk of incomingMessage) {
577
- // consume body before lightweight request reads it
578
- expect(chunk).toBeDefined()
579
- }
580
-
581
- const req = newRequest(incomingMessage)
582
- await expect(req.text()).rejects.toBeInstanceOf(TypeError)
583
- })
584
-
585
- it('should resolve direct body read when stream already ended before first read', async () => {
586
- const socket = new Socket()
587
- const incomingMessage = new IncomingMessage(socket)
588
- incomingMessage.method = 'POST'
589
- incomingMessage.headers = {
590
- host: 'localhost',
591
- }
592
- incomingMessage.rawHeaders = ['host', 'localhost']
593
- incomingMessage.url = '/foo.txt'
594
-
595
- const req = newRequest(incomingMessage)
596
- const ended = new Promise<void>((resolve) => {
597
- incomingMessage.once('end', () => {
598
- resolve()
599
- })
600
- })
601
- incomingMessage.push(null)
602
- incomingMessage.resume()
603
- await ended
604
-
605
- await expect(req.text()).resolves.toBe('')
606
- expect(req.bodyUsed).toBe(true)
607
- })
608
-
609
- it('should reject on second direct json() read', async () => {
610
- const req = newRequest({
611
- method: 'POST',
612
- headers: {
613
- host: 'localhost',
614
- 'content-type': 'application/json',
615
- },
616
- rawHeaders: ['host', 'localhost', 'content-type', 'application/json'],
617
- rawBody: Buffer.from('{"foo":"bar"}'),
618
- url: '/foo.txt',
619
- } as IncomingMessage & { rawBody: Buffer })
620
-
621
- await expect(req.json()).resolves.toEqual({ foo: 'bar' })
622
- await expect(req.json()).rejects.toBeInstanceOf(TypeError)
623
- })
624
-
625
- it('should set bodyUsed and reject clone() after direct json() read', async () => {
626
- const req = newRequest({
627
- method: 'POST',
628
- headers: {
629
- host: 'localhost',
630
- 'content-type': 'application/json',
631
- },
632
- rawHeaders: ['host', 'localhost', 'content-type', 'application/json'],
633
- rawBody: Buffer.from('{"foo":"bar"}'),
634
- url: '/foo.txt',
635
- } as IncomingMessage & { rawBody: Buffer })
636
-
637
- await expect(req.json()).resolves.toEqual({ foo: 'bar' })
638
- expect(req.bodyUsed).toBe(true)
639
- expect(() => req.clone()).toThrow(TypeError)
640
- await expect(req.text()).rejects.toBeInstanceOf(TypeError)
641
- })
642
-
643
- it('should support UTF-8 BOM in direct json() read', async () => {
644
- const req = newRequest({
645
- method: 'POST',
646
- headers: {
647
- host: 'localhost',
648
- 'content-type': 'application/json',
649
- },
650
- rawHeaders: ['host', 'localhost', 'content-type', 'application/json'],
651
- rawBody: Buffer.concat([Buffer.from([0xef, 0xbb, 0xbf]), Buffer.from('{"foo":"bar"}')]),
652
- url: '/foo.txt',
653
- } as IncomingMessage & { rawBody: Buffer })
654
-
655
- await expect(req.json()).resolves.toEqual({ foo: 'bar' })
656
- })
657
-
658
- it('should set bodyUsed and reject clone() after direct text() read', async () => {
659
- const req = newRequest({
660
- method: 'POST',
661
- headers: {
662
- host: 'localhost',
663
- 'content-type': 'text/plain',
664
- },
665
- rawHeaders: ['host', 'localhost', 'content-type', 'text/plain'],
666
- rawBody: Buffer.from('foo'),
667
- url: '/foo.txt',
668
- } as IncomingMessage & { rawBody: Buffer })
669
-
670
- await expect(req.text()).resolves.toBe('foo')
671
- expect(req.bodyUsed).toBe(true)
672
- expect(() => req.clone()).toThrow(TypeError)
673
- await expect(req.arrayBuffer()).rejects.toBeInstanceOf(TypeError)
674
- })
675
-
676
- it('should reject second direct text() read even after accessing signal', async () => {
677
- const req = newRequest({
678
- method: 'POST',
679
- headers: {
680
- host: 'localhost',
681
- 'content-type': 'text/plain',
682
- },
683
- rawHeaders: ['host', 'localhost', 'content-type', 'text/plain'],
684
- rawBody: Buffer.from('foo'),
685
- url: '/foo.txt',
686
- } as IncomingMessage & { rawBody: Buffer })
687
-
688
- await expect(req.text()).resolves.toBe('foo')
689
- expect(req.signal.aborted).toBe(false)
690
- await expect(req.text()).rejects.toBeInstanceOf(TypeError)
691
- })
692
-
693
- it('should reject second direct json() read even after accessing signal', async () => {
694
- const req = newRequest({
695
- method: 'POST',
696
- headers: {
697
- host: 'localhost',
698
- 'content-type': 'application/json',
699
- },
700
- rawHeaders: ['host', 'localhost', 'content-type', 'application/json'],
701
- rawBody: Buffer.from('{"foo":"bar"}'),
702
- url: '/foo.txt',
703
- } as IncomingMessage & { rawBody: Buffer })
704
-
705
- await expect(req.json()).resolves.toEqual({ foo: 'bar' })
706
- expect(req.signal.aborted).toBe(false)
707
- await expect(req.json()).rejects.toBeInstanceOf(TypeError)
708
- })
709
-
710
- it('should set bodyUsed and reject clone() after direct arrayBuffer() read', async () => {
711
- const req = newRequest({
712
- method: 'POST',
713
- headers: {
714
- host: 'localhost',
715
- 'content-type': 'application/octet-stream',
716
- },
717
- rawHeaders: ['host', 'localhost', 'content-type', 'application/octet-stream'],
718
- rawBody: Buffer.from([1, 2, 3]),
719
- url: '/foo.txt',
720
- } as IncomingMessage & { rawBody: Buffer })
721
-
722
- const ab = await req.arrayBuffer()
723
- expect(new Uint8Array(ab)).toEqual(new Uint8Array([1, 2, 3]))
724
- expect(req.bodyUsed).toBe(true)
725
- expect(() => req.clone()).toThrow(TypeError)
726
- await expect(req.text()).rejects.toBeInstanceOf(TypeError)
727
- })
728
-
729
- it('should preserve content type in blob() result', async () => {
730
- const req = newRequest({
731
- method: 'POST',
732
- headers: {
733
- host: 'localhost',
734
- 'content-type': 'text/plain',
735
- },
736
- rawHeaders: ['host', 'localhost', 'content-type', 'text/plain'],
737
- rawBody: Buffer.from('foo'),
738
- url: '/foo.txt',
739
- } as IncomingMessage & { rawBody: Buffer })
740
-
741
- await expect(req.blob().then((blob: Blob) => blob.type)).resolves.toBe('text/plain')
742
- })
743
-
744
- it('should normalize content type in blob() result like native Request', async () => {
745
- const req = newRequest({
746
- method: 'POST',
747
- headers: {
748
- host: 'localhost',
749
- 'content-type': 'text/plain; charset=UTF-8',
750
- },
751
- rawHeaders: ['host', 'localhost', 'content-type', 'text/plain; charset=UTF-8'],
752
- rawBody: Buffer.from('foo'),
753
- url: '/foo.txt',
754
- } as IncomingMessage & { rawBody: Buffer })
755
-
756
- const expectedType = await new GlobalRequest('http://localhost/foo.txt', {
757
- method: 'POST',
758
- headers: {
759
- 'content-type': 'text/plain; charset=UTF-8',
760
- },
761
- body: 'foo',
762
- })
763
- .blob()
764
- .then((blob: Blob) => blob.type)
765
-
766
- await expect(req.blob().then((blob: Blob) => blob.type)).resolves.toBe(expectedType)
767
- })
768
-
769
- it('should set bodyUsed and reject clone() after direct blob() read', async () => {
770
- const req = newRequest({
771
- method: 'POST',
772
- headers: {
773
- host: 'localhost',
774
- 'content-type': 'text/plain',
775
- },
776
- rawHeaders: ['host', 'localhost', 'content-type', 'text/plain'],
777
- rawBody: Buffer.from('foo'),
778
- url: '/foo.txt',
779
- } as IncomingMessage & { rawBody: Buffer })
780
-
781
- const blob = await req.blob()
782
- expect(blob.type).toBe('text/plain')
783
- expect(await blob.text()).toBe('foo')
784
- expect(req.bodyUsed).toBe(true)
785
- expect(() => req.clone()).toThrow(TypeError)
786
- await expect(req.json()).rejects.toBeInstanceOf(TypeError)
787
- })
788
-
789
- it('should allow constructing Request from consumed lightweight request when body is replaced', async () => {
790
- const req = newRequest({
791
- method: 'POST',
792
- headers: {
793
- host: 'localhost',
794
- 'content-type': 'text/plain',
795
- },
796
- rawHeaders: ['host', 'localhost', 'content-type', 'text/plain'],
797
- rawBody: Buffer.from('foo'),
798
- url: '/foo.txt',
799
- } as IncomingMessage & { rawBody: Buffer })
800
-
801
- await req.text()
802
-
803
- const req2 = new LightweightRequest(req, {
804
- method: 'POST',
805
- body: 'bar',
806
- })
807
- await expect(req2.text()).resolves.toBe('bar')
808
- })
809
-
810
- it('should throw when constructing Request from consumed lightweight request without body replacement', async () => {
811
- const req = newRequest({
812
- method: 'POST',
813
- headers: {
814
- host: 'localhost',
815
- 'content-type': 'text/plain',
816
- },
817
- rawHeaders: ['host', 'localhost', 'content-type', 'text/plain'],
818
- rawBody: Buffer.from('foo'),
819
- url: '/foo.txt',
820
- } as IncomingMessage & { rawBody: Buffer })
821
-
822
- await req.text()
823
-
824
- expect(() => {
825
- new LightweightRequest(req)
826
- }).toThrow(TypeError)
827
- })
828
-
829
- it('should preserve TRACE workaround after direct read when accessing signal', async () => {
830
- const socket = new Socket()
831
- const incomingMessage = new IncomingMessage(socket)
832
- incomingMessage.method = 'TRACE'
833
- incomingMessage.headers = {
834
- host: 'localhost',
835
- 'content-type': 'text/plain',
836
- }
837
- incomingMessage.rawHeaders = ['host', 'localhost', 'content-type', 'text/plain']
838
- incomingMessage.url = '/foo.txt'
839
- ;(incomingMessage as IncomingMessage & { rawBody: Buffer }).rawBody = Buffer.from('foo')
840
- const req = newRequest(incomingMessage)
841
-
842
- await expect(req.text()).resolves.toBe('foo')
843
- expect(() => req.signal).not.toThrow()
844
- })
845
-
846
- it('should keep TRACE direct body read behavior regardless of signal access order', async () => {
847
- const createTraceRequest = () => {
848
- const socket = new Socket()
849
- const incomingMessage = new IncomingMessage(socket)
850
- incomingMessage.method = 'TRACE'
851
- incomingMessage.headers = {
852
- host: 'localhost',
853
- 'content-type': 'text/plain',
854
- }
855
- incomingMessage.rawHeaders = ['host', 'localhost', 'content-type', 'text/plain']
856
- incomingMessage.url = '/foo.txt'
857
- ;(incomingMessage as IncomingMessage & { rawBody: Buffer }).rawBody = Buffer.from('foo')
858
- return newRequest(incomingMessage)
859
- }
860
-
861
- const reqBeforeSignal = createTraceRequest()
862
- await expect(reqBeforeSignal.text()).resolves.toBe('foo')
863
-
864
- const reqAfterSignal = createTraceRequest()
865
- expect(() => reqAfterSignal.signal).not.toThrow()
866
- await expect(reqAfterSignal.text()).resolves.toBe('foo')
867
- })
868
-
869
- it('should reject non-uppercase trace consistently regardless of access order', async () => {
870
- const createTraceLikeRequest = (method: string) => {
871
- const socket = new Socket()
872
- const incomingMessage = new IncomingMessage(socket)
873
- incomingMessage.method = method
874
- incomingMessage.headers = {
875
- host: 'localhost',
876
- 'content-type': 'text/plain',
877
- }
878
- incomingMessage.rawHeaders = ['host', 'localhost', 'content-type', 'text/plain']
879
- incomingMessage.url = '/foo.txt'
880
- ;(incomingMessage as IncomingMessage & { rawBody: Buffer }).rawBody = Buffer.from('foo')
881
- return newRequest(incomingMessage)
882
- }
883
-
884
- for (const method of ['trace', 'TrAcE']) {
885
- const req = createTraceLikeRequest(method)
886
- await expect(req.text()).rejects.toBeInstanceOf(TypeError)
887
-
888
- const reqAfterSignal = createTraceLikeRequest(method)
889
- expect(() => reqAfterSignal.signal).not.toThrow()
890
- await expect(reqAfterSignal.text()).rejects.toBeInstanceOf(TypeError)
891
- }
892
- })
893
-
894
- it('should preserve non-standard method casing like native Request', async () => {
895
- for (const method of ['patch', 'CuStOm']) {
896
- const req = newRequest({
897
- method,
898
- headers: {
899
- host: 'localhost',
900
- },
901
- rawHeaders: ['host', 'localhost'],
902
- url: '/foo.txt',
903
- } as IncomingMessage)
904
-
905
- const expected = new GlobalRequest('http://localhost/foo.txt', { method }).method
906
- expect(req.method).toBe(expected)
907
- }
908
- })
909
-
910
- it('should normalize lowercase methods and keep GET behavior consistent', async () => {
911
- const req = newRequest({
912
- method: 'get',
913
- headers: {
914
- host: 'localhost',
915
- 'content-type': 'text/plain',
916
- },
917
- rawHeaders: ['host', 'localhost', 'content-type', 'text/plain'],
918
- rawBody: Buffer.from('foo'),
919
- url: '/foo.txt',
920
- } as IncomingMessage & { rawBody: Buffer })
921
-
922
- expect(req.method).toBe('GET')
923
- expect(() => req.signal).not.toThrow()
924
- await expect(req.text()).resolves.toBe('')
925
- expect(req.bodyUsed).toBe(false)
926
- })
927
-
928
- it('should keep default GET behavior when incoming.method is missing', async () => {
929
- const req = newRequest({
930
- headers: {
931
- host: 'localhost',
932
- },
933
- rawHeaders: ['host', 'localhost'],
934
- rawBody: Buffer.from('foo'),
935
- url: '/foo.txt',
936
- } as IncomingMessage & { rawBody: Buffer })
937
-
938
- expect(req.method).toBe('GET')
939
- expect(await req.text()).toBe('')
940
- expect(req.bodyUsed).toBe(false)
941
- expect(() => req.signal).not.toThrow()
942
- })
943
-
944
- it('Should throw error if host header contains path', async () => {
945
- expect(() => {
946
- newRequest({
947
- headers: {
948
- host: 'localhost/..',
949
- },
950
- url: '/foo.txt',
951
- } as IncomingMessage)
952
- }).toThrow(RequestError)
953
- })
954
-
955
- it('Should throw error if host header is empty', async () => {
956
- expect(() => {
957
- newRequest({
958
- headers: {
959
- host: '',
960
- },
961
- url: '/foo.txt',
962
- } as IncomingMessage)
963
- }).toThrow(RequestError)
964
- })
965
-
966
- it('Should throw error if host header contains query parameter', async () => {
967
- expect(() => {
968
- newRequest({
969
- headers: {
970
- host: 'localhost?foo=bar',
971
- },
972
- url: '/foo.txt',
973
- } as IncomingMessage)
974
- }).toThrow(RequestError)
975
- })
976
-
977
- it('Should throw error if host header port is invalid', async () => {
978
- expect(() => {
979
- newRequest({
980
- headers: {
981
- host: 'localhost:65536',
982
- },
983
- url: '/foo.txt',
984
- } as IncomingMessage)
985
- }).toThrow(RequestError)
986
- })
987
-
988
- it('Should be created request body from `req.rawBody` if it exists', async () => {
989
- const rawBody = Buffer.from('foo')
990
- const socket = new Socket()
991
- const incomingMessage = new IncomingMessage(socket)
992
- incomingMessage.method = 'POST'
993
- incomingMessage.headers = {
994
- host: 'localhost',
995
- }
996
- incomingMessage.url = '/foo.txt'
997
- ;(incomingMessage as IncomingMessage & { rawBody: Buffer }).rawBody = rawBody
998
- incomingMessage.push(rawBody)
999
- incomingMessage.push(null)
1000
-
1001
- for await (const chunk of incomingMessage) {
1002
- // consume body
1003
- expect(chunk).toBeDefined()
1004
- }
1005
-
1006
- const req = newRequest(incomingMessage)
1007
- const text = await req.text()
1008
- expect(text).toBe('foo')
1009
- })
1010
-
1011
- describe('absolute-form for request-target', () => {
1012
- it('should be created from valid absolute URL', async () => {
1013
- const req = newRequest({
1014
- url: 'http://localhost/path/to/file.html',
1015
- } as IncomingMessage)
1016
- expect(req).toBeInstanceOf(GlobalRequest)
1017
- expect(req.url).toBe('http://localhost/path/to/file.html')
1018
- })
1019
-
1020
- it('should throw error if host header is invalid', async () => {
1021
- expect(() => {
1022
- newRequest({
1023
- url: 'http://',
1024
- } as IncomingMessage)
1025
- }).toThrow(RequestError)
1026
- })
1027
-
1028
- it('should throw error if absolute-form is specified via HTTP/2', async () => {
1029
- expect(() => {
1030
- newRequest(
1031
- new Http2ServerRequest(
1032
- new Duplex() as ServerHttp2Stream,
1033
- {
1034
- ':scheme': 'http',
1035
- ':authority': 'localhost',
1036
- ':path': 'http://localhost/foo.txt',
1037
- },
1038
- {},
1039
- []
1040
- )
1041
- )
1042
- }).toThrow(RequestError)
1043
- })
1044
- })
1045
-
1046
- describe('HTTP/2', () => {
1047
- it('should be created from "http" scheme', async () => {
1048
- const req = newRequest(
1049
- new Http2ServerRequest(
1050
- new Duplex() as ServerHttp2Stream,
1051
- {
1052
- ':scheme': 'http',
1053
- ':authority': 'localhost',
1054
- ':path': '/foo.txt',
1055
- },
1056
- {},
1057
- []
1058
- )
1059
- )
1060
- expect(req).toBeInstanceOf(GlobalRequest)
1061
- expect(req.url).toBe('http://localhost/foo.txt')
1062
- })
1063
-
1064
- it('should be created from "https" scheme', async () => {
1065
- const req = newRequest(
1066
- new Http2ServerRequest(
1067
- new Duplex() as ServerHttp2Stream,
1068
- {
1069
- ':scheme': 'https',
1070
- ':authority': 'localhost',
1071
- ':path': '/foo.txt',
1072
- },
1073
- {},
1074
- []
1075
- )
1076
- )
1077
- expect(req).toBeInstanceOf(GlobalRequest)
1078
- expect(req.url).toBe('https://localhost/foo.txt')
1079
- })
1080
-
1081
- it('should throw error if scheme is missing', async () => {
1082
- expect(() => {
1083
- newRequest(
1084
- new Http2ServerRequest(
1085
- new Duplex() as ServerHttp2Stream,
1086
- {
1087
- ':authority': 'localhost',
1088
- ':path': '/foo.txt',
1089
- },
1090
- {},
1091
- []
1092
- )
1093
- )
1094
- }).toThrow(RequestError)
1095
- })
1096
-
1097
- it('should throw error if unsupported scheme is specified', async () => {
1098
- expect(() => {
1099
- newRequest(
1100
- new Http2ServerRequest(
1101
- new Duplex() as ServerHttp2Stream,
1102
- {
1103
- ':scheme': 'ftp',
1104
- ':authority': 'localhost',
1105
- ':path': '/foo.txt',
1106
- },
1107
- {},
1108
- []
1109
- )
1110
- )
1111
- }).toThrow(RequestError)
1112
- })
1113
- })
1114
- })
1115
-
1116
- describe('GlobalRequest', () => {
1117
- it('should be overrode by Request', () => {
1118
- expect(Request).not.toBe(GlobalRequest)
1119
- })
1120
-
1121
- it('should be instance of GlobalRequest', () => {
1122
- const req = new Request('http://localhost/')
1123
- expect(req).toBeInstanceOf(GlobalRequest)
1124
- })
1125
-
1126
- it('should be success to create instance from old light weight instance', async () => {
1127
- const req = newRequest({
1128
- method: 'GET',
1129
- url: '/',
1130
- headers: {
1131
- host: 'localhost',
1132
- },
1133
- rawHeaders: ['host', 'localhost'],
1134
- } as IncomingMessage)
1135
- const req2 = new Request(req, {
1136
- method: 'POST',
1137
- body: 'foo',
1138
- })
1139
- expect(req2).toBeInstanceOf(GlobalRequest)
1140
- expect(await req2.text()).toBe('foo')
1141
- })
1142
-
1143
- it('should set `duplex: "half"` automatically if body is a ReadableStream', async () => {
1144
- const stream = new ReadableStream({
1145
- start(controller) {
1146
- controller.enqueue(new TextEncoder().encode('bar'))
1147
- controller.close()
1148
- },
1149
- })
1150
- const req2 = new Request('http://localhost', {
1151
- method: 'POST',
1152
- body: stream,
1153
- })
1154
- expect(req2).toBeInstanceOf(GlobalRequest)
1155
- await expect(req2.text()).resolves.toBe('bar')
1156
- })
1157
-
1158
- it('should skip to set `duplex: "half"` if init option is a Request object', async () => {
1159
- const stream = new ReadableStream({
1160
- start(controller) {
1161
- controller.enqueue(new TextEncoder().encode('bar'))
1162
- controller.close()
1163
- },
1164
- })
1165
- const req = new Request('http://localhost', {
1166
- method: 'POST',
1167
- body: stream,
1168
- })
1169
- const req2 = new Request('http://localhost/subapp', req)
1170
- expect(req2).toBeInstanceOf(GlobalRequest)
1171
- await expect(req2.text()).resolves.toBe('bar')
1172
- })
1173
- })
1174
- })
1175
-
1176
- describe('util.inspect', () => {
1177
- it('should show a lightweight request summary without creating a native Request', () => {
1178
- const { inspect } = require('node:util')
1179
- const req = newRequest({
1180
- method: 'GET',
1181
- url: '/',
1182
- headers: {
1183
- host: 'localhost',
1184
- },
1185
- rawHeaders: ['host', 'localhost'],
1186
- } as IncomingMessage)
1187
-
1188
- expect(() => inspect(req)).not.toThrow()
1189
- const result = inspect(req)
1190
- expect(result).toContain('Request (lightweight)')
1191
- expect(result).toContain('GET')
1192
- expect(result).toContain('http://localhost/')
1193
- expect(result).toContain('nativeRequest: undefined')
1194
- expect(req[abortControllerKey]).toBeUndefined()
1195
- expect(req).toBeInstanceOf(global.Request)
1196
- })
1197
-
1198
- it('should include the native Request after cache creation', () => {
1199
- const { inspect } = require('node:util')
1200
- const req = newRequest({
1201
- method: 'GET',
1202
- url: '/',
1203
- headers: {
1204
- host: 'localhost',
1205
- },
1206
- rawHeaders: ['host', 'localhost'],
1207
- } as IncomingMessage)
1208
-
1209
- // Access keepalive to trigger native Request creation via getRequestCache()
1210
- req.keepalive
1211
-
1212
- const result = inspect(req)
1213
- expect(result).toContain('Request (lightweight)')
1214
- expect(result).toContain('nativeRequest: Request {')
1215
- expect(req[abortControllerKey]).toBeDefined()
1216
- })
1217
- })
1218
-
1219
- describe('RequestError', () => {
1220
- it('should have a static name property (class name)', () => {
1221
- expect(RequestError.name).toBe('RequestError')
1222
- expect(Object.hasOwn(RequestError, 'name')).toBe(true)
1223
- })
1224
-
1225
- it('should have an instance name property', () => {
1226
- const error = new RequestError('message')
1227
- expect(error.name).toBe('RequestError')
1228
- expect(Object.hasOwn(error, 'name')).toBe(true)
1229
- })
1230
- })