acrylblends 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (335) hide show
  1. package/dist/index.d.ts +22 -16
  2. package/dist/index.js +65 -20
  3. package/dist/index.js.map +1 -1
  4. package/package.json +1 -1
  5. package/plugins/acryl-inspect/LICENSE +21 -0
  6. package/plugins/acryl-inspect/plugin.json +12 -0
  7. package/plugins/acryl-inspect/src/index.ts +3 -0
  8. package/plugins/acryl-inspect/src/report.ts +126 -0
  9. package/plugins/acryl-inspect/src/service.ts +68 -0
  10. package/plugins/acryl-inspect/tests/report.spec.ts +63 -0
  11. package/plugins/acryl-inspect/tests/service.spec.ts +121 -0
  12. package/plugins/acryl-inspect/upstream.json +18 -0
  13. package/plugins/acryl-node-server/LICENSE +47 -0
  14. package/plugins/acryl-node-server/plugin.json +13 -0
  15. package/plugins/acryl-node-server/src/adapter/conninfo.ts +23 -0
  16. package/plugins/acryl-node-server/src/adapter/early-hints.ts +55 -0
  17. package/plugins/acryl-node-server/src/adapter/error.ts +11 -0
  18. package/plugins/acryl-node-server/src/adapter/headers.ts +249 -0
  19. package/plugins/acryl-node-server/src/adapter/index.ts +6 -0
  20. package/plugins/acryl-node-server/src/adapter/listener.ts +540 -0
  21. package/plugins/acryl-node-server/src/adapter/request.ts +797 -0
  22. package/plugins/acryl-node-server/src/adapter/response.ts +186 -0
  23. package/plugins/acryl-node-server/src/adapter/serve-static.ts +238 -0
  24. package/plugins/acryl-node-server/src/adapter/server.ts +36 -0
  25. package/plugins/acryl-node-server/src/adapter/types.ts +82 -0
  26. package/plugins/acryl-node-server/src/adapter/url.ts +42 -0
  27. package/plugins/acryl-node-server/src/adapter/utils/response/constants.ts +1 -0
  28. package/plugins/acryl-node-server/src/adapter/utils/response.ts +4 -0
  29. package/plugins/acryl-node-server/src/adapter/utils/stream.ts +88 -0
  30. package/plugins/acryl-node-server/src/adapter/utils.ts +96 -0
  31. package/plugins/acryl-node-server/src/adapter/websocket-types.ts +45 -0
  32. package/plugins/acryl-node-server/src/adapter/websocket.ts +375 -0
  33. package/plugins/acryl-node-server/src/index.ts +107 -0
  34. package/plugins/acryl-node-server/tests/adapter/app.ts +76 -0
  35. package/plugins/acryl-node-server/tests/adapter/assets/.static/plain.txt +1 -0
  36. package/plugins/acryl-node-server/tests/adapter/assets/favicon.ico +0 -0
  37. package/plugins/acryl-node-server/tests/adapter/assets/secret.txt +1 -0
  38. package/plugins/acryl-node-server/tests/adapter/assets/static/acryl.html +1 -0
  39. package/plugins/acryl-node-server/tests/adapter/assets/static/admin/secret.txt +1 -0
  40. package/plugins/acryl-node-server/tests/adapter/assets/static/data.json +5 -0
  41. package/plugins/acryl-node-server/tests/adapter/assets/static/extensionless +1 -0
  42. package/plugins/acryl-node-server/tests/adapter/assets/static/foo..bar.txt +0 -0
  43. package/plugins/acryl-node-server/tests/adapter/assets/static/index.html +1 -0
  44. package/plugins/acryl-node-server/tests/adapter/assets/static/plain.txt +1 -0
  45. package/plugins/acryl-node-server/tests/adapter/assets/static-with-context-path/plain.txt +1 -0
  46. package/plugins/acryl-node-server/tests/adapter/assets/static-with-precompressed/hello.bin +1 -0
  47. package/plugins/acryl-node-server/tests/adapter/assets/static-with-precompressed/hello.bin.br +1 -0
  48. package/plugins/acryl-node-server/tests/adapter/assets/static-with-precompressed/hello.txt +1 -0
  49. package/plugins/acryl-node-server/tests/adapter/assets/static-with-precompressed/hello.txt.br +1 -0
  50. package/plugins/acryl-node-server/tests/adapter/assets/static-with-precompressed/hello.txt.zst +1 -0
  51. package/plugins/acryl-node-server/tests/adapter/conninfo.test.ts +34 -0
  52. package/plugins/acryl-node-server/tests/adapter/content-length.test.ts +248 -0
  53. package/plugins/acryl-node-server/tests/adapter/early-hints.test.ts +457 -0
  54. package/plugins/acryl-node-server/tests/adapter/fixtures/keys/agent1-cert.pem +23 -0
  55. package/plugins/acryl-node-server/tests/adapter/fixtures/keys/agent1-key.pem +27 -0
  56. package/plugins/acryl-node-server/tests/adapter/headers.test.ts +252 -0
  57. package/plugins/acryl-node-server/tests/adapter/helpers/request.ts +190 -0
  58. package/plugins/acryl-node-server/tests/adapter/listener.test.ts +727 -0
  59. package/plugins/acryl-node-server/tests/adapter/request-client-disconnect.test.ts +189 -0
  60. package/plugins/acryl-node-server/tests/adapter/request.test.ts +1230 -0
  61. package/plugins/acryl-node-server/tests/adapter/response.test.ts +285 -0
  62. package/plugins/acryl-node-server/tests/adapter/serve-static.test.ts +849 -0
  63. package/plugins/acryl-node-server/tests/adapter/server-socket.test.ts +790 -0
  64. package/plugins/acryl-node-server/tests/adapter/server.test.ts +1302 -0
  65. package/plugins/acryl-node-server/tests/adapter/setup.ts +16 -0
  66. package/plugins/acryl-node-server/tests/adapter/url.test.ts +76 -0
  67. package/plugins/acryl-node-server/tests/adapter/utils/response.test.ts +23 -0
  68. package/plugins/acryl-node-server/tests/adapter/utils/stream.test.ts +75 -0
  69. package/plugins/acryl-node-server/tests/adapter/utils.test.ts +235 -0
  70. package/plugins/acryl-node-server/tests/adapter/websocket.test.ts +260 -0
  71. package/plugins/acryl-node-server/tests/server.spec.ts +141 -0
  72. package/plugins/acryl-node-server/upstream.json +18 -0
  73. package/plugins/acryl-router/LICENSE +47 -0
  74. package/plugins/acryl-router/plugin.json +13 -0
  75. package/plugins/acryl-router/src/client.ts +2 -0
  76. package/plugins/acryl-router/src/connection.ts +42 -0
  77. package/plugins/acryl-router/src/core/app-base.ts +544 -0
  78. package/plugins/acryl-router/src/core/app.test.ts +3833 -0
  79. package/plugins/acryl-router/src/core/app.ts +34 -0
  80. package/plugins/acryl-router/src/core/client/client.test.ts +2313 -0
  81. package/plugins/acryl-router/src/core/client/client.ts +249 -0
  82. package/plugins/acryl-router/src/core/client/fetch-result-please.ts +95 -0
  83. package/plugins/acryl-router/src/core/client/index.ts +17 -0
  84. package/plugins/acryl-router/src/core/client/types.test.ts +149 -0
  85. package/plugins/acryl-router/src/core/client/types.ts +393 -0
  86. package/plugins/acryl-router/src/core/client/utils.test.ts +405 -0
  87. package/plugins/acryl-router/src/core/client/utils.ts +117 -0
  88. package/plugins/acryl-router/src/core/compose.test.ts +751 -0
  89. package/plugins/acryl-router/src/core/compose.ts +73 -0
  90. package/plugins/acryl-router/src/core/context.test.ts +629 -0
  91. package/plugins/acryl-router/src/core/context.ts +781 -0
  92. package/plugins/acryl-router/src/core/helper/accepts/accepts.test.ts +344 -0
  93. package/plugins/acryl-router/src/core/helper/accepts/accepts.ts +96 -0
  94. package/plugins/acryl-router/src/core/helper/accepts/index.ts +6 -0
  95. package/plugins/acryl-router/src/core/helper/adapter/index.test.ts +55 -0
  96. package/plugins/acryl-router/src/core/helper/adapter/index.ts +90 -0
  97. package/plugins/acryl-router/src/core/helper/conninfo/index.ts +6 -0
  98. package/plugins/acryl-router/src/core/helper/conninfo/types.ts +45 -0
  99. package/plugins/acryl-router/src/core/helper/cookie/index.test.ts +510 -0
  100. package/plugins/acryl-router/src/core/helper/cookie/index.ts +146 -0
  101. package/plugins/acryl-router/src/core/helper/factory/index.test.ts +434 -0
  102. package/plugins/acryl-router/src/core/helper/factory/index.ts +375 -0
  103. package/plugins/acryl-router/src/core/helper/html/index.test.ts +78 -0
  104. package/plugins/acryl-router/src/core/helper/html/index.ts +57 -0
  105. package/plugins/acryl-router/src/core/helper/route/index.test.ts +259 -0
  106. package/plugins/acryl-router/src/core/helper/route/index.ts +144 -0
  107. package/plugins/acryl-router/src/core/helper/streaming/index.ts +9 -0
  108. package/plugins/acryl-router/src/core/helper/streaming/sse.ts +101 -0
  109. package/plugins/acryl-router/src/core/helper/streaming/stream.test.ts +128 -0
  110. package/plugins/acryl-router/src/core/helper/streaming/stream.ts +45 -0
  111. package/plugins/acryl-router/src/core/helper/streaming/text.test.ts +34 -0
  112. package/plugins/acryl-router/src/core/helper/streaming/text.ts +15 -0
  113. package/plugins/acryl-router/src/core/helper/streaming/utils.ts +11 -0
  114. package/plugins/acryl-router/src/core/helper/testing/index.test.ts +42 -0
  115. package/plugins/acryl-router/src/core/helper/testing/index.ts +28 -0
  116. package/plugins/acryl-router/src/core/helper/websocket/index.test.ts +124 -0
  117. package/plugins/acryl-router/src/core/helper/websocket/index.ts +140 -0
  118. package/plugins/acryl-router/src/core/http-exception.test.ts +45 -0
  119. package/plugins/acryl-router/src/core/http-exception.ts +77 -0
  120. package/plugins/acryl-router/src/core/index.ts +53 -0
  121. package/plugins/acryl-router/src/core/middleware/basic-auth/index.test.ts +434 -0
  122. package/plugins/acryl-router/src/core/middleware/basic-auth/index.ts +152 -0
  123. package/plugins/acryl-router/src/core/middleware/bearer-auth/index.test.ts +1018 -0
  124. package/plugins/acryl-router/src/core/middleware/bearer-auth/index.ts +220 -0
  125. package/plugins/acryl-router/src/core/middleware/body-limit/index.test.ts +273 -0
  126. package/plugins/acryl-router/src/core/middleware/body-limit/index.ts +110 -0
  127. package/plugins/acryl-router/src/core/middleware/combine/index.test.ts +355 -0
  128. package/plugins/acryl-router/src/core/middleware/combine/index.ts +171 -0
  129. package/plugins/acryl-router/src/core/middleware/compress/index.test.ts +535 -0
  130. package/plugins/acryl-router/src/core/middleware/compress/index.ts +142 -0
  131. package/plugins/acryl-router/src/core/middleware/context-storage/index.test.ts +43 -0
  132. package/plugins/acryl-router/src/core/middleware/context-storage/index.ts +58 -0
  133. package/plugins/acryl-router/src/core/middleware/cors/index.test.ts +532 -0
  134. package/plugins/acryl-router/src/core/middleware/cors/index.ts +163 -0
  135. package/plugins/acryl-router/src/core/middleware/csrf/index.test.ts +534 -0
  136. package/plugins/acryl-router/src/core/middleware/csrf/index.ts +150 -0
  137. package/plugins/acryl-router/src/core/middleware/etag/digest.test.ts +21 -0
  138. package/plugins/acryl-router/src/core/middleware/etag/digest.ts +95 -0
  139. package/plugins/acryl-router/src/core/middleware/etag/index.test.ts +503 -0
  140. package/plugins/acryl-router/src/core/middleware/etag/index.ts +135 -0
  141. package/plugins/acryl-router/src/core/middleware/ip-restriction/index.test.ts +217 -0
  142. package/plugins/acryl-router/src/core/middleware/ip-restriction/index.ts +278 -0
  143. package/plugins/acryl-router/src/core/middleware/jwk/index.test.ts +1135 -0
  144. package/plugins/acryl-router/src/core/middleware/jwk/index.ts +1 -0
  145. package/plugins/acryl-router/src/core/middleware/jwk/jwk.ts +195 -0
  146. package/plugins/acryl-router/src/core/middleware/jwk/keys.test.json +48 -0
  147. package/plugins/acryl-router/src/core/middleware/jwt/index.test.ts +802 -0
  148. package/plugins/acryl-router/src/core/middleware/jwt/index.ts +9 -0
  149. package/plugins/acryl-router/src/core/middleware/jwt/jwt.ts +189 -0
  150. package/plugins/acryl-router/src/core/middleware/language/index.test.ts +552 -0
  151. package/plugins/acryl-router/src/core/middleware/language/index.ts +12 -0
  152. package/plugins/acryl-router/src/core/middleware/language/language.ts +314 -0
  153. package/plugins/acryl-router/src/core/middleware/logger/index.test.ts +204 -0
  154. package/plugins/acryl-router/src/core/middleware/logger/index.ts +95 -0
  155. package/plugins/acryl-router/src/core/middleware/method-not-allowed/index.test.ts +292 -0
  156. package/plugins/acryl-router/src/core/middleware/method-not-allowed/index.ts +141 -0
  157. package/plugins/acryl-router/src/core/middleware/method-override/index.test.ts +221 -0
  158. package/plugins/acryl-router/src/core/middleware/method-override/index.ts +147 -0
  159. package/plugins/acryl-router/src/core/middleware/powered-by/index.test.ts +37 -0
  160. package/plugins/acryl-router/src/core/middleware/powered-by/index.ts +35 -0
  161. package/plugins/acryl-router/src/core/middleware/pretty-json/index.test.ts +218 -0
  162. package/plugins/acryl-router/src/core/middleware/pretty-json/index.ts +67 -0
  163. package/plugins/acryl-router/src/core/middleware/request-id/index.test.ts +168 -0
  164. package/plugins/acryl-router/src/core/middleware/request-id/index.ts +7 -0
  165. package/plugins/acryl-router/src/core/middleware/request-id/request-id.ts +59 -0
  166. package/plugins/acryl-router/src/core/middleware/secure-headers/index.test.ts +671 -0
  167. package/plugins/acryl-router/src/core/middleware/secure-headers/index.ts +8 -0
  168. package/plugins/acryl-router/src/core/middleware/secure-headers/permissions-policy.ts +101 -0
  169. package/plugins/acryl-router/src/core/middleware/secure-headers/secure-headers.ts +340 -0
  170. package/plugins/acryl-router/src/core/middleware/serve-static/index.test.ts +450 -0
  171. package/plugins/acryl-router/src/core/middleware/serve-static/index.ts +135 -0
  172. package/plugins/acryl-router/src/core/middleware/serve-static/path.test.ts +50 -0
  173. package/plugins/acryl-router/src/core/middleware/serve-static/path.ts +25 -0
  174. package/plugins/acryl-router/src/core/middleware/timeout/index.test.ts +70 -0
  175. package/plugins/acryl-router/src/core/middleware/timeout/index.ts +58 -0
  176. package/plugins/acryl-router/src/core/middleware/timing/index.test.ts +310 -0
  177. package/plugins/acryl-router/src/core/middleware/timing/index.ts +8 -0
  178. package/plugins/acryl-router/src/core/middleware/timing/timing.ts +256 -0
  179. package/plugins/acryl-router/src/core/middleware/trailing-slash/index.test.ts +381 -0
  180. package/plugins/acryl-router/src/core/middleware/trailing-slash/index.ts +156 -0
  181. package/plugins/acryl-router/src/core/request/constants.ts +1 -0
  182. package/plugins/acryl-router/src/core/request.test.ts +751 -0
  183. package/plugins/acryl-router/src/core/request.ts +502 -0
  184. package/plugins/acryl-router/src/core/router/common.case.test.ts +1132 -0
  185. package/plugins/acryl-router/src/core/router/linear-router/index.ts +6 -0
  186. package/plugins/acryl-router/src/core/router/linear-router/router.test.ts +80 -0
  187. package/plugins/acryl-router/src/core/router/linear-router/router.ts +152 -0
  188. package/plugins/acryl-router/src/core/router/pattern-router/index.ts +6 -0
  189. package/plugins/acryl-router/src/core/router/pattern-router/router.test.ts +46 -0
  190. package/plugins/acryl-router/src/core/router/pattern-router/router.ts +54 -0
  191. package/plugins/acryl-router/src/core/router/reg-exp-router/index.ts +7 -0
  192. package/plugins/acryl-router/src/core/router/reg-exp-router/matcher.ts +33 -0
  193. package/plugins/acryl-router/src/core/router/reg-exp-router/node.ts +170 -0
  194. package/plugins/acryl-router/src/core/router/reg-exp-router/prepared-router.test.ts +154 -0
  195. package/plugins/acryl-router/src/core/router/reg-exp-router/prepared-router.ts +165 -0
  196. package/plugins/acryl-router/src/core/router/reg-exp-router/router.test.ts +265 -0
  197. package/plugins/acryl-router/src/core/router/reg-exp-router/router.ts +174 -0
  198. package/plugins/acryl-router/src/core/router/reg-exp-router/trie.ts +84 -0
  199. package/plugins/acryl-router/src/core/router/smart-router/index.ts +6 -0
  200. package/plugins/acryl-router/src/core/router/smart-router/router.test.ts +13 -0
  201. package/plugins/acryl-router/src/core/router/smart-router/router.ts +70 -0
  202. package/plugins/acryl-router/src/core/router/trie-router/index.ts +6 -0
  203. package/plugins/acryl-router/src/core/router/trie-router/node.test.ts +896 -0
  204. package/plugins/acryl-router/src/core/router/trie-router/node.ts +221 -0
  205. package/plugins/acryl-router/src/core/router/trie-router/router.test.ts +8 -0
  206. package/plugins/acryl-router/src/core/router/trie-router/router.ts +18 -0
  207. package/plugins/acryl-router/src/core/router/utils.ts +1 -0
  208. package/plugins/acryl-router/src/core/router.ts +103 -0
  209. package/plugins/acryl-router/src/core/types.test.ts +4134 -0
  210. package/plugins/acryl-router/src/core/types.ts +2782 -0
  211. package/plugins/acryl-router/src/core/utils/accept.test.ts +299 -0
  212. package/plugins/acryl-router/src/core/utils/accept.ts +268 -0
  213. package/plugins/acryl-router/src/core/utils/basic-auth.test.ts +56 -0
  214. package/plugins/acryl-router/src/core/utils/basic-auth.ts +26 -0
  215. package/plugins/acryl-router/src/core/utils/body.test.ts +496 -0
  216. package/plugins/acryl-router/src/core/utils/body.ts +263 -0
  217. package/plugins/acryl-router/src/core/utils/buffer.test.ts +147 -0
  218. package/plugins/acryl-router/src/core/utils/buffer.ts +117 -0
  219. package/plugins/acryl-router/src/core/utils/color.test.ts +23 -0
  220. package/plugins/acryl-router/src/core/utils/color.ts +56 -0
  221. package/plugins/acryl-router/src/core/utils/compress.ts +10 -0
  222. package/plugins/acryl-router/src/core/utils/concurrent.test.ts +69 -0
  223. package/plugins/acryl-router/src/core/utils/concurrent.ts +55 -0
  224. package/plugins/acryl-router/src/core/utils/constants.ts +4 -0
  225. package/plugins/acryl-router/src/core/utils/cookie.test.ts +460 -0
  226. package/plugins/acryl-router/src/core/utils/cookie.ts +281 -0
  227. package/plugins/acryl-router/src/core/utils/crypto.test.ts +43 -0
  228. package/plugins/acryl-router/src/core/utils/crypto.ts +58 -0
  229. package/plugins/acryl-router/src/core/utils/encode.test.ts +61 -0
  230. package/plugins/acryl-router/src/core/utils/encode.ts +34 -0
  231. package/plugins/acryl-router/src/core/utils/filepath.test.ts +78 -0
  232. package/plugins/acryl-router/src/core/utils/filepath.ts +60 -0
  233. package/plugins/acryl-router/src/core/utils/handler.ts +15 -0
  234. package/plugins/acryl-router/src/core/utils/headers.ts +355 -0
  235. package/plugins/acryl-router/src/core/utils/html.test.ts +16 -0
  236. package/plugins/acryl-router/src/core/utils/html.ts +182 -0
  237. package/plugins/acryl-router/src/core/utils/http-status.ts +72 -0
  238. package/plugins/acryl-router/src/core/utils/ipaddr.test.ts +149 -0
  239. package/plugins/acryl-router/src/core/utils/ipaddr.ts +374 -0
  240. package/plugins/acryl-router/src/core/utils/jwt/index.ts +7 -0
  241. package/plugins/acryl-router/src/core/utils/jwt/jwa.test.ts +86 -0
  242. package/plugins/acryl-router/src/core/utils/jwt/jwa.ts +38 -0
  243. package/plugins/acryl-router/src/core/utils/jwt/jws.ts +235 -0
  244. package/plugins/acryl-router/src/core/utils/jwt/jwt.test.ts +1788 -0
  245. package/plugins/acryl-router/src/core/utils/jwt/jwt.ts +292 -0
  246. package/plugins/acryl-router/src/core/utils/jwt/types.ts +157 -0
  247. package/plugins/acryl-router/src/core/utils/jwt/utf8.ts +7 -0
  248. package/plugins/acryl-router/src/core/utils/mime.test.ts +64 -0
  249. package/plugins/acryl-router/src/core/utils/mime.ts +94 -0
  250. package/plugins/acryl-router/src/core/utils/stream.test.ts +212 -0
  251. package/plugins/acryl-router/src/core/utils/stream.ts +107 -0
  252. package/plugins/acryl-router/src/core/utils/types.test.ts +295 -0
  253. package/plugins/acryl-router/src/core/utils/types.ts +123 -0
  254. package/plugins/acryl-router/src/core/utils/url.test.ts +384 -0
  255. package/plugins/acryl-router/src/core/utils/url.ts +337 -0
  256. package/plugins/acryl-router/src/core/validator/index.ts +8 -0
  257. package/plugins/acryl-router/src/core/validator/utils.test.ts +82 -0
  258. package/plugins/acryl-router/src/core/validator/utils.ts +69 -0
  259. package/plugins/acryl-router/src/core/validator/validator.test.ts +1488 -0
  260. package/plugins/acryl-router/src/core/validator/validator.ts +185 -0
  261. package/plugins/acryl-router/src/helpers/index.ts +13 -0
  262. package/plugins/acryl-router/src/helpers/schema-validator.ts +35 -0
  263. package/plugins/acryl-router/src/index.ts +6 -0
  264. package/plugins/acryl-router/src/middleware/index.ts +25 -0
  265. package/plugins/acryl-router/src/middleware/loopback-only.ts +88 -0
  266. package/plugins/acryl-router/src/service.ts +356 -0
  267. package/plugins/acryl-router/tests/docs-examples.spec.ts +152 -0
  268. package/plugins/acryl-router/tests/live-install.spec.ts +64 -0
  269. package/plugins/acryl-router/tests/loader.spec.ts +72 -0
  270. package/plugins/acryl-router/tests/service.spec.ts +409 -0
  271. package/plugins/acryl-router/tests/shadow-state.spec.ts +79 -0
  272. package/plugins/acryl-router/upstream.json +20 -0
  273. package/plugins/acryl-router-http/LICENSE +21 -0
  274. package/plugins/acryl-router-http/plugin.json +12 -0
  275. package/plugins/acryl-router-http/src/bridge.ts +54 -0
  276. package/plugins/acryl-router-http/src/index.ts +63 -0
  277. package/plugins/acryl-router-http/src/serve.ts +24 -0
  278. package/plugins/acryl-router-http/tests/adapter.spec.ts +174 -0
  279. package/plugins/acryl-router-http/tests/stream.spec.ts +133 -0
  280. package/plugins/acryl-router-http/tests/surfaces.spec.ts +101 -0
  281. package/plugins/acryl-router-http/upstream.json +22 -0
  282. package/plugins/acryl-state/LICENSE +21 -0
  283. package/plugins/acryl-state/NOTICE +30 -0
  284. package/plugins/acryl-state/plugin.json +12 -0
  285. package/plugins/acryl-state/src/cell.ts +67 -0
  286. package/plugins/acryl-state/src/index.ts +3 -0
  287. package/plugins/acryl-state/src/react.ts +11 -0
  288. package/plugins/acryl-state/src/service.ts +119 -0
  289. package/plugins/acryl-state/tests/cell.spec.ts +71 -0
  290. package/plugins/acryl-state/tests/react.spec.ts +39 -0
  291. package/plugins/acryl-state/tests/service.spec.ts +132 -0
  292. package/plugins/acryl-state/upstream.json +22 -0
  293. package/plugins/builder/script.ts +6 -10
  294. package/plugins/extensions/index.ts +20 -3
  295. package/plugins/extensions/resolve.ts +28 -3
  296. package/plugins/openai/prompt.ts +12 -6
  297. package/plugins/tasks/index.ts +14 -0
  298. package/plugins/tsconfig.json +14 -5
  299. package/templates/agent/blend.yaml +5 -7
  300. package/templates/agent/src/app.ts +8 -8
  301. package/templates/agent/src/routes.ts +39 -38
  302. package/templates/agent/template.json +3 -3
  303. package/templates/agent/test/builder.test.ts +22 -14
  304. package/templates/agent/test/model.test.ts +3 -3
  305. package/templates/agent/tsconfig.json +8 -5
  306. package/templates/{app → blank}/blend.yaml +3 -3
  307. package/templates/{app → blank}/src/app.ts +5 -5
  308. package/templates/blank/src/routes.ts +16 -0
  309. package/templates/blank/template.json +1 -0
  310. package/templates/{app → blank}/test/app.test.ts +1 -1
  311. package/templates/{app → blank}/tsconfig.json +2 -4
  312. package/templates/todo/README.md +27 -0
  313. package/templates/todo/blend.yaml +25 -0
  314. package/templates/todo/gitignore +2 -0
  315. package/templates/todo/package.json +23 -0
  316. package/templates/todo/src/app.ts +71 -0
  317. package/templates/todo/src/main.ts +19 -0
  318. package/templates/todo/src/page.ts +28 -0
  319. package/templates/todo/src/todo.ts +123 -0
  320. package/templates/todo/template.json +1 -0
  321. package/templates/todo/test/todo.test.ts +145 -0
  322. package/templates/todo/tsconfig.json +17 -0
  323. package/templates/todo/upstream.json +12 -0
  324. package/plugins/events/index.ts +0 -42
  325. package/plugins/events/plugin.json +0 -9
  326. package/plugins/http/index.ts +0 -64
  327. package/plugins/http/plugin.json +0 -10
  328. package/plugins/server/index.ts +0 -138
  329. package/plugins/server/plugin.json +0 -10
  330. package/templates/app/src/routes.ts +0 -14
  331. package/templates/app/template.json +0 -1
  332. /package/templates/{app → blank}/README.md +0 -0
  333. /package/templates/{app → blank}/gitignore +0 -0
  334. /package/templates/{app → blank}/package.json +0 -0
  335. /package/templates/{app → blank}/src/main.ts +0 -0
@@ -0,0 +1,1230 @@
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
+ })