acrylblends 0.3.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.
- package/dist/index.d.ts +6 -1
- package/dist/index.js +28 -3
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/plugins/acryl-inspect/plugin.json +2 -1
- package/plugins/acryl-inspect/src/report.ts +16 -13
- package/plugins/acryl-inspect/src/service.ts +9 -7
- package/plugins/acryl-inspect/tests/report.spec.ts +7 -8
- package/plugins/acryl-inspect/tests/service.spec.ts +11 -10
- package/plugins/acryl-inspect/upstream.json +2 -2
- package/plugins/acryl-node-server/LICENSE +26 -0
- package/plugins/acryl-node-server/plugin.json +2 -1
- package/plugins/acryl-node-server/src/adapter/conninfo.ts +23 -0
- package/plugins/acryl-node-server/src/adapter/early-hints.ts +55 -0
- package/plugins/acryl-node-server/src/adapter/error.ts +11 -0
- package/plugins/acryl-node-server/src/adapter/headers.ts +249 -0
- package/plugins/acryl-node-server/src/adapter/index.ts +6 -0
- package/plugins/acryl-node-server/src/adapter/listener.ts +540 -0
- package/plugins/acryl-node-server/src/adapter/request.ts +797 -0
- package/plugins/acryl-node-server/src/adapter/response.ts +186 -0
- package/plugins/acryl-node-server/src/adapter/serve-static.ts +238 -0
- package/plugins/acryl-node-server/src/adapter/server.ts +36 -0
- package/plugins/acryl-node-server/src/adapter/types.ts +82 -0
- package/plugins/acryl-node-server/src/adapter/url.ts +42 -0
- package/plugins/acryl-node-server/src/adapter/utils/response/constants.ts +1 -0
- package/plugins/acryl-node-server/src/adapter/utils/response.ts +4 -0
- package/plugins/acryl-node-server/src/adapter/utils/stream.ts +88 -0
- package/plugins/acryl-node-server/src/adapter/utils.ts +96 -0
- package/plugins/acryl-node-server/src/adapter/websocket-types.ts +45 -0
- package/plugins/acryl-node-server/src/adapter/websocket.ts +375 -0
- package/plugins/acryl-node-server/src/index.ts +19 -6
- package/plugins/acryl-node-server/tests/adapter/app.ts +76 -0
- package/plugins/acryl-node-server/tests/adapter/assets/.static/plain.txt +1 -0
- package/plugins/acryl-node-server/tests/adapter/assets/favicon.ico +0 -0
- package/plugins/acryl-node-server/tests/adapter/assets/secret.txt +1 -0
- package/plugins/acryl-node-server/tests/adapter/assets/static/acryl.html +1 -0
- package/plugins/acryl-node-server/tests/adapter/assets/static/admin/secret.txt +1 -0
- package/plugins/acryl-node-server/tests/adapter/assets/static/data.json +5 -0
- package/plugins/acryl-node-server/tests/adapter/assets/static/extensionless +1 -0
- package/plugins/acryl-node-server/tests/adapter/assets/static/foo..bar.txt +0 -0
- package/plugins/acryl-node-server/tests/adapter/assets/static/index.html +1 -0
- package/plugins/acryl-node-server/tests/adapter/assets/static/plain.txt +1 -0
- package/plugins/acryl-node-server/tests/adapter/assets/static-with-context-path/plain.txt +1 -0
- package/plugins/acryl-node-server/tests/adapter/assets/static-with-precompressed/hello.bin +1 -0
- package/plugins/acryl-node-server/tests/adapter/assets/static-with-precompressed/hello.bin.br +1 -0
- package/plugins/acryl-node-server/tests/adapter/assets/static-with-precompressed/hello.txt +1 -0
- package/plugins/acryl-node-server/tests/adapter/assets/static-with-precompressed/hello.txt.br +1 -0
- package/plugins/acryl-node-server/tests/adapter/assets/static-with-precompressed/hello.txt.zst +1 -0
- package/plugins/acryl-node-server/tests/adapter/conninfo.test.ts +34 -0
- package/plugins/acryl-node-server/tests/adapter/content-length.test.ts +248 -0
- package/plugins/acryl-node-server/tests/adapter/early-hints.test.ts +457 -0
- package/plugins/acryl-node-server/tests/adapter/fixtures/keys/agent1-cert.pem +23 -0
- package/plugins/acryl-node-server/tests/adapter/fixtures/keys/agent1-key.pem +27 -0
- package/plugins/acryl-node-server/tests/adapter/headers.test.ts +252 -0
- package/plugins/acryl-node-server/tests/adapter/helpers/request.ts +190 -0
- package/plugins/acryl-node-server/tests/adapter/listener.test.ts +727 -0
- package/plugins/acryl-node-server/tests/adapter/request-client-disconnect.test.ts +189 -0
- package/plugins/acryl-node-server/tests/adapter/request.test.ts +1230 -0
- package/plugins/acryl-node-server/tests/adapter/response.test.ts +285 -0
- package/plugins/acryl-node-server/tests/adapter/serve-static.test.ts +849 -0
- package/plugins/acryl-node-server/tests/adapter/server-socket.test.ts +790 -0
- package/plugins/acryl-node-server/tests/adapter/server.test.ts +1302 -0
- package/plugins/acryl-node-server/tests/adapter/setup.ts +16 -0
- package/plugins/acryl-node-server/tests/adapter/url.test.ts +76 -0
- package/plugins/acryl-node-server/tests/adapter/utils/response.test.ts +23 -0
- package/plugins/acryl-node-server/tests/adapter/utils/stream.test.ts +75 -0
- package/plugins/acryl-node-server/tests/adapter/utils.test.ts +235 -0
- package/plugins/acryl-node-server/tests/adapter/websocket.test.ts +260 -0
- package/plugins/acryl-node-server/tests/server.spec.ts +44 -8
- package/plugins/acryl-node-server/upstream.json +3 -3
- package/plugins/acryl-router/LICENSE +26 -0
- package/plugins/acryl-router/plugin.json +2 -1
- package/plugins/acryl-router/src/client.ts +2 -0
- package/plugins/acryl-router/src/connection.ts +42 -0
- package/plugins/acryl-router/src/core/app-base.ts +544 -0
- package/plugins/acryl-router/src/core/app.test.ts +3833 -0
- package/plugins/acryl-router/src/core/app.ts +34 -0
- package/plugins/acryl-router/src/core/client/client.test.ts +2313 -0
- package/plugins/acryl-router/src/core/client/client.ts +249 -0
- package/plugins/acryl-router/src/core/client/fetch-result-please.ts +95 -0
- package/plugins/acryl-router/src/core/client/index.ts +17 -0
- package/plugins/acryl-router/src/core/client/types.test.ts +149 -0
- package/plugins/acryl-router/src/core/client/types.ts +393 -0
- package/plugins/acryl-router/src/core/client/utils.test.ts +405 -0
- package/plugins/acryl-router/src/core/client/utils.ts +117 -0
- package/plugins/acryl-router/src/core/compose.test.ts +751 -0
- package/plugins/acryl-router/src/core/compose.ts +73 -0
- package/plugins/acryl-router/src/core/context.test.ts +629 -0
- package/plugins/acryl-router/src/core/context.ts +781 -0
- package/plugins/acryl-router/src/core/helper/accepts/accepts.test.ts +344 -0
- package/plugins/acryl-router/src/core/helper/accepts/accepts.ts +96 -0
- package/plugins/acryl-router/src/core/helper/accepts/index.ts +6 -0
- package/plugins/acryl-router/src/core/helper/adapter/index.test.ts +55 -0
- package/plugins/acryl-router/src/core/helper/adapter/index.ts +90 -0
- package/plugins/acryl-router/src/core/helper/conninfo/index.ts +6 -0
- package/plugins/acryl-router/src/core/helper/conninfo/types.ts +45 -0
- package/plugins/acryl-router/src/core/helper/cookie/index.test.ts +510 -0
- package/plugins/acryl-router/src/core/helper/cookie/index.ts +146 -0
- package/plugins/acryl-router/src/core/helper/factory/index.test.ts +434 -0
- package/plugins/acryl-router/src/core/helper/factory/index.ts +375 -0
- package/plugins/acryl-router/src/core/helper/html/index.test.ts +78 -0
- package/plugins/acryl-router/src/core/helper/html/index.ts +57 -0
- package/plugins/acryl-router/src/core/helper/route/index.test.ts +259 -0
- package/plugins/acryl-router/src/core/helper/route/index.ts +144 -0
- package/plugins/acryl-router/src/core/helper/streaming/index.ts +9 -0
- package/plugins/acryl-router/src/core/helper/streaming/sse.ts +101 -0
- package/plugins/acryl-router/src/core/helper/streaming/stream.test.ts +128 -0
- package/plugins/acryl-router/src/core/helper/streaming/stream.ts +45 -0
- package/plugins/acryl-router/src/core/helper/streaming/text.test.ts +34 -0
- package/plugins/acryl-router/src/core/helper/streaming/text.ts +15 -0
- package/plugins/acryl-router/src/core/helper/streaming/utils.ts +11 -0
- package/plugins/acryl-router/src/core/helper/testing/index.test.ts +42 -0
- package/plugins/acryl-router/src/core/helper/testing/index.ts +28 -0
- package/plugins/acryl-router/src/core/helper/websocket/index.test.ts +124 -0
- package/plugins/acryl-router/src/core/helper/websocket/index.ts +140 -0
- package/plugins/acryl-router/src/core/http-exception.test.ts +45 -0
- package/plugins/acryl-router/src/core/http-exception.ts +77 -0
- package/plugins/acryl-router/src/core/index.ts +53 -0
- package/plugins/acryl-router/src/core/middleware/basic-auth/index.test.ts +434 -0
- package/plugins/acryl-router/src/core/middleware/basic-auth/index.ts +152 -0
- package/plugins/acryl-router/src/core/middleware/bearer-auth/index.test.ts +1018 -0
- package/plugins/acryl-router/src/core/middleware/bearer-auth/index.ts +220 -0
- package/plugins/acryl-router/src/core/middleware/body-limit/index.test.ts +273 -0
- package/plugins/acryl-router/src/core/middleware/body-limit/index.ts +110 -0
- package/plugins/acryl-router/src/core/middleware/combine/index.test.ts +355 -0
- package/plugins/acryl-router/src/core/middleware/combine/index.ts +171 -0
- package/plugins/acryl-router/src/core/middleware/compress/index.test.ts +535 -0
- package/plugins/acryl-router/src/core/middleware/compress/index.ts +142 -0
- package/plugins/acryl-router/src/core/middleware/context-storage/index.test.ts +43 -0
- package/plugins/acryl-router/src/core/middleware/context-storage/index.ts +58 -0
- package/plugins/acryl-router/src/core/middleware/cors/index.test.ts +532 -0
- package/plugins/acryl-router/src/core/middleware/cors/index.ts +163 -0
- package/plugins/acryl-router/src/core/middleware/csrf/index.test.ts +534 -0
- package/plugins/acryl-router/src/core/middleware/csrf/index.ts +150 -0
- package/plugins/acryl-router/src/core/middleware/etag/digest.test.ts +21 -0
- package/plugins/acryl-router/src/core/middleware/etag/digest.ts +95 -0
- package/plugins/acryl-router/src/core/middleware/etag/index.test.ts +503 -0
- package/plugins/acryl-router/src/core/middleware/etag/index.ts +135 -0
- package/plugins/acryl-router/src/core/middleware/ip-restriction/index.test.ts +217 -0
- package/plugins/acryl-router/src/core/middleware/ip-restriction/index.ts +278 -0
- package/plugins/acryl-router/src/core/middleware/jwk/index.test.ts +1135 -0
- package/plugins/acryl-router/src/core/middleware/jwk/index.ts +1 -0
- package/plugins/acryl-router/src/core/middleware/jwk/jwk.ts +195 -0
- package/plugins/acryl-router/src/core/middleware/jwk/keys.test.json +48 -0
- package/plugins/acryl-router/src/core/middleware/jwt/index.test.ts +802 -0
- package/plugins/acryl-router/src/core/middleware/jwt/index.ts +9 -0
- package/plugins/acryl-router/src/core/middleware/jwt/jwt.ts +189 -0
- package/plugins/acryl-router/src/core/middleware/language/index.test.ts +552 -0
- package/plugins/acryl-router/src/core/middleware/language/index.ts +12 -0
- package/plugins/acryl-router/src/core/middleware/language/language.ts +314 -0
- package/plugins/acryl-router/src/core/middleware/logger/index.test.ts +204 -0
- package/plugins/acryl-router/src/core/middleware/logger/index.ts +95 -0
- package/plugins/acryl-router/src/core/middleware/method-not-allowed/index.test.ts +292 -0
- package/plugins/acryl-router/src/core/middleware/method-not-allowed/index.ts +141 -0
- package/plugins/acryl-router/src/core/middleware/method-override/index.test.ts +221 -0
- package/plugins/acryl-router/src/core/middleware/method-override/index.ts +147 -0
- package/plugins/acryl-router/src/core/middleware/powered-by/index.test.ts +37 -0
- package/plugins/acryl-router/src/core/middleware/powered-by/index.ts +35 -0
- package/plugins/acryl-router/src/core/middleware/pretty-json/index.test.ts +218 -0
- package/plugins/acryl-router/src/core/middleware/pretty-json/index.ts +67 -0
- package/plugins/acryl-router/src/core/middleware/request-id/index.test.ts +168 -0
- package/plugins/acryl-router/src/core/middleware/request-id/index.ts +7 -0
- package/plugins/acryl-router/src/core/middleware/request-id/request-id.ts +59 -0
- package/plugins/acryl-router/src/core/middleware/secure-headers/index.test.ts +671 -0
- package/plugins/acryl-router/src/core/middleware/secure-headers/index.ts +8 -0
- package/plugins/acryl-router/src/core/middleware/secure-headers/permissions-policy.ts +101 -0
- package/plugins/acryl-router/src/core/middleware/secure-headers/secure-headers.ts +340 -0
- package/plugins/acryl-router/src/core/middleware/serve-static/index.test.ts +450 -0
- package/plugins/acryl-router/src/core/middleware/serve-static/index.ts +135 -0
- package/plugins/acryl-router/src/core/middleware/serve-static/path.test.ts +50 -0
- package/plugins/acryl-router/src/core/middleware/serve-static/path.ts +25 -0
- package/plugins/acryl-router/src/core/middleware/timeout/index.test.ts +70 -0
- package/plugins/acryl-router/src/core/middleware/timeout/index.ts +58 -0
- package/plugins/acryl-router/src/core/middleware/timing/index.test.ts +310 -0
- package/plugins/acryl-router/src/core/middleware/timing/index.ts +8 -0
- package/plugins/acryl-router/src/core/middleware/timing/timing.ts +256 -0
- package/plugins/acryl-router/src/core/middleware/trailing-slash/index.test.ts +381 -0
- package/plugins/acryl-router/src/core/middleware/trailing-slash/index.ts +156 -0
- package/plugins/acryl-router/src/core/request/constants.ts +1 -0
- package/plugins/acryl-router/src/core/request.test.ts +751 -0
- package/plugins/acryl-router/src/core/request.ts +502 -0
- package/plugins/acryl-router/src/core/router/common.case.test.ts +1132 -0
- package/plugins/acryl-router/src/core/router/linear-router/index.ts +6 -0
- package/plugins/acryl-router/src/core/router/linear-router/router.test.ts +80 -0
- package/plugins/acryl-router/src/core/router/linear-router/router.ts +152 -0
- package/plugins/acryl-router/src/core/router/pattern-router/index.ts +6 -0
- package/plugins/acryl-router/src/core/router/pattern-router/router.test.ts +46 -0
- package/plugins/acryl-router/src/core/router/pattern-router/router.ts +54 -0
- package/plugins/acryl-router/src/core/router/reg-exp-router/index.ts +7 -0
- package/plugins/acryl-router/src/core/router/reg-exp-router/matcher.ts +33 -0
- package/plugins/acryl-router/src/core/router/reg-exp-router/node.ts +170 -0
- package/plugins/acryl-router/src/core/router/reg-exp-router/prepared-router.test.ts +154 -0
- package/plugins/acryl-router/src/core/router/reg-exp-router/prepared-router.ts +165 -0
- package/plugins/acryl-router/src/core/router/reg-exp-router/router.test.ts +265 -0
- package/plugins/acryl-router/src/core/router/reg-exp-router/router.ts +174 -0
- package/plugins/acryl-router/src/core/router/reg-exp-router/trie.ts +84 -0
- package/plugins/acryl-router/src/core/router/smart-router/index.ts +6 -0
- package/plugins/acryl-router/src/core/router/smart-router/router.test.ts +13 -0
- package/plugins/acryl-router/src/core/router/smart-router/router.ts +70 -0
- package/plugins/acryl-router/src/core/router/trie-router/index.ts +6 -0
- package/plugins/acryl-router/src/core/router/trie-router/node.test.ts +896 -0
- package/plugins/acryl-router/src/core/router/trie-router/node.ts +221 -0
- package/plugins/acryl-router/src/core/router/trie-router/router.test.ts +8 -0
- package/plugins/acryl-router/src/core/router/trie-router/router.ts +18 -0
- package/plugins/acryl-router/src/core/router/utils.ts +1 -0
- package/plugins/acryl-router/src/core/router.ts +103 -0
- package/plugins/acryl-router/src/core/types.test.ts +4134 -0
- package/plugins/acryl-router/src/core/types.ts +2782 -0
- package/plugins/acryl-router/src/core/utils/accept.test.ts +299 -0
- package/plugins/acryl-router/src/core/utils/accept.ts +268 -0
- package/plugins/acryl-router/src/core/utils/basic-auth.test.ts +56 -0
- package/plugins/acryl-router/src/core/utils/basic-auth.ts +26 -0
- package/plugins/acryl-router/src/core/utils/body.test.ts +496 -0
- package/plugins/acryl-router/src/core/utils/body.ts +263 -0
- package/plugins/acryl-router/src/core/utils/buffer.test.ts +147 -0
- package/plugins/acryl-router/src/core/utils/buffer.ts +117 -0
- package/plugins/acryl-router/src/core/utils/color.test.ts +23 -0
- package/plugins/acryl-router/src/core/utils/color.ts +56 -0
- package/plugins/acryl-router/src/core/utils/compress.ts +10 -0
- package/plugins/acryl-router/src/core/utils/concurrent.test.ts +69 -0
- package/plugins/acryl-router/src/core/utils/concurrent.ts +55 -0
- package/plugins/acryl-router/src/core/utils/constants.ts +4 -0
- package/plugins/acryl-router/src/core/utils/cookie.test.ts +460 -0
- package/plugins/acryl-router/src/core/utils/cookie.ts +281 -0
- package/plugins/acryl-router/src/core/utils/crypto.test.ts +43 -0
- package/plugins/acryl-router/src/core/utils/crypto.ts +58 -0
- package/plugins/acryl-router/src/core/utils/encode.test.ts +61 -0
- package/plugins/acryl-router/src/core/utils/encode.ts +34 -0
- package/plugins/acryl-router/src/core/utils/filepath.test.ts +78 -0
- package/plugins/acryl-router/src/core/utils/filepath.ts +60 -0
- package/plugins/acryl-router/src/core/utils/handler.ts +15 -0
- package/plugins/acryl-router/src/core/utils/headers.ts +355 -0
- package/plugins/acryl-router/src/core/utils/html.test.ts +16 -0
- package/plugins/acryl-router/src/core/utils/html.ts +182 -0
- package/plugins/acryl-router/src/core/utils/http-status.ts +72 -0
- package/plugins/acryl-router/src/core/utils/ipaddr.test.ts +149 -0
- package/plugins/acryl-router/src/core/utils/ipaddr.ts +374 -0
- package/plugins/acryl-router/src/core/utils/jwt/index.ts +7 -0
- package/plugins/acryl-router/src/core/utils/jwt/jwa.test.ts +86 -0
- package/plugins/acryl-router/src/core/utils/jwt/jwa.ts +38 -0
- package/plugins/acryl-router/src/core/utils/jwt/jws.ts +235 -0
- package/plugins/acryl-router/src/core/utils/jwt/jwt.test.ts +1788 -0
- package/plugins/acryl-router/src/core/utils/jwt/jwt.ts +292 -0
- package/plugins/acryl-router/src/core/utils/jwt/types.ts +157 -0
- package/plugins/acryl-router/src/core/utils/jwt/utf8.ts +7 -0
- package/plugins/acryl-router/src/core/utils/mime.test.ts +64 -0
- package/plugins/acryl-router/src/core/utils/mime.ts +94 -0
- package/plugins/acryl-router/src/core/utils/stream.test.ts +212 -0
- package/plugins/acryl-router/src/core/utils/stream.ts +107 -0
- package/plugins/acryl-router/src/core/utils/types.test.ts +295 -0
- package/plugins/acryl-router/src/core/utils/types.ts +123 -0
- package/plugins/acryl-router/src/core/utils/url.test.ts +384 -0
- package/plugins/acryl-router/src/core/utils/url.ts +337 -0
- package/plugins/acryl-router/src/core/validator/index.ts +8 -0
- package/plugins/acryl-router/src/core/validator/utils.test.ts +82 -0
- package/plugins/acryl-router/src/core/validator/utils.ts +69 -0
- package/plugins/acryl-router/src/core/validator/validator.test.ts +1488 -0
- package/plugins/acryl-router/src/core/validator/validator.ts +185 -0
- package/plugins/acryl-router/src/helpers/index.ts +13 -3
- package/plugins/acryl-router/src/helpers/schema-validator.ts +35 -0
- package/plugins/acryl-router/src/index.ts +6 -6
- package/plugins/acryl-router/src/middleware/index.ts +25 -6
- package/plugins/acryl-router/src/middleware/loopback-only.ts +19 -13
- package/plugins/acryl-router/src/service.ts +256 -64
- package/plugins/acryl-router/tests/docs-examples.spec.ts +152 -0
- package/plugins/acryl-router/tests/live-install.spec.ts +2 -2
- package/plugins/acryl-router/tests/loader.spec.ts +3 -2
- package/plugins/acryl-router/tests/service.spec.ts +264 -66
- package/plugins/acryl-router/tests/shadow-state.spec.ts +79 -0
- package/plugins/acryl-router/upstream.json +2 -2
- package/plugins/acryl-router-http/plugin.json +2 -1
- package/plugins/acryl-router-http/src/bridge.ts +3 -1
- package/plugins/acryl-router-http/tests/adapter.spec.ts +16 -12
- package/plugins/acryl-router-http/tests/surfaces.spec.ts +12 -10
- package/plugins/acryl-router-http/upstream.json +3 -3
- package/plugins/acryl-state/NOTICE +30 -0
- package/plugins/acryl-state/plugin.json +1 -1
- package/plugins/acryl-state/src/cell.ts +1 -1
- package/plugins/acryl-state/src/service.ts +33 -15
- package/plugins/acryl-state/upstream.json +6 -4
- package/plugins/builder/script.ts +4 -7
- package/plugins/extensions/index.ts +2 -2
- package/plugins/extensions/resolve.ts +28 -3
- package/plugins/openai/prompt.ts +9 -7
- package/plugins/tsconfig.json +5 -4
- package/templates/agent/src/routes.ts +37 -27
- package/templates/agent/test/model.test.ts +2 -1
- package/templates/agent/tsconfig.json +8 -5
- package/templates/blank/src/routes.ts +6 -4
- package/templates/blank/tsconfig.json +2 -4
- package/templates/todo/src/page.ts +1 -1
- package/templates/todo/src/todo.ts +56 -29
- package/templates/todo/tsconfig.json +2 -4
- package/templates/todo/upstream.json +2 -2
- package/plugins/acryl-router/src/compose.ts +0 -21
- package/plugins/acryl-router/src/context.ts +0 -59
- package/plugins/acryl-router/src/helpers/read-body.ts +0 -59
- package/plugins/acryl-router/src/helpers/sse.ts +0 -83
- package/plugins/acryl-router/src/helpers/validate.ts +0 -44
- package/plugins/acryl-router/src/http-error.ts +0 -21
- package/plugins/acryl-router/src/middleware/body-limit.ts +0 -25
- package/plugins/acryl-router/src/middleware/cors.ts +0 -46
- package/plugins/acryl-router/src/middleware/logger.ts +0 -21
- package/plugins/acryl-router/src/middleware/request-id.ts +0 -24
- package/plugins/acryl-router/src/middleware/secure-headers.ts +0 -23
- package/plugins/acryl-router/src/pattern.ts +0 -89
- package/plugins/acryl-router/src/registrar.ts +0 -90
- package/plugins/acryl-router/src/table.ts +0 -156
- package/plugins/acryl-router/src/types.ts +0 -73
- package/plugins/acryl-router/src/util.ts +0 -23
- package/plugins/acryl-router/tests/api.spec.ts +0 -223
- package/plugins/acryl-router/tests/conformance.ts +0 -170
- package/plugins/acryl-router/tests/helpers.spec.ts +0 -172
- package/plugins/acryl-router/tests/middleware.spec.ts +0 -183
- package/plugins/acryl-router/tests/table.spec.ts +0 -47
|
@@ -0,0 +1,797 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
+
// Define prototype for lightweight pseudo Request object
|
|
3
|
+
|
|
4
|
+
import type { IncomingMessage } from 'node:http'
|
|
5
|
+
import { Http2ServerRequest } from 'node:http2'
|
|
6
|
+
import { Readable } from 'node:stream'
|
|
7
|
+
import type { ReadableStreamDefaultReader } from 'node:stream/web'
|
|
8
|
+
import type { TLSSocket } from 'node:tls'
|
|
9
|
+
import { RequestError } from './error.ts'
|
|
10
|
+
import { newHeadersFromIncoming } from './headers.ts'
|
|
11
|
+
import type { GlobalHeaders } from './headers.ts'
|
|
12
|
+
import { buildUrl } from './url.ts'
|
|
13
|
+
|
|
14
|
+
export { RequestError }
|
|
15
|
+
|
|
16
|
+
export const toRequestError = (e: unknown): RequestError => {
|
|
17
|
+
if (e instanceof RequestError) {
|
|
18
|
+
return e
|
|
19
|
+
}
|
|
20
|
+
return new RequestError((e as Error).message, { cause: e })
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const GlobalRequest = global.Request
|
|
24
|
+
export class Request extends GlobalRequest {
|
|
25
|
+
constructor(input: string | Request, options?: RequestInit) {
|
|
26
|
+
if (typeof input === 'object' && getRequestCache in input) {
|
|
27
|
+
// Match native Request behavior:
|
|
28
|
+
// constructing from a consumed Request is allowed only when init.body is non-null.
|
|
29
|
+
const hasReplacementBody =
|
|
30
|
+
options !== undefined && 'body' in options && (options as RequestInit).body != null
|
|
31
|
+
if ((input as any)[bodyConsumedDirectlyKey] && !hasReplacementBody) {
|
|
32
|
+
throw new TypeError(
|
|
33
|
+
'Cannot construct a Request with a Request object that has already been used.'
|
|
34
|
+
)
|
|
35
|
+
}
|
|
36
|
+
input = (input as any)[getRequestCache]()
|
|
37
|
+
}
|
|
38
|
+
// Check if body is ReadableStream like. This makes it compatbile with ReadableStream polyfills.
|
|
39
|
+
if (typeof (options?.body as ReadableStream)?.getReader !== 'undefined') {
|
|
40
|
+
// Half duplex mode is required when request body is a stream.
|
|
41
|
+
// If already set, do nothing since a Request object was passed to the options or explicitly set by the user.
|
|
42
|
+
;(options as any).duplex ??= 'half'
|
|
43
|
+
}
|
|
44
|
+
super(input, options)
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export type IncomingMessageWithWrapBodyStream = IncomingMessage & { [wrapBodyStream]: boolean }
|
|
49
|
+
export const wrapBodyStream = Symbol('wrapBodyStream')
|
|
50
|
+
|
|
51
|
+
// Encodings whose decode → re-encode round-trip is byte-exact for any input.
|
|
52
|
+
// utf8/utf16le replace invalid sequences with U+FFFD and ascii masks the high
|
|
53
|
+
// bit of every byte, so a body decoded through them cannot be reconstructed
|
|
54
|
+
// reliably — recovery is refused for those instead of returning corrupt bytes.
|
|
55
|
+
const byteExactEncodings = new Set(['latin1', 'binary', 'hex', 'base64', 'base64url'])
|
|
56
|
+
|
|
57
|
+
const isByteExactEncoding = (encoding: BufferEncoding | null): boolean =>
|
|
58
|
+
encoding === null || byteExactEncodings.has(encoding)
|
|
59
|
+
|
|
60
|
+
const bodyBufferedBeforeDisconnectKey = Symbol('bodyBufferedBeforeDisconnect')
|
|
61
|
+
const bodyBufferedLengthBeforeDisconnectKey = Symbol('bodyBufferedLengthBeforeDisconnect')
|
|
62
|
+
|
|
63
|
+
type IncomingWithBodyRecovery = IncomingMessage & {
|
|
64
|
+
[bodyBufferedBeforeDisconnectKey]?: Buffer | Error
|
|
65
|
+
[bodyBufferedLengthBeforeDisconnectKey]?: number
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// When setEncoding() was called on the stream, chunks arrive as strings in
|
|
69
|
+
// that encoding — decode with it so the original bytes are reconstructed.
|
|
70
|
+
const toBufferChunk = (chunk: Buffer | string, encoding: BufferEncoding | null): Buffer =>
|
|
71
|
+
Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk, encoding ?? 'utf8')
|
|
72
|
+
|
|
73
|
+
const isRecoverableDisconnectedIncoming = (
|
|
74
|
+
incoming: IncomingMessage | Http2ServerRequest
|
|
75
|
+
): incoming is IncomingMessage =>
|
|
76
|
+
!(incoming instanceof Http2ServerRequest) &&
|
|
77
|
+
!!incoming.complete &&
|
|
78
|
+
!!incoming.readableAborted &&
|
|
79
|
+
typeof incoming.read === 'function' &&
|
|
80
|
+
isByteExactEncoding(incoming.readableEncoding)
|
|
81
|
+
|
|
82
|
+
// Remember how much complete HTTP/1 body data remained buffered when the
|
|
83
|
+
// disconnect was observed. A later raw read can drain a destroyed stream
|
|
84
|
+
// without updating readableDidRead, so the length snapshot is needed to keep
|
|
85
|
+
// the Fetch-style body from silently resolving with only the remainder.
|
|
86
|
+
export const recordBodyBufferedBeforeDisconnect = (
|
|
87
|
+
incoming: IncomingMessage | Http2ServerRequest
|
|
88
|
+
): void => {
|
|
89
|
+
if (incoming.readableDidRead || !isRecoverableDisconnectedIncoming(incoming)) {
|
|
90
|
+
return
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const incomingWithRecovery = incoming as IncomingWithBodyRecovery
|
|
94
|
+
incomingWithRecovery[bodyBufferedLengthBeforeDisconnectKey] ??= incoming.readableLength
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// A client may close the connection after Node has parsed the complete HTTP/1
|
|
98
|
+
// request but before the application starts reading it. In that state the
|
|
99
|
+
// IncomingMessage is disturbed/aborted, while the untouched body is still in
|
|
100
|
+
// its internal readable buffer — recover it instead of failing the read.
|
|
101
|
+
//
|
|
102
|
+
// HTTP/2 is excluded deliberately: an RST_STREAM discards the buffered request
|
|
103
|
+
// data at the protocol layer, and Http2ServerRequest#complete is also true for
|
|
104
|
+
// aborted/destroyed streams, so it cannot be trusted as a "fully received"
|
|
105
|
+
// signal.
|
|
106
|
+
//
|
|
107
|
+
// Returns the buffered body, an Error the read must fail with, or undefined
|
|
108
|
+
// when the stream is not in the recoverable state. The result is cached on the
|
|
109
|
+
// incoming object so every read path observes the same outcome.
|
|
110
|
+
const readBodyBufferedBeforeDisconnect = (
|
|
111
|
+
incoming: IncomingMessage | Http2ServerRequest,
|
|
112
|
+
chunks?: Buffer[]
|
|
113
|
+
): Buffer | Error | undefined => {
|
|
114
|
+
if ((incoming.readableDidRead && !chunks) || !isRecoverableDisconnectedIncoming(incoming)) {
|
|
115
|
+
return undefined
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const incomingWithRecovery = incoming as IncomingWithBodyRecovery
|
|
119
|
+
if (incomingWithRecovery[bodyBufferedBeforeDisconnectKey] !== undefined) {
|
|
120
|
+
return incomingWithRecovery[bodyBufferedBeforeDisconnectKey]
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
let result: Buffer | Error
|
|
124
|
+
const errored = incoming.errored
|
|
125
|
+
if (errored && (errored as NodeJS.ErrnoException).code !== 'ECONNRESET') {
|
|
126
|
+
// The stream was destroyed with an application-provided error (e.g. a
|
|
127
|
+
// body-size guard calling incoming.destroy(err)). Node's own teardown of a
|
|
128
|
+
// disconnected client uses ECONNRESET errors, which must still recover the
|
|
129
|
+
// body — anything else is surfaced to the reader instead of swallowed.
|
|
130
|
+
result = errored
|
|
131
|
+
} else if (
|
|
132
|
+
incomingWithRecovery[bodyBufferedLengthBeforeDisconnectKey] !== undefined &&
|
|
133
|
+
incoming.readableLength !== incomingWithRecovery[bodyBufferedLengthBeforeDisconnectKey]
|
|
134
|
+
) {
|
|
135
|
+
result = newBodyUnusableError()
|
|
136
|
+
} else {
|
|
137
|
+
const bodyChunks = chunks ?? []
|
|
138
|
+
const chunk = incoming.read() as Buffer | string | null
|
|
139
|
+
if (chunk !== null) {
|
|
140
|
+
bodyChunks.push(toBufferChunk(chunk, incoming.readableEncoding))
|
|
141
|
+
}
|
|
142
|
+
const buffer = bodyChunks.length === 1 ? bodyChunks[0] : Buffer.concat(bodyChunks)
|
|
143
|
+
result = buffer
|
|
144
|
+
// Validate only canonical digit values: the HTTP parser guarantees this
|
|
145
|
+
// form on real connections, and lenient coercion of synthetic inputs
|
|
146
|
+
// (e.g. '0x10') would validate against a length the header never meant.
|
|
147
|
+
const contentLength = incoming.headers['content-length']
|
|
148
|
+
if (typeof contentLength === 'string' && /^\d+$/.test(contentLength)) {
|
|
149
|
+
const expectedLength = Number(contentLength)
|
|
150
|
+
if (Number.isSafeInteger(expectedLength) && buffer.length !== expectedLength) {
|
|
151
|
+
result = newBodyUnusableError()
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
incomingWithRecovery[bodyBufferedBeforeDisconnectKey] = result
|
|
156
|
+
return result
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const enqueueBufferedBody = (
|
|
160
|
+
controller: ReadableStreamDefaultController,
|
|
161
|
+
buffered: Buffer | Error
|
|
162
|
+
): void => {
|
|
163
|
+
if (buffered instanceof Error) {
|
|
164
|
+
controller.error(buffered)
|
|
165
|
+
return
|
|
166
|
+
}
|
|
167
|
+
if (buffered.length > 0) {
|
|
168
|
+
controller.enqueue(buffered)
|
|
169
|
+
}
|
|
170
|
+
controller.close()
|
|
171
|
+
}
|
|
172
|
+
const newRequestFromIncoming = (
|
|
173
|
+
method: string,
|
|
174
|
+
url: string,
|
|
175
|
+
headers: GlobalHeaders,
|
|
176
|
+
incoming: IncomingMessage | Http2ServerRequest,
|
|
177
|
+
abortController: AbortController
|
|
178
|
+
): Request => {
|
|
179
|
+
const init = {
|
|
180
|
+
method: method,
|
|
181
|
+
headers,
|
|
182
|
+
signal: abortController.signal,
|
|
183
|
+
} as RequestInit
|
|
184
|
+
|
|
185
|
+
if (method === 'TRACE') {
|
|
186
|
+
init.method = 'GET'
|
|
187
|
+
const req = new Request(url, init)
|
|
188
|
+
Object.defineProperty(req, 'method', {
|
|
189
|
+
get() {
|
|
190
|
+
return 'TRACE'
|
|
191
|
+
},
|
|
192
|
+
})
|
|
193
|
+
return req
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (!(method === 'GET' || method === 'HEAD')) {
|
|
197
|
+
if ('rawBody' in incoming && incoming.rawBody instanceof Buffer) {
|
|
198
|
+
// In some environments (e.g. firebase functions), the body is already consumed.
|
|
199
|
+
// So we need to re-read the request body from `incoming.rawBody` if available.
|
|
200
|
+
init.body = new ReadableStream({
|
|
201
|
+
start(controller) {
|
|
202
|
+
controller.enqueue(incoming.rawBody)
|
|
203
|
+
controller.close()
|
|
204
|
+
},
|
|
205
|
+
})
|
|
206
|
+
} else if ((incoming as IncomingMessageWithWrapBodyStream)[wrapBodyStream]) {
|
|
207
|
+
let reader: ReadableStreamDefaultReader<Uint8Array> | undefined
|
|
208
|
+
init.body = new ReadableStream({
|
|
209
|
+
async pull(controller) {
|
|
210
|
+
try {
|
|
211
|
+
if (!reader) {
|
|
212
|
+
const buffered = readBodyBufferedBeforeDisconnect(incoming)
|
|
213
|
+
if (buffered !== undefined) {
|
|
214
|
+
enqueueBufferedBody(controller, buffered)
|
|
215
|
+
return
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
reader ||= Readable.toWeb(incoming).getReader()
|
|
219
|
+
const { done, value } = await reader.read()
|
|
220
|
+
if (done) {
|
|
221
|
+
controller.close()
|
|
222
|
+
} else {
|
|
223
|
+
controller.enqueue(value)
|
|
224
|
+
}
|
|
225
|
+
} catch (error) {
|
|
226
|
+
controller.error(error)
|
|
227
|
+
}
|
|
228
|
+
},
|
|
229
|
+
})
|
|
230
|
+
} else {
|
|
231
|
+
const buffered = readBodyBufferedBeforeDisconnect(incoming)
|
|
232
|
+
if (buffered !== undefined) {
|
|
233
|
+
init.body = new ReadableStream({
|
|
234
|
+
start(controller) {
|
|
235
|
+
enqueueBufferedBody(controller, buffered)
|
|
236
|
+
},
|
|
237
|
+
})
|
|
238
|
+
} else {
|
|
239
|
+
// lazy-consume request body
|
|
240
|
+
init.body = Readable.toWeb(incoming) as ReadableStream<Uint8Array>
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
return new Request(url, init)
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const getRequestCache = Symbol('getRequestCache')
|
|
249
|
+
const requestCache = Symbol('requestCache')
|
|
250
|
+
const incomingKey = Symbol('incomingKey')
|
|
251
|
+
const urlKey = Symbol('urlKey')
|
|
252
|
+
const methodKey = Symbol('methodKey')
|
|
253
|
+
const headersKey = Symbol('headersKey')
|
|
254
|
+
export const abortControllerKey = Symbol('abortControllerKey')
|
|
255
|
+
export const getAbortController = Symbol('getAbortController')
|
|
256
|
+
export const abortRequest = Symbol('abortRequest')
|
|
257
|
+
const bodyBufferKey = Symbol('bodyBuffer')
|
|
258
|
+
const bodyReadPromiseKey = Symbol('bodyReadPromise')
|
|
259
|
+
const bodyConsumedDirectlyKey = Symbol('bodyConsumedDirectly')
|
|
260
|
+
const bodyLockReaderKey = Symbol('bodyLockReader')
|
|
261
|
+
const abortReasonKey = Symbol('abortReason')
|
|
262
|
+
|
|
263
|
+
const newBodyUnusableError = (): TypeError => {
|
|
264
|
+
return new TypeError('Body is unusable')
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const rejectBodyUnusable = (): Promise<never> => {
|
|
268
|
+
return Promise.reject(newBodyUnusableError())
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const textDecoder = new TextDecoder()
|
|
272
|
+
|
|
273
|
+
const consumeBodyDirectOnce = (
|
|
274
|
+
request: Record<string | symbol, any>
|
|
275
|
+
): Promise<never> | undefined => {
|
|
276
|
+
if (request[bodyConsumedDirectlyKey]) {
|
|
277
|
+
return rejectBodyUnusable()
|
|
278
|
+
}
|
|
279
|
+
request[bodyConsumedDirectlyKey] = true
|
|
280
|
+
return undefined
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const toArrayBuffer = (buf: Buffer): ArrayBuffer => {
|
|
284
|
+
return buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength) as ArrayBuffer
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const contentType = (request: Record<string | symbol, any>): string => {
|
|
288
|
+
return (
|
|
289
|
+
(request[headersKey] ||= newHeadersFromIncoming(request[incomingKey])).get('content-type') || ''
|
|
290
|
+
)
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
type DirectBodyReadMethod = 'text' | 'arrayBuffer' | 'blob'
|
|
294
|
+
|
|
295
|
+
const methodTokenRegExp = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/
|
|
296
|
+
|
|
297
|
+
const normalizeIncomingMethod = (method: unknown): string => {
|
|
298
|
+
if (typeof method !== 'string' || method.length === 0) {
|
|
299
|
+
return 'GET'
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// fast path for already-uppercase common methods from Node.js.
|
|
303
|
+
switch (method) {
|
|
304
|
+
case 'DELETE':
|
|
305
|
+
case 'GET':
|
|
306
|
+
case 'HEAD':
|
|
307
|
+
case 'OPTIONS':
|
|
308
|
+
case 'PATCH':
|
|
309
|
+
case 'POST':
|
|
310
|
+
case 'PUT':
|
|
311
|
+
case 'QUERY':
|
|
312
|
+
return method
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const upper = method.toUpperCase()
|
|
316
|
+
// Fetch only normalizes these methods for backwards compatibility.
|
|
317
|
+
// HTTP methods are otherwise case-sensitive, so methods not in this list
|
|
318
|
+
// (including `query`) must retain their original casing.
|
|
319
|
+
switch (upper) {
|
|
320
|
+
case 'DELETE':
|
|
321
|
+
case 'GET':
|
|
322
|
+
case 'HEAD':
|
|
323
|
+
case 'OPTIONS':
|
|
324
|
+
case 'POST':
|
|
325
|
+
case 'PUT':
|
|
326
|
+
return upper
|
|
327
|
+
default:
|
|
328
|
+
return method
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const validateDirectReadMethod = (method: string): TypeError | undefined => {
|
|
333
|
+
if (!methodTokenRegExp.test(method)) {
|
|
334
|
+
return new TypeError(`'${method}' is not a valid HTTP method.`)
|
|
335
|
+
}
|
|
336
|
+
// Keep TRACE workaround behavior, but preserve native rejection for other
|
|
337
|
+
// forbidden methods when using the direct-read fast path.
|
|
338
|
+
// Only exact upper-case TRACE is treated as the existing workaround target.
|
|
339
|
+
const normalized = method.toUpperCase()
|
|
340
|
+
if (
|
|
341
|
+
normalized === 'CONNECT' ||
|
|
342
|
+
normalized === 'TRACK' ||
|
|
343
|
+
(normalized === 'TRACE' && method !== 'TRACE')
|
|
344
|
+
) {
|
|
345
|
+
return new TypeError(`'${method}' HTTP method is unsupported.`)
|
|
346
|
+
}
|
|
347
|
+
return undefined
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
const readBodyWithFastPath = <T>(
|
|
351
|
+
request: Record<string | symbol, any>,
|
|
352
|
+
method: DirectBodyReadMethod,
|
|
353
|
+
fromBuffer: (buf: Buffer, request: Record<string | symbol, any>) => T | Promise<T>
|
|
354
|
+
): Promise<T> => {
|
|
355
|
+
if (request[bodyConsumedDirectlyKey]) {
|
|
356
|
+
return rejectBodyUnusable()
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
const methodName = request.method as string
|
|
360
|
+
if (methodName === 'GET' || methodName === 'HEAD') {
|
|
361
|
+
return request[getRequestCache]()[method]()
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
const methodValidationError = validateDirectReadMethod(methodName)
|
|
365
|
+
if (methodValidationError) {
|
|
366
|
+
return Promise.reject(methodValidationError)
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
if (request[requestCache]) {
|
|
370
|
+
// Keep TRACE direct-read behavior stable even if non-body properties
|
|
371
|
+
// created requestCache earlier (e.g. signal access).
|
|
372
|
+
if (methodName !== 'TRACE') {
|
|
373
|
+
const cachedRequest = request[requestCache] as Request
|
|
374
|
+
return cachedRequest[method]() as Promise<T>
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
const alreadyUsedError = consumeBodyDirectOnce(request)
|
|
379
|
+
if (alreadyUsedError) {
|
|
380
|
+
return alreadyUsedError
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
const raw = readRawBodyIfAvailable(request)
|
|
384
|
+
if (raw) {
|
|
385
|
+
const result = Promise.resolve(fromBuffer(raw, request))
|
|
386
|
+
request[bodyBufferKey] = undefined
|
|
387
|
+
return result
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
return readBodyDirect(request).then((buf) => {
|
|
391
|
+
const result = fromBuffer(buf, request)
|
|
392
|
+
request[bodyBufferKey] = undefined
|
|
393
|
+
return result
|
|
394
|
+
})
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
const readRawBodyIfAvailable = (request: Record<string | symbol, any>): Buffer | undefined => {
|
|
398
|
+
const incoming = request[incomingKey] as IncomingMessage | Http2ServerRequest
|
|
399
|
+
if ('rawBody' in incoming && (incoming as any).rawBody instanceof Buffer) {
|
|
400
|
+
return (incoming as any).rawBody as Buffer
|
|
401
|
+
}
|
|
402
|
+
return undefined
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// The error a body read fails with after an abort: the stream's own error if
|
|
406
|
+
// it has one, otherwise the abort reason recorded on the request, otherwise a
|
|
407
|
+
// generic disconnect error.
|
|
408
|
+
const normalizeAbortError = (
|
|
409
|
+
request: Record<string | symbol, any>,
|
|
410
|
+
incoming: IncomingMessage | Http2ServerRequest
|
|
411
|
+
): Error => {
|
|
412
|
+
if (incoming.errored) {
|
|
413
|
+
return incoming.errored
|
|
414
|
+
}
|
|
415
|
+
const reason = request[abortReasonKey]
|
|
416
|
+
if (reason !== undefined) {
|
|
417
|
+
return reason instanceof Error ? reason : new Error(String(reason))
|
|
418
|
+
}
|
|
419
|
+
return new Error('Client connection prematurely closed.')
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// Read body directly from the IncomingMessage stream, bypassing Request object creation.
|
|
423
|
+
// Precondition: the caller (listener.ts) must ensure that the IncomingMessage stream is
|
|
424
|
+
// properly cleaned up (e.g. via incoming.resume()) when the response ends or the connection
|
|
425
|
+
// closes. This function does not call incoming.destroy() on abort.
|
|
426
|
+
const readBodyDirect = (request: Record<string | symbol, any>): Promise<Buffer> => {
|
|
427
|
+
if (request[bodyBufferKey]) {
|
|
428
|
+
return Promise.resolve(request[bodyBufferKey] as Buffer)
|
|
429
|
+
}
|
|
430
|
+
if (request[bodyReadPromiseKey]) {
|
|
431
|
+
return request[bodyReadPromiseKey] as Promise<Buffer>
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
const incoming = request[incomingKey] as IncomingMessage | Http2ServerRequest
|
|
435
|
+
if (incoming.readableDidRead) {
|
|
436
|
+
return rejectBodyUnusable()
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
const buffered = readBodyBufferedBeforeDisconnect(incoming)
|
|
440
|
+
if (buffered !== undefined) {
|
|
441
|
+
if (buffered instanceof Error) {
|
|
442
|
+
return Promise.reject(buffered)
|
|
443
|
+
}
|
|
444
|
+
request[bodyBufferKey] = buffered
|
|
445
|
+
return Promise.resolve(buffered)
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
const promise = new Promise<Buffer>((resolve, reject) => {
|
|
449
|
+
const chunks: Buffer[] = []
|
|
450
|
+
let settled = false
|
|
451
|
+
|
|
452
|
+
const finish = (callback: () => void) => {
|
|
453
|
+
if (settled) {
|
|
454
|
+
return
|
|
455
|
+
}
|
|
456
|
+
settled = true
|
|
457
|
+
cleanup()
|
|
458
|
+
callback()
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// readBodyDirect started while the stream was untouched, so it owns every
|
|
462
|
+
// chunk emitted from that point onward. If HTTP/1 parsing completed before
|
|
463
|
+
// the connection closed, combine those chunks with anything still buffered
|
|
464
|
+
// and treat the body as complete. Transport-level ECONNRESET is recoverable;
|
|
465
|
+
// application-provided stream errors are not.
|
|
466
|
+
const recoverCompleteBodyAfterDisconnect = (error?: unknown): boolean => {
|
|
467
|
+
const streamError = incoming.errored ?? error
|
|
468
|
+
if (
|
|
469
|
+
!isRecoverableDisconnectedIncoming(incoming) ||
|
|
470
|
+
(streamError && (streamError as NodeJS.ErrnoException).code !== 'ECONNRESET')
|
|
471
|
+
) {
|
|
472
|
+
return false
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
finish(() => {
|
|
476
|
+
const recovered = readBodyBufferedBeforeDisconnect(incoming, chunks)
|
|
477
|
+
if (recovered instanceof Error) {
|
|
478
|
+
reject(recovered)
|
|
479
|
+
} else if (recovered === undefined) {
|
|
480
|
+
reject(error ?? normalizeAbortError(request, incoming))
|
|
481
|
+
} else {
|
|
482
|
+
request[bodyBufferKey] = recovered
|
|
483
|
+
resolve(recovered)
|
|
484
|
+
}
|
|
485
|
+
})
|
|
486
|
+
return true
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
const onData = (chunk: Buffer | string) => {
|
|
490
|
+
chunks.push(toBufferChunk(chunk, incoming.readableEncoding))
|
|
491
|
+
}
|
|
492
|
+
const onEnd = () => {
|
|
493
|
+
finish(() => {
|
|
494
|
+
const buffer = chunks.length === 1 ? chunks[0] : Buffer.concat(chunks)
|
|
495
|
+
request[bodyBufferKey] = buffer
|
|
496
|
+
resolve(buffer)
|
|
497
|
+
})
|
|
498
|
+
}
|
|
499
|
+
const onError = (error: unknown) => {
|
|
500
|
+
if (recoverCompleteBodyAfterDisconnect(error)) {
|
|
501
|
+
return
|
|
502
|
+
}
|
|
503
|
+
finish(() => {
|
|
504
|
+
reject(error)
|
|
505
|
+
})
|
|
506
|
+
}
|
|
507
|
+
const onClose = () => {
|
|
508
|
+
if (incoming.readableEnded) {
|
|
509
|
+
onEnd()
|
|
510
|
+
return
|
|
511
|
+
}
|
|
512
|
+
if (recoverCompleteBodyAfterDisconnect()) {
|
|
513
|
+
return
|
|
514
|
+
}
|
|
515
|
+
finish(() => {
|
|
516
|
+
reject(normalizeAbortError(request, incoming))
|
|
517
|
+
})
|
|
518
|
+
}
|
|
519
|
+
const cleanup = () => {
|
|
520
|
+
incoming.off('data', onData)
|
|
521
|
+
incoming.off('end', onEnd)
|
|
522
|
+
incoming.off('error', onError)
|
|
523
|
+
incoming.off('close', onClose)
|
|
524
|
+
request[bodyReadPromiseKey] = undefined
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
incoming.on('data', onData)
|
|
528
|
+
incoming.on('end', onEnd)
|
|
529
|
+
incoming.on('error', onError)
|
|
530
|
+
incoming.on('close', onClose)
|
|
531
|
+
|
|
532
|
+
// If the stream has already settled before listeners were attached,
|
|
533
|
+
// no further events will fire, so resolve/reject from the current state.
|
|
534
|
+
queueMicrotask(() => {
|
|
535
|
+
if (settled) {
|
|
536
|
+
return
|
|
537
|
+
}
|
|
538
|
+
if (incoming.readableEnded) {
|
|
539
|
+
onEnd()
|
|
540
|
+
} else if (incoming.errored) {
|
|
541
|
+
onError(incoming.errored)
|
|
542
|
+
} else if (incoming.destroyed) {
|
|
543
|
+
onClose()
|
|
544
|
+
}
|
|
545
|
+
})
|
|
546
|
+
})
|
|
547
|
+
|
|
548
|
+
request[bodyReadPromiseKey] = promise
|
|
549
|
+
return promise
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
const requestPrototype: Record<string | symbol, any> = {
|
|
553
|
+
get method() {
|
|
554
|
+
return this[methodKey]
|
|
555
|
+
},
|
|
556
|
+
|
|
557
|
+
get url() {
|
|
558
|
+
return this[urlKey]
|
|
559
|
+
},
|
|
560
|
+
|
|
561
|
+
get headers() {
|
|
562
|
+
return (this[headersKey] ||= newHeadersFromIncoming(this[incomingKey]))
|
|
563
|
+
},
|
|
564
|
+
|
|
565
|
+
[abortRequest](reason: unknown) {
|
|
566
|
+
if (this[abortReasonKey] === undefined) {
|
|
567
|
+
this[abortReasonKey] = reason
|
|
568
|
+
}
|
|
569
|
+
const abortController = this[abortControllerKey] as AbortController | undefined
|
|
570
|
+
if (abortController && !abortController.signal.aborted) {
|
|
571
|
+
abortController.abort(reason)
|
|
572
|
+
}
|
|
573
|
+
},
|
|
574
|
+
|
|
575
|
+
[getAbortController]() {
|
|
576
|
+
this[abortControllerKey] ||= new AbortController()
|
|
577
|
+
if (this[abortReasonKey] !== undefined && !this[abortControllerKey].signal.aborted) {
|
|
578
|
+
this[abortControllerKey].abort(this[abortReasonKey])
|
|
579
|
+
}
|
|
580
|
+
return this[abortControllerKey]
|
|
581
|
+
},
|
|
582
|
+
|
|
583
|
+
[getRequestCache]() {
|
|
584
|
+
const abortController = this[getAbortController]()
|
|
585
|
+
if (this[requestCache]) {
|
|
586
|
+
return this[requestCache]
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
const method = this.method
|
|
590
|
+
|
|
591
|
+
// If body was already consumed directly, create a minimal Request with an empty body
|
|
592
|
+
// to avoid holding the body buffer in memory via ReadableStream re-wrapping.
|
|
593
|
+
if (this[bodyConsumedDirectlyKey] && !(method === 'GET' || method === 'HEAD')) {
|
|
594
|
+
this[bodyBufferKey] = undefined
|
|
595
|
+
const init = {
|
|
596
|
+
method: method === 'TRACE' ? 'GET' : method,
|
|
597
|
+
headers: this.headers,
|
|
598
|
+
signal: abortController.signal,
|
|
599
|
+
} as RequestInit
|
|
600
|
+
if (method !== 'TRACE') {
|
|
601
|
+
init.body = new ReadableStream({
|
|
602
|
+
start(c) {
|
|
603
|
+
c.close()
|
|
604
|
+
},
|
|
605
|
+
})
|
|
606
|
+
;(init as any).duplex = 'half'
|
|
607
|
+
}
|
|
608
|
+
const req = new Request(this[urlKey], init)
|
|
609
|
+
if (method === 'TRACE') {
|
|
610
|
+
Object.defineProperty(req, 'method', {
|
|
611
|
+
get() {
|
|
612
|
+
return 'TRACE'
|
|
613
|
+
},
|
|
614
|
+
})
|
|
615
|
+
}
|
|
616
|
+
return (this[requestCache] = req)
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
return (this[requestCache] = newRequestFromIncoming(
|
|
620
|
+
this.method,
|
|
621
|
+
this[urlKey],
|
|
622
|
+
this.headers,
|
|
623
|
+
this[incomingKey],
|
|
624
|
+
abortController
|
|
625
|
+
))
|
|
626
|
+
},
|
|
627
|
+
|
|
628
|
+
get body() {
|
|
629
|
+
if (!this[bodyConsumedDirectlyKey]) {
|
|
630
|
+
return this[getRequestCache]().body
|
|
631
|
+
}
|
|
632
|
+
const request = this[getRequestCache]()
|
|
633
|
+
if (!this[bodyLockReaderKey] && request.body) {
|
|
634
|
+
// Web standard requires body.locked === true when bodyUsed === true.
|
|
635
|
+
// After direct consumption (text/json/arrayBuffer/blob), getRequestCache() returns
|
|
636
|
+
// a Request with an empty ReadableStream body. We lock it here so that
|
|
637
|
+
// body.locked reflects the consumed state correctly.
|
|
638
|
+
this[bodyLockReaderKey] = request.body.getReader()
|
|
639
|
+
}
|
|
640
|
+
return request.body
|
|
641
|
+
},
|
|
642
|
+
|
|
643
|
+
get bodyUsed() {
|
|
644
|
+
if (this[bodyConsumedDirectlyKey]) {
|
|
645
|
+
return true
|
|
646
|
+
}
|
|
647
|
+
if (this[requestCache]) {
|
|
648
|
+
return this[requestCache].bodyUsed
|
|
649
|
+
}
|
|
650
|
+
return false
|
|
651
|
+
},
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
Object.defineProperty(requestPrototype, 'signal', {
|
|
655
|
+
get() {
|
|
656
|
+
return this[getAbortController]().signal
|
|
657
|
+
},
|
|
658
|
+
})
|
|
659
|
+
;[
|
|
660
|
+
'cache',
|
|
661
|
+
'credentials',
|
|
662
|
+
'destination',
|
|
663
|
+
'integrity',
|
|
664
|
+
'mode',
|
|
665
|
+
'redirect',
|
|
666
|
+
'referrer',
|
|
667
|
+
'referrerPolicy',
|
|
668
|
+
'keepalive',
|
|
669
|
+
].forEach((k) => {
|
|
670
|
+
Object.defineProperty(requestPrototype, k, {
|
|
671
|
+
get() {
|
|
672
|
+
return this[getRequestCache]()[k]
|
|
673
|
+
},
|
|
674
|
+
})
|
|
675
|
+
})
|
|
676
|
+
;['clone', 'formData'].forEach((k) => {
|
|
677
|
+
Object.defineProperty(requestPrototype, k, {
|
|
678
|
+
value: function () {
|
|
679
|
+
if (this[bodyConsumedDirectlyKey]) {
|
|
680
|
+
if (k === 'clone') {
|
|
681
|
+
throw newBodyUnusableError()
|
|
682
|
+
}
|
|
683
|
+
return rejectBodyUnusable()
|
|
684
|
+
}
|
|
685
|
+
return this[getRequestCache]()[k]()
|
|
686
|
+
},
|
|
687
|
+
})
|
|
688
|
+
})
|
|
689
|
+
|
|
690
|
+
// Direct body reading for text/arrayBuffer/blob/json: bypass getRequestCache()
|
|
691
|
+
// → new AbortController() → newHeadersFromIncoming() → new Request(url, init)
|
|
692
|
+
// → Readable.toWeb() chain for common body parsing cases.
|
|
693
|
+
Object.defineProperty(requestPrototype, 'text', {
|
|
694
|
+
value: function (): Promise<string> {
|
|
695
|
+
return readBodyWithFastPath(this, 'text', (buf) => textDecoder.decode(buf))
|
|
696
|
+
},
|
|
697
|
+
})
|
|
698
|
+
Object.defineProperty(requestPrototype, 'arrayBuffer', {
|
|
699
|
+
value: function (): Promise<ArrayBuffer> {
|
|
700
|
+
return readBodyWithFastPath(this, 'arrayBuffer', (buf) => toArrayBuffer(buf))
|
|
701
|
+
},
|
|
702
|
+
})
|
|
703
|
+
Object.defineProperty(requestPrototype, 'blob', {
|
|
704
|
+
value: function (): Promise<Blob> {
|
|
705
|
+
return readBodyWithFastPath(this, 'blob', (buf, request) => {
|
|
706
|
+
const type = contentType(request)
|
|
707
|
+
const init = type ? { headers: { 'content-type': type } } : undefined
|
|
708
|
+
// Buffer is typed over ArrayBufferLike, but request bodies are always
|
|
709
|
+
// backed by an ArrayBuffer, so cast instead of copying the bytes
|
|
710
|
+
return new Response(buf as Uint8Array<ArrayBuffer>, init).blob()
|
|
711
|
+
})
|
|
712
|
+
},
|
|
713
|
+
})
|
|
714
|
+
// json() reuses text() fast path to keep body consumption logic centralized.
|
|
715
|
+
Object.defineProperty(requestPrototype, 'json', {
|
|
716
|
+
value: function (): Promise<any> {
|
|
717
|
+
if (this[bodyConsumedDirectlyKey]) {
|
|
718
|
+
return rejectBodyUnusable()
|
|
719
|
+
}
|
|
720
|
+
return this.text().then(JSON.parse)
|
|
721
|
+
},
|
|
722
|
+
})
|
|
723
|
+
|
|
724
|
+
Object.defineProperty(requestPrototype, Symbol.for('nodejs.util.inspect.custom'), {
|
|
725
|
+
value: function (depth: number, options: object, inspectFn: Function) {
|
|
726
|
+
const props: Record<string, unknown> = {
|
|
727
|
+
method: this.method,
|
|
728
|
+
url: this.url,
|
|
729
|
+
headers: this.headers,
|
|
730
|
+
nativeRequest: this[requestCache],
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
return `Request (lightweight) ${inspectFn(props, { ...options, depth: depth == null ? null : depth - 1 })}`
|
|
734
|
+
},
|
|
735
|
+
})
|
|
736
|
+
|
|
737
|
+
Object.setPrototypeOf(requestPrototype, Request.prototype)
|
|
738
|
+
|
|
739
|
+
export const newRequest = (
|
|
740
|
+
incoming: IncomingMessage | Http2ServerRequest,
|
|
741
|
+
defaultHostname?: string
|
|
742
|
+
) => {
|
|
743
|
+
const req = Object.create(requestPrototype)
|
|
744
|
+
req[incomingKey] = incoming
|
|
745
|
+
req[methodKey] = normalizeIncomingMethod(incoming.method)
|
|
746
|
+
|
|
747
|
+
const incomingUrl = incoming.url || ''
|
|
748
|
+
|
|
749
|
+
// handle absolute URL in request.url
|
|
750
|
+
if (
|
|
751
|
+
incomingUrl[0] !== '/' && // short-circuit for performance. most requests are relative URL.
|
|
752
|
+
(incomingUrl.startsWith('http://') || incomingUrl.startsWith('https://'))
|
|
753
|
+
) {
|
|
754
|
+
if (incoming instanceof Http2ServerRequest) {
|
|
755
|
+
throw new RequestError('Absolute URL for :path is not allowed in HTTP/2') // RFC 9113 8.3.1.
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
try {
|
|
759
|
+
const url = new URL(incomingUrl)
|
|
760
|
+
req[urlKey] = url.href
|
|
761
|
+
} catch (e) {
|
|
762
|
+
throw new RequestError('Invalid absolute URL', { cause: e })
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
return req
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
// Otherwise, relative URL
|
|
769
|
+
const host =
|
|
770
|
+
(incoming instanceof Http2ServerRequest ? incoming.authority : incoming.headers.host) ||
|
|
771
|
+
defaultHostname
|
|
772
|
+
if (!host) {
|
|
773
|
+
throw new RequestError('Missing host header')
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
let scheme: string
|
|
777
|
+
if (incoming instanceof Http2ServerRequest) {
|
|
778
|
+
scheme = incoming.scheme
|
|
779
|
+
if (!(scheme === 'http' || scheme === 'https')) {
|
|
780
|
+
throw new RequestError('Unsupported scheme')
|
|
781
|
+
}
|
|
782
|
+
} else {
|
|
783
|
+
scheme = incoming.socket && (incoming.socket as TLSSocket).encrypted ? 'https' : 'http'
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
try {
|
|
787
|
+
req[urlKey] = buildUrl(scheme, host, incomingUrl)
|
|
788
|
+
} catch (e) {
|
|
789
|
+
if (e instanceof RequestError) {
|
|
790
|
+
throw e
|
|
791
|
+
} else {
|
|
792
|
+
throw new RequestError('Invalid URL', { cause: e })
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
return req
|
|
797
|
+
}
|