Repository: nodejs/undici
Branch: main
Commit: 4c3a2be56abc
Files: 733
Total size: 5.8 MB
Directory structure:
gitextract_26dbkv5y/
├── .c8rc.json
├── .dockerignore
├── .editorconfig
├── .github/
│ ├── ISSUE_TEMPLATE/
│ │ ├── bug-report.md
│ │ └── feature-request.md
│ ├── PULL_REQUEST_TEMPLATE.md
│ ├── dependabot.yml
│ └── workflows/
│ ├── autobahn.yml
│ ├── backport.yml
│ ├── bench.yml
│ ├── ci.yml
│ ├── codeql.yml
│ ├── nodejs-nightly.yml
│ ├── nodejs-shared.yml
│ ├── nodejs.yml
│ ├── release-create-pr.yml
│ ├── release.yml
│ ├── scorecard.yml
│ ├── triggered-autobahn.yml
│ └── update-submodules.yml
├── .gitignore
├── .gitmodules
├── .husky/
│ └── pre-commit
├── .npmignore
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── GOVERNANCE.md
├── LICENSE
├── MAINTAINERS.md
├── README.md
├── SECURITY.md
├── benchmarks/
│ ├── _util/
│ │ ├── index.js
│ │ └── runner.js
│ ├── benchmark-http2.js
│ ├── benchmark-https.js
│ ├── benchmark.js
│ ├── cache/
│ │ ├── date.mjs
│ │ └── get-field-values.mjs
│ ├── cookies/
│ │ ├── is-ctl-excluding-htab.mjs
│ │ ├── to-imf-date.mjs
│ │ ├── validate-cookie-name.mjs
│ │ └── validate-cookie-value.mjs
│ ├── core/
│ │ ├── is-blob-like.mjs
│ │ ├── is-valid-header-char.mjs
│ │ ├── is-valid-port.mjs
│ │ ├── parse-headers.mjs
│ │ ├── parse-raw-headers.mjs
│ │ ├── request-instantiation.mjs
│ │ └── tree.mjs
│ ├── fetch/
│ │ ├── body-arraybuffer.mjs
│ │ ├── bytes-match.mjs
│ │ ├── headers-length32.mjs
│ │ ├── headers.mjs
│ │ ├── is-valid-encoded-url.mjs
│ │ ├── is-valid-header-value.mjs
│ │ ├── isomorphic-encode.mjs
│ │ ├── request-creation.mjs
│ │ └── url-has-https-scheme.mjs
│ ├── package.json
│ ├── post-benchmark.js
│ ├── server-http2.js
│ ├── server-https.js
│ ├── server.js
│ ├── timers/
│ │ └── compare-timer-getters.mjs
│ ├── utils/
│ │ └── date.mjs
│ ├── wait.js
│ ├── webidl/
│ │ └── webidl-is.mjs
│ ├── websocket/
│ │ ├── generate-mask.mjs
│ │ ├── is-valid-subprotocol.mjs
│ │ └── messageevent.mjs
│ ├── websocket-benchmark.mjs
│ └── websocket-echo-server.mjs
├── build/
│ └── wasm.js
├── deps/
│ └── llhttp/
│ ├── include/
│ │ └── llhttp.h
│ └── src/
│ ├── api.c
│ ├── http.c
│ └── llhttp.c
├── docs/
│ ├── .nojekyll
│ ├── CNAME
│ ├── docs/
│ │ ├── api/
│ │ │ ├── Agent.md
│ │ │ ├── BalancedPool.md
│ │ │ ├── CacheStorage.md
│ │ │ ├── CacheStore.md
│ │ │ ├── Client.md
│ │ │ ├── ClientStats.md
│ │ │ ├── Connector.md
│ │ │ ├── ContentType.md
│ │ │ ├── Cookies.md
│ │ │ ├── Debug.md
│ │ │ ├── DiagnosticsChannel.md
│ │ │ ├── Dispatcher.md
│ │ │ ├── EnvHttpProxyAgent.md
│ │ │ ├── Errors.md
│ │ │ ├── EventSource.md
│ │ │ ├── Fetch.md
│ │ │ ├── GlobalInstallation.md
│ │ │ ├── H2CClient.md
│ │ │ ├── MockAgent.md
│ │ │ ├── MockCallHistory.md
│ │ │ ├── MockCallHistoryLog.md
│ │ │ ├── MockClient.md
│ │ │ ├── MockErrors.md
│ │ │ ├── MockPool.md
│ │ │ ├── Pool.md
│ │ │ ├── PoolStats.md
│ │ │ ├── ProxyAgent.md
│ │ │ ├── RedirectHandler.md
│ │ │ ├── RetryAgent.md
│ │ │ ├── RetryHandler.md
│ │ │ ├── RoundRobinPool.md
│ │ │ ├── SnapshotAgent.md
│ │ │ ├── Socks5ProxyAgent.md
│ │ │ ├── Util.md
│ │ │ ├── WebSocket.md
│ │ │ └── api-lifecycle.md
│ │ └── best-practices/
│ │ ├── client-certificate.md
│ │ ├── crawling.md
│ │ ├── mocking-request.md
│ │ ├── proxy.md
│ │ ├── undici-vs-builtin-fetch.md
│ │ └── writing-tests.md
│ ├── docsify/
│ │ └── sidebar.md
│ ├── examples/
│ │ ├── README.md
│ │ ├── ca-fingerprint/
│ │ │ └── index.js
│ │ ├── eventsource.js
│ │ ├── fetch.js
│ │ ├── proxy/
│ │ │ ├── fetch.mjs
│ │ │ ├── index.js
│ │ │ ├── proxy.js
│ │ │ └── websocket.js
│ │ ├── proxy-agent.js
│ │ ├── request.js
│ │ ├── snapshot-testing.js
│ │ └── socks5-proxy.js
│ ├── index.html
│ └── package.json
├── eslint.config.js
├── index-fetch.js
├── index.d.ts
├── index.js
├── lib/
│ ├── api/
│ │ ├── abort-signal.js
│ │ ├── api-connect.js
│ │ ├── api-pipeline.js
│ │ ├── api-request.js
│ │ ├── api-stream.js
│ │ ├── api-upgrade.js
│ │ ├── index.js
│ │ └── readable.js
│ ├── cache/
│ │ ├── memory-cache-store.js
│ │ └── sqlite-cache-store.js
│ ├── core/
│ │ ├── connect.js
│ │ ├── constants.js
│ │ ├── diagnostics.js
│ │ ├── errors.js
│ │ ├── request.js
│ │ ├── socks5-client.js
│ │ ├── socks5-utils.js
│ │ ├── symbols.js
│ │ ├── tree.js
│ │ └── util.js
│ ├── dispatcher/
│ │ ├── agent.js
│ │ ├── balanced-pool.js
│ │ ├── client-h1.js
│ │ ├── client-h2.js
│ │ ├── client.js
│ │ ├── dispatcher-base.js
│ │ ├── dispatcher.js
│ │ ├── env-http-proxy-agent.js
│ │ ├── fixed-queue.js
│ │ ├── h2c-client.js
│ │ ├── pool-base.js
│ │ ├── pool.js
│ │ ├── proxy-agent.js
│ │ ├── retry-agent.js
│ │ ├── round-robin-pool.js
│ │ └── socks5-proxy-agent.js
│ ├── encoding/
│ │ └── index.js
│ ├── global.js
│ ├── handler/
│ │ ├── cache-handler.js
│ │ ├── cache-revalidation-handler.js
│ │ ├── decorator-handler.js
│ │ ├── deduplication-handler.js
│ │ ├── redirect-handler.js
│ │ ├── retry-handler.js
│ │ ├── unwrap-handler.js
│ │ └── wrap-handler.js
│ ├── interceptor/
│ │ ├── cache.js
│ │ ├── decompress.js
│ │ ├── deduplicate.js
│ │ ├── dns.js
│ │ ├── dump.js
│ │ ├── redirect.js
│ │ ├── response-error.js
│ │ └── retry.js
│ ├── llhttp/
│ │ ├── .gitkeep
│ │ ├── constants.d.ts
│ │ ├── constants.js
│ │ ├── llhttp-wasm.js
│ │ ├── llhttp.wasm
│ │ ├── llhttp_simd-wasm.js
│ │ ├── llhttp_simd.wasm
│ │ ├── utils.d.ts
│ │ └── utils.js
│ ├── mock/
│ │ ├── mock-agent.js
│ │ ├── mock-call-history.js
│ │ ├── mock-client.js
│ │ ├── mock-errors.js
│ │ ├── mock-interceptor.js
│ │ ├── mock-pool.js
│ │ ├── mock-symbols.js
│ │ ├── mock-utils.js
│ │ ├── pending-interceptors-formatter.js
│ │ ├── snapshot-agent.js
│ │ ├── snapshot-recorder.js
│ │ └── snapshot-utils.js
│ ├── util/
│ │ ├── cache.js
│ │ ├── date.js
│ │ ├── promise.js
│ │ ├── runtime-features.js
│ │ ├── stats.js
│ │ └── timers.js
│ └── web/
│ ├── cache/
│ │ ├── cache.js
│ │ ├── cachestorage.js
│ │ └── util.js
│ ├── cookies/
│ │ ├── constants.js
│ │ ├── index.js
│ │ ├── parse.js
│ │ └── util.js
│ ├── eventsource/
│ │ ├── eventsource-stream.js
│ │ ├── eventsource.js
│ │ └── util.js
│ ├── fetch/
│ │ ├── LICENSE
│ │ ├── body.js
│ │ ├── constants.js
│ │ ├── data-url.js
│ │ ├── formdata-parser.js
│ │ ├── formdata.js
│ │ ├── global.js
│ │ ├── headers.js
│ │ ├── index.js
│ │ ├── request.js
│ │ ├── response.js
│ │ └── util.js
│ ├── infra/
│ │ └── index.js
│ ├── subresource-integrity/
│ │ ├── Readme.md
│ │ └── subresource-integrity.js
│ ├── webidl/
│ │ └── index.js
│ └── websocket/
│ ├── connection.js
│ ├── constants.js
│ ├── events.js
│ ├── frame.js
│ ├── permessage-deflate.js
│ ├── receiver.js
│ ├── sender.js
│ ├── stream/
│ │ ├── websocketerror.js
│ │ └── websocketstream.js
│ ├── util.js
│ └── websocket.js
├── package.json
├── scripts/
│ ├── clean-coverage.js
│ ├── generate-pem.js
│ ├── generate-undici-types-package-json.js
│ ├── platform-shell.js
│ ├── release.js
│ └── strip-comments.js
├── test/
│ ├── autobahn/
│ │ ├── .gitignore
│ │ ├── client.js
│ │ ├── config/
│ │ │ └── fuzzingserver.json
│ │ ├── report.js
│ │ └── run.sh
│ ├── busboy/
│ │ ├── LICENSE
│ │ ├── formdata-test.js
│ │ ├── issue-3676.js
│ │ ├── issue-3760.js
│ │ ├── issue-4660.js
│ │ ├── issue-4671.js
│ │ ├── test-parser.js
│ │ ├── test-types-multipart-charsets.js
│ │ └── test-types-multipart.js
│ ├── cache/
│ │ ├── cache.js
│ │ ├── cachestorage.js
│ │ └── get-field-values.js
│ ├── cache-interceptor/
│ │ ├── cache-store-test-utils.js
│ │ ├── cache-tests-worker.mjs
│ │ ├── cache-tests.mjs
│ │ ├── cache-utils.js
│ │ ├── memory-cache-store-tests.js
│ │ ├── sqlite-cache-store-tests.js
│ │ └── utils.js
│ ├── client-connect.js
│ ├── client-head-reset-override.js
│ ├── client-idempotent-body.js
│ ├── client-keep-alive.js
│ ├── client-node-max-header-size.js
│ ├── client-pipeline.js
│ ├── client-pipelining.js
│ ├── client-post.js
│ ├── client-reconnect.js
│ ├── client-request.js
│ ├── client-stream.js
│ ├── client-timeout.js
│ ├── client-unref.js
│ ├── client-upgrade.js
│ ├── client-wasm.js
│ ├── client-write-max-listeners.js
│ ├── client.js
│ ├── close-and-destroy.js
│ ├── connect-abort.js
│ ├── connect-errconnect.js
│ ├── connect-pre-shared-session.js
│ ├── connect-timeout.js
│ ├── content-length.js
│ ├── cookie/
│ │ ├── cookies.js
│ │ ├── global-headers.js
│ │ ├── is-ctl-excluding-htab.js
│ │ ├── npm-cookie.js
│ │ ├── to-imf-date.js
│ │ ├── validate-cookie-name.js
│ │ ├── validate-cookie-path.js
│ │ └── validate-cookie-value.js
│ ├── decorator-handler.js
│ ├── dispatcher.js
│ ├── env-http-proxy-agent-nodejs-bundle.js
│ ├── env-http-proxy-agent.js
│ ├── errors.js
│ ├── esm-wrapper.js
│ ├── eventsource/
│ │ ├── eventsource-attributes.js
│ │ ├── eventsource-close.js
│ │ ├── eventsource-connect.js
│ │ ├── eventsource-constructor-stringify.js
│ │ ├── eventsource-constructor.js
│ │ ├── eventsource-custom-dispatcher.js
│ │ ├── eventsource-message.js
│ │ ├── eventsource-properties.js
│ │ ├── eventsource-reconnect.js
│ │ ├── eventsource-redirecting.js
│ │ ├── eventsource-request-status-error.js
│ │ ├── eventsource-stream-bom.js
│ │ ├── eventsource-stream-parse-line.js
│ │ ├── eventsource-stream-process-event.js
│ │ ├── eventsource-stream.js
│ │ ├── eventsource.js
│ │ └── util.js
│ ├── examples.js
│ ├── fetch/
│ │ ├── 401-statuscode-no-infinite-loop.js
│ │ ├── 407-statuscode-window-null.js
│ │ ├── abort.js
│ │ ├── abort2.js
│ │ ├── about-uri.js
│ │ ├── blob-uri.js
│ │ ├── bundle.js
│ │ ├── client-error-stack-trace.js
│ │ ├── client-fetch.js
│ │ ├── client-node-max-header-size.js
│ │ ├── content-length.js
│ │ ├── cookies.js
│ │ ├── data-uri.js
│ │ ├── encoding.js
│ │ ├── exiting.js
│ │ ├── export-env-proxy-agent.js
│ │ ├── fetch-leak.js
│ │ ├── fetch-timeouts.js
│ │ ├── fetch-url-after-redirect.js
│ │ ├── fire-and-forget.js
│ │ ├── formdata-inspect-custom.js
│ │ ├── formdata.js
│ │ ├── general.js
│ │ ├── headers-case.js
│ │ ├── headers-inspect-custom.js
│ │ ├── headers.js
│ │ ├── headerslist-sortedarray.js
│ │ ├── http2.js
│ │ ├── includes-credentials.js
│ │ ├── integrity.js
│ │ ├── issue-1447.js
│ │ ├── issue-1711.js
│ │ ├── issue-2009.js
│ │ ├── issue-2021.js
│ │ ├── issue-2171.js
│ │ ├── issue-2242.js
│ │ ├── issue-2294-patch-method.js
│ │ ├── issue-2318.js
│ │ ├── issue-2828.js
│ │ ├── issue-2898-comment.js
│ │ ├── issue-2898.js
│ │ ├── issue-3267.js
│ │ ├── issue-3334.js
│ │ ├── issue-3616.js
│ │ ├── issue-3624.js
│ │ ├── issue-3630.js
│ │ ├── issue-3767.js
│ │ ├── issue-4105.js
│ │ ├── issue-4627.js
│ │ ├── issue-4647.js
│ │ ├── issue-4789.js
│ │ ├── issue-4799.js
│ │ ├── issue-4836.js
│ │ ├── issue-4897.js
│ │ ├── issue-node-46525.js
│ │ ├── issue-rsshub-15532.js
│ │ ├── iterators.js
│ │ ├── long-lived-abort-controller.js
│ │ ├── max-listeners.js
│ │ ├── pull-dont-push.js
│ │ ├── readable-stream-from.js
│ │ ├── redirect-cross-origin-header.js
│ │ ├── redirect.js
│ │ ├── referrrer-policy.js
│ │ ├── relative-url.js
│ │ ├── request-inspect-custom.js
│ │ ├── request.js
│ │ ├── resource-timing.js
│ │ ├── response-inspect-custom.js
│ │ ├── response-json.js
│ │ ├── response.js
│ │ ├── spread.js
│ │ ├── user-agent.js
│ │ └── util.js
│ ├── fixed-queue.js
│ ├── fixtures/
│ │ ├── ca.pem
│ │ ├── cert.pem
│ │ ├── docker/
│ │ │ ├── dante/
│ │ │ │ ├── Dockerfile
│ │ │ │ └── danted.conf
│ │ │ └── docker-compose.yml
│ │ ├── duplicate-debug.js
│ │ ├── fetch.js
│ │ ├── interceptors/
│ │ │ └── retry-event-loop.js
│ │ ├── key.pem
│ │ ├── socks5-test-server.js
│ │ ├── undici.js
│ │ └── websocket.js
│ ├── fuzzing/
│ │ ├── client/
│ │ │ ├── client-fuzz-body.js
│ │ │ ├── client-fuzz-headers.js
│ │ │ ├── client-fuzz-options.js
│ │ │ └── index.js
│ │ ├── fuzzing.test.js
│ │ └── server/
│ │ ├── index.js
│ │ ├── server-fuzz-append-data.js
│ │ └── server-fuzz-split-data.js
│ ├── gc.js
│ ├── get-head-body.js
│ ├── h2c-client.js
│ ├── headers-as-array.js
│ ├── headers-crlf.js
│ ├── http-100.js
│ ├── http-req-destroy.js
│ ├── http2-abort.js
│ ├── http2-agent.js
│ ├── http2-alpn.js
│ ├── http2-body.js
│ ├── http2-connection.js
│ ├── http2-continue.js
│ ├── http2-dispatcher.js
│ ├── http2-goaway.js
│ ├── http2-instantiation.js
│ ├── http2-late-data.js
│ ├── http2-pseudo-headers.js
│ ├── http2-resume-null-request.js
│ ├── http2-stream.js
│ ├── http2-timeout.js
│ ├── http2-trailers.js
│ ├── http2-window-size.js
│ ├── https.js
│ ├── imports/
│ │ └── undici-import.ts
│ ├── inflight-and-close.js
│ ├── infra/
│ │ └── collect-a-sequence-of-code-points.js
│ ├── install.js
│ ├── interceptors/
│ │ ├── cache-async-store.js
│ │ ├── cache-query-params.js
│ │ ├── cache-revalidate-stale.js
│ │ ├── cache.js
│ │ ├── decompress.js
│ │ ├── deduplicate.js
│ │ ├── dns.js
│ │ ├── dump-interceptor.js
│ │ ├── redirect-cross-origin-fix.js
│ │ ├── redirect-issue-3803.js
│ │ ├── redirect.js
│ │ ├── response-error.js
│ │ └── retry.js
│ ├── invalid-headers.js
│ ├── ip-prioritization.js
│ ├── issue-1757.js
│ ├── issue-2065.js
│ ├── issue-2078.js
│ ├── issue-2283.js
│ ├── issue-2349.js
│ ├── issue-2590.js
│ ├── issue-3356.js
│ ├── issue-3410.js
│ ├── issue-3904.js
│ ├── issue-3934.js
│ ├── issue-3959.js
│ ├── issue-4244.js
│ ├── issue-4691.js
│ ├── issue-4806.js
│ ├── issue-4880.js
│ ├── issue-803.js
│ ├── issue-810.js
│ ├── jest/
│ │ ├── instanceof-error.test.js
│ │ ├── issue-1757.test.js
│ │ ├── mock-agent.test.js
│ │ ├── mock-scope.test.js
│ │ ├── parser-timeout.test.js
│ │ ├── test.js
│ │ └── util-timers.test.js
│ ├── max-headers.js
│ ├── max-response-size.js
│ ├── mock-agent.js
│ ├── mock-call-history-log.js
│ ├── mock-call-history.js
│ ├── mock-client.js
│ ├── mock-delayed-abort.js
│ ├── mock-errors.js
│ ├── mock-interceptor-unused-assertions.js
│ ├── mock-interceptor.js
│ ├── mock-pool.js
│ ├── mock-scope.js
│ ├── mock-utils.js
│ ├── no-strict-content-length.js
│ ├── node-fetch/
│ │ ├── LICENSE
│ │ ├── headers.js
│ │ ├── main.js
│ │ ├── mock.js
│ │ ├── request.js
│ │ ├── response.js
│ │ └── utils/
│ │ ├── dummy.txt
│ │ ├── read-stream.js
│ │ └── server.js
│ ├── node-platform-objects.js
│ ├── node-test/
│ │ ├── abort-controller.js
│ │ ├── abort-event-emitter.js
│ │ ├── agent.js
│ │ ├── async_hooks.js
│ │ ├── autoselectfamily.js
│ │ ├── balanced-pool.js
│ │ ├── ca-fingerprint.js
│ │ ├── client-abort.js
│ │ ├── client-connect.js
│ │ ├── client-dispatch.js
│ │ ├── client-errors.js
│ │ ├── debug.js
│ │ ├── diagnostics-channel/
│ │ │ ├── connect-error.js
│ │ │ ├── error.js
│ │ │ ├── get-h2.js
│ │ │ ├── get.js
│ │ │ ├── post-stream.js
│ │ │ └── post.js
│ │ ├── large-body.js
│ │ ├── tree.js
│ │ ├── unix.js
│ │ ├── util.js
│ │ └── validations.js
│ ├── parser-issues.js
│ ├── pipeline-pipelining.js
│ ├── pool-connection-error-memory-leak.js
│ ├── pool.js
│ ├── promises.js
│ ├── proxy-agent.js
│ ├── proxy.js
│ ├── readable.js
│ ├── redirect-pipeline.js
│ ├── redirect-request.js
│ ├── redirect-stream.js
│ ├── request-crlf.js
│ ├── request-signal.js
│ ├── request-timeout.js
│ ├── request-timeout2.js
│ ├── request.js
│ ├── retry-agent.js
│ ├── retry-handler.js
│ ├── retry-handler2.js
│ ├── round-robin-pool.js
│ ├── snapshot-recorder.js
│ ├── snapshot-redirect-interceptor.js
│ ├── snapshot-testing.js
│ ├── socket-back-pressure.js
│ ├── socket-timeout.js
│ ├── socks5-client.js
│ ├── socks5-proxy-agent.js
│ ├── socks5-utils.js
│ ├── stream-compat.js
│ ├── subresource-integrity/
│ │ ├── apply-algorithm-to-bytes.js
│ │ ├── bytes-match.js
│ │ ├── case-sensitive-match.js
│ │ ├── get-strongest-metadata.js
│ │ ├── is-valid-sri-hash-algorithm.js
│ │ └── parse-metadata.js
│ ├── sync-error-in-callback.js
│ ├── timers.js
│ ├── tls-cert-leak.js
│ ├── tls-session-reuse.js
│ ├── tls.js
│ ├── trailers.js
│ ├── types/
│ │ ├── agent.test-d.ts
│ │ ├── api.test-d.ts
│ │ ├── balanced-pool.test-d.ts
│ │ ├── cache-interceptor.test-d.ts
│ │ ├── cache-storage.test-d.ts
│ │ ├── client.test-d.ts
│ │ ├── connector.test-d.ts
│ │ ├── diagnostics-channel.test-d.ts
│ │ ├── dispatcher.events.test-d.ts
│ │ ├── dispatcher.test-d.ts
│ │ ├── dns-interceptor.test-d.ts
│ │ ├── env-http-proxy-agent.test-d.ts
│ │ ├── errors.test-d.ts
│ │ ├── event-source-d.ts
│ │ ├── fetch.test-d.ts
│ │ ├── formdata.test-d.ts
│ │ ├── global-dispatcher.test-d.ts
│ │ ├── header.test-d.ts
│ │ ├── index.test-d.ts
│ │ ├── mock-agent.test-d.ts
│ │ ├── mock-call-history.test-d.ts
│ │ ├── mock-client.test-d.ts
│ │ ├── mock-errors.test-d.ts
│ │ ├── mock-interceptor.test-d.ts
│ │ ├── mock-pool.test-d.ts
│ │ ├── pool.test-d.ts
│ │ ├── proxy-agent.test-d.ts
│ │ ├── readable.test-d.ts
│ │ ├── retry-agent.test-d.ts
│ │ ├── retry-handler.test-d.ts
│ │ ├── snapshot-agent.test-d.ts
│ │ ├── util.test-d.ts
│ │ └── websocket.test-d.ts
│ ├── upgrade-crlf.js
│ ├── util.js
│ ├── utils/
│ │ ├── async-iterators.js
│ │ ├── date.js
│ │ ├── esm-wrapper.mjs
│ │ ├── event-loop-blocker.js
│ │ ├── formdata.js
│ │ ├── hello-world-server.js
│ │ ├── node-http.js
│ │ ├── redirecting-servers.js
│ │ └── stream.js
│ ├── web-platform-tests/
│ │ ├── expectation.json
│ │ ├── runner/
│ │ │ ├── certs/
│ │ │ │ ├── cacert.key
│ │ │ │ ├── cacert.pem
│ │ │ │ ├── web-platform.test.key
│ │ │ │ └── web-platform.test.pem
│ │ │ ├── config.json
│ │ │ ├── test-runner.mjs
│ │ │ └── utils.mjs
│ │ └── wpt-runner.mjs
│ ├── webidl/
│ │ ├── converters.js
│ │ ├── errors.js
│ │ ├── helpers.js
│ │ └── util.js
│ └── websocket/
│ ├── client-received-masked-frame.js
│ ├── close-invalid-status-code.js
│ ├── close-invalid-utf-8.js
│ ├── close.js
│ ├── constructor.js
│ ├── continuation-frames.js
│ ├── custom-headers.js
│ ├── diagnostics-channel-handshake-response.js
│ ├── diagnostics-channel-open-close.js
│ ├── diagnostics-channel-ping-pong.js
│ ├── events.js
│ ├── fragments.js
│ ├── frame.js
│ ├── issue-2679.js
│ ├── issue-2844.js
│ ├── issue-2859.js
│ ├── issue-3202.js
│ ├── issue-3506.js
│ ├── issue-3546.js
│ ├── issue-3697-2399493917.js
│ ├── issue-4273.js
│ ├── issue-4487.js
│ ├── issue-4628.js
│ ├── issue-4889.js
│ ├── messageevent.js
│ ├── opening-handshake.js
│ ├── permessage-deflate-limit.js
│ ├── permessage-deflate-windowbits.js
│ ├── ping-pong.js
│ ├── ping-util.js
│ ├── receive.js
│ ├── receiver-unit.js
│ ├── send-mutable.js
│ ├── send.js
│ ├── stream/
│ │ └── readable-closed-after-close.js
│ ├── util.js
│ └── websocketinit.js
└── types/
├── README.md
├── agent.d.ts
├── api.d.ts
├── balanced-pool.d.ts
├── cache-interceptor.d.ts
├── cache.d.ts
├── client-stats.d.ts
├── client.d.ts
├── connector.d.ts
├── content-type.d.ts
├── cookies.d.ts
├── diagnostics-channel.d.ts
├── dispatcher.d.ts
├── env-http-proxy-agent.d.ts
├── errors.d.ts
├── eventsource.d.ts
├── fetch.d.ts
├── formdata.d.ts
├── global-dispatcher.d.ts
├── global-origin.d.ts
├── h2c-client.d.ts
├── handlers.d.ts
├── header.d.ts
├── index.d.ts
├── interceptors.d.ts
├── mock-agent.d.ts
├── mock-call-history.d.ts
├── mock-client.d.ts
├── mock-errors.d.ts
├── mock-interceptor.d.ts
├── mock-pool.d.ts
├── patch.d.ts
├── pool-stats.d.ts
├── pool.d.ts
├── proxy-agent.d.ts
├── readable.d.ts
├── retry-agent.d.ts
├── retry-handler.d.ts
├── round-robin-pool.d.ts
├── snapshot-agent.d.ts
├── socks5-proxy-agent.d.ts
├── util.d.ts
├── utility.d.ts
├── webidl.d.ts
└── websocket.d.ts
================================================
FILE CONTENTS
================================================
================================================
FILE: .c8rc.json
================================================
{
"all": true,
"reporter": [
"lcov",
"text",
"html",
"text-summary"
],
"include": [
"lib/**/*.js",
"index.js"
]
}
================================================
FILE: .dockerignore
================================================
# Ignore everything but the stuff following the `*` with the `!`
# See https://docs.docker.com/engine/reference/builder/#dockerignore-file
*
!package.json
!lib
!deps
!build
================================================
FILE: .editorconfig
================================================
# https://editorconfig.org/
root = true
[*]
indent_size = 2
indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true
================================================
FILE: .github/ISSUE_TEMPLATE/bug-report.md
================================================
---
name: Bug Report
about: Report an issue
title: ''
labels: bug
assignees: ''
---
## Bug Description
## Reproducible By
## Expected Behavior
## Logs & Screenshots
## Environment
### Additional context
================================================
FILE: .github/ISSUE_TEMPLATE/feature-request.md
================================================
---
name: Feature Request
about: Make a suggestion on a feature or improvement for the project
title: ''
labels: enhancement
assignees: ''
---
## This would solve...
## The implementation should look like...
## I have also considered...
## Additional context
================================================
FILE: .github/PULL_REQUEST_TEMPLATE.md
================================================
## This relates to...
## Rationale
## Changes
### Features
### Bug Fixes
### Breaking Changes and Deprecations
## Status
- [ ] I have read and agreed to the [Developer's Certificate of Origin][cert]
- [ ] Tested
- [ ] Benchmarked (**optional**)
- [ ] Documented
- [ ] Review ready
- [ ] In review
- [ ] Merge ready
[cert]: https://github.com/nodejs/undici/blob/main/CONTRIBUTING.md#developers-certificate-of-origin
================================================
FILE: .github/dependabot.yml
================================================
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "monthly"
open-pull-requests-limit: 10
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "weekly"
open-pull-requests-limit: 10
- package-ecosystem: "npm"
directory: /docs
schedule:
interval: "weekly"
open-pull-requests-limit: 10
- package-ecosystem: "npm"
directory: /benchmarks
schedule:
interval: "weekly"
open-pull-requests-limit: 10
- package-ecosystem: docker
directory: /build
schedule:
interval: daily
================================================
FILE: .github/workflows/autobahn.yml
================================================
name: Autobahn
on:
workflow_dispatch:
workflow_call:
inputs:
node-version:
default: '24'
type: string
pull_request:
paths:
- '.github/workflows/autobahn.yml'
- 'lib/web/websocket/**'
- 'test/autobahn/**'
permissions:
contents: read
jobs:
autobahn:
name: Autobahn Test Suite
runs-on: ubuntu-latest
container: node:24
services:
fuzzingserver:
image: crossbario/autobahn-testsuite:latest
ports:
- '9001:9001'
options: --name fuzzingserver
volumes:
- ${{ github.workspace }}/test/autobahn/config:/config
- ${{ github.workspace }}/test/autobahn/reports:/reports
steps:
- name: Checkout Repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
clean: false
- name: Restart Autobahn Server
# Restart service after volumes have been checked out
uses: docker://docker
with:
args: docker restart --time 0 --signal=SIGKILL fuzzingserver
- name: Setup Node.js@${{ inputs.node-version }}
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
with:
node-version: ${{ inputs.node-version }}
- name: Run Autobahn Test Suite
run: npm run test:websocket:autobahn
env:
FUZZING_SERVER_URL: ws://fuzzingserver:9001
LOG_ON_ERROR: false
- name: Report CI
id: report-ci
run: npm run test:websocket:autobahn:report
env:
FAIL_ON_ERROR: true
================================================
FILE: .github/workflows/backport.yml
================================================
name: Backport
on:
pull_request_target:
types:
- closed
- labeled
jobs:
backport:
name: Backport
runs-on: ubuntu-latest
if: >
github.event.pull_request.merged
&& (
github.event.action == 'closed'
|| (
github.event.action == 'labeled'
&& contains(github.event.label.name, 'backport')
)
)
permissions:
pull-requests: write
contents: write
steps:
- name: Backport
uses: tibdex/backport@9565281eda0731b1d20c4025c43339fb0a23812e # v2.0.4
id: backport
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
================================================
FILE: .github/workflows/bench.yml
================================================
name: Benchmarks
on:
push:
branches:
- main
- current
- next
- 'v*'
pull_request:
permissions:
contents: read
jobs:
benchmark_current:
name: benchmark current
runs-on: ubuntu-latest
steps:
- name: Checkout Repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
ref: ${{ github.base_ref }}
- name: Setup Node
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
with:
node-version: 'lts/*'
- name: Install Modules for undici
run: npm i --ignore-scripts --omit=dev
- name: Install Modules for Benchmarks
run: npm i
working-directory: ./benchmarks
- name: Run Benchmark
run: npm run bench
working-directory: ./benchmarks
benchmark_branch:
name: benchmark branch
runs-on: ubuntu-latest
steps:
- name: Checkout Repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Setup Node
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
with:
node-version: 'lts/*'
- name: Install Modules for undici
run: npm i --ignore-scripts --omit=dev
- name: Install Modules for Benchmarks
run: npm i
working-directory: ./benchmarks
- name: Run Benchmark
run: npm run bench
working-directory: ./benchmarks
benchmark_post_current:
name: benchmark (sending data) current
runs-on: ubuntu-latest
steps:
- name: Checkout Repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
ref: ${{ github.base_ref }}
- name: Setup Node
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
with:
node-version: 'lts/*'
- name: Install Modules for undici
run: npm i --ignore-scripts --omit=dev
- name: Install Modules for Benchmarks
run: npm i
working-directory: ./benchmarks
- name: Run Benchmark
run: npm run bench-post
working-directory: ./benchmarks
benchmark_post_branch:
name: benchmark (sending data) branch
runs-on: ubuntu-latest
steps:
- name: Checkout Repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Setup Node
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
with:
node-version: 'lts/*'
- name: Install Modules for undici
run: npm i --ignore-scripts --omit=dev
- name: Install Modules for Benchmarks
run: npm i
working-directory: ./benchmarks
- name: Run Benchmark
run: npm run bench-post
working-directory: ./benchmarks
benchmark_current_h2:
name: benchmark current h2
runs-on: ubuntu-latest
steps:
- name: Checkout Repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
ref: ${{ github.base_ref }}
- name: Setup Node
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
with:
node-version: 'lts/*'
- name: Install Modules for undici
run: npm i --ignore-scripts --omit=dev
- name: Install Modules for Benchmarks
run: npm i
working-directory: ./benchmarks
- name: Run Benchmark
run: npm run bench:h2
working-directory: ./benchmarks
benchmark_branch_h2:
name: benchmark branch h2
runs-on: ubuntu-latest
steps:
- name: Checkout Repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Setup Node
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
with:
node-version: 'lts/*'
- name: Install Modules for undici
run: npm i --ignore-scripts --omit=dev
- name: Install Modules for Benchmarks
run: npm i
working-directory: ./benchmarks
- name: Run Benchmark
run: npm run bench:h2
working-directory: ./benchmarks
================================================
FILE: .github/workflows/ci.yml
================================================
name: CI
on:
push:
branches:
- main
- current
- next
- 'v*'
pull_request:
permissions:
contents: read
jobs:
dependency-review:
if: ${{ github.event_name == 'pull_request' }}
runs-on: ubuntu-latest
steps:
- name: Harden Runner
uses: step-security/harden-runner@e3f713f2d8f53843e71c69a996d56f51aa9adfb9 # v2.14.1
with:
egress-policy: audit
- name: Checkout Repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Dependency Review
uses: actions/dependency-review-action@40c09b7dc99638e5ddb0bfd91c1673effc064d8a # v4.8.1
lint:
name: Lint
runs-on: ubuntu-latest
steps:
- name: Checkout Repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Setup Node.js
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
with:
# Using `latest` as `lts` could point to previous major version.
# Different versions of Node.js can cause different linting results
# (e.g. dependening on process.getBuiltinModule())
node-version: 'latest'
- name: Install dependencies
run: npm install
- name: Lint
run: npm run lint
test:
name: Test ${{ matrix.node-version == '24' && matrix.runs-on == 'ubuntu-latest' && 'and Coverage ' || ''}}with Node.js ${{ matrix.node-version }} on ${{ matrix.runs-on }}
strategy:
fail-fast: false
max-parallel: 0
matrix:
node-version: ['20', '22', '24', '25']
runs-on: ['ubuntu-latest', 'windows-latest', 'macos-latest']
exclude:
- node-version: '20'
runs-on: windows-latest
uses: ./.github/workflows/nodejs.yml
with:
# Disable coverage on Node.js 25 until https://github.com/nodejs/node/issues/61971 is resolved.
codecov: ${{ matrix.node-version == '24' && matrix.runs-on == 'ubuntu-latest' }}
node-version: ${{ matrix.node-version }}
runs-on: ${{ matrix.runs-on }}
secrets: inherit
test-with-no-wasm-simd:
name: Test with Node.js ${{ matrix.node-version }} on ${{ matrix.runs-on }} with WASM SIMD disabled
strategy:
fail-fast: false
max-parallel: 0
matrix:
node-version: ['24', '25']
runs-on: ['ubuntu-latest']
uses: ./.github/workflows/nodejs.yml
with:
node-version: ${{ matrix.node-version }}
runs-on: ${{ matrix.runs-on }}
no-wasm-simd: '1'
secrets: inherit
test-without-intl:
name: Test with Node.js ${{ matrix.node-version }} compiled --without-intl
strategy:
fail-fast: false
max-parallel: 0
matrix:
node-version: ['20', '22', '24', '25']
runs-on: ubuntu-latest
timeout-minutes: 120
steps:
- name: Checkout Repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
submodules: recursive
# Setup node, install deps, and build undici prior to building icu-less node and testing
- name: Setup Node.js@${{ matrix.node-version }}
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
with:
node-version: ${{ matrix.node-version }}
- name: Install dependencies
run: npm install
- name: Build undici
run: npm run build:node
- name: Determine latest release
id: release
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
with:
result-encoding: string
script: |
const req = await fetch('https://nodejs.org/download/release/index.json')
const releases = await req.json()
const latest = releases.find((r) => r.version.startsWith('v${{ matrix.node-version }}'))
return latest.version
- name: Download and extract source
run: curl https://nodejs.org/download/release/${{ steps.release.outputs.result }}/node-${{ steps.release.outputs.result }}.tar.xz | tar xfJ -
- name: Install ninja
run: sudo apt-get install ninja-build
- name: ccache
uses: hendrikmuhs/ccache-action@bfa03e1de4d7f7c3e80ad9109feedd05c4f5a716 #v1.2.19
with:
key: node${{ matrix.node-version }}
- name: Build node
working-directory: ./node-${{ steps.release.outputs.result }}
run: |
export CC="ccache gcc"
export CXX="ccache g++"
./configure --without-intl --ninja --prefix=./final
make
make install
echo "$(pwd)/final/bin" >> $GITHUB_PATH
- name: Print version information
run: |
echo OS: $(node -p "os.version()")
echo Node.js: $(node --version)
echo "Node.js built-in dependencies: $(node -p "'\r\n' + (Object.entries(process.versions).map(([k, v], i, arr) => (i !== arr.length - 1 ? '├──' : '└──') + k + '@' + v)).join('\r\n')")"
echo npm: $(npm --version)
echo git: $(git --version)
echo icu config: $(node -e "console.log(process.config)" | grep icu)
- name: Configure hosts file for WPT (Windows)
if: runner.os == 'Windows'
run: |
cd ${{ github.workspace }}\test\web-platform-tests\wpt
python wpt make-hosts-file | Out-File $env:SystemRoot\System32\drivers\etc\hosts -Encoding ascii -Append
shell: powershell
- name: Configure hosts file for WPT (Unix)
if: runner.os != 'Windows'
run: |
cd ${{ github.workspace }}/test/web-platform-tests/wpt
python3 wpt make-hosts-file | sudo tee -a /etc/hosts
- name: Run tests
run: npm run test:javascript:without-intl
test-without-ssl:
name: Test with Node.js ${{ matrix.node-version }} compiled --without-ssl
strategy:
fail-fast: false
max-parallel: 0
matrix:
node-version: ['20', '22', '24', '25']
runs-on: ubuntu-latest
timeout-minutes: 120
steps:
- name: Checkout Repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
submodules: recursive
# Setup node, install deps, and build undici prior to building icu-less node and testing
- name: Setup Node.js@${{ matrix.node-version }}
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
with:
node-version: ${{ matrix.node-version }}
- name: Install dependencies
run: npm install
- name: Build undici
run: npm run build:node
- name: Determine latest release
id: release
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
with:
result-encoding: string
script: |
const req = await fetch('https://nodejs.org/download/release/index.json')
const releases = await req.json()
const latest = releases.find((r) => r.version.startsWith('v${{ matrix.node-version }}'))
return latest.version
- name: Download and extract source
run: curl https://nodejs.org/download/release/${{ steps.release.outputs.result }}/node-${{ steps.release.outputs.result }}.tar.xz | tar xfJ -
- name: Install ninja
run: sudo apt-get install ninja-build
- name: ccache
uses: hendrikmuhs/ccache-action@bfa03e1de4d7f7c3e80ad9109feedd05c4f5a716 #v1.2.19
with:
key: node${{ matrix.node-version }}
- name: Build node
working-directory: ./node-${{ steps.release.outputs.result }}
run: |
export CC="ccache gcc"
export CXX="ccache g++"
./configure --without-ssl --ninja --prefix=./final
make
make install
echo "$(pwd)/final/bin" >> $GITHUB_PATH
- name: Print version information
run: |
echo OS: $(node -p "os.version()")
echo Node.js: $(node --version)
echo "Node.js built-in dependencies: $(node -p "'\r\n' + (Object.entries(process.versions).map(([k, v], i, arr) => (i !== arr.length - 1 ? '├──' : '└──') + k + '@' + v)).join('\r\n')")"
echo npm: $(npm --version)
echo git: $(git --version)
echo icu config: $(node -e "console.log(process.config)" | grep icu)
- name: Try loading Node.js without Crypto
run: node index.js
test-fuzzing:
name: Fuzzing
runs-on: ubuntu-latest
steps:
- name: Checkout Repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Setup Node.js
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
with:
node-version: lts/*
- name: Install dependencies
run: npm install
- name: Run fuzzing tests
run: npm run test:fuzzing
test-shared-builtin:
name: Test with Node.js ${{ matrix.node-version }} compiled --shared-builtin-undici/undici-path
uses: ./.github/workflows/nodejs-shared.yml
strategy:
fail-fast: false
max-parallel: 0
matrix:
node-version: ['24', '25']
runs-on: ['ubuntu-latest']
with:
node-version: ${{ matrix.node-version }}
test-types:
name: Test TypeScript types
timeout-minutes: 15
runs-on: ubuntu-latest
steps:
- name: Checkout Repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Setup Node.js
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
with:
node-version: lts/*
- name: Install dependencies
run: npm install
- name: Run typings tests
run: npm run test:typescript
automerge:
if: >
github.event_name == 'pull_request' && github.event.pull_request.user.login == 'dependabot[bot]'
needs:
- dependency-review
- test
- test-types
- test-with-no-wasm-simd
- test-without-intl
- test-fuzzing
- lint
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
steps:
- name: Merge Dependabot PR
uses: fastify/github-action-merge-dependabot@1b2ed42db8f9d81a46bac83adedfc03eb5149dff # v3.11.2
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
================================================
FILE: .github/workflows/codeql.yml
================================================
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
#
# ******** NOTE ********
# We have attempted to detect the languages in your repository. Please check
# the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages.
#
name: "CodeQL"
on:
push:
branches: ["main"]
pull_request:
# The branches below must be a subset of the branches above
branches: ["main"]
schedule:
- cron: "0 0 * * 1"
permissions:
contents: read
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
security-events: write
strategy:
fail-fast: false
matrix:
language: ["javascript", "typescript"]
# CodeQL supports [ $supported-codeql-languages ]
# Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support
steps:
- name: Harden Runner
uses: step-security/harden-runner@e3f713f2d8f53843e71c69a996d56f51aa9adfb9 # v2.14.1
with:
egress-policy: audit
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@9e907b5e64f6b83e7804b09294d44122997950d6 # v2.3.3
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@9e907b5e64f6b83e7804b09294d44122997950d6 # v2.3.3
# ℹ️ Command-line programs to run using the OS shell.
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
# If the Autobuild fails above, remove it and uncomment the following three lines.
# modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance.
# - run: |
# echo "Run, Build Application using script"
# ./location_of_script_within_repo/buildscript.sh
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@9e907b5e64f6b83e7804b09294d44122997950d6 # v2.3.3
with:
category: "/language:${{matrix.language}}"
================================================
FILE: .github/workflows/nodejs-nightly.yml
================================================
name: Node.js Nightly
on:
workflow_dispatch:
schedule:
- cron: "0 10 * * *"
permissions:
contents: read
jobs:
test:
name: Test with Node.js ${{ matrix.node-version }} on ${{ matrix.runs-on }}
if: github.repository == 'nodejs/undici'
strategy:
fail-fast: false
max-parallel: 0
matrix:
node-version: ['25-nightly']
runs-on: [ubuntu-latest, windows-latest, macos-latest]
uses: ./.github/workflows/nodejs.yml
with:
node-version: ${{ matrix.node-version }}
runs-on: ${{ matrix.runs-on }}
secrets: inherit
autobahn:
if: github.repository == 'nodejs/undici'
uses: ./.github/workflows/autobahn.yml
with:
node-version: '25-nightly'
secrets: inherit
test-shared-builtin:
if: github.repository == 'nodejs/undici'
uses: ./.github/workflows/nodejs-shared.yml
with:
node-version: '25'
node-download-server-path: '/nightly'
report-failure:
if: ${{ always() && (needs.test.result == 'failure' || needs.test-shared-builtin.result == 'failure' || needs.autobahn.result == 'failure') }}
needs:
- test
- test-shared-builtin
- autobahn
runs-on: ubuntu-latest
permissions:
issues: write
steps:
- name: Create or update issue
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
with:
script: |
const ISSUE_TITLE = "Nightly tests are failing"
const actionRunUrl = "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
const issueContext = {
owner: context.repo.owner,
repo: context.repo.repo
}
let issue = (await github.rest.issues.listForRepo({
state: "open",
creator: "github-actions[bot]",
...issueContext
})).data.find((issue) => issue.title === ISSUE_TITLE)
if(!issue) {
issue = (await github.rest.issues.create({
title: ISSUE_TITLE,
body: `Tests against nightly failed, see: ${actionRunUrl}`,
...issueContext
})).data
}
================================================
FILE: .github/workflows/nodejs-shared.yml
================================================
name: Node.js compiled --shared-builtin-undici/undici-path CI
on:
workflow_call:
inputs:
node-version:
required: true
type: string
node-download-server-path:
required: false
type: string
default: '/release'
permissions:
contents: read
jobs:
test-shared-builtin:
name: Test with Node.js ${{ inputs.node-version }} compiled --shared-builtin-undici/undici-path
runs-on: ubuntu-latest
timeout-minutes: 120
steps:
# Checkout into a subdirectory otherwise Node.js tests will break due to finding Undici's package.json in a parent directory.
- name: Checkout Repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
path: ./undici
persist-credentials: false
# Setup node, install deps, and build undici prior to building node with `--shared-builtin-undici/undici-path` and testing
- name: Setup Node.js lts/*
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
with:
node-version: 'lts/*'
- name: Install dependencies
working-directory: ./undici
run: npm install
- name: Install wasi-libc
run: sudo apt-get install -y wasi-libc binaryen
- name: Build WASM
working-directory: ./undici
run: |
export EXTERNAL_PATH=${{ github.workspace }}/undici
export WASM_CC=clang
export WASM_CFLAGS='--target=wasm32-wasi --sysroot=/usr'
export WASM_LDFLAGS='-nodefaultlibs'
export WASM_LDLIBS='-lc'
node build/wasm.js
- name: Determine latest release for Node.js ${{ inputs.node-version }}
id: release
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
with:
result-encoding: string
script: |
const req = await fetch('https://nodejs.org/download${{ inputs.node-download-server-path }}/index.json')
const releases = await req.json()
const latest = releases.find((r) => r.version.startsWith('v${{ inputs.node-version }}'))
return latest.version
- name: Download and extract source for Node.js ${{ steps.release.outputs.result }}
run: curl https://nodejs.org/download${{ inputs.node-download-server-path }}/${{ steps.release.outputs.result }}/node-${{ steps.release.outputs.result }}.tar.xz | tar xfJ -
- name: Install ninja
run: sudo apt-get install ninja-build
- name: ccache
uses: hendrikmuhs/ccache-action@bfa03e1de4d7f7c3e80ad9109feedd05c4f5a716 #v1.2.19
with:
key: node(external_undici)${{ inputs.node-version }}
- name: Build node ${{ steps.release.outputs.result }} with --shared-builtin-undici/undici-path
working-directory: ./node-${{ steps.release.outputs.result }}
run: |
export CC="ccache gcc"
export CXX="ccache g++"
rm -rf deps/undici
./configure --shared-builtin-undici/undici-path ${{ github.workspace }}/undici/loader.js --ninja --prefix=./final
make
make install
echo "$(pwd)/final/bin" >> $GITHUB_PATH
- name: Print version information
run: |
echo OS: $(node -p "os.version()")
echo Node.js: $(node --version)
echo "Node.js built-in dependencies: $(node -p "'\r\n' + (Object.entries(process.versions).map(([k, v], i, arr) => (i !== arr.length - 1 ? '├──' : '└──') + k + '@' + v)).join('\r\n')")"
echo npm: $(npm --version)
echo git: $(git --version)
echo external config: $(node -e "console.log(process.config)" | grep NODE_SHARED_BUILTIN_UNDICI_UNDICI_PATH)
echo Node.js built-in undici version: $(node -p "process.versions.undici") # undefined for external Undici
- name: Run tests
working-directory: ./node-${{ steps.release.outputs.result }}
run: tools/test.py -p dots --flaky-tests=dontcare
================================================
FILE: .github/workflows/nodejs.yml
================================================
name: Node.js
on:
workflow_call:
inputs:
node-version:
required: true
type: string
runs-on:
required: true
type: string
codecov:
required: false
type: boolean
default: false
no-wasm-simd:
type: string
required: false
default: ''
permissions:
contents: read
jobs:
test:
name: Test ${{ inputs.codecov == true && 'and Coverage ' || '' }}with Node.js ${{ inputs.node-version }} on ${{ inputs.runs-on }}
timeout-minutes: 20
runs-on: ${{ inputs.runs-on }}
steps:
- name: Checkout Repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
submodules: recursive
- name: Setup Node.js@${{ inputs.node-version }}
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
with:
node-version: ${{ inputs.node-version }}
- name: Print version information
run: |
echo OS: $(node -p "os.version()")
echo Node.js: $(node --version)
echo "Node.js built-in dependencies: $(node -p "'\r\n' + (Object.entries(process.versions).map(([k, v], i, arr) => (i !== arr.length - 1 ? '├──' : '└──') + k + '@' + v)).join('\r\n')")"
echo npm: $(npm --version)
echo git: $(git --version)
- name: Install dependencies
run: npm install
- name: Print installed dependencies
run: npm ls --all
continue-on-error: true
- name: Configure hosts file for WPT (Windows)
if: runner.os == 'Windows'
run: |
cd ${{ github.workspace }}\test\web-platform-tests\wpt
python wpt make-hosts-file | Out-File $env:SystemRoot\System32\drivers\etc\hosts -Encoding ascii -Append
shell: powershell
- name: Configure hosts file for WPT (Unix)
if: runner.os != 'Windows'
run: |
cd ${{ github.workspace }}/test/web-platform-tests/wpt
python3 wpt make-hosts-file | sudo tee -a /etc/hosts
- name: Generate PEM files
run: npm run generate-pem
id: generate-pem
- name: Test unit
run: npm run test:unit
id: test-unit
env:
CI: true
NODE_V8_COVERAGE: ${{ inputs.codecov == true && './coverage/tmp' || '' }}
UNDICI_NO_WASM_SIMD: ${{ inputs['no-wasm-simd'] }}
- name: Test node-test
run: npm run test:node-test
id: test-node-test
env:
CI: true
NODE_V8_COVERAGE: ${{ inputs.codecov == true && './coverage/tmp' || '' }}
UNDICI_NO_WASM_SIMD: ${{ inputs['no-wasm-simd'] }}
- name: Test fetch
run: npm run test:fetch
id: test-fetch
env:
CI: true
NODE_V8_COVERAGE: ${{ inputs.codecov == true && './coverage/tmp' || '' }}
UNDICI_NO_WASM_SIMD: ${{ inputs['no-wasm-simd'] }}
- name: Test node-fetch
run: npm run test:node-fetch
id: test-node-fetch
env:
CI: true
NODE_V8_COVERAGE: ${{ inputs.codecov == true && './coverage/tmp' || '' }}
UNDICI_NO_WASM_SIMD: ${{ inputs['no-wasm-simd'] }}
- name: Test cache
run: npm run test:cache
id: test-cache
env:
CI: true
NODE_V8_COVERAGE: ${{ inputs.codecov == true && './coverage/tmp' || '' }}
UNDICI_NO_WASM_SIMD: ${{ inputs['no-wasm-simd'] }}
- name: Test cache-interceptor ${{ inputs.node-version != '20' && 'with' || 'without' }} sqlite
run: npm run test:cache-interceptor
id: test-cache-interceptor
env:
CI: true
NODE_OPTIONS: ${{ inputs.node-version != '20' && '--experimental-sqlite' || '' }}
NODE_V8_COVERAGE: ${{ inputs.codecov == true && './coverage/tmp' || '' }}
UNDICI_NO_WASM_SIMD: ${{ inputs['no-wasm-simd'] }}
- name: Test cache-tests
run: npm run test:cache-tests
id: test-cache-tests
env:
CI: true
NODE_V8_COVERAGE: ${{ inputs.codecov == true && './coverage/tmp' || '' }}
UNDICI_NO_WASM_SIMD: ${{ inputs['no-wasm-simd'] }}
- name: Test cookies
run: npm run test:cookies
id: test-cookies
env:
CI: true
NODE_V8_COVERAGE: ${{ inputs.codecov == true && './coverage/tmp' || '' }}
UNDICI_NO_WASM_SIMD: ${{ inputs['no-wasm-simd'] }}
- name: Test interceptors
run: npm run test:interceptors
id: test-interceptors
env:
CI: true
NODE_V8_COVERAGE: ${{ inputs.codecov == true && './coverage/tmp' || '' }}
UNDICI_NO_WASM_SIMD: ${{ inputs['no-wasm-simd'] }}
- name: Test eventsource
run: npm run test:eventsource
id: test-eventsource
env:
CI: true
NODE_V8_COVERAGE: ${{ inputs.codecov == true && './coverage/tmp' || '' }}
UNDICI_NO_WASM_SIMD: ${{ inputs['no-wasm-simd'] }}
- name: Test infra
run: npm run test:infra
id: test-infra
env:
CI: true
NODE_V8_COVERAGE: ${{ inputs.codecov == true && './coverage/tmp' || '' }}
UNDICI_NO_WASM_SIMD: ${{ inputs['no-wasm-simd'] }}
- name: Test subresource-integrity
run: npm run test:subresource-integrity
id: test-subresource-integrity
env:
CI: true
NODE_V8_COVERAGE: ${{ inputs.codecov == true && './coverage/tmp' || '' }}
UNDICI_NO_WASM_SIMD: ${{ inputs['no-wasm-simd'] }}
- name: Test websocket
run: npm run test:websocket
id: test-websocket
env:
CI: true
NODE_V8_COVERAGE: ${{ inputs.codecov == true && './coverage/tmp' || '' }}
UNDICI_NO_WASM_SIMD: ${{ inputs['no-wasm-simd'] }}
- name: Test jest
run: npm run test:jest
id: test-jest
env:
CI: true
NODE_V8_COVERAGE: ''
UNDICI_NO_WASM_SIMD: ${{ inputs['no-wasm-simd'] }}
- name: Test wpt
run: npm run test:wpt
id: test-wpt
env:
CI: true
NODE_V8_COVERAGE: ${{ inputs.codecov == true && './coverage/tmp' || '' }}
UNDICI_NO_WASM_SIMD: ${{ inputs['no-wasm-simd'] }}
- name: Coverage summary
if: inputs.codecov == true
run: npm run coverage:report:ci
id: prepare-coverage-report
- name: Upload coverage report to Codecov
if: inputs.codecov == true
uses: codecov/codecov-action@5a1091511ad55cbe89839c7260b706298ca349f7 # v5.5.1
with:
token: ${{ secrets.CODECOV_TOKEN }}
================================================
FILE: .github/workflows/release-create-pr.yml
================================================
name: Create release PR
permissions:
contents: read
on:
workflow_dispatch:
inputs:
version:
description: 'The version number to release (has priority over release_type)'
type: string
release_type:
description: Type of release
type: choice
default: patch
options:
- patch
- minor
- major
jobs:
create-pr:
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
outputs:
version: ${{ steps.bump.outputs.version }}
steps:
- name: Checkout Repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
id: checkout
with:
persist-credentials: true
- name: Setup Node.js
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
id: setup-node
with:
node-version: 'lts/*'
- name: Git Config
id: git-config
run: |
git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com"
git config --global user.name "github-actions[bot]"
- name: Change version number and push
id: bump
run: |
npm version ${{ inputs.version || inputs.release_type }} --git-tag-version=false
VERSION=`jq -r ".version" package.json`
RELEASE_BRANCH="release/v$VERSION"
git add -u
git commit -m "Bumped v$VERSION"
git push origin "HEAD:$RELEASE_BRANCH"
echo "version=$VERSION" >> $GITHUB_OUTPUT
- name: Create PR
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
id: create-pr
with:
script: |
const defaultBranch = "${{ github.event.repository.default_branch }}"
const versionTag = "v${{ steps.bump.outputs.version }}"
const commitHash = "${{ github.sha }}"
await require('./scripts/release').generatePr({ github, context, defaultBranch, versionTag, commitHash })
================================================
FILE: .github/workflows/release.yml
================================================
name: Release undici and undici-types on NPM and create GitHub Release
on:
push:
branches:
- main
paths:
- package.json
permissions:
contents: read
jobs:
determine-release-version:
runs-on: ubuntu-latest
outputs:
release-version: ${{ steps.determine-release-version.outputs.result }}
steps:
- name: Checkout Repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
id: checkout
with:
persist-credentials: false
- name: Determine release version
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
id: determine-release-version
with:
result-encoding: string
script: |
const { owner, repo } = context.repo
const version = require("./package.json").version
const versionTag = `v${version}`
const { data: releases } = await github.rest.repos.listReleases({
owner,
repo
})
const previousRelease = releases.find((r) => r.tag_name.startsWith('v7'))
if (versionTag !== previousRelease?.tag_name) {
return versionTag
}
release:
runs-on: ubuntu-latest
needs: determine-release-version
if: ${{ startsWith(needs.determine-release-version.outputs.release-version, 'v') }}
permissions:
contents: write
id-token: write
environment: release
steps:
- name: Checkout Repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
id: checkout
with:
persist-credentials: true
- name: Setup Node.js
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
with:
node-version: 'lts/*'
registry-url: 'https://registry.npmjs.org'
- name: Install globally latest npm
run: npm install -g npm@latest
id: install-globally-latest-npm
- name: Install dependencies
run: npm install
id: install-dependencies
- name: Publish undici on NPM
run: npm publish --access public
id: npm-publish-undici
- name: Generate Types Package
run: node scripts/generate-undici-types-package-json.js
id: generate-types-package
- name: Publish undici-types on NPM
run: npm publish
id: npm-publish-undici-types
working-directory: './types'
- name: Create GitHub release
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
id: create-gh-release
with:
script: |
const defaultBranch = "${{ github.event.repository.default_branch }}"
const versionTag = "${{ needs.determine-release-version.outputs.release-version }}"
const commitHash = "${{ github.sha }}"
await require('./scripts/release').release({ github, context, defaultBranch, versionTag, commitHash })
================================================
FILE: .github/workflows/scorecard.yml
================================================
# This workflow uses actions that are not certified by GitHub. They are provided
# by a third-party and are governed by separate terms of service, privacy
# policy, and support documentation.
name: Scorecard supply-chain security
on:
# For Branch-Protection check. Only the default branch is supported. See
# https://github.com/ossf/scorecard/blob/main/docs/checks.md#branch-protection
branch_protection_rule:
# To guarantee Maintained check is occasionally updated. See
# https://github.com/ossf/scorecard/blob/main/docs/checks.md#maintained
schedule:
- cron: '16 10 * * 2'
push:
branches: [ "main" ]
# Declare default permissions as read only.
permissions: read-all
jobs:
analysis:
name: Scorecard analysis
runs-on: ubuntu-latest
permissions:
# Needed to upload the results to code-scanning dashboard.
security-events: write
# Needed to publish results and get a badge (see publish_results below).
id-token: write
steps:
- name: "Checkout code"
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: "Run analysis"
uses: ossf/scorecard-action@4eaacf0543bb3f2c246792bd56e8cdeffafb205a # v2.4.3
with:
results_file: results.sarif
results_format: sarif
publish_results: true
# Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF
# format to the repository Actions tab.
- name: "Upload artifact"
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
with:
name: SARIF file
path: results.sarif
retention-days: 5
# Upload the results to GitHub's code scanning dashboard.
- name: "Upload to code-scanning"
uses: github/codeql-action/upload-sarif@9e907b5e64f6b83e7804b09294d44122997950d6 # v3.29.5
with:
sarif_file: results.sarif
================================================
FILE: .github/workflows/triggered-autobahn.yml
================================================
name: Autobahn
on:
pull_request:
types:
- labeled
jobs:
autobahn:
if: ${{ github.event.label.name == 'autobahn' }}
name: Autobahn Test Suite
uses: ./.github/workflows/autobahn.yml
================================================
FILE: .github/workflows/update-submodules.yml
================================================
name: Update Submodules
on:
workflow_dispatch:
schedule:
- cron: '0 0 * * *'
jobs:
update-wpt:
name: Update Submodules
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
steps:
- name: Checkout Repository
id: checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: true
- name: Git Config
id: git-config
run: |
git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com"
git config --global user.name "github-actions[bot]"
- name: Update Submodules
id: update-submodules
run: |
git submodule update --init --recursive --remote --merge
- name: Check for Changes
id: check-for-changes
run: |
if git diff --quiet; then
echo "no changes detected"
echo "change=false" >> "$GITHUB_OUTPUT"
else
echo "changes detected"
echo "change=true" >> "$GITHUB_OUTPUT"
fi
- name: Create Branch
id: create-branch
if: ${{ steps.check-for-changes.outputs.change == 'true'}}
run: |
git checkout -b submodules-update
git add .
git commit -m "chore: update Submodules"
git push --force --set-upstream origin submodules-update
- name: Create Pull Request
id: create-pr
if: ${{ steps.check-for-changes.outputs.change == 'true'}}
uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0
with:
base: main
branch: submodules-update
title: Update Submodules
body: Automated update of the Submodules
commit-message: "chore: update Submodules"
================================================
FILE: .gitignore
================================================
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
# nyc test coverage
.nyc_output
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# TypeScript v1 declaration files
typings/
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
# next.js build output
.next
# lock files
package-lock.json
yarn.lock
pnpm-lock.yaml
# IDE files
.idea
.vscode
*0x
*clinic*
# Fuzzing
corpus/
crash-*
fuzz-results-*.json
# Bundle output
undici-fetch.js
/test/imports/undici-import.js
# .npmrc has platform specific value for windows
.npmrc
.tap
# File generated by /test/request-timeout.js
test/request-timeout.10mb.bin
# Claude files
CLAUDE.md
.claude
# Ignore .pi
.pi
# Ignore .githuman
.githuman
================================================
FILE: .gitmodules
================================================
[submodule "test/web-platform-tests/wpt"]
path = test/web-platform-tests/wpt
url = https://github.com/web-platform-tests/wpt.git
[submodule "test/fixtures/cache-tests"]
path = test/fixtures/cache-tests
url = https://github.com/http-tests/cache-tests
================================================
FILE: .husky/pre-commit
================================================
npm run lint
================================================
FILE: .npmignore
================================================
*
!lib/**/*
!index.js
!index-fetch.js
# The wasm files are stored as base64 strings in the corresponding .js files
lib/llhttp/llhttp_simd.wasm
lib/llhttp/llhttp.wasm
!types/**/*
!index.d.ts
!docs/docs/**/*
!scripts/strip-comments.js
# File generated by /test/request-timeout.js
test/request-timeout.10mb.bin
================================================
FILE: CODE_OF_CONDUCT.md
================================================
# Code of Conduct
Undici is committed to upholding the Node.js Code of Conduct.
The Node.js Code of Conduct document can be found at
https://github.com/nodejs/admin/blob/main/CODE_OF_CONDUCT.md
================================================
FILE: CONTRIBUTING.md
================================================
# Contributing to Undici
* [Guides](#guides)
* [Update `llhttp`](#update-llhttp)
* [Lint](#lint)
* [Test](#test)
* [Coverage](#coverage)
* [Releases](#releases)
* [Update `WPTs`](#update-wpts)
* [Building for externally shared node builtins](#external-builds)
* [Benchmarks](#benchmarks)
* [Documentation](#documentation)
* [Developer's Certificate of Origin 1.1](#developers-certificate-of-origin)
* [Moderation Policy](#moderation-policy)
## Guides
This is a collection of guides on how to run and update `undici`, and how to run different parts of the project.
### Update `llhttp`
The HTTP parser used by `undici` is a WebAssembly build of [`llhttp`](https://github.com/nodejs/llhttp).
While the project itself provides a way to compile targeting WebAssembly, at the moment we embed the sources
directly and compile the module in `undici`.
The `deps/llhttp/include` folder contains the C header files, while the `deps/llhttp/src` folder contains
the C source files needed to compile the module.
The `lib/llhttp` folder contains the `.js` transpiled assets required to implement a parser.
The following are the steps required to perform an update.
#### Clone the [llhttp](https://github.com/nodejs/llhttp) project
```bash
git clone git@github.com:nodejs/llhttp.git
cd llhttp
```
#### Checkout a `llhttp` release
```bash
git checkout
```
#### Install the `llhttp` dependencies
```bash
npm i
```
#### Run the wasm build script
> This requires [docker](https://www.docker.com/) installed on your machine.
```bash
npm run build-wasm
```
#### Copy the sources to `undici`
```bash
cp build/wasm/*.js /lib/llhttp/
cp build/wasm/*.js.map /lib/llhttp/
cp build/wasm/*.d.ts /lib/llhttp/
cp src/native/api.c src/native/http.c build/c/llhttp.c /deps/llhttp/src/
cp src/native/api.h build/llhttp.h /deps/llhttp/include/
```
#### Build the WebAssembly module in `undici`
> This requires [docker](https://www.docker.com/) installed on your machine.
```bash
cd
npm run build:wasm
```
#### Commit the contents of lib/llhttp
Create a commit which includes all of the updated files in lib/llhttp.
### Update `WPTs`
`undici` runs a subset of the [`web-platform-tests`](https://github.com/web-platform-tests/wpt).
### Steps:
```bash
git submodule update --init --recursive
```
### Run the tests
Run the tests to ensure that any new failures are marked as such.
Before running the tests for the first time, you must setup the testing environment.
```bash
cd test/web-platform-tests
node wpt-runner.mjs setup
```
To run all tests:
```bash
npm run test:wpt
```
To run a subset of tests:
```bash
cd test/web-platform-tests
node wpt-runner.mjs run [filter] [filterb]
```
To run a single file:
```bash
cd test/web-platform-tests
node wpt-runner.mjs run /path/to/test
```
### Debugging
Verbose logging can be enabled by setting the [`NODE_DEBUG`](https://nodejs.org/api/cli.html#node_debugmodule) flag:
```bash
npx cross-env NODE_DEBUG=UNDICI_WPT node --run test:wpt
```
(`npx cross-env` can be omitted on Linux and Mac)
### Lint
```bash
npm run lint
```
### Test
```bash
npm run test
```
### Coverage
```bash
npm run coverage
```
### Issuing Releases
Release is automatic on commit to main which bumps the package.json version field.
Use the "Create release PR" github action to generate a release PR.
### Building for externally shared node builtins
If you are packaging `undici` for a distro, this might help if you would like to use
an unbundled version instead of bundling one in `libnode.so`.
To enable this, pass `EXTERNAL_PATH=/path/to/global/node_modules/undici` to `build/wasm.js`.
Pass this path with `loader.js` appended to `--shared-builtin-undici/undici-path` in Node.js's `configure.py`.
If building on a non-Alpine Linux distribution, you may need to also set the `WASM_CC`, `WASM_CFLAGS`, `WASM_LDFLAGS` and `WASM_LDLIBS` environment variables before running `build/wasm.js`.
Similarly, you can set the `WASM_OPT` environment variable to utilize your own `wasm-opt` optimizer.
### Benchmarks
```bash
cd benchmarks && npm i && npm run bench
```
The benchmarks will be available at `http://localhost:3042`.
### Documentation
```bash
cd docs && npm i && npm run serve
```
The documentation will be available at `http://localhost:3000`.
## Developer's Certificate of Origin 1.1
By making a contribution to this project, I certify that:
* (a) The contribution was created in whole or in part by me and I
have the right to submit it under the open source license
indicated in the file; or
* (b) The contribution is based upon previous work that, to the best
of my knowledge, is covered under an appropriate open source
license and I have the right under that license to submit that
work with modifications, whether created in whole or in part
by me, under the same open source license (unless I am
permitted to submit under a different license), as indicated
in the file; or
* (c) The contribution was provided directly to me by some other
person who certified (a), (b) or (c) and I have not modified
it.
* (d) I understand and agree that this project and the contribution
are public and that a record of the contribution (including all
personal information I submit with it, including my sign-off) is
maintained indefinitely and may be redistributed consistent with
this project or the open source license(s) involved.
### Moderation Policy
The [Node.js Moderation Policy] applies to this project.
[Node.js Moderation Policy]: https://github.com/nodejs/admin/blob/main/Moderation-Policy.md
================================================
FILE: GOVERNANCE.md
================================================
### Undici Working Group
The Node.js Undici project is governed by a Working Group (WG)
that is responsible for high-level guidance of the project.
The WG has final authority over this project including:
* Technical direction
* Project governance and process (including this policy)
* Contribution policy
* GitHub repository hosting
* Conduct guidelines
* Maintaining the list of additional Collaborators
For the current list of WG members, see the project
[README.md](./README.md#collaborators).
### Collaborators
The undici GitHub repository is
maintained by the WG and additional Collaborators who are added by the
WG on an ongoing basis.
Individuals making significant and valuable contributions are made
Collaborators and given commit-access to the project. These
individuals are identified by the WG and their addition as
Collaborators is discussed during the WG meeting.
_Note:_ If you make a significant contribution and are not considered
for commit-access log an issue or contact a WG member directly and it
will be brought up in the next WG meeting.
Modifications of the contents of the undici repository are
made on
a collaborative basis. Anybody with a GitHub account may propose a
modification via pull request and it will be considered by the project
Collaborators. All pull requests must be reviewed and accepted by a
Collaborator with sufficient expertise who is able to take full
responsibility for the change. In the case of pull requests proposed
by an existing Collaborator, an additional Collaborator is required
for sign-off. Consensus should be sought if additional Collaborators
participate and there is disagreement around a particular
modification. See _Consensus Seeking Process_ below for further detail
on the consensus model used for governance.
Collaborators may opt to elevate significant or controversial
modifications, or modifications that have not found consensus to the
WG for discussion by assigning the ***WG-agenda*** tag to a pull
request or issue. The WG should serve as the final arbiter where
required.
For the current list of Collaborators, see the project
[README.md](./README.md#collaborators). The list should be in
alphabetical order.
### WG Membership
WG seats are not time-limited. There is no fixed size of the WG.
However, the expected target is between 6 and 12, to ensure adequate
coverage of important areas of expertise, balanced with the ability to
make decisions efficiently.
There is no specific set of requirements or qualifications for WG
membership beyond these rules.
The WG may add additional members to the WG by unanimous consensus.
A WG member may be removed from the WG by voluntary resignation, or by
unanimous consensus of all other WG members.
Changes to WG membership should be posted in the agenda, and may be
suggested as any other agenda item (see "WG Meetings" below).
If an addition or removal is proposed during a meeting, and the full
WG is not in attendance to participate, then the addition or removal
is added to the agenda for the subsequent meeting. This is to ensure
that all members are given the opportunity to participate in all
membership decisions. If a WG member is unable to attend a meeting
where a planned membership decision is being made, then their consent
is assumed.
No more than 1/3 of the WG members may be affiliated with the same
employer. If removal or resignation of a WG member, or a change of
employment by a WG member, creates a situation where more than 1/3 of
the WG membership shares an employer, then the situation must be
immediately remedied by the resignation or removal of one or more WG
members affiliated with the over-represented employer(s).
### WG Meetings
The WG meets occasionally on Zoom. A designated moderator
approved by the WG runs the meeting. Each meeting should be
published to YouTube.
Items are added to the WG agenda that are considered contentious or
are modifications of governance, contribution policy, WG membership,
or release process.
The intention of the agenda is not to approve or review all patches;
that should happen continuously on GitHub and be handled by the larger
group of Collaborators.
Any community member or contributor can ask that something be added to
the next meeting's agenda by logging a GitHub Issue. Any Collaborator,
WG member or the moderator can add the item to the agenda by adding
the ***WG-agenda*** tag to the issue.
Prior to each WG meeting the moderator will share the Agenda with
members of the WG. WG members can add any items they like to the
agenda at the beginning of each meeting. The moderator and the WG
cannot veto or remove items.
The WG may invite persons or representatives from certain projects to
participate in a non-voting capacity.
The moderator is responsible for summarizing the discussion of each
agenda item and sends it as a pull request after the meeting.
### Consensus Seeking Process
The WG follows a
[Consensus
Seeking](http://en.wikipedia.org/wiki/Consensus-seeking_decision-making)
decision-making model.
When an agenda item has appeared to reach a consensus the moderator
will ask "Does anyone object?" as a final call for dissent from the
consensus.
If an agenda item cannot reach a consensus a WG member can call for
either a closing vote or a vote to table the issue to the next
meeting. The call for a vote must be seconded by a majority of the WG
or else the discussion will continue. Simple majority wins.
Note that changes to WG membership require a majority consensus. See
"WG Membership" above.
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) Matteo Collina and Undici contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: MAINTAINERS.md
================================================
# Maintainers
This document details any and all processes relevant to project maintainers. Maintainers should feel empowered to contribute back to this document with any process changes they feel improve the overall experience for themselves and other maintainers.
## Labels
Maintainers are encouraged to use the extensive and detailed list of labels for easier repo management.
* Generally, all issues should be labelled. The most general labels are `bug`, `enhancement`, and `Status: help-wanted`.
* Issues specific to a certain aspect of the project should be labeled using one of the specificity labels listed below. For example, a bug in the `Client` class should have the `Client` and `bug` label assigned.
* Specificity labels:
* `Agent`
* `Client`
* `Docs`
* `Performance`
* `Pool`
* `Tests`
* `Types`
* Any `question` or `usage help` issues should be converted into Q&A Discussions
* `Status:` labels should be added to all open issues indicating their relative development status.
* Status labels:
* `Status: blocked`
* `Status: help-wanted`
* `Status: in-progress`
* `Status: wontfix`
* Issues and/or pull requests with an agreed upon semver status can be assigned the appropriate `semver-` label.
* Semver labels:
* `semver-major`
* `semver-minor`
* `semver-patch`
* Issues with a low-barrier of entry should be assigned the `good first issue` label.
* Do not use the `invalid` label, instead use `bug` or `Status: wontfix`.
* Duplicate issues should initially be assigned the `duplicate` label.
## Making a Release
1. Go to github actions, then select ["Create Release PR"](https://github.com/nodejs/undici/actions/workflows/release-create-pr.yml).
2. Run the workflow, selecting `main` and indicating if you want a specific version number or a patch/minor/major release
3. Wait for the PR to be created. Approve the PR ([this](https://github.com/nodejs/undici/pull/4021) is a an example).
4. Land the PR, wait for the CI to pass.
5. Got to the ["Release"](https://github.com/nodejs/undici/actions/workflows/release.yml) workflow, you should see a job waiting.
6. If you are one of the [releases](https://github.com/nodejs/undici?tab=readme-ov-file#releasers), then click "review deployments", then select "release" and click "approve and deploy". If you are not a releaser, contact one.
================================================
FILE: README.md
================================================
# undici
[](https://github.com/nodejs/undici/actions/workflows/nodejs.yml) [](https://github.com/neostandard/neostandard) [](https://badge.fury.io/js/undici) [](https://codecov.io/gh/nodejs/undici)
An HTTP/1.1 client, written from scratch for Node.js.
> Undici means eleven in Italian. 1.1 -> 11 -> Eleven -> Undici.
It is also a Stranger Things reference.
## How to get involved
Have a question about using Undici? Open a [Q&A Discussion](https://github.com/nodejs/undici/discussions/new) or join our official OpenJS [Slack](https://openjs-foundation.slack.com/archives/C01QF9Q31QD) channel.
Looking to contribute? Start by reading the [contributing guide](./CONTRIBUTING.md)
## Install
```
npm i undici
```
## Benchmarks
The benchmark is a simple getting data [example](https://github.com/nodejs/undici/blob/main/benchmarks/benchmark.js) using a
50 TCP connections with a pipelining depth of 10 running on Node 22.11.0.
```
┌────────────────────────┬─────────┬────────────────────┬────────────┬─────────────────────────┐
│ Tests │ Samples │ Result │ Tolerance │ Difference with slowest │
├────────────────────────┼─────────┼────────────────────┼────────────┼─────────────────────────┤
│ 'axios' │ 15 │ '5708.26 req/sec' │ '± 2.91 %' │ '-' │
│ 'http - no keepalive' │ 10 │ '5809.80 req/sec' │ '± 2.30 %' │ '+ 1.78 %' │
│ 'request' │ 30 │ '5828.80 req/sec' │ '± 2.91 %' │ '+ 2.11 %' │
│ 'undici - fetch' │ 40 │ '5903.78 req/sec' │ '± 2.87 %' │ '+ 3.43 %' │
│ 'node-fetch' │ 10 │ '5945.40 req/sec' │ '± 2.13 %' │ '+ 4.15 %' │
│ 'got' │ 35 │ '6511.45 req/sec' │ '± 2.84 %' │ '+ 14.07 %' │
│ 'http - keepalive' │ 65 │ '9193.24 req/sec' │ '± 2.92 %' │ '+ 61.05 %' │
│ 'superagent' │ 35 │ '9339.43 req/sec' │ '± 2.95 %' │ '+ 63.61 %' │
│ 'undici - pipeline' │ 50 │ '13364.62 req/sec' │ '± 2.93 %' │ '+ 134.13 %' │
│ 'undici - stream' │ 95 │ '18245.36 req/sec' │ '± 2.99 %' │ '+ 219.63 %' │
│ 'undici - request' │ 50 │ '18340.17 req/sec' │ '± 2.84 %' │ '+ 221.29 %' │
│ 'undici - dispatch' │ 40 │ '22234.42 req/sec' │ '± 2.94 %' │ '+ 289.51 %' │
└────────────────────────┴─────────┴────────────────────┴────────────┴─────────────────────────┘
```
## Undici vs. Fetch
### Overview
Node.js includes a built-in `fetch()` implementation powered by undici starting from Node.js v18. However, there are important differences between using the built-in fetch and installing undici as a separate module.
### Built-in Fetch (Node.js v18+)
Node.js's built-in fetch is powered by a bundled version of undici:
```js
// Available globally in Node.js v18+
const response = await fetch('https://api.example.com/data');
const data = await response.json();
// Check the bundled undici version
console.log(process.versions.undici); // e.g., "5.28.4"
```
**Pros:**
- No additional dependencies required
- Works across different JavaScript runtimes
- Automatic compression handling (gzip, deflate, br)
- Built-in caching support (in development)
**Cons:**
- Limited to the undici version bundled with your Node.js version
- Less control over connection pooling and advanced features
- Error handling follows Web API standards (errors wrapped in `TypeError`)
- Performance overhead due to Web Streams implementation
### Undici Module
Installing undici as a separate module gives you access to the latest features and APIs:
```bash
npm install undici
```
```js
import { request, fetch, Agent, setGlobalDispatcher } from 'undici';
// Use undici.request for maximum performance
const { statusCode, headers, body } = await request('https://api.example.com/data');
const data = await body.json();
// Or use undici.fetch with custom configuration
const agent = new Agent({ keepAliveTimeout: 10000 });
setGlobalDispatcher(agent);
const response = await fetch('https://api.example.com/data');
```
**Pros:**
- Latest undici features and bug fixes
- Access to advanced APIs (`request`, `stream`, `pipeline`)
- Fine-grained control over connection pooling
- Better error handling with clearer error messages
- Superior performance, especially with `undici.request`
- HTTP/1.1 pipelining support
- Custom interceptors and middleware
- Advanced features like `ProxyAgent`, `Socks5Agent`, `MockAgent`
**Cons:**
- Additional dependency to manage
- Larger bundle size
### When to Use Each
#### Use Built-in Fetch When:
- You want zero dependencies
- Building isomorphic code that runs in browsers and Node.js
- Publishing to npm and want to maximize compatibility with JS runtimes
- Simple HTTP requests without advanced configuration
- You're publishing to npm and you want to maximize compatiblity
- You don't depend on features from a specific version of undici
#### Use Undici Module When:
- You need the latest undici features and performance improvements
- You require advanced connection pooling configuration
- You need APIs not available in the built-in fetch (`ProxyAgent`, `Socks5Agent`, `MockAgent`, etc.)
- Performance is critical (use `undici.request` for maximum speed)
- You want better error handling and debugging capabilities
- You need HTTP/1.1 pipelining or advanced interceptors
- You prefer decoupled protocol and API interfaces
### Performance Comparison
Based on benchmarks, here's the typical performance hierarchy:
1. **`undici.request()`** - Fastest, most efficient
2. **`undici.fetch()`** - Good performance, standard compliance
3. **Node.js `http`/`https`** - Baseline performance
### Migration Guide
If you're currently using built-in fetch and want to migrate to undici:
```js
// Before: Built-in fetch
const response = await fetch('https://api.example.com/data');
// After: Undici fetch (drop-in replacement)
import { fetch } from 'undici';
const response = await fetch('https://api.example.com/data');
// Or: Undici request (better performance)
import { request } from 'undici';
const { statusCode, body } = await request('https://api.example.com/data');
const data = await body.json();
```
### Version Compatibility
You can check which version of undici is bundled with your Node.js version:
```js
console.log(process.versions.undici);
```
Installing undici as a module allows you to use a newer version than what's bundled with Node.js, giving you access to the latest features and performance improvements.
## Quick Start
### Basic Request
```js
import { request } from 'undici'
const {
statusCode,
headers,
trailers,
body
} = await request('http://localhost:3000/foo')
console.log('response received', statusCode)
console.log('headers', headers)
for await (const data of body) { console.log('data', data) }
console.log('trailers', trailers)
```
### Using Cache Interceptor
Undici provides a powerful HTTP caching interceptor that follows HTTP caching best practices. Here's how to use it:
```js
import { fetch, Agent, interceptors, cacheStores } from 'undici';
// Create a client with cache interceptor
const client = new Agent().compose(interceptors.cache({
// Optional: Configure cache store (defaults to MemoryCacheStore)
store: new cacheStores.MemoryCacheStore({
maxSize: 100 * 1024 * 1024, // 100MB
maxCount: 1000,
maxEntrySize: 5 * 1024 * 1024 // 5MB
}),
// Optional: Specify which HTTP methods to cache (default: ['GET', 'HEAD'])
methods: ['GET', 'HEAD']
}));
// Set the global dispatcher to use our caching client
setGlobalDispatcher(client);
// Now all fetch requests will use the cache
async function getData() {
const response = await fetch('https://api.example.com/data');
// The server should set appropriate Cache-Control headers in the response
// which the cache will respect based on the cache policy
return response.json();
}
// First request - fetches from origin
const data1 = await getData();
// Second request - served from cache if within max-age
const data2 = await getData();
```
#### Key Features:
- **Automatic Caching**: Respects `Cache-Control` and `Expires` headers
- **Validation**: Supports `ETag` and `Last-Modified` validation
- **Storage Options**: In-memory or persistent SQLite storage
- **Flexible**: Configure cache size, TTL, and more
## Global Installation
Undici provides an `install()` function to add all WHATWG fetch classes to `globalThis`, making them available globally:
```js
import { install } from 'undici'
// Install all WHATWG fetch classes globally
install()
// Now you can use fetch classes globally without importing
const response = await fetch('https://api.example.com/data')
const data = await response.json()
// All classes are available globally:
const headers = new Headers([['content-type', 'application/json']])
const request = new Request('https://example.com')
const formData = new FormData()
const ws = new WebSocket('wss://example.com')
const eventSource = new EventSource('https://example.com/events')
```
The `install()` function adds the following classes to `globalThis`:
- `fetch` - The fetch function
- `Headers` - HTTP headers management
- `Response` - HTTP response representation
- `Request` - HTTP request representation
- `FormData` - Form data handling
- `WebSocket` - WebSocket client
- `CloseEvent`, `ErrorEvent`, `MessageEvent` - WebSocket events
- `EventSource` - Server-sent events client
This is useful for:
- Polyfilling environments that don't have fetch
- Ensuring consistent fetch behavior across different Node.js versions
- Making undici's implementations available globally for libraries that expect them
## Body Mixins
The `body` mixins are the most common way to format the request/response body. Mixins include:
- [`.arrayBuffer()`](https://fetch.spec.whatwg.org/#dom-body-arraybuffer)
- [`.blob()`](https://fetch.spec.whatwg.org/#dom-body-blob)
- [`.bytes()`](https://fetch.spec.whatwg.org/#dom-body-bytes)
- [`.json()`](https://fetch.spec.whatwg.org/#dom-body-json)
- [`.text()`](https://fetch.spec.whatwg.org/#dom-body-text)
> [!NOTE]
> The body returned from `undici.request` does not implement `.formData()`.
Example usage:
```js
import { request } from 'undici'
const {
statusCode,
headers,
trailers,
body
} = await request('http://localhost:3000/foo')
console.log('response received', statusCode)
console.log('headers', headers)
console.log('data', await body.json())
console.log('trailers', trailers)
```
_Note: Once a mixin has been called then the body cannot be reused, thus calling additional mixins on `.body`, e.g. `.body.json(); .body.text()` will result in an error `TypeError: unusable` being thrown and returned through the `Promise` rejection._
Should you need to access the `body` in plain-text after using a mixin, the best practice is to use the `.text()` mixin first and then manually parse the text to the desired format.
For more information about their behavior, please reference the body mixin from the [Fetch Standard](https://fetch.spec.whatwg.org/#body-mixin).
## Common API Methods
This section documents our most commonly used API methods. Additional APIs are documented in their own files within the [docs](./docs/) folder and are accessible via the navigation list on the left side of the docs site.
### `undici.request([url, options]): Promise`
Arguments:
* **url** `string | URL | UrlObject`
* **options** [`RequestOptions`](./docs/docs/api/Dispatcher.md#parameter-requestoptions)
* **dispatcher** `Dispatcher` - Default: [getGlobalDispatcher](#undicigetglobaldispatcher)
* **method** `String` - Default: `PUT` if `options.body`, otherwise `GET`
Returns a promise with the result of the `Dispatcher.request` method.
Calls `options.dispatcher.request(options)`.
See [Dispatcher.request](./docs/docs/api/Dispatcher.md#dispatcherrequestoptions-callback) for more details, and [request examples](./docs/examples/README.md) for examples.
### `undici.stream([url, options, ]factory): Promise`
Arguments:
* **url** `string | URL | UrlObject`
* **options** [`StreamOptions`](./docs/docs/api/Dispatcher.md#parameter-streamoptions)
* **dispatcher** `Dispatcher` - Default: [getGlobalDispatcher](#undicigetglobaldispatcher)
* **method** `String` - Default: `PUT` if `options.body`, otherwise `GET`
* **factory** `Dispatcher.stream.factory`
Returns a promise with the result of the `Dispatcher.stream` method.
Calls `options.dispatcher.stream(options, factory)`.
See [Dispatcher.stream](./docs/docs/api/Dispatcher.md#dispatcherstreamoptions-factory-callback) for more details.
### `undici.pipeline([url, options, ]handler): Duplex`
Arguments:
* **url** `string | URL | UrlObject`
* **options** [`PipelineOptions`](./docs/docs/api/Dispatcher.md#parameter-pipelineoptions)
* **dispatcher** `Dispatcher` - Default: [getGlobalDispatcher](#undicigetglobaldispatcher)
* **method** `String` - Default: `PUT` if `options.body`, otherwise `GET`
* **handler** `Dispatcher.pipeline.handler`
Returns: `stream.Duplex`
Calls `options.dispatch.pipeline(options, handler)`.
See [Dispatcher.pipeline](./docs/docs/api/Dispatcher.md#dispatcherpipelineoptions-handler) for more details.
### `undici.connect([url, options]): Promise`
Starts two-way communications with the requested resource using [HTTP CONNECT](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/CONNECT).
Arguments:
* **url** `string | URL | UrlObject`
* **options** [`ConnectOptions`](./docs/docs/api/Dispatcher.md#parameter-connectoptions)
* **dispatcher** `Dispatcher` - Default: [getGlobalDispatcher](#undicigetglobaldispatcher)
* **callback** `(err: Error | null, data: ConnectData | null) => void` (optional)
Returns a promise with the result of the `Dispatcher.connect` method.
Calls `options.dispatch.connect(options)`.
See [Dispatcher.connect](./docs/docs/api/Dispatcher.md#dispatcherconnectoptions-callback) for more details.
### `undici.fetch(input[, init]): Promise`
Implements [fetch](https://fetch.spec.whatwg.org/#fetch-method).
* https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch
* https://fetch.spec.whatwg.org/#fetch-method
Basic usage example:
```js
import { fetch } from 'undici'
const res = await fetch('https://example.com')
const json = await res.json()
console.log(json)
```
You can pass an optional dispatcher to `fetch` as:
```js
import { fetch, Agent } from 'undici'
const res = await fetch('https://example.com', {
// Mocks are also supported
dispatcher: new Agent({
keepAliveTimeout: 10,
keepAliveMaxTimeout: 10
})
})
const json = await res.json()
console.log(json)
```
#### `request.body`
A body can be of the following types:
- ArrayBuffer
- ArrayBufferView
- AsyncIterables
- Blob
- Iterables
- String
- URLSearchParams
- FormData
In this implementation of fetch, ```request.body``` now accepts ```Async Iterables```. It is not present in the [Fetch Standard](https://fetch.spec.whatwg.org).
```js
import { fetch } from 'undici'
const data = {
async *[Symbol.asyncIterator]() {
yield 'hello'
yield 'world'
},
}
await fetch('https://example.com', { body: data, method: 'POST', duplex: 'half' })
```
[FormData](https://developer.mozilla.org/en-US/docs/Web/API/FormData) besides text data and buffers can also utilize streams via [Blob](https://developer.mozilla.org/en-US/docs/Web/API/Blob) objects:
```js
import { openAsBlob } from 'node:fs'
const file = await openAsBlob('./big.csv')
const body = new FormData()
body.set('file', file, 'big.csv')
await fetch('http://example.com', { method: 'POST', body })
```
#### `request.duplex`
- `'half'`
In this implementation of fetch, `request.duplex` must be set if `request.body` is `ReadableStream` or `Async Iterables`, however, even though the value must be set to `'half'`, it is actually a _full_ duplex. For more detail refer to the [Fetch Standard](https://fetch.spec.whatwg.org/#dom-requestinit-duplex).
#### `response.body`
Nodejs has two kinds of streams: [web streams](https://nodejs.org/api/webstreams.html), which follow the API of the WHATWG web standard found in browsers, and an older Node-specific [streams API](https://nodejs.org/api/stream.html). `response.body` returns a readable web stream. If you would prefer to work with a Node stream you can convert a web stream using `.fromWeb()`.
```js
import { fetch } from 'undici'
import { Readable } from 'node:stream'
const response = await fetch('https://example.com')
const readableWebStream = response.body
const readableNodeStream = Readable.fromWeb(readableWebStream)
```
## Specification Compliance
This section documents parts of the [HTTP/1.1](https://www.rfc-editor.org/rfc/rfc9110.html) and [Fetch Standard](https://fetch.spec.whatwg.org) that Undici does
not support or does not fully implement.
#### CORS
Unlike browsers, Undici does not implement CORS (Cross-Origin Resource Sharing) checks by default. This means:
- No preflight requests are automatically sent for cross-origin requests
- No validation of `Access-Control-Allow-Origin` headers is performed
- Requests to any origin are allowed regardless of the source
This behavior is intentional for server-side environments where CORS restrictions are typically unnecessary. If your application requires CORS-like protections, you will need to implement these checks manually.
#### Garbage Collection
* https://fetch.spec.whatwg.org/#garbage-collection
The [Fetch Standard](https://fetch.spec.whatwg.org) allows users to skip consuming the response body by relying on
[garbage collection](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Memory_Management#garbage_collection) to release connection resources.
Garbage collection in Node is less aggressive and deterministic
(due to the lack of clear idle periods that browsers have through the rendering refresh rate)
which means that leaving the release of connection resources to the garbage collector can lead
to excessive connection usage, reduced performance (due to less connection re-use), and even
stalls or deadlocks when running out of connections.
Therefore, __it is important to always either consume or cancel the response body anyway__.
```js
// Do
const { body, headers } = await fetch(url);
for await (const chunk of body) {
// force consumption of body
}
// Do not
const { headers } = await fetch(url);
```
However, if you want to get only headers, it might be better to use `HEAD` request method. Usage of this method will obviate the need for consumption or cancelling of the response body. See [MDN - HTTP - HTTP request methods - HEAD](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/HEAD) for more details.
```js
const headers = await fetch(url, { method: 'HEAD' })
.then(res => res.headers)
```
Note that consuming the response body is _mandatory_ for `request`:
```js
// Do
const { body, headers } = await request(url);
await body.dump(); // force consumption of body
// Do not
const { headers } = await request(url);
```
#### Forbidden and Safelisted Header Names
* https://fetch.spec.whatwg.org/#cors-safelisted-response-header-name
* https://fetch.spec.whatwg.org/#forbidden-header-name
* https://fetch.spec.whatwg.org/#forbidden-response-header-name
* https://github.com/wintercg/fetch/issues/6
The [Fetch Standard](https://fetch.spec.whatwg.org) requires implementations to exclude certain headers from requests and responses. In browser environments, some headers are forbidden so the user agent remains in full control over them. In Undici, these constraints are removed to give more control to the user.
#### Content-Encoding
* https://www.rfc-editor.org/rfc/rfc9110#field.content-encoding
Undici limits the number of `Content-Encoding` layers in a response to **5** to prevent resource exhaustion attacks. If a server responds with more than 5 content-encodings (e.g., `Content-Encoding: gzip, gzip, gzip, gzip, gzip, gzip`), the fetch will be rejected with an error. This limit matches the approach taken by [curl](https://curl.se/docs/CVE-2022-32206.html) and [urllib3](https://github.com/advisories/GHSA-gm62-xv2j-4rw9).
#### `undici.upgrade([url, options]): Promise`
Upgrade to a different protocol. See [MDN - HTTP - Protocol upgrade mechanism](https://developer.mozilla.org/en-US/docs/Web/HTTP/Protocol_upgrade_mechanism) for more details.
Arguments:
* **url** `string | URL | UrlObject`
* **options** [`UpgradeOptions`](./docs/docs/api/Dispatcher.md#parameter-upgradeoptions)
* **dispatcher** `Dispatcher` - Default: [getGlobalDispatcher](#undicigetglobaldispatcher)
* **callback** `(error: Error | null, data: UpgradeData) => void` (optional)
Returns a promise with the result of the `Dispatcher.upgrade` method.
Calls `options.dispatcher.upgrade(options)`.
See [Dispatcher.upgrade](./docs/docs/api/Dispatcher.md#dispatcherupgradeoptions-callback) for more details.
### `undici.setGlobalDispatcher(dispatcher)`
* dispatcher `Dispatcher`
Sets the global dispatcher used by Common API Methods. Global dispatcher is shared among compatible undici modules,
including undici that is bundled internally with node.js.
### `undici.getGlobalDispatcher()`
Gets the global dispatcher used by Common API Methods.
Returns: `Dispatcher`
### `undici.setGlobalOrigin(origin)`
* origin `string | URL | undefined`
Sets the global origin used in `fetch`.
If `undefined` is passed, the global origin will be reset. This will cause `Response.redirect`, `new Request()`, and `fetch` to throw an error when a relative path is passed.
```js
setGlobalOrigin('http://localhost:3000')
const response = await fetch('/api/ping')
console.log(response.url) // http://localhost:3000/api/ping
```
### `undici.getGlobalOrigin()`
Gets the global origin used in `fetch`.
Returns: `URL`
### `UrlObject`
* **port** `string | number` (optional)
* **path** `string` (optional)
* **pathname** `string` (optional)
* **hostname** `string` (optional)
* **origin** `string` (optional)
* **protocol** `string` (optional)
* **search** `string` (optional)
#### Expect
Undici does not support the `Expect` request header field. The request
body is always immediately sent and the `100 Continue` response will be
ignored.
Refs: https://tools.ietf.org/html/rfc7231#section-5.1.1
#### Pipelining
Undici will only use pipelining if configured with a `pipelining` factor
greater than `1`. Also it is important to pass `blocking: false` to the
request options to properly pipeline requests.
Undici always assumes that connections are persistent and will immediately
pipeline requests, without checking whether the connection is persistent.
Hence, automatic fallback to HTTP/1.0 or HTTP/1.1 without pipelining is
not supported.
Undici will immediately pipeline when retrying requests after a failed
connection. However, Undici will not retry the first remaining requests in
the prior pipeline and instead error the corresponding callback/promise/stream.
Undici will abort all running requests in the pipeline when any of them are
aborted.
* Refs: https://tools.ietf.org/html/rfc2616#section-8.1.2.2
* Refs: https://tools.ietf.org/html/rfc7230#section-6.3.2
#### Manual Redirect
Since it is not possible to manually follow an HTTP redirect on the server-side,
Undici returns the actual response instead of an `opaqueredirect` filtered one
when invoked with a `manual` redirect. This aligns `fetch()` with the other
implementations in Deno and Cloudflare Workers.
Refs: https://fetch.spec.whatwg.org/#atomic-http-redirect-handling
### Workarounds
#### Network address family autoselection.
If you experience problem when connecting to a remote server that is resolved by your DNS servers to a IPv6 (AAAA record)
first, there are chances that your local router or ISP might have problem connecting to IPv6 networks. In that case
undici will throw an error with code `UND_ERR_CONNECT_TIMEOUT`.
If the target server resolves to both a IPv6 and IPv4 (A records) address and you are using a compatible Node version
(18.3.0 and above), you can fix the problem by providing the `autoSelectFamily` option (support by both `undici.request`
and `undici.Agent`) which will enable the family autoselection algorithm when establishing the connection.
## Collaborators
* [__Daniele Belardi__](https://github.com/dnlup),
* [__Ethan Arrowood__](https://github.com/ethan-arrowood),
* [__Matteo Collina__](https://github.com/mcollina),
* [__Matthew Aitken__](https://github.com/KhafraDev),
* [__Robert Nagy__](https://github.com/ronag),
* [__Szymon Marczak__](https://github.com/szmarczak),
## Past Collaborators
* [__Tomas Della Vedova__](https://github.com/delvedor),
### Releasers
* [__Ethan Arrowood__](https://github.com/ethan-arrowood),
* [__Matteo Collina__](https://github.com/mcollina),
* [__Robert Nagy__](https://github.com/ronag),
* [__Matthew Aitken__](https://github.com/KhafraDev),
## Long Term Support
Undici aligns with the Node.js LTS schedule. The following table shows the supported versions:
| Undici Version | Bundled in Node.js | Node.js Versions Supported | End of Life |
|----------------|-------------------|----------------------------|-------------|
| 5.x | 18.x | ≥14.0 (tested: 14, 16, 18) | 2024-04-30 |
| 6.x | 20.x, 22.x | ≥18.17 (tested: 18, 20, 21, 22) | 2026-04-30 |
| 7.x | 24.x | ≥20.18.1 (tested: 20, 22, 24) | 2027-04-30 |
## License
MIT
================================================
FILE: SECURITY.md
================================================
If you believe you have found a security issue in the software in this
repository, please consult https://github.com/nodejs/node/blob/HEAD/SECURITY.md.
================================================
FILE: benchmarks/_util/index.js
================================================
'use strict'
const parallelRequests = parseInt(process.env.PARALLEL, 10) || 100
function makeParallelRequests (cb) {
const promises = new Array(parallelRequests)
for (let i = 0; i < parallelRequests; ++i) {
promises[i] = new Promise(cb)
}
return Promise.all(promises)
}
function printResults (results) {
// Sort results by least performant first, then compare relative performances and also printing padding
let last
const rows = Object.entries(results)
// If any failed, put on the top of the list, otherwise order by mean, ascending
.sort((a, b) => (!a[1].success ? -1 : b[1].mean - a[1].mean))
.map(([name, result]) => {
if (!result.success) {
return {
Tests: name,
Samples: result.size,
Result: 'Errored',
Tolerance: 'N/A',
'Difference with Slowest': 'N/A'
}
}
// Calculate throughput and relative performance
const { size, mean, standardError } = result
const relative = last !== 0 ? (last / mean - 1) * 100 : 0
// Save the slowest for relative comparison
if (typeof last === 'undefined') {
last = mean
}
return {
Tests: name,
Samples: size,
Result: `${((parallelRequests * 1e9) / mean).toFixed(2)} req/sec`,
Tolerance: `± ${((standardError / mean) * 100).toFixed(2)} %`,
'Difference with slowest':
relative > 0 ? `+ ${relative.toFixed(2)} %` : '-'
}
})
return console.table(rows)
}
/**
* @param {number} num
* @returns {string}
*/
function formatBytes (num) {
if (!Number.isFinite(num)) {
throw new Error('invalid number')
}
const prefixes = ['B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB']
const idx = Math.min(Math.floor(Math.log(num) / Math.log(1024)), prefixes.length - 1)
return `${(num / Math.pow(1024, idx)).toFixed(2)}${prefixes[idx]}`
}
module.exports = { makeParallelRequests, printResults, formatBytes }
================================================
FILE: benchmarks/_util/runner.js
================================================
// @ts-check
'use strict'
class Info {
/** @type {string} */
#name
/** @type {bigint} */
#current
/** @type {bigint} */
#finish
/** @type {(...args: any[]) => any} */
#callback
/** @type {boolean} */
#finalized = false
/**
* @param {string} name
* @param {(...args: any[]) => any} callback
*/
constructor (name, callback) {
this.#name = name
this.#callback = callback
}
get name () {
return this.#name
}
start () {
if (this.#finalized) {
throw new TypeError('called after finished.')
}
this.#current = process.hrtime.bigint()
}
end () {
if (this.#finalized) {
throw new TypeError('called after finished.')
}
this.#finish = process.hrtime.bigint()
this.#finalized = true
this.#callback()
}
diff () {
return Number(this.#finish - this.#current)
}
}
/**
* @typedef BenchMarkHandler
* @type {(ev: { name: string; start(): void; end(): void; }) => any}
*/
/**
* @param {Record} experiments
* @param {{ minSamples?: number; maxSamples?: number }} [options]
* @returns {Promise<{ name: string; average: number; samples: number; fn: BenchMarkHandler; iterationPerSecond: number; min: number; max: number }[]>}
*/
async function bench (experiments, options = {}) {
const names = Object.keys(experiments)
/** @type {{ name: string; average: number; samples: number; fn: BenchMarkHandler; iterationPerSecond: number; min: number; max: number }[]} */
const results = []
async function waitMaybePromiseLike (p) {
if (
(typeof p === 'object' || typeof p === 'function') &&
p !== null &&
typeof p.then === 'function'
) {
await p
}
}
for (let i = 0; i < names.length; ++i) {
const name = names[i]
const fn = experiments[name]
const samples = []
for (let i = 0; i < 8; ++i) {
// warmup
await new Promise((resolve, reject) => {
const info = new Info(name, resolve)
try {
const p = fn(info)
waitMaybePromiseLike(p).catch((err) => reject(err))
} catch (err) {
reject(err)
}
})
}
let timing = 0
const minSamples = options.minSamples ?? 128
for (let j = 0; (j < minSamples || timing < 800_000_000) && (typeof options.maxSamples === 'number' ? options.maxSamples > j : true); ++j) {
let resolve = (value) => {}
let reject = (reason) => {}
const promise = new Promise(
(_resolve, _reject) => { resolve = _resolve; reject = _reject }
)
const info = new Info(name, resolve)
try {
const p = fn(info)
await waitMaybePromiseLike(p)
} catch (err) {
reject(err)
}
await promise
samples.push({ time: info.diff() })
timing += info.diff()
}
const average =
samples.map((v) => v.time).reduce((a, b) => a + b, 0) / samples.length
results.push({
name: names[i],
average,
samples: samples.length,
fn,
iterationPerSecond: 1e9 / average,
min: samples.reduce((a, acc) => Math.min(a, acc.time), samples[0].time),
max: samples.reduce((a, acc) => Math.max(a, acc.time), samples[0].time)
})
}
return results
}
module.exports = { bench }
================================================
FILE: benchmarks/benchmark-http2.js
================================================
'use strict'
const os = require('node:os')
const path = require('node:path')
const http2 = require('node:http2')
const { readFileSync } = require('node:fs')
const { Writable } = require('node:stream')
const { isMainThread } = require('node:worker_threads')
const { Pool, Client, fetch, Agent, setGlobalDispatcher } = require('..')
const ca = readFileSync(path.join(__dirname, '..', 'test', 'fixtures', 'ca.pem'), 'utf8')
const servername = 'agent1'
const iterations = (parseInt(process.env.SAMPLES, 10) || 10) + 1
const errorThreshold = parseInt(process.env.ERROR_THRESHOLD, 10) || 3
const connections = parseInt(process.env.CONNECTIONS, 10) || 50
const pipelining = parseInt(process.env.PIPELINING, 10) || 10
const parallelRequests = parseInt(process.env.PARALLEL, 10) || 100
const headersTimeout = parseInt(process.env.HEADERS_TIMEOUT, 10) || 0
const bodyTimeout = parseInt(process.env.BODY_TIMEOUT, 10) || 0
const dest = {}
if (process.env.PORT) {
dest.port = process.env.PORT
dest.url = `https://localhost:${process.env.PORT}`
} else {
dest.url = 'https://localhost'
dest.socketPath = path.join(os.tmpdir(), 'undici.sock')
}
const httpsBaseOptions = {
ca,
servername,
protocol: 'https:',
hostname: 'localhost',
method: 'GET',
path: '/',
query: {
frappucino: 'muffin',
goat: 'scone',
pond: 'moose',
foo: ['bar', 'baz', 'bal'],
bool: true,
numberKey: 256
},
...dest
}
const undiciOptions = {
path: '/',
method: 'GET',
headersTimeout,
bodyTimeout
}
const http2NativeClient = http2.connect(httpsBaseOptions.url, {
rejectUnauthorized: false
})
const Class = connections > 1 ? Pool : Client
const dispatcher = new Class(httpsBaseOptions.url, {
allowH2: true,
pipelining,
connections,
connect: {
rejectUnauthorized: false,
ca,
servername
},
...dest
})
setGlobalDispatcher(new Agent({
allowH2: true,
pipelining,
connections,
connect: {
rejectUnauthorized: false,
ca,
servername
}
}))
class SimpleRequest {
constructor (resolve) {
this.dst = new Writable({
write (chunk, encoding, callback) {
callback()
}
}).on('finish', resolve)
}
onConnect (abort) { }
onHeaders (statusCode, headers, resume) {
this.dst.on('drain', resume)
}
onData (chunk) {
return this.dst.write(chunk)
}
onComplete () {
this.dst.end()
}
onError (err) {
throw err
}
}
function makeParallelRequests (cb) {
const res = Promise.all(Array.from(Array(parallelRequests)).map(() => new Promise(cb)))
res.catch(console.error)
return res
}
function printResults (results) {
// Sort results by least performant first, then compare relative performances and also printing padding
let last
const rows = Object.entries(results)
// If any failed, put on the top of the list, otherwise order by mean, ascending
.sort((a, b) => (!a[1].success ? -1 : b[1].mean - a[1].mean))
.map(([name, result]) => {
if (!result.success) {
return {
Tests: name,
Samples: result.size,
Result: 'Errored',
Tolerance: 'N/A',
'Difference with Slowest': 'N/A'
}
}
// Calculate throughput and relative performance
const { size, mean, standardError } = result
const relative = last !== 0 ? (last / mean - 1) * 100 : 0
// Save the slowest for relative comparison
if (typeof last === 'undefined') {
last = mean
}
console.log(mean)
return {
Tests: name,
Samples: size,
Result: `${((1e9 * parallelRequests) / mean).toFixed(2)} req/sec`,
Tolerance: `± ${((standardError / mean) * 100).toFixed(2)} %`,
'Difference with slowest': relative > 0 ? `+ ${relative.toFixed(2)} %` : '-'
}
})
return console.table(rows)
}
const experiments = {
'native - http2' () {
return makeParallelRequests(resolve => {
const stream = http2NativeClient.request({
[http2.constants.HTTP2_HEADER_PATH]: httpsBaseOptions.path,
[http2.constants.HTTP2_HEADER_METHOD]: httpsBaseOptions.method
})
stream.end().on('response', () => {
stream.pipe(
new Writable({
write (chunk, encoding, callback) {
callback()
}
})
)
.on('error', (err) => {
console.log('http2 - request - response - error', err)
})
.on('finish', () => {
resolve()
})
})
})
},
'undici - pipeline' () {
return makeParallelRequests(resolve => {
dispatcher
.pipeline(undiciOptions, data => {
return data.body
})
.end()
.pipe(
new Writable({
write (chunk, encoding, callback) {
callback()
}
})
)
.on('finish', resolve)
})
},
'undici - request' () {
return makeParallelRequests(resolve => {
try {
dispatcher.request(undiciOptions).then(({ body }) => {
body
.pipe(
new Writable({
write (chunk, encoding, callback) {
callback()
}
})
)
.on('error', (err) => {
console.log('undici - request - dispatcher.request - body - error', err)
})
.on('finish', () => {
resolve()
})
})
} catch (err) {
console.error('undici - request - dispatcher.request - requestCount', err)
}
})
},
'undici - stream' () {
return makeParallelRequests(resolve => {
return dispatcher
.stream(undiciOptions, () => {
return new Writable({
write (chunk, encoding, callback) {
callback()
}
})
})
.then(resolve)
})
},
'undici - dispatch' () {
return makeParallelRequests(resolve => {
dispatcher.dispatch(undiciOptions, new SimpleRequest(resolve))
})
}
}
if (process.env.PORT) {
// fetch does not support the socket
experiments['undici - fetch'] = () => {
return makeParallelRequests(resolve => {
fetch(dest.url, {}).then(res => {
res.body.pipeTo(new WritableStream({ write () { }, close () { resolve() } }))
}).catch(console.log)
})
}
}
async function main () {
const { cronometro } = await import('cronometro')
cronometro(
experiments,
{
iterations,
errorThreshold,
print: false
},
(err, results) => {
if (err) {
throw err
}
printResults(results)
dispatcher.destroy()
http2NativeClient.close()
}
)
}
if (isMainThread) {
main()
} else {
module.exports = main
}
================================================
FILE: benchmarks/benchmark-https.js
================================================
'use strict'
const https = require('node:https')
const os = require('node:os')
const path = require('node:path')
const { readFileSync } = require('node:fs')
const { Writable } = require('node:stream')
const { isMainThread } = require('node:worker_threads')
const { Pool, Client, fetch, Agent, setGlobalDispatcher } = require('..')
const ca = readFileSync(path.join(__dirname, '..', 'test', 'fixtures', 'ca.pem'), 'utf8')
const servername = 'agent1'
const iterations = (parseInt(process.env.SAMPLES, 10) || 10) + 1
const errorThreshold = parseInt(process.env.ERROR_THRESHOLD, 10) || 3
const connections = parseInt(process.env.CONNECTIONS, 10) || 50
const pipelining = parseInt(process.env.PIPELINING, 10) || 10
const parallelRequests = parseInt(process.env.PARALLEL, 10) || 100
const headersTimeout = parseInt(process.env.HEADERS_TIMEOUT, 10) || 0
const bodyTimeout = parseInt(process.env.BODY_TIMEOUT, 10) || 0
const dest = {}
if (process.env.PORT) {
dest.port = process.env.PORT
dest.url = `https://localhost:${process.env.PORT}`
} else {
dest.url = 'https://localhost'
dest.socketPath = path.join(os.tmpdir(), 'undici.sock')
}
const httpsBaseOptions = {
ca,
servername,
protocol: 'https:',
hostname: 'localhost',
method: 'GET',
path: '/',
query: {
frappucino: 'muffin',
goat: 'scone',
pond: 'moose',
foo: ['bar', 'baz', 'bal'],
bool: true,
numberKey: 256
},
...dest
}
const httpsNoKeepAliveOptions = {
...httpsBaseOptions,
agent: new https.Agent({
keepAlive: false,
maxSockets: connections,
// rejectUnauthorized: false,
ca,
servername
})
}
const httpsKeepAliveOptions = {
...httpsBaseOptions,
agent: new https.Agent({
keepAlive: true,
maxSockets: connections,
// rejectUnauthorized: false,
ca,
servername
})
}
const undiciOptions = {
path: '/',
method: 'GET',
headersTimeout,
bodyTimeout
}
const Class = connections > 1 ? Pool : Client
const dispatcher = new Class(httpsBaseOptions.url, {
pipelining,
connections,
connect: {
// rejectUnauthorized: false,
ca,
servername
},
...dest
})
setGlobalDispatcher(new Agent({
pipelining,
connections,
connect: {
// rejectUnauthorized: false,
ca,
servername
}
}))
class SimpleRequest {
constructor (resolve) {
this.dst = new Writable({
write (chunk, encoding, callback) {
callback()
}
}).on('finish', resolve)
}
onConnect (abort) { }
onHeaders (statusCode, headers, resume) {
this.dst.on('drain', resume)
}
onData (chunk) {
return this.dst.write(chunk)
}
onComplete () {
this.dst.end()
}
onError (err) {
throw err
}
}
function makeParallelRequests (cb) {
return Promise.all(Array.from(Array(parallelRequests)).map(() => new Promise(cb)))
}
function printResults (results) {
// Sort results by least performant first, then compare relative performances and also printing padding
let last
const rows = Object.entries(results)
// If any failed, put on the top of the list, otherwise order by mean, ascending
.sort((a, b) => (!a[1].success ? -1 : b[1].mean - a[1].mean))
.map(([name, result]) => {
if (!result.success) {
return {
Tests: name,
Samples: result.size,
Result: 'Errored',
Tolerance: 'N/A',
'Difference with Slowest': 'N/A'
}
}
// Calculate throughput and relative performance
const { size, mean, standardError } = result
const relative = last !== 0 ? (last / mean - 1) * 100 : 0
// Save the slowest for relative comparison
if (typeof last === 'undefined') {
last = mean
}
return {
Tests: name,
Samples: size,
Result: `${((parallelRequests * 1e9) / mean).toFixed(2)} req/sec`,
Tolerance: `± ${((standardError / mean) * 100).toFixed(2)} %`,
'Difference with slowest': relative > 0 ? `+ ${relative.toFixed(2)} %` : '-'
}
})
return console.table(rows)
}
const experiments = {
'https - no keepalive' () {
return makeParallelRequests(resolve => {
https.get(httpsNoKeepAliveOptions, res => {
res
.pipe(
new Writable({
write (chunk, encoding, callback) {
callback()
}
})
)
.on('finish', resolve)
})
})
},
'https - keepalive' () {
return makeParallelRequests(resolve => {
https.get(httpsKeepAliveOptions, res => {
res
.pipe(
new Writable({
write (chunk, encoding, callback) {
callback()
}
})
)
.on('finish', resolve)
})
})
},
'undici - pipeline' () {
return makeParallelRequests(resolve => {
dispatcher
.pipeline(undiciOptions, data => {
return data.body
})
.end()
.pipe(
new Writable({
write (chunk, encoding, callback) {
callback()
}
})
)
.on('finish', resolve)
})
},
'undici - request' () {
return makeParallelRequests(resolve => {
dispatcher.request(undiciOptions).then(({ body }) => {
body
.pipe(
new Writable({
write (chunk, encoding, callback) {
callback()
}
})
)
.on('finish', resolve)
})
})
},
'undici - stream' () {
return makeParallelRequests(resolve => {
return dispatcher
.stream(undiciOptions, () => {
return new Writable({
write (chunk, encoding, callback) {
callback()
}
})
})
.then(resolve)
})
},
'undici - dispatch' () {
return makeParallelRequests(resolve => {
dispatcher.dispatch(undiciOptions, new SimpleRequest(resolve))
})
}
}
if (process.env.PORT) {
// fetch does not support the socket
experiments['undici - fetch'] = () => {
return makeParallelRequests(resolve => {
fetch(dest.url, {}).then(res => {
res.body.pipeTo(new WritableStream({ write () { }, close () { resolve() } }))
}).catch(console.log)
})
}
}
async function main () {
const { cronometro } = await import('cronometro')
cronometro(
experiments,
{
iterations,
errorThreshold,
print: false
},
(err, results) => {
if (err) {
throw err
}
printResults(results)
dispatcher.destroy()
}
)
}
if (isMainThread) {
main()
} else {
module.exports = main
}
================================================
FILE: benchmarks/benchmark.js
================================================
'use strict'
const http = require('node:http')
const os = require('node:os')
const path = require('node:path')
const { Writable } = require('node:stream')
const { isMainThread } = require('node:worker_threads')
const { Pool, Client, fetch, Agent, setGlobalDispatcher } = require('..')
const { makeParallelRequests, printResults } = require('./_util')
let nodeFetch
const axios = require('axios')
let superagent
let got
const { promisify } = require('node:util')
const request = promisify(require('request'))
const iterations = (parseInt(process.env.SAMPLES, 10) || 10) + 1
const errorThreshold = parseInt(process.env.ERROR_THRESHOLD, 10) || 3
const connections = parseInt(process.env.CONNECTIONS, 10) || 50
const pipelining = parseInt(process.env.PIPELINING, 10) || 10
const headersTimeout = parseInt(process.env.HEADERS_TIMEOUT, 10) || 0
const bodyTimeout = parseInt(process.env.BODY_TIMEOUT, 10) || 0
const dest = {}
if (process.env.PORT) {
dest.port = process.env.PORT
dest.url = `http://localhost:${process.env.PORT}`
} else {
dest.url = 'http://localhost'
dest.socketPath = path.join(os.tmpdir(), 'undici.sock')
}
/** @type {http.RequestOptions} */
const httpBaseOptions = {
protocol: 'http:',
hostname: 'localhost',
method: 'GET',
path: '/',
...dest
}
/** @type {http.RequestOptions} */
const httpNoKeepAliveOptions = {
...httpBaseOptions,
agent: new http.Agent({
keepAlive: false,
maxSockets: connections
})
}
/** @type {http.RequestOptions} */
const httpKeepAliveOptions = {
...httpBaseOptions,
agent: new http.Agent({
keepAlive: true,
maxSockets: connections
})
}
const axiosAgent = new http.Agent({
keepAlive: true,
maxSockets: connections
})
const fetchAgent = new http.Agent({
keepAlive: true,
maxSockets: connections
})
const gotAgent = new http.Agent({
keepAlive: true,
maxSockets: connections
})
const requestAgent = new http.Agent({
keepAlive: true,
maxSockets: connections
})
const superagentAgent = new http.Agent({
keepAlive: true,
maxSockets: connections
})
const undiciOptions = {
path: '/',
method: 'GET',
blocking: false,
reset: false,
headersTimeout,
bodyTimeout
}
const Class = connections > 1 ? Pool : Client
const dispatcher = new Class(httpBaseOptions.url, {
pipelining,
connections,
...dest
})
setGlobalDispatcher(new Agent({
pipelining,
connections,
connect: {
rejectUnauthorized: false
}
}))
class SimpleRequest {
constructor (resolve) {
this.dst = new Writable({
write (chunk, encoding, callback) {
callback()
}
}).on('finish', resolve)
}
onConnect (abort) { }
onHeaders (statusCode, headers, resume) {
this.dst.on('drain', resume)
}
onData (chunk) {
return this.dst.write(chunk)
}
onComplete () {
this.dst.end()
}
onError (err) {
throw err
}
}
const experiments = {
'http - no keepalive' () {
return makeParallelRequests(resolve => {
http.get(httpNoKeepAliveOptions, res => {
res
.pipe(
new Writable({
write (chunk, encoding, callback) {
callback()
}
})
)
.on('finish', resolve)
})
})
},
'http - keepalive' () {
return makeParallelRequests(resolve => {
http.get(httpKeepAliveOptions, res => {
res
.pipe(
new Writable({
write (chunk, encoding, callback) {
callback()
}
})
)
.on('finish', resolve)
})
})
},
'undici - pipeline' () {
return makeParallelRequests(resolve => {
dispatcher
.pipeline(undiciOptions, ({ body }) => {
return body
})
.end()
.pipe(
new Writable({
write (chunk, encoding, callback) {
callback()
}
})
)
.on('finish', resolve)
})
},
'undici - request' () {
return makeParallelRequests(resolve => {
dispatcher.request(undiciOptions).then(({ body }) => {
body
.pipe(
new Writable({
write (chunk, encoding, callback) {
callback()
}
})
)
.on('finish', resolve)
})
})
},
'undici - stream' () {
return makeParallelRequests(resolve => {
return dispatcher
.stream(undiciOptions, () => {
return new Writable({
write (chunk, encoding, callback) {
callback()
}
})
})
.then(resolve)
})
},
'undici - dispatch' () {
return makeParallelRequests(resolve => {
dispatcher.dispatch(undiciOptions, new SimpleRequest(resolve))
})
}
}
if (process.env.PORT) {
// fetch does not support the socket
experiments['undici - fetch'] = () => {
return makeParallelRequests(resolve => {
fetch(dest.url).then(res => {
res.body.pipeTo(new WritableStream({ write () { }, close () { resolve() } }))
}).catch(console.log)
})
}
experiments['node-fetch'] = () => {
return makeParallelRequests(resolve => {
nodeFetch(dest.url, { agent: fetchAgent }).then(res => {
res.body.pipe(new Writable({
write (chunk, encoding, callback) {
callback()
}
})).on('finish', resolve)
}).catch(console.log)
})
}
const axiosOptions = {
url: dest.url,
method: 'GET',
responseType: 'stream',
httpAgent: axiosAgent
}
experiments.axios = () => {
return makeParallelRequests(resolve => {
axios.request(axiosOptions).then(res => {
res.data.pipe(new Writable({
write (chunk, encoding, callback) {
callback()
}
})).on('finish', resolve)
}).catch(console.log)
})
}
const gotOptions = {
url: dest.url,
method: 'GET',
agent: {
http: gotAgent
},
// avoid body processing
isStream: true
}
experiments.got = () => {
return makeParallelRequests(resolve => {
got(gotOptions).pipe(new Writable({
write (chunk, encoding, callback) {
callback()
}
})).on('finish', resolve)
})
}
const requestOptions = {
url: dest.url,
method: 'GET',
agent: requestAgent,
// avoid body toString
encoding: null
}
experiments.request = () => {
return makeParallelRequests(resolve => {
request(requestOptions).then(() => {
// already body consumed
resolve()
}).catch(console.log)
})
}
experiments.superagent = () => {
return makeParallelRequests(resolve => {
superagent.get(dest.url).pipe(new Writable({
write (chunk, encoding, callback) {
callback()
}
})).on('finish', resolve)
})
}
}
async function main () {
const { cronometro } = await import('cronometro')
const _nodeFetch = await import('node-fetch')
nodeFetch = _nodeFetch.default
const _got = await import('got')
got = _got.default
const _superagent = await import('superagent')
// https://github.com/ladjs/superagent/issues/1540#issue-561464561
superagent = _superagent.agent().use((req) => req.agent(superagentAgent))
cronometro(
experiments,
{
iterations,
errorThreshold,
print: false
},
(err, results) => {
if (err) {
throw err
}
printResults(results)
dispatcher.destroy()
}
)
}
if (isMainThread) {
main()
} else {
module.exports = main
}
================================================
FILE: benchmarks/cache/date.mjs
================================================
'use strict'
import { group, bench, run } from 'mitata'
import { parseHttpDate } from '../../lib/util/date.js'
const DATES = [
// IMF
'Sun, 06 Nov 1994 08:49:37 GMT',
'Thu, 18 Aug 1950 02:01:18 GMT',
'Wed, 11 Dec 2024 23:20:57 GMT',
'Wed, aa Dec 2024 23:20:57 GMT',
'aaa, 06 Dec 2024 23:20:57 GMT',
'Wed, 01 aaa 2024 23:20:57 GMT',
'Wed, 6 Dec 2024 23:20:07 GMT',
'Wed, 06 Dec 2024 3:20:07 GMT',
'Wed, 06 Dec 2024 23:1:07 GMT',
'Wed, 06 Dec 2024 23:01:7 GMT',
'Wed, 06 Dec aaaa 23:01:07 GMT',
'Wed, 06 Dec 2024 aa:01:07 GMT',
'Wed, 06 Dec 2024 23:aa:07 GMT',
'Wed, 06 Dec 2024 23:01:aa GMT',
// RFC850
'Sunday, 06-Nov-94 08:49:37 GMT',
'Thursday, 18-Aug-50 02:01:18 GMT',
'Wednesday, 11-Dec-24 23:20:57 GMT',
'Wednesday, aa Dec 2024 23:20:57 GMT',
'aaa, 06 Dec 2024 23:20:57 GMT',
'Wednesday, 01-aaa-24 23:20:57 GMT',
'Wednesday, 6-Dec-24 23:20:07 GMT',
'Wednesday, 06-Dec-24 3:20:07 GMT',
'Wednesday, 06-Dec-24 23:1:07 GMT',
'Wednesday, 06-Dec-24 23:01:7 GMT',
'Wednesday, 06 Dec-aa 23:01:07 GMT',
'Wednesday, 06-Dec-24 aa:01:07 GMT',
'Wednesday, 06-Dec-24 23:aa:07 GMT',
'Wednesday, 06-Dec-24 23:01:aa GMT',
// asctime()
'Sun Nov 6 08:49:37 1994',
'Thu Aug 18 02:01:18 1950',
'Wed Dec 11 23:20:57 2024',
'Wed Dec aa 23:20:57 2024',
'aaa Dec 06 23:20:57 2024',
'Wed aaa 01 23:20:57 2024',
'Wed Dec 6 23:20:07 2024',
'Wed Dec 06 3:20:07 2024',
'Wed Dec 06 23:1:07 2024',
'Wed Dec 06 23:01:7 2024',
'Wed 06 Dec 23:01:07 aaaa',
'Wed Dec 06 aa:01:07 2024',
'Wed Dec 06 23:aa:07 2024',
'Wed Dec 06 23:01:aa 2024'
]
group(() => {
bench('parseHttpDate', () => {
for (const date of DATES) {
parseHttpDate(date)
}
})
bench('new Date()', () => {
for (const date of DATES) {
// eslint-disable-next-line no-new
new Date(date)
}
})
})
await run()
================================================
FILE: benchmarks/cache/get-field-values.mjs
================================================
import { bench, group, run } from 'mitata'
import { getFieldValues } from '../../lib/web/cache/util.js'
const values = [
'',
'foo',
'invälid',
'foo, ',
'foo, bar',
'foo, bar, baz',
'foo, bar, baz, ',
'foo, bar, baz, , '
]
group('getFieldValues', () => {
bench('getFieldValues', () => {
for (let i = 0; i < values.length; ++i) {
getFieldValues(values[i])
}
})
})
await run()
================================================
FILE: benchmarks/cookies/is-ctl-excluding-htab.mjs
================================================
import { bench, group, run } from 'mitata'
import { isCTLExcludingHtab } from '../../lib/web/cookies/util.js'
const valid = 'Space=Cat; Secure; HttpOnly; Max-Age=2'
const invalid = 'Space=Cat; Secure; HttpOnly; Max-Age=2\x7F'
group('isCTLExcludingHtab', () => {
bench(`valid: ${valid}`, () => {
return isCTLExcludingHtab(valid)
})
bench(`invalid: ${invalid}`, () => {
return isCTLExcludingHtab(invalid)
})
})
await run()
================================================
FILE: benchmarks/cookies/to-imf-date.mjs
================================================
import { bench, group, run } from 'mitata'
import { toIMFDate } from '../../lib/web/cookies/util.js'
const date = new Date()
group('toIMFDate', () => {
bench(`toIMFDate: ${date}`, () => {
return toIMFDate(date)
})
})
await run()
================================================
FILE: benchmarks/cookies/validate-cookie-name.mjs
================================================
import { bench, group, run } from 'mitata'
import { validateCookieName } from '../../lib/web/cookies/util.js'
const valid = 'Cat'
group('validateCookieName', () => {
bench(`valid: ${valid}`, () => {
return validateCookieName(valid)
})
})
await run()
================================================
FILE: benchmarks/cookies/validate-cookie-value.mjs
================================================
import { bench, group, run } from 'mitata'
import { validateCookieValue } from '../../lib/web/cookies/util.js'
const valid = 'Cat'
const wrappedValid = `"${valid}"`
group('validateCookieValue', () => {
bench(`valid: ${valid}`, () => {
return validateCookieValue(valid)
})
bench(`valid: ${wrappedValid}`, () => {
return validateCookieValue(wrappedValid)
})
})
await run()
================================================
FILE: benchmarks/core/is-blob-like.mjs
================================================
import { bench, group, run } from 'mitata'
import { isBlobLike } from '../../lib/core/util.js'
const buffer = Buffer.alloc(1)
const blob = new Blob(['asd'], {
type: 'application/json'
})
const file = new File(['asd'], 'file.txt', {
type: 'text/plain'
})
const blobLikeStream = {
[Symbol.toStringTag]: 'Blob',
stream: () => {}
}
const fileLikeStream = {
stream: () => {},
[Symbol.toStringTag]: 'File'
}
const blobLikeArrayBuffer = {
[Symbol.toStringTag]: 'Blob',
arrayBuffer: () => {}
}
const fileLikeArrayBuffer = {
[Symbol.toStringTag]: 'File',
arrayBuffer: () => {}
}
group('isBlobLike', () => {
bench('blob', () => {
return isBlobLike(blob)
})
bench('file', () => {
return isBlobLike(file)
})
bench('blobLikeStream', () => {
return isBlobLike(blobLikeStream)
})
bench('fileLikeStream', () => {
return isBlobLike(fileLikeStream)
})
bench('fileLikeArrayBuffer', () => {
return isBlobLike(fileLikeArrayBuffer)
})
bench('blobLikeArrayBuffer', () => {
return isBlobLike(blobLikeArrayBuffer)
})
bench('buffer', () => {
return isBlobLike(buffer)
})
bench('null', () => {
return isBlobLike(null)
})
bench('string', () => {
return isBlobLike('invalid')
})
})
await run()
================================================
FILE: benchmarks/core/is-valid-header-char.mjs
================================================
import { bench, group, run } from 'mitata'
import { isValidHeaderChar } from '../../lib/core/util.js'
const html = 'text/html'
const json = 'application/json; charset=UTF-8'
const headerCharRegex = /[^\t\x20-\x7e\x80-\xff]/
/**
* @param {string} characters
*/
function charCodeAtApproach (characters) {
// Validate if characters is a valid field-vchar.
// field-value = *( field-content / obs-fold )
// field-content = field-vchar [ 1*( SP / HTAB ) field-vchar ]
// field-vchar = VCHAR / obs-text
for (let i = 0; i < characters.length; ++i) {
const code = characters.charCodeAt(i)
// not \x20-\x7e, \t and \x80-\xff
if ((code < 0x20 && code !== 0x09) || code === 0x7f || code > 0xff) {
return false
}
}
return true
}
group(`isValidHeaderChar# ${html}`, () => {
bench('regexp.test', () => {
return !headerCharRegex.test(html)
})
bench('regexp.exec', () => {
return headerCharRegex.exec(html) === null
})
bench('charCodeAt', () => {
return charCodeAtApproach(html)
})
bench('isValidHeaderChar', () => {
return isValidHeaderChar(html)
})
})
group(`isValidHeaderChar# ${json}`, () => {
bench('regexp.test', () => {
return !headerCharRegex.test(json)
})
bench('regexp.exec', () => {
return headerCharRegex.exec(json) === null
})
bench('charCodeAt', () => {
return charCodeAtApproach(json)
})
bench('isValidHeaderChar', () => {
return isValidHeaderChar(json)
})
})
await run()
================================================
FILE: benchmarks/core/is-valid-port.mjs
================================================
import { bench, group, run } from 'mitata'
import { isValidPort } from '../../lib/core/util.js'
const string = '1234'
const number = 1234
group('isValidPort', () => {
bench('string', () => {
return isValidPort(string)
})
bench('number', () => {
return isValidPort(number)
})
})
await run()
================================================
FILE: benchmarks/core/parse-headers.mjs
================================================
import { bench, group, run } from 'mitata'
import { parseHeaders } from '../../lib/core/util.js'
const target = [
{
'Content-Type': 'application/json',
Date: 'Wed, 01 Nov 2023 00:00:00 GMT',
'Powered-By': 'NodeJS',
'Content-Encoding': 'gzip',
'Set-Cookie': '__Secure-ID=123; Secure; Domain=example.com',
'Content-Length': '150',
Vary: 'Accept-Encoding, Accept, X-Requested-With'
},
{
'Content-Type': 'text/html; charset=UTF-8',
'Content-Length': '1234',
Date: 'Wed, 06 Dec 2023 12:47:57 GMT',
Server: 'Bing'
},
{
'Content-Type': 'image/jpeg',
'Content-Length': '56789',
Date: 'Wed, 06 Dec 2023 12:48:12 GMT',
Server: 'Bing',
ETag: '"a1b2c3d4e5f6g7h8i9j0"'
},
{
Cookie: 'session_id=1234567890abcdef',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)',
Host: 'www.bing.com',
Accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
'Accept-Language': 'en-US,en;q=0.5',
'Accept-Encoding': 'gzip, deflate, br'
},
{
Location: 'https://www.bing.com/search?q=bing',
Status: '302 Found',
Date: 'Wed, 06 Dec 2023 12:48:27 GMT',
Server: 'Bing',
'Content-Type': 'text/html; charset=UTF-8',
'Content-Length': '0'
},
{
'Content-Type':
'multipart/form-data; boundary=----WebKitFormBoundary1234567890',
'Content-Length': '98765',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)',
Host: 'www.bing.com',
Accept: '*/*',
'Accept-Language': 'en-US,en;q=0.5',
'Accept-Encoding': 'gzip, deflate, br'
},
{
'Content-Type': 'application/json; charset=UTF-8',
'Content-Length': '2345',
Date: 'Wed, 06 Dec 2023 12:48:42 GMT',
Server: 'Bing',
Status: '200 OK',
'Cache-Control': 'no-cache, no-store, must-revalidate'
},
{
Host: 'www.example.com',
Connection: 'keep-alive',
Accept: 'text/html, application/xhtml+xml, application/xml;q=0.9,;q=0.8',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'
}
]
const headers = Array.from(target, (x) =>
Object.entries(x)
.flat()
.map((c) => Buffer.from(c))
)
const headersIrregular = Array.from(
target,
(x) => Object.entries(x)
.flat()
.map((c) => Buffer.from(c.toUpperCase()))
)
// avoid JIT bias
bench('noop', () => {})
bench('noop', () => {})
bench('noop', () => {})
bench('noop', () => {})
bench('noop', () => {})
bench('noop', () => {})
group('parseHeaders', () => {
bench('parseHeaders', () => {
for (let i = 0; i < headers.length; ++i) {
parseHeaders(headers[i])
}
})
bench('parseHeaders (irregular)', () => {
for (let i = 0; i < headersIrregular.length; ++i) {
parseHeaders(headersIrregular[i])
}
})
})
await new Promise((resolve) => setTimeout(resolve, 7000))
await run()
================================================
FILE: benchmarks/core/parse-raw-headers.mjs
================================================
import { bench, group, run } from 'mitata'
import { parseRawHeaders } from '../../lib/core/util.js'
const rawHeadersMixed = ['key', 'value', Buffer.from('key'), Buffer.from('value')]
const rawHeadersOnlyStrings = ['key', 'value', 'key', 'value']
const rawHeadersOnlyBuffers = [Buffer.from('key'), Buffer.from('value'), Buffer.from('key'), Buffer.from('value')]
const rawHeadersContent = ['content-length', 'value', 'content-disposition', 'form-data; name="fieldName"']
group('parseRawHeaders', () => {
bench('only strings', () => {
parseRawHeaders(rawHeadersOnlyStrings)
})
bench('only buffers', () => {
parseRawHeaders(rawHeadersOnlyBuffers)
})
bench('mixed', () => {
parseRawHeaders(rawHeadersMixed)
})
bench('content-disposition special case', () => {
parseRawHeaders(rawHeadersContent)
})
})
await run()
================================================
FILE: benchmarks/core/request-instantiation.mjs
================================================
import { bench, run } from 'mitata'
import Request from '../../lib/core/request.js'
import DecoratorHandler from '../../lib/handler/decorator-handler.js'
const handler = new DecoratorHandler({})
bench('new Request()', () => {
return new Request('https://localhost', { path: '/', method: 'get', body: null }, handler)
})
await run()
================================================
FILE: benchmarks/core/tree.mjs
================================================
import { bench, group, run } from 'mitata'
import { tree } from '../../lib/core/tree.js'
const contentLength = Buffer.from('Content-Length')
const contentLengthUpperCase = Buffer.from('Content-Length'.toUpperCase())
const contentLengthLowerCase = Buffer.from('Content-Length'.toLowerCase())
group('tree.search', () => {
bench('content-length', () => {
tree.lookup(contentLengthLowerCase)
})
bench('CONTENT-LENGTH', () => {
tree.lookup(contentLengthUpperCase)
})
bench('Content-Length', () => {
tree.lookup(contentLength)
})
})
await run()
================================================
FILE: benchmarks/fetch/body-arraybuffer.mjs
================================================
import { group, bench, run } from 'mitata'
import { Response } from '../../lib/web/fetch/response.js'
const settings = {
small: 2 << 8,
middle: 2 << 12,
long: 2 << 16
}
for (const [name, length] of Object.entries(settings)) {
const buffer = Buffer.allocUnsafe(length).map(() => (Math.random() * 100) | 0)
group(`${name} (length ${length})`, () => {
bench('Response#arrayBuffer', async () => {
return await new Response(buffer).arrayBuffer()
})
// for comparison
bench('Response#text', async () => {
return await new Response(buffer).text()
})
})
}
await run()
================================================
FILE: benchmarks/fetch/bytes-match.mjs
================================================
import { createHash } from 'node:crypto'
import { bench, run } from 'mitata'
import { bytesMatch } from '../../lib/web/fetch/util.js'
const body = Buffer.from('Hello world!')
const validSha256Base64 = `sha256-${createHash('sha256').update(body).digest('base64')}`
const invalidSha256Base64 = `sha256-${createHash('sha256').update(body).digest('base64')}`
const validSha256Base64Url = `sha256-${createHash('sha256').update(body).digest('base64url')}`
const invalidSha256Base64Url = `sha256-${createHash('sha256').update(body).digest('base64url')}`
bench('bytesMatch valid sha256 and base64', () => {
bytesMatch(body, validSha256Base64)
})
bench('bytesMatch invalid sha256 and base64', () => {
bytesMatch(body, invalidSha256Base64)
})
bench('bytesMatch valid sha256 and base64url', () => {
bytesMatch(body, validSha256Base64Url)
})
bench('bytesMatch invalid sha256 and base64url', () => {
bytesMatch(body, invalidSha256Base64Url)
})
await run()
================================================
FILE: benchmarks/fetch/headers-length32.mjs
================================================
import { bench, run } from 'mitata'
import { Headers, getHeadersList } from '../../lib/web/fetch/headers.js'
const headers = new Headers(
[
'Origin-Agent-Cluster',
'RTT',
'Accept-CH-Lifetime',
'X-Frame-Options',
'Sec-CH-UA-Platform-Version',
'Digest',
'Cache-Control',
'Sec-CH-UA-Platform',
'If-Range',
'SourceMap',
'Strict-Transport-Security',
'Want-Digest',
'Cross-Origin-Resource-Policy',
'Width',
'Accept-CH',
'Via',
'Set-Cookie',
'Server',
'Sec-Fetch-Dest',
'Sec-CH-UA-Model',
'Access-Control-Request-Method',
'Access-Control-Request-Headers',
'Date',
'Expires',
'DNT',
'Proxy-Authorization',
'Alt-Svc',
'Alt-Used',
'ETag',
'Sec-Fetch-User',
'Sec-CH-UA-Full-Version-List',
'Referrer-Policy'
].map((v) => [v, ''])
)
const headersList = getHeadersList(headers)
const kHeadersSortedMap = Reflect.ownKeys(headersList).find(
(c) => String(c) === 'Symbol(headers map sorted)'
)
bench('Headers@@iterator', () => {
headersList[kHeadersSortedMap] = null
return [...headers]
})
await run()
================================================
FILE: benchmarks/fetch/headers.mjs
================================================
import { bench, group, run } from 'mitata'
import { Headers, getHeadersList } from '../../lib/web/fetch/headers.js'
const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
const charactersLength = characters.length
function generateAsciiString (length) {
let result = ''
for (let i = 0; i < length; ++i) {
result += characters[Math.floor(Math.random() * charactersLength)]
}
return result
}
const settings = {
'fast-path (tiny array)': 4,
'fast-path (small array)': 8,
'fast-path (middle array)': 16,
'fast-path': 32,
'slow-path': 64
}
for (const [name, length] of Object.entries(settings)) {
const headers = new Headers(
Array.from(Array(length), () => [generateAsciiString(12), ''])
)
const headersSorted = new Headers(headers)
const headersList = getHeadersList(headers)
const headersListSorted = getHeadersList(headersSorted)
const kHeadersSortedMap = Reflect.ownKeys(headersList).find(
(c) => String(c) === 'Symbol(headers map sorted)'
)
group(`length ${length} #${name}`, () => {
bench('Headers@@iterator', () => {
// prevention of memoization of results
headersList[kHeadersSortedMap] = null
return [...headers]
})
bench('Headers@@iterator (sorted)', () => {
// prevention of memoization of results
headersListSorted[kHeadersSortedMap] = null
return [...headersSorted]
})
})
}
await run()
================================================
FILE: benchmarks/fetch/is-valid-encoded-url.mjs
================================================
import { bench, run } from 'mitata'
import { isValidEncodedURL } from '../../lib/web/fetch/util.js'
const validUrl = 'https://example.com'
const invalidUrl = 'https://example.com\x00'
bench('isValidEncodedURL valid', () => {
isValidEncodedURL(validUrl)
})
bench('isValidEncodedURL invalid', () => {
isValidEncodedURL(invalidUrl)
})
await run()
================================================
FILE: benchmarks/fetch/is-valid-header-value.mjs
================================================
import { bench, run } from 'mitata'
import { isValidHeaderValue } from '../../lib/web/fetch/util.js'
const valid = 'valid123'
const invalidNUL = 'invalid\x00'
const invalidCR = 'invalid\r'
const invalidLF = 'invalid\n'
const invalidTrailingTab = 'invalid\t'
const invalidLeadingTab = '\tinvalid'
const invalidTrailingSpace = 'invalid '
const invalidLeadingSpace = ' invalid'
bench('isValidHeaderValue valid', () => {
isValidHeaderValue(valid)
})
bench('isValidHeaderValue invalid containing NUL', () => {
isValidHeaderValue(invalidNUL)
})
bench('isValidHeaderValue invalid containing CR', () => {
isValidHeaderValue(invalidCR)
})
bench('isValidHeaderValue invalid containing LF', () => {
isValidHeaderValue(invalidLF)
})
bench('isValidHeaderValue invalid trailing TAB', () => {
isValidHeaderValue(invalidTrailingTab)
})
bench('isValidHeaderValue invalid leading TAB', () => {
isValidHeaderValue(invalidLeadingTab)
})
bench('isValidHeaderValue invalid trailing SPACE', () => {
isValidHeaderValue(invalidTrailingSpace)
})
bench('isValidHeaderValue invalid leading SPACE', () => {
isValidHeaderValue(invalidLeadingSpace)
})
await run()
================================================
FILE: benchmarks/fetch/isomorphic-encode.mjs
================================================
import { bench, group, run } from 'mitata'
import { isomorphicEncode } from '../../lib/web/fetch/util.js'
const characters =
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
const charactersLength = characters.length
function generateAsciiString (length) {
let result = ''
for (let i = 0; i < length; ++i) {
result += characters[Math.floor(Math.random() * charactersLength)]
}
return result
}
const invalidIsomorphicEncodeValueRegex = /[^\x00-\xFF]/ // eslint-disable-line
function isomorphicEncode1 (input) {
for (let i = 0; i < input.length; i++) {
if (input.charCodeAt(i) > 0xff) {
throw new TypeError('Unreachable')
}
}
return input
}
function isomorphicEncode2 (input) {
if (invalidIsomorphicEncodeValueRegex.test(input)) {
throw new TypeError('Unreachable')
}
return input
}
const settings = {
small: 10,
middle: 30,
long: 70
}
for (const [runName, length] of Object.entries(settings)) {
const value = generateAsciiString(length);
[
{ name: `${runName} (valid)`, value },
{
name: `${runName} (invalid)`,
value: `${value.slice(0, -1)}${String.fromCharCode(0xff + 1)}`
}
].forEach(({ name, value }) => {
group(name, () => {
[
{
name: 'original',
fn: isomorphicEncode
},
{
name: 'String#charCodeAt',
fn: isomorphicEncode1
},
{
name: 'RegExp#test',
fn: isomorphicEncode2
}
].forEach(({ name, fn }) => {
bench(name, () => {
try {
return fn(value)
} catch (err) {}
})
})
})
})
}
await run()
================================================
FILE: benchmarks/fetch/request-creation.mjs
================================================
import { bench, run } from 'mitata'
import { Request } from '../../lib/web/fetch/request.js'
const input = 'https://example.com/post'
bench('new Request(input)', () => new Request(input, undefined))
await run()
================================================
FILE: benchmarks/fetch/url-has-https-scheme.mjs
================================================
import { bench, run } from 'mitata'
import { urlHasHttpsScheme } from '../../lib/web/fetch/util.js'
const httpString = 'http://example.com'
const httpObject = { protocol: 'http:' }
const httpsString = 'https://example.com'
const httpsObject = { protocol: 'https:' }
bench('urlHasHttpsScheme "http:" String', () => {
urlHasHttpsScheme(httpString)
})
bench('urlHasHttpsScheme "https:" String', () => {
urlHasHttpsScheme(httpsString)
})
bench('urlHasHttpsScheme "http:" Object', () => {
urlHasHttpsScheme(httpObject)
})
bench('urlHasHttpsScheme "https:" Object', () => {
urlHasHttpsScheme(httpsObject)
})
await run()
================================================
FILE: benchmarks/package.json
================================================
{
"name": "benchmarks",
"scripts": {
"bench": "PORT=3042 concurrently -k -s first npm:bench:server npm:bench:run",
"bench:h2": "PORT=3052 concurrently -k -s first npm:bench:server:h2 npm:bench:run:h2",
"bench-post": "PORT=3042 concurrently -k -s first npm:bench:server npm:bench-post:run",
"bench:server": "node ./server.js",
"bench:server:h2": "node ./server-http2.js",
"prebench:run": "node ./wait.js",
"bench:run": "SAMPLES=100 CONNECTIONS=50 node ./benchmark.js",
"bench:run:h2": "SAMPLES=100 CONNECTIONS=50 node ./benchmark-http2.js",
"prebench-post:run": "node ./wait.js",
"bench-post:run": "SAMPLES=100 CONNECTIONS=50 node ./post-benchmark.js"
},
"dependencies": {
"axios": "^1.6.7",
"concurrently": "^9.0.0",
"cronometro": "^5.3.0",
"got": "^14.2.0",
"mitata": "^1.0.4",
"node-fetch": "^3.3.2",
"request": "^2.88.2",
"superagent": "^10.0.0",
"tinybench": "^5.0.0",
"uWebSockets.js": "uNetworking/uWebSockets.js#v20.58.0",
"wait-on": "^9.0.1"
}
}
================================================
FILE: benchmarks/post-benchmark.js
================================================
'use strict'
const http = require('node:http')
const os = require('node:os')
const path = require('node:path')
const { Writable, Readable, pipeline } = require('node:stream')
const { isMainThread } = require('node:worker_threads')
const { Pool, Client, fetch, Agent, setGlobalDispatcher } = require('..')
const { makeParallelRequests, printResults } = require('./_util')
let nodeFetch
const axios = require('axios')
let superagent
let got
const { promisify } = require('node:util')
const request = promisify(require('request'))
const iterations = (parseInt(process.env.SAMPLES, 10) || 10) + 1
const errorThreshold = parseInt(process.env.ERROR_THRESHOLD, 10) || 3
const connections = parseInt(process.env.CONNECTIONS, 10) || 50
const pipelining = parseInt(process.env.PIPELINING, 10) || 10
const headersTimeout = parseInt(process.env.HEADERS_TIMEOUT, 10) || 0
const bodyTimeout = parseInt(process.env.BODY_TIMEOUT, 10) || 0
const dest = {}
const data = '_'.repeat(128 * 1024)
const dataLength = `${Buffer.byteLength(data)}`
if (process.env.PORT) {
dest.port = process.env.PORT
dest.url = `http://localhost:${process.env.PORT}`
} else {
dest.url = 'http://localhost'
dest.socketPath = path.join(os.tmpdir(), 'undici.sock')
}
const headers = {
'Content-Type': 'text/plain; charset=UTF-8',
'Content-Length': dataLength
}
/** @type {http.RequestOptions} */
const httpBaseOptions = {
protocol: 'http:',
hostname: 'localhost',
method: 'POST',
path: '/',
headers,
...dest
}
/** @type {http.RequestOptions} */
const httpNoKeepAliveOptions = {
...httpBaseOptions,
agent: new http.Agent({
keepAlive: false,
maxSockets: connections
})
}
/** @type {http.RequestOptions} */
const httpKeepAliveOptions = {
...httpBaseOptions,
agent: new http.Agent({
keepAlive: true,
maxSockets: connections
})
}
const axiosAgent = new http.Agent({
keepAlive: true,
maxSockets: connections
})
const fetchAgent = new http.Agent({
keepAlive: true,
maxSockets: connections
})
const gotAgent = new http.Agent({
keepAlive: true,
maxSockets: connections
})
const requestAgent = new http.Agent({
keepAlive: true,
maxSockets: connections
})
const superagentAgent = new http.Agent({
keepAlive: true,
maxSockets: connections
})
/** @type {import("..").Dispatcher.DispatchOptions} */
const undiciOptions = {
path: '/',
method: 'POST',
headersTimeout,
bodyTimeout,
body: data,
headers
}
const Class = connections > 1 ? Pool : Client
const dispatcher = new Class(httpBaseOptions.url, {
pipelining,
connections,
...dest
})
setGlobalDispatcher(new Agent({
pipelining,
connections,
connect: {
rejectUnauthorized: false
}
}))
class SimpleRequest {
constructor (resolve) {
this.dst = new Writable({
write (chunk, encoding, callback) {
callback()
}
}).on('finish', resolve)
}
onConnect (abort) { }
onHeaders (statusCode, headers, resume) {
this.dst.on('drain', resume)
}
onData (chunk) {
return this.dst.write(chunk)
}
onComplete () {
this.dst.end()
}
onError (err) {
throw err
}
}
const experiments = {
'http - no keepalive' () {
return makeParallelRequests(resolve => {
const request = http.request(httpNoKeepAliveOptions, res => {
res
.pipe(
new Writable({
write (chunk, encoding, callback) {
callback()
}
})
)
.on('finish', resolve)
})
request.end(data)
})
},
'http - keepalive' () {
return makeParallelRequests(resolve => {
const request = http.request(httpKeepAliveOptions, res => {
res
.pipe(
new Writable({
write (chunk, encoding, callback) {
callback()
}
})
)
.on('finish', resolve)
})
request.end(data)
})
},
'undici - pipeline' () {
return makeParallelRequests(resolve => {
pipeline(
new Readable({
read () {
this.push(data)
this.push(null)
}
}),
dispatcher.pipeline(undiciOptions, ({ body }) => {
return body
}),
new Writable({
write (chunk, encoding, callback) {
callback()
}
}),
(err) => {
if (err != null) {
console.log(err)
}
resolve()
}
)
})
},
'undici - request' () {
return makeParallelRequests(resolve => {
dispatcher.request(undiciOptions).then(({ body }) => {
body
.pipe(
new Writable({
write (chunk, encoding, callback) {
callback()
}
})
)
.on('finish', resolve)
})
})
},
'undici - stream' () {
return makeParallelRequests(resolve => {
return dispatcher
.stream(undiciOptions, () => {
return new Writable({
write (chunk, encoding, callback) {
callback()
}
})
})
.then(resolve)
})
},
'undici - dispatch' () {
return makeParallelRequests(resolve => {
dispatcher.dispatch(undiciOptions, new SimpleRequest(resolve))
})
}
}
if (process.env.PORT) {
/** @type {RequestInit} */
const fetchOptions = {
method: 'POST',
body: data,
headers
}
// fetch does not support the socket
experiments['undici - fetch'] = () => {
return makeParallelRequests(resolve => {
fetch(dest.url, fetchOptions).then(res => {
res.body.pipeTo(new WritableStream({ write () { }, close () { resolve() } }))
}).catch(console.log)
})
}
const nodeFetchOptions = {
...fetchOptions,
agent: fetchAgent
}
experiments['node-fetch'] = () => {
return makeParallelRequests(resolve => {
nodeFetch(dest.url, nodeFetchOptions).then(res => {
res.body.pipe(new Writable({
write (chunk, encoding, callback) {
callback()
}
})).on('finish', resolve)
}).catch(console.log)
})
}
const axiosOptions = {
url: dest.url,
method: 'POST',
headers,
responseType: 'stream',
httpAgent: axiosAgent,
data
}
experiments.axios = () => {
return makeParallelRequests(resolve => {
axios.request(axiosOptions).then(res => {
res.data.pipe(new Writable({
write (chunk, encoding, callback) {
callback()
}
})).on('finish', resolve)
}).catch(console.log)
})
}
const gotOptions = {
url: dest.url,
method: 'POST',
headers,
agent: {
http: gotAgent
},
// avoid body processing
isStream: true,
body: data
}
experiments.got = () => {
return makeParallelRequests(resolve => {
got(gotOptions).pipe(new Writable({
write (chunk, encoding, callback) {
callback()
}
})).on('finish', resolve)
})
}
const requestOptions = {
url: dest.url,
method: 'POST',
headers,
agent: requestAgent,
body: data,
// avoid body toString
encoding: null
}
experiments.request = () => {
return makeParallelRequests(resolve => {
request(requestOptions).then(() => {
// already body consumed
resolve()
}).catch(console.log)
})
}
experiments.superagent = () => {
return makeParallelRequests(resolve => {
superagent
.post(dest.url)
.send(data)
.set('Content-Type', 'text/plain; charset=UTF-8')
.set('Content-Length', dataLength)
.pipe(new Writable({
write (chunk, encoding, callback) {
callback()
}
})).on('finish', resolve)
})
}
}
async function main () {
const { cronometro } = await import('cronometro')
const _nodeFetch = await import('node-fetch')
nodeFetch = _nodeFetch.default
const _got = await import('got')
got = _got.default
const _superagent = await import('superagent')
// https://github.com/ladjs/superagent/issues/1540#issue-561464561
superagent = _superagent.agent().use((req) => req.agent(superagentAgent))
cronometro(
experiments,
{
iterations,
errorThreshold,
print: false
},
(err, results) => {
if (err) {
throw err
}
printResults(results)
dispatcher.destroy()
}
)
}
if (isMainThread) {
main()
} else {
module.exports = main
}
================================================
FILE: benchmarks/server-http2.js
================================================
'use strict'
const { unlinkSync, readFileSync } = require('node:fs')
const { createSecureServer } = require('node:http2')
const os = require('node:os')
const path = require('node:path')
const cluster = require('node:cluster')
const key = readFileSync(path.join(__dirname, '..', 'test', 'fixtures', 'key.pem'), 'utf8')
const cert = readFileSync(path.join(__dirname, '..', 'test', 'fixtures', 'cert.pem'), 'utf8')
const socketPath = path.join(os.tmpdir(), 'undici.sock')
const port = process.env.PORT || socketPath
const timeout = parseInt(process.env.TIMEOUT, 10) || 1
const workers = parseInt(process.env.WORKERS) || os.cpus().length
const sessionTimeout = 600e3 // 10 minutes
if (cluster.isPrimary) {
try {
unlinkSync(socketPath)
} catch (_) {
// Do nothing if the socket does not exist
}
for (let i = 0; i < workers; i++) {
cluster.fork()
}
} else {
const buf = Buffer.alloc(64 * 1024, '_')
const server = createSecureServer(
{
key,
cert,
allowHTTP1: true,
sessionTimeout
}
)
server.on('stream', (stream) => {
setTimeout(() => {
stream.setEncoding('utf-8').end(buf)
}, timeout)
stream.respond({
'content-type': 'text/plain; charset=utf-8',
':status': 200
})
})
server.keepAliveTimeout = 600e3
server.listen(port)
}
================================================
FILE: benchmarks/server-https.js
================================================
'use strict'
const { unlinkSync, readFileSync } = require('node:fs')
const { createServer } = require('node:https')
const os = require('node:os')
const path = require('node:path')
const cluster = require('node:cluster')
const key = readFileSync(path.join(__dirname, '..', 'test', 'fixtures', 'key.pem'), 'utf8')
const cert = readFileSync(path.join(__dirname, '..', 'test', 'fixtures', 'cert.pem'), 'utf8')
const socketPath = path.join(os.tmpdir(), 'undici.sock')
const port = process.env.PORT || socketPath
const timeout = parseInt(process.env.TIMEOUT, 10) || 1
const workers = parseInt(process.env.WORKERS) || os.cpus().length
if (cluster.isPrimary) {
try {
unlinkSync(socketPath)
} catch (_) {
// Do nothing if the socket does not exist
}
for (let i = 0; i < workers; i++) {
cluster.fork()
}
} else {
const buf = Buffer.alloc(64 * 1024, '_')
const server = createServer({
key,
cert,
keepAliveTimeout: 600e3
}, (req, res) => {
setTimeout(() => {
res.end(buf)
}, timeout)
})
server.listen(port)
}
================================================
FILE: benchmarks/server.js
================================================
'use strict'
const { unlinkSync } = require('node:fs')
const { createServer } = require('node:http')
const os = require('node:os')
const path = require('node:path')
const cluster = require('node:cluster')
const socketPath = path.join(os.tmpdir(), 'undici.sock')
const port = process.env.PORT || socketPath
const timeout = parseInt(process.env.TIMEOUT, 10) || 1
const workers = parseInt(process.env.WORKERS) || os.cpus().length
if (cluster.isPrimary) {
try {
unlinkSync(socketPath)
} catch (_) {
// Do nothing if the socket does not exist
}
for (let i = 0; i < workers; i++) {
cluster.fork()
}
} else {
const buf = Buffer.alloc(64 * 1024, '_')
const headers = {
'Content-Length': `${buf.byteLength}`,
'Content-Type': 'text/plain; charset=UTF-8'
}
let i = 0
const server = createServer((_req, res) => {
i++
setTimeout(() => {
res.writeHead(200, headers)
res.end(buf)
}, timeout)
}).listen(port)
server.keepAliveTimeout = 600e3
setInterval(() => {
console.log(`Worker ${process.pid} processed ${i} requests`)
}, 5000)
}
================================================
FILE: benchmarks/timers/compare-timer-getters.mjs
================================================
import { bench, group, run } from 'mitata'
group('timers', () => {
bench('Date.now()', () => {
Date.now()
})
bench('performance.now()', () => {
performance.now()
})
bench('Math.trunc(performance.now())', () => {
Math.trunc(performance.now())
})
bench('process.uptime()', () => {
process.uptime()
})
})
await run()
================================================
FILE: benchmarks/utils/date.mjs
================================================
import { Bench } from 'tinybench'
import { parseHttpDate } from '../../lib/util/date.js'
const asctime = 'Sun Nov 6 08:49:37 1994'
const rfc850 = 'Sunday, 06-Nov-94 08:49:37 GMT'
const imf = 'Sun, 06 Nov 1994 08:49:37 GMT'
console.assert(parseHttpDate(asctime) instanceof Date, 'asctime should return a Date')
console.assert(parseHttpDate(rfc850) instanceof Date, 'rfc850 should return a Date')
console.assert(parseHttpDate(imf) instanceof Date, 'imf should return a Date')
const bench = new Bench({ name: 'parseHttpDate' })
bench
.add('asctime', () => {
parseHttpDate(asctime)
})
.add('rfc850', () => {
parseHttpDate(rfc850)
})
.add('imf', () => {
parseHttpDate(imf)
})
await bench.run()
console.log(bench.name)
console.table(bench.table())
================================================
FILE: benchmarks/wait.js
================================================
'use strict'
const os = require('node:os')
const path = require('node:path')
const waitOn = require('wait-on')
const socketPath = path.join(os.tmpdir(), 'undici.sock')
let resources
if (process.env.PORT) {
resources = [`http-get://localhost:${process.env.PORT}/`]
} else {
resources = [`http-get://unix:${socketPath}:/`]
}
waitOn({
resources,
timeout: 5000
}).catch((err) => {
console.error(err)
process.exitCode = 1
})
================================================
FILE: benchmarks/webidl/webidl-is.mjs
================================================
import { bench, run, barplot } from 'mitata'
import { Headers, FormData } from '../../index.js'
import { webidl } from '../../lib/web/webidl/index.js'
const headers = new Headers()
const fd = new FormData()
barplot(() => {
bench('webidl.is.FormData (ok)', () => {
return webidl.is.FormData(fd)
})
bench('webidl.is.FormData (bad)', () => {
return !webidl.is.FormData(headers)
})
bench('instanceof (ok)', () => {
return fd instanceof FormData
})
bench('instanceof (bad)', () => {
return !(headers instanceof FormData)
})
})
await run()
================================================
FILE: benchmarks/websocket/generate-mask.mjs
================================================
import { randomBytes } from 'node:crypto'
import { bench, summary, run } from 'mitata'
import { generateMask } from '../../lib/web/websocket/frame.js'
summary(() => {
bench('generateMask', () => generateMask())
bench('crypto.randomBytes(4)', () => randomBytes(4))
})
await run()
================================================
FILE: benchmarks/websocket/is-valid-subprotocol.mjs
================================================
import { bench, group, run } from 'mitata'
import { isValidSubprotocol } from '../../lib/web/websocket/util.js'
const valid = 'valid'
const invalid = 'invalid '
group('isValidSubprotocol', () => {
bench(`valid: ${valid}`, () => {
return isValidSubprotocol(valid)
})
bench(`invalid: ${invalid}`, () => {
return isValidSubprotocol(invalid)
})
})
await run()
================================================
FILE: benchmarks/websocket/messageevent.mjs
================================================
import { bench, group, run } from 'mitata'
import { createFastMessageEvent, MessageEvent as UndiciMessageEvent } from '../../lib/web/websocket/events.js'
const { port1, port2 } = new MessageChannel()
group('MessageEvent instantiation', () => {
bench('undici - fast MessageEvent init', () => {
return createFastMessageEvent('event', { data: null, ports: [port1, port2] })
})
bench('undici - MessageEvent init', () => {
return new UndiciMessageEvent('event', { data: null, ports: [port1, port2] })
})
bench('global - MessageEvent init', () => {
// eslint-disable-next-line no-restricted-globals
return new MessageEvent('event', { data: null, ports: [port1, port2] })
})
})
await run()
================================================
FILE: benchmarks/websocket-benchmark.mjs
================================================
// @ts-check
import { bench } from './_util/runner.js'
import { formatBytes } from './_util/index.js'
import { WebSocket, WebSocketStream } from '../index.js'
import { WebSocket as WsWebSocket } from 'ws'
/**
* @type {Record import('./_util/runner.js').BenchMarkHandler; connect: (url: string) => Promise; binaries: (string | Uint8Array)[] }>}
*/
const experiments = {}
/**
* @type {Record}
*/
const experimentsInfo = {}
/**
* @type {any[]}
*/
const connections = []
const binary = Buffer.alloc(256 * 1024, '_')
const binaries = [binary, binary.subarray(0, 256 * 1024).toString('utf-8')]
experiments['undici'] = {
fn: (ws, binary) => {
if (!(ws instanceof WebSocket)) {
throw new Error("'undici' websocket are expected.")
}
return (ev) => {
ws.addEventListener(
'message',
() => {
ev.end()
},
{ once: true }
)
ev.start()
ws.send(binary)
}
},
connect: async (url) => {
const ws = new WebSocket(url)
await /** @type {Promise} */ (
new Promise((resolve, reject) => {
function onOpen () {
resolve()
ws.removeEventListener('open', onOpen)
ws.removeEventListener('error', onError)
}
function onError (err) {
reject(err)
ws.removeEventListener('open', onOpen)
ws.removeEventListener('error', onError)
}
ws.addEventListener('open', onOpen)
ws.addEventListener('error', onError)
})
)
// avoid create blob
ws.binaryType = 'arraybuffer'
return ws
},
binaries
}
experiments['undici - stream'] = {
fn: (ws, binary) => {
/** @type {ReadableStreamDefaultReader} */
const reader = ws.reader
/** @type {WritableStreamDefaultWriter} */
const writer = ws.writer
return async (ev) => {
ev.start()
await writer.write(binary)
await reader.read()
ev.end()
}
},
connect: async (url) => {
const ws = new WebSocketStream(url)
const { readable, writable } = await ws.opened
const reader = readable.getReader()
const writer = writable.getWriter()
// @ts-ignore
return { reader, writer, close: () => ws.close() }
},
binaries
}
experiments['ws'] = {
fn: (ws, binary) => {
if (!(ws instanceof WsWebSocket)) {
throw new Error("'ws' websocket are expected.")
}
return (ev) => {
ws.once('message', () => {
ev.end()
})
ev.start()
ws.send(binary)
}
},
connect: async (url) => {
const ws = new WsWebSocket(url, { maxPayload: 1024 * 1024 * 1024 })
await /** @type {Promise} */ (
new Promise((resolve, reject) => {
function onOpen () {
resolve()
ws.off('open', onOpen)
ws.off('error', onError)
}
function onError (err) {
reject(err)
ws.off('open', onOpen)
ws.off('error', onError)
}
ws.on('open', onOpen)
ws.on('error', onError)
})
)
ws.binaryType = 'arraybuffer'
return ws
},
binaries
}
async function init () {
/** @type {Record} */
const round = {}
const keys = Object.keys(experiments)
for (let i = 0; i < keys.length; ++i) {
const name = keys[i]
const { fn, connect, binaries } = experiments[name]
const ws = await connect('ws://localhost:8080')
const needShowBytes = binaries.length !== 2 || typeof binaries[0] === typeof binaries[1]
for (let i = 0; i < binaries.length; ++i) {
const binary = binaries[i]
const bytes = Buffer.byteLength(binary)
const binaryType = typeof binary === 'string' ? 'string' : 'binary'
const roundName = needShowBytes
? `${name} [${formatBytes(bytes)} (${binaryType})]`
: `${name} [${binaryType}]`
round[roundName] = fn(ws, binary)
experimentsInfo[roundName] = { bytes, binaryType }
}
connections.push(ws)
}
return round
}
init()
.then((round) => bench(round, {
minSamples: 2048
}))
.then((results) => {
print(results)
for (const ws of connections) {
ws.close()
}
}, (err) => {
process.nextTick((err) => {
throw err
}, err)
})
/**
* @param {{ name: string; average: number; iterationPerSecond: number; }[]} results
*/
function print (results) {
for (const { name, average, iterationPerSecond } of results) {
const { bytes } = experimentsInfo[name]
console.log(
`${name}: transferred ${formatBytes((bytes / average) * 1e9)} Bytes/s (${iterationPerSecond.toFixed(4)} per/sec)`
)
}
}
export {}
================================================
FILE: benchmarks/websocket-echo-server.mjs
================================================
import { Worker, isMainThread, parentPort, threadId } from 'node:worker_threads'
import { cpus } from 'node:os'
import url from 'node:url'
import uws from 'uWebSockets.js'
const __filename = url.fileURLToPath(import.meta.url)
const app = uws.App()
if (isMainThread) {
for (let i = cpus().length - 1; i >= 0; --i) {
new Worker(__filename).on('message', (workerAppDescriptor) => {
app.addChildAppDescriptor(workerAppDescriptor)
})
}
} else {
app
.ws('/*', {
compression: uws.DISABLED,
maxPayloadLength: 1024 * 1024 * 1024,
maxBackpressure: 1 * 1024 * 1024,
idleTimeout: 60,
message: (ws, message, isBinary) => {
/* Here we echo the message back, using compression if available */
const ok = ws.send(message, isBinary) // eslint-disable-line
}
})
.get('/*', (res, req) => {
/* It does Http as well */
res
.writeStatus('200 OK')
.end('Hello there!')
})
parentPort.postMessage(app.getDescriptor())
}
app.listen(8080, (listenSocket) => {
if (listenSocket) {
if (threadId === 0) {
console.log('Listening to port 8080')
} else {
console.log(`Listening to port 8080 from thread ${threadId}`)
}
}
})
================================================
FILE: build/wasm.js
================================================
'use strict'
const WASM_BUILDER_CONTAINER = 'ghcr.io/nodejs/wasm-builder@sha256:975f391d907e42a75b8c72eb77c782181e941608687d4d8694c3e9df415a0970' // v0.0.9
const { execSync, execFileSync } = require('node:child_process')
const { writeFileSync, readFileSync } = require('node:fs')
const { join, resolve } = require('node:path')
const ROOT = resolve(__dirname, '../')
const WASM_SRC = resolve(__dirname, '../deps/llhttp')
const WASM_OUT = resolve(__dirname, '../lib/llhttp')
// These are defined by build environment
const WASM_CC = process.env.WASM_CC || 'clang'
let WASM_CFLAGS = process.env.WASM_CFLAGS || '--sysroot=/usr/share/wasi-sysroot -target wasm32-unknown-wasi'
let WASM_LDFLAGS = process.env.WASM_LDFLAGS || ''
const WASM_LDLIBS = process.env.WASM_LDLIBS || ''
const WASM_OPT = process.env.WASM_OPT || 'wasm-opt'
// For compatibility with Node.js' `configure --shared-builtin-undici/undici-path ...`
const EXTERNAL_PATH = process.env.EXTERNAL_PATH
// These are relevant for undici and should not be overridden
WASM_CFLAGS += ' -Ofast -fno-exceptions -fvisibility=hidden -mexec-model=reactor'
WASM_LDFLAGS += ' -Wl,-error-limit=0 -Wl,-O3 -Wl,--lto-O3 -Wl,--strip-all'
WASM_LDFLAGS += ' -Wl,--allow-undefined -Wl,--export-dynamic -Wl,--export-table'
WASM_LDFLAGS += ' -Wl,--export=malloc -Wl,--export=free -Wl,--no-entry'
const WASM_OPT_FLAGS = '-O4 --converge --strip-debug --strip-dwarf --strip-producers'
const writeWasmChunk = (path, dest) => {
const base64 = readFileSync(join(WASM_OUT, path)).toString('base64')
writeFileSync(join(WASM_OUT, dest), `'use strict'
const { Buffer } = require('node:buffer')
const wasmBase64 = '${base64}'
let wasmBuffer
Object.defineProperty(module, 'exports', {
get: () => {
return wasmBuffer
? wasmBuffer
: (wasmBuffer = Buffer.from(wasmBase64, 'base64'))
}
})
`)
}
let platform = process.env.WASM_PLATFORM
if (!platform && process.argv[2]) {
platform = execSync('docker info -f "{{.OSType}}/{{.Architecture}}"').toString().trim()
}
if (process.argv[2] === '--docker') {
let cmd = `docker run --rm --platform=${platform.toString().trim()} `
if (process.platform === 'linux') {
cmd += ` --user ${process.getuid()}:${process.getegid()}`
}
cmd += ` --mount type=bind,source=${ROOT}/lib/llhttp,target=/home/node/build/lib/llhttp \
--mount type=bind,source=${ROOT}/build,target=/home/node/build/build \
--mount type=bind,source=${ROOT}/deps,target=/home/node/build/deps \
-t ${WASM_BUILDER_CONTAINER} node build/wasm.js`
console.log(`> ${cmd}\n\n`)
execSync(cmd, { stdio: 'inherit' })
process.exit(0) // eslint-disable-line n/no-process-exit
}
const hasApk = (function () {
try { execSync('command -v apk'); return true } catch { return false }
})()
const hasOptimizer = (function () {
try { execSync(`${WASM_OPT} --version`); return true } catch { return false }
})()
if (hasApk) {
// Gather information about the tools used for the build
const buildInfo = execSync('apk info -v').toString()
if (!buildInfo.includes('wasi-sdk')) {
throw new Error('Failed to generate build environment information')
}
console.log(buildInfo)
}
// Build wasm binary
execSync(`${WASM_CC} ${WASM_CFLAGS} ${WASM_LDFLAGS} \
${join(WASM_SRC, 'src')}/*.c \
-I${join(WASM_SRC, 'include')} \
-o ${join(WASM_OUT, 'llhttp.wasm')} \
${WASM_LDLIBS}`, { stdio: 'inherit' })
if (hasOptimizer) {
execSync(`${WASM_OPT} ${WASM_OPT_FLAGS} -o ${join(WASM_OUT, 'llhttp.wasm')} ${join(WASM_OUT, 'llhttp.wasm')}`, { stdio: 'inherit' })
}
writeWasmChunk('llhttp.wasm', 'llhttp-wasm.js')
// Build wasm simd binary
execSync(`${WASM_CC} ${WASM_CFLAGS} -msimd128 ${WASM_LDFLAGS} \
${join(WASM_SRC, 'src')}/*.c \
-I${join(WASM_SRC, 'include')} \
-o ${join(WASM_OUT, 'llhttp_simd.wasm')} \
${WASM_LDLIBS}`, { stdio: 'inherit' })
if (hasOptimizer) {
// Split WASM_OPT_FLAGS into an array, if not empty
const wasmOptFlagsArray = WASM_OPT_FLAGS ? WASM_OPT_FLAGS.split(/\s+/).filter(Boolean) : []
execFileSync(
WASM_OPT,
[
...wasmOptFlagsArray,
'--enable-simd',
'-o',
join(WASM_OUT, 'llhttp_simd.wasm'),
join(WASM_OUT, 'llhttp_simd.wasm')
],
{ stdio: 'inherit' }
)
}
writeWasmChunk('llhttp_simd.wasm', 'llhttp_simd-wasm.js')
// For compatibility with Node.js' `configure --shared-builtin-undici/undici-path ...`
if (EXTERNAL_PATH) {
writeFileSync(join(ROOT, 'loader.js'), `
'use strict'
globalThis.__UNDICI_IS_NODE__ = true
module.exports = require('node:module').createRequire('${EXTERNAL_PATH}/loader.js')('./index-fetch.js')
delete globalThis.__UNDICI_IS_NODE__
`)
}
================================================
FILE: deps/llhttp/include/llhttp.h
================================================
#ifndef INCLUDE_LLHTTP_H_
#define INCLUDE_LLHTTP_H_
#define LLHTTP_VERSION_MAJOR 9
#define LLHTTP_VERSION_MINOR 3
#define LLHTTP_VERSION_PATCH 0
#ifndef INCLUDE_LLHTTP_ITSELF_H_
#define INCLUDE_LLHTTP_ITSELF_H_
#ifdef __cplusplus
extern "C" {
#endif
#include
typedef struct llhttp__internal_s llhttp__internal_t;
struct llhttp__internal_s {
int32_t _index;
void* _span_pos0;
void* _span_cb0;
int32_t error;
const char* reason;
const char* error_pos;
void* data;
void* _current;
uint64_t content_length;
uint8_t type;
uint8_t method;
uint8_t http_major;
uint8_t http_minor;
uint8_t header_state;
uint16_t lenient_flags;
uint8_t upgrade;
uint8_t finish;
uint16_t flags;
uint16_t status_code;
uint8_t initial_message_completed;
void* settings;
};
int llhttp__internal_init(llhttp__internal_t* s);
int llhttp__internal_execute(llhttp__internal_t* s, const char* p, const char* endp);
#ifdef __cplusplus
} /* extern "C" */
#endif
#endif /* INCLUDE_LLHTTP_ITSELF_H_ */
#ifndef LLLLHTTP_C_HEADERS_
#define LLLLHTTP_C_HEADERS_
#ifdef __cplusplus
extern "C" {
#endif
enum llhttp_errno {
HPE_OK = 0,
HPE_INTERNAL = 1,
HPE_STRICT = 2,
HPE_CR_EXPECTED = 25,
HPE_LF_EXPECTED = 3,
HPE_UNEXPECTED_CONTENT_LENGTH = 4,
HPE_UNEXPECTED_SPACE = 30,
HPE_CLOSED_CONNECTION = 5,
HPE_INVALID_METHOD = 6,
HPE_INVALID_URL = 7,
HPE_INVALID_CONSTANT = 8,
HPE_INVALID_VERSION = 9,
HPE_INVALID_HEADER_TOKEN = 10,
HPE_INVALID_CONTENT_LENGTH = 11,
HPE_INVALID_CHUNK_SIZE = 12,
HPE_INVALID_STATUS = 13,
HPE_INVALID_EOF_STATE = 14,
HPE_INVALID_TRANSFER_ENCODING = 15,
HPE_CB_MESSAGE_BEGIN = 16,
HPE_CB_HEADERS_COMPLETE = 17,
HPE_CB_MESSAGE_COMPLETE = 18,
HPE_CB_CHUNK_HEADER = 19,
HPE_CB_CHUNK_COMPLETE = 20,
HPE_PAUSED = 21,
HPE_PAUSED_UPGRADE = 22,
HPE_PAUSED_H2_UPGRADE = 23,
HPE_USER = 24,
HPE_CB_URL_COMPLETE = 26,
HPE_CB_STATUS_COMPLETE = 27,
HPE_CB_METHOD_COMPLETE = 32,
HPE_CB_VERSION_COMPLETE = 33,
HPE_CB_HEADER_FIELD_COMPLETE = 28,
HPE_CB_HEADER_VALUE_COMPLETE = 29,
HPE_CB_CHUNK_EXTENSION_NAME_COMPLETE = 34,
HPE_CB_CHUNK_EXTENSION_VALUE_COMPLETE = 35,
HPE_CB_RESET = 31,
HPE_CB_PROTOCOL_COMPLETE = 38
};
typedef enum llhttp_errno llhttp_errno_t;
enum llhttp_flags {
F_CONNECTION_KEEP_ALIVE = 0x1,
F_CONNECTION_CLOSE = 0x2,
F_CONNECTION_UPGRADE = 0x4,
F_CHUNKED = 0x8,
F_UPGRADE = 0x10,
F_CONTENT_LENGTH = 0x20,
F_SKIPBODY = 0x40,
F_TRAILING = 0x80,
F_TRANSFER_ENCODING = 0x200
};
typedef enum llhttp_flags llhttp_flags_t;
enum llhttp_lenient_flags {
LENIENT_HEADERS = 0x1,
LENIENT_CHUNKED_LENGTH = 0x2,
LENIENT_KEEP_ALIVE = 0x4,
LENIENT_TRANSFER_ENCODING = 0x8,
LENIENT_VERSION = 0x10,
LENIENT_DATA_AFTER_CLOSE = 0x20,
LENIENT_OPTIONAL_LF_AFTER_CR = 0x40,
LENIENT_OPTIONAL_CRLF_AFTER_CHUNK = 0x80,
LENIENT_OPTIONAL_CR_BEFORE_LF = 0x100,
LENIENT_SPACES_AFTER_CHUNK_SIZE = 0x200
};
typedef enum llhttp_lenient_flags llhttp_lenient_flags_t;
enum llhttp_type {
HTTP_BOTH = 0,
HTTP_REQUEST = 1,
HTTP_RESPONSE = 2
};
typedef enum llhttp_type llhttp_type_t;
enum llhttp_finish {
HTTP_FINISH_SAFE = 0,
HTTP_FINISH_SAFE_WITH_CB = 1,
HTTP_FINISH_UNSAFE = 2
};
typedef enum llhttp_finish llhttp_finish_t;
enum llhttp_method {
HTTP_DELETE = 0,
HTTP_GET = 1,
HTTP_HEAD = 2,
HTTP_POST = 3,
HTTP_PUT = 4,
HTTP_CONNECT = 5,
HTTP_OPTIONS = 6,
HTTP_TRACE = 7,
HTTP_COPY = 8,
HTTP_LOCK = 9,
HTTP_MKCOL = 10,
HTTP_MOVE = 11,
HTTP_PROPFIND = 12,
HTTP_PROPPATCH = 13,
HTTP_SEARCH = 14,
HTTP_UNLOCK = 15,
HTTP_BIND = 16,
HTTP_REBIND = 17,
HTTP_UNBIND = 18,
HTTP_ACL = 19,
HTTP_REPORT = 20,
HTTP_MKACTIVITY = 21,
HTTP_CHECKOUT = 22,
HTTP_MERGE = 23,
HTTP_MSEARCH = 24,
HTTP_NOTIFY = 25,
HTTP_SUBSCRIBE = 26,
HTTP_UNSUBSCRIBE = 27,
HTTP_PATCH = 28,
HTTP_PURGE = 29,
HTTP_MKCALENDAR = 30,
HTTP_LINK = 31,
HTTP_UNLINK = 32,
HTTP_SOURCE = 33,
HTTP_PRI = 34,
HTTP_DESCRIBE = 35,
HTTP_ANNOUNCE = 36,
HTTP_SETUP = 37,
HTTP_PLAY = 38,
HTTP_PAUSE = 39,
HTTP_TEARDOWN = 40,
HTTP_GET_PARAMETER = 41,
HTTP_SET_PARAMETER = 42,
HTTP_REDIRECT = 43,
HTTP_RECORD = 44,
HTTP_FLUSH = 45,
HTTP_QUERY = 46
};
typedef enum llhttp_method llhttp_method_t;
enum llhttp_status {
HTTP_STATUS_CONTINUE = 100,
HTTP_STATUS_SWITCHING_PROTOCOLS = 101,
HTTP_STATUS_PROCESSING = 102,
HTTP_STATUS_EARLY_HINTS = 103,
HTTP_STATUS_RESPONSE_IS_STALE = 110,
HTTP_STATUS_REVALIDATION_FAILED = 111,
HTTP_STATUS_DISCONNECTED_OPERATION = 112,
HTTP_STATUS_HEURISTIC_EXPIRATION = 113,
HTTP_STATUS_MISCELLANEOUS_WARNING = 199,
HTTP_STATUS_OK = 200,
HTTP_STATUS_CREATED = 201,
HTTP_STATUS_ACCEPTED = 202,
HTTP_STATUS_NON_AUTHORITATIVE_INFORMATION = 203,
HTTP_STATUS_NO_CONTENT = 204,
HTTP_STATUS_RESET_CONTENT = 205,
HTTP_STATUS_PARTIAL_CONTENT = 206,
HTTP_STATUS_MULTI_STATUS = 207,
HTTP_STATUS_ALREADY_REPORTED = 208,
HTTP_STATUS_TRANSFORMATION_APPLIED = 214,
HTTP_STATUS_IM_USED = 226,
HTTP_STATUS_MISCELLANEOUS_PERSISTENT_WARNING = 299,
HTTP_STATUS_MULTIPLE_CHOICES = 300,
HTTP_STATUS_MOVED_PERMANENTLY = 301,
HTTP_STATUS_FOUND = 302,
HTTP_STATUS_SEE_OTHER = 303,
HTTP_STATUS_NOT_MODIFIED = 304,
HTTP_STATUS_USE_PROXY = 305,
HTTP_STATUS_SWITCH_PROXY = 306,
HTTP_STATUS_TEMPORARY_REDIRECT = 307,
HTTP_STATUS_PERMANENT_REDIRECT = 308,
HTTP_STATUS_BAD_REQUEST = 400,
HTTP_STATUS_UNAUTHORIZED = 401,
HTTP_STATUS_PAYMENT_REQUIRED = 402,
HTTP_STATUS_FORBIDDEN = 403,
HTTP_STATUS_NOT_FOUND = 404,
HTTP_STATUS_METHOD_NOT_ALLOWED = 405,
HTTP_STATUS_NOT_ACCEPTABLE = 406,
HTTP_STATUS_PROXY_AUTHENTICATION_REQUIRED = 407,
HTTP_STATUS_REQUEST_TIMEOUT = 408,
HTTP_STATUS_CONFLICT = 409,
HTTP_STATUS_GONE = 410,
HTTP_STATUS_LENGTH_REQUIRED = 411,
HTTP_STATUS_PRECONDITION_FAILED = 412,
HTTP_STATUS_PAYLOAD_TOO_LARGE = 413,
HTTP_STATUS_URI_TOO_LONG = 414,
HTTP_STATUS_UNSUPPORTED_MEDIA_TYPE = 415,
HTTP_STATUS_RANGE_NOT_SATISFIABLE = 416,
HTTP_STATUS_EXPECTATION_FAILED = 417,
HTTP_STATUS_IM_A_TEAPOT = 418,
HTTP_STATUS_PAGE_EXPIRED = 419,
HTTP_STATUS_ENHANCE_YOUR_CALM = 420,
HTTP_STATUS_MISDIRECTED_REQUEST = 421,
HTTP_STATUS_UNPROCESSABLE_ENTITY = 422,
HTTP_STATUS_LOCKED = 423,
HTTP_STATUS_FAILED_DEPENDENCY = 424,
HTTP_STATUS_TOO_EARLY = 425,
HTTP_STATUS_UPGRADE_REQUIRED = 426,
HTTP_STATUS_PRECONDITION_REQUIRED = 428,
HTTP_STATUS_TOO_MANY_REQUESTS = 429,
HTTP_STATUS_REQUEST_HEADER_FIELDS_TOO_LARGE_UNOFFICIAL = 430,
HTTP_STATUS_REQUEST_HEADER_FIELDS_TOO_LARGE = 431,
HTTP_STATUS_LOGIN_TIMEOUT = 440,
HTTP_STATUS_NO_RESPONSE = 444,
HTTP_STATUS_RETRY_WITH = 449,
HTTP_STATUS_BLOCKED_BY_PARENTAL_CONTROL = 450,
HTTP_STATUS_UNAVAILABLE_FOR_LEGAL_REASONS = 451,
HTTP_STATUS_CLIENT_CLOSED_LOAD_BALANCED_REQUEST = 460,
HTTP_STATUS_INVALID_X_FORWARDED_FOR = 463,
HTTP_STATUS_REQUEST_HEADER_TOO_LARGE = 494,
HTTP_STATUS_SSL_CERTIFICATE_ERROR = 495,
HTTP_STATUS_SSL_CERTIFICATE_REQUIRED = 496,
HTTP_STATUS_HTTP_REQUEST_SENT_TO_HTTPS_PORT = 497,
HTTP_STATUS_INVALID_TOKEN = 498,
HTTP_STATUS_CLIENT_CLOSED_REQUEST = 499,
HTTP_STATUS_INTERNAL_SERVER_ERROR = 500,
HTTP_STATUS_NOT_IMPLEMENTED = 501,
HTTP_STATUS_BAD_GATEWAY = 502,
HTTP_STATUS_SERVICE_UNAVAILABLE = 503,
HTTP_STATUS_GATEWAY_TIMEOUT = 504,
HTTP_STATUS_HTTP_VERSION_NOT_SUPPORTED = 505,
HTTP_STATUS_VARIANT_ALSO_NEGOTIATES = 506,
HTTP_STATUS_INSUFFICIENT_STORAGE = 507,
HTTP_STATUS_LOOP_DETECTED = 508,
HTTP_STATUS_BANDWIDTH_LIMIT_EXCEEDED = 509,
HTTP_STATUS_NOT_EXTENDED = 510,
HTTP_STATUS_NETWORK_AUTHENTICATION_REQUIRED = 511,
HTTP_STATUS_WEB_SERVER_UNKNOWN_ERROR = 520,
HTTP_STATUS_WEB_SERVER_IS_DOWN = 521,
HTTP_STATUS_CONNECTION_TIMEOUT = 522,
HTTP_STATUS_ORIGIN_IS_UNREACHABLE = 523,
HTTP_STATUS_TIMEOUT_OCCURED = 524,
HTTP_STATUS_SSL_HANDSHAKE_FAILED = 525,
HTTP_STATUS_INVALID_SSL_CERTIFICATE = 526,
HTTP_STATUS_RAILGUN_ERROR = 527,
HTTP_STATUS_SITE_IS_OVERLOADED = 529,
HTTP_STATUS_SITE_IS_FROZEN = 530,
HTTP_STATUS_IDENTITY_PROVIDER_AUTHENTICATION_ERROR = 561,
HTTP_STATUS_NETWORK_READ_TIMEOUT = 598,
HTTP_STATUS_NETWORK_CONNECT_TIMEOUT = 599
};
typedef enum llhttp_status llhttp_status_t;
#define HTTP_ERRNO_MAP(XX) \
XX(0, OK, OK) \
XX(1, INTERNAL, INTERNAL) \
XX(2, STRICT, STRICT) \
XX(25, CR_EXPECTED, CR_EXPECTED) \
XX(3, LF_EXPECTED, LF_EXPECTED) \
XX(4, UNEXPECTED_CONTENT_LENGTH, UNEXPECTED_CONTENT_LENGTH) \
XX(30, UNEXPECTED_SPACE, UNEXPECTED_SPACE) \
XX(5, CLOSED_CONNECTION, CLOSED_CONNECTION) \
XX(6, INVALID_METHOD, INVALID_METHOD) \
XX(7, INVALID_URL, INVALID_URL) \
XX(8, INVALID_CONSTANT, INVALID_CONSTANT) \
XX(9, INVALID_VERSION, INVALID_VERSION) \
XX(10, INVALID_HEADER_TOKEN, INVALID_HEADER_TOKEN) \
XX(11, INVALID_CONTENT_LENGTH, INVALID_CONTENT_LENGTH) \
XX(12, INVALID_CHUNK_SIZE, INVALID_CHUNK_SIZE) \
XX(13, INVALID_STATUS, INVALID_STATUS) \
XX(14, INVALID_EOF_STATE, INVALID_EOF_STATE) \
XX(15, INVALID_TRANSFER_ENCODING, INVALID_TRANSFER_ENCODING) \
XX(16, CB_MESSAGE_BEGIN, CB_MESSAGE_BEGIN) \
XX(17, CB_HEADERS_COMPLETE, CB_HEADERS_COMPLETE) \
XX(18, CB_MESSAGE_COMPLETE, CB_MESSAGE_COMPLETE) \
XX(19, CB_CHUNK_HEADER, CB_CHUNK_HEADER) \
XX(20, CB_CHUNK_COMPLETE, CB_CHUNK_COMPLETE) \
XX(21, PAUSED, PAUSED) \
XX(22, PAUSED_UPGRADE, PAUSED_UPGRADE) \
XX(23, PAUSED_H2_UPGRADE, PAUSED_H2_UPGRADE) \
XX(24, USER, USER) \
XX(26, CB_URL_COMPLETE, CB_URL_COMPLETE) \
XX(27, CB_STATUS_COMPLETE, CB_STATUS_COMPLETE) \
XX(32, CB_METHOD_COMPLETE, CB_METHOD_COMPLETE) \
XX(33, CB_VERSION_COMPLETE, CB_VERSION_COMPLETE) \
XX(28, CB_HEADER_FIELD_COMPLETE, CB_HEADER_FIELD_COMPLETE) \
XX(29, CB_HEADER_VALUE_COMPLETE, CB_HEADER_VALUE_COMPLETE) \
XX(34, CB_CHUNK_EXTENSION_NAME_COMPLETE, CB_CHUNK_EXTENSION_NAME_COMPLETE) \
XX(35, CB_CHUNK_EXTENSION_VALUE_COMPLETE, CB_CHUNK_EXTENSION_VALUE_COMPLETE) \
XX(31, CB_RESET, CB_RESET) \
XX(38, CB_PROTOCOL_COMPLETE, CB_PROTOCOL_COMPLETE) \
#define HTTP_METHOD_MAP(XX) \
XX(0, DELETE, DELETE) \
XX(1, GET, GET) \
XX(2, HEAD, HEAD) \
XX(3, POST, POST) \
XX(4, PUT, PUT) \
XX(5, CONNECT, CONNECT) \
XX(6, OPTIONS, OPTIONS) \
XX(7, TRACE, TRACE) \
XX(8, COPY, COPY) \
XX(9, LOCK, LOCK) \
XX(10, MKCOL, MKCOL) \
XX(11, MOVE, MOVE) \
XX(12, PROPFIND, PROPFIND) \
XX(13, PROPPATCH, PROPPATCH) \
XX(14, SEARCH, SEARCH) \
XX(15, UNLOCK, UNLOCK) \
XX(16, BIND, BIND) \
XX(17, REBIND, REBIND) \
XX(18, UNBIND, UNBIND) \
XX(19, ACL, ACL) \
XX(20, REPORT, REPORT) \
XX(21, MKACTIVITY, MKACTIVITY) \
XX(22, CHECKOUT, CHECKOUT) \
XX(23, MERGE, MERGE) \
XX(24, MSEARCH, M-SEARCH) \
XX(25, NOTIFY, NOTIFY) \
XX(26, SUBSCRIBE, SUBSCRIBE) \
XX(27, UNSUBSCRIBE, UNSUBSCRIBE) \
XX(28, PATCH, PATCH) \
XX(29, PURGE, PURGE) \
XX(30, MKCALENDAR, MKCALENDAR) \
XX(31, LINK, LINK) \
XX(32, UNLINK, UNLINK) \
XX(33, SOURCE, SOURCE) \
XX(46, QUERY, QUERY) \
#define RTSP_METHOD_MAP(XX) \
XX(1, GET, GET) \
XX(3, POST, POST) \
XX(6, OPTIONS, OPTIONS) \
XX(35, DESCRIBE, DESCRIBE) \
XX(36, ANNOUNCE, ANNOUNCE) \
XX(37, SETUP, SETUP) \
XX(38, PLAY, PLAY) \
XX(39, PAUSE, PAUSE) \
XX(40, TEARDOWN, TEARDOWN) \
XX(41, GET_PARAMETER, GET_PARAMETER) \
XX(42, SET_PARAMETER, SET_PARAMETER) \
XX(43, REDIRECT, REDIRECT) \
XX(44, RECORD, RECORD) \
XX(45, FLUSH, FLUSH) \
#define HTTP_ALL_METHOD_MAP(XX) \
XX(0, DELETE, DELETE) \
XX(1, GET, GET) \
XX(2, HEAD, HEAD) \
XX(3, POST, POST) \
XX(4, PUT, PUT) \
XX(5, CONNECT, CONNECT) \
XX(6, OPTIONS, OPTIONS) \
XX(7, TRACE, TRACE) \
XX(8, COPY, COPY) \
XX(9, LOCK, LOCK) \
XX(10, MKCOL, MKCOL) \
XX(11, MOVE, MOVE) \
XX(12, PROPFIND, PROPFIND) \
XX(13, PROPPATCH, PROPPATCH) \
XX(14, SEARCH, SEARCH) \
XX(15, UNLOCK, UNLOCK) \
XX(16, BIND, BIND) \
XX(17, REBIND, REBIND) \
XX(18, UNBIND, UNBIND) \
XX(19, ACL, ACL) \
XX(20, REPORT, REPORT) \
XX(21, MKACTIVITY, MKACTIVITY) \
XX(22, CHECKOUT, CHECKOUT) \
XX(23, MERGE, MERGE) \
XX(24, MSEARCH, M-SEARCH) \
XX(25, NOTIFY, NOTIFY) \
XX(26, SUBSCRIBE, SUBSCRIBE) \
XX(27, UNSUBSCRIBE, UNSUBSCRIBE) \
XX(28, PATCH, PATCH) \
XX(29, PURGE, PURGE) \
XX(30, MKCALENDAR, MKCALENDAR) \
XX(31, LINK, LINK) \
XX(32, UNLINK, UNLINK) \
XX(33, SOURCE, SOURCE) \
XX(34, PRI, PRI) \
XX(35, DESCRIBE, DESCRIBE) \
XX(36, ANNOUNCE, ANNOUNCE) \
XX(37, SETUP, SETUP) \
XX(38, PLAY, PLAY) \
XX(39, PAUSE, PAUSE) \
XX(40, TEARDOWN, TEARDOWN) \
XX(41, GET_PARAMETER, GET_PARAMETER) \
XX(42, SET_PARAMETER, SET_PARAMETER) \
XX(43, REDIRECT, REDIRECT) \
XX(44, RECORD, RECORD) \
XX(45, FLUSH, FLUSH) \
XX(46, QUERY, QUERY) \
#define HTTP_STATUS_MAP(XX) \
XX(100, CONTINUE, CONTINUE) \
XX(101, SWITCHING_PROTOCOLS, SWITCHING_PROTOCOLS) \
XX(102, PROCESSING, PROCESSING) \
XX(103, EARLY_HINTS, EARLY_HINTS) \
XX(110, RESPONSE_IS_STALE, RESPONSE_IS_STALE) \
XX(111, REVALIDATION_FAILED, REVALIDATION_FAILED) \
XX(112, DISCONNECTED_OPERATION, DISCONNECTED_OPERATION) \
XX(113, HEURISTIC_EXPIRATION, HEURISTIC_EXPIRATION) \
XX(199, MISCELLANEOUS_WARNING, MISCELLANEOUS_WARNING) \
XX(200, OK, OK) \
XX(201, CREATED, CREATED) \
XX(202, ACCEPTED, ACCEPTED) \
XX(203, NON_AUTHORITATIVE_INFORMATION, NON_AUTHORITATIVE_INFORMATION) \
XX(204, NO_CONTENT, NO_CONTENT) \
XX(205, RESET_CONTENT, RESET_CONTENT) \
XX(206, PARTIAL_CONTENT, PARTIAL_CONTENT) \
XX(207, MULTI_STATUS, MULTI_STATUS) \
XX(208, ALREADY_REPORTED, ALREADY_REPORTED) \
XX(214, TRANSFORMATION_APPLIED, TRANSFORMATION_APPLIED) \
XX(226, IM_USED, IM_USED) \
XX(299, MISCELLANEOUS_PERSISTENT_WARNING, MISCELLANEOUS_PERSISTENT_WARNING) \
XX(300, MULTIPLE_CHOICES, MULTIPLE_CHOICES) \
XX(301, MOVED_PERMANENTLY, MOVED_PERMANENTLY) \
XX(302, FOUND, FOUND) \
XX(303, SEE_OTHER, SEE_OTHER) \
XX(304, NOT_MODIFIED, NOT_MODIFIED) \
XX(305, USE_PROXY, USE_PROXY) \
XX(306, SWITCH_PROXY, SWITCH_PROXY) \
XX(307, TEMPORARY_REDIRECT, TEMPORARY_REDIRECT) \
XX(308, PERMANENT_REDIRECT, PERMANENT_REDIRECT) \
XX(400, BAD_REQUEST, BAD_REQUEST) \
XX(401, UNAUTHORIZED, UNAUTHORIZED) \
XX(402, PAYMENT_REQUIRED, PAYMENT_REQUIRED) \
XX(403, FORBIDDEN, FORBIDDEN) \
XX(404, NOT_FOUND, NOT_FOUND) \
XX(405, METHOD_NOT_ALLOWED, METHOD_NOT_ALLOWED) \
XX(406, NOT_ACCEPTABLE, NOT_ACCEPTABLE) \
XX(407, PROXY_AUTHENTICATION_REQUIRED, PROXY_AUTHENTICATION_REQUIRED) \
XX(408, REQUEST_TIMEOUT, REQUEST_TIMEOUT) \
XX(409, CONFLICT, CONFLICT) \
XX(410, GONE, GONE) \
XX(411, LENGTH_REQUIRED, LENGTH_REQUIRED) \
XX(412, PRECONDITION_FAILED, PRECONDITION_FAILED) \
XX(413, PAYLOAD_TOO_LARGE, PAYLOAD_TOO_LARGE) \
XX(414, URI_TOO_LONG, URI_TOO_LONG) \
XX(415, UNSUPPORTED_MEDIA_TYPE, UNSUPPORTED_MEDIA_TYPE) \
XX(416, RANGE_NOT_SATISFIABLE, RANGE_NOT_SATISFIABLE) \
XX(417, EXPECTATION_FAILED, EXPECTATION_FAILED) \
XX(418, IM_A_TEAPOT, IM_A_TEAPOT) \
XX(419, PAGE_EXPIRED, PAGE_EXPIRED) \
XX(420, ENHANCE_YOUR_CALM, ENHANCE_YOUR_CALM) \
XX(421, MISDIRECTED_REQUEST, MISDIRECTED_REQUEST) \
XX(422, UNPROCESSABLE_ENTITY, UNPROCESSABLE_ENTITY) \
XX(423, LOCKED, LOCKED) \
XX(424, FAILED_DEPENDENCY, FAILED_DEPENDENCY) \
XX(425, TOO_EARLY, TOO_EARLY) \
XX(426, UPGRADE_REQUIRED, UPGRADE_REQUIRED) \
XX(428, PRECONDITION_REQUIRED, PRECONDITION_REQUIRED) \
XX(429, TOO_MANY_REQUESTS, TOO_MANY_REQUESTS) \
XX(430, REQUEST_HEADER_FIELDS_TOO_LARGE_UNOFFICIAL, REQUEST_HEADER_FIELDS_TOO_LARGE_UNOFFICIAL) \
XX(431, REQUEST_HEADER_FIELDS_TOO_LARGE, REQUEST_HEADER_FIELDS_TOO_LARGE) \
XX(440, LOGIN_TIMEOUT, LOGIN_TIMEOUT) \
XX(444, NO_RESPONSE, NO_RESPONSE) \
XX(449, RETRY_WITH, RETRY_WITH) \
XX(450, BLOCKED_BY_PARENTAL_CONTROL, BLOCKED_BY_PARENTAL_CONTROL) \
XX(451, UNAVAILABLE_FOR_LEGAL_REASONS, UNAVAILABLE_FOR_LEGAL_REASONS) \
XX(460, CLIENT_CLOSED_LOAD_BALANCED_REQUEST, CLIENT_CLOSED_LOAD_BALANCED_REQUEST) \
XX(463, INVALID_X_FORWARDED_FOR, INVALID_X_FORWARDED_FOR) \
XX(494, REQUEST_HEADER_TOO_LARGE, REQUEST_HEADER_TOO_LARGE) \
XX(495, SSL_CERTIFICATE_ERROR, SSL_CERTIFICATE_ERROR) \
XX(496, SSL_CERTIFICATE_REQUIRED, SSL_CERTIFICATE_REQUIRED) \
XX(497, HTTP_REQUEST_SENT_TO_HTTPS_PORT, HTTP_REQUEST_SENT_TO_HTTPS_PORT) \
XX(498, INVALID_TOKEN, INVALID_TOKEN) \
XX(499, CLIENT_CLOSED_REQUEST, CLIENT_CLOSED_REQUEST) \
XX(500, INTERNAL_SERVER_ERROR, INTERNAL_SERVER_ERROR) \
XX(501, NOT_IMPLEMENTED, NOT_IMPLEMENTED) \
XX(502, BAD_GATEWAY, BAD_GATEWAY) \
XX(503, SERVICE_UNAVAILABLE, SERVICE_UNAVAILABLE) \
XX(504, GATEWAY_TIMEOUT, GATEWAY_TIMEOUT) \
XX(505, HTTP_VERSION_NOT_SUPPORTED, HTTP_VERSION_NOT_SUPPORTED) \
XX(506, VARIANT_ALSO_NEGOTIATES, VARIANT_ALSO_NEGOTIATES) \
XX(507, INSUFFICIENT_STORAGE, INSUFFICIENT_STORAGE) \
XX(508, LOOP_DETECTED, LOOP_DETECTED) \
XX(509, BANDWIDTH_LIMIT_EXCEEDED, BANDWIDTH_LIMIT_EXCEEDED) \
XX(510, NOT_EXTENDED, NOT_EXTENDED) \
XX(511, NETWORK_AUTHENTICATION_REQUIRED, NETWORK_AUTHENTICATION_REQUIRED) \
XX(520, WEB_SERVER_UNKNOWN_ERROR, WEB_SERVER_UNKNOWN_ERROR) \
XX(521, WEB_SERVER_IS_DOWN, WEB_SERVER_IS_DOWN) \
XX(522, CONNECTION_TIMEOUT, CONNECTION_TIMEOUT) \
XX(523, ORIGIN_IS_UNREACHABLE, ORIGIN_IS_UNREACHABLE) \
XX(524, TIMEOUT_OCCURED, TIMEOUT_OCCURED) \
XX(525, SSL_HANDSHAKE_FAILED, SSL_HANDSHAKE_FAILED) \
XX(526, INVALID_SSL_CERTIFICATE, INVALID_SSL_CERTIFICATE) \
XX(527, RAILGUN_ERROR, RAILGUN_ERROR) \
XX(529, SITE_IS_OVERLOADED, SITE_IS_OVERLOADED) \
XX(530, SITE_IS_FROZEN, SITE_IS_FROZEN) \
XX(561, IDENTITY_PROVIDER_AUTHENTICATION_ERROR, IDENTITY_PROVIDER_AUTHENTICATION_ERROR) \
XX(598, NETWORK_READ_TIMEOUT, NETWORK_READ_TIMEOUT) \
XX(599, NETWORK_CONNECT_TIMEOUT, NETWORK_CONNECT_TIMEOUT) \
#ifdef __cplusplus
} /* extern "C" */
#endif
#endif /* LLLLHTTP_C_HEADERS_ */
#ifndef INCLUDE_LLHTTP_API_H_
#define INCLUDE_LLHTTP_API_H_
#ifdef __cplusplus
extern "C" {
#endif
#include
#if defined(__wasm__)
#define LLHTTP_EXPORT __attribute__((visibility("default")))
#elif defined(_WIN32)
#define LLHTTP_EXPORT __declspec(dllexport)
#else
#define LLHTTP_EXPORT
#endif
typedef llhttp__internal_t llhttp_t;
typedef struct llhttp_settings_s llhttp_settings_t;
typedef int (*llhttp_data_cb)(llhttp_t*, const char *at, size_t length);
typedef int (*llhttp_cb)(llhttp_t*);
struct llhttp_settings_s {
/* Possible return values 0, -1, `HPE_PAUSED` */
llhttp_cb on_message_begin;
/* Possible return values 0, -1, HPE_USER */
llhttp_data_cb on_protocol;
llhttp_data_cb on_url;
llhttp_data_cb on_status;
llhttp_data_cb on_method;
llhttp_data_cb on_version;
llhttp_data_cb on_header_field;
llhttp_data_cb on_header_value;
llhttp_data_cb on_chunk_extension_name;
llhttp_data_cb on_chunk_extension_value;
/* Possible return values:
* 0 - Proceed normally
* 1 - Assume that request/response has no body, and proceed to parsing the
* next message
* 2 - Assume absence of body (as above) and make `llhttp_execute()` return
* `HPE_PAUSED_UPGRADE`
* -1 - Error
* `HPE_PAUSED`
*/
llhttp_cb on_headers_complete;
/* Possible return values 0, -1, HPE_USER */
llhttp_data_cb on_body;
/* Possible return values 0, -1, `HPE_PAUSED` */
llhttp_cb on_message_complete;
llhttp_cb on_protocol_complete;
llhttp_cb on_url_complete;
llhttp_cb on_status_complete;
llhttp_cb on_method_complete;
llhttp_cb on_version_complete;
llhttp_cb on_header_field_complete;
llhttp_cb on_header_value_complete;
llhttp_cb on_chunk_extension_name_complete;
llhttp_cb on_chunk_extension_value_complete;
/* When on_chunk_header is called, the current chunk length is stored
* in parser->content_length.
* Possible return values 0, -1, `HPE_PAUSED`
*/
llhttp_cb on_chunk_header;
llhttp_cb on_chunk_complete;
llhttp_cb on_reset;
};
/* Initialize the parser with specific type and user settings.
*
* NOTE: lifetime of `settings` has to be at least the same as the lifetime of
* the `parser` here. In practice, `settings` has to be either a static
* variable or be allocated with `malloc`, `new`, etc.
*/
LLHTTP_EXPORT
void llhttp_init(llhttp_t* parser, llhttp_type_t type,
const llhttp_settings_t* settings);
LLHTTP_EXPORT
llhttp_t* llhttp_alloc(llhttp_type_t type);
LLHTTP_EXPORT
void llhttp_free(llhttp_t* parser);
LLHTTP_EXPORT
uint8_t llhttp_get_type(llhttp_t* parser);
LLHTTP_EXPORT
uint8_t llhttp_get_http_major(llhttp_t* parser);
LLHTTP_EXPORT
uint8_t llhttp_get_http_minor(llhttp_t* parser);
LLHTTP_EXPORT
uint8_t llhttp_get_method(llhttp_t* parser);
LLHTTP_EXPORT
int llhttp_get_status_code(llhttp_t* parser);
LLHTTP_EXPORT
uint8_t llhttp_get_upgrade(llhttp_t* parser);
/* Reset an already initialized parser back to the start state, preserving the
* existing parser type, callback settings, user data, and lenient flags.
*/
LLHTTP_EXPORT
void llhttp_reset(llhttp_t* parser);
/* Initialize the settings object */
LLHTTP_EXPORT
void llhttp_settings_init(llhttp_settings_t* settings);
/* Parse full or partial request/response, invoking user callbacks along the
* way.
*
* If any of `llhttp_data_cb` returns errno not equal to `HPE_OK` - the parsing
* interrupts, and such errno is returned from `llhttp_execute()`. If
* `HPE_PAUSED` was used as a errno, the execution can be resumed with
* `llhttp_resume()` call.
*
* In a special case of CONNECT/Upgrade request/response `HPE_PAUSED_UPGRADE`
* is returned after fully parsing the request/response. If the user wishes to
* continue parsing, they need to invoke `llhttp_resume_after_upgrade()`.
*
* NOTE: if this function ever returns a non-pause type error, it will continue
* to return the same error upon each successive call up until `llhttp_init()`
* is called.
*/
LLHTTP_EXPORT
llhttp_errno_t llhttp_execute(llhttp_t* parser, const char* data, size_t len);
/* This method should be called when the other side has no further bytes to
* send (e.g. shutdown of readable side of the TCP connection.)
*
* Requests without `Content-Length` and other messages might require treating
* all incoming bytes as the part of the body, up to the last byte of the
* connection. This method will invoke `on_message_complete()` callback if the
* request was terminated safely. Otherwise a error code would be returned.
*/
LLHTTP_EXPORT
llhttp_errno_t llhttp_finish(llhttp_t* parser);
/* Returns `1` if the incoming message is parsed until the last byte, and has
* to be completed by calling `llhttp_finish()` on EOF
*/
LLHTTP_EXPORT
int llhttp_message_needs_eof(const llhttp_t* parser);
/* Returns `1` if there might be any other messages following the last that was
* successfully parsed.
*/
LLHTTP_EXPORT
int llhttp_should_keep_alive(const llhttp_t* parser);
/* Make further calls of `llhttp_execute()` return `HPE_PAUSED` and set
* appropriate error reason.
*
* Important: do not call this from user callbacks! User callbacks must return
* `HPE_PAUSED` if pausing is required.
*/
LLHTTP_EXPORT
void llhttp_pause(llhttp_t* parser);
/* Might be called to resume the execution after the pause in user's callback.
* See `llhttp_execute()` above for details.
*
* Call this only if `llhttp_execute()` returns `HPE_PAUSED`.
*/
LLHTTP_EXPORT
void llhttp_resume(llhttp_t* parser);
/* Might be called to resume the execution after the pause in user's callback.
* See `llhttp_execute()` above for details.
*
* Call this only if `llhttp_execute()` returns `HPE_PAUSED_UPGRADE`
*/
LLHTTP_EXPORT
void llhttp_resume_after_upgrade(llhttp_t* parser);
/* Returns the latest return error */
LLHTTP_EXPORT
llhttp_errno_t llhttp_get_errno(const llhttp_t* parser);
/* Returns the verbal explanation of the latest returned error.
*
* Note: User callback should set error reason when returning the error. See
* `llhttp_set_error_reason()` for details.
*/
LLHTTP_EXPORT
const char* llhttp_get_error_reason(const llhttp_t* parser);
/* Assign verbal description to the returned error. Must be called in user
* callbacks right before returning the errno.
*
* Note: `HPE_USER` error code might be useful in user callbacks.
*/
LLHTTP_EXPORT
void llhttp_set_error_reason(llhttp_t* parser, const char* reason);
/* Returns the pointer to the last parsed byte before the returned error. The
* pointer is relative to the `data` argument of `llhttp_execute()`.
*
* Note: this method might be useful for counting the number of parsed bytes.
*/
LLHTTP_EXPORT
const char* llhttp_get_error_pos(const llhttp_t* parser);
/* Returns textual name of error code */
LLHTTP_EXPORT
const char* llhttp_errno_name(llhttp_errno_t err);
/* Returns textual name of HTTP method */
LLHTTP_EXPORT
const char* llhttp_method_name(llhttp_method_t method);
/* Returns textual name of HTTP status */
LLHTTP_EXPORT
const char* llhttp_status_name(llhttp_status_t status);
/* Enables/disables lenient header value parsing (disabled by default).
*
* Lenient parsing disables header value token checks, extending llhttp's
* protocol support to highly non-compliant clients/server. No
* `HPE_INVALID_HEADER_TOKEN` will be raised for incorrect header values when
* lenient parsing is "on".
*
* **Enabling this flag can pose a security issue since you will be exposed to
* request smuggling attacks. USE WITH CAUTION!**
*/
LLHTTP_EXPORT
void llhttp_set_lenient_headers(llhttp_t* parser, int enabled);
/* Enables/disables lenient handling of conflicting `Transfer-Encoding` and
* `Content-Length` headers (disabled by default).
*
* Normally `llhttp` would error when `Transfer-Encoding` is present in
* conjunction with `Content-Length`. This error is important to prevent HTTP
* request smuggling, but may be less desirable for small number of cases
* involving legacy servers.
*
* **Enabling this flag can pose a security issue since you will be exposed to
* request smuggling attacks. USE WITH CAUTION!**
*/
LLHTTP_EXPORT
void llhttp_set_lenient_chunked_length(llhttp_t* parser, int enabled);
/* Enables/disables lenient handling of `Connection: close` and HTTP/1.0
* requests responses.
*
* Normally `llhttp` would error on (in strict mode) or discard (in loose mode)
* the HTTP request/response after the request/response with `Connection: close`
* and `Content-Length`. This is important to prevent cache poisoning attacks,
* but might interact badly with outdated and insecure clients. With this flag
* the extra request/response will be parsed normally.
*
* **Enabling this flag can pose a security issue since you will be exposed to
* poisoning attacks. USE WITH CAUTION!**
*/
LLHTTP_EXPORT
void llhttp_set_lenient_keep_alive(llhttp_t* parser, int enabled);
/* Enables/disables lenient handling of `Transfer-Encoding` header.
*
* Normally `llhttp` would error when a `Transfer-Encoding` has `chunked` value
* and another value after it (either in a single header or in multiple
* headers whose value are internally joined using `, `).
* This is mandated by the spec to reliably determine request body size and thus
* avoid request smuggling.
* With this flag the extra value will be parsed normally.
*
* **Enabling this flag can pose a security issue since you will be exposed to
* request smuggling attacks. USE WITH CAUTION!**
*/
LLHTTP_EXPORT
void llhttp_set_lenient_transfer_encoding(llhttp_t* parser, int enabled);
/* Enables/disables lenient handling of HTTP version.
*
* Normally `llhttp` would error when the HTTP version in the request or status line
* is not `0.9`, `1.0`, `1.1` or `2.0`.
* With this flag the invalid value will be parsed normally.
*
* **Enabling this flag can pose a security issue since you will allow unsupported
* HTTP versions. USE WITH CAUTION!**
*/
LLHTTP_EXPORT
void llhttp_set_lenient_version(llhttp_t* parser, int enabled);
/* Enables/disables lenient handling of additional data received after a message ends
* and keep-alive is disabled.
*
* Normally `llhttp` would error when additional unexpected data is received if the message
* contains the `Connection` header with `close` value.
* With this flag the extra data will discarded without throwing an error.
*
* **Enabling this flag can pose a security issue since you will be exposed to
* poisoning attacks. USE WITH CAUTION!**
*/
LLHTTP_EXPORT
void llhttp_set_lenient_data_after_close(llhttp_t* parser, int enabled);
/* Enables/disables lenient handling of incomplete CRLF sequences.
*
* Normally `llhttp` would error when a CR is not followed by LF when terminating the
* request line, the status line, the headers or a chunk header.
* With this flag only a CR is required to terminate such sections.
*
* **Enabling this flag can pose a security issue since you will be exposed to
* request smuggling attacks. USE WITH CAUTION!**
*/
LLHTTP_EXPORT
void llhttp_set_lenient_optional_lf_after_cr(llhttp_t* parser, int enabled);
/*
* Enables/disables lenient handling of line separators.
*
* Normally `llhttp` would error when a LF is not preceded by CR when terminating the
* request line, the status line, the headers, a chunk header or a chunk data.
* With this flag only a LF is required to terminate such sections.
*
* **Enabling this flag can pose a security issue since you will be exposed to
* request smuggling attacks. USE WITH CAUTION!**
*/
LLHTTP_EXPORT
void llhttp_set_lenient_optional_cr_before_lf(llhttp_t* parser, int enabled);
/* Enables/disables lenient handling of chunks not separated via CRLF.
*
* Normally `llhttp` would error when after a chunk data a CRLF is missing before
* starting a new chunk.
* With this flag the new chunk can start immediately after the previous one.
*
* **Enabling this flag can pose a security issue since you will be exposed to
* request smuggling attacks. USE WITH CAUTION!**
*/
LLHTTP_EXPORT
void llhttp_set_lenient_optional_crlf_after_chunk(llhttp_t* parser, int enabled);
/* Enables/disables lenient handling of spaces after chunk size.
*
* Normally `llhttp` would error when after a chunk size is followed by one or more
* spaces are present instead of a CRLF or `;`.
* With this flag this check is disabled.
*
* **Enabling this flag can pose a security issue since you will be exposed to
* request smuggling attacks. USE WITH CAUTION!**
*/
LLHTTP_EXPORT
void llhttp_set_lenient_spaces_after_chunk_size(llhttp_t* parser, int enabled);
#ifdef __cplusplus
} /* extern "C" */
#endif
#endif /* INCLUDE_LLHTTP_API_H_ */
#endif /* INCLUDE_LLHTTP_H_ */
================================================
FILE: deps/llhttp/src/api.c
================================================
#include
#include
#include
#include "llhttp.h"
#define CALLBACK_MAYBE(PARSER, NAME) \
do { \
const llhttp_settings_t* settings; \
settings = (const llhttp_settings_t*) (PARSER)->settings; \
if (settings == NULL || settings->NAME == NULL) { \
err = 0; \
break; \
} \
err = settings->NAME((PARSER)); \
} while (0)
#define SPAN_CALLBACK_MAYBE(PARSER, NAME, START, LEN) \
do { \
const llhttp_settings_t* settings; \
settings = (const llhttp_settings_t*) (PARSER)->settings; \
if (settings == NULL || settings->NAME == NULL) { \
err = 0; \
break; \
} \
err = settings->NAME((PARSER), (START), (LEN)); \
if (err == -1) { \
err = HPE_USER; \
llhttp_set_error_reason((PARSER), "Span callback error in " #NAME); \
} \
} while (0)
void llhttp_init(llhttp_t* parser, llhttp_type_t type,
const llhttp_settings_t* settings) {
llhttp__internal_init(parser);
parser->type = type;
parser->settings = (void*) settings;
}
#if defined(__wasm__)
extern int wasm_on_message_begin(llhttp_t * p);
extern int wasm_on_url(llhttp_t* p, const char* at, size_t length);
extern int wasm_on_status(llhttp_t* p, const char* at, size_t length);
extern int wasm_on_header_field(llhttp_t* p, const char* at, size_t length);
extern int wasm_on_header_value(llhttp_t* p, const char* at, size_t length);
extern int wasm_on_headers_complete(llhttp_t * p, int status_code,
uint8_t upgrade, int should_keep_alive);
extern int wasm_on_body(llhttp_t* p, const char* at, size_t length);
extern int wasm_on_message_complete(llhttp_t * p);
static int wasm_on_headers_complete_wrap(llhttp_t* p) {
return wasm_on_headers_complete(p, p->status_code, p->upgrade,
llhttp_should_keep_alive(p));
}
const llhttp_settings_t wasm_settings = {
.on_message_begin = wasm_on_message_begin,
.on_url = wasm_on_url,
.on_status = wasm_on_status,
.on_header_field = wasm_on_header_field,
.on_header_value = wasm_on_header_value,
.on_headers_complete = wasm_on_headers_complete_wrap,
.on_body = wasm_on_body,
.on_message_complete = wasm_on_message_complete,
};
llhttp_t* llhttp_alloc(llhttp_type_t type) {
llhttp_t* parser = malloc(sizeof(llhttp_t));
llhttp_init(parser, type, &wasm_settings);
return parser;
}
void llhttp_free(llhttp_t* parser) {
free(parser);
}
#endif // defined(__wasm__)
/* Some getters required to get stuff from the parser */
uint8_t llhttp_get_type(llhttp_t* parser) {
return parser->type;
}
uint8_t llhttp_get_http_major(llhttp_t* parser) {
return parser->http_major;
}
uint8_t llhttp_get_http_minor(llhttp_t* parser) {
return parser->http_minor;
}
uint8_t llhttp_get_method(llhttp_t* parser) {
return parser->method;
}
int llhttp_get_status_code(llhttp_t* parser) {
return parser->status_code;
}
uint8_t llhttp_get_upgrade(llhttp_t* parser) {
return parser->upgrade;
}
void llhttp_reset(llhttp_t* parser) {
llhttp_type_t type = parser->type;
const llhttp_settings_t* settings = parser->settings;
void* data = parser->data;
uint16_t lenient_flags = parser->lenient_flags;
llhttp__internal_init(parser);
parser->type = type;
parser->settings = (void*) settings;
parser->data = data;
parser->lenient_flags = lenient_flags;
}
llhttp_errno_t llhttp_execute(llhttp_t* parser, const char* data, size_t len) {
return llhttp__internal_execute(parser, data, data + len);
}
void llhttp_settings_init(llhttp_settings_t* settings) {
memset(settings, 0, sizeof(*settings));
}
llhttp_errno_t llhttp_finish(llhttp_t* parser) {
int err;
/* We're in an error state. Don't bother doing anything. */
if (parser->error != 0) {
return 0;
}
switch (parser->finish) {
case HTTP_FINISH_SAFE_WITH_CB:
CALLBACK_MAYBE(parser, on_message_complete);
if (err != HPE_OK) return err;
/* FALLTHROUGH */
case HTTP_FINISH_SAFE:
return HPE_OK;
case HTTP_FINISH_UNSAFE:
parser->reason = "Invalid EOF state";
return HPE_INVALID_EOF_STATE;
default:
abort();
}
}
void llhttp_pause(llhttp_t* parser) {
if (parser->error != HPE_OK) {
return;
}
parser->error = HPE_PAUSED;
parser->reason = "Paused";
}
void llhttp_resume(llhttp_t* parser) {
if (parser->error != HPE_PAUSED) {
return;
}
parser->error = 0;
}
void llhttp_resume_after_upgrade(llhttp_t* parser) {
if (parser->error != HPE_PAUSED_UPGRADE) {
return;
}
parser->error = 0;
}
llhttp_errno_t llhttp_get_errno(const llhttp_t* parser) {
return parser->error;
}
const char* llhttp_get_error_reason(const llhttp_t* parser) {
return parser->reason;
}
void llhttp_set_error_reason(llhttp_t* parser, const char* reason) {
parser->reason = reason;
}
const char* llhttp_get_error_pos(const llhttp_t* parser) {
return parser->error_pos;
}
const char* llhttp_errno_name(llhttp_errno_t err) {
#define HTTP_ERRNO_GEN(CODE, NAME, _) case HPE_##NAME: return "HPE_" #NAME;
switch (err) {
HTTP_ERRNO_MAP(HTTP_ERRNO_GEN)
default: abort();
}
#undef HTTP_ERRNO_GEN
}
const char* llhttp_method_name(llhttp_method_t method) {
#define HTTP_METHOD_GEN(NUM, NAME, STRING) case HTTP_##NAME: return #STRING;
switch (method) {
HTTP_ALL_METHOD_MAP(HTTP_METHOD_GEN)
default: abort();
}
#undef HTTP_METHOD_GEN
}
const char* llhttp_status_name(llhttp_status_t status) {
#define HTTP_STATUS_GEN(NUM, NAME, STRING) case HTTP_STATUS_##NAME: return #STRING;
switch (status) {
HTTP_STATUS_MAP(HTTP_STATUS_GEN)
default: abort();
}
#undef HTTP_STATUS_GEN
}
void llhttp_set_lenient_headers(llhttp_t* parser, int enabled) {
if (enabled) {
parser->lenient_flags |= LENIENT_HEADERS;
} else {
parser->lenient_flags &= ~LENIENT_HEADERS;
}
}
void llhttp_set_lenient_chunked_length(llhttp_t* parser, int enabled) {
if (enabled) {
parser->lenient_flags |= LENIENT_CHUNKED_LENGTH;
} else {
parser->lenient_flags &= ~LENIENT_CHUNKED_LENGTH;
}
}
void llhttp_set_lenient_keep_alive(llhttp_t* parser, int enabled) {
if (enabled) {
parser->lenient_flags |= LENIENT_KEEP_ALIVE;
} else {
parser->lenient_flags &= ~LENIENT_KEEP_ALIVE;
}
}
void llhttp_set_lenient_transfer_encoding(llhttp_t* parser, int enabled) {
if (enabled) {
parser->lenient_flags |= LENIENT_TRANSFER_ENCODING;
} else {
parser->lenient_flags &= ~LENIENT_TRANSFER_ENCODING;
}
}
void llhttp_set_lenient_version(llhttp_t* parser, int enabled) {
if (enabled) {
parser->lenient_flags |= LENIENT_VERSION;
} else {
parser->lenient_flags &= ~LENIENT_VERSION;
}
}
void llhttp_set_lenient_data_after_close(llhttp_t* parser, int enabled) {
if (enabled) {
parser->lenient_flags |= LENIENT_DATA_AFTER_CLOSE;
} else {
parser->lenient_flags &= ~LENIENT_DATA_AFTER_CLOSE;
}
}
void llhttp_set_lenient_optional_lf_after_cr(llhttp_t* parser, int enabled) {
if (enabled) {
parser->lenient_flags |= LENIENT_OPTIONAL_LF_AFTER_CR;
} else {
parser->lenient_flags &= ~LENIENT_OPTIONAL_LF_AFTER_CR;
}
}
void llhttp_set_lenient_optional_crlf_after_chunk(llhttp_t* parser, int enabled) {
if (enabled) {
parser->lenient_flags |= LENIENT_OPTIONAL_CRLF_AFTER_CHUNK;
} else {
parser->lenient_flags &= ~LENIENT_OPTIONAL_CRLF_AFTER_CHUNK;
}
}
void llhttp_set_lenient_optional_cr_before_lf(llhttp_t* parser, int enabled) {
if (enabled) {
parser->lenient_flags |= LENIENT_OPTIONAL_CR_BEFORE_LF;
} else {
parser->lenient_flags &= ~LENIENT_OPTIONAL_CR_BEFORE_LF;
}
}
void llhttp_set_lenient_spaces_after_chunk_size(llhttp_t* parser, int enabled) {
if (enabled) {
parser->lenient_flags |= LENIENT_SPACES_AFTER_CHUNK_SIZE;
} else {
parser->lenient_flags &= ~LENIENT_SPACES_AFTER_CHUNK_SIZE;
}
}
/* Callbacks */
int llhttp__on_message_begin(llhttp_t* s, const char* p, const char* endp) {
int err;
CALLBACK_MAYBE(s, on_message_begin);
return err;
}
int llhttp__on_protocol(llhttp_t* s, const char* p, const char* endp) {
int err;
SPAN_CALLBACK_MAYBE(s, on_protocol, p, endp - p);
return err;
}
int llhttp__on_protocol_complete(llhttp_t* s, const char* p, const char* endp) {
int err;
CALLBACK_MAYBE(s, on_protocol_complete);
return err;
}
int llhttp__on_url(llhttp_t* s, const char* p, const char* endp) {
int err;
SPAN_CALLBACK_MAYBE(s, on_url, p, endp - p);
return err;
}
int llhttp__on_url_complete(llhttp_t* s, const char* p, const char* endp) {
int err;
CALLBACK_MAYBE(s, on_url_complete);
return err;
}
int llhttp__on_status(llhttp_t* s, const char* p, const char* endp) {
int err;
SPAN_CALLBACK_MAYBE(s, on_status, p, endp - p);
return err;
}
int llhttp__on_status_complete(llhttp_t* s, const char* p, const char* endp) {
int err;
CALLBACK_MAYBE(s, on_status_complete);
return err;
}
int llhttp__on_method(llhttp_t* s, const char* p, const char* endp) {
int err;
SPAN_CALLBACK_MAYBE(s, on_method, p, endp - p);
return err;
}
int llhttp__on_method_complete(llhttp_t* s, const char* p, const char* endp) {
int err;
CALLBACK_MAYBE(s, on_method_complete);
return err;
}
int llhttp__on_version(llhttp_t* s, const char* p, const char* endp) {
int err;
SPAN_CALLBACK_MAYBE(s, on_version, p, endp - p);
return err;
}
int llhttp__on_version_complete(llhttp_t* s, const char* p, const char* endp) {
int err;
CALLBACK_MAYBE(s, on_version_complete);
return err;
}
int llhttp__on_header_field(llhttp_t* s, const char* p, const char* endp) {
int err;
SPAN_CALLBACK_MAYBE(s, on_header_field, p, endp - p);
return err;
}
int llhttp__on_header_field_complete(llhttp_t* s, const char* p, const char* endp) {
int err;
CALLBACK_MAYBE(s, on_header_field_complete);
return err;
}
int llhttp__on_header_value(llhttp_t* s, const char* p, const char* endp) {
int err;
SPAN_CALLBACK_MAYBE(s, on_header_value, p, endp - p);
return err;
}
int llhttp__on_header_value_complete(llhttp_t* s, const char* p, const char* endp) {
int err;
CALLBACK_MAYBE(s, on_header_value_complete);
return err;
}
int llhttp__on_headers_complete(llhttp_t* s, const char* p, const char* endp) {
int err;
CALLBACK_MAYBE(s, on_headers_complete);
return err;
}
int llhttp__on_message_complete(llhttp_t* s, const char* p, const char* endp) {
int err;
CALLBACK_MAYBE(s, on_message_complete);
return err;
}
int llhttp__on_body(llhttp_t* s, const char* p, const char* endp) {
int err;
SPAN_CALLBACK_MAYBE(s, on_body, p, endp - p);
return err;
}
int llhttp__on_chunk_header(llhttp_t* s, const char* p, const char* endp) {
int err;
CALLBACK_MAYBE(s, on_chunk_header);
return err;
}
int llhttp__on_chunk_extension_name(llhttp_t* s, const char* p, const char* endp) {
int err;
SPAN_CALLBACK_MAYBE(s, on_chunk_extension_name, p, endp - p);
return err;
}
int llhttp__on_chunk_extension_name_complete(llhttp_t* s, const char* p, const char* endp) {
int err;
CALLBACK_MAYBE(s, on_chunk_extension_name_complete);
return err;
}
int llhttp__on_chunk_extension_value(llhttp_t* s, const char* p, const char* endp) {
int err;
SPAN_CALLBACK_MAYBE(s, on_chunk_extension_value, p, endp - p);
return err;
}
int llhttp__on_chunk_extension_value_complete(llhttp_t* s, const char* p, const char* endp) {
int err;
CALLBACK_MAYBE(s, on_chunk_extension_value_complete);
return err;
}
int llhttp__on_chunk_complete(llhttp_t* s, const char* p, const char* endp) {
int err;
CALLBACK_MAYBE(s, on_chunk_complete);
return err;
}
int llhttp__on_reset(llhttp_t* s, const char* p, const char* endp) {
int err;
CALLBACK_MAYBE(s, on_reset);
return err;
}
/* Private */
void llhttp__debug(llhttp_t* s, const char* p, const char* endp,
const char* msg) {
if (p == endp) {
fprintf(stderr, "p=%p type=%d flags=%02x next=null debug=%s\n", s, s->type,
s->flags, msg);
} else {
fprintf(stderr, "p=%p type=%d flags=%02x next=%02x debug=%s\n", s,
s->type, s->flags, *p, msg);
}
}
================================================
FILE: deps/llhttp/src/http.c
================================================
#include
#ifndef LLHTTP__TEST
# include "llhttp.h"
#else
# define llhttp_t llparse_t
#endif /* */
int llhttp_message_needs_eof(const llhttp_t* parser);
int llhttp_should_keep_alive(const llhttp_t* parser);
int llhttp__before_headers_complete(llhttp_t* parser, const char* p,
const char* endp) {
/* Set this here so that on_headers_complete() callbacks can see it */
if ((parser->flags & F_UPGRADE) &&
(parser->flags & F_CONNECTION_UPGRADE)) {
/* For responses, "Upgrade: foo" and "Connection: upgrade" are
* mandatory only when it is a 101 Switching Protocols response,
* otherwise it is purely informational, to announce support.
*/
parser->upgrade =
(parser->type == HTTP_REQUEST || parser->status_code == 101);
} else {
parser->upgrade = (parser->method == HTTP_CONNECT);
}
return 0;
}
/* Return values:
* 0 - No body, `restart`, message_complete
* 1 - CONNECT request, `restart`, message_complete, and pause
* 2 - chunk_size_start
* 3 - body_identity
* 4 - body_identity_eof
* 5 - invalid transfer-encoding for request
*/
int llhttp__after_headers_complete(llhttp_t* parser, const char* p,
const char* endp) {
int hasBody;
hasBody = parser->flags & F_CHUNKED || parser->content_length > 0;
if (
(parser->upgrade && (parser->method == HTTP_CONNECT ||
(parser->flags & F_SKIPBODY) || !hasBody)) ||
/* See RFC 2616 section 4.4 - 1xx e.g. Continue */
(parser->type == HTTP_RESPONSE && parser->status_code == 101)
) {
/* Exit, the rest of the message is in a different protocol. */
return 1;
}
if (parser->type == HTTP_RESPONSE && parser->status_code == 100) {
/* No body, restart as the message is complete */
return 0;
}
/* See RFC 2616 section 4.4 */
if (
parser->flags & F_SKIPBODY || /* response to a HEAD request */
(
parser->type == HTTP_RESPONSE && (
parser->status_code == 102 || /* Processing */
parser->status_code == 103 || /* Early Hints */
parser->status_code == 204 || /* No Content */
parser->status_code == 304 /* Not Modified */
)
)
) {
return 0;
} else if (parser->flags & F_CHUNKED) {
/* chunked encoding - ignore Content-Length header, prepare for a chunk */
return 2;
} else if (parser->flags & F_TRANSFER_ENCODING) {
if (parser->type == HTTP_REQUEST &&
(parser->lenient_flags & LENIENT_CHUNKED_LENGTH) == 0 &&
(parser->lenient_flags & LENIENT_TRANSFER_ENCODING) == 0) {
/* RFC 7230 3.3.3 */
/* If a Transfer-Encoding header field
* is present in a request and the chunked transfer coding is not
* the final encoding, the message body length cannot be determined
* reliably; the server MUST respond with the 400 (Bad Request)
* status code and then close the connection.
*/
return 5;
} else {
/* RFC 7230 3.3.3 */
/* If a Transfer-Encoding header field is present in a response and
* the chunked transfer coding is not the final encoding, the
* message body length is determined by reading the connection until
* it is closed by the server.
*/
return 4;
}
} else {
if (!(parser->flags & F_CONTENT_LENGTH)) {
if (!llhttp_message_needs_eof(parser)) {
/* Assume content-length 0 - read the next */
return 0;
} else {
/* Read body until EOF */
return 4;
}
} else if (parser->content_length == 0) {
/* Content-Length header given but zero: Content-Length: 0\r\n */
return 0;
} else {
/* Content-Length header given and non-zero */
return 3;
}
}
}
int llhttp__after_message_complete(llhttp_t* parser, const char* p,
const char* endp) {
int should_keep_alive;
should_keep_alive = llhttp_should_keep_alive(parser);
parser->finish = HTTP_FINISH_SAFE;
parser->flags = 0;
/* NOTE: this is ignored in loose parsing mode */
return should_keep_alive;
}
int llhttp_message_needs_eof(const llhttp_t* parser) {
if (parser->type == HTTP_REQUEST) {
return 0;
}
/* See RFC 2616 section 4.4 */
if (parser->status_code / 100 == 1 || /* 1xx e.g. Continue */
parser->status_code == 204 || /* No Content */
parser->status_code == 304 || /* Not Modified */
(parser->flags & F_SKIPBODY)) { /* response to a HEAD request */
return 0;
}
/* RFC 7230 3.3.3, see `llhttp__after_headers_complete` */
if ((parser->flags & F_TRANSFER_ENCODING) &&
(parser->flags & F_CHUNKED) == 0) {
return 1;
}
if (parser->flags & (F_CHUNKED | F_CONTENT_LENGTH)) {
return 0;
}
return 1;
}
int llhttp_should_keep_alive(const llhttp_t* parser) {
if (parser->http_major > 0 && parser->http_minor > 0) {
/* HTTP/1.1 */
if (parser->flags & F_CONNECTION_CLOSE) {
return 0;
}
} else {
/* HTTP/1.0 or earlier */
if (!(parser->flags & F_CONNECTION_KEEP_ALIVE)) {
return 0;
}
}
return !llhttp_message_needs_eof(parser);
}
================================================
FILE: deps/llhttp/src/llhttp.c
================================================
#include
#include
#include
#ifdef __SSE4_2__
#ifdef _MSC_VER
#include
#else /* !_MSC_VER */
#include
#endif /* _MSC_VER */
#endif /* __SSE4_2__ */
#ifdef __ARM_NEON__
#include
#endif /* __ARM_NEON__ */
#ifdef __wasm__
#include
#endif /* __wasm__ */
#ifdef _MSC_VER
#define ALIGN(n) _declspec(align(n))
#define UNREACHABLE __assume(0)
#else /* !_MSC_VER */
#define ALIGN(n) __attribute__((aligned(n)))
#define UNREACHABLE __builtin_unreachable()
#endif /* _MSC_VER */
#include "llhttp.h"
typedef int (*llhttp__internal__span_cb)(
llhttp__internal_t*, const char*, const char*);
static const unsigned char llparse_blob0[] = {
'o', 'n'
};
static const unsigned char llparse_blob1[] = {
'e', 'c', 't', 'i', 'o', 'n'
};
static const unsigned char llparse_blob2[] = {
'l', 'o', 's', 'e'
};
static const unsigned char llparse_blob3[] = {
'e', 'e', 'p', '-', 'a', 'l', 'i', 'v', 'e'
};
static const unsigned char llparse_blob4[] = {
'p', 'g', 'r', 'a', 'd', 'e'
};
static const unsigned char llparse_blob5[] = {
'c', 'h', 'u', 'n', 'k', 'e', 'd'
};
#ifdef __SSE4_2__
static const unsigned char ALIGN(16) llparse_blob6[] = {
0x9, 0x9, ' ', '~', 0x80, 0xff, 0x0, 0x0, 0x0, 0x0, 0x0,
0x0, 0x0, 0x0, 0x0, 0x0
};
#endif /* __SSE4_2__ */
#ifdef __SSE4_2__
static const unsigned char ALIGN(16) llparse_blob7[] = {
'!', '!', '#', '\'', '*', '+', '-', '.', '0', '9', 'A',
'Z', '^', 'z', '|', '|'
};
#endif /* __SSE4_2__ */
#ifdef __SSE4_2__
static const unsigned char ALIGN(16) llparse_blob8[] = {
'~', '~', 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
0x0, 0x0, 0x0, 0x0, 0x0
};
#endif /* __SSE4_2__ */
static const unsigned char llparse_blob9[] = {
'e', 'n', 't', '-', 'l', 'e', 'n', 'g', 't', 'h'
};
static const unsigned char llparse_blob10[] = {
'r', 'o', 'x', 'y', '-', 'c', 'o', 'n', 'n', 'e', 'c',
't', 'i', 'o', 'n'
};
static const unsigned char llparse_blob11[] = {
'r', 'a', 'n', 's', 'f', 'e', 'r', '-', 'e', 'n', 'c',
'o', 'd', 'i', 'n', 'g'
};
static const unsigned char llparse_blob12[] = {
'p', 'g', 'r', 'a', 'd', 'e'
};
static const unsigned char llparse_blob13[] = {
'T', 'T', 'P'
};
static const unsigned char llparse_blob14[] = {
0xd, 0xa, 0xd, 0xa, 'S', 'M', 0xd, 0xa, 0xd, 0xa
};
static const unsigned char llparse_blob15[] = {
'C', 'E'
};
static const unsigned char llparse_blob16[] = {
'T', 'S', 'P'
};
static const unsigned char llparse_blob17[] = {
'N', 'O', 'U', 'N', 'C', 'E'
};
static const unsigned char llparse_blob18[] = {
'I', 'N', 'D'
};
static const unsigned char llparse_blob19[] = {
'E', 'C', 'K', 'O', 'U', 'T'
};
static const unsigned char llparse_blob20[] = {
'N', 'E', 'C', 'T'
};
static const unsigned char llparse_blob21[] = {
'E', 'T', 'E'
};
static const unsigned char llparse_blob22[] = {
'C', 'R', 'I', 'B', 'E'
};
static const unsigned char llparse_blob23[] = {
'L', 'U', 'S', 'H'
};
static const unsigned char llparse_blob24[] = {
'E', 'T'
};
static const unsigned char llparse_blob25[] = {
'P', 'A', 'R', 'A', 'M', 'E', 'T', 'E', 'R'
};
static const unsigned char llparse_blob26[] = {
'E', 'A', 'D'
};
static const unsigned char llparse_blob27[] = {
'N', 'K'
};
static const unsigned char llparse_blob28[] = {
'C', 'K'
};
static const unsigned char llparse_blob29[] = {
'S', 'E', 'A', 'R', 'C', 'H'
};
static const unsigned char llparse_blob30[] = {
'R', 'G', 'E'
};
static const unsigned char llparse_blob31[] = {
'C', 'T', 'I', 'V', 'I', 'T', 'Y'
};
static const unsigned char llparse_blob32[] = {
'L', 'E', 'N', 'D', 'A', 'R'
};
static const unsigned char llparse_blob33[] = {
'V', 'E'
};
static const unsigned char llparse_blob34[] = {
'O', 'T', 'I', 'F', 'Y'
};
static const unsigned char llparse_blob35[] = {
'P', 'T', 'I', 'O', 'N', 'S'
};
static const unsigned char llparse_blob36[] = {
'C', 'H'
};
static const unsigned char llparse_blob37[] = {
'S', 'E'
};
static const unsigned char llparse_blob38[] = {
'A', 'Y'
};
static const unsigned char llparse_blob39[] = {
'S', 'T'
};
static const unsigned char llparse_blob40[] = {
'I', 'N', 'D'
};
static const unsigned char llparse_blob41[] = {
'A', 'T', 'C', 'H'
};
static const unsigned char llparse_blob42[] = {
'G', 'E'
};
static const unsigned char llparse_blob43[] = {
'U', 'E', 'R', 'Y'
};
static const unsigned char llparse_blob44[] = {
'I', 'N', 'D'
};
static const unsigned char llparse_blob45[] = {
'O', 'R', 'D'
};
static const unsigned char llparse_blob46[] = {
'I', 'R', 'E', 'C', 'T'
};
static const unsigned char llparse_blob47[] = {
'O', 'R', 'T'
};
static const unsigned char llparse_blob48[] = {
'R', 'C', 'H'
};
static const unsigned char llparse_blob49[] = {
'P', 'A', 'R', 'A', 'M', 'E', 'T', 'E', 'R'
};
static const unsigned char llparse_blob50[] = {
'U', 'R', 'C', 'E'
};
static const unsigned char llparse_blob51[] = {
'B', 'S', 'C', 'R', 'I', 'B', 'E'
};
static const unsigned char llparse_blob52[] = {
'A', 'R', 'D', 'O', 'W', 'N'
};
static const unsigned char llparse_blob53[] = {
'A', 'C', 'E'
};
static const unsigned char llparse_blob54[] = {
'I', 'N', 'D'
};
static const unsigned char llparse_blob55[] = {
'N', 'K'
};
static const unsigned char llparse_blob56[] = {
'C', 'K'
};
static const unsigned char llparse_blob57[] = {
'U', 'B', 'S', 'C', 'R', 'I', 'B', 'E'
};
static const unsigned char llparse_blob58[] = {
'T', 'T', 'P'
};
static const unsigned char llparse_blob59[] = {
'C', 'E'
};
static const unsigned char llparse_blob60[] = {
'T', 'S', 'P'
};
static const unsigned char llparse_blob61[] = {
'A', 'D'
};
static const unsigned char llparse_blob62[] = {
'T', 'P', '/'
};
enum llparse_match_status_e {
kMatchComplete,
kMatchPause,
kMatchMismatch
};
typedef enum llparse_match_status_e llparse_match_status_t;
struct llparse_match_s {
llparse_match_status_t status;
const unsigned char* current;
};
typedef struct llparse_match_s llparse_match_t;
static llparse_match_t llparse__match_sequence_to_lower(
llhttp__internal_t* s, const unsigned char* p,
const unsigned char* endp,
const unsigned char* seq, uint32_t seq_len) {
uint32_t index;
llparse_match_t res;
index = s->_index;
for (; p != endp; p++) {
unsigned char current;
current = ((*p) >= 'A' && (*p) <= 'Z' ? (*p | 0x20) : (*p));
if (current == seq[index]) {
if (++index == seq_len) {
res.status = kMatchComplete;
goto reset;
}
} else {
res.status = kMatchMismatch;
goto reset;
}
}
s->_index = index;
res.status = kMatchPause;
res.current = p;
return res;
reset:
s->_index = 0;
res.current = p;
return res;
}
static llparse_match_t llparse__match_sequence_to_lower_unsafe(
llhttp__internal_t* s, const unsigned char* p,
const unsigned char* endp,
const unsigned char* seq, uint32_t seq_len) {
uint32_t index;
llparse_match_t res;
index = s->_index;
for (; p != endp; p++) {
unsigned char current;
current = ((*p) | 0x20);
if (current == seq[index]) {
if (++index == seq_len) {
res.status = kMatchComplete;
goto reset;
}
} else {
res.status = kMatchMismatch;
goto reset;
}
}
s->_index = index;
res.status = kMatchPause;
res.current = p;
return res;
reset:
s->_index = 0;
res.current = p;
return res;
}
static llparse_match_t llparse__match_sequence_id(
llhttp__internal_t* s, const unsigned char* p,
const unsigned char* endp,
const unsigned char* seq, uint32_t seq_len) {
uint32_t index;
llparse_match_t res;
index = s->_index;
for (; p != endp; p++) {
unsigned char current;
current = *p;
if (current == seq[index]) {
if (++index == seq_len) {
res.status = kMatchComplete;
goto reset;
}
} else {
res.status = kMatchMismatch;
goto reset;
}
}
s->_index = index;
res.status = kMatchPause;
res.current = p;
return res;
reset:
s->_index = 0;
res.current = p;
return res;
}
enum llparse_state_e {
s_error,
s_n_llhttp__internal__n_closed,
s_n_llhttp__internal__n_invoke_llhttp__after_message_complete,
s_n_llhttp__internal__n_pause_1,
s_n_llhttp__internal__n_invoke_is_equal_upgrade,
s_n_llhttp__internal__n_invoke_llhttp__on_message_complete_2,
s_n_llhttp__internal__n_chunk_data_almost_done_1,
s_n_llhttp__internal__n_chunk_data_almost_done,
s_n_llhttp__internal__n_consume_content_length,
s_n_llhttp__internal__n_span_start_llhttp__on_body,
s_n_llhttp__internal__n_invoke_is_equal_content_length,
s_n_llhttp__internal__n_chunk_size_almost_done,
s_n_llhttp__internal__n_invoke_test_lenient_flags_9,
s_n_llhttp__internal__n_invoke_llhttp__on_chunk_extension_name_complete,
s_n_llhttp__internal__n_invoke_llhttp__on_chunk_extension_name_complete_1,
s_n_llhttp__internal__n_invoke_llhttp__on_chunk_extension_name_complete_2,
s_n_llhttp__internal__n_invoke_test_lenient_flags_10,
s_n_llhttp__internal__n_invoke_llhttp__on_chunk_extension_value_complete,
s_n_llhttp__internal__n_invoke_llhttp__on_chunk_extension_value_complete_1,
s_n_llhttp__internal__n_chunk_extension_quoted_value_done,
s_n_llhttp__internal__n_invoke_llhttp__on_chunk_extension_value_complete_2,
s_n_llhttp__internal__n_error_30,
s_n_llhttp__internal__n_chunk_extension_quoted_value_quoted_pair,
s_n_llhttp__internal__n_error_31,
s_n_llhttp__internal__n_chunk_extension_quoted_value,
s_n_llhttp__internal__n_invoke_llhttp__on_chunk_extension_value_complete_3,
s_n_llhttp__internal__n_error_33,
s_n_llhttp__internal__n_chunk_extension_value,
s_n_llhttp__internal__n_span_start_llhttp__on_chunk_extension_value,
s_n_llhttp__internal__n_error_34,
s_n_llhttp__internal__n_chunk_extension_name,
s_n_llhttp__internal__n_span_start_llhttp__on_chunk_extension_name,
s_n_llhttp__internal__n_chunk_extensions,
s_n_llhttp__internal__n_chunk_size_otherwise,
s_n_llhttp__internal__n_chunk_size,
s_n_llhttp__internal__n_chunk_size_digit,
s_n_llhttp__internal__n_invoke_update_content_length_1,
s_n_llhttp__internal__n_consume_content_length_1,
s_n_llhttp__internal__n_span_start_llhttp__on_body_1,
s_n_llhttp__internal__n_eof,
s_n_llhttp__internal__n_span_start_llhttp__on_body_2,
s_n_llhttp__internal__n_invoke_llhttp__after_headers_complete,
s_n_llhttp__internal__n_error_5,
s_n_llhttp__internal__n_headers_almost_done,
s_n_llhttp__internal__n_header_field_colon_discard_ws,
s_n_llhttp__internal__n_invoke_llhttp__on_header_value_complete,
s_n_llhttp__internal__n_span_start_llhttp__on_header_value,
s_n_llhttp__internal__n_header_value_discard_lws,
s_n_llhttp__internal__n_header_value_discard_ws_almost_done,
s_n_llhttp__internal__n_header_value_lws,
s_n_llhttp__internal__n_header_value_almost_done,
s_n_llhttp__internal__n_invoke_test_lenient_flags_17,
s_n_llhttp__internal__n_header_value_lenient,
s_n_llhttp__internal__n_error_54,
s_n_llhttp__internal__n_header_value_otherwise,
s_n_llhttp__internal__n_header_value_connection_token,
s_n_llhttp__internal__n_header_value_connection_ws,
s_n_llhttp__internal__n_header_value_connection_1,
s_n_llhttp__internal__n_header_value_connection_2,
s_n_llhttp__internal__n_header_value_connection_3,
s_n_llhttp__internal__n_header_value_connection,
s_n_llhttp__internal__n_error_56,
s_n_llhttp__internal__n_error_57,
s_n_llhttp__internal__n_header_value_content_length_ws,
s_n_llhttp__internal__n_header_value_content_length,
s_n_llhttp__internal__n_error_59,
s_n_llhttp__internal__n_error_58,
s_n_llhttp__internal__n_header_value_te_token_ows,
s_n_llhttp__internal__n_header_value,
s_n_llhttp__internal__n_header_value_te_token,
s_n_llhttp__internal__n_header_value_te_chunked_last,
s_n_llhttp__internal__n_header_value_te_chunked,
s_n_llhttp__internal__n_span_start_llhttp__on_header_value_1,
s_n_llhttp__internal__n_header_value_discard_ws,
s_n_llhttp__internal__n_invoke_load_header_state,
s_n_llhttp__internal__n_invoke_llhttp__on_header_field_complete,
s_n_llhttp__internal__n_header_field_general_otherwise,
s_n_llhttp__internal__n_header_field_general,
s_n_llhttp__internal__n_header_field_colon,
s_n_llhttp__internal__n_header_field_3,
s_n_llhttp__internal__n_header_field_4,
s_n_llhttp__internal__n_header_field_2,
s_n_llhttp__internal__n_header_field_1,
s_n_llhttp__internal__n_header_field_5,
s_n_llhttp__internal__n_header_field_6,
s_n_llhttp__internal__n_header_field_7,
s_n_llhttp__internal__n_header_field,
s_n_llhttp__internal__n_span_start_llhttp__on_header_field,
s_n_llhttp__internal__n_header_field_start,
s_n_llhttp__internal__n_headers_start,
s_n_llhttp__internal__n_url_to_http_09,
s_n_llhttp__internal__n_url_skip_to_http09,
s_n_llhttp__internal__n_url_skip_lf_to_http09_1,
s_n_llhttp__internal__n_url_skip_lf_to_http09,
s_n_llhttp__internal__n_req_pri_upgrade,
s_n_llhttp__internal__n_req_http_complete_crlf,
s_n_llhttp__internal__n_req_http_complete,
s_n_llhttp__internal__n_invoke_load_method_1,
s_n_llhttp__internal__n_invoke_llhttp__on_version_complete,
s_n_llhttp__internal__n_error_67,
s_n_llhttp__internal__n_error_74,
s_n_llhttp__internal__n_req_http_minor,
s_n_llhttp__internal__n_error_75,
s_n_llhttp__internal__n_req_http_dot,
s_n_llhttp__internal__n_error_76,
s_n_llhttp__internal__n_req_http_major,
s_n_llhttp__internal__n_span_start_llhttp__on_version,
s_n_llhttp__internal__n_req_after_protocol,
s_n_llhttp__internal__n_invoke_load_method,
s_n_llhttp__internal__n_invoke_llhttp__on_protocol_complete,
s_n_llhttp__internal__n_error_82,
s_n_llhttp__internal__n_req_after_http_start_1,
s_n_llhttp__internal__n_invoke_load_method_2,
s_n_llhttp__internal__n_invoke_llhttp__on_protocol_complete_1,
s_n_llhttp__internal__n_req_after_http_start_2,
s_n_llhttp__internal__n_invoke_load_method_3,
s_n_llhttp__internal__n_invoke_llhttp__on_protocol_complete_2,
s_n_llhttp__internal__n_req_after_http_start_3,
s_n_llhttp__internal__n_req_after_http_start,
s_n_llhttp__internal__n_span_start_llhttp__on_protocol,
s_n_llhttp__internal__n_req_http_start,
s_n_llhttp__internal__n_url_to_http,
s_n_llhttp__internal__n_url_skip_to_http,
s_n_llhttp__internal__n_url_fragment,
s_n_llhttp__internal__n_span_end_stub_query_3,
s_n_llhttp__internal__n_url_query,
s_n_llhttp__internal__n_url_query_or_fragment,
s_n_llhttp__internal__n_url_path,
s_n_llhttp__internal__n_span_start_stub_path_2,
s_n_llhttp__internal__n_span_start_stub_path,
s_n_llhttp__internal__n_span_start_stub_path_1,
s_n_llhttp__internal__n_url_server_with_at,
s_n_llhttp__internal__n_url_server,
s_n_llhttp__internal__n_url_schema_delim_1,
s_n_llhttp__internal__n_url_schema_delim,
s_n_llhttp__internal__n_span_end_stub_schema,
s_n_llhttp__internal__n_url_schema,
s_n_llhttp__internal__n_url_start,
s_n_llhttp__internal__n_span_start_llhttp__on_url_1,
s_n_llhttp__internal__n_url_entry_normal,
s_n_llhttp__internal__n_span_start_llhttp__on_url,
s_n_llhttp__internal__n_url_entry_connect,
s_n_llhttp__internal__n_req_spaces_before_url,
s_n_llhttp__internal__n_req_first_space_before_url,
s_n_llhttp__internal__n_invoke_llhttp__on_method_complete_1,
s_n_llhttp__internal__n_after_start_req_2,
s_n_llhttp__internal__n_after_start_req_3,
s_n_llhttp__internal__n_after_start_req_1,
s_n_llhttp__internal__n_after_start_req_4,
s_n_llhttp__internal__n_after_start_req_6,
s_n_llhttp__internal__n_after_start_req_8,
s_n_llhttp__internal__n_after_start_req_9,
s_n_llhttp__internal__n_after_start_req_7,
s_n_llhttp__internal__n_after_start_req_5,
s_n_llhttp__internal__n_after_start_req_12,
s_n_llhttp__internal__n_after_start_req_13,
s_n_llhttp__internal__n_after_start_req_11,
s_n_llhttp__internal__n_after_start_req_10,
s_n_llhttp__internal__n_after_start_req_14,
s_n_llhttp__internal__n_after_start_req_17,
s_n_llhttp__internal__n_after_start_req_16,
s_n_llhttp__internal__n_after_start_req_15,
s_n_llhttp__internal__n_after_start_req_18,
s_n_llhttp__internal__n_after_start_req_20,
s_n_llhttp__internal__n_after_start_req_21,
s_n_llhttp__internal__n_after_start_req_19,
s_n_llhttp__internal__n_after_start_req_23,
s_n_llhttp__internal__n_after_start_req_24,
s_n_llhttp__internal__n_after_start_req_26,
s_n_llhttp__internal__n_after_start_req_28,
s_n_llhttp__internal__n_after_start_req_29,
s_n_llhttp__internal__n_after_start_req_27,
s_n_llhttp__internal__n_after_start_req_25,
s_n_llhttp__internal__n_after_start_req_30,
s_n_llhttp__internal__n_after_start_req_22,
s_n_llhttp__internal__n_after_start_req_31,
s_n_llhttp__internal__n_after_start_req_32,
s_n_llhttp__internal__n_after_start_req_35,
s_n_llhttp__internal__n_after_start_req_36,
s_n_llhttp__internal__n_after_start_req_34,
s_n_llhttp__internal__n_after_start_req_37,
s_n_llhttp__internal__n_after_start_req_38,
s_n_llhttp__internal__n_after_start_req_42,
s_n_llhttp__internal__n_after_start_req_43,
s_n_llhttp__internal__n_after_start_req_41,
s_n_llhttp__internal__n_after_start_req_40,
s_n_llhttp__internal__n_after_start_req_39,
s_n_llhttp__internal__n_after_start_req_45,
s_n_llhttp__internal__n_after_start_req_44,
s_n_llhttp__internal__n_after_start_req_33,
s_n_llhttp__internal__n_after_start_req_46,
s_n_llhttp__internal__n_after_start_req_49,
s_n_llhttp__internal__n_after_start_req_50,
s_n_llhttp__internal__n_after_start_req_51,
s_n_llhttp__internal__n_after_start_req_52,
s_n_llhttp__internal__n_after_start_req_48,
s_n_llhttp__internal__n_after_start_req_47,
s_n_llhttp__internal__n_after_start_req_55,
s_n_llhttp__internal__n_after_start_req_57,
s_n_llhttp__internal__n_after_start_req_58,
s_n_llhttp__internal__n_after_start_req_56,
s_n_llhttp__internal__n_after_start_req_54,
s_n_llhttp__internal__n_after_start_req_59,
s_n_llhttp__internal__n_after_start_req_60,
s_n_llhttp__internal__n_after_start_req_53,
s_n_llhttp__internal__n_after_start_req_62,
s_n_llhttp__internal__n_after_start_req_63,
s_n_llhttp__internal__n_after_start_req_61,
s_n_llhttp__internal__n_after_start_req_66,
s_n_llhttp__internal__n_after_start_req_68,
s_n_llhttp__internal__n_after_start_req_69,
s_n_llhttp__internal__n_after_start_req_67,
s_n_llhttp__internal__n_after_start_req_70,
s_n_llhttp__internal__n_after_start_req_65,
s_n_llhttp__internal__n_after_start_req_64,
s_n_llhttp__internal__n_after_start_req,
s_n_llhttp__internal__n_span_start_llhttp__on_method_1,
s_n_llhttp__internal__n_res_line_almost_done,
s_n_llhttp__internal__n_invoke_test_lenient_flags_30,
s_n_llhttp__internal__n_res_status,
s_n_llhttp__internal__n_span_start_llhttp__on_status,
s_n_llhttp__internal__n_res_status_code_otherwise,
s_n_llhttp__internal__n_res_status_code_digit_3,
s_n_llhttp__internal__n_res_status_code_digit_2,
s_n_llhttp__internal__n_res_status_code_digit_1,
s_n_llhttp__internal__n_res_after_version,
s_n_llhttp__internal__n_invoke_llhttp__on_version_complete_1,
s_n_llhttp__internal__n_error_93,
s_n_llhttp__internal__n_error_107,
s_n_llhttp__internal__n_res_http_minor,
s_n_llhttp__internal__n_error_108,
s_n_llhttp__internal__n_res_http_dot,
s_n_llhttp__internal__n_error_109,
s_n_llhttp__internal__n_res_http_major,
s_n_llhttp__internal__n_span_start_llhttp__on_version_1,
s_n_llhttp__internal__n_res_after_protocol,
s_n_llhttp__internal__n_invoke_llhttp__on_protocol_complete_3,
s_n_llhttp__internal__n_error_115,
s_n_llhttp__internal__n_res_after_start_1,
s_n_llhttp__internal__n_res_after_start_2,
s_n_llhttp__internal__n_res_after_start_3,
s_n_llhttp__internal__n_res_after_start,
s_n_llhttp__internal__n_span_start_llhttp__on_protocol_1,
s_n_llhttp__internal__n_invoke_llhttp__on_method_complete,
s_n_llhttp__internal__n_req_or_res_method_2,
s_n_llhttp__internal__n_invoke_update_type_1,
s_n_llhttp__internal__n_req_or_res_method_3,
s_n_llhttp__internal__n_req_or_res_method_1,
s_n_llhttp__internal__n_req_or_res_method,
s_n_llhttp__internal__n_span_start_llhttp__on_method,
s_n_llhttp__internal__n_start_req_or_res,
s_n_llhttp__internal__n_invoke_load_type,
s_n_llhttp__internal__n_invoke_update_finish,
s_n_llhttp__internal__n_start,
};
typedef enum llparse_state_e llparse_state_t;
int llhttp__on_method(
llhttp__internal_t* s, const unsigned char* p,
const unsigned char* endp);
int llhttp__on_url(
llhttp__internal_t* s, const unsigned char* p,
const unsigned char* endp);
int llhttp__on_protocol(
llhttp__internal_t* s, const unsigned char* p,
const unsigned char* endp);
int llhttp__on_version(
llhttp__internal_t* s, const unsigned char* p,
const unsigned char* endp);
int llhttp__on_header_field(
llhttp__internal_t* s, const unsigned char* p,
const unsigned char* endp);
int llhttp__on_header_value(
llhttp__internal_t* s, const unsigned char* p,
const unsigned char* endp);
int llhttp__on_body(
llhttp__internal_t* s, const unsigned char* p,
const unsigned char* endp);
int llhttp__on_chunk_extension_name(
llhttp__internal_t* s, const unsigned char* p,
const unsigned char* endp);
int llhttp__on_chunk_extension_value(
llhttp__internal_t* s, const unsigned char* p,
const unsigned char* endp);
int llhttp__on_status(
llhttp__internal_t* s, const unsigned char* p,
const unsigned char* endp);
int llhttp__internal__c_load_initial_message_completed(
llhttp__internal_t* state,
const unsigned char* p,
const unsigned char* endp) {
return state->initial_message_completed;
}
int llhttp__on_reset(
llhttp__internal_t* s, const unsigned char* p,
const unsigned char* endp);
int llhttp__internal__c_update_finish(
llhttp__internal_t* state,
const unsigned char* p,
const unsigned char* endp) {
state->finish = 2;
return 0;
}
int llhttp__on_message_begin(
llhttp__internal_t* s, const unsigned char* p,
const unsigned char* endp);
int llhttp__internal__c_load_type(
llhttp__internal_t* state,
const unsigned char* p,
const unsigned char* endp) {
return state->type;
}
int llhttp__internal__c_store_method(
llhttp__internal_t* state,
const unsigned char* p,
const unsigned char* endp,
int match) {
state->method = match;
return 0;
}
int llhttp__on_method_complete(
llhttp__internal_t* s, const unsigned char* p,
const unsigned char* endp);
int llhttp__internal__c_is_equal_method(
llhttp__internal_t* state,
const unsigned char* p,
const unsigned char* endp) {
return state->method == 5;
}
int llhttp__internal__c_update_http_major(
llhttp__internal_t* state,
const unsigned char* p,
const unsigned char* endp) {
state->http_major = 0;
return 0;
}
int llhttp__internal__c_update_http_minor(
llhttp__internal_t* state,
const unsigned char* p,
const unsigned char* endp) {
state->http_minor = 9;
return 0;
}
int llhttp__on_url_complete(
llhttp__internal_t* s, const unsigned char* p,
const unsigned char* endp);
int llhttp__internal__c_test_lenient_flags(
llhttp__internal_t* state,
const unsigned char* p,
const unsigned char* endp) {
return (state->lenient_flags & 1) == 1;
}
int llhttp__internal__c_test_lenient_flags_1(
llhttp__internal_t* state,
const unsigned char* p,
const unsigned char* endp) {
return (state->lenient_flags & 256) == 256;
}
int llhttp__internal__c_test_flags(
llhttp__internal_t* state,
const unsigned char* p,
const unsigned char* endp) {
return (state->flags & 128) == 128;
}
int llhttp__on_chunk_complete(
llhttp__internal_t* s, const unsigned char* p,
const unsigned char* endp);
int llhttp__on_message_complete(
llhttp__internal_t* s, const unsigned char* p,
const unsigned char* endp);
int llhttp__internal__c_is_equal_upgrade(
llhttp__internal_t* state,
const unsigned char* p,
const unsigned char* endp) {
return state->upgrade == 1;
}
int llhttp__after_message_complete(
llhttp__internal_t* s, const unsigned char* p,
const unsigned char* endp);
int llhttp__internal__c_update_content_length(
llhttp__internal_t* state,
const unsigned char* p,
const unsigned char* endp) {
state->content_length = 0;
return 0;
}
int llhttp__internal__c_update_initial_message_completed(
llhttp__internal_t* state,
const unsigned char* p,
const unsigned char* endp) {
state->initial_message_completed = 1;
return 0;
}
int llhttp__internal__c_update_finish_1(
llhttp__internal_t* state,
const unsigned char* p,
const unsigned char* endp) {
state->finish = 0;
return 0;
}
int llhttp__internal__c_test_lenient_flags_2(
llhttp__internal_t* state,
const unsigned char* p,
const unsigned char* endp) {
return (state->lenient_flags & 4) == 4;
}
int llhttp__internal__c_test_lenient_flags_3(
llhttp__internal_t* state,
const unsigned char* p,
const unsigned char* endp) {
return (state->lenient_flags & 32) == 32;
}
int llhttp__before_headers_complete(
llhttp__internal_t* s, const unsigned char* p,
const unsigned char* endp);
int llhttp__on_headers_complete(
llhttp__internal_t* s, const unsigned char* p,
const unsigned char* endp);
int llhttp__after_headers_complete(
llhttp__internal_t* s, const unsigned char* p,
const unsigned char* endp);
int llhttp__internal__c_mul_add_content_length(
llhttp__internal_t* state,
const unsigned char* p,
const unsigned char* endp,
int match) {
/* Multiplication overflow */
if (state->content_length > 0xffffffffffffffffULL / 16) {
return 1;
}
state->content_length *= 16;
/* Addition overflow */
if (match >= 0) {
if (state->content_length > 0xffffffffffffffffULL - match) {
return 1;
}
} else {
if (state->content_length < 0ULL - match) {
return 1;
}
}
state->content_length += match;
return 0;
}
int llhttp__internal__c_test_lenient_flags_4(
llhttp__internal_t* state,
const unsigned char* p,
const unsigned char* endp) {
return (state->lenient_flags & 512) == 512;
}
int llhttp__on_chunk_header(
llhttp__internal_t* s, const unsigned char* p,
const unsigned char* endp);
int llhttp__internal__c_is_equal_content_length(
llhttp__internal_t* state,
const unsigned char* p,
const unsigned char* endp) {
return state->content_length == 0;
}
int llhttp__internal__c_test_lenient_flags_7(
llhttp__internal_t* state,
const unsigned char* p,
const unsigned char* endp) {
return (state->lenient_flags & 128) == 128;
}
int llhttp__internal__c_or_flags(
llhttp__internal_t* state,
const unsigned char* p,
const unsigned char* endp) {
state->flags |= 128;
return 0;
}
int llhttp__internal__c_test_lenient_flags_8(
llhttp__internal_t* state,
const unsigned char* p,
const unsigned char* endp) {
return (state->lenient_flags & 64) == 64;
}
int llhttp__on_chunk_extension_name_complete(
llhttp__internal_t* s, const unsigned char* p,
const unsigned char* endp);
int llhttp__on_chunk_extension_value_complete(
llhttp__internal_t* s, const unsigned char* p,
const unsigned char* endp);
int llhttp__internal__c_update_finish_3(
llhttp__internal_t* state,
const unsigned char* p,
const unsigned char* endp) {
state->finish = 1;
return 0;
}
int llhttp__internal__c_or_flags_1(
llhttp__internal_t* state,
const unsigned char* p,
const unsigned char* endp) {
state->flags |= 64;
return 0;
}
int llhttp__internal__c_update_upgrade(
llhttp__internal_t* state,
const unsigned char* p,
const unsigned char* endp) {
state->upgrade = 1;
return 0;
}
int llhttp__internal__c_store_header_state(
llhttp__internal_t* state,
const unsigned char* p,
const unsigned char* endp,
int match) {
state->header_state = match;
return 0;
}
int llhttp__on_header_field_complete(
llhttp__internal_t* s, const unsigned char* p,
const unsigned char* endp);
int llhttp__internal__c_load_header_state(
llhttp__internal_t* state,
const unsigned char* p,
const unsigned char* endp) {
return state->header_state;
}
int llhttp__internal__c_test_flags_4(
llhttp__internal_t* state,
const unsigned char* p,
const unsigned char* endp) {
return (state->flags & 512) == 512;
}
int llhttp__internal__c_test_lenient_flags_22(
llhttp__internal_t* state,
const unsigned char* p,
const unsigned char* endp) {
return (state->lenient_flags & 2) == 2;
}
int llhttp__internal__c_or_flags_5(
llhttp__internal_t* state,
const unsigned char* p,
const unsigned char* endp) {
state->flags |= 1;
return 0;
}
int llhttp__internal__c_update_header_state(
llhttp__internal_t* state,
const unsigned char* p,
const unsigned char* endp) {
state->header_state = 1;
return 0;
}
int llhttp__on_header_value_complete(
llhttp__internal_t* s, const unsigned char* p,
const unsigned char* endp);
int llhttp__internal__c_or_flags_6(
llhttp__internal_t* state,
const unsigned char* p,
const unsigned char* endp) {
state->flags |= 2;
return 0;
}
int llhttp__internal__c_or_flags_7(
llhttp__internal_t* state,
const unsigned char* p,
const unsigned char* endp) {
state->flags |= 4;
return 0;
}
int llhttp__internal__c_or_flags_8(
llhttp__internal_t* state,
const unsigned char* p,
const unsigned char* endp) {
state->flags |= 8;
return 0;
}
int llhttp__internal__c_update_header_state_3(
llhttp__internal_t* state,
const unsigned char* p,
const unsigned char* endp) {
state->header_state = 6;
return 0;
}
int llhttp__internal__c_update_header_state_1(
llhttp__internal_t* state,
const unsigned char* p,
const unsigned char* endp) {
state->header_state = 0;
return 0;
}
int llhttp__internal__c_update_header_state_6(
llhttp__internal_t* state,
const unsigned char* p,
const unsigned char* endp) {
state->header_state = 5;
return 0;
}
int llhttp__internal__c_update_header_state_7(
llhttp__internal_t* state,
const unsigned char* p,
const unsigned char* endp) {
state->header_state = 7;
return 0;
}
int llhttp__internal__c_test_flags_2(
llhttp__internal_t* state,
const unsigned char* p,
const unsigned char* endp) {
return (state->flags & 32) == 32;
}
int llhttp__internal__c_mul_add_content_length_1(
llhttp__internal_t* state,
const unsigned char* p,
const unsigned char* endp,
int match) {
/* Multiplication overflow */
if (state->content_length > 0xffffffffffffffffULL / 10) {
return 1;
}
state->content_length *= 10;
/* Addition overflow */
if (match >= 0) {
if (state->content_length > 0xffffffffffffffffULL - match) {
return 1;
}
} else {
if (state->content_length < 0ULL - match) {
return 1;
}
}
state->content_length += match;
return 0;
}
int llhttp__internal__c_or_flags_17(
llhttp__internal_t* state,
const unsigned char* p,
const unsigned char* endp) {
state->flags |= 32;
return 0;
}
int llhttp__internal__c_test_flags_3(
llhttp__internal_t* state,
const unsigned char* p,
const unsigned char* endp) {
return (state->flags & 8) == 8;
}
int llhttp__internal__c_test_lenient_flags_20(
llhttp__internal_t* state,
const unsigned char* p,
const unsigned char* endp) {
return (state->lenient_flags & 8) == 8;
}
int llhttp__internal__c_or_flags_18(
llhttp__internal_t* state,
const unsigned char* p,
const unsigned char* endp) {
state->flags |= 512;
return 0;
}
int llhttp__internal__c_and_flags(
llhttp__internal_t* state,
const unsigned char* p,
const unsigned char* endp) {
state->flags &= -9;
return 0;
}
int llhttp__internal__c_update_header_state_8(
llhttp__internal_t* state,
const unsigned char* p,
const unsigned char* endp) {
state->header_state = 8;
return 0;
}
int llhttp__internal__c_or_flags_20(
llhttp__internal_t* state,
const unsigned char* p,
const unsigned char* endp) {
state->flags |= 16;
return 0;
}
int llhttp__on_protocol_complete(
llhttp__internal_t* s, const unsigned char* p,
const unsigned char* endp);
int llhttp__internal__c_load_method(
llhttp__internal_t* state,
const unsigned char* p,
const unsigned char* endp) {
return state->method;
}
int llhttp__internal__c_store_http_major(
llhttp__internal_t* state,
const unsigned char* p,
const unsigned char* endp,
int match) {
state->http_major = match;
return 0;
}
int llhttp__internal__c_store_http_minor(
llhttp__internal_t* state,
const unsigned char* p,
const unsigned char* endp,
int match) {
state->http_minor = match;
return 0;
}
int llhttp__internal__c_test_lenient_flags_24(
llhttp__internal_t* state,
const unsigned char* p,
const unsigned char* endp) {
return (state->lenient_flags & 16) == 16;
}
int llhttp__on_version_complete(
llhttp__internal_t* s, const unsigned char* p,
const unsigned char* endp);
int llhttp__internal__c_load_http_major(
llhttp__internal_t* state,
const unsigned char* p,
const unsigned char* endp) {
return state->http_major;
}
int llhttp__internal__c_load_http_minor(
llhttp__internal_t* state,
const unsigned char* p,
const unsigned char* endp) {
return state->http_minor;
}
int llhttp__internal__c_update_status_code(
llhttp__internal_t* state,
const unsigned char* p,
const unsigned char* endp) {
state->status_code = 0;
return 0;
}
int llhttp__internal__c_mul_add_status_code(
llhttp__internal_t* state,
const unsigned char* p,
const unsigned char* endp,
int match) {
/* Multiplication overflow */
if (state->status_code > 0xffff / 10) {
return 1;
}
state->status_code *= 10;
/* Addition overflow */
if (match >= 0) {
if (state->status_code > 0xffff - match) {
return 1;
}
} else {
if (state->status_code < 0 - match) {
return 1;
}
}
state->status_code += match;
return 0;
}
int llhttp__on_status_complete(
llhttp__internal_t* s, const unsigned char* p,
const unsigned char* endp);
int llhttp__internal__c_update_type(
llhttp__internal_t* state,
const unsigned char* p,
const unsigned char* endp) {
state->type = 1;
return 0;
}
int llhttp__internal__c_update_type_1(
llhttp__internal_t* state,
const unsigned char* p,
const unsigned char* endp) {
state->type = 2;
return 0;
}
int llhttp__internal_init(llhttp__internal_t* state) {
memset(state, 0, sizeof(*state));
state->_current = (void*) (intptr_t) s_n_llhttp__internal__n_start;
return 0;
}
static llparse_state_t llhttp__internal__run(
llhttp__internal_t* state,
const unsigned char* p,
const unsigned char* endp) {
int match;
switch ((llparse_state_t) (intptr_t) state->_current) {
case s_n_llhttp__internal__n_closed:
s_n_llhttp__internal__n_closed: {
if (p == endp) {
return s_n_llhttp__internal__n_closed;
}
switch (*p) {
case 10: {
p++;
goto s_n_llhttp__internal__n_closed;
}
case 13: {
p++;
goto s_n_llhttp__internal__n_closed;
}
default: {
p++;
goto s_n_llhttp__internal__n_invoke_test_lenient_flags_3;
}
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_invoke_llhttp__after_message_complete:
s_n_llhttp__internal__n_invoke_llhttp__after_message_complete: {
switch (llhttp__after_message_complete(state, p, endp)) {
case 1:
goto s_n_llhttp__internal__n_invoke_update_content_length;
default:
goto s_n_llhttp__internal__n_invoke_update_finish_1;
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_pause_1:
s_n_llhttp__internal__n_pause_1: {
state->error = 0x16;
state->reason = "Pause on CONNECT/Upgrade";
state->error_pos = (const char*) p;
state->_current = (void*) (intptr_t) s_n_llhttp__internal__n_invoke_llhttp__after_message_complete;
return s_error;
UNREACHABLE;
}
case s_n_llhttp__internal__n_invoke_is_equal_upgrade:
s_n_llhttp__internal__n_invoke_is_equal_upgrade: {
switch (llhttp__internal__c_is_equal_upgrade(state, p, endp)) {
case 0:
goto s_n_llhttp__internal__n_invoke_llhttp__after_message_complete;
default:
goto s_n_llhttp__internal__n_pause_1;
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_invoke_llhttp__on_message_complete_2:
s_n_llhttp__internal__n_invoke_llhttp__on_message_complete_2: {
switch (llhttp__on_message_complete(state, p, endp)) {
case 0:
goto s_n_llhttp__internal__n_invoke_is_equal_upgrade;
case 21:
goto s_n_llhttp__internal__n_pause_13;
default:
goto s_n_llhttp__internal__n_error_38;
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_chunk_data_almost_done_1:
s_n_llhttp__internal__n_chunk_data_almost_done_1: {
if (p == endp) {
return s_n_llhttp__internal__n_chunk_data_almost_done_1;
}
switch (*p) {
case 10: {
p++;
goto s_n_llhttp__internal__n_invoke_llhttp__on_chunk_complete;
}
default: {
goto s_n_llhttp__internal__n_invoke_test_lenient_flags_7;
}
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_chunk_data_almost_done:
s_n_llhttp__internal__n_chunk_data_almost_done: {
if (p == endp) {
return s_n_llhttp__internal__n_chunk_data_almost_done;
}
switch (*p) {
case 10: {
p++;
goto s_n_llhttp__internal__n_invoke_test_lenient_flags_6;
}
case 13: {
p++;
goto s_n_llhttp__internal__n_chunk_data_almost_done_1;
}
default: {
goto s_n_llhttp__internal__n_invoke_test_lenient_flags_7;
}
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_consume_content_length:
s_n_llhttp__internal__n_consume_content_length: {
size_t avail;
uint64_t need;
avail = endp - p;
need = state->content_length;
if (avail >= need) {
p += need;
state->content_length = 0;
goto s_n_llhttp__internal__n_span_end_llhttp__on_body;
}
state->content_length -= avail;
return s_n_llhttp__internal__n_consume_content_length;
UNREACHABLE;
}
case s_n_llhttp__internal__n_span_start_llhttp__on_body:
s_n_llhttp__internal__n_span_start_llhttp__on_body: {
if (p == endp) {
return s_n_llhttp__internal__n_span_start_llhttp__on_body;
}
state->_span_pos0 = (void*) p;
state->_span_cb0 = llhttp__on_body;
goto s_n_llhttp__internal__n_consume_content_length;
UNREACHABLE;
}
case s_n_llhttp__internal__n_invoke_is_equal_content_length:
s_n_llhttp__internal__n_invoke_is_equal_content_length: {
switch (llhttp__internal__c_is_equal_content_length(state, p, endp)) {
case 0:
goto s_n_llhttp__internal__n_span_start_llhttp__on_body;
default:
goto s_n_llhttp__internal__n_invoke_or_flags;
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_chunk_size_almost_done:
s_n_llhttp__internal__n_chunk_size_almost_done: {
if (p == endp) {
return s_n_llhttp__internal__n_chunk_size_almost_done;
}
switch (*p) {
case 10: {
p++;
goto s_n_llhttp__internal__n_invoke_llhttp__on_chunk_header;
}
default: {
goto s_n_llhttp__internal__n_invoke_test_lenient_flags_8;
}
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_invoke_test_lenient_flags_9:
s_n_llhttp__internal__n_invoke_test_lenient_flags_9: {
switch (llhttp__internal__c_test_lenient_flags_1(state, p, endp)) {
case 1:
goto s_n_llhttp__internal__n_chunk_size_almost_done;
default:
goto s_n_llhttp__internal__n_error_20;
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_invoke_llhttp__on_chunk_extension_name_complete:
s_n_llhttp__internal__n_invoke_llhttp__on_chunk_extension_name_complete: {
switch (llhttp__on_chunk_extension_name_complete(state, p, endp)) {
case 0:
goto s_n_llhttp__internal__n_invoke_test_lenient_flags_9;
case 21:
goto s_n_llhttp__internal__n_pause_5;
default:
goto s_n_llhttp__internal__n_error_19;
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_invoke_llhttp__on_chunk_extension_name_complete_1:
s_n_llhttp__internal__n_invoke_llhttp__on_chunk_extension_name_complete_1: {
switch (llhttp__on_chunk_extension_name_complete(state, p, endp)) {
case 0:
goto s_n_llhttp__internal__n_chunk_size_almost_done;
case 21:
goto s_n_llhttp__internal__n_pause_6;
default:
goto s_n_llhttp__internal__n_error_21;
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_invoke_llhttp__on_chunk_extension_name_complete_2:
s_n_llhttp__internal__n_invoke_llhttp__on_chunk_extension_name_complete_2: {
switch (llhttp__on_chunk_extension_name_complete(state, p, endp)) {
case 0:
goto s_n_llhttp__internal__n_chunk_extensions;
case 21:
goto s_n_llhttp__internal__n_pause_7;
default:
goto s_n_llhttp__internal__n_error_22;
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_invoke_test_lenient_flags_10:
s_n_llhttp__internal__n_invoke_test_lenient_flags_10: {
switch (llhttp__internal__c_test_lenient_flags_1(state, p, endp)) {
case 1:
goto s_n_llhttp__internal__n_chunk_size_almost_done;
default:
goto s_n_llhttp__internal__n_error_25;
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_invoke_llhttp__on_chunk_extension_value_complete:
s_n_llhttp__internal__n_invoke_llhttp__on_chunk_extension_value_complete: {
switch (llhttp__on_chunk_extension_value_complete(state, p, endp)) {
case 0:
goto s_n_llhttp__internal__n_invoke_test_lenient_flags_10;
case 21:
goto s_n_llhttp__internal__n_pause_8;
default:
goto s_n_llhttp__internal__n_error_24;
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_invoke_llhttp__on_chunk_extension_value_complete_1:
s_n_llhttp__internal__n_invoke_llhttp__on_chunk_extension_value_complete_1: {
switch (llhttp__on_chunk_extension_value_complete(state, p, endp)) {
case 0:
goto s_n_llhttp__internal__n_chunk_size_almost_done;
case 21:
goto s_n_llhttp__internal__n_pause_9;
default:
goto s_n_llhttp__internal__n_error_26;
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_chunk_extension_quoted_value_done:
s_n_llhttp__internal__n_chunk_extension_quoted_value_done: {
if (p == endp) {
return s_n_llhttp__internal__n_chunk_extension_quoted_value_done;
}
switch (*p) {
case 10: {
goto s_n_llhttp__internal__n_invoke_test_lenient_flags_11;
}
case 13: {
p++;
goto s_n_llhttp__internal__n_chunk_size_almost_done;
}
case ';': {
p++;
goto s_n_llhttp__internal__n_chunk_extensions;
}
default: {
goto s_n_llhttp__internal__n_error_29;
}
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_invoke_llhttp__on_chunk_extension_value_complete_2:
s_n_llhttp__internal__n_invoke_llhttp__on_chunk_extension_value_complete_2: {
switch (llhttp__on_chunk_extension_value_complete(state, p, endp)) {
case 0:
goto s_n_llhttp__internal__n_chunk_extension_quoted_value_done;
case 21:
goto s_n_llhttp__internal__n_pause_10;
default:
goto s_n_llhttp__internal__n_error_27;
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_error_30:
s_n_llhttp__internal__n_error_30: {
state->error = 0x2;
state->reason = "Invalid quoted-pair in chunk extensions quoted value";
state->error_pos = (const char*) p;
state->_current = (void*) (intptr_t) s_error;
return s_error;
UNREACHABLE;
}
case s_n_llhttp__internal__n_chunk_extension_quoted_value_quoted_pair:
s_n_llhttp__internal__n_chunk_extension_quoted_value_quoted_pair: {
static uint8_t lookup_table[] = {
0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1
};
if (p == endp) {
return s_n_llhttp__internal__n_chunk_extension_quoted_value_quoted_pair;
}
switch (lookup_table[(uint8_t) *p]) {
case 1: {
p++;
goto s_n_llhttp__internal__n_chunk_extension_quoted_value;
}
default: {
goto s_n_llhttp__internal__n_span_end_llhttp__on_chunk_extension_value_3;
}
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_error_31:
s_n_llhttp__internal__n_error_31: {
state->error = 0x2;
state->reason = "Invalid character in chunk extensions quoted value";
state->error_pos = (const char*) p;
state->_current = (void*) (intptr_t) s_error;
return s_error;
UNREACHABLE;
}
case s_n_llhttp__internal__n_chunk_extension_quoted_value:
s_n_llhttp__internal__n_chunk_extension_quoted_value: {
static uint8_t lookup_table[] = {
0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1
};
if (p == endp) {
return s_n_llhttp__internal__n_chunk_extension_quoted_value;
}
switch (lookup_table[(uint8_t) *p]) {
case 1: {
p++;
goto s_n_llhttp__internal__n_chunk_extension_quoted_value;
}
case 2: {
p++;
goto s_n_llhttp__internal__n_span_end_llhttp__on_chunk_extension_value_2;
}
case 3: {
p++;
goto s_n_llhttp__internal__n_chunk_extension_quoted_value_quoted_pair;
}
default: {
goto s_n_llhttp__internal__n_span_end_llhttp__on_chunk_extension_value_4;
}
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_invoke_llhttp__on_chunk_extension_value_complete_3:
s_n_llhttp__internal__n_invoke_llhttp__on_chunk_extension_value_complete_3: {
switch (llhttp__on_chunk_extension_value_complete(state, p, endp)) {
case 0:
goto s_n_llhttp__internal__n_chunk_extensions;
case 21:
goto s_n_llhttp__internal__n_pause_11;
default:
goto s_n_llhttp__internal__n_error_32;
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_error_33:
s_n_llhttp__internal__n_error_33: {
state->error = 0x2;
state->reason = "Invalid character in chunk extensions value";
state->error_pos = (const char*) p;
state->_current = (void*) (intptr_t) s_error;
return s_error;
UNREACHABLE;
}
case s_n_llhttp__internal__n_chunk_extension_value:
s_n_llhttp__internal__n_chunk_extension_value: {
static uint8_t lookup_table[] = {
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 2, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 3, 4, 3, 3, 3, 3, 3, 0, 0, 3, 3, 0, 3, 3, 0,
3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 0, 5, 0, 0, 0, 0,
0, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3,
3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 0, 0, 0, 3, 3,
3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3,
3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 0, 3, 0, 3, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
};
if (p == endp) {
return s_n_llhttp__internal__n_chunk_extension_value;
}
switch (lookup_table[(uint8_t) *p]) {
case 1: {
goto s_n_llhttp__internal__n_span_end_llhttp__on_chunk_extension_value;
}
case 2: {
goto s_n_llhttp__internal__n_span_end_llhttp__on_chunk_extension_value_1;
}
case 3: {
p++;
goto s_n_llhttp__internal__n_chunk_extension_value;
}
case 4: {
p++;
goto s_n_llhttp__internal__n_chunk_extension_quoted_value;
}
case 5: {
goto s_n_llhttp__internal__n_span_end_llhttp__on_chunk_extension_value_5;
}
default: {
goto s_n_llhttp__internal__n_span_end_llhttp__on_chunk_extension_value_6;
}
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_span_start_llhttp__on_chunk_extension_value:
s_n_llhttp__internal__n_span_start_llhttp__on_chunk_extension_value: {
if (p == endp) {
return s_n_llhttp__internal__n_span_start_llhttp__on_chunk_extension_value;
}
state->_span_pos0 = (void*) p;
state->_span_cb0 = llhttp__on_chunk_extension_value;
goto s_n_llhttp__internal__n_invoke_llhttp__on_chunk_extension_name_complete_3;
UNREACHABLE;
}
case s_n_llhttp__internal__n_error_34:
s_n_llhttp__internal__n_error_34: {
state->error = 0x2;
state->reason = "Invalid character in chunk extensions name";
state->error_pos = (const char*) p;
state->_current = (void*) (intptr_t) s_error;
return s_error;
UNREACHABLE;
}
case s_n_llhttp__internal__n_chunk_extension_name:
s_n_llhttp__internal__n_chunk_extension_name: {
static uint8_t lookup_table[] = {
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 2, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 3, 0, 3, 3, 3, 3, 3, 0, 0, 3, 3, 0, 3, 3, 0,
3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 0, 4, 0, 5, 0, 0,
0, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3,
3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 0, 0, 0, 3, 3,
3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3,
3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 0, 3, 0, 3, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
};
if (p == endp) {
return s_n_llhttp__internal__n_chunk_extension_name;
}
switch (lookup_table[(uint8_t) *p]) {
case 1: {
goto s_n_llhttp__internal__n_span_end_llhttp__on_chunk_extension_name;
}
case 2: {
goto s_n_llhttp__internal__n_span_end_llhttp__on_chunk_extension_name_1;
}
case 3: {
p++;
goto s_n_llhttp__internal__n_chunk_extension_name;
}
case 4: {
goto s_n_llhttp__internal__n_span_end_llhttp__on_chunk_extension_name_2;
}
case 5: {
goto s_n_llhttp__internal__n_span_end_llhttp__on_chunk_extension_name_3;
}
default: {
goto s_n_llhttp__internal__n_span_end_llhttp__on_chunk_extension_name_4;
}
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_span_start_llhttp__on_chunk_extension_name:
s_n_llhttp__internal__n_span_start_llhttp__on_chunk_extension_name: {
if (p == endp) {
return s_n_llhttp__internal__n_span_start_llhttp__on_chunk_extension_name;
}
state->_span_pos0 = (void*) p;
state->_span_cb0 = llhttp__on_chunk_extension_name;
goto s_n_llhttp__internal__n_chunk_extension_name;
UNREACHABLE;
}
case s_n_llhttp__internal__n_chunk_extensions:
s_n_llhttp__internal__n_chunk_extensions: {
if (p == endp) {
return s_n_llhttp__internal__n_chunk_extensions;
}
switch (*p) {
case 13: {
p++;
goto s_n_llhttp__internal__n_error_17;
}
case ' ': {
p++;
goto s_n_llhttp__internal__n_error_18;
}
default: {
goto s_n_llhttp__internal__n_span_start_llhttp__on_chunk_extension_name;
}
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_chunk_size_otherwise:
s_n_llhttp__internal__n_chunk_size_otherwise: {
if (p == endp) {
return s_n_llhttp__internal__n_chunk_size_otherwise;
}
switch (*p) {
case 9: {
p++;
goto s_n_llhttp__internal__n_invoke_test_lenient_flags_4;
}
case 10: {
p++;
goto s_n_llhttp__internal__n_invoke_test_lenient_flags_5;
}
case 13: {
p++;
goto s_n_llhttp__internal__n_chunk_size_almost_done;
}
case ' ': {
p++;
goto s_n_llhttp__internal__n_invoke_test_lenient_flags_4;
}
case ';': {
p++;
goto s_n_llhttp__internal__n_chunk_extensions;
}
default: {
goto s_n_llhttp__internal__n_error_35;
}
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_chunk_size:
s_n_llhttp__internal__n_chunk_size: {
if (p == endp) {
return s_n_llhttp__internal__n_chunk_size;
}
switch (*p) {
case '0': {
p++;
match = 0;
goto s_n_llhttp__internal__n_invoke_mul_add_content_length;
}
case '1': {
p++;
match = 1;
goto s_n_llhttp__internal__n_invoke_mul_add_content_length;
}
case '2': {
p++;
match = 2;
goto s_n_llhttp__internal__n_invoke_mul_add_content_length;
}
case '3': {
p++;
match = 3;
goto s_n_llhttp__internal__n_invoke_mul_add_content_length;
}
case '4': {
p++;
match = 4;
goto s_n_llhttp__internal__n_invoke_mul_add_content_length;
}
case '5': {
p++;
match = 5;
goto s_n_llhttp__internal__n_invoke_mul_add_content_length;
}
case '6': {
p++;
match = 6;
goto s_n_llhttp__internal__n_invoke_mul_add_content_length;
}
case '7': {
p++;
match = 7;
goto s_n_llhttp__internal__n_invoke_mul_add_content_length;
}
case '8': {
p++;
match = 8;
goto s_n_llhttp__internal__n_invoke_mul_add_content_length;
}
case '9': {
p++;
match = 9;
goto s_n_llhttp__internal__n_invoke_mul_add_content_length;
}
case 'A': {
p++;
match = 10;
goto s_n_llhttp__internal__n_invoke_mul_add_content_length;
}
case 'B': {
p++;
match = 11;
goto s_n_llhttp__internal__n_invoke_mul_add_content_length;
}
case 'C': {
p++;
match = 12;
goto s_n_llhttp__internal__n_invoke_mul_add_content_length;
}
case 'D': {
p++;
match = 13;
goto s_n_llhttp__internal__n_invoke_mul_add_content_length;
}
case 'E': {
p++;
match = 14;
goto s_n_llhttp__internal__n_invoke_mul_add_content_length;
}
case 'F': {
p++;
match = 15;
goto s_n_llhttp__internal__n_invoke_mul_add_content_length;
}
case 'a': {
p++;
match = 10;
goto s_n_llhttp__internal__n_invoke_mul_add_content_length;
}
case 'b': {
p++;
match = 11;
goto s_n_llhttp__internal__n_invoke_mul_add_content_length;
}
case 'c': {
p++;
match = 12;
goto s_n_llhttp__internal__n_invoke_mul_add_content_length;
}
case 'd': {
p++;
match = 13;
goto s_n_llhttp__internal__n_invoke_mul_add_content_length;
}
case 'e': {
p++;
match = 14;
goto s_n_llhttp__internal__n_invoke_mul_add_content_length;
}
case 'f': {
p++;
match = 15;
goto s_n_llhttp__internal__n_invoke_mul_add_content_length;
}
default: {
goto s_n_llhttp__internal__n_chunk_size_otherwise;
}
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_chunk_size_digit:
s_n_llhttp__internal__n_chunk_size_digit: {
if (p == endp) {
return s_n_llhttp__internal__n_chunk_size_digit;
}
switch (*p) {
case '0': {
p++;
match = 0;
goto s_n_llhttp__internal__n_invoke_mul_add_content_length;
}
case '1': {
p++;
match = 1;
goto s_n_llhttp__internal__n_invoke_mul_add_content_length;
}
case '2': {
p++;
match = 2;
goto s_n_llhttp__internal__n_invoke_mul_add_content_length;
}
case '3': {
p++;
match = 3;
goto s_n_llhttp__internal__n_invoke_mul_add_content_length;
}
case '4': {
p++;
match = 4;
goto s_n_llhttp__internal__n_invoke_mul_add_content_length;
}
case '5': {
p++;
match = 5;
goto s_n_llhttp__internal__n_invoke_mul_add_content_length;
}
case '6': {
p++;
match = 6;
goto s_n_llhttp__internal__n_invoke_mul_add_content_length;
}
case '7': {
p++;
match = 7;
goto s_n_llhttp__internal__n_invoke_mul_add_content_length;
}
case '8': {
p++;
match = 8;
goto s_n_llhttp__internal__n_invoke_mul_add_content_length;
}
case '9': {
p++;
match = 9;
goto s_n_llhttp__internal__n_invoke_mul_add_content_length;
}
case 'A': {
p++;
match = 10;
goto s_n_llhttp__internal__n_invoke_mul_add_content_length;
}
case 'B': {
p++;
match = 11;
goto s_n_llhttp__internal__n_invoke_mul_add_content_length;
}
case 'C': {
p++;
match = 12;
goto s_n_llhttp__internal__n_invoke_mul_add_content_length;
}
case 'D': {
p++;
match = 13;
goto s_n_llhttp__internal__n_invoke_mul_add_content_length;
}
case 'E': {
p++;
match = 14;
goto s_n_llhttp__internal__n_invoke_mul_add_content_length;
}
case 'F': {
p++;
match = 15;
goto s_n_llhttp__internal__n_invoke_mul_add_content_length;
}
case 'a': {
p++;
match = 10;
goto s_n_llhttp__internal__n_invoke_mul_add_content_length;
}
case 'b': {
p++;
match = 11;
goto s_n_llhttp__internal__n_invoke_mul_add_content_length;
}
case 'c': {
p++;
match = 12;
goto s_n_llhttp__internal__n_invoke_mul_add_content_length;
}
case 'd': {
p++;
match = 13;
goto s_n_llhttp__internal__n_invoke_mul_add_content_length;
}
case 'e': {
p++;
match = 14;
goto s_n_llhttp__internal__n_invoke_mul_add_content_length;
}
case 'f': {
p++;
match = 15;
goto s_n_llhttp__internal__n_invoke_mul_add_content_length;
}
default: {
goto s_n_llhttp__internal__n_error_37;
}
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_invoke_update_content_length_1:
s_n_llhttp__internal__n_invoke_update_content_length_1: {
switch (llhttp__internal__c_update_content_length(state, p, endp)) {
default:
goto s_n_llhttp__internal__n_chunk_size_digit;
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_consume_content_length_1:
s_n_llhttp__internal__n_consume_content_length_1: {
size_t avail;
uint64_t need;
avail = endp - p;
need = state->content_length;
if (avail >= need) {
p += need;
state->content_length = 0;
goto s_n_llhttp__internal__n_span_end_llhttp__on_body_1;
}
state->content_length -= avail;
return s_n_llhttp__internal__n_consume_content_length_1;
UNREACHABLE;
}
case s_n_llhttp__internal__n_span_start_llhttp__on_body_1:
s_n_llhttp__internal__n_span_start_llhttp__on_body_1: {
if (p == endp) {
return s_n_llhttp__internal__n_span_start_llhttp__on_body_1;
}
state->_span_pos0 = (void*) p;
state->_span_cb0 = llhttp__on_body;
goto s_n_llhttp__internal__n_consume_content_length_1;
UNREACHABLE;
}
case s_n_llhttp__internal__n_eof:
s_n_llhttp__internal__n_eof: {
if (p == endp) {
return s_n_llhttp__internal__n_eof;
}
p++;
goto s_n_llhttp__internal__n_eof;
UNREACHABLE;
}
case s_n_llhttp__internal__n_span_start_llhttp__on_body_2:
s_n_llhttp__internal__n_span_start_llhttp__on_body_2: {
if (p == endp) {
return s_n_llhttp__internal__n_span_start_llhttp__on_body_2;
}
state->_span_pos0 = (void*) p;
state->_span_cb0 = llhttp__on_body;
goto s_n_llhttp__internal__n_eof;
UNREACHABLE;
}
case s_n_llhttp__internal__n_invoke_llhttp__after_headers_complete:
s_n_llhttp__internal__n_invoke_llhttp__after_headers_complete: {
switch (llhttp__after_headers_complete(state, p, endp)) {
case 1:
goto s_n_llhttp__internal__n_invoke_llhttp__on_message_complete_1;
case 2:
goto s_n_llhttp__internal__n_invoke_update_content_length_1;
case 3:
goto s_n_llhttp__internal__n_span_start_llhttp__on_body_1;
case 4:
goto s_n_llhttp__internal__n_invoke_update_finish_3;
case 5:
goto s_n_llhttp__internal__n_error_39;
default:
goto s_n_llhttp__internal__n_invoke_llhttp__on_message_complete;
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_error_5:
s_n_llhttp__internal__n_error_5: {
state->error = 0xa;
state->reason = "Invalid header field char";
state->error_pos = (const char*) p;
state->_current = (void*) (intptr_t) s_error;
return s_error;
UNREACHABLE;
}
case s_n_llhttp__internal__n_headers_almost_done:
s_n_llhttp__internal__n_headers_almost_done: {
if (p == endp) {
return s_n_llhttp__internal__n_headers_almost_done;
}
switch (*p) {
case 10: {
p++;
goto s_n_llhttp__internal__n_invoke_test_flags_1;
}
default: {
goto s_n_llhttp__internal__n_invoke_test_lenient_flags_12;
}
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_header_field_colon_discard_ws:
s_n_llhttp__internal__n_header_field_colon_discard_ws: {
if (p == endp) {
return s_n_llhttp__internal__n_header_field_colon_discard_ws;
}
switch (*p) {
case ' ': {
p++;
goto s_n_llhttp__internal__n_header_field_colon_discard_ws;
}
default: {
goto s_n_llhttp__internal__n_header_field_colon;
}
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_invoke_llhttp__on_header_value_complete:
s_n_llhttp__internal__n_invoke_llhttp__on_header_value_complete: {
switch (llhttp__on_header_value_complete(state, p, endp)) {
case 0:
goto s_n_llhttp__internal__n_header_field_start;
case 21:
goto s_n_llhttp__internal__n_pause_18;
default:
goto s_n_llhttp__internal__n_error_48;
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_span_start_llhttp__on_header_value:
s_n_llhttp__internal__n_span_start_llhttp__on_header_value: {
if (p == endp) {
return s_n_llhttp__internal__n_span_start_llhttp__on_header_value;
}
state->_span_pos0 = (void*) p;
state->_span_cb0 = llhttp__on_header_value;
goto s_n_llhttp__internal__n_span_end_llhttp__on_header_value;
UNREACHABLE;
}
case s_n_llhttp__internal__n_header_value_discard_lws:
s_n_llhttp__internal__n_header_value_discard_lws: {
if (p == endp) {
return s_n_llhttp__internal__n_header_value_discard_lws;
}
switch (*p) {
case 9: {
p++;
goto s_n_llhttp__internal__n_invoke_test_lenient_flags_15;
}
case ' ': {
p++;
goto s_n_llhttp__internal__n_invoke_test_lenient_flags_15;
}
default: {
goto s_n_llhttp__internal__n_invoke_load_header_state_1;
}
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_header_value_discard_ws_almost_done:
s_n_llhttp__internal__n_header_value_discard_ws_almost_done: {
if (p == endp) {
return s_n_llhttp__internal__n_header_value_discard_ws_almost_done;
}
switch (*p) {
case 10: {
p++;
goto s_n_llhttp__internal__n_header_value_discard_lws;
}
default: {
goto s_n_llhttp__internal__n_invoke_test_lenient_flags_16;
}
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_header_value_lws:
s_n_llhttp__internal__n_header_value_lws: {
if (p == endp) {
return s_n_llhttp__internal__n_header_value_lws;
}
switch (*p) {
case 9: {
goto s_n_llhttp__internal__n_invoke_test_lenient_flags_18;
}
case ' ': {
goto s_n_llhttp__internal__n_invoke_test_lenient_flags_18;
}
default: {
goto s_n_llhttp__internal__n_invoke_load_header_state_5;
}
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_header_value_almost_done:
s_n_llhttp__internal__n_header_value_almost_done: {
if (p == endp) {
return s_n_llhttp__internal__n_header_value_almost_done;
}
switch (*p) {
case 10: {
p++;
goto s_n_llhttp__internal__n_header_value_lws;
}
default: {
goto s_n_llhttp__internal__n_error_53;
}
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_invoke_test_lenient_flags_17:
s_n_llhttp__internal__n_invoke_test_lenient_flags_17: {
switch (llhttp__internal__c_test_lenient_flags_1(state, p, endp)) {
case 1:
goto s_n_llhttp__internal__n_header_value_almost_done;
default:
goto s_n_llhttp__internal__n_error_51;
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_header_value_lenient:
s_n_llhttp__internal__n_header_value_lenient: {
if (p == endp) {
return s_n_llhttp__internal__n_header_value_lenient;
}
switch (*p) {
case 10: {
goto s_n_llhttp__internal__n_span_end_llhttp__on_header_value_4;
}
case 13: {
goto s_n_llhttp__internal__n_span_end_llhttp__on_header_value_5;
}
default: {
p++;
goto s_n_llhttp__internal__n_header_value_lenient;
}
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_error_54:
s_n_llhttp__internal__n_error_54: {
state->error = 0xa;
state->reason = "Invalid header value char";
state->error_pos = (const char*) p;
state->_current = (void*) (intptr_t) s_error;
return s_error;
UNREACHABLE;
}
case s_n_llhttp__internal__n_header_value_otherwise:
s_n_llhttp__internal__n_header_value_otherwise: {
if (p == endp) {
return s_n_llhttp__internal__n_header_value_otherwise;
}
switch (*p) {
case 10: {
goto s_n_llhttp__internal__n_span_end_llhttp__on_header_value_1;
}
case 13: {
goto s_n_llhttp__internal__n_span_end_llhttp__on_header_value_2;
}
default: {
goto s_n_llhttp__internal__n_invoke_test_lenient_flags_19;
}
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_header_value_connection_token:
s_n_llhttp__internal__n_header_value_connection_token: {
static uint8_t lookup_table[] = {
0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1
};
if (p == endp) {
return s_n_llhttp__internal__n_header_value_connection_token;
}
switch (lookup_table[(uint8_t) *p]) {
case 1: {
p++;
goto s_n_llhttp__internal__n_header_value_connection_token;
}
case 2: {
p++;
goto s_n_llhttp__internal__n_header_value_connection;
}
default: {
goto s_n_llhttp__internal__n_header_value_otherwise;
}
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_header_value_connection_ws:
s_n_llhttp__internal__n_header_value_connection_ws: {
if (p == endp) {
return s_n_llhttp__internal__n_header_value_connection_ws;
}
switch (*p) {
case 10: {
goto s_n_llhttp__internal__n_header_value_otherwise;
}
case 13: {
goto s_n_llhttp__internal__n_header_value_otherwise;
}
case ' ': {
p++;
goto s_n_llhttp__internal__n_header_value_connection_ws;
}
case ',': {
p++;
goto s_n_llhttp__internal__n_invoke_load_header_state_6;
}
default: {
goto s_n_llhttp__internal__n_invoke_update_header_state_5;
}
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_header_value_connection_1:
s_n_llhttp__internal__n_header_value_connection_1: {
llparse_match_t match_seq;
if (p == endp) {
return s_n_llhttp__internal__n_header_value_connection_1;
}
match_seq = llparse__match_sequence_to_lower(state, p, endp, llparse_blob2, 4);
p = match_seq.current;
switch (match_seq.status) {
case kMatchComplete: {
p++;
goto s_n_llhttp__internal__n_invoke_update_header_state_3;
}
case kMatchPause: {
return s_n_llhttp__internal__n_header_value_connection_1;
}
case kMatchMismatch: {
goto s_n_llhttp__internal__n_header_value_connection_token;
}
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_header_value_connection_2:
s_n_llhttp__internal__n_header_value_connection_2: {
llparse_match_t match_seq;
if (p == endp) {
return s_n_llhttp__internal__n_header_value_connection_2;
}
match_seq = llparse__match_sequence_to_lower(state, p, endp, llparse_blob3, 9);
p = match_seq.current;
switch (match_seq.status) {
case kMatchComplete: {
p++;
goto s_n_llhttp__internal__n_invoke_update_header_state_6;
}
case kMatchPause: {
return s_n_llhttp__internal__n_header_value_connection_2;
}
case kMatchMismatch: {
goto s_n_llhttp__internal__n_header_value_connection_token;
}
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_header_value_connection_3:
s_n_llhttp__internal__n_header_value_connection_3: {
llparse_match_t match_seq;
if (p == endp) {
return s_n_llhttp__internal__n_header_value_connection_3;
}
match_seq = llparse__match_sequence_to_lower(state, p, endp, llparse_blob4, 6);
p = match_seq.current;
switch (match_seq.status) {
case kMatchComplete: {
p++;
goto s_n_llhttp__internal__n_invoke_update_header_state_7;
}
case kMatchPause: {
return s_n_llhttp__internal__n_header_value_connection_3;
}
case kMatchMismatch: {
goto s_n_llhttp__internal__n_header_value_connection_token;
}
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_header_value_connection:
s_n_llhttp__internal__n_header_value_connection: {
if (p == endp) {
return s_n_llhttp__internal__n_header_value_connection;
}
switch (((*p) >= 'A' && (*p) <= 'Z' ? (*p | 0x20) : (*p))) {
case 9: {
p++;
goto s_n_llhttp__internal__n_header_value_connection;
}
case ' ': {
p++;
goto s_n_llhttp__internal__n_header_value_connection;
}
case 'c': {
p++;
goto s_n_llhttp__internal__n_header_value_connection_1;
}
case 'k': {
p++;
goto s_n_llhttp__internal__n_header_value_connection_2;
}
case 'u': {
p++;
goto s_n_llhttp__internal__n_header_value_connection_3;
}
default: {
goto s_n_llhttp__internal__n_header_value_connection_token;
}
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_error_56:
s_n_llhttp__internal__n_error_56: {
state->error = 0xb;
state->reason = "Content-Length overflow";
state->error_pos = (const char*) p;
state->_current = (void*) (intptr_t) s_error;
return s_error;
UNREACHABLE;
}
case s_n_llhttp__internal__n_error_57:
s_n_llhttp__internal__n_error_57: {
state->error = 0xb;
state->reason = "Invalid character in Content-Length";
state->error_pos = (const char*) p;
state->_current = (void*) (intptr_t) s_error;
return s_error;
UNREACHABLE;
}
case s_n_llhttp__internal__n_header_value_content_length_ws:
s_n_llhttp__internal__n_header_value_content_length_ws: {
if (p == endp) {
return s_n_llhttp__internal__n_header_value_content_length_ws;
}
switch (*p) {
case 10: {
goto s_n_llhttp__internal__n_invoke_or_flags_17;
}
case 13: {
goto s_n_llhttp__internal__n_invoke_or_flags_17;
}
case ' ': {
p++;
goto s_n_llhttp__internal__n_header_value_content_length_ws;
}
default: {
goto s_n_llhttp__internal__n_span_end_llhttp__on_header_value_7;
}
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_header_value_content_length:
s_n_llhttp__internal__n_header_value_content_length: {
if (p == endp) {
return s_n_llhttp__internal__n_header_value_content_length;
}
switch (*p) {
case '0': {
p++;
match = 0;
goto s_n_llhttp__internal__n_invoke_mul_add_content_length_1;
}
case '1': {
p++;
match = 1;
goto s_n_llhttp__internal__n_invoke_mul_add_content_length_1;
}
case '2': {
p++;
match = 2;
goto s_n_llhttp__internal__n_invoke_mul_add_content_length_1;
}
case '3': {
p++;
match = 3;
goto s_n_llhttp__internal__n_invoke_mul_add_content_length_1;
}
case '4': {
p++;
match = 4;
goto s_n_llhttp__internal__n_invoke_mul_add_content_length_1;
}
case '5': {
p++;
match = 5;
goto s_n_llhttp__internal__n_invoke_mul_add_content_length_1;
}
case '6': {
p++;
match = 6;
goto s_n_llhttp__internal__n_invoke_mul_add_content_length_1;
}
case '7': {
p++;
match = 7;
goto s_n_llhttp__internal__n_invoke_mul_add_content_length_1;
}
case '8': {
p++;
match = 8;
goto s_n_llhttp__internal__n_invoke_mul_add_content_length_1;
}
case '9': {
p++;
match = 9;
goto s_n_llhttp__internal__n_invoke_mul_add_content_length_1;
}
default: {
goto s_n_llhttp__internal__n_header_value_content_length_ws;
}
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_error_59:
s_n_llhttp__internal__n_error_59: {
state->error = 0xf;
state->reason = "Invalid `Transfer-Encoding` header value";
state->error_pos = (const char*) p;
state->_current = (void*) (intptr_t) s_error;
return s_error;
UNREACHABLE;
}
case s_n_llhttp__internal__n_error_58:
s_n_llhttp__internal__n_error_58: {
state->error = 0xf;
state->reason = "Invalid `Transfer-Encoding` header value";
state->error_pos = (const char*) p;
state->_current = (void*) (intptr_t) s_error;
return s_error;
UNREACHABLE;
}
case s_n_llhttp__internal__n_header_value_te_token_ows:
s_n_llhttp__internal__n_header_value_te_token_ows: {
if (p == endp) {
return s_n_llhttp__internal__n_header_value_te_token_ows;
}
switch (*p) {
case 9: {
p++;
goto s_n_llhttp__internal__n_header_value_te_token_ows;
}
case ' ': {
p++;
goto s_n_llhttp__internal__n_header_value_te_token_ows;
}
default: {
goto s_n_llhttp__internal__n_header_value_te_chunked;
}
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_header_value:
s_n_llhttp__internal__n_header_value: {
static uint8_t lookup_table[] = {
0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1
};
if (p == endp) {
return s_n_llhttp__internal__n_header_value;
}
#ifdef __SSE4_2__
if (endp - p >= 16) {
__m128i ranges;
__m128i input;
int match_len;
/* Load input */
input = _mm_loadu_si128((__m128i const*) p);
ranges = _mm_loadu_si128((__m128i const*) llparse_blob6);
/* Find first character that does not match `ranges` */
match_len = _mm_cmpestri(ranges, 6,
input, 16,
_SIDD_UBYTE_OPS | _SIDD_CMP_RANGES |
_SIDD_NEGATIVE_POLARITY);
if (match_len != 0) {
p += match_len;
goto s_n_llhttp__internal__n_header_value;
}
goto s_n_llhttp__internal__n_header_value_otherwise;
}
#endif /* __SSE4_2__ */
#ifdef __ARM_NEON__
while (endp - p >= 16) {
uint8x16_t input;
uint8x16_t single;
uint8x16_t mask;
uint8x8_t narrow;
uint64_t match_mask;
int match_len;
/* Load input */
input = vld1q_u8(p);
/* Find first character that does not match `ranges` */
single = vceqq_u8(input, vdupq_n_u8(0x9));
mask = single;
single = vandq_u16(
vcgeq_u8(input, vdupq_n_u8(' ')),
vcleq_u8(input, vdupq_n_u8('~'))
);
mask = vorrq_u16(mask, single);
single = vandq_u16(
vcgeq_u8(input, vdupq_n_u8(0x80)),
vcleq_u8(input, vdupq_n_u8(0xff))
);
mask = vorrq_u16(mask, single);
narrow = vshrn_n_u16(mask, 4);
match_mask = ~vget_lane_u64(vreinterpret_u64_u8(narrow), 0);
match_len = __builtin_ctzll(match_mask) >> 2;
if (match_len != 16) {
p += match_len;
goto s_n_llhttp__internal__n_header_value_otherwise;
}
p += 16;
}
if (p == endp) {
return s_n_llhttp__internal__n_header_value;
}
#endif /* __ARM_NEON__ */
#ifdef __wasm_simd128__
while (endp - p >= 16) {
v128_t input;
v128_t mask;
v128_t single;
int match_len;
/* Load input */
input = wasm_v128_load(p);
/* Find first character that does not match `ranges` */
single = wasm_i8x16_eq(input, wasm_u8x16_const_splat(0x9));
mask = single;
single = wasm_v128_and(
wasm_i8x16_ge(input, wasm_u8x16_const_splat(' ')),
wasm_i8x16_le(input, wasm_u8x16_const_splat('~'))
);
mask = wasm_v128_or(mask, single);
single = wasm_v128_and(
wasm_i8x16_ge(input, wasm_u8x16_const_splat(0x80)),
wasm_i8x16_le(input, wasm_u8x16_const_splat(0xff))
);
mask = wasm_v128_or(mask, single);
match_len = __builtin_ctz(
~wasm_i8x16_bitmask(mask)
);
if (match_len != 16) {
p += match_len;
goto s_n_llhttp__internal__n_header_value_otherwise;
}
p += 16;
}
if (p == endp) {
return s_n_llhttp__internal__n_header_value;
}
#endif /* __wasm_simd128__ */
switch (lookup_table[(uint8_t) *p]) {
case 1: {
p++;
goto s_n_llhttp__internal__n_header_value;
}
default: {
goto s_n_llhttp__internal__n_header_value_otherwise;
}
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_header_value_te_token:
s_n_llhttp__internal__n_header_value_te_token: {
static uint8_t lookup_table[] = {
0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1
};
if (p == endp) {
return s_n_llhttp__internal__n_header_value_te_token;
}
switch (lookup_table[(uint8_t) *p]) {
case 1: {
p++;
goto s_n_llhttp__internal__n_header_value_te_token;
}
case 2: {
p++;
goto s_n_llhttp__internal__n_header_value_te_token_ows;
}
default: {
goto s_n_llhttp__internal__n_invoke_update_header_state_9;
}
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_header_value_te_chunked_last:
s_n_llhttp__internal__n_header_value_te_chunked_last: {
if (p == endp) {
return s_n_llhttp__internal__n_header_value_te_chunked_last;
}
switch (*p) {
case 10: {
goto s_n_llhttp__internal__n_invoke_update_header_state_8;
}
case 13: {
goto s_n_llhttp__internal__n_invoke_update_header_state_8;
}
case ' ': {
p++;
goto s_n_llhttp__internal__n_header_value_te_chunked_last;
}
case ',': {
goto s_n_llhttp__internal__n_invoke_load_type_1;
}
default: {
goto s_n_llhttp__internal__n_header_value_te_token;
}
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_header_value_te_chunked:
s_n_llhttp__internal__n_header_value_te_chunked: {
llparse_match_t match_seq;
if (p == endp) {
return s_n_llhttp__internal__n_header_value_te_chunked;
}
match_seq = llparse__match_sequence_to_lower_unsafe(state, p, endp, llparse_blob5, 7);
p = match_seq.current;
switch (match_seq.status) {
case kMatchComplete: {
p++;
goto s_n_llhttp__internal__n_header_value_te_chunked_last;
}
case kMatchPause: {
return s_n_llhttp__internal__n_header_value_te_chunked;
}
case kMatchMismatch: {
goto s_n_llhttp__internal__n_header_value_te_token;
}
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_span_start_llhttp__on_header_value_1:
s_n_llhttp__internal__n_span_start_llhttp__on_header_value_1: {
if (p == endp) {
return s_n_llhttp__internal__n_span_start_llhttp__on_header_value_1;
}
state->_span_pos0 = (void*) p;
state->_span_cb0 = llhttp__on_header_value;
goto s_n_llhttp__internal__n_invoke_load_header_state_3;
UNREACHABLE;
}
case s_n_llhttp__internal__n_header_value_discard_ws:
s_n_llhttp__internal__n_header_value_discard_ws: {
if (p == endp) {
return s_n_llhttp__internal__n_header_value_discard_ws;
}
switch (*p) {
case 9: {
p++;
goto s_n_llhttp__internal__n_header_value_discard_ws;
}
case 10: {
p++;
goto s_n_llhttp__internal__n_invoke_test_lenient_flags_14;
}
case 13: {
p++;
goto s_n_llhttp__internal__n_header_value_discard_ws_almost_done;
}
case ' ': {
p++;
goto s_n_llhttp__internal__n_header_value_discard_ws;
}
default: {
goto s_n_llhttp__internal__n_span_start_llhttp__on_header_value_1;
}
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_invoke_load_header_state:
s_n_llhttp__internal__n_invoke_load_header_state: {
switch (llhttp__internal__c_load_header_state(state, p, endp)) {
case 2:
goto s_n_llhttp__internal__n_invoke_test_flags_4;
case 3:
goto s_n_llhttp__internal__n_invoke_test_flags_5;
default:
goto s_n_llhttp__internal__n_header_value_discard_ws;
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_invoke_llhttp__on_header_field_complete:
s_n_llhttp__internal__n_invoke_llhttp__on_header_field_complete: {
switch (llhttp__on_header_field_complete(state, p, endp)) {
case 0:
goto s_n_llhttp__internal__n_invoke_load_header_state;
case 21:
goto s_n_llhttp__internal__n_pause_19;
default:
goto s_n_llhttp__internal__n_error_45;
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_header_field_general_otherwise:
s_n_llhttp__internal__n_header_field_general_otherwise: {
if (p == endp) {
return s_n_llhttp__internal__n_header_field_general_otherwise;
}
switch (*p) {
case ':': {
goto s_n_llhttp__internal__n_span_end_llhttp__on_header_field_2;
}
default: {
goto s_n_llhttp__internal__n_error_62;
}
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_header_field_general:
s_n_llhttp__internal__n_header_field_general: {
static uint8_t lookup_table[] = {
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 1, 0, 1, 1, 1, 1, 1, 0, 0, 1, 1, 0, 1, 1, 0,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0,
0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
};
if (p == endp) {
return s_n_llhttp__internal__n_header_field_general;
}
#ifdef __SSE4_2__
if (endp - p >= 16) {
__m128i ranges;
__m128i input;
int match_len;
/* Load input */
input = _mm_loadu_si128((__m128i const*) p);
ranges = _mm_loadu_si128((__m128i const*) llparse_blob7);
/* Find first character that does not match `ranges` */
match_len = _mm_cmpestri(ranges, 16,
input, 16,
_SIDD_UBYTE_OPS | _SIDD_CMP_RANGES |
_SIDD_NEGATIVE_POLARITY);
if (match_len != 0) {
p += match_len;
goto s_n_llhttp__internal__n_header_field_general;
}
ranges = _mm_loadu_si128((__m128i const*) llparse_blob8);
/* Find first character that does not match `ranges` */
match_len = _mm_cmpestri(ranges, 2,
input, 16,
_SIDD_UBYTE_OPS | _SIDD_CMP_RANGES |
_SIDD_NEGATIVE_POLARITY);
if (match_len != 0) {
p += match_len;
goto s_n_llhttp__internal__n_header_field_general;
}
goto s_n_llhttp__internal__n_header_field_general_otherwise;
}
#endif /* __SSE4_2__ */
switch (lookup_table[(uint8_t) *p]) {
case 1: {
p++;
goto s_n_llhttp__internal__n_header_field_general;
}
default: {
goto s_n_llhttp__internal__n_header_field_general_otherwise;
}
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_header_field_colon:
s_n_llhttp__internal__n_header_field_colon: {
if (p == endp) {
return s_n_llhttp__internal__n_header_field_colon;
}
switch (*p) {
case ' ': {
goto s_n_llhttp__internal__n_invoke_test_lenient_flags_13;
}
case ':': {
goto s_n_llhttp__internal__n_span_end_llhttp__on_header_field_1;
}
default: {
goto s_n_llhttp__internal__n_invoke_update_header_state_10;
}
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_header_field_3:
s_n_llhttp__internal__n_header_field_3: {
llparse_match_t match_seq;
if (p == endp) {
return s_n_llhttp__internal__n_header_field_3;
}
match_seq = llparse__match_sequence_to_lower(state, p, endp, llparse_blob1, 6);
p = match_seq.current;
switch (match_seq.status) {
case kMatchComplete: {
p++;
match = 1;
goto s_n_llhttp__internal__n_invoke_store_header_state;
}
case kMatchPause: {
return s_n_llhttp__internal__n_header_field_3;
}
case kMatchMismatch: {
goto s_n_llhttp__internal__n_invoke_update_header_state_11;
}
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_header_field_4:
s_n_llhttp__internal__n_header_field_4: {
llparse_match_t match_seq;
if (p == endp) {
return s_n_llhttp__internal__n_header_field_4;
}
match_seq = llparse__match_sequence_to_lower(state, p, endp, llparse_blob9, 10);
p = match_seq.current;
switch (match_seq.status) {
case kMatchComplete: {
p++;
match = 2;
goto s_n_llhttp__internal__n_invoke_store_header_state;
}
case kMatchPause: {
return s_n_llhttp__internal__n_header_field_4;
}
case kMatchMismatch: {
goto s_n_llhttp__internal__n_invoke_update_header_state_11;
}
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_header_field_2:
s_n_llhttp__internal__n_header_field_2: {
if (p == endp) {
return s_n_llhttp__internal__n_header_field_2;
}
switch (((*p) >= 'A' && (*p) <= 'Z' ? (*p | 0x20) : (*p))) {
case 'n': {
p++;
goto s_n_llhttp__internal__n_header_field_3;
}
case 't': {
p++;
goto s_n_llhttp__internal__n_header_field_4;
}
default: {
goto s_n_llhttp__internal__n_invoke_update_header_state_11;
}
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_header_field_1:
s_n_llhttp__internal__n_header_field_1: {
llparse_match_t match_seq;
if (p == endp) {
return s_n_llhttp__internal__n_header_field_1;
}
match_seq = llparse__match_sequence_to_lower(state, p, endp, llparse_blob0, 2);
p = match_seq.current;
switch (match_seq.status) {
case kMatchComplete: {
p++;
goto s_n_llhttp__internal__n_header_field_2;
}
case kMatchPause: {
return s_n_llhttp__internal__n_header_field_1;
}
case kMatchMismatch: {
goto s_n_llhttp__internal__n_invoke_update_header_state_11;
}
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_header_field_5:
s_n_llhttp__internal__n_header_field_5: {
llparse_match_t match_seq;
if (p == endp) {
return s_n_llhttp__internal__n_header_field_5;
}
match_seq = llparse__match_sequence_to_lower(state, p, endp, llparse_blob10, 15);
p = match_seq.current;
switch (match_seq.status) {
case kMatchComplete: {
p++;
match = 1;
goto s_n_llhttp__internal__n_invoke_store_header_state;
}
case kMatchPause: {
return s_n_llhttp__internal__n_header_field_5;
}
case kMatchMismatch: {
goto s_n_llhttp__internal__n_invoke_update_header_state_11;
}
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_header_field_6:
s_n_llhttp__internal__n_header_field_6: {
llparse_match_t match_seq;
if (p == endp) {
return s_n_llhttp__internal__n_header_field_6;
}
match_seq = llparse__match_sequence_to_lower(state, p, endp, llparse_blob11, 16);
p = match_seq.current;
switch (match_seq.status) {
case kMatchComplete: {
p++;
match = 3;
goto s_n_llhttp__internal__n_invoke_store_header_state;
}
case kMatchPause: {
return s_n_llhttp__internal__n_header_field_6;
}
case kMatchMismatch: {
goto s_n_llhttp__internal__n_invoke_update_header_state_11;
}
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_header_field_7:
s_n_llhttp__internal__n_header_field_7: {
llparse_match_t match_seq;
if (p == endp) {
return s_n_llhttp__internal__n_header_field_7;
}
match_seq = llparse__match_sequence_to_lower(state, p, endp, llparse_blob12, 6);
p = match_seq.current;
switch (match_seq.status) {
case kMatchComplete: {
p++;
match = 4;
goto s_n_llhttp__internal__n_invoke_store_header_state;
}
case kMatchPause: {
return s_n_llhttp__internal__n_header_field_7;
}
case kMatchMismatch: {
goto s_n_llhttp__internal__n_invoke_update_header_state_11;
}
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_header_field:
s_n_llhttp__internal__n_header_field: {
if (p == endp) {
return s_n_llhttp__internal__n_header_field;
}
switch (((*p) >= 'A' && (*p) <= 'Z' ? (*p | 0x20) : (*p))) {
case 'c': {
p++;
goto s_n_llhttp__internal__n_header_field_1;
}
case 'p': {
p++;
goto s_n_llhttp__internal__n_header_field_5;
}
case 't': {
p++;
goto s_n_llhttp__internal__n_header_field_6;
}
case 'u': {
p++;
goto s_n_llhttp__internal__n_header_field_7;
}
default: {
goto s_n_llhttp__internal__n_invoke_update_header_state_11;
}
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_span_start_llhttp__on_header_field:
s_n_llhttp__internal__n_span_start_llhttp__on_header_field: {
if (p == endp) {
return s_n_llhttp__internal__n_span_start_llhttp__on_header_field;
}
state->_span_pos0 = (void*) p;
state->_span_cb0 = llhttp__on_header_field;
goto s_n_llhttp__internal__n_header_field;
UNREACHABLE;
}
case s_n_llhttp__internal__n_header_field_start:
s_n_llhttp__internal__n_header_field_start: {
if (p == endp) {
return s_n_llhttp__internal__n_header_field_start;
}
switch (*p) {
case 10: {
p++;
goto s_n_llhttp__internal__n_invoke_test_lenient_flags_1;
}
case 13: {
p++;
goto s_n_llhttp__internal__n_headers_almost_done;
}
case ':': {
goto s_n_llhttp__internal__n_error_44;
}
default: {
goto s_n_llhttp__internal__n_span_start_llhttp__on_header_field;
}
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_headers_start:
s_n_llhttp__internal__n_headers_start: {
if (p == endp) {
return s_n_llhttp__internal__n_headers_start;
}
switch (*p) {
case ' ': {
p++;
goto s_n_llhttp__internal__n_invoke_test_lenient_flags;
}
default: {
goto s_n_llhttp__internal__n_header_field_start;
}
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_url_to_http_09:
s_n_llhttp__internal__n_url_to_http_09: {
if (p == endp) {
return s_n_llhttp__internal__n_url_to_http_09;
}
switch (*p) {
case 9: {
p++;
goto s_n_llhttp__internal__n_error_2;
}
case 12: {
p++;
goto s_n_llhttp__internal__n_error_2;
}
default: {
goto s_n_llhttp__internal__n_invoke_update_http_major;
}
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_url_skip_to_http09:
s_n_llhttp__internal__n_url_skip_to_http09: {
if (p == endp) {
return s_n_llhttp__internal__n_url_skip_to_http09;
}
switch (*p) {
case 9: {
p++;
goto s_n_llhttp__internal__n_error_2;
}
case 12: {
p++;
goto s_n_llhttp__internal__n_error_2;
}
default: {
p++;
goto s_n_llhttp__internal__n_url_to_http_09;
}
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_url_skip_lf_to_http09_1:
s_n_llhttp__internal__n_url_skip_lf_to_http09_1: {
if (p == endp) {
return s_n_llhttp__internal__n_url_skip_lf_to_http09_1;
}
switch (*p) {
case 10: {
p++;
goto s_n_llhttp__internal__n_url_to_http_09;
}
default: {
goto s_n_llhttp__internal__n_error_63;
}
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_url_skip_lf_to_http09:
s_n_llhttp__internal__n_url_skip_lf_to_http09: {
if (p == endp) {
return s_n_llhttp__internal__n_url_skip_lf_to_http09;
}
switch (*p) {
case 9: {
p++;
goto s_n_llhttp__internal__n_error_2;
}
case 12: {
p++;
goto s_n_llhttp__internal__n_error_2;
}
case 13: {
p++;
goto s_n_llhttp__internal__n_url_skip_lf_to_http09_1;
}
default: {
goto s_n_llhttp__internal__n_error_63;
}
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_req_pri_upgrade:
s_n_llhttp__internal__n_req_pri_upgrade: {
llparse_match_t match_seq;
if (p == endp) {
return s_n_llhttp__internal__n_req_pri_upgrade;
}
match_seq = llparse__match_sequence_id(state, p, endp, llparse_blob14, 10);
p = match_seq.current;
switch (match_seq.status) {
case kMatchComplete: {
p++;
goto s_n_llhttp__internal__n_error_72;
}
case kMatchPause: {
return s_n_llhttp__internal__n_req_pri_upgrade;
}
case kMatchMismatch: {
goto s_n_llhttp__internal__n_error_73;
}
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_req_http_complete_crlf:
s_n_llhttp__internal__n_req_http_complete_crlf: {
if (p == endp) {
return s_n_llhttp__internal__n_req_http_complete_crlf;
}
switch (*p) {
case 10: {
p++;
goto s_n_llhttp__internal__n_headers_start;
}
default: {
goto s_n_llhttp__internal__n_invoke_test_lenient_flags_26;
}
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_req_http_complete:
s_n_llhttp__internal__n_req_http_complete: {
if (p == endp) {
return s_n_llhttp__internal__n_req_http_complete;
}
switch (*p) {
case 10: {
p++;
goto s_n_llhttp__internal__n_invoke_test_lenient_flags_25;
}
case 13: {
p++;
goto s_n_llhttp__internal__n_req_http_complete_crlf;
}
default: {
goto s_n_llhttp__internal__n_error_71;
}
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_invoke_load_method_1:
s_n_llhttp__internal__n_invoke_load_method_1: {
switch (llhttp__internal__c_load_method(state, p, endp)) {
case 34:
goto s_n_llhttp__internal__n_req_pri_upgrade;
default:
goto s_n_llhttp__internal__n_req_http_complete;
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_invoke_llhttp__on_version_complete:
s_n_llhttp__internal__n_invoke_llhttp__on_version_complete: {
switch (llhttp__on_version_complete(state, p, endp)) {
case 0:
goto s_n_llhttp__internal__n_invoke_load_method_1;
case 21:
goto s_n_llhttp__internal__n_pause_21;
default:
goto s_n_llhttp__internal__n_error_68;
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_error_67:
s_n_llhttp__internal__n_error_67: {
state->error = 0x9;
state->reason = "Invalid HTTP version";
state->error_pos = (const char*) p;
state->_current = (void*) (intptr_t) s_error;
return s_error;
UNREACHABLE;
}
case s_n_llhttp__internal__n_error_74:
s_n_llhttp__internal__n_error_74: {
state->error = 0x9;
state->reason = "Invalid minor version";
state->error_pos = (const char*) p;
state->_current = (void*) (intptr_t) s_error;
return s_error;
UNREACHABLE;
}
case s_n_llhttp__internal__n_req_http_minor:
s_n_llhttp__internal__n_req_http_minor: {
if (p == endp) {
return s_n_llhttp__internal__n_req_http_minor;
}
switch (*p) {
case '0': {
p++;
match = 0;
goto s_n_llhttp__internal__n_invoke_store_http_minor;
}
case '1': {
p++;
match = 1;
goto s_n_llhttp__internal__n_invoke_store_http_minor;
}
case '2': {
p++;
match = 2;
goto s_n_llhttp__internal__n_invoke_store_http_minor;
}
case '3': {
p++;
match = 3;
goto s_n_llhttp__internal__n_invoke_store_http_minor;
}
case '4': {
p++;
match = 4;
goto s_n_llhttp__internal__n_invoke_store_http_minor;
}
case '5': {
p++;
match = 5;
goto s_n_llhttp__internal__n_invoke_store_http_minor;
}
case '6': {
p++;
match = 6;
goto s_n_llhttp__internal__n_invoke_store_http_minor;
}
case '7': {
p++;
match = 7;
goto s_n_llhttp__internal__n_invoke_store_http_minor;
}
case '8': {
p++;
match = 8;
goto s_n_llhttp__internal__n_invoke_store_http_minor;
}
case '9': {
p++;
match = 9;
goto s_n_llhttp__internal__n_invoke_store_http_minor;
}
default: {
goto s_n_llhttp__internal__n_span_end_llhttp__on_version_2;
}
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_error_75:
s_n_llhttp__internal__n_error_75: {
state->error = 0x9;
state->reason = "Expected dot";
state->error_pos = (const char*) p;
state->_current = (void*) (intptr_t) s_error;
return s_error;
UNREACHABLE;
}
case s_n_llhttp__internal__n_req_http_dot:
s_n_llhttp__internal__n_req_http_dot: {
if (p == endp) {
return s_n_llhttp__internal__n_req_http_dot;
}
switch (*p) {
case '.': {
p++;
goto s_n_llhttp__internal__n_req_http_minor;
}
default: {
goto s_n_llhttp__internal__n_span_end_llhttp__on_version_3;
}
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_error_76:
s_n_llhttp__internal__n_error_76: {
state->error = 0x9;
state->reason = "Invalid major version";
state->error_pos = (const char*) p;
state->_current = (void*) (intptr_t) s_error;
return s_error;
UNREACHABLE;
}
case s_n_llhttp__internal__n_req_http_major:
s_n_llhttp__internal__n_req_http_major: {
if (p == endp) {
return s_n_llhttp__internal__n_req_http_major;
}
switch (*p) {
case '0': {
p++;
match = 0;
goto s_n_llhttp__internal__n_invoke_store_http_major;
}
case '1': {
p++;
match = 1;
goto s_n_llhttp__internal__n_invoke_store_http_major;
}
case '2': {
p++;
match = 2;
goto s_n_llhttp__internal__n_invoke_store_http_major;
}
case '3': {
p++;
match = 3;
goto s_n_llhttp__internal__n_invoke_store_http_major;
}
case '4': {
p++;
match = 4;
goto s_n_llhttp__internal__n_invoke_store_http_major;
}
case '5': {
p++;
match = 5;
goto s_n_llhttp__internal__n_invoke_store_http_major;
}
case '6': {
p++;
match = 6;
goto s_n_llhttp__internal__n_invoke_store_http_major;
}
case '7': {
p++;
match = 7;
goto s_n_llhttp__internal__n_invoke_store_http_major;
}
case '8': {
p++;
match = 8;
goto s_n_llhttp__internal__n_invoke_store_http_major;
}
case '9': {
p++;
match = 9;
goto s_n_llhttp__internal__n_invoke_store_http_major;
}
default: {
goto s_n_llhttp__internal__n_span_end_llhttp__on_version_4;
}
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_span_start_llhttp__on_version:
s_n_llhttp__internal__n_span_start_llhttp__on_version: {
if (p == endp) {
return s_n_llhttp__internal__n_span_start_llhttp__on_version;
}
state->_span_pos0 = (void*) p;
state->_span_cb0 = llhttp__on_version;
goto s_n_llhttp__internal__n_req_http_major;
UNREACHABLE;
}
case s_n_llhttp__internal__n_req_after_protocol:
s_n_llhttp__internal__n_req_after_protocol: {
if (p == endp) {
return s_n_llhttp__internal__n_req_after_protocol;
}
switch (*p) {
case '/': {
p++;
goto s_n_llhttp__internal__n_span_start_llhttp__on_version;
}
default: {
goto s_n_llhttp__internal__n_error_77;
}
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_invoke_load_method:
s_n_llhttp__internal__n_invoke_load_method: {
switch (llhttp__internal__c_load_method(state, p, endp)) {
case 0:
goto s_n_llhttp__internal__n_req_after_protocol;
case 1:
goto s_n_llhttp__internal__n_req_after_protocol;
case 2:
goto s_n_llhttp__internal__n_req_after_protocol;
case 3:
goto s_n_llhttp__internal__n_req_after_protocol;
case 4:
goto s_n_llhttp__internal__n_req_after_protocol;
case 5:
goto s_n_llhttp__internal__n_req_after_protocol;
case 6:
goto s_n_llhttp__internal__n_req_after_protocol;
case 7:
goto s_n_llhttp__internal__n_req_after_protocol;
case 8:
goto s_n_llhttp__internal__n_req_after_protocol;
case 9:
goto s_n_llhttp__internal__n_req_after_protocol;
case 10:
goto s_n_llhttp__internal__n_req_after_protocol;
case 11:
goto s_n_llhttp__internal__n_req_after_protocol;
case 12:
goto s_n_llhttp__internal__n_req_after_protocol;
case 13:
goto s_n_llhttp__internal__n_req_after_protocol;
case 14:
goto s_n_llhttp__internal__n_req_after_protocol;
case 15:
goto s_n_llhttp__internal__n_req_after_protocol;
case 16:
goto s_n_llhttp__internal__n_req_after_protocol;
case 17:
goto s_n_llhttp__internal__n_req_after_protocol;
case 18:
goto s_n_llhttp__internal__n_req_after_protocol;
case 19:
goto s_n_llhttp__internal__n_req_after_protocol;
case 20:
goto s_n_llhttp__internal__n_req_after_protocol;
case 21:
goto s_n_llhttp__internal__n_req_after_protocol;
case 22:
goto s_n_llhttp__internal__n_req_after_protocol;
case 23:
goto s_n_llhttp__internal__n_req_after_protocol;
case 24:
goto s_n_llhttp__internal__n_req_after_protocol;
case 25:
goto s_n_llhttp__internal__n_req_after_protocol;
case 26:
goto s_n_llhttp__internal__n_req_after_protocol;
case 27:
goto s_n_llhttp__internal__n_req_after_protocol;
case 28:
goto s_n_llhttp__internal__n_req_after_protocol;
case 29:
goto s_n_llhttp__internal__n_req_after_protocol;
case 30:
goto s_n_llhttp__internal__n_req_after_protocol;
case 31:
goto s_n_llhttp__internal__n_req_after_protocol;
case 32:
goto s_n_llhttp__internal__n_req_after_protocol;
case 33:
goto s_n_llhttp__internal__n_req_after_protocol;
case 34:
goto s_n_llhttp__internal__n_req_after_protocol;
case 46:
goto s_n_llhttp__internal__n_req_after_protocol;
default:
goto s_n_llhttp__internal__n_error_66;
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_invoke_llhttp__on_protocol_complete:
s_n_llhttp__internal__n_invoke_llhttp__on_protocol_complete: {
switch (llhttp__on_protocol_complete(state, p, endp)) {
case 0:
goto s_n_llhttp__internal__n_invoke_load_method;
case 21:
goto s_n_llhttp__internal__n_pause_22;
default:
goto s_n_llhttp__internal__n_error_65;
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_error_82:
s_n_llhttp__internal__n_error_82: {
state->error = 0x8;
state->reason = "Expected HTTP/, RTSP/ or ICE/";
state->error_pos = (const char*) p;
state->_current = (void*) (intptr_t) s_error;
return s_error;
UNREACHABLE;
}
case s_n_llhttp__internal__n_req_after_http_start_1:
s_n_llhttp__internal__n_req_after_http_start_1: {
llparse_match_t match_seq;
if (p == endp) {
return s_n_llhttp__internal__n_req_after_http_start_1;
}
match_seq = llparse__match_sequence_id(state, p, endp, llparse_blob13, 3);
p = match_seq.current;
switch (match_seq.status) {
case kMatchComplete: {
p++;
goto s_n_llhttp__internal__n_span_end_llhttp__on_protocol;
}
case kMatchPause: {
return s_n_llhttp__internal__n_req_after_http_start_1;
}
case kMatchMismatch: {
goto s_n_llhttp__internal__n_span_end_llhttp__on_protocol_3;
}
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_invoke_load_method_2:
s_n_llhttp__internal__n_invoke_load_method_2: {
switch (llhttp__internal__c_load_method(state, p, endp)) {
case 33:
goto s_n_llhttp__internal__n_req_after_protocol;
default:
goto s_n_llhttp__internal__n_error_79;
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_invoke_llhttp__on_protocol_complete_1:
s_n_llhttp__internal__n_invoke_llhttp__on_protocol_complete_1: {
switch (llhttp__on_protocol_complete(state, p, endp)) {
case 0:
goto s_n_llhttp__internal__n_invoke_load_method_2;
case 21:
goto s_n_llhttp__internal__n_pause_23;
default:
goto s_n_llhttp__internal__n_error_78;
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_req_after_http_start_2:
s_n_llhttp__internal__n_req_after_http_start_2: {
llparse_match_t match_seq;
if (p == endp) {
return s_n_llhttp__internal__n_req_after_http_start_2;
}
match_seq = llparse__match_sequence_id(state, p, endp, llparse_blob15, 2);
p = match_seq.current;
switch (match_seq.status) {
case kMatchComplete: {
p++;
goto s_n_llhttp__internal__n_span_end_llhttp__on_protocol_1;
}
case kMatchPause: {
return s_n_llhttp__internal__n_req_after_http_start_2;
}
case kMatchMismatch: {
goto s_n_llhttp__internal__n_span_end_llhttp__on_protocol_3;
}
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_invoke_load_method_3:
s_n_llhttp__internal__n_invoke_load_method_3: {
switch (llhttp__internal__c_load_method(state, p, endp)) {
case 1:
goto s_n_llhttp__internal__n_req_after_protocol;
case 3:
goto s_n_llhttp__internal__n_req_after_protocol;
case 6:
goto s_n_llhttp__internal__n_req_after_protocol;
case 35:
goto s_n_llhttp__internal__n_req_after_protocol;
case 36:
goto s_n_llhttp__internal__n_req_after_protocol;
case 37:
goto s_n_llhttp__internal__n_req_after_protocol;
case 38:
goto s_n_llhttp__internal__n_req_after_protocol;
case 39:
goto s_n_llhttp__internal__n_req_after_protocol;
case 40:
goto s_n_llhttp__internal__n_req_after_protocol;
case 41:
goto s_n_llhttp__internal__n_req_after_protocol;
case 42:
goto s_n_llhttp__internal__n_req_after_protocol;
case 43:
goto s_n_llhttp__internal__n_req_after_protocol;
case 44:
goto s_n_llhttp__internal__n_req_after_protocol;
case 45:
goto s_n_llhttp__internal__n_req_after_protocol;
default:
goto s_n_llhttp__internal__n_error_81;
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_invoke_llhttp__on_protocol_complete_2:
s_n_llhttp__internal__n_invoke_llhttp__on_protocol_complete_2: {
switch (llhttp__on_protocol_complete(state, p, endp)) {
case 0:
goto s_n_llhttp__internal__n_invoke_load_method_3;
case 21:
goto s_n_llhttp__internal__n_pause_24;
default:
goto s_n_llhttp__internal__n_error_80;
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_req_after_http_start_3:
s_n_llhttp__internal__n_req_after_http_start_3: {
llparse_match_t match_seq;
if (p == endp) {
return s_n_llhttp__internal__n_req_after_http_start_3;
}
match_seq = llparse__match_sequence_id(state, p, endp, llparse_blob16, 3);
p = match_seq.current;
switch (match_seq.status) {
case kMatchComplete: {
p++;
goto s_n_llhttp__internal__n_span_end_llhttp__on_protocol_2;
}
case kMatchPause: {
return s_n_llhttp__internal__n_req_after_http_start_3;
}
case kMatchMismatch: {
goto s_n_llhttp__internal__n_span_end_llhttp__on_protocol_3;
}
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_req_after_http_start:
s_n_llhttp__internal__n_req_after_http_start: {
if (p == endp) {
return s_n_llhttp__internal__n_req_after_http_start;
}
switch (*p) {
case 'H': {
p++;
goto s_n_llhttp__internal__n_req_after_http_start_1;
}
case 'I': {
p++;
goto s_n_llhttp__internal__n_req_after_http_start_2;
}
case 'R': {
p++;
goto s_n_llhttp__internal__n_req_after_http_start_3;
}
default: {
goto s_n_llhttp__internal__n_span_end_llhttp__on_protocol_3;
}
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_span_start_llhttp__on_protocol:
s_n_llhttp__internal__n_span_start_llhttp__on_protocol: {
if (p == endp) {
return s_n_llhttp__internal__n_span_start_llhttp__on_protocol;
}
state->_span_pos0 = (void*) p;
state->_span_cb0 = llhttp__on_protocol;
goto s_n_llhttp__internal__n_req_after_http_start;
UNREACHABLE;
}
case s_n_llhttp__internal__n_req_http_start:
s_n_llhttp__internal__n_req_http_start: {
if (p == endp) {
return s_n_llhttp__internal__n_req_http_start;
}
switch (*p) {
case ' ': {
p++;
goto s_n_llhttp__internal__n_req_http_start;
}
default: {
goto s_n_llhttp__internal__n_span_start_llhttp__on_protocol;
}
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_url_to_http:
s_n_llhttp__internal__n_url_to_http: {
if (p == endp) {
return s_n_llhttp__internal__n_url_to_http;
}
switch (*p) {
case 9: {
p++;
goto s_n_llhttp__internal__n_error_2;
}
case 12: {
p++;
goto s_n_llhttp__internal__n_error_2;
}
default: {
goto s_n_llhttp__internal__n_invoke_llhttp__on_url_complete_1;
}
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_url_skip_to_http:
s_n_llhttp__internal__n_url_skip_to_http: {
if (p == endp) {
return s_n_llhttp__internal__n_url_skip_to_http;
}
switch (*p) {
case 9: {
p++;
goto s_n_llhttp__internal__n_error_2;
}
case 12: {
p++;
goto s_n_llhttp__internal__n_error_2;
}
default: {
p++;
goto s_n_llhttp__internal__n_url_to_http;
}
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_url_fragment:
s_n_llhttp__internal__n_url_fragment: {
static uint8_t lookup_table[] = {
0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 0, 1, 3, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
4, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5,
5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5,
5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5,
5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5,
5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5,
5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
};
if (p == endp) {
return s_n_llhttp__internal__n_url_fragment;
}
switch (lookup_table[(uint8_t) *p]) {
case 1: {
p++;
goto s_n_llhttp__internal__n_error_2;
}
case 2: {
goto s_n_llhttp__internal__n_span_end_llhttp__on_url_6;
}
case 3: {
goto s_n_llhttp__internal__n_span_end_llhttp__on_url_7;
}
case 4: {
goto s_n_llhttp__internal__n_span_end_llhttp__on_url_8;
}
case 5: {
p++;
goto s_n_llhttp__internal__n_url_fragment;
}
default: {
goto s_n_llhttp__internal__n_error_83;
}
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_span_end_stub_query_3:
s_n_llhttp__internal__n_span_end_stub_query_3: {
if (p == endp) {
return s_n_llhttp__internal__n_span_end_stub_query_3;
}
p++;
goto s_n_llhttp__internal__n_url_fragment;
UNREACHABLE;
}
case s_n_llhttp__internal__n_url_query:
s_n_llhttp__internal__n_url_query: {
static uint8_t lookup_table[] = {
0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 0, 1, 3, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
4, 5, 5, 6, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5,
5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5,
5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5,
5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5,
5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5,
5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
};
if (p == endp) {
return s_n_llhttp__internal__n_url_query;
}
switch (lookup_table[(uint8_t) *p]) {
case 1: {
p++;
goto s_n_llhttp__internal__n_error_2;
}
case 2: {
goto s_n_llhttp__internal__n_span_end_llhttp__on_url_9;
}
case 3: {
goto s_n_llhttp__internal__n_span_end_llhttp__on_url_10;
}
case 4: {
goto s_n_llhttp__internal__n_span_end_llhttp__on_url_11;
}
case 5: {
p++;
goto s_n_llhttp__internal__n_url_query;
}
case 6: {
goto s_n_llhttp__internal__n_span_end_stub_query_3;
}
default: {
goto s_n_llhttp__internal__n_error_84;
}
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_url_query_or_fragment:
s_n_llhttp__internal__n_url_query_or_fragment: {
if (p == endp) {
return s_n_llhttp__internal__n_url_query_or_fragment;
}
switch (*p) {
case 9: {
p++;
goto s_n_llhttp__internal__n_error_2;
}
case 10: {
goto s_n_llhttp__internal__n_span_end_llhttp__on_url_3;
}
case 12: {
p++;
goto s_n_llhttp__internal__n_error_2;
}
case 13: {
goto s_n_llhttp__internal__n_span_end_llhttp__on_url_4;
}
case ' ': {
goto s_n_llhttp__internal__n_span_end_llhttp__on_url_5;
}
case '#': {
p++;
goto s_n_llhttp__internal__n_url_fragment;
}
case '?': {
p++;
goto s_n_llhttp__internal__n_url_query;
}
default: {
goto s_n_llhttp__internal__n_error_85;
}
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_url_path:
s_n_llhttp__internal__n_url_path: {
static uint8_t lookup_table[] = {
0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 2, 2, 0, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0,
2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
};
if (p == endp) {
return s_n_llhttp__internal__n_url_path;
}
switch (lookup_table[(uint8_t) *p]) {
case 1: {
p++;
goto s_n_llhttp__internal__n_error_2;
}
case 2: {
p++;
goto s_n_llhttp__internal__n_url_path;
}
default: {
goto s_n_llhttp__internal__n_url_query_or_fragment;
}
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_span_start_stub_path_2:
s_n_llhttp__internal__n_span_start_stub_path_2: {
if (p == endp) {
return s_n_llhttp__internal__n_span_start_stub_path_2;
}
p++;
goto s_n_llhttp__internal__n_url_path;
UNREACHABLE;
}
case s_n_llhttp__internal__n_span_start_stub_path:
s_n_llhttp__internal__n_span_start_stub_path: {
if (p == endp) {
return s_n_llhttp__internal__n_span_start_stub_path;
}
p++;
goto s_n_llhttp__internal__n_url_path;
UNREACHABLE;
}
case s_n_llhttp__internal__n_span_start_stub_path_1:
s_n_llhttp__internal__n_span_start_stub_path_1: {
if (p == endp) {
return s_n_llhttp__internal__n_span_start_stub_path_1;
}
p++;
goto s_n_llhttp__internal__n_url_path;
UNREACHABLE;
}
case s_n_llhttp__internal__n_url_server_with_at:
s_n_llhttp__internal__n_url_server_with_at: {
static uint8_t lookup_table[] = {
0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 0, 1, 3, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
4, 5, 0, 0, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 6,
5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 0, 5, 0, 7,
8, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5,
5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 0, 5, 0, 5,
0, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5,
5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 0, 0, 0, 5, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
};
if (p == endp) {
return s_n_llhttp__internal__n_url_server_with_at;
}
switch (lookup_table[(uint8_t) *p]) {
case 1: {
p++;
goto s_n_llhttp__internal__n_error_2;
}
case 2: {
goto s_n_llhttp__internal__n_span_end_llhttp__on_url_12;
}
case 3: {
goto s_n_llhttp__internal__n_span_end_llhttp__on_url_13;
}
case 4: {
goto s_n_llhttp__internal__n_span_end_llhttp__on_url_14;
}
case 5: {
p++;
goto s_n_llhttp__internal__n_url_server;
}
case 6: {
goto s_n_llhttp__internal__n_span_start_stub_path_1;
}
case 7: {
p++;
goto s_n_llhttp__internal__n_url_query;
}
case 8: {
p++;
goto s_n_llhttp__internal__n_error_86;
}
default: {
goto s_n_llhttp__internal__n_error_87;
}
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_url_server:
s_n_llhttp__internal__n_url_server: {
static uint8_t lookup_table[] = {
0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 0, 1, 3, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
4, 5, 0, 0, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 6,
5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 0, 5, 0, 7,
8, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5,
5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 0, 5, 0, 5,
0, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5,
5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 0, 0, 0, 5, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
};
if (p == endp) {
return s_n_llhttp__internal__n_url_server;
}
switch (lookup_table[(uint8_t) *p]) {
case 1: {
p++;
goto s_n_llhttp__internal__n_error_2;
}
case 2: {
goto s_n_llhttp__internal__n_span_end_llhttp__on_url;
}
case 3: {
goto s_n_llhttp__internal__n_span_end_llhttp__on_url_1;
}
case 4: {
goto s_n_llhttp__internal__n_span_end_llhttp__on_url_2;
}
case 5: {
p++;
goto s_n_llhttp__internal__n_url_server;
}
case 6: {
goto s_n_llhttp__internal__n_span_start_stub_path;
}
case 7: {
p++;
goto s_n_llhttp__internal__n_url_query;
}
case 8: {
p++;
goto s_n_llhttp__internal__n_url_server_with_at;
}
default: {
goto s_n_llhttp__internal__n_error_88;
}
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_url_schema_delim_1:
s_n_llhttp__internal__n_url_schema_delim_1: {
if (p == endp) {
return s_n_llhttp__internal__n_url_schema_delim_1;
}
switch (*p) {
case '/': {
p++;
goto s_n_llhttp__internal__n_url_server;
}
default: {
goto s_n_llhttp__internal__n_error_89;
}
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_url_schema_delim:
s_n_llhttp__internal__n_url_schema_delim: {
if (p == endp) {
return s_n_llhttp__internal__n_url_schema_delim;
}
switch (*p) {
case 9: {
p++;
goto s_n_llhttp__internal__n_error_2;
}
case 10: {
p++;
goto s_n_llhttp__internal__n_error_2;
}
case 12: {
p++;
goto s_n_llhttp__internal__n_error_2;
}
case 13: {
p++;
goto s_n_llhttp__internal__n_error_2;
}
case ' ': {
p++;
goto s_n_llhttp__internal__n_error_2;
}
case '/': {
p++;
goto s_n_llhttp__internal__n_url_schema_delim_1;
}
default: {
goto s_n_llhttp__internal__n_error_89;
}
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_span_end_stub_schema:
s_n_llhttp__internal__n_span_end_stub_schema: {
if (p == endp) {
return s_n_llhttp__internal__n_span_end_stub_schema;
}
p++;
goto s_n_llhttp__internal__n_url_schema_delim;
UNREACHABLE;
}
case s_n_llhttp__internal__n_url_schema:
s_n_llhttp__internal__n_url_schema: {
static uint8_t lookup_table[] = {
0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 1, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0,
0, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3,
3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 0, 0, 0, 0, 0,
0, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3,
3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
};
if (p == endp) {
return s_n_llhttp__internal__n_url_schema;
}
switch (lookup_table[(uint8_t) *p]) {
case 1: {
p++;
goto s_n_llhttp__internal__n_error_2;
}
case 2: {
goto s_n_llhttp__internal__n_span_end_stub_schema;
}
case 3: {
p++;
goto s_n_llhttp__internal__n_url_schema;
}
default: {
goto s_n_llhttp__internal__n_error_90;
}
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_url_start:
s_n_llhttp__internal__n_url_start: {
static uint8_t lookup_table[] = {
0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 1, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 2,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3,
3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 0, 0, 0, 0, 0,
0, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3,
3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
};
if (p == endp) {
return s_n_llhttp__internal__n_url_start;
}
switch (lookup_table[(uint8_t) *p]) {
case 1: {
p++;
goto s_n_llhttp__internal__n_error_2;
}
case 2: {
goto s_n_llhttp__internal__n_span_start_stub_path_2;
}
case 3: {
goto s_n_llhttp__internal__n_url_schema;
}
default: {
goto s_n_llhttp__internal__n_error_91;
}
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_span_start_llhttp__on_url_1:
s_n_llhttp__internal__n_span_start_llhttp__on_url_1: {
if (p == endp) {
return s_n_llhttp__internal__n_span_start_llhttp__on_url_1;
}
state->_span_pos0 = (void*) p;
state->_span_cb0 = llhttp__on_url;
goto s_n_llhttp__internal__n_url_start;
UNREACHABLE;
}
case s_n_llhttp__internal__n_url_entry_normal:
s_n_llhttp__internal__n_url_entry_normal: {
if (p == endp) {
return s_n_llhttp__internal__n_url_entry_normal;
}
switch (*p) {
case 9: {
p++;
goto s_n_llhttp__internal__n_error_2;
}
case 12: {
p++;
goto s_n_llhttp__internal__n_error_2;
}
default: {
goto s_n_llhttp__internal__n_span_start_llhttp__on_url_1;
}
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_span_start_llhttp__on_url:
s_n_llhttp__internal__n_span_start_llhttp__on_url: {
if (p == endp) {
return s_n_llhttp__internal__n_span_start_llhttp__on_url;
}
state->_span_pos0 = (void*) p;
state->_span_cb0 = llhttp__on_url;
goto s_n_llhttp__internal__n_url_server;
UNREACHABLE;
}
case s_n_llhttp__internal__n_url_entry_connect:
s_n_llhttp__internal__n_url_entry_connect: {
if (p == endp) {
return s_n_llhttp__internal__n_url_entry_connect;
}
switch (*p) {
case 9: {
p++;
goto s_n_llhttp__internal__n_error_2;
}
case 12: {
p++;
goto s_n_llhttp__internal__n_error_2;
}
default: {
goto s_n_llhttp__internal__n_span_start_llhttp__on_url;
}
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_req_spaces_before_url:
s_n_llhttp__internal__n_req_spaces_before_url: {
if (p == endp) {
return s_n_llhttp__internal__n_req_spaces_before_url;
}
switch (*p) {
case ' ': {
p++;
goto s_n_llhttp__internal__n_req_spaces_before_url;
}
default: {
goto s_n_llhttp__internal__n_invoke_is_equal_method;
}
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_req_first_space_before_url:
s_n_llhttp__internal__n_req_first_space_before_url: {
if (p == endp) {
return s_n_llhttp__internal__n_req_first_space_before_url;
}
switch (*p) {
case ' ': {
p++;
goto s_n_llhttp__internal__n_req_spaces_before_url;
}
default: {
goto s_n_llhttp__internal__n_error_92;
}
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_invoke_llhttp__on_method_complete_1:
s_n_llhttp__internal__n_invoke_llhttp__on_method_complete_1: {
switch (llhttp__on_method_complete(state, p, endp)) {
case 0:
goto s_n_llhttp__internal__n_req_first_space_before_url;
case 21:
goto s_n_llhttp__internal__n_pause_29;
default:
goto s_n_llhttp__internal__n_error_111;
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_after_start_req_2:
s_n_llhttp__internal__n_after_start_req_2: {
if (p == endp) {
return s_n_llhttp__internal__n_after_start_req_2;
}
switch (*p) {
case 'L': {
p++;
match = 19;
goto s_n_llhttp__internal__n_invoke_store_method_1;
}
default: {
goto s_n_llhttp__internal__n_error_112;
}
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_after_start_req_3:
s_n_llhttp__internal__n_after_start_req_3: {
llparse_match_t match_seq;
if (p == endp) {
return s_n_llhttp__internal__n_after_start_req_3;
}
match_seq = llparse__match_sequence_id(state, p, endp, llparse_blob17, 6);
p = match_seq.current;
switch (match_seq.status) {
case kMatchComplete: {
p++;
match = 36;
goto s_n_llhttp__internal__n_invoke_store_method_1;
}
case kMatchPause: {
return s_n_llhttp__internal__n_after_start_req_3;
}
case kMatchMismatch: {
goto s_n_llhttp__internal__n_error_112;
}
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_after_start_req_1:
s_n_llhttp__internal__n_after_start_req_1: {
if (p == endp) {
return s_n_llhttp__internal__n_after_start_req_1;
}
switch (*p) {
case 'C': {
p++;
goto s_n_llhttp__internal__n_after_start_req_2;
}
case 'N': {
p++;
goto s_n_llhttp__internal__n_after_start_req_3;
}
default: {
goto s_n_llhttp__internal__n_error_112;
}
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_after_start_req_4:
s_n_llhttp__internal__n_after_start_req_4: {
llparse_match_t match_seq;
if (p == endp) {
return s_n_llhttp__internal__n_after_start_req_4;
}
match_seq = llparse__match_sequence_id(state, p, endp, llparse_blob18, 3);
p = match_seq.current;
switch (match_seq.status) {
case kMatchComplete: {
p++;
match = 16;
goto s_n_llhttp__internal__n_invoke_store_method_1;
}
case kMatchPause: {
return s_n_llhttp__internal__n_after_start_req_4;
}
case kMatchMismatch: {
goto s_n_llhttp__internal__n_error_112;
}
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_after_start_req_6:
s_n_llhttp__internal__n_after_start_req_6: {
llparse_match_t match_seq;
if (p == endp) {
return s_n_llhttp__internal__n_after_start_req_6;
}
match_seq = llparse__match_sequence_id(state, p, endp, llparse_blob19, 6);
p = match_seq.current;
switch (match_seq.status) {
case kMatchComplete: {
p++;
match = 22;
goto s_n_llhttp__internal__n_invoke_store_method_1;
}
case kMatchPause: {
return s_n_llhttp__internal__n_after_start_req_6;
}
case kMatchMismatch: {
goto s_n_llhttp__internal__n_error_112;
}
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_after_start_req_8:
s_n_llhttp__internal__n_after_start_req_8: {
llparse_match_t match_seq;
if (p == endp) {
return s_n_llhttp__internal__n_after_start_req_8;
}
match_seq = llparse__match_sequence_id(state, p, endp, llparse_blob20, 4);
p = match_seq.current;
switch (match_seq.status) {
case kMatchComplete: {
p++;
match = 5;
goto s_n_llhttp__internal__n_invoke_store_method_1;
}
case kMatchPause: {
return s_n_llhttp__internal__n_after_start_req_8;
}
case kMatchMismatch: {
goto s_n_llhttp__internal__n_error_112;
}
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_after_start_req_9:
s_n_llhttp__internal__n_after_start_req_9: {
if (p == endp) {
return s_n_llhttp__internal__n_after_start_req_9;
}
switch (*p) {
case 'Y': {
p++;
match = 8;
goto s_n_llhttp__internal__n_invoke_store_method_1;
}
default: {
goto s_n_llhttp__internal__n_error_112;
}
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_after_start_req_7:
s_n_llhttp__internal__n_after_start_req_7: {
if (p == endp) {
return s_n_llhttp__internal__n_after_start_req_7;
}
switch (*p) {
case 'N': {
p++;
goto s_n_llhttp__internal__n_after_start_req_8;
}
case 'P': {
p++;
goto s_n_llhttp__internal__n_after_start_req_9;
}
default: {
goto s_n_llhttp__internal__n_error_112;
}
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_after_start_req_5:
s_n_llhttp__internal__n_after_start_req_5: {
if (p == endp) {
return s_n_llhttp__internal__n_after_start_req_5;
}
switch (*p) {
case 'H': {
p++;
goto s_n_llhttp__internal__n_after_start_req_6;
}
case 'O': {
p++;
goto s_n_llhttp__internal__n_after_start_req_7;
}
default: {
goto s_n_llhttp__internal__n_error_112;
}
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_after_start_req_12:
s_n_llhttp__internal__n_after_start_req_12: {
llparse_match_t match_seq;
if (p == endp) {
return s_n_llhttp__internal__n_after_start_req_12;
}
match_seq = llparse__match_sequence_id(state, p, endp, llparse_blob21, 3);
p = match_seq.current;
switch (match_seq.status) {
case kMatchComplete: {
p++;
match = 0;
goto s_n_llhttp__internal__n_invoke_store_method_1;
}
case kMatchPause: {
return s_n_llhttp__internal__n_after_start_req_12;
}
case kMatchMismatch: {
goto s_n_llhttp__internal__n_error_112;
}
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_after_start_req_13:
s_n_llhttp__internal__n_after_start_req_13: {
llparse_match_t match_seq;
if (p == endp) {
return s_n_llhttp__internal__n_after_start_req_13;
}
match_seq = llparse__match_sequence_id(state, p, endp, llparse_blob22, 5);
p = match_seq.current;
switch (match_seq.status) {
case kMatchComplete: {
p++;
match = 35;
goto s_n_llhttp__internal__n_invoke_store_method_1;
}
case kMatchPause: {
return s_n_llhttp__internal__n_after_start_req_13;
}
case kMatchMismatch: {
goto s_n_llhttp__internal__n_error_112;
}
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_after_start_req_11:
s_n_llhttp__internal__n_after_start_req_11: {
if (p == endp) {
return s_n_llhttp__internal__n_after_start_req_11;
}
switch (*p) {
case 'L': {
p++;
goto s_n_llhttp__internal__n_after_start_req_12;
}
case 'S': {
p++;
goto s_n_llhttp__internal__n_after_start_req_13;
}
default: {
goto s_n_llhttp__internal__n_error_112;
}
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_after_start_req_10:
s_n_llhttp__internal__n_after_start_req_10: {
if (p == endp) {
return s_n_llhttp__internal__n_after_start_req_10;
}
switch (*p) {
case 'E': {
p++;
goto s_n_llhttp__internal__n_after_start_req_11;
}
default: {
goto s_n_llhttp__internal__n_error_112;
}
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_after_start_req_14:
s_n_llhttp__internal__n_after_start_req_14: {
llparse_match_t match_seq;
if (p == endp) {
return s_n_llhttp__internal__n_after_start_req_14;
}
match_seq = llparse__match_sequence_id(state, p, endp, llparse_blob23, 4);
p = match_seq.current;
switch (match_seq.status) {
case kMatchComplete: {
p++;
match = 45;
goto s_n_llhttp__internal__n_invoke_store_method_1;
}
case kMatchPause: {
return s_n_llhttp__internal__n_after_start_req_14;
}
case kMatchMismatch: {
goto s_n_llhttp__internal__n_error_112;
}
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_after_start_req_17:
s_n_llhttp__internal__n_after_start_req_17: {
llparse_match_t match_seq;
if (p == endp) {
return s_n_llhttp__internal__n_after_start_req_17;
}
match_seq = llparse__match_sequence_id(state, p, endp, llparse_blob25, 9);
p = match_seq.current;
switch (match_seq.status) {
case kMatchComplete: {
p++;
match = 41;
goto s_n_llhttp__internal__n_invoke_store_method_1;
}
case kMatchPause: {
return s_n_llhttp__internal__n_after_start_req_17;
}
case kMatchMismatch: {
goto s_n_llhttp__internal__n_error_112;
}
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_after_start_req_16:
s_n_llhttp__internal__n_after_start_req_16: {
if (p == endp) {
return s_n_llhttp__internal__n_after_start_req_16;
}
switch (*p) {
case '_': {
p++;
goto s_n_llhttp__internal__n_after_start_req_17;
}
default: {
match = 1;
goto s_n_llhttp__internal__n_invoke_store_method_1;
}
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_after_start_req_15:
s_n_llhttp__internal__n_after_start_req_15: {
llparse_match_t match_seq;
if (p == endp) {
return s_n_llhttp__internal__n_after_start_req_15;
}
match_seq = llparse__match_sequence_id(state, p, endp, llparse_blob24, 2);
p = match_seq.current;
switch (match_seq.status) {
case kMatchComplete: {
p++;
goto s_n_llhttp__internal__n_after_start_req_16;
}
case kMatchPause: {
return s_n_llhttp__internal__n_after_start_req_15;
}
case kMatchMismatch: {
goto s_n_llhttp__internal__n_error_112;
}
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_after_start_req_18:
s_n_llhttp__internal__n_after_start_req_18: {
llparse_match_t match_seq;
if (p == endp) {
return s_n_llhttp__internal__n_after_start_req_18;
}
match_seq = llparse__match_sequence_id(state, p, endp, llparse_blob26, 3);
p = match_seq.current;
switch (match_seq.status) {
case kMatchComplete: {
p++;
match = 2;
goto s_n_llhttp__internal__n_invoke_store_method_1;
}
case kMatchPause: {
return s_n_llhttp__internal__n_after_start_req_18;
}
case kMatchMismatch: {
goto s_n_llhttp__internal__n_error_112;
}
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_after_start_req_20:
s_n_llhttp__internal__n_after_start_req_20: {
llparse_match_t match_seq;
if (p == endp) {
return s_n_llhttp__internal__n_after_start_req_20;
}
match_seq = llparse__match_sequence_id(state, p, endp, llparse_blob27, 2);
p = match_seq.current;
switch (match_seq.status) {
case kMatchComplete: {
p++;
match = 31;
goto s_n_llhttp__internal__n_invoke_store_method_1;
}
case kMatchPause: {
return s_n_llhttp__internal__n_after_start_req_20;
}
case kMatchMismatch: {
goto s_n_llhttp__internal__n_error_112;
}
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_after_start_req_21:
s_n_llhttp__internal__n_after_start_req_21: {
llparse_match_t match_seq;
if (p == endp) {
return s_n_llhttp__internal__n_after_start_req_21;
}
match_seq = llparse__match_sequence_id(state, p, endp, llparse_blob28, 2);
p = match_seq.current;
switch (match_seq.status) {
case kMatchComplete: {
p++;
match = 9;
goto s_n_llhttp__internal__n_invoke_store_method_1;
}
case kMatchPause: {
return s_n_llhttp__internal__n_after_start_req_21;
}
case kMatchMismatch: {
goto s_n_llhttp__internal__n_error_112;
}
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_after_start_req_19:
s_n_llhttp__internal__n_after_start_req_19: {
if (p == endp) {
return s_n_llhttp__internal__n_after_start_req_19;
}
switch (*p) {
case 'I': {
p++;
goto s_n_llhttp__internal__n_after_start_req_20;
}
case 'O': {
p++;
goto s_n_llhttp__internal__n_after_start_req_21;
}
default: {
goto s_n_llhttp__internal__n_error_112;
}
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_after_start_req_23:
s_n_llhttp__internal__n_after_start_req_23: {
llparse_match_t match_seq;
if (p == endp) {
return s_n_llhttp__internal__n_after_start_req_23;
}
match_seq = llparse__match_sequence_id(state, p, endp, llparse_blob29, 6);
p = match_seq.current;
switch (match_seq.status) {
case kMatchComplete: {
p++;
match = 24;
goto s_n_llhttp__internal__n_invoke_store_method_1;
}
case kMatchPause: {
return s_n_llhttp__internal__n_after_start_req_23;
}
case kMatchMismatch: {
goto s_n_llhttp__internal__n_error_112;
}
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_after_start_req_24:
s_n_llhttp__internal__n_after_start_req_24: {
llparse_match_t match_seq;
if (p == endp) {
return s_n_llhttp__internal__n_after_start_req_24;
}
match_seq = llparse__match_sequence_id(state, p, endp, llparse_blob30, 3);
p = match_seq.current;
switch (match_seq.status) {
case kMatchComplete: {
p++;
match = 23;
goto s_n_llhttp__internal__n_invoke_store_method_1;
}
case kMatchPause: {
return s_n_llhttp__internal__n_after_start_req_24;
}
case kMatchMismatch: {
goto s_n_llhttp__internal__n_error_112;
}
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_after_start_req_26:
s_n_llhttp__internal__n_after_start_req_26: {
llparse_match_t match_seq;
if (p == endp) {
return s_n_llhttp__internal__n_after_start_req_26;
}
match_seq = llparse__match_sequence_id(state, p, endp, llparse_blob31, 7);
p = match_seq.current;
switch (match_seq.status) {
case kMatchComplete: {
p++;
match = 21;
goto s_n_llhttp__internal__n_invoke_store_method_1;
}
case kMatchPause: {
return s_n_llhttp__internal__n_after_start_req_26;
}
case kMatchMismatch: {
goto s_n_llhttp__internal__n_error_112;
}
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_after_start_req_28:
s_n_llhttp__internal__n_after_start_req_28: {
llparse_match_t match_seq;
if (p == endp) {
return s_n_llhttp__internal__n_after_start_req_28;
}
match_seq = llparse__match_sequence_id(state, p, endp, llparse_blob32, 6);
p = match_seq.current;
switch (match_seq.status) {
case kMatchComplete: {
p++;
match = 30;
goto s_n_llhttp__internal__n_invoke_store_method_1;
}
case kMatchPause: {
return s_n_llhttp__internal__n_after_start_req_28;
}
case kMatchMismatch: {
goto s_n_llhttp__internal__n_error_112;
}
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_after_start_req_29:
s_n_llhttp__internal__n_after_start_req_29: {
if (p == endp) {
return s_n_llhttp__internal__n_after_start_req_29;
}
switch (*p) {
case 'L': {
p++;
match = 10;
goto s_n_llhttp__internal__n_invoke_store_method_1;
}
default: {
goto s_n_llhttp__internal__n_error_112;
}
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_after_start_req_27:
s_n_llhttp__internal__n_after_start_req_27: {
if (p == endp) {
return s_n_llhttp__internal__n_after_start_req_27;
}
switch (*p) {
case 'A': {
p++;
goto s_n_llhttp__internal__n_after_start_req_28;
}
case 'O': {
p++;
goto s_n_llhttp__internal__n_after_start_req_29;
}
default: {
goto s_n_llhttp__internal__n_error_112;
}
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_after_start_req_25:
s_n_llhttp__internal__n_after_start_req_25: {
if (p == endp) {
return s_n_llhttp__internal__n_after_start_req_25;
}
switch (*p) {
case 'A': {
p++;
goto s_n_llhttp__internal__n_after_start_req_26;
}
case 'C': {
p++;
goto s_n_llhttp__internal__n_after_start_req_27;
}
default: {
goto s_n_llhttp__internal__n_error_112;
}
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_after_start_req_30:
s_n_llhttp__internal__n_after_start_req_30: {
llparse_match_t match_seq;
if (p == endp) {
return s_n_llhttp__internal__n_after_start_req_30;
}
match_seq = llparse__match_sequence_id(state, p, endp, llparse_blob33, 2);
p = match_seq.current;
switch (match_seq.status) {
case kMatchComplete: {
p++;
match = 11;
goto s_n_llhttp__internal__n_invoke_store_method_1;
}
case kMatchPause: {
return s_n_llhttp__internal__n_after_start_req_30;
}
case kMatchMismatch: {
goto s_n_llhttp__internal__n_error_112;
}
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_after_start_req_22:
s_n_llhttp__internal__n_after_start_req_22: {
if (p == endp) {
return s_n_llhttp__internal__n_after_start_req_22;
}
switch (*p) {
case '-': {
p++;
goto s_n_llhttp__internal__n_after_start_req_23;
}
case 'E': {
p++;
goto s_n_llhttp__internal__n_after_start_req_24;
}
case 'K': {
p++;
goto s_n_llhttp__internal__n_after_start_req_25;
}
case 'O': {
p++;
goto s_n_llhttp__internal__n_after_start_req_30;
}
default: {
goto s_n_llhttp__internal__n_error_112;
}
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_after_start_req_31:
s_n_llhttp__internal__n_after_start_req_31: {
llparse_match_t match_seq;
if (p == endp) {
return s_n_llhttp__internal__n_after_start_req_31;
}
match_seq = llparse__match_sequence_id(state, p, endp, llparse_blob34, 5);
p = match_seq.current;
switch (match_seq.status) {
case kMatchComplete: {
p++;
match = 25;
goto s_n_llhttp__internal__n_invoke_store_method_1;
}
case kMatchPause: {
return s_n_llhttp__internal__n_after_start_req_31;
}
case kMatchMismatch: {
goto s_n_llhttp__internal__n_error_112;
}
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_after_start_req_32:
s_n_llhttp__internal__n_after_start_req_32: {
llparse_match_t match_seq;
if (p == endp) {
return s_n_llhttp__internal__n_after_start_req_32;
}
match_seq = llparse__match_sequence_id(state, p, endp, llparse_blob35, 6);
p = match_seq.current;
switch (match_seq.status) {
case kMatchComplete: {
p++;
match = 6;
goto s_n_llhttp__internal__n_invoke_store_method_1;
}
case kMatchPause: {
return s_n_llhttp__internal__n_after_start_req_32;
}
case kMatchMismatch: {
goto s_n_llhttp__internal__n_error_112;
}
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_after_start_req_35:
s_n_llhttp__internal__n_after_start_req_35: {
llparse_match_t match_seq;
if (p == endp) {
return s_n_llhttp__internal__n_after_start_req_35;
}
match_seq = llparse__match_sequence_id(state, p, endp, llparse_blob36, 2);
p = match_seq.current;
switch (match_seq.status) {
case kMatchComplete: {
p++;
match = 28;
goto s_n_llhttp__internal__n_invoke_store_method_1;
}
case kMatchPause: {
return s_n_llhttp__internal__n_after_start_req_35;
}
case kMatchMismatch: {
goto s_n_llhttp__internal__n_error_112;
}
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_after_start_req_36:
s_n_llhttp__internal__n_after_start_req_36: {
llparse_match_t match_seq;
if (p == endp) {
return s_n_llhttp__internal__n_after_start_req_36;
}
match_seq = llparse__match_sequence_id(state, p, endp, llparse_blob37, 2);
p = match_seq.current;
switch (match_seq.status) {
case kMatchComplete: {
p++;
match = 39;
goto s_n_llhttp__internal__n_invoke_store_method_1;
}
case kMatchPause: {
return s_n_llhttp__internal__n_after_start_req_36;
}
case kMatchMismatch: {
goto s_n_llhttp__internal__n_error_112;
}
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_after_start_req_34:
s_n_llhttp__internal__n_after_start_req_34: {
if (p == endp) {
return s_n_llhttp__internal__n_after_start_req_34;
}
switch (*p) {
case 'T': {
p++;
goto s_n_llhttp__internal__n_after_start_req_35;
}
case 'U': {
p++;
goto s_n_llhttp__internal__n_after_start_req_36;
}
default: {
goto s_n_llhttp__internal__n_error_112;
}
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_after_start_req_37:
s_n_llhttp__internal__n_after_start_req_37: {
llparse_match_t match_seq;
if (p == endp) {
return s_n_llhttp__internal__n_after_start_req_37;
}
match_seq = llparse__match_sequence_id(state, p, endp, llparse_blob38, 2);
p = match_seq.current;
switch (match_seq.status) {
case kMatchComplete: {
p++;
match = 38;
goto s_n_llhttp__internal__n_invoke_store_method_1;
}
case kMatchPause: {
return s_n_llhttp__internal__n_after_start_req_37;
}
case kMatchMismatch: {
goto s_n_llhttp__internal__n_error_112;
}
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_after_start_req_38:
s_n_llhttp__internal__n_after_start_req_38: {
llparse_match_t match_seq;
if (p == endp) {
return s_n_llhttp__internal__n_after_start_req_38;
}
match_seq = llparse__match_sequence_id(state, p, endp, llparse_blob39, 2);
p = match_seq.current;
switch (match_seq.status) {
case kMatchComplete: {
p++;
match = 3;
goto s_n_llhttp__internal__n_invoke_store_method_1;
}
case kMatchPause: {
return s_n_llhttp__internal__n_after_start_req_38;
}
case kMatchMismatch: {
goto s_n_llhttp__internal__n_error_112;
}
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_after_start_req_42:
s_n_llhttp__internal__n_after_start_req_42: {
llparse_match_t match_seq;
if (p == endp) {
return s_n_llhttp__internal__n_after_start_req_42;
}
match_seq = llparse__match_sequence_id(state, p, endp, llparse_blob40, 3);
p = match_seq.current;
switch (match_seq.status) {
case kMatchComplete: {
p++;
match = 12;
goto s_n_llhttp__internal__n_invoke_store_method_1;
}
case kMatchPause: {
return s_n_llhttp__internal__n_after_start_req_42;
}
case kMatchMismatch: {
goto s_n_llhttp__internal__n_error_112;
}
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_after_start_req_43:
s_n_llhttp__internal__n_after_start_req_43: {
llparse_match_t match_seq;
if (p == endp) {
return s_n_llhttp__internal__n_after_start_req_43;
}
match_seq = llparse__match_sequence_id(state, p, endp, llparse_blob41, 4);
p = match_seq.current;
switch (match_seq.status) {
case kMatchComplete: {
p++;
match = 13;
goto s_n_llhttp__internal__n_invoke_store_method_1;
}
case kMatchPause: {
return s_n_llhttp__internal__n_after_start_req_43;
}
case kMatchMismatch: {
goto s_n_llhttp__internal__n_error_112;
}
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_after_start_req_41:
s_n_llhttp__internal__n_after_start_req_41: {
if (p == endp) {
return s_n_llhttp__internal__n_after_start_req_41;
}
switch (*p) {
case 'F': {
p++;
goto s_n_llhttp__internal__n_after_start_req_42;
}
case 'P': {
p++;
goto s_n_llhttp__internal__n_after_start_req_43;
}
default: {
goto s_n_llhttp__internal__n_error_112;
}
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_after_start_req_40:
s_n_llhttp__internal__n_after_start_req_40: {
if (p == endp) {
return s_n_llhttp__internal__n_after_start_req_40;
}
switch (*p) {
case 'P': {
p++;
goto s_n_llhttp__internal__n_after_start_req_41;
}
default: {
goto s_n_llhttp__internal__n_error_112;
}
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_after_start_req_39:
s_n_llhttp__internal__n_after_start_req_39: {
if (p == endp) {
return s_n_llhttp__internal__n_after_start_req_39;
}
switch (*p) {
case 'I': {
p++;
match = 34;
goto s_n_llhttp__internal__n_invoke_store_method_1;
}
case 'O': {
p++;
goto s_n_llhttp__internal__n_after_start_req_40;
}
default: {
goto s_n_llhttp__internal__n_error_112;
}
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_after_start_req_45:
s_n_llhttp__internal__n_after_start_req_45: {
llparse_match_t match_seq;
if (p == endp) {
return s_n_llhttp__internal__n_after_start_req_45;
}
match_seq = llparse__match_sequence_id(state, p, endp, llparse_blob42, 2);
p = match_seq.current;
switch (match_seq.status) {
case kMatchComplete: {
p++;
match = 29;
goto s_n_llhttp__internal__n_invoke_store_method_1;
}
case kMatchPause: {
return s_n_llhttp__internal__n_after_start_req_45;
}
case kMatchMismatch: {
goto s_n_llhttp__internal__n_error_112;
}
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_after_start_req_44:
s_n_llhttp__internal__n_after_start_req_44: {
if (p == endp) {
return s_n_llhttp__internal__n_after_start_req_44;
}
switch (*p) {
case 'R': {
p++;
goto s_n_llhttp__internal__n_after_start_req_45;
}
case 'T': {
p++;
match = 4;
goto s_n_llhttp__internal__n_invoke_store_method_1;
}
default: {
goto s_n_llhttp__internal__n_error_112;
}
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_after_start_req_33:
s_n_llhttp__internal__n_after_start_req_33: {
if (p == endp) {
return s_n_llhttp__internal__n_after_start_req_33;
}
switch (*p) {
case 'A': {
p++;
goto s_n_llhttp__internal__n_after_start_req_34;
}
case 'L': {
p++;
goto s_n_llhttp__internal__n_after_start_req_37;
}
case 'O': {
p++;
goto s_n_llhttp__internal__n_after_start_req_38;
}
case 'R': {
p++;
goto s_n_llhttp__internal__n_after_start_req_39;
}
case 'U': {
p++;
goto s_n_llhttp__internal__n_after_start_req_44;
}
default: {
goto s_n_llhttp__internal__n_error_112;
}
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_after_start_req_46:
s_n_llhttp__internal__n_after_start_req_46: {
llparse_match_t match_seq;
if (p == endp) {
return s_n_llhttp__internal__n_after_start_req_46;
}
match_seq = llparse__match_sequence_id(state, p, endp, llparse_blob43, 4);
p = match_seq.current;
switch (match_seq.status) {
case kMatchComplete: {
p++;
match = 46;
goto s_n_llhttp__internal__n_invoke_store_method_1;
}
case kMatchPause: {
return s_n_llhttp__internal__n_after_start_req_46;
}
case kMatchMismatch: {
goto s_n_llhttp__internal__n_error_112;
}
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_after_start_req_49:
s_n_llhttp__internal__n_after_start_req_49: {
llparse_match_t match_seq;
if (p == endp) {
return s_n_llhttp__internal__n_after_start_req_49;
}
match_seq = llparse__match_sequence_id(state, p, endp, llparse_blob44, 3);
p = match_seq.current;
switch (match_seq.status) {
case kMatchComplete: {
p++;
match = 17;
goto s_n_llhttp__internal__n_invoke_store_method_1;
}
case kMatchPause: {
return s_n_llhttp__internal__n_after_start_req_49;
}
case kMatchMismatch: {
goto s_n_llhttp__internal__n_error_112;
}
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_after_start_req_50:
s_n_llhttp__internal__n_after_start_req_50: {
llparse_match_t match_seq;
if (p == endp) {
return s_n_llhttp__internal__n_after_start_req_50;
}
match_seq = llparse__match_sequence_id(state, p, endp, llparse_blob45, 3);
p = match_seq.current;
switch (match_seq.status) {
case kMatchComplete: {
p++;
match = 44;
goto s_n_llhttp__internal__n_invoke_store_method_1;
}
case kMatchPause: {
return s_n_llhttp__internal__n_after_start_req_50;
}
case kMatchMismatch: {
goto s_n_llhttp__internal__n_error_112;
}
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_after_start_req_51:
s_n_llhttp__internal__n_after_start_req_51: {
llparse_match_t match_seq;
if (p == endp) {
return s_n_llhttp__internal__n_after_start_req_51;
}
match_seq = llparse__match_sequence_id(state, p, endp, llparse_blob46, 5);
p = match_seq.current;
switch (match_seq.status) {
case kMatchComplete: {
p++;
match = 43;
goto s_n_llhttp__internal__n_invoke_store_method_1;
}
case kMatchPause: {
return s_n_llhttp__internal__n_after_start_req_51;
}
case kMatchMismatch: {
goto s_n_llhttp__internal__n_error_112;
}
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_after_start_req_52:
s_n_llhttp__internal__n_after_start_req_52: {
llparse_match_t match_seq;
if (p == endp) {
return s_n_llhttp__internal__n_after_start_req_52;
}
match_seq = llparse__match_sequence_id(state, p, endp, llparse_blob47, 3);
p = match_seq.current;
switch (match_seq.status) {
case kMatchComplete: {
p++;
match = 20;
goto s_n_llhttp__internal__n_invoke_store_method_1;
}
case kMatchPause: {
return s_n_llhttp__internal__n_after_start_req_52;
}
case kMatchMismatch: {
goto s_n_llhttp__internal__n_error_112;
}
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_after_start_req_48:
s_n_llhttp__internal__n_after_start_req_48: {
if (p == endp) {
return s_n_llhttp__internal__n_after_start_req_48;
}
switch (*p) {
case 'B': {
p++;
goto s_n_llhttp__internal__n_after_start_req_49;
}
case 'C': {
p++;
goto s_n_llhttp__internal__n_after_start_req_50;
}
case 'D': {
p++;
goto s_n_llhttp__internal__n_after_start_req_51;
}
case 'P': {
p++;
goto s_n_llhttp__internal__n_after_start_req_52;
}
default: {
goto s_n_llhttp__internal__n_error_112;
}
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_after_start_req_47:
s_n_llhttp__internal__n_after_start_req_47: {
if (p == endp) {
return s_n_llhttp__internal__n_after_start_req_47;
}
switch (*p) {
case 'E': {
p++;
goto s_n_llhttp__internal__n_after_start_req_48;
}
default: {
goto s_n_llhttp__internal__n_error_112;
}
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_after_start_req_55:
s_n_llhttp__internal__n_after_start_req_55: {
llparse_match_t match_seq;
if (p == endp) {
return s_n_llhttp__internal__n_after_start_req_55;
}
match_seq = llparse__match_sequence_id(state, p, endp, llparse_blob48, 3);
p = match_seq.current;
switch (match_seq.status) {
case kMatchComplete: {
p++;
match = 14;
goto s_n_llhttp__internal__n_invoke_store_method_1;
}
case kMatchPause: {
return s_n_llhttp__internal__n_after_start_req_55;
}
case kMatchMismatch: {
goto s_n_llhttp__internal__n_error_112;
}
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_after_start_req_57:
s_n_llhttp__internal__n_after_start_req_57: {
if (p == endp) {
return s_n_llhttp__internal__n_after_start_req_57;
}
switch (*p) {
case 'P': {
p++;
match = 37;
goto s_n_llhttp__internal__n_invoke_store_method_1;
}
default: {
goto s_n_llhttp__internal__n_error_112;
}
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_after_start_req_58:
s_n_llhttp__internal__n_after_start_req_58: {
llparse_match_t match_seq;
if (p == endp) {
return s_n_llhttp__internal__n_after_start_req_58;
}
match_seq = llparse__match_sequence_id(state, p, endp, llparse_blob49, 9);
p = match_seq.current;
switch (match_seq.status) {
case kMatchComplete: {
p++;
match = 42;
goto s_n_llhttp__internal__n_invoke_store_method_1;
}
case kMatchPause: {
return s_n_llhttp__internal__n_after_start_req_58;
}
case kMatchMismatch: {
goto s_n_llhttp__internal__n_error_112;
}
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_after_start_req_56:
s_n_llhttp__internal__n_after_start_req_56: {
if (p == endp) {
return s_n_llhttp__internal__n_after_start_req_56;
}
switch (*p) {
case 'U': {
p++;
goto s_n_llhttp__internal__n_after_start_req_57;
}
case '_': {
p++;
goto s_n_llhttp__internal__n_after_start_req_58;
}
default: {
goto s_n_llhttp__internal__n_error_112;
}
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_after_start_req_54:
s_n_llhttp__internal__n_after_start_req_54: {
if (p == endp) {
return s_n_llhttp__internal__n_after_start_req_54;
}
switch (*p) {
case 'A': {
p++;
goto s_n_llhttp__internal__n_after_start_req_55;
}
case 'T': {
p++;
goto s_n_llhttp__internal__n_after_start_req_56;
}
default: {
goto s_n_llhttp__internal__n_error_112;
}
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_after_start_req_59:
s_n_llhttp__internal__n_after_start_req_59: {
llparse_match_t match_seq;
if (p == endp) {
return s_n_llhttp__internal__n_after_start_req_59;
}
match_seq = llparse__match_sequence_id(state, p, endp, llparse_blob50, 4);
p = match_seq.current;
switch (match_seq.status) {
case kMatchComplete: {
p++;
match = 33;
goto s_n_llhttp__internal__n_invoke_store_method_1;
}
case kMatchPause: {
return s_n_llhttp__internal__n_after_start_req_59;
}
case kMatchMismatch: {
goto s_n_llhttp__internal__n_error_112;
}
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_after_start_req_60:
s_n_llhttp__internal__n_after_start_req_60: {
llparse_match_t match_seq;
if (p == endp) {
return s_n_llhttp__internal__n_after_start_req_60;
}
match_seq = llparse__match_sequence_id(state, p, endp, llparse_blob51, 7);
p = match_seq.current;
switch (match_seq.status) {
case kMatchComplete: {
p++;
match = 26;
goto s_n_llhttp__internal__n_invoke_store_method_1;
}
case kMatchPause: {
return s_n_llhttp__internal__n_after_start_req_60;
}
case kMatchMismatch: {
goto s_n_llhttp__internal__n_error_112;
}
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_after_start_req_53:
s_n_llhttp__internal__n_after_start_req_53: {
if (p == endp) {
return s_n_llhttp__internal__n_after_start_req_53;
}
switch (*p) {
case 'E': {
p++;
goto s_n_llhttp__internal__n_after_start_req_54;
}
case 'O': {
p++;
goto s_n_llhttp__internal__n_after_start_req_59;
}
case 'U': {
p++;
goto s_n_llhttp__internal__n_after_start_req_60;
}
default: {
goto s_n_llhttp__internal__n_error_112;
}
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_after_start_req_62:
s_n_llhttp__internal__n_after_start_req_62: {
llparse_match_t match_seq;
if (p == endp) {
return s_n_llhttp__internal__n_after_start_req_62;
}
match_seq = llparse__match_sequence_id(state, p, endp, llparse_blob52, 6);
p = match_seq.current;
switch (match_seq.status) {
case kMatchComplete: {
p++;
match = 40;
goto s_n_llhttp__internal__n_invoke_store_method_1;
}
case kMatchPause: {
return s_n_llhttp__internal__n_after_start_req_62;
}
case kMatchMismatch: {
goto s_n_llhttp__internal__n_error_112;
}
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_after_start_req_63:
s_n_llhttp__internal__n_after_start_req_63: {
llparse_match_t match_seq;
if (p == endp) {
return s_n_llhttp__internal__n_after_start_req_63;
}
match_seq = llparse__match_sequence_id(state, p, endp, llparse_blob53, 3);
p = match_seq.current;
switch (match_seq.status) {
case kMatchComplete: {
p++;
match = 7;
goto s_n_llhttp__internal__n_invoke_store_method_1;
}
case kMatchPause: {
return s_n_llhttp__internal__n_after_start_req_63;
}
case kMatchMismatch: {
goto s_n_llhttp__internal__n_error_112;
}
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_after_start_req_61:
s_n_llhttp__internal__n_after_start_req_61: {
if (p == endp) {
return s_n_llhttp__internal__n_after_start_req_61;
}
switch (*p) {
case 'E': {
p++;
goto s_n_llhttp__internal__n_after_start_req_62;
}
case 'R': {
p++;
goto s_n_llhttp__internal__n_after_start_req_63;
}
default: {
goto s_n_llhttp__internal__n_error_112;
}
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_after_start_req_66:
s_n_llhttp__internal__n_after_start_req_66: {
llparse_match_t match_seq;
if (p == endp) {
return s_n_llhttp__internal__n_after_start_req_66;
}
match_seq = llparse__match_sequence_id(state, p, endp, llparse_blob54, 3);
p = match_seq.current;
switch (match_seq.status) {
case kMatchComplete: {
p++;
match = 18;
goto s_n_llhttp__internal__n_invoke_store_method_1;
}
case kMatchPause: {
return s_n_llhttp__internal__n_after_start_req_66;
}
case kMatchMismatch: {
goto s_n_llhttp__internal__n_error_112;
}
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_after_start_req_68:
s_n_llhttp__internal__n_after_start_req_68: {
llparse_match_t match_seq;
if (p == endp) {
return s_n_llhttp__internal__n_after_start_req_68;
}
match_seq = llparse__match_sequence_id(state, p, endp, llparse_blob55, 2);
p = match_seq.current;
switch (match_seq.status) {
case kMatchComplete: {
p++;
match = 32;
goto s_n_llhttp__internal__n_invoke_store_method_1;
}
case kMatchPause: {
return s_n_llhttp__internal__n_after_start_req_68;
}
case kMatchMismatch: {
goto s_n_llhttp__internal__n_error_112;
}
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_after_start_req_69:
s_n_llhttp__internal__n_after_start_req_69: {
llparse_match_t match_seq;
if (p == endp) {
return s_n_llhttp__internal__n_after_start_req_69;
}
match_seq = llparse__match_sequence_id(state, p, endp, llparse_blob56, 2);
p = match_seq.current;
switch (match_seq.status) {
case kMatchComplete: {
p++;
match = 15;
goto s_n_llhttp__internal__n_invoke_store_method_1;
}
case kMatchPause: {
return s_n_llhttp__internal__n_after_start_req_69;
}
case kMatchMismatch: {
goto s_n_llhttp__internal__n_error_112;
}
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_after_start_req_67:
s_n_llhttp__internal__n_after_start_req_67: {
if (p == endp) {
return s_n_llhttp__internal__n_after_start_req_67;
}
switch (*p) {
case 'I': {
p++;
goto s_n_llhttp__internal__n_after_start_req_68;
}
case 'O': {
p++;
goto s_n_llhttp__internal__n_after_start_req_69;
}
default: {
goto s_n_llhttp__internal__n_error_112;
}
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_after_start_req_70:
s_n_llhttp__internal__n_after_start_req_70: {
llparse_match_t match_seq;
if (p == endp) {
return s_n_llhttp__internal__n_after_start_req_70;
}
match_seq = llparse__match_sequence_id(state, p, endp, llparse_blob57, 8);
p = match_seq.current;
switch (match_seq.status) {
case kMatchComplete: {
p++;
match = 27;
goto s_n_llhttp__internal__n_invoke_store_method_1;
}
case kMatchPause: {
return s_n_llhttp__internal__n_after_start_req_70;
}
case kMatchMismatch: {
goto s_n_llhttp__internal__n_error_112;
}
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_after_start_req_65:
s_n_llhttp__internal__n_after_start_req_65: {
if (p == endp) {
return s_n_llhttp__internal__n_after_start_req_65;
}
switch (*p) {
case 'B': {
p++;
goto s_n_llhttp__internal__n_after_start_req_66;
}
case 'L': {
p++;
goto s_n_llhttp__internal__n_after_start_req_67;
}
case 'S': {
p++;
goto s_n_llhttp__internal__n_after_start_req_70;
}
default: {
goto s_n_llhttp__internal__n_error_112;
}
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_after_start_req_64:
s_n_llhttp__internal__n_after_start_req_64: {
if (p == endp) {
return s_n_llhttp__internal__n_after_start_req_64;
}
switch (*p) {
case 'N': {
p++;
goto s_n_llhttp__internal__n_after_start_req_65;
}
default: {
goto s_n_llhttp__internal__n_error_112;
}
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_after_start_req:
s_n_llhttp__internal__n_after_start_req: {
if (p == endp) {
return s_n_llhttp__internal__n_after_start_req;
}
switch (*p) {
case 'A': {
p++;
goto s_n_llhttp__internal__n_after_start_req_1;
}
case 'B': {
p++;
goto s_n_llhttp__internal__n_after_start_req_4;
}
case 'C': {
p++;
goto s_n_llhttp__internal__n_after_start_req_5;
}
case 'D': {
p++;
goto s_n_llhttp__internal__n_after_start_req_10;
}
case 'F': {
p++;
goto s_n_llhttp__internal__n_after_start_req_14;
}
case 'G': {
p++;
goto s_n_llhttp__internal__n_after_start_req_15;
}
case 'H': {
p++;
goto s_n_llhttp__internal__n_after_start_req_18;
}
case 'L': {
p++;
goto s_n_llhttp__internal__n_after_start_req_19;
}
case 'M': {
p++;
goto s_n_llhttp__internal__n_after_start_req_22;
}
case 'N': {
p++;
goto s_n_llhttp__internal__n_after_start_req_31;
}
case 'O': {
p++;
goto s_n_llhttp__internal__n_after_start_req_32;
}
case 'P': {
p++;
goto s_n_llhttp__internal__n_after_start_req_33;
}
case 'Q': {
p++;
goto s_n_llhttp__internal__n_after_start_req_46;
}
case 'R': {
p++;
goto s_n_llhttp__internal__n_after_start_req_47;
}
case 'S': {
p++;
goto s_n_llhttp__internal__n_after_start_req_53;
}
case 'T': {
p++;
goto s_n_llhttp__internal__n_after_start_req_61;
}
case 'U': {
p++;
goto s_n_llhttp__internal__n_after_start_req_64;
}
default: {
goto s_n_llhttp__internal__n_error_112;
}
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_span_start_llhttp__on_method_1:
s_n_llhttp__internal__n_span_start_llhttp__on_method_1: {
if (p == endp) {
return s_n_llhttp__internal__n_span_start_llhttp__on_method_1;
}
state->_span_pos0 = (void*) p;
state->_span_cb0 = llhttp__on_method;
goto s_n_llhttp__internal__n_after_start_req;
UNREACHABLE;
}
case s_n_llhttp__internal__n_res_line_almost_done:
s_n_llhttp__internal__n_res_line_almost_done: {
if (p == endp) {
return s_n_llhttp__internal__n_res_line_almost_done;
}
switch (*p) {
case 10: {
p++;
goto s_n_llhttp__internal__n_invoke_llhttp__on_status_complete;
}
case 13: {
p++;
goto s_n_llhttp__internal__n_invoke_llhttp__on_status_complete;
}
default: {
goto s_n_llhttp__internal__n_invoke_test_lenient_flags_29;
}
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_invoke_test_lenient_flags_30:
s_n_llhttp__internal__n_invoke_test_lenient_flags_30: {
switch (llhttp__internal__c_test_lenient_flags_1(state, p, endp)) {
case 1:
goto s_n_llhttp__internal__n_invoke_llhttp__on_status_complete;
default:
goto s_n_llhttp__internal__n_error_98;
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_res_status:
s_n_llhttp__internal__n_res_status: {
if (p == endp) {
return s_n_llhttp__internal__n_res_status;
}
switch (*p) {
case 10: {
goto s_n_llhttp__internal__n_span_end_llhttp__on_status;
}
case 13: {
goto s_n_llhttp__internal__n_span_end_llhttp__on_status_1;
}
default: {
p++;
goto s_n_llhttp__internal__n_res_status;
}
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_span_start_llhttp__on_status:
s_n_llhttp__internal__n_span_start_llhttp__on_status: {
if (p == endp) {
return s_n_llhttp__internal__n_span_start_llhttp__on_status;
}
state->_span_pos0 = (void*) p;
state->_span_cb0 = llhttp__on_status;
goto s_n_llhttp__internal__n_res_status;
UNREACHABLE;
}
case s_n_llhttp__internal__n_res_status_code_otherwise:
s_n_llhttp__internal__n_res_status_code_otherwise: {
if (p == endp) {
return s_n_llhttp__internal__n_res_status_code_otherwise;
}
switch (*p) {
case 10: {
p++;
goto s_n_llhttp__internal__n_invoke_test_lenient_flags_28;
}
case 13: {
p++;
goto s_n_llhttp__internal__n_res_line_almost_done;
}
case ' ': {
p++;
goto s_n_llhttp__internal__n_span_start_llhttp__on_status;
}
default: {
goto s_n_llhttp__internal__n_error_99;
}
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_res_status_code_digit_3:
s_n_llhttp__internal__n_res_status_code_digit_3: {
if (p == endp) {
return s_n_llhttp__internal__n_res_status_code_digit_3;
}
switch (*p) {
case '0': {
p++;
match = 0;
goto s_n_llhttp__internal__n_invoke_mul_add_status_code_2;
}
case '1': {
p++;
match = 1;
goto s_n_llhttp__internal__n_invoke_mul_add_status_code_2;
}
case '2': {
p++;
match = 2;
goto s_n_llhttp__internal__n_invoke_mul_add_status_code_2;
}
case '3': {
p++;
match = 3;
goto s_n_llhttp__internal__n_invoke_mul_add_status_code_2;
}
case '4': {
p++;
match = 4;
goto s_n_llhttp__internal__n_invoke_mul_add_status_code_2;
}
case '5': {
p++;
match = 5;
goto s_n_llhttp__internal__n_invoke_mul_add_status_code_2;
}
case '6': {
p++;
match = 6;
goto s_n_llhttp__internal__n_invoke_mul_add_status_code_2;
}
case '7': {
p++;
match = 7;
goto s_n_llhttp__internal__n_invoke_mul_add_status_code_2;
}
case '8': {
p++;
match = 8;
goto s_n_llhttp__internal__n_invoke_mul_add_status_code_2;
}
case '9': {
p++;
match = 9;
goto s_n_llhttp__internal__n_invoke_mul_add_status_code_2;
}
default: {
goto s_n_llhttp__internal__n_error_101;
}
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_res_status_code_digit_2:
s_n_llhttp__internal__n_res_status_code_digit_2: {
if (p == endp) {
return s_n_llhttp__internal__n_res_status_code_digit_2;
}
switch (*p) {
case '0': {
p++;
match = 0;
goto s_n_llhttp__internal__n_invoke_mul_add_status_code_1;
}
case '1': {
p++;
match = 1;
goto s_n_llhttp__internal__n_invoke_mul_add_status_code_1;
}
case '2': {
p++;
match = 2;
goto s_n_llhttp__internal__n_invoke_mul_add_status_code_1;
}
case '3': {
p++;
match = 3;
goto s_n_llhttp__internal__n_invoke_mul_add_status_code_1;
}
case '4': {
p++;
match = 4;
goto s_n_llhttp__internal__n_invoke_mul_add_status_code_1;
}
case '5': {
p++;
match = 5;
goto s_n_llhttp__internal__n_invoke_mul_add_status_code_1;
}
case '6': {
p++;
match = 6;
goto s_n_llhttp__internal__n_invoke_mul_add_status_code_1;
}
case '7': {
p++;
match = 7;
goto s_n_llhttp__internal__n_invoke_mul_add_status_code_1;
}
case '8': {
p++;
match = 8;
goto s_n_llhttp__internal__n_invoke_mul_add_status_code_1;
}
case '9': {
p++;
match = 9;
goto s_n_llhttp__internal__n_invoke_mul_add_status_code_1;
}
default: {
goto s_n_llhttp__internal__n_error_103;
}
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_res_status_code_digit_1:
s_n_llhttp__internal__n_res_status_code_digit_1: {
if (p == endp) {
return s_n_llhttp__internal__n_res_status_code_digit_1;
}
switch (*p) {
case '0': {
p++;
match = 0;
goto s_n_llhttp__internal__n_invoke_mul_add_status_code;
}
case '1': {
p++;
match = 1;
goto s_n_llhttp__internal__n_invoke_mul_add_status_code;
}
case '2': {
p++;
match = 2;
goto s_n_llhttp__internal__n_invoke_mul_add_status_code;
}
case '3': {
p++;
match = 3;
goto s_n_llhttp__internal__n_invoke_mul_add_status_code;
}
case '4': {
p++;
match = 4;
goto s_n_llhttp__internal__n_invoke_mul_add_status_code;
}
case '5': {
p++;
match = 5;
goto s_n_llhttp__internal__n_invoke_mul_add_status_code;
}
case '6': {
p++;
match = 6;
goto s_n_llhttp__internal__n_invoke_mul_add_status_code;
}
case '7': {
p++;
match = 7;
goto s_n_llhttp__internal__n_invoke_mul_add_status_code;
}
case '8': {
p++;
match = 8;
goto s_n_llhttp__internal__n_invoke_mul_add_status_code;
}
case '9': {
p++;
match = 9;
goto s_n_llhttp__internal__n_invoke_mul_add_status_code;
}
default: {
goto s_n_llhttp__internal__n_error_105;
}
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_res_after_version:
s_n_llhttp__internal__n_res_after_version: {
if (p == endp) {
return s_n_llhttp__internal__n_res_after_version;
}
switch (*p) {
case ' ': {
p++;
goto s_n_llhttp__internal__n_invoke_update_status_code;
}
default: {
goto s_n_llhttp__internal__n_error_106;
}
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_invoke_llhttp__on_version_complete_1:
s_n_llhttp__internal__n_invoke_llhttp__on_version_complete_1: {
switch (llhttp__on_version_complete(state, p, endp)) {
case 0:
goto s_n_llhttp__internal__n_res_after_version;
case 21:
goto s_n_llhttp__internal__n_pause_28;
default:
goto s_n_llhttp__internal__n_error_94;
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_error_93:
s_n_llhttp__internal__n_error_93: {
state->error = 0x9;
state->reason = "Invalid HTTP version";
state->error_pos = (const char*) p;
state->_current = (void*) (intptr_t) s_error;
return s_error;
UNREACHABLE;
}
case s_n_llhttp__internal__n_error_107:
s_n_llhttp__internal__n_error_107: {
state->error = 0x9;
state->reason = "Invalid minor version";
state->error_pos = (const char*) p;
state->_current = (void*) (intptr_t) s_error;
return s_error;
UNREACHABLE;
}
case s_n_llhttp__internal__n_res_http_minor:
s_n_llhttp__internal__n_res_http_minor: {
if (p == endp) {
return s_n_llhttp__internal__n_res_http_minor;
}
switch (*p) {
case '0': {
p++;
match = 0;
goto s_n_llhttp__internal__n_invoke_store_http_minor_1;
}
case '1': {
p++;
match = 1;
goto s_n_llhttp__internal__n_invoke_store_http_minor_1;
}
case '2': {
p++;
match = 2;
goto s_n_llhttp__internal__n_invoke_store_http_minor_1;
}
case '3': {
p++;
match = 3;
goto s_n_llhttp__internal__n_invoke_store_http_minor_1;
}
case '4': {
p++;
match = 4;
goto s_n_llhttp__internal__n_invoke_store_http_minor_1;
}
case '5': {
p++;
match = 5;
goto s_n_llhttp__internal__n_invoke_store_http_minor_1;
}
case '6': {
p++;
match = 6;
goto s_n_llhttp__internal__n_invoke_store_http_minor_1;
}
case '7': {
p++;
match = 7;
goto s_n_llhttp__internal__n_invoke_store_http_minor_1;
}
case '8': {
p++;
match = 8;
goto s_n_llhttp__internal__n_invoke_store_http_minor_1;
}
case '9': {
p++;
match = 9;
goto s_n_llhttp__internal__n_invoke_store_http_minor_1;
}
default: {
goto s_n_llhttp__internal__n_span_end_llhttp__on_version_7;
}
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_error_108:
s_n_llhttp__internal__n_error_108: {
state->error = 0x9;
state->reason = "Expected dot";
state->error_pos = (const char*) p;
state->_current = (void*) (intptr_t) s_error;
return s_error;
UNREACHABLE;
}
case s_n_llhttp__internal__n_res_http_dot:
s_n_llhttp__internal__n_res_http_dot: {
if (p == endp) {
return s_n_llhttp__internal__n_res_http_dot;
}
switch (*p) {
case '.': {
p++;
goto s_n_llhttp__internal__n_res_http_minor;
}
default: {
goto s_n_llhttp__internal__n_span_end_llhttp__on_version_8;
}
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_error_109:
s_n_llhttp__internal__n_error_109: {
state->error = 0x9;
state->reason = "Invalid major version";
state->error_pos = (const char*) p;
state->_current = (void*) (intptr_t) s_error;
return s_error;
UNREACHABLE;
}
case s_n_llhttp__internal__n_res_http_major:
s_n_llhttp__internal__n_res_http_major: {
if (p == endp) {
return s_n_llhttp__internal__n_res_http_major;
}
switch (*p) {
case '0': {
p++;
match = 0;
goto s_n_llhttp__internal__n_invoke_store_http_major_1;
}
case '1': {
p++;
match = 1;
goto s_n_llhttp__internal__n_invoke_store_http_major_1;
}
case '2': {
p++;
match = 2;
goto s_n_llhttp__internal__n_invoke_store_http_major_1;
}
case '3': {
p++;
match = 3;
goto s_n_llhttp__internal__n_invoke_store_http_major_1;
}
case '4': {
p++;
match = 4;
goto s_n_llhttp__internal__n_invoke_store_http_major_1;
}
case '5': {
p++;
match = 5;
goto s_n_llhttp__internal__n_invoke_store_http_major_1;
}
case '6': {
p++;
match = 6;
goto s_n_llhttp__internal__n_invoke_store_http_major_1;
}
case '7': {
p++;
match = 7;
goto s_n_llhttp__internal__n_invoke_store_http_major_1;
}
case '8': {
p++;
match = 8;
goto s_n_llhttp__internal__n_invoke_store_http_major_1;
}
case '9': {
p++;
match = 9;
goto s_n_llhttp__internal__n_invoke_store_http_major_1;
}
default: {
goto s_n_llhttp__internal__n_span_end_llhttp__on_version_9;
}
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_span_start_llhttp__on_version_1:
s_n_llhttp__internal__n_span_start_llhttp__on_version_1: {
if (p == endp) {
return s_n_llhttp__internal__n_span_start_llhttp__on_version_1;
}
state->_span_pos0 = (void*) p;
state->_span_cb0 = llhttp__on_version;
goto s_n_llhttp__internal__n_res_http_major;
UNREACHABLE;
}
case s_n_llhttp__internal__n_res_after_protocol:
s_n_llhttp__internal__n_res_after_protocol: {
if (p == endp) {
return s_n_llhttp__internal__n_res_after_protocol;
}
switch (*p) {
case '/': {
p++;
goto s_n_llhttp__internal__n_span_start_llhttp__on_version_1;
}
default: {
goto s_n_llhttp__internal__n_error_114;
}
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_invoke_llhttp__on_protocol_complete_3:
s_n_llhttp__internal__n_invoke_llhttp__on_protocol_complete_3: {
switch (llhttp__on_protocol_complete(state, p, endp)) {
case 0:
goto s_n_llhttp__internal__n_res_after_protocol;
case 21:
goto s_n_llhttp__internal__n_pause_30;
default:
goto s_n_llhttp__internal__n_error_113;
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_error_115:
s_n_llhttp__internal__n_error_115: {
state->error = 0x8;
state->reason = "Expected HTTP/, RTSP/ or ICE/";
state->error_pos = (const char*) p;
state->_current = (void*) (intptr_t) s_error;
return s_error;
UNREACHABLE;
}
case s_n_llhttp__internal__n_res_after_start_1:
s_n_llhttp__internal__n_res_after_start_1: {
llparse_match_t match_seq;
if (p == endp) {
return s_n_llhttp__internal__n_res_after_start_1;
}
match_seq = llparse__match_sequence_id(state, p, endp, llparse_blob58, 3);
p = match_seq.current;
switch (match_seq.status) {
case kMatchComplete: {
p++;
goto s_n_llhttp__internal__n_span_end_llhttp__on_protocol_4;
}
case kMatchPause: {
return s_n_llhttp__internal__n_res_after_start_1;
}
case kMatchMismatch: {
goto s_n_llhttp__internal__n_span_end_llhttp__on_protocol_5;
}
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_res_after_start_2:
s_n_llhttp__internal__n_res_after_start_2: {
llparse_match_t match_seq;
if (p == endp) {
return s_n_llhttp__internal__n_res_after_start_2;
}
match_seq = llparse__match_sequence_id(state, p, endp, llparse_blob59, 2);
p = match_seq.current;
switch (match_seq.status) {
case kMatchComplete: {
p++;
goto s_n_llhttp__internal__n_span_end_llhttp__on_protocol_4;
}
case kMatchPause: {
return s_n_llhttp__internal__n_res_after_start_2;
}
case kMatchMismatch: {
goto s_n_llhttp__internal__n_span_end_llhttp__on_protocol_5;
}
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_res_after_start_3:
s_n_llhttp__internal__n_res_after_start_3: {
llparse_match_t match_seq;
if (p == endp) {
return s_n_llhttp__internal__n_res_after_start_3;
}
match_seq = llparse__match_sequence_id(state, p, endp, llparse_blob60, 3);
p = match_seq.current;
switch (match_seq.status) {
case kMatchComplete: {
p++;
goto s_n_llhttp__internal__n_span_end_llhttp__on_protocol_4;
}
case kMatchPause: {
return s_n_llhttp__internal__n_res_after_start_3;
}
case kMatchMismatch: {
goto s_n_llhttp__internal__n_span_end_llhttp__on_protocol_5;
}
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_res_after_start:
s_n_llhttp__internal__n_res_after_start: {
if (p == endp) {
return s_n_llhttp__internal__n_res_after_start;
}
switch (*p) {
case 'H': {
p++;
goto s_n_llhttp__internal__n_res_after_start_1;
}
case 'I': {
p++;
goto s_n_llhttp__internal__n_res_after_start_2;
}
case 'R': {
p++;
goto s_n_llhttp__internal__n_res_after_start_3;
}
default: {
goto s_n_llhttp__internal__n_span_end_llhttp__on_protocol_5;
}
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_span_start_llhttp__on_protocol_1:
s_n_llhttp__internal__n_span_start_llhttp__on_protocol_1: {
if (p == endp) {
return s_n_llhttp__internal__n_span_start_llhttp__on_protocol_1;
}
state->_span_pos0 = (void*) p;
state->_span_cb0 = llhttp__on_protocol;
goto s_n_llhttp__internal__n_res_after_start;
UNREACHABLE;
}
case s_n_llhttp__internal__n_invoke_llhttp__on_method_complete:
s_n_llhttp__internal__n_invoke_llhttp__on_method_complete: {
switch (llhttp__on_method_complete(state, p, endp)) {
case 0:
goto s_n_llhttp__internal__n_req_first_space_before_url;
case 21:
goto s_n_llhttp__internal__n_pause_26;
default:
goto s_n_llhttp__internal__n_error_1;
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_req_or_res_method_2:
s_n_llhttp__internal__n_req_or_res_method_2: {
llparse_match_t match_seq;
if (p == endp) {
return s_n_llhttp__internal__n_req_or_res_method_2;
}
match_seq = llparse__match_sequence_id(state, p, endp, llparse_blob61, 2);
p = match_seq.current;
switch (match_seq.status) {
case kMatchComplete: {
p++;
match = 2;
goto s_n_llhttp__internal__n_invoke_store_method;
}
case kMatchPause: {
return s_n_llhttp__internal__n_req_or_res_method_2;
}
case kMatchMismatch: {
goto s_n_llhttp__internal__n_error_110;
}
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_invoke_update_type_1:
s_n_llhttp__internal__n_invoke_update_type_1: {
switch (llhttp__internal__c_update_type_1(state, p, endp)) {
default:
goto s_n_llhttp__internal__n_span_start_llhttp__on_version_1;
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_req_or_res_method_3:
s_n_llhttp__internal__n_req_or_res_method_3: {
llparse_match_t match_seq;
if (p == endp) {
return s_n_llhttp__internal__n_req_or_res_method_3;
}
match_seq = llparse__match_sequence_id(state, p, endp, llparse_blob62, 3);
p = match_seq.current;
switch (match_seq.status) {
case kMatchComplete: {
p++;
goto s_n_llhttp__internal__n_span_end_llhttp__on_method_1;
}
case kMatchPause: {
return s_n_llhttp__internal__n_req_or_res_method_3;
}
case kMatchMismatch: {
goto s_n_llhttp__internal__n_error_110;
}
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_req_or_res_method_1:
s_n_llhttp__internal__n_req_or_res_method_1: {
if (p == endp) {
return s_n_llhttp__internal__n_req_or_res_method_1;
}
switch (*p) {
case 'E': {
p++;
goto s_n_llhttp__internal__n_req_or_res_method_2;
}
case 'T': {
p++;
goto s_n_llhttp__internal__n_req_or_res_method_3;
}
default: {
goto s_n_llhttp__internal__n_error_110;
}
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_req_or_res_method:
s_n_llhttp__internal__n_req_or_res_method: {
if (p == endp) {
return s_n_llhttp__internal__n_req_or_res_method;
}
switch (*p) {
case 'H': {
p++;
goto s_n_llhttp__internal__n_req_or_res_method_1;
}
default: {
goto s_n_llhttp__internal__n_error_110;
}
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_span_start_llhttp__on_method:
s_n_llhttp__internal__n_span_start_llhttp__on_method: {
if (p == endp) {
return s_n_llhttp__internal__n_span_start_llhttp__on_method;
}
state->_span_pos0 = (void*) p;
state->_span_cb0 = llhttp__on_method;
goto s_n_llhttp__internal__n_req_or_res_method;
UNREACHABLE;
}
case s_n_llhttp__internal__n_start_req_or_res:
s_n_llhttp__internal__n_start_req_or_res: {
if (p == endp) {
return s_n_llhttp__internal__n_start_req_or_res;
}
switch (*p) {
case 'H': {
goto s_n_llhttp__internal__n_span_start_llhttp__on_method;
}
default: {
goto s_n_llhttp__internal__n_invoke_update_type_2;
}
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_invoke_load_type:
s_n_llhttp__internal__n_invoke_load_type: {
switch (llhttp__internal__c_load_type(state, p, endp)) {
case 1:
goto s_n_llhttp__internal__n_span_start_llhttp__on_method_1;
case 2:
goto s_n_llhttp__internal__n_span_start_llhttp__on_protocol_1;
default:
goto s_n_llhttp__internal__n_start_req_or_res;
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_invoke_update_finish:
s_n_llhttp__internal__n_invoke_update_finish: {
switch (llhttp__internal__c_update_finish(state, p, endp)) {
default:
goto s_n_llhttp__internal__n_invoke_llhttp__on_message_begin;
}
UNREACHABLE;
}
case s_n_llhttp__internal__n_start:
s_n_llhttp__internal__n_start: {
if (p == endp) {
return s_n_llhttp__internal__n_start;
}
switch (*p) {
case 10: {
p++;
goto s_n_llhttp__internal__n_start;
}
case 13: {
p++;
goto s_n_llhttp__internal__n_start;
}
default: {
goto s_n_llhttp__internal__n_invoke_load_initial_message_completed;
}
}
UNREACHABLE;
}
default:
UNREACHABLE;
}
s_n_llhttp__internal__n_error_2: {
state->error = 0x7;
state->reason = "Invalid characters in url";
state->error_pos = (const char*) p;
state->_current = (void*) (intptr_t) s_error;
return s_error;
UNREACHABLE;
}
s_n_llhttp__internal__n_invoke_update_finish_2: {
switch (llhttp__internal__c_update_finish_1(state, p, endp)) {
default:
goto s_n_llhttp__internal__n_start;
}
UNREACHABLE;
}
s_n_llhttp__internal__n_invoke_update_initial_message_completed: {
switch (llhttp__internal__c_update_initial_message_completed(state, p, endp)) {
default:
goto s_n_llhttp__internal__n_invoke_update_finish_2;
}
UNREACHABLE;
}
s_n_llhttp__internal__n_invoke_update_content_length: {
switch (llhttp__internal__c_update_content_length(state, p, endp)) {
default:
goto s_n_llhttp__internal__n_invoke_update_initial_message_completed;
}
UNREACHABLE;
}
s_n_llhttp__internal__n_error_8: {
state->error = 0x5;
state->reason = "Data after `Connection: close`";
state->error_pos = (const char*) p;
state->_current = (void*) (intptr_t) s_error;
return s_error;
UNREACHABLE;
}
s_n_llhttp__internal__n_invoke_test_lenient_flags_3: {
switch (llhttp__internal__c_test_lenient_flags_3(state, p, endp)) {
case 1:
goto s_n_llhttp__internal__n_closed;
default:
goto s_n_llhttp__internal__n_error_8;
}
UNREACHABLE;
}
s_n_llhttp__internal__n_invoke_test_lenient_flags_2: {
switch (llhttp__internal__c_test_lenient_flags_2(state, p, endp)) {
case 1:
goto s_n_llhttp__internal__n_invoke_update_initial_message_completed;
default:
goto s_n_llhttp__internal__n_closed;
}
UNREACHABLE;
}
s_n_llhttp__internal__n_invoke_update_finish_1: {
switch (llhttp__internal__c_update_finish_1(state, p, endp)) {
default:
goto s_n_llhttp__internal__n_invoke_test_lenient_flags_2;
}
UNREACHABLE;
}
s_n_llhttp__internal__n_pause_13: {
state->error = 0x15;
state->reason = "on_message_complete pause";
state->error_pos = (const char*) p;
state->_current = (void*) (intptr_t) s_n_llhttp__internal__n_invoke_is_equal_upgrade;
return s_error;
UNREACHABLE;
}
s_n_llhttp__internal__n_error_38: {
state->error = 0x12;
state->reason = "`on_message_complete` callback error";
state->error_pos = (const char*) p;
state->_current = (void*) (intptr_t) s_error;
return s_error;
UNREACHABLE;
}
s_n_llhttp__internal__n_pause_15: {
state->error = 0x15;
state->reason = "on_chunk_complete pause";
state->error_pos = (const char*) p;
state->_current = (void*) (intptr_t) s_n_llhttp__internal__n_invoke_llhttp__on_message_complete_2;
return s_error;
UNREACHABLE;
}
s_n_llhttp__internal__n_error_40: {
state->error = 0x14;
state->reason = "`on_chunk_complete` callback error";
state->error_pos = (const char*) p;
state->_current = (void*) (intptr_t) s_error;
return s_error;
UNREACHABLE;
}
s_n_llhttp__internal__n_invoke_llhttp__on_chunk_complete_1: {
switch (llhttp__on_chunk_complete(state, p, endp)) {
case 0:
goto s_n_llhttp__internal__n_invoke_llhttp__on_message_complete_2;
case 21:
goto s_n_llhttp__internal__n_pause_15;
default:
goto s_n_llhttp__internal__n_error_40;
}
UNREACHABLE;
}
s_n_llhttp__internal__n_pause_2: {
state->error = 0x15;
state->reason = "on_message_complete pause";
state->error_pos = (const char*) p;
state->_current = (void*) (intptr_t) s_n_llhttp__internal__n_pause_1;
return s_error;
UNREACHABLE;
}
s_n_llhttp__internal__n_error_9: {
state->error = 0x12;
state->reason = "`on_message_complete` callback error";
state->error_pos = (const char*) p;
state->_current = (void*) (intptr_t) s_error;
return s_error;
UNREACHABLE;
}
s_n_llhttp__internal__n_invoke_llhttp__on_message_complete_1: {
switch (llhttp__on_message_complete(state, p, endp)) {
case 0:
goto s_n_llhttp__internal__n_pause_1;
case 21:
goto s_n_llhttp__internal__n_pause_2;
default:
goto s_n_llhttp__internal__n_error_9;
}
UNREACHABLE;
}
s_n_llhttp__internal__n_error_36: {
state->error = 0xc;
state->reason = "Chunk size overflow";
state->error_pos = (const char*) p;
state->_current = (void*) (intptr_t) s_error;
return s_error;
UNREACHABLE;
}
s_n_llhttp__internal__n_error_10: {
state->error = 0xc;
state->reason = "Invalid character in chunk size";
state->error_pos = (const char*) p;
state->_current = (void*) (intptr_t) s_error;
return s_error;
UNREACHABLE;
}
s_n_llhttp__internal__n_invoke_test_lenient_flags_4: {
switch (llhttp__internal__c_test_lenient_flags_4(state, p, endp)) {
case 1:
goto s_n_llhttp__internal__n_chunk_size_otherwise;
default:
goto s_n_llhttp__internal__n_error_10;
}
UNREACHABLE;
}
s_n_llhttp__internal__n_pause_3: {
state->error = 0x15;
state->reason = "on_chunk_complete pause";
state->error_pos = (const char*) p;
state->_current = (void*) (intptr_t) s_n_llhttp__internal__n_invoke_update_content_length_1;
return s_error;
UNREACHABLE;
}
s_n_llhttp__internal__n_error_14: {
state->error = 0x14;
state->reason = "`on_chunk_complete` callback error";
state->error_pos = (const char*) p;
state->_current = (void*) (intptr_t) s_error;
return s_error;
UNREACHABLE;
}
s_n_llhttp__internal__n_invoke_llhttp__on_chunk_complete: {
switch (llhttp__on_chunk_complete(state, p, endp)) {
case 0:
goto s_n_llhttp__internal__n_invoke_update_content_length_1;
case 21:
goto s_n_llhttp__internal__n_pause_3;
default:
goto s_n_llhttp__internal__n_error_14;
}
UNREACHABLE;
}
s_n_llhttp__internal__n_error_13: {
state->error = 0x19;
state->reason = "Missing expected CR after chunk data";
state->error_pos = (const char*) p;
state->_current = (void*) (intptr_t) s_error;
return s_error;
UNREACHABLE;
}
s_n_llhttp__internal__n_invoke_test_lenient_flags_6: {
switch (llhttp__internal__c_test_lenient_flags_1(state, p, endp)) {
case 1:
goto s_n_llhttp__internal__n_invoke_llhttp__on_chunk_complete;
default:
goto s_n_llhttp__internal__n_error_13;
}
UNREACHABLE;
}
s_n_llhttp__internal__n_error_15: {
state->error = 0x2;
state->reason = "Expected LF after chunk data";
state->error_pos = (const char*) p;
state->_current = (void*) (intptr_t) s_error;
return s_error;
UNREACHABLE;
}
s_n_llhttp__internal__n_invoke_test_lenient_flags_7: {
switch (llhttp__internal__c_test_lenient_flags_7(state, p, endp)) {
case 1:
goto s_n_llhttp__internal__n_invoke_llhttp__on_chunk_complete;
default:
goto s_n_llhttp__internal__n_error_15;
}
UNREACHABLE;
}
s_n_llhttp__internal__n_span_end_llhttp__on_body: {
const unsigned char* start;
int err;
start = state->_span_pos0;
state->_span_pos0 = NULL;
err = llhttp__on_body(state, start, p);
if (err != 0) {
state->error = err;
state->error_pos = (const char*) p;
state->_current = (void*) (intptr_t) s_n_llhttp__internal__n_chunk_data_almost_done;
return s_error;
}
goto s_n_llhttp__internal__n_chunk_data_almost_done;
UNREACHABLE;
}
s_n_llhttp__internal__n_invoke_or_flags: {
switch (llhttp__internal__c_or_flags(state, p, endp)) {
default:
goto s_n_llhttp__internal__n_header_field_start;
}
UNREACHABLE;
}
s_n_llhttp__internal__n_pause_4: {
state->error = 0x15;
state->reason = "on_chunk_header pause";
state->error_pos = (const char*) p;
state->_current = (void*) (intptr_t) s_n_llhttp__internal__n_invoke_is_equal_content_length;
return s_error;
UNREACHABLE;
}
s_n_llhttp__internal__n_error_12: {
state->error = 0x13;
state->reason = "`on_chunk_header` callback error";
state->error_pos = (const char*) p;
state->_current = (void*) (intptr_t) s_error;
return s_error;
UNREACHABLE;
}
s_n_llhttp__internal__n_invoke_llhttp__on_chunk_header: {
switch (llhttp__on_chunk_header(state, p, endp)) {
case 0:
goto s_n_llhttp__internal__n_invoke_is_equal_content_length;
case 21:
goto s_n_llhttp__internal__n_pause_4;
default:
goto s_n_llhttp__internal__n_error_12;
}
UNREACHABLE;
}
s_n_llhttp__internal__n_error_16: {
state->error = 0x2;
state->reason = "Expected LF after chunk size";
state->error_pos = (const char*) p;
state->_current = (void*) (intptr_t) s_error;
return s_error;
UNREACHABLE;
}
s_n_llhttp__internal__n_invoke_test_lenient_flags_8: {
switch (llhttp__internal__c_test_lenient_flags_8(state, p, endp)) {
case 1:
goto s_n_llhttp__internal__n_invoke_llhttp__on_chunk_header;
default:
goto s_n_llhttp__internal__n_error_16;
}
UNREACHABLE;
}
s_n_llhttp__internal__n_error_11: {
state->error = 0x19;
state->reason = "Missing expected CR after chunk size";
state->error_pos = (const char*) p;
state->_current = (void*) (intptr_t) s_error;
return s_error;
UNREACHABLE;
}
s_n_llhttp__internal__n_invoke_test_lenient_flags_5: {
switch (llhttp__internal__c_test_lenient_flags_1(state, p, endp)) {
case 1:
goto s_n_llhttp__internal__n_chunk_size_almost_done;
default:
goto s_n_llhttp__internal__n_error_11;
}
UNREACHABLE;
}
s_n_llhttp__internal__n_error_17: {
state->error = 0x2;
state->reason = "Invalid character in chunk extensions";
state->error_pos = (const char*) p;
state->_current = (void*) (intptr_t) s_error;
return s_error;
UNREACHABLE;
}
s_n_llhttp__internal__n_error_18: {
state->error = 0x2;
state->reason = "Invalid character in chunk extensions";
state->error_pos = (const char*) p;
state->_current = (void*) (intptr_t) s_error;
return s_error;
UNREACHABLE;
}
s_n_llhttp__internal__n_error_20: {
state->error = 0x19;
state->reason = "Missing expected CR after chunk extension name";
state->error_pos = (const char*) p;
state->_current = (void*) (intptr_t) s_error;
return s_error;
UNREACHABLE;
}
s_n_llhttp__internal__n_pause_5: {
state->error = 0x15;
state->reason = "on_chunk_extension_name pause";
state->error_pos = (const char*) p;
state->_current = (void*) (intptr_t) s_n_llhttp__internal__n_invoke_test_lenient_flags_9;
return s_error;
UNREACHABLE;
}
s_n_llhttp__internal__n_error_19: {
state->error = 0x22;
state->reason = "`on_chunk_extension_name` callback error";
state->error_pos = (const char*) p;
state->_current = (void*) (intptr_t) s_error;
return s_error;
UNREACHABLE;
}
s_n_llhttp__internal__n_span_end_llhttp__on_chunk_extension_name: {
const unsigned char* start;
int err;
start = state->_span_pos0;
state->_span_pos0 = NULL;
err = llhttp__on_chunk_extension_name(state, start, p);
if (err != 0) {
state->error = err;
state->error_pos = (const char*) p;
state->_current = (void*) (intptr_t) s_n_llhttp__internal__n_invoke_llhttp__on_chunk_extension_name_complete;
return s_error;
}
goto s_n_llhttp__internal__n_invoke_llhttp__on_chunk_extension_name_complete;
UNREACHABLE;
}
s_n_llhttp__internal__n_pause_6: {
state->error = 0x15;
state->reason = "on_chunk_extension_name pause";
state->error_pos = (const char*) p;
state->_current = (void*) (intptr_t) s_n_llhttp__internal__n_chunk_size_almost_done;
return s_error;
UNREACHABLE;
}
s_n_llhttp__internal__n_error_21: {
state->error = 0x22;
state->reason = "`on_chunk_extension_name` callback error";
state->error_pos = (const char*) p;
state->_current = (void*) (intptr_t) s_error;
return s_error;
UNREACHABLE;
}
s_n_llhttp__internal__n_span_end_llhttp__on_chunk_extension_name_1: {
const unsigned char* start;
int err;
start = state->_span_pos0;
state->_span_pos0 = NULL;
err = llhttp__on_chunk_extension_name(state, start, p);
if (err != 0) {
state->error = err;
state->error_pos = (const char*) (p + 1);
state->_current = (void*) (intptr_t) s_n_llhttp__internal__n_invoke_llhttp__on_chunk_extension_name_complete_1;
return s_error;
}
p++;
goto s_n_llhttp__internal__n_invoke_llhttp__on_chunk_extension_name_complete_1;
UNREACHABLE;
}
s_n_llhttp__internal__n_pause_7: {
state->error = 0x15;
state->reason = "on_chunk_extension_name pause";
state->error_pos = (const char*) p;
state->_current = (void*) (intptr_t) s_n_llhttp__internal__n_chunk_extensions;
return s_error;
UNREACHABLE;
}
s_n_llhttp__internal__n_error_22: {
state->error = 0x22;
state->reason = "`on_chunk_extension_name` callback error";
state->error_pos = (const char*) p;
state->_current = (void*) (intptr_t) s_error;
return s_error;
UNREACHABLE;
}
s_n_llhttp__internal__n_span_end_llhttp__on_chunk_extension_name_2: {
const unsigned char* start;
int err;
start = state->_span_pos0;
state->_span_pos0 = NULL;
err = llhttp__on_chunk_extension_name(state, start, p);
if (err != 0) {
state->error = err;
state->error_pos = (const char*) (p + 1);
state->_current = (void*) (intptr_t) s_n_llhttp__internal__n_invoke_llhttp__on_chunk_extension_name_complete_2;
return s_error;
}
p++;
goto s_n_llhttp__internal__n_invoke_llhttp__on_chunk_extension_name_complete_2;
UNREACHABLE;
}
s_n_llhttp__internal__n_error_25: {
state->error = 0x19;
state->reason = "Missing expected CR after chunk extension value";
state->error_pos = (const char*) p;
state->_current = (void*) (intptr_t) s_error;
return s_error;
UNREACHABLE;
}
s_n_llhttp__internal__n_pause_8: {
state->error = 0x15;
state->reason = "on_chunk_extension_value pause";
state->error_pos = (const char*) p;
state->_current = (void*) (intptr_t) s_n_llhttp__internal__n_invoke_test_lenient_flags_10;
return s_error;
UNREACHABLE;
}
s_n_llhttp__internal__n_error_24: {
state->error = 0x23;
state->reason = "`on_chunk_extension_value` callback error";
state->error_pos = (const char*) p;
state->_current = (void*) (intptr_t) s_error;
return s_error;
UNREACHABLE;
}
s_n_llhttp__internal__n_span_end_llhttp__on_chunk_extension_value: {
const unsigned char* start;
int err;
start = state->_span_pos0;
state->_span_pos0 = NULL;
err = llhttp__on_chunk_extension_value(state, start, p);
if (err != 0) {
state->error = err;
state->error_pos = (const char*) p;
state->_current = (void*) (intptr_t) s_n_llhttp__internal__n_invoke_llhttp__on_chunk_extension_value_complete;
return s_error;
}
goto s_n_llhttp__internal__n_invoke_llhttp__on_chunk_extension_value_complete;
UNREACHABLE;
}
s_n_llhttp__internal__n_pause_9: {
state->error = 0x15;
state->reason = "on_chunk_extension_value pause";
state->error_pos = (const char*) p;
state->_current = (void*) (intptr_t) s_n_llhttp__internal__n_chunk_size_almost_done;
return s_error;
UNREACHABLE;
}
s_n_llhttp__internal__n_error_26: {
state->error = 0x23;
state->reason = "`on_chunk_extension_value` callback error";
state->error_pos = (const char*) p;
state->_current = (void*) (intptr_t) s_error;
return s_error;
UNREACHABLE;
}
s_n_llhttp__internal__n_span_end_llhttp__on_chunk_extension_value_1: {
const unsigned char* start;
int err;
start = state->_span_pos0;
state->_span_pos0 = NULL;
err = llhttp__on_chunk_extension_value(state, start, p);
if (err != 0) {
state->error = err;
state->error_pos = (const char*) (p + 1);
state->_current = (void*) (intptr_t) s_n_llhttp__internal__n_invoke_llhttp__on_chunk_extension_value_complete_1;
return s_error;
}
p++;
goto s_n_llhttp__internal__n_invoke_llhttp__on_chunk_extension_value_complete_1;
UNREACHABLE;
}
s_n_llhttp__internal__n_error_28: {
state->error = 0x19;
state->reason = "Missing expected CR after chunk extension value";
state->error_pos = (const char*) p;
state->_current = (void*) (intptr_t) s_error;
return s_error;
UNREACHABLE;
}
s_n_llhttp__internal__n_invoke_test_lenient_flags_11: {
switch (llhttp__internal__c_test_lenient_flags_1(state, p, endp)) {
case 1:
goto s_n_llhttp__internal__n_chunk_size_almost_done;
default:
goto s_n_llhttp__internal__n_error_28;
}
UNREACHABLE;
}
s_n_llhttp__internal__n_error_29: {
state->error = 0x2;
state->reason = "Invalid character in chunk extensions quote value";
state->error_pos = (const char*) p;
state->_current = (void*) (intptr_t) s_error;
return s_error;
UNREACHABLE;
}
s_n_llhttp__internal__n_pause_10: {
state->error = 0x15;
state->reason = "on_chunk_extension_value pause";
state->error_pos = (const char*) p;
state->_current = (void*) (intptr_t) s_n_llhttp__internal__n_chunk_extension_quoted_value_done;
return s_error;
UNREACHABLE;
}
s_n_llhttp__internal__n_error_27: {
state->error = 0x23;
state->reason = "`on_chunk_extension_value` callback error";
state->error_pos = (const char*) p;
state->_current = (void*) (intptr_t) s_error;
return s_error;
UNREACHABLE;
}
s_n_llhttp__internal__n_span_end_llhttp__on_chunk_extension_value_2: {
const unsigned char* start;
int err;
start = state->_span_pos0;
state->_span_pos0 = NULL;
err = llhttp__on_chunk_extension_value(state, start, p);
if (err != 0) {
state->error = err;
state->error_pos = (const char*) p;
state->_current = (void*) (intptr_t) s_n_llhttp__internal__n_invoke_llhttp__on_chunk_extension_value_complete_2;
return s_error;
}
goto s_n_llhttp__internal__n_invoke_llhttp__on_chunk_extension_value_complete_2;
UNREACHABLE;
}
s_n_llhttp__internal__n_span_end_llhttp__on_chunk_extension_value_3: {
const unsigned char* start;
int err;
start = state->_span_pos0;
state->_span_pos0 = NULL;
err = llhttp__on_chunk_extension_value(state, start, p);
if (err != 0) {
state->error = err;
state->error_pos = (const char*) (p + 1);
state->_current = (void*) (intptr_t) s_n_llhttp__internal__n_error_30;
return s_error;
}
p++;
goto s_n_llhttp__internal__n_error_30;
UNREACHABLE;
}
s_n_llhttp__internal__n_span_end_llhttp__on_chunk_extension_value_4: {
const unsigned char* start;
int err;
start = state->_span_pos0;
state->_span_pos0 = NULL;
err = llhttp__on_chunk_extension_value(state, start, p);
if (err != 0) {
state->error = err;
state->error_pos = (const char*) (p + 1);
state->_current = (void*) (intptr_t) s_n_llhttp__internal__n_error_31;
return s_error;
}
p++;
goto s_n_llhttp__internal__n_error_31;
UNREACHABLE;
}
s_n_llhttp__internal__n_pause_11: {
state->error = 0x15;
state->reason = "on_chunk_extension_value pause";
state->error_pos = (const char*) p;
state->_current = (void*) (intptr_t) s_n_llhttp__internal__n_chunk_extensions;
return s_error;
UNREACHABLE;
}
s_n_llhttp__internal__n_error_32: {
state->error = 0x23;
state->reason = "`on_chunk_extension_value` callback error";
state->error_pos = (const char*) p;
state->_current = (void*) (intptr_t) s_error;
return s_error;
UNREACHABLE;
}
s_n_llhttp__internal__n_span_end_llhttp__on_chunk_extension_value_5: {
const unsigned char* start;
int err;
start = state->_span_pos0;
state->_span_pos0 = NULL;
err = llhttp__on_chunk_extension_value(state, start, p);
if (err != 0) {
state->error = err;
state->error_pos = (const char*) (p + 1);
state->_current = (void*) (intptr_t) s_n_llhttp__internal__n_invoke_llhttp__on_chunk_extension_value_complete_3;
return s_error;
}
p++;
goto s_n_llhttp__internal__n_invoke_llhttp__on_chunk_extension_value_complete_3;
UNREACHABLE;
}
s_n_llhttp__internal__n_span_end_llhttp__on_chunk_extension_value_6: {
const unsigned char* start;
int err;
start = state->_span_pos0;
state->_span_pos0 = NULL;
err = llhttp__on_chunk_extension_value(state, start, p);
if (err != 0) {
state->error = err;
state->error_pos = (const char*) (p + 1);
state->_current = (void*) (intptr_t) s_n_llhttp__internal__n_error_33;
return s_error;
}
p++;
goto s_n_llhttp__internal__n_error_33;
UNREACHABLE;
}
s_n_llhttp__internal__n_pause_12: {
state->error = 0x15;
state->reason = "on_chunk_extension_name pause";
state->error_pos = (const char*) p;
state->_current = (void*) (intptr_t) s_n_llhttp__internal__n_chunk_extension_value;
return s_error;
UNREACHABLE;
}
s_n_llhttp__internal__n_error_23: {
state->error = 0x22;
state->reason = "`on_chunk_extension_name` callback error";
state->error_pos = (const char*) p;
state->_current = (void*) (intptr_t) s_error;
return s_error;
UNREACHABLE;
}
s_n_llhttp__internal__n_invoke_llhttp__on_chunk_extension_name_complete_3: {
switch (llhttp__on_chunk_extension_name_complete(state, p, endp)) {
case 0:
goto s_n_llhttp__internal__n_chunk_extension_value;
case 21:
goto s_n_llhttp__internal__n_pause_12;
default:
goto s_n_llhttp__internal__n_error_23;
}
UNREACHABLE;
}
s_n_llhttp__internal__n_span_end_llhttp__on_chunk_extension_name_3: {
const unsigned char* start;
int err;
start = state->_span_pos0;
state->_span_pos0 = NULL;
err = llhttp__on_chunk_extension_name(state, start, p);
if (err != 0) {
state->error = err;
state->error_pos = (const char*) (p + 1);
state->_current = (void*) (intptr_t) s_n_llhttp__internal__n_span_start_llhttp__on_chunk_extension_value;
return s_error;
}
p++;
goto s_n_llhttp__internal__n_span_start_llhttp__on_chunk_extension_value;
UNREACHABLE;
}
s_n_llhttp__internal__n_span_end_llhttp__on_chunk_extension_name_4: {
const unsigned char* start;
int err;
start = state->_span_pos0;
state->_span_pos0 = NULL;
err = llhttp__on_chunk_extension_name(state, start, p);
if (err != 0) {
state->error = err;
state->error_pos = (const char*) (p + 1);
state->_current = (void*) (intptr_t) s_n_llhttp__internal__n_error_34;
return s_error;
}
p++;
goto s_n_llhttp__internal__n_error_34;
UNREACHABLE;
}
s_n_llhttp__internal__n_error_35: {
state->error = 0xc;
state->reason = "Invalid character in chunk size";
state->error_pos = (const char*) p;
state->_current = (void*) (intptr_t) s_error;
return s_error;
UNREACHABLE;
}
s_n_llhttp__internal__n_invoke_mul_add_content_length: {
switch (llhttp__internal__c_mul_add_content_length(state, p, endp, match)) {
case 1:
goto s_n_llhttp__internal__n_error_36;
default:
goto s_n_llhttp__internal__n_chunk_size;
}
UNREACHABLE;
}
s_n_llhttp__internal__n_error_37: {
state->error = 0xc;
state->reason = "Invalid character in chunk size";
state->error_pos = (const char*) p;
state->_current = (void*) (intptr_t) s_error;
return s_error;
UNREACHABLE;
}
s_n_llhttp__internal__n_span_end_llhttp__on_body_1: {
const unsigned char* start;
int err;
start = state->_span_pos0;
state->_span_pos0 = NULL;
err = llhttp__on_body(state, start, p);
if (err != 0) {
state->error = err;
state->error_pos = (const char*) p;
state->_current = (void*) (intptr_t) s_n_llhttp__internal__n_invoke_llhttp__on_message_complete_2;
return s_error;
}
goto s_n_llhttp__internal__n_invoke_llhttp__on_message_complete_2;
UNREACHABLE;
}
s_n_llhttp__internal__n_invoke_update_finish_3: {
switch (llhttp__internal__c_update_finish_3(state, p, endp)) {
default:
goto s_n_llhttp__internal__n_span_start_llhttp__on_body_2;
}
UNREACHABLE;
}
s_n_llhttp__internal__n_error_39: {
state->error = 0xf;
state->reason = "Request has invalid `Transfer-Encoding`";
state->error_pos = (const char*) p;
state->_current = (void*) (intptr_t) s_error;
return s_error;
UNREACHABLE;
}
s_n_llhttp__internal__n_pause: {
state->error = 0x15;
state->reason = "on_message_complete pause";
state->error_pos = (const char*) p;
state->_current = (void*) (intptr_t) s_n_llhttp__internal__n_invoke_llhttp__after_message_complete;
return s_error;
UNREACHABLE;
}
s_n_llhttp__internal__n_error_7: {
state->error = 0x12;
state->reason = "`on_message_complete` callback error";
state->error_pos = (const char*) p;
state->_current = (void*) (intptr_t) s_error;
return s_error;
UNREACHABLE;
}
s_n_llhttp__internal__n_invoke_llhttp__on_message_complete: {
switch (llhttp__on_message_complete(state, p, endp)) {
case 0:
goto s_n_llhttp__internal__n_invoke_llhttp__after_message_complete;
case 21:
goto s_n_llhttp__internal__n_pause;
default:
goto s_n_llhttp__internal__n_error_7;
}
UNREACHABLE;
}
s_n_llhttp__internal__n_invoke_or_flags_1: {
switch (llhttp__internal__c_or_flags_1(state, p, endp)) {
default:
goto s_n_llhttp__internal__n_invoke_llhttp__after_headers_complete;
}
UNREACHABLE;
}
s_n_llhttp__internal__n_invoke_or_flags_2: {
switch (llhttp__internal__c_or_flags_1(state, p, endp)) {
default:
goto s_n_llhttp__internal__n_invoke_llhttp__after_headers_complete;
}
UNREACHABLE;
}
s_n_llhttp__internal__n_invoke_update_upgrade: {
switch (llhttp__internal__c_update_upgrade(state, p, endp)) {
default:
goto s_n_llhttp__internal__n_invoke_or_flags_2;
}
UNREACHABLE;
}
s_n_llhttp__internal__n_pause_14: {
state->error = 0x15;
state->reason = "Paused by on_headers_complete";
state->error_pos = (const char*) p;
state->_current = (void*) (intptr_t) s_n_llhttp__internal__n_invoke_llhttp__after_headers_complete;
return s_error;
UNREACHABLE;
}
s_n_llhttp__internal__n_error_6: {
state->error = 0x11;
state->reason = "User callback error";
state->error_pos = (const char*) p;
state->_current = (void*) (intptr_t) s_error;
return s_error;
UNREACHABLE;
}
s_n_llhttp__internal__n_invoke_llhttp__on_headers_complete: {
switch (llhttp__on_headers_complete(state, p, endp)) {
case 0:
goto s_n_llhttp__internal__n_invoke_llhttp__after_headers_complete;
case 1:
goto s_n_llhttp__internal__n_invoke_or_flags_1;
case 2:
goto s_n_llhttp__internal__n_invoke_update_upgrade;
case 21:
goto s_n_llhttp__internal__n_pause_14;
default:
goto s_n_llhttp__internal__n_error_6;
}
UNREACHABLE;
}
s_n_llhttp__internal__n_invoke_llhttp__before_headers_complete: {
switch (llhttp__before_headers_complete(state, p, endp)) {
default:
goto s_n_llhttp__internal__n_invoke_llhttp__on_headers_complete;
}
UNREACHABLE;
}
s_n_llhttp__internal__n_invoke_test_flags: {
switch (llhttp__internal__c_test_flags(state, p, endp)) {
case 1:
goto s_n_llhttp__internal__n_invoke_llhttp__on_chunk_complete_1;
default:
goto s_n_llhttp__internal__n_invoke_llhttp__before_headers_complete;
}
UNREACHABLE;
}
s_n_llhttp__internal__n_invoke_test_lenient_flags_1: {
switch (llhttp__internal__c_test_lenient_flags_1(state, p, endp)) {
case 1:
goto s_n_llhttp__internal__n_invoke_test_flags;
default:
goto s_n_llhttp__internal__n_error_5;
}
UNREACHABLE;
}
s_n_llhttp__internal__n_pause_17: {
state->error = 0x15;
state->reason = "on_chunk_complete pause";
state->error_pos = (const char*) p;
state->_current = (void*) (intptr_t) s_n_llhttp__internal__n_invoke_llhttp__on_message_complete_2;
return s_error;
UNREACHABLE;
}
s_n_llhttp__internal__n_error_42: {
state->error = 0x14;
state->reason = "`on_chunk_complete` callback error";
state->error_pos = (const char*) p;
state->_current = (void*) (intptr_t) s_error;
return s_error;
UNREACHABLE;
}
s_n_llhttp__internal__n_invoke_llhttp__on_chunk_complete_2: {
switch (llhttp__on_chunk_complete(state, p, endp)) {
case 0:
goto s_n_llhttp__internal__n_invoke_llhttp__on_message_complete_2;
case 21:
goto s_n_llhttp__internal__n_pause_17;
default:
goto s_n_llhttp__internal__n_error_42;
}
UNREACHABLE;
}
s_n_llhttp__internal__n_invoke_or_flags_3: {
switch (llhttp__internal__c_or_flags_1(state, p, endp)) {
default:
goto s_n_llhttp__internal__n_invoke_llhttp__after_headers_complete;
}
UNREACHABLE;
}
s_n_llhttp__internal__n_invoke_or_flags_4: {
switch (llhttp__internal__c_or_flags_1(state, p, endp)) {
default:
goto s_n_llhttp__internal__n_invoke_llhttp__after_headers_complete;
}
UNREACHABLE;
}
s_n_llhttp__internal__n_invoke_update_upgrade_1: {
switch (llhttp__internal__c_update_upgrade(state, p, endp)) {
default:
goto s_n_llhttp__internal__n_invoke_or_flags_4;
}
UNREACHABLE;
}
s_n_llhttp__internal__n_pause_16: {
state->error = 0x15;
state->reason = "Paused by on_headers_complete";
state->error_pos = (const char*) p;
state->_current = (void*) (intptr_t) s_n_llhttp__internal__n_invoke_llhttp__after_headers_complete;
return s_error;
UNREACHABLE;
}
s_n_llhttp__internal__n_error_41: {
state->error = 0x11;
state->reason = "User callback error";
state->error_pos = (const char*) p;
state->_current = (void*) (intptr_t) s_error;
return s_error;
UNREACHABLE;
}
s_n_llhttp__internal__n_invoke_llhttp__on_headers_complete_1: {
switch (llhttp__on_headers_complete(state, p, endp)) {
case 0:
goto s_n_llhttp__internal__n_invoke_llhttp__after_headers_complete;
case 1:
goto s_n_llhttp__internal__n_invoke_or_flags_3;
case 2:
goto s_n_llhttp__internal__n_invoke_update_upgrade_1;
case 21:
goto s_n_llhttp__internal__n_pause_16;
default:
goto s_n_llhttp__internal__n_error_41;
}
UNREACHABLE;
}
s_n_llhttp__internal__n_invoke_llhttp__before_headers_complete_1: {
switch (llhttp__before_headers_complete(state, p, endp)) {
default:
goto s_n_llhttp__internal__n_invoke_llhttp__on_headers_complete_1;
}
UNREACHABLE;
}
s_n_llhttp__internal__n_invoke_test_flags_1: {
switch (llhttp__internal__c_test_flags(state, p, endp)) {
case 1:
goto s_n_llhttp__internal__n_invoke_llhttp__on_chunk_complete_2;
default:
goto s_n_llhttp__internal__n_invoke_llhttp__before_headers_complete_1;
}
UNREACHABLE;
}
s_n_llhttp__internal__n_error_43: {
state->error = 0x2;
state->reason = "Expected LF after headers";
state->error_pos = (const char*) p;
state->_current = (void*) (intptr_t) s_error;
return s_error;
UNREACHABLE;
}
s_n_llhttp__internal__n_invoke_test_lenient_flags_12: {
switch (llhttp__internal__c_test_lenient_flags_8(state, p, endp)) {
case 1:
goto s_n_llhttp__internal__n_invoke_test_flags_1;
default:
goto s_n_llhttp__internal__n_error_43;
}
UNREACHABLE;
}
s_n_llhttp__internal__n_error_44: {
state->error = 0xa;
state->reason = "Invalid header token";
state->error_pos = (const char*) p;
state->_current = (void*) (intptr_t) s_error;
return s_error;
UNREACHABLE;
}
s_n_llhttp__internal__n_span_end_llhttp__on_header_field: {
const unsigned char* start;
int err;
start = state->_span_pos0;
state->_span_pos0 = NULL;
err = llhttp__on_header_field(state, start, p);
if (err != 0) {
state->error = err;
state->error_pos = (const char*) (p + 1);
state->_current = (void*) (intptr_t) s_n_llhttp__internal__n_error_5;
return s_error;
}
p++;
goto s_n_llhttp__internal__n_error_5;
UNREACHABLE;
}
s_n_llhttp__internal__n_invoke_test_lenient_flags_13: {
switch (llhttp__internal__c_test_lenient_flags(state, p, endp)) {
case 1:
goto s_n_llhttp__internal__n_header_field_colon_discard_ws;
default:
goto s_n_llhttp__internal__n_span_end_llhttp__on_header_field;
}
UNREACHABLE;
}
s_n_llhttp__internal__n_error_60: {
state->error = 0xb;
state->reason = "Content-Length can't be present with Transfer-Encoding";
state->error_pos = (const char*) p;
state->_current = (void*) (intptr_t) s_error;
return s_error;
UNREACHABLE;
}
s_n_llhttp__internal__n_error_47: {
state->error = 0xa;
state->reason = "Invalid header value char";
state->error_pos = (const char*) p;
state->_current = (void*) (intptr_t) s_error;
return s_error;
UNREACHABLE;
}
s_n_llhttp__internal__n_invoke_test_lenient_flags_15: {
switch (llhttp__internal__c_test_lenient_flags(state, p, endp)) {
case 1:
goto s_n_llhttp__internal__n_header_value_discard_ws;
default:
goto s_n_llhttp__internal__n_error_47;
}
UNREACHABLE;
}
s_n_llhttp__internal__n_error_49: {
state->error = 0xb;
state->reason = "Empty Content-Length";
state->error_pos = (const char*) p;
state->_current = (void*) (intptr_t) s_error;
return s_error;
UNREACHABLE;
}
s_n_llhttp__internal__n_pause_18: {
state->error = 0x15;
state->reason = "on_header_value_complete pause";
state->error_pos = (const char*) p;
state->_current = (void*) (intptr_t) s_n_llhttp__internal__n_header_field_start;
return s_error;
UNREACHABLE;
}
s_n_llhttp__internal__n_error_48: {
state->error = 0x1d;
state->reason = "`on_header_value_complete` callback error";
state->error_pos = (const char*) p;
state->_current = (void*) (intptr_t) s_error;
return s_error;
UNREACHABLE;
}
s_n_llhttp__internal__n_span_end_llhttp__on_header_value: {
const unsigned char* start;
int err;
start = state->_span_pos0;
state->_span_pos0 = NULL;
err = llhttp__on_header_value(state, start, p);
if (err != 0) {
state->error = err;
state->error_pos = (const char*) p;
state->_current = (void*) (intptr_t) s_n_llhttp__internal__n_invoke_llhttp__on_header_value_complete;
return s_error;
}
goto s_n_llhttp__internal__n_invoke_llhttp__on_header_value_complete;
UNREACHABLE;
}
s_n_llhttp__internal__n_invoke_update_header_state: {
switch (llhttp__internal__c_update_header_state(state, p, endp)) {
default:
goto s_n_llhttp__internal__n_span_start_llhttp__on_header_value;
}
UNREACHABLE;
}
s_n_llhttp__internal__n_invoke_or_flags_5: {
switch (llhttp__internal__c_or_flags_5(state, p, endp)) {
default:
goto s_n_llhttp__internal__n_invoke_update_header_state;
}
UNREACHABLE;
}
s_n_llhttp__internal__n_invoke_or_flags_6: {
switch (llhttp__internal__c_or_flags_6(state, p, endp)) {
default:
goto s_n_llhttp__internal__n_invoke_update_header_state;
}
UNREACHABLE;
}
s_n_llhttp__internal__n_invoke_or_flags_7: {
switch (llhttp__internal__c_or_flags_7(state, p, endp)) {
default:
goto s_n_llhttp__internal__n_invoke_update_header_state;
}
UNREACHABLE;
}
s_n_llhttp__internal__n_invoke_or_flags_8: {
switch (llhttp__internal__c_or_flags_8(state, p, endp)) {
default:
goto s_n_llhttp__internal__n_span_start_llhttp__on_header_value;
}
UNREACHABLE;
}
s_n_llhttp__internal__n_invoke_load_header_state_2: {
switch (llhttp__internal__c_load_header_state(state, p, endp)) {
case 5:
goto s_n_llhttp__internal__n_invoke_or_flags_5;
case 6:
goto s_n_llhttp__internal__n_invoke_or_flags_6;
case 7:
goto s_n_llhttp__internal__n_invoke_or_flags_7;
case 8:
goto s_n_llhttp__internal__n_invoke_or_flags_8;
default:
goto s_n_llhttp__internal__n_span_start_llhttp__on_header_value;
}
UNREACHABLE;
}
s_n_llhttp__internal__n_invoke_load_header_state_1: {
switch (llhttp__internal__c_load_header_state(state, p, endp)) {
case 2:
goto s_n_llhttp__internal__n_error_49;
default:
goto s_n_llhttp__internal__n_invoke_load_header_state_2;
}
UNREACHABLE;
}
s_n_llhttp__internal__n_error_46: {
state->error = 0xa;
state->reason = "Invalid header value char";
state->error_pos = (const char*) p;
state->_current = (void*) (intptr_t) s_error;
return s_error;
UNREACHABLE;
}
s_n_llhttp__internal__n_invoke_test_lenient_flags_14: {
switch (llhttp__internal__c_test_lenient_flags_1(state, p, endp)) {
case 1:
goto s_n_llhttp__internal__n_header_value_discard_lws;
default:
goto s_n_llhttp__internal__n_error_46;
}
UNREACHABLE;
}
s_n_llhttp__internal__n_error_50: {
state->error = 0x2;
state->reason = "Expected LF after CR";
state->error_pos = (const char*) p;
state->_current = (void*) (intptr_t) s_error;
return s_error;
UNREACHABLE;
}
s_n_llhttp__internal__n_invoke_test_lenient_flags_16: {
switch (llhttp__internal__c_test_lenient_flags(state, p, endp)) {
case 1:
goto s_n_llhttp__internal__n_header_value_discard_lws;
default:
goto s_n_llhttp__internal__n_error_50;
}
UNREACHABLE;
}
s_n_llhttp__internal__n_invoke_update_header_state_1: {
switch (llhttp__internal__c_update_header_state_1(state, p, endp)) {
default:
goto s_n_llhttp__internal__n_span_start_llhttp__on_header_value_1;
}
UNREACHABLE;
}
s_n_llhttp__internal__n_invoke_load_header_state_4: {
switch (llhttp__internal__c_load_header_state(state, p, endp)) {
case 8:
goto s_n_llhttp__internal__n_invoke_update_header_state_1;
default:
goto s_n_llhttp__internal__n_span_start_llhttp__on_header_value_1;
}
UNREACHABLE;
}
s_n_llhttp__internal__n_error_52: {
state->error = 0xa;
state->reason = "Unexpected whitespace after header value";
state->error_pos = (const char*) p;
state->_current = (void*) (intptr_t) s_error;
return s_error;
UNREACHABLE;
}
s_n_llhttp__internal__n_invoke_test_lenient_flags_18: {
switch (llhttp__internal__c_test_lenient_flags(state, p, endp)) {
case 1:
goto s_n_llhttp__internal__n_invoke_load_header_state_4;
default:
goto s_n_llhttp__internal__n_error_52;
}
UNREACHABLE;
}
s_n_llhttp__internal__n_invoke_update_header_state_2: {
switch (llhttp__internal__c_update_header_state(state, p, endp)) {
default:
goto s_n_llhttp__internal__n_invoke_llhttp__on_header_value_complete;
}
UNREACHABLE;
}
s_n_llhttp__internal__n_invoke_or_flags_9: {
switch (llhttp__internal__c_or_flags_5(state, p, endp)) {
default:
goto s_n_llhttp__internal__n_invoke_update_header_state_2;
}
UNREACHABLE;
}
s_n_llhttp__internal__n_invoke_or_flags_10: {
switch (llhttp__internal__c_or_flags_6(state, p, endp)) {
default:
goto s_n_llhttp__internal__n_invoke_update_header_state_2;
}
UNREACHABLE;
}
s_n_llhttp__internal__n_invoke_or_flags_11: {
switch (llhttp__internal__c_or_flags_7(state, p, endp)) {
default:
goto s_n_llhttp__internal__n_invoke_update_header_state_2;
}
UNREACHABLE;
}
s_n_llhttp__internal__n_invoke_or_flags_12: {
switch (llhttp__internal__c_or_flags_8(state, p, endp)) {
default:
goto s_n_llhttp__internal__n_invoke_llhttp__on_header_value_complete;
}
UNREACHABLE;
}
s_n_llhttp__internal__n_invoke_load_header_state_5: {
switch (llhttp__internal__c_load_header_state(state, p, endp)) {
case 5:
goto s_n_llhttp__internal__n_invoke_or_flags_9;
case 6:
goto s_n_llhttp__internal__n_invoke_or_flags_10;
case 7:
goto s_n_llhttp__internal__n_invoke_or_flags_11;
case 8:
goto s_n_llhttp__internal__n_invoke_or_flags_12;
default:
goto s_n_llhttp__internal__n_invoke_llhttp__on_header_value_complete;
}
UNREACHABLE;
}
s_n_llhttp__internal__n_error_53: {
state->error = 0x3;
state->reason = "Missing expected LF after header value";
state->error_pos = (const char*) p;
state->_current = (void*) (intptr_t) s_error;
return s_error;
UNREACHABLE;
}
s_n_llhttp__internal__n_error_51: {
state->error = 0x19;
state->reason = "Missing expected CR after header value";
state->error_pos = (const char*) p;
state->_current = (void*) (intptr_t) s_error;
return s_error;
UNREACHABLE;
}
s_n_llhttp__internal__n_span_end_llhttp__on_header_value_1: {
const unsigned char* start;
int err;
start = state->_span_pos0;
state->_span_pos0 = NULL;
err = llhttp__on_header_value(state, start, p);
if (err != 0) {
state->error = err;
state->error_pos = (const char*) p;
state->_current = (void*) (intptr_t) s_n_llhttp__internal__n_invoke_test_lenient_flags_17;
return s_error;
}
goto s_n_llhttp__internal__n_invoke_test_lenient_flags_17;
UNREACHABLE;
}
s_n_llhttp__internal__n_span_end_llhttp__on_header_value_2: {
const unsigned char* start;
int err;
start = state->_span_pos0;
state->_span_pos0 = NULL;
err = llhttp__on_header_value(state, start, p);
if (err != 0) {
state->error = err;
state->error_pos = (const char*) (p + 1);
state->_current = (void*) (intptr_t) s_n_llhttp__internal__n_header_value_almost_done;
return s_error;
}
p++;
goto s_n_llhttp__internal__n_header_value_almost_done;
UNREACHABLE;
}
s_n_llhttp__internal__n_span_end_llhttp__on_header_value_4: {
const unsigned char* start;
int err;
start = state->_span_pos0;
state->_span_pos0 = NULL;
err = llhttp__on_header_value(state, start, p);
if (err != 0) {
state->error = err;
state->error_pos = (const char*) p;
state->_current = (void*) (intptr_t) s_n_llhttp__internal__n_header_value_almost_done;
return s_error;
}
goto s_n_llhttp__internal__n_header_value_almost_done;
UNREACHABLE;
}
s_n_llhttp__internal__n_span_end_llhttp__on_header_value_5: {
const unsigned char* start;
int err;
start = state->_span_pos0;
state->_span_pos0 = NULL;
err = llhttp__on_header_value(state, start, p);
if (err != 0) {
state->error = err;
state->error_pos = (const char*) (p + 1);
state->_current = (void*) (intptr_t) s_n_llhttp__internal__n_header_value_almost_done;
return s_error;
}
p++;
goto s_n_llhttp__internal__n_header_value_almost_done;
UNREACHABLE;
}
s_n_llhttp__internal__n_span_end_llhttp__on_header_value_3: {
const unsigned char* start;
int err;
start = state->_span_pos0;
state->_span_pos0 = NULL;
err = llhttp__on_header_value(state, start, p);
if (err != 0) {
state->error = err;
state->error_pos = (const char*) p;
state->_current = (void*) (intptr_t) s_n_llhttp__internal__n_error_54;
return s_error;
}
goto s_n_llhttp__internal__n_error_54;
UNREACHABLE;
}
s_n_llhttp__internal__n_invoke_test_lenient_flags_19: {
switch (llhttp__internal__c_test_lenient_flags(state, p, endp)) {
case 1:
goto s_n_llhttp__internal__n_header_value_lenient;
default:
goto s_n_llhttp__internal__n_span_end_llhttp__on_header_value_3;
}
UNREACHABLE;
}
s_n_llhttp__internal__n_invoke_update_header_state_4: {
switch (llhttp__internal__c_update_header_state(state, p, endp)) {
default:
goto s_n_llhttp__internal__n_header_value_connection;
}
UNREACHABLE;
}
s_n_llhttp__internal__n_invoke_or_flags_13: {
switch (llhttp__internal__c_or_flags_5(state, p, endp)) {
default:
goto s_n_llhttp__internal__n_invoke_update_header_state_4;
}
UNREACHABLE;
}
s_n_llhttp__internal__n_invoke_or_flags_14: {
switch (llhttp__internal__c_or_flags_6(state, p, endp)) {
default:
goto s_n_llhttp__internal__n_invoke_update_header_state_4;
}
UNREACHABLE;
}
s_n_llhttp__internal__n_invoke_or_flags_15: {
switch (llhttp__internal__c_or_flags_7(state, p, endp)) {
default:
goto s_n_llhttp__internal__n_invoke_update_header_state_4;
}
UNREACHABLE;
}
s_n_llhttp__internal__n_invoke_or_flags_16: {
switch (llhttp__internal__c_or_flags_8(state, p, endp)) {
default:
goto s_n_llhttp__internal__n_header_value_connection;
}
UNREACHABLE;
}
s_n_llhttp__internal__n_invoke_load_header_state_6: {
switch (llhttp__internal__c_load_header_state(state, p, endp)) {
case 5:
goto s_n_llhttp__internal__n_invoke_or_flags_13;
case 6:
goto s_n_llhttp__internal__n_invoke_or_flags_14;
case 7:
goto s_n_llhttp__internal__n_invoke_or_flags_15;
case 8:
goto s_n_llhttp__internal__n_invoke_or_flags_16;
default:
goto s_n_llhttp__internal__n_header_value_connection;
}
UNREACHABLE;
}
s_n_llhttp__internal__n_invoke_update_header_state_5: {
switch (llhttp__internal__c_update_header_state_1(state, p, endp)) {
default:
goto s_n_llhttp__internal__n_header_value_connection_token;
}
UNREACHABLE;
}
s_n_llhttp__internal__n_invoke_update_header_state_3: {
switch (llhttp__internal__c_update_header_state_3(state, p, endp)) {
default:
goto s_n_llhttp__internal__n_header_value_connection_ws;
}
UNREACHABLE;
}
s_n_llhttp__internal__n_invoke_update_header_state_6: {
switch (llhttp__internal__c_update_header_state_6(state, p, endp)) {
default:
goto s_n_llhttp__internal__n_header_value_connection_ws;
}
UNREACHABLE;
}
s_n_llhttp__internal__n_invoke_update_header_state_7: {
switch (llhttp__internal__c_update_header_state_7(state, p, endp)) {
default:
goto s_n_llhttp__internal__n_header_value_connection_ws;
}
UNREACHABLE;
}
s_n_llhttp__internal__n_span_end_llhttp__on_header_value_6: {
const unsigned char* start;
int err;
start = state->_span_pos0;
state->_span_pos0 = NULL;
err = llhttp__on_header_value(state, start, p);
if (err != 0) {
state->error = err;
state->error_pos = (const char*) p;
state->_current = (void*) (intptr_t) s_n_llhttp__internal__n_error_56;
return s_error;
}
goto s_n_llhttp__internal__n_error_56;
UNREACHABLE;
}
s_n_llhttp__internal__n_invoke_mul_add_content_length_1: {
switch (llhttp__internal__c_mul_add_content_length_1(state, p, endp, match)) {
case 1:
goto s_n_llhttp__internal__n_span_end_llhttp__on_header_value_6;
default:
goto s_n_llhttp__internal__n_header_value_content_length;
}
UNREACHABLE;
}
s_n_llhttp__internal__n_invoke_or_flags_17: {
switch (llhttp__internal__c_or_flags_17(state, p, endp)) {
default:
goto s_n_llhttp__internal__n_header_value_otherwise;
}
UNREACHABLE;
}
s_n_llhttp__internal__n_span_end_llhttp__on_header_value_7: {
const unsigned char* start;
int err;
start = state->_span_pos0;
state->_span_pos0 = NULL;
err = llhttp__on_header_value(state, start, p);
if (err != 0) {
state->error = err;
state->error_pos = (const char*) p;
state->_current = (void*) (intptr_t) s_n_llhttp__internal__n_error_57;
return s_error;
}
goto s_n_llhttp__internal__n_error_57;
UNREACHABLE;
}
s_n_llhttp__internal__n_error_55: {
state->error = 0x4;
state->reason = "Duplicate Content-Length";
state->error_pos = (const char*) p;
state->_current = (void*) (intptr_t) s_error;
return s_error;
UNREACHABLE;
}
s_n_llhttp__internal__n_invoke_test_flags_2: {
switch (llhttp__internal__c_test_flags_2(state, p, endp)) {
case 0:
goto s_n_llhttp__internal__n_header_value_content_length;
default:
goto s_n_llhttp__internal__n_error_55;
}
UNREACHABLE;
}
s_n_llhttp__internal__n_span_end_llhttp__on_header_value_9: {
const unsigned char* start;
int err;
start = state->_span_pos0;
state->_span_pos0 = NULL;
err = llhttp__on_header_value(state, start, p);
if (err != 0) {
state->error = err;
state->error_pos = (const char*) (p + 1);
state->_current = (void*) (intptr_t) s_n_llhttp__internal__n_error_59;
return s_error;
}
p++;
goto s_n_llhttp__internal__n_error_59;
UNREACHABLE;
}
s_n_llhttp__internal__n_invoke_update_header_state_8: {
switch (llhttp__internal__c_update_header_state_8(state, p, endp)) {
default:
goto s_n_llhttp__internal__n_header_value_otherwise;
}
UNREACHABLE;
}
s_n_llhttp__internal__n_span_end_llhttp__on_header_value_8: {
const unsigned char* start;
int err;
start = state->_span_pos0;
state->_span_pos0 = NULL;
err = llhttp__on_header_value(state, start, p);
if (err != 0) {
state->error = err;
state->error_pos = (const char*) (p + 1);
state->_current = (void*) (intptr_t) s_n_llhttp__internal__n_error_58;
return s_error;
}
p++;
goto s_n_llhttp__internal__n_error_58;
UNREACHABLE;
}
s_n_llhttp__internal__n_invoke_test_lenient_flags_20: {
switch (llhttp__internal__c_test_lenient_flags_20(state, p, endp)) {
case 0:
goto s_n_llhttp__internal__n_span_end_llhttp__on_header_value_8;
default:
goto s_n_llhttp__internal__n_header_value_te_chunked;
}
UNREACHABLE;
}
s_n_llhttp__internal__n_invoke_load_type_1: {
switch (llhttp__internal__c_load_type(state, p, endp)) {
case 1:
goto s_n_llhttp__internal__n_invoke_test_lenient_flags_20;
default:
goto s_n_llhttp__internal__n_header_value_te_chunked;
}
UNREACHABLE;
}
s_n_llhttp__internal__n_invoke_update_header_state_9: {
switch (llhttp__internal__c_update_header_state_1(state, p, endp)) {
default:
goto s_n_llhttp__internal__n_header_value;
}
UNREACHABLE;
}
s_n_llhttp__internal__n_invoke_and_flags: {
switch (llhttp__internal__c_and_flags(state, p, endp)) {
default:
goto s_n_llhttp__internal__n_header_value_te_chunked;
}
UNREACHABLE;
}
s_n_llhttp__internal__n_invoke_or_flags_19: {
switch (llhttp__internal__c_or_flags_18(state, p, endp)) {
default:
goto s_n_llhttp__internal__n_invoke_and_flags;
}
UNREACHABLE;
}
s_n_llhttp__internal__n_invoke_test_lenient_flags_21: {
switch (llhttp__internal__c_test_lenient_flags_20(state, p, endp)) {
case 0:
goto s_n_llhttp__internal__n_span_end_llhttp__on_header_value_9;
default:
goto s_n_llhttp__internal__n_invoke_or_flags_19;
}
UNREACHABLE;
}
s_n_llhttp__internal__n_invoke_load_type_2: {
switch (llhttp__internal__c_load_type(state, p, endp)) {
case 1:
goto s_n_llhttp__internal__n_invoke_test_lenient_flags_21;
default:
goto s_n_llhttp__internal__n_invoke_or_flags_19;
}
UNREACHABLE;
}
s_n_llhttp__internal__n_invoke_or_flags_18: {
switch (llhttp__internal__c_or_flags_18(state, p, endp)) {
default:
goto s_n_llhttp__internal__n_invoke_and_flags;
}
UNREACHABLE;
}
s_n_llhttp__internal__n_invoke_test_flags_3: {
switch (llhttp__internal__c_test_flags_3(state, p, endp)) {
case 1:
goto s_n_llhttp__internal__n_invoke_load_type_2;
default:
goto s_n_llhttp__internal__n_invoke_or_flags_18;
}
UNREACHABLE;
}
s_n_llhttp__internal__n_invoke_or_flags_20: {
switch (llhttp__internal__c_or_flags_20(state, p, endp)) {
default:
goto s_n_llhttp__internal__n_invoke_update_header_state_9;
}
UNREACHABLE;
}
s_n_llhttp__internal__n_invoke_load_header_state_3: {
switch (llhttp__internal__c_load_header_state(state, p, endp)) {
case 1:
goto s_n_llhttp__internal__n_header_value_connection;
case 2:
goto s_n_llhttp__internal__n_invoke_test_flags_2;
case 3:
goto s_n_llhttp__internal__n_invoke_test_flags_3;
case 4:
goto s_n_llhttp__internal__n_invoke_or_flags_20;
default:
goto s_n_llhttp__internal__n_header_value;
}
UNREACHABLE;
}
s_n_llhttp__internal__n_invoke_test_lenient_flags_22: {
switch (llhttp__internal__c_test_lenient_flags_22(state, p, endp)) {
case 0:
goto s_n_llhttp__internal__n_error_60;
default:
goto s_n_llhttp__internal__n_header_value_discard_ws;
}
UNREACHABLE;
}
s_n_llhttp__internal__n_invoke_test_flags_4: {
switch (llhttp__internal__c_test_flags_4(state, p, endp)) {
case 1:
goto s_n_llhttp__internal__n_invoke_test_lenient_flags_22;
default:
goto s_n_llhttp__internal__n_header_value_discard_ws;
}
UNREACHABLE;
}
s_n_llhttp__internal__n_error_61: {
state->error = 0xf;
state->reason = "Transfer-Encoding can't be present with Content-Length";
state->error_pos = (const char*) p;
state->_current = (void*) (intptr_t) s_error;
return s_error;
UNREACHABLE;
}
s_n_llhttp__internal__n_invoke_test_lenient_flags_23: {
switch (llhttp__internal__c_test_lenient_flags_22(state, p, endp)) {
case 0:
goto s_n_llhttp__internal__n_error_61;
default:
goto s_n_llhttp__internal__n_header_value_discard_ws;
}
UNREACHABLE;
}
s_n_llhttp__internal__n_invoke_test_flags_5: {
switch (llhttp__internal__c_test_flags_2(state, p, endp)) {
case 1:
goto s_n_llhttp__internal__n_invoke_test_lenient_flags_23;
default:
goto s_n_llhttp__internal__n_header_value_discard_ws;
}
UNREACHABLE;
}
s_n_llhttp__internal__n_pause_19: {
state->error = 0x15;
state->reason = "on_header_field_complete pause";
state->error_pos = (const char*) p;
state->_current = (void*) (intptr_t) s_n_llhttp__internal__n_invoke_load_header_state;
return s_error;
UNREACHABLE;
}
s_n_llhttp__internal__n_error_45: {
state->error = 0x1c;
state->reason = "`on_header_field_complete` callback error";
state->error_pos = (const char*) p;
state->_current = (void*) (intptr_t) s_error;
return s_error;
UNREACHABLE;
}
s_n_llhttp__internal__n_span_end_llhttp__on_header_field_1: {
const unsigned char* start;
int err;
start = state->_span_pos0;
state->_span_pos0 = NULL;
err = llhttp__on_header_field(state, start, p);
if (err != 0) {
state->error = err;
state->error_pos = (const char*) (p + 1);
state->_current = (void*) (intptr_t) s_n_llhttp__internal__n_invoke_llhttp__on_header_field_complete;
return s_error;
}
p++;
goto s_n_llhttp__internal__n_invoke_llhttp__on_header_field_complete;
UNREACHABLE;
}
s_n_llhttp__internal__n_span_end_llhttp__on_header_field_2: {
const unsigned char* start;
int err;
start = state->_span_pos0;
state->_span_pos0 = NULL;
err = llhttp__on_header_field(state, start, p);
if (err != 0) {
state->error = err;
state->error_pos = (const char*) (p + 1);
state->_current = (void*) (intptr_t) s_n_llhttp__internal__n_invoke_llhttp__on_header_field_complete;
return s_error;
}
p++;
goto s_n_llhttp__internal__n_invoke_llhttp__on_header_field_complete;
UNREACHABLE;
}
s_n_llhttp__internal__n_error_62: {
state->error = 0xa;
state->reason = "Invalid header token";
state->error_pos = (const char*) p;
state->_current = (void*) (intptr_t) s_error;
return s_error;
UNREACHABLE;
}
s_n_llhttp__internal__n_invoke_update_header_state_10: {
switch (llhttp__internal__c_update_header_state_1(state, p, endp)) {
default:
goto s_n_llhttp__internal__n_header_field_general;
}
UNREACHABLE;
}
s_n_llhttp__internal__n_invoke_store_header_state: {
switch (llhttp__internal__c_store_header_state(state, p, endp, match)) {
default:
goto s_n_llhttp__internal__n_header_field_colon;
}
UNREACHABLE;
}
s_n_llhttp__internal__n_invoke_update_header_state_11: {
switch (llhttp__internal__c_update_header_state_1(state, p, endp)) {
default:
goto s_n_llhttp__internal__n_header_field_general;
}
UNREACHABLE;
}
s_n_llhttp__internal__n_error_4: {
state->error = 0x1e;
state->reason = "Unexpected space after start line";
state->error_pos = (const char*) p;
state->_current = (void*) (intptr_t) s_error;
return s_error;
UNREACHABLE;
}
s_n_llhttp__internal__n_invoke_test_lenient_flags: {
switch (llhttp__internal__c_test_lenient_flags(state, p, endp)) {
case 1:
goto s_n_llhttp__internal__n_header_field_start;
default:
goto s_n_llhttp__internal__n_error_4;
}
UNREACHABLE;
}
s_n_llhttp__internal__n_pause_20: {
state->error = 0x15;
state->reason = "on_url_complete pause";
state->error_pos = (const char*) p;
state->_current = (void*) (intptr_t) s_n_llhttp__internal__n_headers_start;
return s_error;
UNREACHABLE;
}
s_n_llhttp__internal__n_error_3: {
state->error = 0x1a;
state->reason = "`on_url_complete` callback error";
state->error_pos = (const char*) p;
state->_current = (void*) (intptr_t) s_error;
return s_error;
UNREACHABLE;
}
s_n_llhttp__internal__n_invoke_llhttp__on_url_complete: {
switch (llhttp__on_url_complete(state, p, endp)) {
case 0:
goto s_n_llhttp__internal__n_headers_start;
case 21:
goto s_n_llhttp__internal__n_pause_20;
default:
goto s_n_llhttp__internal__n_error_3;
}
UNREACHABLE;
}
s_n_llhttp__internal__n_invoke_update_http_minor: {
switch (llhttp__internal__c_update_http_minor(state, p, endp)) {
default:
goto s_n_llhttp__internal__n_invoke_llhttp__on_url_complete;
}
UNREACHABLE;
}
s_n_llhttp__internal__n_invoke_update_http_major: {
switch (llhttp__internal__c_update_http_major(state, p, endp)) {
default:
goto s_n_llhttp__internal__n_invoke_update_http_minor;
}
UNREACHABLE;
}
s_n_llhttp__internal__n_span_end_llhttp__on_url_3: {
const unsigned char* start;
int err;
start = state->_span_pos0;
state->_span_pos0 = NULL;
err = llhttp__on_url(state, start, p);
if (err != 0) {
state->error = err;
state->error_pos = (const char*) p;
state->_current = (void*) (intptr_t) s_n_llhttp__internal__n_url_skip_to_http09;
return s_error;
}
goto s_n_llhttp__internal__n_url_skip_to_http09;
UNREACHABLE;
}
s_n_llhttp__internal__n_error_63: {
state->error = 0x7;
state->reason = "Expected CRLF";
state->error_pos = (const char*) p;
state->_current = (void*) (intptr_t) s_error;
return s_error;
UNREACHABLE;
}
s_n_llhttp__internal__n_span_end_llhttp__on_url_4: {
const unsigned char* start;
int err;
start = state->_span_pos0;
state->_span_pos0 = NULL;
err = llhttp__on_url(state, start, p);
if (err != 0) {
state->error = err;
state->error_pos = (const char*) p;
state->_current = (void*) (intptr_t) s_n_llhttp__internal__n_url_skip_lf_to_http09;
return s_error;
}
goto s_n_llhttp__internal__n_url_skip_lf_to_http09;
UNREACHABLE;
}
s_n_llhttp__internal__n_error_72: {
state->error = 0x17;
state->reason = "Pause on PRI/Upgrade";
state->error_pos = (const char*) p;
state->_current = (void*) (intptr_t) s_error;
return s_error;
UNREACHABLE;
}
s_n_llhttp__internal__n_error_73: {
state->error = 0x9;
state->reason = "Expected HTTP/2 Connection Preface";
state->error_pos = (const char*) p;
state->_current = (void*) (intptr_t) s_error;
return s_error;
UNREACHABLE;
}
s_n_llhttp__internal__n_error_70: {
state->error = 0x2;
state->reason = "Expected CRLF after version";
state->error_pos = (const char*) p;
state->_current = (void*) (intptr_t) s_error;
return s_error;
UNREACHABLE;
}
s_n_llhttp__internal__n_invoke_test_lenient_flags_26: {
switch (llhttp__internal__c_test_lenient_flags_8(state, p, endp)) {
case 1:
goto s_n_llhttp__internal__n_headers_start;
default:
goto s_n_llhttp__internal__n_error_70;
}
UNREACHABLE;
}
s_n_llhttp__internal__n_error_69: {
state->error = 0x9;
state->reason = "Expected CRLF after version";
state->error_pos = (const char*) p;
state->_current = (void*) (intptr_t) s_error;
return s_error;
UNREACHABLE;
}
s_n_llhttp__internal__n_invoke_test_lenient_flags_25: {
switch (llhttp__internal__c_test_lenient_flags_1(state, p, endp)) {
case 1:
goto s_n_llhttp__internal__n_req_http_complete_crlf;
default:
goto s_n_llhttp__internal__n_error_69;
}
UNREACHABLE;
}
s_n_llhttp__internal__n_error_71: {
state->error = 0x9;
state->reason = "Expected CRLF after version";
state->error_pos = (const char*) p;
state->_current = (void*) (intptr_t) s_error;
return s_error;
UNREACHABLE;
}
s_n_llhttp__internal__n_pause_21: {
state->error = 0x15;
state->reason = "on_version_complete pause";
state->error_pos = (const char*) p;
state->_current = (void*) (intptr_t) s_n_llhttp__internal__n_invoke_load_method_1;
return s_error;
UNREACHABLE;
}
s_n_llhttp__internal__n_error_68: {
state->error = 0x21;
state->reason = "`on_version_complete` callback error";
state->error_pos = (const char*) p;
state->_current = (void*) (intptr_t) s_error;
return s_error;
UNREACHABLE;
}
s_n_llhttp__internal__n_span_end_llhttp__on_version_1: {
const unsigned char* start;
int err;
start = state->_span_pos0;
state->_span_pos0 = NULL;
err = llhttp__on_version(state, start, p);
if (err != 0) {
state->error = err;
state->error_pos = (const char*) p;
state->_current = (void*) (intptr_t) s_n_llhttp__internal__n_invoke_llhttp__on_version_complete;
return s_error;
}
goto s_n_llhttp__internal__n_invoke_llhttp__on_version_complete;
UNREACHABLE;
}
s_n_llhttp__internal__n_span_end_llhttp__on_version: {
const unsigned char* start;
int err;
start = state->_span_pos0;
state->_span_pos0 = NULL;
err = llhttp__on_version(state, start, p);
if (err != 0) {
state->error = err;
state->error_pos = (const char*) p;
state->_current = (void*) (intptr_t) s_n_llhttp__internal__n_error_67;
return s_error;
}
goto s_n_llhttp__internal__n_error_67;
UNREACHABLE;
}
s_n_llhttp__internal__n_invoke_load_http_minor: {
switch (llhttp__internal__c_load_http_minor(state, p, endp)) {
case 9:
goto s_n_llhttp__internal__n_span_end_llhttp__on_version_1;
default:
goto s_n_llhttp__internal__n_span_end_llhttp__on_version;
}
UNREACHABLE;
}
s_n_llhttp__internal__n_invoke_load_http_minor_1: {
switch (llhttp__internal__c_load_http_minor(state, p, endp)) {
case 0:
goto s_n_llhttp__internal__n_span_end_llhttp__on_version_1;
case 1:
goto s_n_llhttp__internal__n_span_end_llhttp__on_version_1;
default:
goto s_n_llhttp__internal__n_span_end_llhttp__on_version;
}
UNREACHABLE;
}
s_n_llhttp__internal__n_invoke_load_http_minor_2: {
switch (llhttp__internal__c_load_http_minor(state, p, endp)) {
case 0:
goto s_n_llhttp__internal__n_span_end_llhttp__on_version_1;
default:
goto s_n_llhttp__internal__n_span_end_llhttp__on_version;
}
UNREACHABLE;
}
s_n_llhttp__internal__n_invoke_load_http_major: {
switch (llhttp__internal__c_load_http_major(state, p, endp)) {
case 0:
goto s_n_llhttp__internal__n_invoke_load_http_minor;
case 1:
goto s_n_llhttp__internal__n_invoke_load_http_minor_1;
case 2:
goto s_n_llhttp__internal__n_invoke_load_http_minor_2;
default:
goto s_n_llhttp__internal__n_span_end_llhttp__on_version;
}
UNREACHABLE;
}
s_n_llhttp__internal__n_invoke_test_lenient_flags_24: {
switch (llhttp__internal__c_test_lenient_flags_24(state, p, endp)) {
case 1:
goto s_n_llhttp__internal__n_span_end_llhttp__on_version_1;
default:
goto s_n_llhttp__internal__n_invoke_load_http_major;
}
UNREACHABLE;
}
s_n_llhttp__internal__n_invoke_store_http_minor: {
switch (llhttp__internal__c_store_http_minor(state, p, endp, match)) {
default:
goto s_n_llhttp__internal__n_invoke_test_lenient_flags_24;
}
UNREACHABLE;
}
s_n_llhttp__internal__n_span_end_llhttp__on_version_2: {
const unsigned char* start;
int err;
start = state->_span_pos0;
state->_span_pos0 = NULL;
err = llhttp__on_version(state, start, p);
if (err != 0) {
state->error = err;
state->error_pos = (const char*) p;
state->_current = (void*) (intptr_t) s_n_llhttp__internal__n_error_74;
return s_error;
}
goto s_n_llhttp__internal__n_error_74;
UNREACHABLE;
}
s_n_llhttp__internal__n_span_end_llhttp__on_version_3: {
const unsigned char* start;
int err;
start = state->_span_pos0;
state->_span_pos0 = NULL;
err = llhttp__on_version(state, start, p);
if (err != 0) {
state->error = err;
state->error_pos = (const char*) p;
state->_current = (void*) (intptr_t) s_n_llhttp__internal__n_error_75;
return s_error;
}
goto s_n_llhttp__internal__n_error_75;
UNREACHABLE;
}
s_n_llhttp__internal__n_invoke_store_http_major: {
switch (llhttp__internal__c_store_http_major(state, p, endp, match)) {
default:
goto s_n_llhttp__internal__n_req_http_dot;
}
UNREACHABLE;
}
s_n_llhttp__internal__n_span_end_llhttp__on_version_4: {
const unsigned char* start;
int err;
start = state->_span_pos0;
state->_span_pos0 = NULL;
err = llhttp__on_version(state, start, p);
if (err != 0) {
state->error = err;
state->error_pos = (const char*) p;
state->_current = (void*) (intptr_t) s_n_llhttp__internal__n_error_76;
return s_error;
}
goto s_n_llhttp__internal__n_error_76;
UNREACHABLE;
}
s_n_llhttp__internal__n_error_77: {
state->error = 0x8;
state->reason = "Expected HTTP/, RTSP/ or ICE/";
state->error_pos = (const char*) p;
state->_current = (void*) (intptr_t) s_error;
return s_error;
UNREACHABLE;
}
s_n_llhttp__internal__n_error_66: {
state->error = 0x8;
state->reason = "Invalid method for HTTP/x.x request";
state->error_pos = (const char*) p;
state->_current = (void*) (intptr_t) s_error;
return s_error;
UNREACHABLE;
}
s_n_llhttp__internal__n_pause_22: {
state->error = 0x15;
state->reason = "on_protocol_complete pause";
state->error_pos = (const char*) p;
state->_current = (void*) (intptr_t) s_n_llhttp__internal__n_invoke_load_method;
return s_error;
UNREACHABLE;
}
s_n_llhttp__internal__n_error_65: {
state->error = 0x26;
state->reason = "`on_protocol_complete` callback error";
state->error_pos = (const char*) p;
state->_current = (void*) (intptr_t) s_error;
return s_error;
UNREACHABLE;
}
s_n_llhttp__internal__n_span_end_llhttp__on_protocol: {
const unsigned char* start;
int err;
start = state->_span_pos0;
state->_span_pos0 = NULL;
err = llhttp__on_protocol(state, start, p);
if (err != 0) {
state->error = err;
state->error_pos = (const char*) p;
state->_current = (void*) (intptr_t) s_n_llhttp__internal__n_invoke_llhttp__on_protocol_complete;
return s_error;
}
goto s_n_llhttp__internal__n_invoke_llhttp__on_protocol_complete;
UNREACHABLE;
}
s_n_llhttp__internal__n_span_end_llhttp__on_protocol_3: {
const unsigned char* start;
int err;
start = state->_span_pos0;
state->_span_pos0 = NULL;
err = llhttp__on_protocol(state, start, p);
if (err != 0) {
state->error = err;
state->error_pos = (const char*) p;
state->_current = (void*) (intptr_t) s_n_llhttp__internal__n_error_82;
return s_error;
}
goto s_n_llhttp__internal__n_error_82;
UNREACHABLE;
}
s_n_llhttp__internal__n_error_79: {
state->error = 0x8;
state->reason = "Expected SOURCE method for ICE/x.x request";
state->error_pos = (const char*) p;
state->_current = (void*) (intptr_t) s_error;
return s_error;
UNREACHABLE;
}
s_n_llhttp__internal__n_pause_23: {
state->error = 0x15;
state->reason = "on_protocol_complete pause";
state->error_pos = (const char*) p;
state->_current = (void*) (intptr_t) s_n_llhttp__internal__n_invoke_load_method_2;
return s_error;
UNREACHABLE;
}
s_n_llhttp__internal__n_error_78: {
state->error = 0x26;
state->reason = "`on_protocol_complete` callback error";
state->error_pos = (const char*) p;
state->_current = (void*) (intptr_t) s_error;
return s_error;
UNREACHABLE;
}
s_n_llhttp__internal__n_span_end_llhttp__on_protocol_1: {
const unsigned char* start;
int err;
start = state->_span_pos0;
state->_span_pos0 = NULL;
err = llhttp__on_protocol(state, start, p);
if (err != 0) {
state->error = err;
state->error_pos = (const char*) p;
state->_current = (void*) (intptr_t) s_n_llhttp__internal__n_invoke_llhttp__on_protocol_complete_1;
return s_error;
}
goto s_n_llhttp__internal__n_invoke_llhttp__on_protocol_complete_1;
UNREACHABLE;
}
s_n_llhttp__internal__n_error_81: {
state->error = 0x8;
state->reason = "Invalid method for RTSP/x.x request";
state->error_pos = (const char*) p;
state->_current = (void*) (intptr_t) s_error;
return s_error;
UNREACHABLE;
}
s_n_llhttp__internal__n_pause_24: {
state->error = 0x15;
state->reason = "on_protocol_complete pause";
state->error_pos = (const char*) p;
state->_current = (void*) (intptr_t) s_n_llhttp__internal__n_invoke_load_method_3;
return s_error;
UNREACHABLE;
}
s_n_llhttp__internal__n_error_80: {
state->error = 0x26;
state->reason = "`on_protocol_complete` callback error";
state->error_pos = (const char*) p;
state->_current = (void*) (intptr_t) s_error;
return s_error;
UNREACHABLE;
}
s_n_llhttp__internal__n_span_end_llhttp__on_protocol_2: {
const unsigned char* start;
int err;
start = state->_span_pos0;
state->_span_pos0 = NULL;
err = llhttp__on_protocol(state, start, p);
if (err != 0) {
state->error = err;
state->error_pos = (const char*) p;
state->_current = (void*) (intptr_t) s_n_llhttp__internal__n_invoke_llhttp__on_protocol_complete_2;
return s_error;
}
goto s_n_llhttp__internal__n_invoke_llhttp__on_protocol_complete_2;
UNREACHABLE;
}
s_n_llhttp__internal__n_pause_25: {
state->error = 0x15;
state->reason = "on_url_complete pause";
state->error_pos = (const char*) p;
state->_current = (void*) (intptr_t) s_n_llhttp__internal__n_req_http_start;
return s_error;
UNREACHABLE;
}
s_n_llhttp__internal__n_error_64: {
state->error = 0x1a;
state->reason = "`on_url_complete` callback error";
state->error_pos = (const char*) p;
state->_current = (void*) (intptr_t) s_error;
return s_error;
UNREACHABLE;
}
s_n_llhttp__internal__n_invoke_llhttp__on_url_complete_1: {
switch (llhttp__on_url_complete(state, p, endp)) {
case 0:
goto s_n_llhttp__internal__n_req_http_start;
case 21:
goto s_n_llhttp__internal__n_pause_25;
default:
goto s_n_llhttp__internal__n_error_64;
}
UNREACHABLE;
}
s_n_llhttp__internal__n_span_end_llhttp__on_url_5: {
const unsigned char* start;
int err;
start = state->_span_pos0;
state->_span_pos0 = NULL;
err = llhttp__on_url(state, start, p);
if (err != 0) {
state->error = err;
state->error_pos = (const char*) p;
state->_current = (void*) (intptr_t) s_n_llhttp__internal__n_url_skip_to_http;
return s_error;
}
goto s_n_llhttp__internal__n_url_skip_to_http;
UNREACHABLE;
}
s_n_llhttp__internal__n_span_end_llhttp__on_url_6: {
const unsigned char* start;
int err;
start = state->_span_pos0;
state->_span_pos0 = NULL;
err = llhttp__on_url(state, start, p);
if (err != 0) {
state->error = err;
state->error_pos = (const char*) p;
state->_current = (void*) (intptr_t) s_n_llhttp__internal__n_url_skip_to_http09;
return s_error;
}
goto s_n_llhttp__internal__n_url_skip_to_http09;
UNREACHABLE;
}
s_n_llhttp__internal__n_span_end_llhttp__on_url_7: {
const unsigned char* start;
int err;
start = state->_span_pos0;
state->_span_pos0 = NULL;
err = llhttp__on_url(state, start, p);
if (err != 0) {
state->error = err;
state->error_pos = (const char*) p;
state->_current = (void*) (intptr_t) s_n_llhttp__internal__n_url_skip_lf_to_http09;
return s_error;
}
goto s_n_llhttp__internal__n_url_skip_lf_to_http09;
UNREACHABLE;
}
s_n_llhttp__internal__n_span_end_llhttp__on_url_8: {
const unsigned char* start;
int err;
start = state->_span_pos0;
state->_span_pos0 = NULL;
err = llhttp__on_url(state, start, p);
if (err != 0) {
state->error = err;
state->error_pos = (const char*) p;
state->_current = (void*) (intptr_t) s_n_llhttp__internal__n_url_skip_to_http;
return s_error;
}
goto s_n_llhttp__internal__n_url_skip_to_http;
UNREACHABLE;
}
s_n_llhttp__internal__n_error_83: {
state->error = 0x7;
state->reason = "Invalid char in url fragment start";
state->error_pos = (const char*) p;
state->_current = (void*) (intptr_t) s_error;
return s_error;
UNREACHABLE;
}
s_n_llhttp__internal__n_span_end_llhttp__on_url_9: {
const unsigned char* start;
int err;
start = state->_span_pos0;
state->_span_pos0 = NULL;
err = llhttp__on_url(state, start, p);
if (err != 0) {
state->error = err;
state->error_pos = (const char*) p;
state->_current = (void*) (intptr_t) s_n_llhttp__internal__n_url_skip_to_http09;
return s_error;
}
goto s_n_llhttp__internal__n_url_skip_to_http09;
UNREACHABLE;
}
s_n_llhttp__internal__n_span_end_llhttp__on_url_10: {
const unsigned char* start;
int err;
start = state->_span_pos0;
state->_span_pos0 = NULL;
err = llhttp__on_url(state, start, p);
if (err != 0) {
state->error = err;
state->error_pos = (const char*) p;
state->_current = (void*) (intptr_t) s_n_llhttp__internal__n_url_skip_lf_to_http09;
return s_error;
}
goto s_n_llhttp__internal__n_url_skip_lf_to_http09;
UNREACHABLE;
}
s_n_llhttp__internal__n_span_end_llhttp__on_url_11: {
const unsigned char* start;
int err;
start = state->_span_pos0;
state->_span_pos0 = NULL;
err = llhttp__on_url(state, start, p);
if (err != 0) {
state->error = err;
state->error_pos = (const char*) p;
state->_current = (void*) (intptr_t) s_n_llhttp__internal__n_url_skip_to_http;
return s_error;
}
goto s_n_llhttp__internal__n_url_skip_to_http;
UNREACHABLE;
}
s_n_llhttp__internal__n_error_84: {
state->error = 0x7;
state->reason = "Invalid char in url query";
state->error_pos = (const char*) p;
state->_current = (void*) (intptr_t) s_error;
return s_error;
UNREACHABLE;
}
s_n_llhttp__internal__n_error_85: {
state->error = 0x7;
state->reason = "Invalid char in url path";
state->error_pos = (const char*) p;
state->_current = (void*) (intptr_t) s_error;
return s_error;
UNREACHABLE;
}
s_n_llhttp__internal__n_span_end_llhttp__on_url: {
const unsigned char* start;
int err;
start = state->_span_pos0;
state->_span_pos0 = NULL;
err = llhttp__on_url(state, start, p);
if (err != 0) {
state->error = err;
state->error_pos = (const char*) p;
state->_current = (void*) (intptr_t) s_n_llhttp__internal__n_url_skip_to_http09;
return s_error;
}
goto s_n_llhttp__internal__n_url_skip_to_http09;
UNREACHABLE;
}
s_n_llhttp__internal__n_span_end_llhttp__on_url_1: {
const unsigned char* start;
int err;
start = state->_span_pos0;
state->_span_pos0 = NULL;
err = llhttp__on_url(state, start, p);
if (err != 0) {
state->error = err;
state->error_pos = (const char*) p;
state->_current = (void*) (intptr_t) s_n_llhttp__internal__n_url_skip_lf_to_http09;
return s_error;
}
goto s_n_llhttp__internal__n_url_skip_lf_to_http09;
UNREACHABLE;
}
s_n_llhttp__internal__n_span_end_llhttp__on_url_2: {
const unsigned char* start;
int err;
start = state->_span_pos0;
state->_span_pos0 = NULL;
err = llhttp__on_url(state, start, p);
if (err != 0) {
state->error = err;
state->error_pos = (const char*) p;
state->_current = (void*) (intptr_t) s_n_llhttp__internal__n_url_skip_to_http;
return s_error;
}
goto s_n_llhttp__internal__n_url_skip_to_http;
UNREACHABLE;
}
s_n_llhttp__internal__n_span_end_llhttp__on_url_12: {
const unsigned char* start;
int err;
start = state->_span_pos0;
state->_span_pos0 = NULL;
err = llhttp__on_url(state, start, p);
if (err != 0) {
state->error = err;
state->error_pos = (const char*) p;
state->_current = (void*) (intptr_t) s_n_llhttp__internal__n_url_skip_to_http09;
return s_error;
}
goto s_n_llhttp__internal__n_url_skip_to_http09;
UNREACHABLE;
}
s_n_llhttp__internal__n_span_end_llhttp__on_url_13: {
const unsigned char* start;
int err;
start = state->_span_pos0;
state->_span_pos0 = NULL;
err = llhttp__on_url(state, start, p);
if (err != 0) {
state->error = err;
state->error_pos = (const char*) p;
state->_current = (void*) (intptr_t) s_n_llhttp__internal__n_url_skip_lf_to_http09;
return s_error;
}
goto s_n_llhttp__internal__n_url_skip_lf_to_http09;
UNREACHABLE;
}
s_n_llhttp__internal__n_span_end_llhttp__on_url_14: {
const unsigned char* start;
int err;
start = state->_span_pos0;
state->_span_pos0 = NULL;
err = llhttp__on_url(state, start, p);
if (err != 0) {
state->error = err;
state->error_pos = (const char*) p;
state->_current = (void*) (intptr_t) s_n_llhttp__internal__n_url_skip_to_http;
return s_error;
}
goto s_n_llhttp__internal__n_url_skip_to_http;
UNREACHABLE;
}
s_n_llhttp__internal__n_error_86: {
state->error = 0x7;
state->reason = "Double @ in url";
state->error_pos = (const char*) p;
state->_current = (void*) (intptr_t) s_error;
return s_error;
UNREACHABLE;
}
s_n_llhttp__internal__n_error_87: {
state->error = 0x7;
state->reason = "Unexpected char in url server";
state->error_pos = (const char*) p;
state->_current = (void*) (intptr_t) s_error;
return s_error;
UNREACHABLE;
}
s_n_llhttp__internal__n_error_88: {
state->error = 0x7;
state->reason = "Unexpected char in url server";
state->error_pos = (const char*) p;
state->_current = (void*) (intptr_t) s_error;
return s_error;
UNREACHABLE;
}
s_n_llhttp__internal__n_error_89: {
state->error = 0x7;
state->reason = "Unexpected char in url schema";
state->error_pos = (const char*) p;
state->_current = (void*) (intptr_t) s_error;
return s_error;
UNREACHABLE;
}
s_n_llhttp__internal__n_error_90: {
state->error = 0x7;
state->reason = "Unexpected char in url schema";
state->error_pos = (const char*) p;
state->_current = (void*) (intptr_t) s_error;
return s_error;
UNREACHABLE;
}
s_n_llhttp__internal__n_error_91: {
state->error = 0x7;
state->reason = "Unexpected start char in url";
state->error_pos = (const char*) p;
state->_current = (void*) (intptr_t) s_error;
return s_error;
UNREACHABLE;
}
s_n_llhttp__internal__n_invoke_is_equal_method: {
switch (llhttp__internal__c_is_equal_method(state, p, endp)) {
case 0:
goto s_n_llhttp__internal__n_url_entry_normal;
default:
goto s_n_llhttp__internal__n_url_entry_connect;
}
UNREACHABLE;
}
s_n_llhttp__internal__n_error_92: {
state->error = 0x6;
state->reason = "Expected space after method";
state->error_pos = (const char*) p;
state->_current = (void*) (intptr_t) s_error;
return s_error;
UNREACHABLE;
}
s_n_llhttp__internal__n_pause_29: {
state->error = 0x15;
state->reason = "on_method_complete pause";
state->error_pos = (const char*) p;
state->_current = (void*) (intptr_t) s_n_llhttp__internal__n_req_first_space_before_url;
return s_error;
UNREACHABLE;
}
s_n_llhttp__internal__n_error_111: {
state->error = 0x20;
state->reason = "`on_method_complete` callback error";
state->error_pos = (const char*) p;
state->_current = (void*) (intptr_t) s_error;
return s_error;
UNREACHABLE;
}
s_n_llhttp__internal__n_span_end_llhttp__on_method_2: {
const unsigned char* start;
int err;
start = state->_span_pos0;
state->_span_pos0 = NULL;
err = llhttp__on_method(state, start, p);
if (err != 0) {
state->error = err;
state->error_pos = (const char*) p;
state->_current = (void*) (intptr_t) s_n_llhttp__internal__n_invoke_llhttp__on_method_complete_1;
return s_error;
}
goto s_n_llhttp__internal__n_invoke_llhttp__on_method_complete_1;
UNREACHABLE;
}
s_n_llhttp__internal__n_invoke_store_method_1: {
switch (llhttp__internal__c_store_method(state, p, endp, match)) {
default:
goto s_n_llhttp__internal__n_span_end_llhttp__on_method_2;
}
UNREACHABLE;
}
s_n_llhttp__internal__n_error_112: {
state->error = 0x6;
state->reason = "Invalid method encountered";
state->error_pos = (const char*) p;
state->_current = (void*) (intptr_t) s_error;
return s_error;
UNREACHABLE;
}
s_n_llhttp__internal__n_error_104: {
state->error = 0xd;
state->reason = "Invalid status code";
state->error_pos = (const char*) p;
state->_current = (void*) (intptr_t) s_error;
return s_error;
UNREACHABLE;
}
s_n_llhttp__internal__n_error_102: {
state->error = 0xd;
state->reason = "Invalid status code";
state->error_pos = (const char*) p;
state->_current = (void*) (intptr_t) s_error;
return s_error;
UNREACHABLE;
}
s_n_llhttp__internal__n_error_100: {
state->error = 0xd;
state->reason = "Invalid status code";
state->error_pos = (const char*) p;
state->_current = (void*) (intptr_t) s_error;
return s_error;
UNREACHABLE;
}
s_n_llhttp__internal__n_pause_27: {
state->error = 0x15;
state->reason = "on_status_complete pause";
state->error_pos = (const char*) p;
state->_current = (void*) (intptr_t) s_n_llhttp__internal__n_headers_start;
return s_error;
UNREACHABLE;
}
s_n_llhttp__internal__n_error_96: {
state->error = 0x1b;
state->reason = "`on_status_complete` callback error";
state->error_pos = (const char*) p;
state->_current = (void*) (intptr_t) s_error;
return s_error;
UNREACHABLE;
}
s_n_llhttp__internal__n_invoke_llhttp__on_status_complete: {
switch (llhttp__on_status_complete(state, p, endp)) {
case 0:
goto s_n_llhttp__internal__n_headers_start;
case 21:
goto s_n_llhttp__internal__n_pause_27;
default:
goto s_n_llhttp__internal__n_error_96;
}
UNREACHABLE;
}
s_n_llhttp__internal__n_error_95: {
state->error = 0xd;
state->reason = "Invalid response status";
state->error_pos = (const char*) p;
state->_current = (void*) (intptr_t) s_error;
return s_error;
UNREACHABLE;
}
s_n_llhttp__internal__n_invoke_test_lenient_flags_28: {
switch (llhttp__internal__c_test_lenient_flags_1(state, p, endp)) {
case 1:
goto s_n_llhttp__internal__n_invoke_llhttp__on_status_complete;
default:
goto s_n_llhttp__internal__n_error_95;
}
UNREACHABLE;
}
s_n_llhttp__internal__n_error_97: {
state->error = 0x2;
state->reason = "Expected LF after CR";
state->error_pos = (const char*) p;
state->_current = (void*) (intptr_t) s_error;
return s_error;
UNREACHABLE;
}
s_n_llhttp__internal__n_invoke_test_lenient_flags_29: {
switch (llhttp__internal__c_test_lenient_flags_8(state, p, endp)) {
case 1:
goto s_n_llhttp__internal__n_invoke_llhttp__on_status_complete;
default:
goto s_n_llhttp__internal__n_error_97;
}
UNREACHABLE;
}
s_n_llhttp__internal__n_error_98: {
state->error = 0x19;
state->reason = "Missing expected CR after response line";
state->error_pos = (const char*) p;
state->_current = (void*) (intptr_t) s_error;
return s_error;
UNREACHABLE;
}
s_n_llhttp__internal__n_span_end_llhttp__on_status: {
const unsigned char* start;
int err;
start = state->_span_pos0;
state->_span_pos0 = NULL;
err = llhttp__on_status(state, start, p);
if (err != 0) {
state->error = err;
state->error_pos = (const char*) (p + 1);
state->_current = (void*) (intptr_t) s_n_llhttp__internal__n_invoke_test_lenient_flags_30;
return s_error;
}
p++;
goto s_n_llhttp__internal__n_invoke_test_lenient_flags_30;
UNREACHABLE;
}
s_n_llhttp__internal__n_span_end_llhttp__on_status_1: {
const unsigned char* start;
int err;
start = state->_span_pos0;
state->_span_pos0 = NULL;
err = llhttp__on_status(state, start, p);
if (err != 0) {
state->error = err;
state->error_pos = (const char*) (p + 1);
state->_current = (void*) (intptr_t) s_n_llhttp__internal__n_res_line_almost_done;
return s_error;
}
p++;
goto s_n_llhttp__internal__n_res_line_almost_done;
UNREACHABLE;
}
s_n_llhttp__internal__n_error_99: {
state->error = 0xd;
state->reason = "Invalid response status";
state->error_pos = (const char*) p;
state->_current = (void*) (intptr_t) s_error;
return s_error;
UNREACHABLE;
}
s_n_llhttp__internal__n_invoke_mul_add_status_code_2: {
switch (llhttp__internal__c_mul_add_status_code(state, p, endp, match)) {
case 1:
goto s_n_llhttp__internal__n_error_100;
default:
goto s_n_llhttp__internal__n_res_status_code_otherwise;
}
UNREACHABLE;
}
s_n_llhttp__internal__n_error_101: {
state->error = 0xd;
state->reason = "Invalid status code";
state->error_pos = (const char*) p;
state->_current = (void*) (intptr_t) s_error;
return s_error;
UNREACHABLE;
}
s_n_llhttp__internal__n_invoke_mul_add_status_code_1: {
switch (llhttp__internal__c_mul_add_status_code(state, p, endp, match)) {
case 1:
goto s_n_llhttp__internal__n_error_102;
default:
goto s_n_llhttp__internal__n_res_status_code_digit_3;
}
UNREACHABLE;
}
s_n_llhttp__internal__n_error_103: {
state->error = 0xd;
state->reason = "Invalid status code";
state->error_pos = (const char*) p;
state->_current = (void*) (intptr_t) s_error;
return s_error;
UNREACHABLE;
}
s_n_llhttp__internal__n_invoke_mul_add_status_code: {
switch (llhttp__internal__c_mul_add_status_code(state, p, endp, match)) {
case 1:
goto s_n_llhttp__internal__n_error_104;
default:
goto s_n_llhttp__internal__n_res_status_code_digit_2;
}
UNREACHABLE;
}
s_n_llhttp__internal__n_error_105: {
state->error = 0xd;
state->reason = "Invalid status code";
state->error_pos = (const char*) p;
state->_current = (void*) (intptr_t) s_error;
return s_error;
UNREACHABLE;
}
s_n_llhttp__internal__n_invoke_update_status_code: {
switch (llhttp__internal__c_update_status_code(state, p, endp)) {
default:
goto s_n_llhttp__internal__n_res_status_code_digit_1;
}
UNREACHABLE;
}
s_n_llhttp__internal__n_error_106: {
state->error = 0x9;
state->reason = "Expected space after version";
state->error_pos = (const char*) p;
state->_current = (void*) (intptr_t) s_error;
return s_error;
UNREACHABLE;
}
s_n_llhttp__internal__n_pause_28: {
state->error = 0x15;
state->reason = "on_version_complete pause";
state->error_pos = (const char*) p;
state->_current = (void*) (intptr_t) s_n_llhttp__internal__n_res_after_version;
return s_error;
UNREACHABLE;
}
s_n_llhttp__internal__n_error_94: {
state->error = 0x21;
state->reason = "`on_version_complete` callback error";
state->error_pos = (const char*) p;
state->_current = (void*) (intptr_t) s_error;
return s_error;
UNREACHABLE;
}
s_n_llhttp__internal__n_span_end_llhttp__on_version_6: {
const unsigned char* start;
int err;
start = state->_span_pos0;
state->_span_pos0 = NULL;
err = llhttp__on_version(state, start, p);
if (err != 0) {
state->error = err;
state->error_pos = (const char*) p;
state->_current = (void*) (intptr_t) s_n_llhttp__internal__n_invoke_llhttp__on_version_complete_1;
return s_error;
}
goto s_n_llhttp__internal__n_invoke_llhttp__on_version_complete_1;
UNREACHABLE;
}
s_n_llhttp__internal__n_span_end_llhttp__on_version_5: {
const unsigned char* start;
int err;
start = state->_span_pos0;
state->_span_pos0 = NULL;
err = llhttp__on_version(state, start, p);
if (err != 0) {
state->error = err;
state->error_pos = (const char*) p;
state->_current = (void*) (intptr_t) s_n_llhttp__internal__n_error_93;
return s_error;
}
goto s_n_llhttp__internal__n_error_93;
UNREACHABLE;
}
s_n_llhttp__internal__n_invoke_load_http_minor_3: {
switch (llhttp__internal__c_load_http_minor(state, p, endp)) {
case 9:
goto s_n_llhttp__internal__n_span_end_llhttp__on_version_6;
default:
goto s_n_llhttp__internal__n_span_end_llhttp__on_version_5;
}
UNREACHABLE;
}
s_n_llhttp__internal__n_invoke_load_http_minor_4: {
switch (llhttp__internal__c_load_http_minor(state, p, endp)) {
case 0:
goto s_n_llhttp__internal__n_span_end_llhttp__on_version_6;
case 1:
goto s_n_llhttp__internal__n_span_end_llhttp__on_version_6;
default:
goto s_n_llhttp__internal__n_span_end_llhttp__on_version_5;
}
UNREACHABLE;
}
s_n_llhttp__internal__n_invoke_load_http_minor_5: {
switch (llhttp__internal__c_load_http_minor(state, p, endp)) {
case 0:
goto s_n_llhttp__internal__n_span_end_llhttp__on_version_6;
default:
goto s_n_llhttp__internal__n_span_end_llhttp__on_version_5;
}
UNREACHABLE;
}
s_n_llhttp__internal__n_invoke_load_http_major_1: {
switch (llhttp__internal__c_load_http_major(state, p, endp)) {
case 0:
goto s_n_llhttp__internal__n_invoke_load_http_minor_3;
case 1:
goto s_n_llhttp__internal__n_invoke_load_http_minor_4;
case 2:
goto s_n_llhttp__internal__n_invoke_load_http_minor_5;
default:
goto s_n_llhttp__internal__n_span_end_llhttp__on_version_5;
}
UNREACHABLE;
}
s_n_llhttp__internal__n_invoke_test_lenient_flags_27: {
switch (llhttp__internal__c_test_lenient_flags_24(state, p, endp)) {
case 1:
goto s_n_llhttp__internal__n_span_end_llhttp__on_version_6;
default:
goto s_n_llhttp__internal__n_invoke_load_http_major_1;
}
UNREACHABLE;
}
s_n_llhttp__internal__n_invoke_store_http_minor_1: {
switch (llhttp__internal__c_store_http_minor(state, p, endp, match)) {
default:
goto s_n_llhttp__internal__n_invoke_test_lenient_flags_27;
}
UNREACHABLE;
}
s_n_llhttp__internal__n_span_end_llhttp__on_version_7: {
const unsigned char* start;
int err;
start = state->_span_pos0;
state->_span_pos0 = NULL;
err = llhttp__on_version(state, start, p);
if (err != 0) {
state->error = err;
state->error_pos = (const char*) p;
state->_current = (void*) (intptr_t) s_n_llhttp__internal__n_error_107;
return s_error;
}
goto s_n_llhttp__internal__n_error_107;
UNREACHABLE;
}
s_n_llhttp__internal__n_span_end_llhttp__on_version_8: {
const unsigned char* start;
int err;
start = state->_span_pos0;
state->_span_pos0 = NULL;
err = llhttp__on_version(state, start, p);
if (err != 0) {
state->error = err;
state->error_pos = (const char*) p;
state->_current = (void*) (intptr_t) s_n_llhttp__internal__n_error_108;
return s_error;
}
goto s_n_llhttp__internal__n_error_108;
UNREACHABLE;
}
s_n_llhttp__internal__n_invoke_store_http_major_1: {
switch (llhttp__internal__c_store_http_major(state, p, endp, match)) {
default:
goto s_n_llhttp__internal__n_res_http_dot;
}
UNREACHABLE;
}
s_n_llhttp__internal__n_span_end_llhttp__on_version_9: {
const unsigned char* start;
int err;
start = state->_span_pos0;
state->_span_pos0 = NULL;
err = llhttp__on_version(state, start, p);
if (err != 0) {
state->error = err;
state->error_pos = (const char*) p;
state->_current = (void*) (intptr_t) s_n_llhttp__internal__n_error_109;
return s_error;
}
goto s_n_llhttp__internal__n_error_109;
UNREACHABLE;
}
s_n_llhttp__internal__n_error_114: {
state->error = 0x8;
state->reason = "Expected HTTP/, RTSP/ or ICE/";
state->error_pos = (const char*) p;
state->_current = (void*) (intptr_t) s_error;
return s_error;
UNREACHABLE;
}
s_n_llhttp__internal__n_pause_30: {
state->error = 0x15;
state->reason = "on_protocol_complete pause";
state->error_pos = (const char*) p;
state->_current = (void*) (intptr_t) s_n_llhttp__internal__n_res_after_protocol;
return s_error;
UNREACHABLE;
}
s_n_llhttp__internal__n_error_113: {
state->error = 0x26;
state->reason = "`on_protocol_complete` callback error";
state->error_pos = (const char*) p;
state->_current = (void*) (intptr_t) s_error;
return s_error;
UNREACHABLE;
}
s_n_llhttp__internal__n_span_end_llhttp__on_protocol_4: {
const unsigned char* start;
int err;
start = state->_span_pos0;
state->_span_pos0 = NULL;
err = llhttp__on_protocol(state, start, p);
if (err != 0) {
state->error = err;
state->error_pos = (const char*) p;
state->_current = (void*) (intptr_t) s_n_llhttp__internal__n_invoke_llhttp__on_protocol_complete_3;
return s_error;
}
goto s_n_llhttp__internal__n_invoke_llhttp__on_protocol_complete_3;
UNREACHABLE;
}
s_n_llhttp__internal__n_span_end_llhttp__on_protocol_5: {
const unsigned char* start;
int err;
start = state->_span_pos0;
state->_span_pos0 = NULL;
err = llhttp__on_protocol(state, start, p);
if (err != 0) {
state->error = err;
state->error_pos = (const char*) p;
state->_current = (void*) (intptr_t) s_n_llhttp__internal__n_error_115;
return s_error;
}
goto s_n_llhttp__internal__n_error_115;
UNREACHABLE;
}
s_n_llhttp__internal__n_pause_26: {
state->error = 0x15;
state->reason = "on_method_complete pause";
state->error_pos = (const char*) p;
state->_current = (void*) (intptr_t) s_n_llhttp__internal__n_req_first_space_before_url;
return s_error;
UNREACHABLE;
}
s_n_llhttp__internal__n_error_1: {
state->error = 0x20;
state->reason = "`on_method_complete` callback error";
state->error_pos = (const char*) p;
state->_current = (void*) (intptr_t) s_error;
return s_error;
UNREACHABLE;
}
s_n_llhttp__internal__n_span_end_llhttp__on_method: {
const unsigned char* start;
int err;
start = state->_span_pos0;
state->_span_pos0 = NULL;
err = llhttp__on_method(state, start, p);
if (err != 0) {
state->error = err;
state->error_pos = (const char*) p;
state->_current = (void*) (intptr_t) s_n_llhttp__internal__n_invoke_llhttp__on_method_complete;
return s_error;
}
goto s_n_llhttp__internal__n_invoke_llhttp__on_method_complete;
UNREACHABLE;
}
s_n_llhttp__internal__n_invoke_update_type: {
switch (llhttp__internal__c_update_type(state, p, endp)) {
default:
goto s_n_llhttp__internal__n_span_end_llhttp__on_method;
}
UNREACHABLE;
}
s_n_llhttp__internal__n_invoke_store_method: {
switch (llhttp__internal__c_store_method(state, p, endp, match)) {
default:
goto s_n_llhttp__internal__n_invoke_update_type;
}
UNREACHABLE;
}
s_n_llhttp__internal__n_error_110: {
state->error = 0x8;
state->reason = "Invalid word encountered";
state->error_pos = (const char*) p;
state->_current = (void*) (intptr_t) s_error;
return s_error;
UNREACHABLE;
}
s_n_llhttp__internal__n_span_end_llhttp__on_method_1: {
const unsigned char* start;
int err;
start = state->_span_pos0;
state->_span_pos0 = NULL;
err = llhttp__on_method(state, start, p);
if (err != 0) {
state->error = err;
state->error_pos = (const char*) p;
state->_current = (void*) (intptr_t) s_n_llhttp__internal__n_invoke_update_type_1;
return s_error;
}
goto s_n_llhttp__internal__n_invoke_update_type_1;
UNREACHABLE;
}
s_n_llhttp__internal__n_invoke_update_type_2: {
switch (llhttp__internal__c_update_type(state, p, endp)) {
default:
goto s_n_llhttp__internal__n_span_start_llhttp__on_method_1;
}
UNREACHABLE;
}
s_n_llhttp__internal__n_pause_31: {
state->error = 0x15;
state->reason = "on_message_begin pause";
state->error_pos = (const char*) p;
state->_current = (void*) (intptr_t) s_n_llhttp__internal__n_invoke_load_type;
return s_error;
UNREACHABLE;
}
s_n_llhttp__internal__n_error: {
state->error = 0x10;
state->reason = "`on_message_begin` callback error";
state->error_pos = (const char*) p;
state->_current = (void*) (intptr_t) s_error;
return s_error;
UNREACHABLE;
}
s_n_llhttp__internal__n_invoke_llhttp__on_message_begin: {
switch (llhttp__on_message_begin(state, p, endp)) {
case 0:
goto s_n_llhttp__internal__n_invoke_load_type;
case 21:
goto s_n_llhttp__internal__n_pause_31;
default:
goto s_n_llhttp__internal__n_error;
}
UNREACHABLE;
}
s_n_llhttp__internal__n_pause_32: {
state->error = 0x15;
state->reason = "on_reset pause";
state->error_pos = (const char*) p;
state->_current = (void*) (intptr_t) s_n_llhttp__internal__n_invoke_update_finish;
return s_error;
UNREACHABLE;
}
s_n_llhttp__internal__n_error_116: {
state->error = 0x1f;
state->reason = "`on_reset` callback error";
state->error_pos = (const char*) p;
state->_current = (void*) (intptr_t) s_error;
return s_error;
UNREACHABLE;
}
s_n_llhttp__internal__n_invoke_llhttp__on_reset: {
switch (llhttp__on_reset(state, p, endp)) {
case 0:
goto s_n_llhttp__internal__n_invoke_update_finish;
case 21:
goto s_n_llhttp__internal__n_pause_32;
default:
goto s_n_llhttp__internal__n_error_116;
}
UNREACHABLE;
}
s_n_llhttp__internal__n_invoke_load_initial_message_completed: {
switch (llhttp__internal__c_load_initial_message_completed(state, p, endp)) {
case 1:
goto s_n_llhttp__internal__n_invoke_llhttp__on_reset;
default:
goto s_n_llhttp__internal__n_invoke_update_finish;
}
UNREACHABLE;
}
}
int llhttp__internal_execute(llhttp__internal_t* state, const char* p, const char* endp) {
llparse_state_t next;
/* check lingering errors */
if (state->error != 0) {
return state->error;
}
/* restart spans */
if (state->_span_pos0 != NULL) {
state->_span_pos0 = (void*) p;
}
next = llhttp__internal__run(state, (const unsigned char*) p, (const unsigned char*) endp);
if (next == s_error) {
return state->error;
}
state->_current = (void*) (intptr_t) next;
/* execute spans */
if (state->_span_pos0 != NULL) {
int error;
error = ((llhttp__internal__span_cb) state->_span_cb0)(state, state->_span_pos0, (const char*) endp);
if (error != 0) {
state->error = error;
state->error_pos = endp;
return error;
}
}
return 0;
}
================================================
FILE: docs/.nojekyll
================================================
================================================
FILE: docs/CNAME
================================================
undici.nodejs.org
================================================
FILE: docs/docs/api/Agent.md
================================================
# Agent
Extends: `undici.Dispatcher`
Agent allows dispatching requests against multiple different origins.
Requests are not guaranteed to be dispatched in order of invocation.
## `new undici.Agent([options])`
Arguments:
* **options** `AgentOptions` (optional)
Returns: `Agent`
### Parameter: `AgentOptions`
Extends: [`PoolOptions`](/docs/docs/api/Pool.md#parameter-pooloptions)
* **factory** `(origin: URL, opts: Object) => Dispatcher` - Default: `(origin, opts) => new Pool(origin, opts)`
* **maxOrigins** `number` (optional) - Default: `Infinity` - Limits the total number of origins that can receive requests at a time, throwing an `MaxOriginsReachedError` error when attempting to dispatch when the max is reached. If `Infinity`, no limit is enforced.
## Instance Properties
### `Agent.closed`
Implements [Client.closed](/docs/docs/api/Client.md#clientclosed)
### `Agent.destroyed`
Implements [Client.destroyed](/docs/docs/api/Client.md#clientdestroyed)
## Instance Methods
### `Agent.close([callback])`
Implements [`Dispatcher.close([callback])`](/docs/docs/api/Dispatcher.md#dispatcherclosecallback-promise).
### `Agent.destroy([error, callback])`
Implements [`Dispatcher.destroy([error, callback])`](/docs/docs/api/Dispatcher.md#dispatcherdestroyerror-callback-promise).
### `Agent.dispatch(options, handler: AgentDispatchOptions)`
Implements [`Dispatcher.dispatch(options, handler)`](/docs/docs/api/Dispatcher.md#dispatcherdispatchoptions-handler).
#### Parameter: `AgentDispatchOptions`
Extends: [`DispatchOptions`](/docs/docs/api/Dispatcher.md#parameter-dispatchoptions)
* **origin** `string | URL`
Implements [`Dispatcher.destroy([error, callback])`](/docs/docs/api/Dispatcher.md#dispatcherdestroyerror-callback-promise).
### `Agent.connect(options[, callback])`
See [`Dispatcher.connect(options[, callback])`](/docs/docs/api/Dispatcher.md#dispatcherconnectoptions-callback).
### `Agent.dispatch(options, handler)`
Implements [`Dispatcher.dispatch(options, handler)`](/docs/docs/api/Dispatcher.md#dispatcherdispatchoptions-handler).
### `Agent.pipeline(options, handler)`
See [`Dispatcher.pipeline(options, handler)`](/docs/docs/api/Dispatcher.md#dispatcherpipelineoptions-handler).
### `Agent.request(options[, callback])`
See [`Dispatcher.request(options [, callback])`](/docs/docs/api/Dispatcher.md#dispatcherrequestoptions-callback).
### `Agent.stream(options, factory[, callback])`
See [`Dispatcher.stream(options, factory[, callback])`](/docs/docs/api/Dispatcher.md#dispatcherstreamoptions-factory-callback).
### `Agent.upgrade(options[, callback])`
See [`Dispatcher.upgrade(options[, callback])`](/docs/docs/api/Dispatcher.md#dispatcherupgradeoptions-callback).
### `Agent.stats()`
Returns an object of stats by origin in the format of `Record`
See [`PoolStats`](/docs/docs/api/PoolStats.md) and [`ClientStats`](/docs/docs/api/ClientStats.md).
================================================
FILE: docs/docs/api/BalancedPool.md
================================================
# Class: BalancedPool
Extends: `undici.Dispatcher`
A pool of [Pool](/docs/docs/api/Pool.md) instances connected to multiple upstreams.
Requests are not guaranteed to be dispatched in order of invocation.
## `new BalancedPool(upstreams [, options])`
Arguments:
* **upstreams** `URL | string | string[]` - It should only include the **protocol, hostname, and port**.
* **options** `BalancedPoolOptions` (optional)
### Parameter: `BalancedPoolOptions`
Extends: [`PoolOptions`](/docs/docs/api/Pool.md#parameter-pooloptions)
* **factory** `(origin: URL, opts: Object) => Dispatcher` - Default: `(origin, opts) => new Pool(origin, opts)`
The `PoolOptions` are passed to each of the `Pool` instances being created.
## Instance Properties
### `BalancedPool.upstreams`
Returns an array of upstreams that were previously added.
### `BalancedPool.closed`
Implements [Client.closed](/docs/docs/api/Client.md#clientclosed)
### `BalancedPool.destroyed`
Implements [Client.destroyed](/docs/docs/api/Client.md#clientdestroyed)
### `Pool.stats`
Returns [`PoolStats`](/docs/docs/api/PoolStats.md) instance for this pool.
## Instance Methods
### `BalancedPool.addUpstream(upstream)`
Add an upstream.
Arguments:
* **upstream** `string` - It should only include the **protocol, hostname, and port**.
### `BalancedPool.removeUpstream(upstream)`
Removes an upstream that was previously added.
### `BalancedPool.close([callback])`
Implements [`Dispatcher.close([callback])`](/docs/docs/api/Dispatcher.md#dispatcherclosecallback-promise).
### `BalancedPool.destroy([error, callback])`
Implements [`Dispatcher.destroy([error, callback])`](/docs/docs/api/Dispatcher.md#dispatcherdestroyerror-callback-promise).
### `BalancedPool.connect(options[, callback])`
See [`Dispatcher.connect(options[, callback])`](/docs/docs/api/Dispatcher.md#dispatcherconnectoptions-callback).
### `BalancedPool.dispatch(options, handlers)`
Implements [`Dispatcher.dispatch(options, handlers)`](/docs/docs/api/Dispatcher.md#dispatcherdispatchoptions-handler).
### `BalancedPool.pipeline(options, handler)`
See [`Dispatcher.pipeline(options, handler)`](/docs/docs/api/Dispatcher.md#dispatcherpipelineoptions-handler).
### `BalancedPool.request(options[, callback])`
See [`Dispatcher.request(options [, callback])`](/docs/docs/api/Dispatcher.md#dispatcherrequestoptions-callback).
### `BalancedPool.stream(options, factory[, callback])`
See [`Dispatcher.stream(options, factory[, callback])`](/docs/docs/api/Dispatcher.md#dispatcherstreamoptions-factory-callback).
### `BalancedPool.upgrade(options[, callback])`
See [`Dispatcher.upgrade(options[, callback])`](/docs/docs/api/Dispatcher.md#dispatcherupgradeoptions-callback).
## Instance Events
### Event: `'connect'`
See [Dispatcher Event: `'connect'`](/docs/docs/api/Dispatcher.md#event-connect).
### Event: `'disconnect'`
See [Dispatcher Event: `'disconnect'`](/docs/docs/api/Dispatcher.md#event-disconnect).
### Event: `'drain'`
See [Dispatcher Event: `'drain'`](/docs/docs/api/Dispatcher.md#event-drain).
================================================
FILE: docs/docs/api/CacheStorage.md
================================================
# CacheStorage
Undici exposes a W3C spec-compliant implementation of [CacheStorage](https://developer.mozilla.org/en-US/docs/Web/API/CacheStorage) and [Cache](https://developer.mozilla.org/en-US/docs/Web/API/Cache).
## Opening a Cache
Undici exports a top-level CacheStorage instance. You can open a new Cache, or duplicate a Cache with an existing name, by using `CacheStorage.prototype.open`. If you open a Cache with the same name as an already-existing Cache, its list of cached Responses will be shared between both instances.
```mjs
import { caches } from 'undici'
const cache_1 = await caches.open('v1')
const cache_2 = await caches.open('v1')
// Although .open() creates a new instance,
assert(cache_1 !== cache_2)
// The same Response is matched in both.
assert.deepStrictEqual(await cache_1.match('/req'), await cache_2.match('/req'))
```
## Deleting a Cache
If a Cache is deleted, the cached Responses/Requests can still be used.
```mjs
const response = await cache_1.match('/req')
await caches.delete('v1')
await response.text() // the Response's body
```
================================================
FILE: docs/docs/api/CacheStore.md
================================================
# Cache Store
A Cache Store is responsible for storing and retrieving cached responses.
It is also responsible for deciding which specific response to use based off of
a response's `Vary` header (if present). It is expected to be compliant with
[RFC-9111](https://www.rfc-editor.org/rfc/rfc9111.html).
## Pre-built Cache Stores
### `MemoryCacheStore`
The `MemoryCacheStore` stores the responses in-memory.
**Options**
- `maxSize` - The maximum total size in bytes of all stored responses. Default `104857600` (100MB).
- `maxCount` - The maximum amount of responses to store. Default `1024`.
- `maxEntrySize` - The maximum size in bytes that a response's body can be. If a response's body is greater than or equal to this, the response will not be cached. Default `5242880` (5MB).
### Getters
#### `MemoryCacheStore.size`
Returns the current total size in bytes of all stored responses.
### Methods
#### `MemoryCacheStore.isFull()`
Returns a boolean indicating whether the cache has reached its maximum size or count.
### Events
#### `'maxSizeExceeded'`
Emitted when the cache exceeds its maximum size or count limits. The event payload contains `size`, `maxSize`, `count`, and `maxCount` properties.
### `SqliteCacheStore`
The `SqliteCacheStore` stores the responses in a SQLite database.
Under the hood, it uses Node.js' [`node:sqlite`](https://nodejs.org/api/sqlite.html) api.
The `SqliteCacheStore` is only exposed if the `node:sqlite` api is present.
**Options**
- `location` - The location of the SQLite database to use. Default `:memory:`.
- `maxCount` - The maximum number of entries to store in the database. Default `Infinity`.
- `maxEntrySize` - The maximum size in bytes that a response's body can be. If a response's body is greater than or equal to this, the response will not be cached. Default `Infinity`.
## Defining a Custom Cache Store
The store must implement the following functions:
### Getter: `isFull`
Optional. This tells the cache interceptor if the store is full or not. If this is true,
the cache interceptor will not attempt to cache the response.
### Function: `get`
Parameters:
* **req** `Dispatcher.RequestOptions` - Incoming request
Returns: `GetResult | Promise | undefined` - If the request is cached, the cached response is returned. If the request's method is anything other than HEAD, the response is also returned.
If the request isn't cached, `undefined` is returned.
The `get` method may return a `Promise` for async cache stores (e.g. Redis-backed or remote stores). The cache interceptor handles both synchronous and asynchronous return values, including in revalidation paths (304 Not Modified handling and stale-while-revalidate background revalidation).
Response properties:
* **response** `CacheValue` - The cached response data.
* **body** `Readable | Iterable | undefined` - The response's body. This can be an array of `Buffer` chunks (with a `.values()` method) or a `Readable` stream. Both formats are supported in all code paths, including 304 revalidation.
### Function: `createWriteStream`
Parameters:
* **req** `Dispatcher.RequestOptions` - Incoming request
* **value** `CacheValue` - Response to store
Returns: `Writable | undefined` - If the store is full, return `undefined`. Otherwise, return a writable so that the cache interceptor can stream the body and trailers to the store.
## `CacheValue`
This is an interface containing the majority of a response's data (minus the body).
### Property `statusCode`
`number` - The response's HTTP status code.
### Property `statusMessage`
`string` - The response's HTTP status message.
### Property `rawHeaders`
`Buffer[]` - The response's headers.
### Property `vary`
`Record | undefined` - The headers defined by the response's `Vary` header
and their respective values for later comparison. Values are `null` when the
header specified in `Vary` was not present in the original request. These `null`
values are automatically filtered out during revalidation so they are not sent
as request headers.
For example, for a response like
```
Vary: content-encoding, accepts
content-encoding: utf8
accepts: application/json
```
This would be
```js
{
'content-encoding': 'utf8',
accepts: 'application/json'
}
```
If the original request did not include the `accepts` header:
```js
{
'content-encoding': 'utf8',
accepts: null
}
```
### Property `cachedAt`
`number` - Time in millis that this value was cached.
### Property `staleAt`
`number` - Time in millis that this value is considered stale.
### Property `deleteAt`
`number` - Time in millis that this value is to be deleted from the cache. This
is either the same sa staleAt or the `max-stale` caching directive.
The store must not return a response after the time defined in this property.
## `CacheStoreReadable`
This extends Node's [`Readable`](https://nodejs.org/api/stream.html#class-streamreadable)
and defines extra properties relevant to the cache interceptor.
### Getter: `value`
The response's [`CacheStoreValue`](/docs/docs/api/CacheStore.md#cachestorevalue)
## `CacheStoreWriteable`
This extends Node's [`Writable`](https://nodejs.org/api/stream.html#class-streamwritable)
and defines extra properties relevant to the cache interceptor.
### Setter: `rawTrailers`
If the response has trailers, the cache interceptor will pass them to the cache
interceptor through this method.
================================================
FILE: docs/docs/api/Client.md
================================================
# Class: Client
Extends: `undici.Dispatcher`
A basic HTTP/1.1 client, mapped on top a single TCP/TLS connection. Pipelining is disabled by default.
Requests are not guaranteed to be dispatched in order of invocation.
## `new Client(url[, options])`
Arguments:
* **url** `URL | string` - Should only include the **protocol, hostname, and port**.
* **options** `ClientOptions` (optional)
Returns: `Client`
### Parameter: `ClientOptions`
* **bodyTimeout** `number | null` (optional) - Default: `300e3` - The timeout after which a request will time out, in milliseconds. Monitors time between receiving body data. Use `0` to disable it entirely. Defaults to 300 seconds. Please note the `timeout` will be reset if you keep writing data to the socket everytime.
* **headersTimeout** `number | null` (optional) - Default: `300e3` - The amount of time, in milliseconds, the parser will wait to receive the complete HTTP headers while not sending the request. Defaults to 300 seconds.
* **keepAliveMaxTimeout** `number | null` (optional) - Default: `600e3` - The maximum allowed `keepAliveTimeout`, in milliseconds, when overridden by *keep-alive* hints from the server. Defaults to 10 minutes.
* **keepAliveTimeout** `number | null` (optional) - Default: `4e3` - The timeout, in milliseconds, after which a socket without active requests will time out. Monitors time between activity on a connected socket. This value may be overridden by *keep-alive* hints from the server. See [MDN: HTTP - Headers - Keep-Alive directives](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Keep-Alive#directives) for more details. Defaults to 4 seconds.
* **keepAliveTimeoutThreshold** `number | null` (optional) - Default: `2e3` - A number of milliseconds subtracted from server *keep-alive* hints when overriding `keepAliveTimeout` to account for timing inaccuracies caused by e.g. transport latency. Defaults to 2 seconds.
* **maxHeaderSize** `number | null` (optional) - Default: `--max-http-header-size` or `16384` - The maximum length of request headers in bytes. Defaults to Node.js' --max-http-header-size or 16KiB.
* **maxResponseSize** `number | null` (optional) - Default: `-1` - The maximum length of response body in bytes. Set to `-1` to disable.
* **pipelining** `number | null` (optional) - Default: `1` - The amount of concurrent requests to be sent over the single TCP/TLS connection according to [RFC7230](https://tools.ietf.org/html/rfc7230#section-6.3.2). Carefully consider your workload and environment before enabling concurrent requests as pipelining may reduce performance if used incorrectly. Pipelining is sensitive to network stack settings as well as head of line blocking caused by e.g. long running requests. Set to `0` to disable keep-alive connections.
* **connect** `ConnectOptions | Function | null` (optional) - Default: `null`.
* **strictContentLength** `Boolean` (optional) - Default: `true` - Whether to treat request content length mismatches as errors. If true, an error is thrown when the request content-length header doesn't match the length of the request body. **Security Warning:** Disabling this option can expose your application to HTTP Request Smuggling attacks, where mismatched content-length headers cause servers and proxies to interpret request boundaries differently. This can lead to cache poisoning, credential hijacking, and bypassing security controls. Only disable this in controlled environments where you fully trust the request source.
* **autoSelectFamily**: `boolean` (optional) - Default: depends on local Node version, on Node 18.13.0 and above is `false`. Enables a family autodetection algorithm that loosely implements section 5 of [RFC 8305](https://tools.ietf.org/html/rfc8305#section-5). See [here](https://nodejs.org/api/net.html#socketconnectoptions-connectlistener) for more details. This option is ignored if not supported by the current Node version.
* **autoSelectFamilyAttemptTimeout**: `number` - Default: depends on local Node version, on Node 18.13.0 and above is `250`. The amount of time in milliseconds to wait for a connection attempt to finish before trying the next address when using the `autoSelectFamily` option. See [here](https://nodejs.org/api/net.html#socketconnectoptions-connectlistener) for more details.
* **allowH2**: `boolean` - Default: `false`. Enables support for H2 if the server has assigned bigger priority to it through ALPN negotiation.
* **useH2c**: `boolean` - Default: `false`. Enforces h2c for non-https connections.
* **maxConcurrentStreams**: `number` - Default: `100`. Dictates the maximum number of concurrent streams for a single H2 session. It can be overridden by a SETTINGS remote frame.
* **initialWindowSize**: `number` (optional) - Default: `262144` (256KB). Sets the HTTP/2 stream-level flow-control window size (SETTINGS_INITIAL_WINDOW_SIZE). Must be a positive integer greater than 0. This default is higher than Node.js core's default (65535 bytes) to improve throughput, Node's choice is very conservative for current high-bandwith networks. See [RFC 7540 Section 6.9.2](https://datatracker.ietf.org/doc/html/rfc7540#section-6.9.2) for more details.
* **connectionWindowSize**: `number` (optional) - Default `524288` (512KB). Sets the HTTP/2 connection-level flow-control window size using `ClientHttp2Session.setLocalWindowSize()`. Must be a positive integer greater than 0. This provides better flow control for the entire connection across multiple streams. See [Node.js HTTP/2 documentation](https://nodejs.org/api/http2.html#clienthttp2sessionsetlocalwindowsize) for more details.
* **pingInterval**: `number` - Default: `60e3`. The time interval in milliseconds between PING frames sent to the server. Set to `0` to disable PING frames. This is only applicable for HTTP/2 connections. This will emit a `ping` event on the client with the duration of the ping in milliseconds.
> **Notes about HTTP/2**
> - It only works under TLS connections. h2c is not supported.
> - The server must support HTTP/2 and choose it as the protocol during the ALPN negotiation.
> - The server must not have a bigger priority for HTTP/1.1 than HTTP/2.
> - Pseudo headers are automatically attached to the request. If you try to set them, they will be overwritten.
> - The `:path` header is automatically set to the request path.
> - The `:method` header is automatically set to the request method.
> - The `:scheme` header is automatically set to the request scheme.
> - The `:authority` header is automatically set to the request `host[:port]`.
> - `PUSH` frames are yet not supported.
#### Parameter: `ConnectOptions`
Every Tls option, see [here](https://nodejs.org/api/tls.html#tls_tls_connect_options_callback).
Furthermore, the following options can be passed:
* **socketPath** `string | null` (optional) - Default: `null` - An IPC endpoint, either Unix domain socket or Windows named pipe.
* **maxCachedSessions** `number | null` (optional) - Default: `100` - Maximum number of TLS cached sessions. Use 0 to disable TLS session caching. Default: 100.
* **timeout** `number | null` (optional) - In milliseconds, Default `10e3`.
* **servername** `string | null` (optional)
* **keepAlive** `boolean | null` (optional) - Default: `true` - TCP keep-alive enabled
* **keepAliveInitialDelay** `number | null` (optional) - Default: `60000` - TCP keep-alive interval for the socket in milliseconds
### Example - Basic Client instantiation
This will instantiate the undici Client, but it will not connect to the origin until something is queued. Consider using `client.connect` to prematurely connect to the origin, or just call `client.request`.
```js
'use strict'
import { Client } from 'undici'
const client = new Client('http://localhost:3000')
```
### Example - Custom connector
This will allow you to perform some additional check on the socket that will be used for the next request.
```js
'use strict'
import { Client, buildConnector } from 'undici'
const connector = buildConnector({ rejectUnauthorized: false })
const client = new Client('https://localhost:3000', {
connect (opts, cb) {
connector(opts, (err, socket) => {
if (err) {
cb(err)
} else if (/* assertion */) {
socket.destroy()
cb(new Error('kaboom'))
} else {
cb(null, socket)
}
})
}
})
```
## Instance Methods
### `Client.close([callback])`
Implements [`Dispatcher.close([callback])`](/docs/docs/api/Dispatcher.md#dispatcherclosecallback-promise).
### `Client.destroy([error, callback])`
Implements [`Dispatcher.destroy([error, callback])`](/docs/docs/api/Dispatcher.md#dispatcherdestroyerror-callback-promise).
Waits until socket is closed before invoking the callback (or returning a promise if no callback is provided).
### `Client.connect(options[, callback])`
See [`Dispatcher.connect(options[, callback])`](/docs/docs/api/Dispatcher.md#dispatcherconnectoptions-callback).
### `Client.dispatch(options, handlers)`
Implements [`Dispatcher.dispatch(options, handlers)`](/docs/docs/api/Dispatcher.md#dispatcherdispatchoptions-handler).
### `Client.pipeline(options, handler)`
See [`Dispatcher.pipeline(options, handler)`](/docs/docs/api/Dispatcher.md#dispatcherpipelineoptions-handler).
### `Client.request(options[, callback])`
See [`Dispatcher.request(options [, callback])`](/docs/docs/api/Dispatcher.md#dispatcherrequestoptions-callback).
### `Client.stream(options, factory[, callback])`
See [`Dispatcher.stream(options, factory[, callback])`](/docs/docs/api/Dispatcher.md#dispatcherstreamoptions-factory-callback).
### `Client.upgrade(options[, callback])`
See [`Dispatcher.upgrade(options[, callback])`](/docs/docs/api/Dispatcher.md#dispatcherupgradeoptions-callback).
## Instance Properties
### `Client.closed`
* `boolean`
`true` after `client.close()` has been called.
### `Client.destroyed`
* `boolean`
`true` after `client.destroyed()` has been called or `client.close()` has been called and the client shutdown has completed.
### `Client.pipelining`
* `number`
Property to get and set the pipelining factor.
## Instance Events
### Event: `'connect'`
See [Dispatcher Event: `'connect'`](/docs/docs/api/Dispatcher.md#event-connect).
Parameters:
* **origin** `URL`
* **targets** `Array`
Emitted when a socket has been created and connected. The client will connect once `client.size > 0`.
#### Example - Client connect event
```js
import { createServer } from 'http'
import { Client } from 'undici'
import { once } from 'events'
const server = createServer((request, response) => {
response.end('Hello, World!')
}).listen()
await once(server, 'listening')
const client = new Client(`http://localhost:${server.address().port}`)
client.on('connect', (origin) => {
console.log(`Connected to ${origin}`) // should print before the request body statement
})
try {
const { body } = await client.request({
path: '/',
method: 'GET'
})
body.setEncoding('utf-8')
body.on('data', console.log)
client.close()
server.close()
} catch (error) {
console.error(error)
client.close()
server.close()
}
```
### Event: `'disconnect'`
See [Dispatcher Event: `'disconnect'`](/docs/docs/api/Dispatcher.md#event-disconnect).
Parameters:
* **origin** `URL`
* **targets** `Array`
* **error** `Error`
Emitted when socket has disconnected. The error argument of the event is the error which caused the socket to disconnect. The client will reconnect if or once `client.size > 0`.
#### Example - Client disconnect event
```js
import { createServer } from 'http'
import { Client } from 'undici'
import { once } from 'events'
const server = createServer((request, response) => {
response.destroy()
}).listen()
await once(server, 'listening')
const client = new Client(`http://localhost:${server.address().port}`)
client.on('disconnect', (origin) => {
console.log(`Disconnected from ${origin}`)
})
try {
await client.request({
path: '/',
method: 'GET'
})
} catch (error) {
console.error(error.message)
client.close()
server.close()
}
```
### Event: `'drain'`
Emitted when pipeline is no longer busy.
See [Dispatcher Event: `'drain'`](/docs/docs/api/Dispatcher.md#event-drain).
#### Example - Client drain event
```js
import { createServer } from 'http'
import { Client } from 'undici'
import { once } from 'events'
const server = createServer((request, response) => {
response.end('Hello, World!')
}).listen()
await once(server, 'listening')
const client = new Client(`http://localhost:${server.address().port}`)
client.on('drain', () => {
console.log('drain event')
client.close()
server.close()
})
const requests = [
client.request({ path: '/', method: 'GET' }),
client.request({ path: '/', method: 'GET' }),
client.request({ path: '/', method: 'GET' })
]
await Promise.all(requests)
console.log('requests completed')
```
### Event: `'error'`
Invoked for users errors such as throwing in the `onError` handler.
================================================
FILE: docs/docs/api/ClientStats.md
================================================
# Class: ClientStats
Stats for a [Client](/docs/docs/api/Client.md).
## `new ClientStats(client)`
Arguments:
* **client** `Client` - Client from which to return stats.
## Instance Properties
### `ClientStats.connected`
Boolean if socket as open connection by this client.
### `ClientStats.pending`
Number of pending requests of this client.
### `ClientStats.running`
Number of currently active requests across this client.
### `ClientStats.size`
Number of active, pending, or queued requests of this clients.
================================================
FILE: docs/docs/api/Connector.md
================================================
# Connector
Undici creates the underlying socket via the connector builder.
Normally, this happens automatically and you don't need to care about this,
but if you need to perform some additional check over the currently used socket,
this is the right place.
If you want to create a custom connector, you must import the `buildConnector` utility.
#### Parameter: `buildConnector.BuildOptions`
Every Tls option, see [here](https://nodejs.org/api/tls.html#tls_tls_connect_options_callback).
Furthermore, the following options can be passed:
* **socketPath** `string | null` (optional) - Default: `null` - An IPC endpoint, either Unix domain socket or Windows named pipe.
* **maxCachedSessions** `number | null` (optional) - Default: `100` - Maximum number of TLS cached sessions. Use 0 to disable TLS session caching. Default: `100`.
* **timeout** `number | null` (optional) - In milliseconds. Default `10e3`.
* **servername** `string | null` (optional)
Once you call `buildConnector`, it will return a connector function, which takes the following parameters.
#### Parameter: `connector.Options`
* **hostname** `string` (required)
* **host** `string` (optional)
* **protocol** `string` (required)
* **port** `string` (required)
* **servername** `string` (optional)
* **localAddress** `string | null` (optional) Local address the socket should connect from.
* **httpSocket** `Socket` (optional) Establish secure connection on a given socket rather than creating a new socket. It can only be sent on TLS update.
### Basic example
```js
'use strict'
import { Client, buildConnector } from 'undici'
const connector = buildConnector({ rejectUnauthorized: false })
const client = new Client('https://localhost:3000', {
connect (opts, cb) {
connector(opts, (err, socket) => {
if (err) {
cb(err)
} else if (/* assertion */) {
socket.destroy()
cb(new Error('kaboom'))
} else {
cb(null, socket)
}
})
}
})
```
### Example: validate the CA fingerprint
```js
'use strict'
import { Client, buildConnector } from 'undici'
const caFingerprint = 'FO:OB:AR'
const connector = buildConnector({ rejectUnauthorized: false })
const client = new Client('https://localhost:3000', {
connect (opts, cb) {
connector(opts, (err, socket) => {
if (err) {
cb(err)
} else if (getIssuerCertificate(socket).fingerprint256 !== caFingerprint) {
socket.destroy()
cb(new Error('Fingerprint does not match or malformed certificate'))
} else {
cb(null, socket)
}
})
}
})
client.request({
path: '/',
method: 'GET'
}, (err, data) => {
if (err) throw err
const bufs = []
data.body.on('data', (buf) => {
bufs.push(buf)
})
data.body.on('end', () => {
console.log(Buffer.concat(bufs).toString('utf8'))
client.close()
})
})
function getIssuerCertificate (socket) {
let certificate = socket.getPeerCertificate(true)
while (certificate && Object.keys(certificate).length > 0) {
// invalid certificate
if (certificate.issuerCertificate == null) {
return null
}
// We have reached the root certificate.
// In case of self-signed certificates, `issuerCertificate` may be a circular reference.
if (certificate.fingerprint256 === certificate.issuerCertificate.fingerprint256) {
break
}
// continue the loop
certificate = certificate.issuerCertificate
}
return certificate
}
```
================================================
FILE: docs/docs/api/ContentType.md
================================================
# MIME Type Parsing
## `MIMEType` interface
* **type** `string`
* **subtype** `string`
* **parameters** `Map`
* **essence** `string`
## `parseMIMEType(input)`
Implements [parse a MIME type](https://mimesniff.spec.whatwg.org/#parse-a-mime-type).
Parses a MIME type, returning its type, subtype, and any associated parameters. If the parser can't parse an input it returns the string literal `'failure'`.
```js
import { parseMIMEType } from 'undici'
parseMIMEType('text/html; charset=gbk')
// {
// type: 'text',
// subtype: 'html',
// parameters: Map(1) { 'charset' => 'gbk' },
// essence: 'text/html'
// }
```
Arguments:
* **input** `string`
Returns: `MIMEType|'failure'`
## `serializeAMimeType(input)`
Implements [serialize a MIME type](https://mimesniff.spec.whatwg.org/#serialize-a-mime-type).
Serializes a MIMEType object.
```js
import { serializeAMimeType } from 'undici'
serializeAMimeType({
type: 'text',
subtype: 'html',
parameters: new Map([['charset', 'gbk']]),
essence: 'text/html'
})
// text/html;charset=gbk
```
Arguments:
* **mimeType** `MIMEType`
Returns: `string`
================================================
FILE: docs/docs/api/Cookies.md
================================================
# Cookie Handling
## `Cookie` interface
* **name** `string`
* **value** `string`
* **expires** `Date|number` (optional)
* **maxAge** `number` (optional)
* **domain** `string` (optional)
* **path** `string` (optional)
* **secure** `boolean` (optional)
* **httpOnly** `boolean` (optional)
* **sameSite** `'String'|'Lax'|'None'` (optional)
* **unparsed** `string[]` (optional) Left over attributes that weren't parsed.
## `deleteCookie(headers, name[, attributes])`
Sets the expiry time of the cookie to the unix epoch, causing browsers to delete it when received.
```js
import { deleteCookie, Headers } from 'undici'
const headers = new Headers()
deleteCookie(headers, 'name')
console.log(headers.get('set-cookie')) // name=; Expires=Thu, 01 Jan 1970 00:00:00 GMT
```
Arguments:
* **headers** `Headers`
* **name** `string`
* **attributes** `{ path?: string, domain?: string }` (optional)
Returns: `void`
## `getCookies(headers)`
Parses the `Cookie` header and returns a list of attributes and values.
```js
import { getCookies, Headers } from 'undici'
const headers = new Headers({
cookie: 'get=cookies; and=attributes'
})
console.log(getCookies(headers)) // { get: 'cookies', and: 'attributes' }
```
Arguments:
* **headers** `Headers`
Returns: `Record`
## `getSetCookies(headers)`
Parses all `Set-Cookie` headers.
```js
import { getSetCookies, Headers } from 'undici'
const headers = new Headers({ 'set-cookie': 'undici=getSetCookies; Secure' })
console.log(getSetCookies(headers))
// [
// {
// name: 'undici',
// value: 'getSetCookies',
// secure: true
// }
// ]
```
Arguments:
* **headers** `Headers`
Returns: `Cookie[]`
## `setCookie(headers, cookie)`
Appends a cookie to the `Set-Cookie` header.
```js
import { setCookie, Headers } from 'undici'
const headers = new Headers()
setCookie(headers, { name: 'undici', value: 'setCookie' })
console.log(headers.get('Set-Cookie')) // undici=setCookie
```
Arguments:
* **headers** `Headers`
* **cookie** `Cookie`
Returns: `void`
================================================
FILE: docs/docs/api/Debug.md
================================================
# Debug
Undici (and subsenquently `fetch` and `websocket`) exposes a debug statement that can be enabled by setting `NODE_DEBUG` within the environment.
The flags available are:
## `undici`
This flag enables debug statements for the core undici library.
```sh
NODE_DEBUG=undici node script.js
UNDICI 16241: connecting to nodejs.org using https:h1
UNDICI 16241: connecting to nodejs.org using https:h1
UNDICI 16241: connected to nodejs.org using https:h1
UNDICI 16241: sending request to GET https://nodejs.org/
UNDICI 16241: received response to GET https://nodejs.org/ - HTTP 307
UNDICI 16241: connecting to nodejs.org using https:h1
UNDICI 16241: trailers received from GET https://nodejs.org/
UNDICI 16241: connected to nodejs.org using https:h1
UNDICI 16241: sending request to GET https://nodejs.org/en
UNDICI 16241: received response to GET https://nodejs.org/en - HTTP 200
UNDICI 16241: trailers received from GET https://nodejs.org/en
```
## `fetch`
This flag enables debug statements for the `fetch` API.
> **Note**: statements are pretty similar to the ones in the `undici` flag, but scoped to `fetch`
```sh
NODE_DEBUG=fetch node script.js
FETCH 16241: connecting to nodejs.org using https:h1
FETCH 16241: connecting to nodejs.org using https:h1
FETCH 16241: connected to nodejs.org using https:h1
FETCH 16241: sending request to GET https://nodejs.org/
FETCH 16241: received response to GET https://nodejs.org/ - HTTP 307
FETCH 16241: connecting to nodejs.org using https:h1
FETCH 16241: trailers received from GET https://nodejs.org/
FETCH 16241: connected to nodejs.org using https:h1
FETCH 16241: sending request to GET https://nodejs.org/en
FETCH 16241: received response to GET https://nodejs.org/en - HTTP 200
FETCH 16241: trailers received from GET https://nodejs.org/en
```
## `websocket`
This flag enables debug statements for the `Websocket` API.
> **Note**: statements can overlap with `UNDICI` ones if `undici` or `fetch` flag has been enabled as well.
```sh
NODE_DEBUG=websocket node script.js
WEBSOCKET 18309: connecting to echo.websocket.org using https:h1
WEBSOCKET 18309: connected to echo.websocket.org using https:h1
WEBSOCKET 18309: sending request to GET https://echo.websocket.org/
WEBSOCKET 18309: connection opened
```
================================================
FILE: docs/docs/api/DiagnosticsChannel.md
================================================
# Diagnostics Channel Support
Stability: Experimental.
Undici supports the [`diagnostics_channel`](https://nodejs.org/api/diagnostics_channel.html) (currently available only on Node.js v16+).
It is the preferred way to instrument Undici and retrieve internal information.
The channels available are the following.
## `undici:request:create`
This message is published when a new outgoing request is created.
```js
import diagnosticsChannel from 'diagnostics_channel'
diagnosticsChannel.channel('undici:request:create').subscribe(({ request }) => {
console.log('origin', request.origin)
console.log('completed', request.completed)
console.log('method', request.method)
console.log('path', request.path)
console.log('headers', request.headers) // array of strings, e.g: ['foo', 'bar']
request.addHeader('hello', 'world')
console.log('headers', request.headers) // e.g. ['foo', 'bar', 'hello', 'world']
})
```
Note: a request is only loosely completed to a given socket.
## `undici:request:bodyChunkSent`
This message is published when a chunk of the request body is being sent.
```js
import diagnosticsChannel from 'diagnostics_channel'
diagnosticsChannel.channel('undici:request:bodyChunkSent').subscribe(({ request, chunk }) => {
// request is the same object undici:request:create
})
```
## `undici:request:bodySent`
This message is published after the request body has been fully sent.
```js
import diagnosticsChannel from 'diagnostics_channel'
diagnosticsChannel.channel('undici:request:bodySent').subscribe(({ request }) => {
// request is the same object undici:request:create
})
```
## `undici:request:headers`
This message is published after the response headers have been received.
```js
import diagnosticsChannel from 'diagnostics_channel'
diagnosticsChannel.channel('undici:request:headers').subscribe(({ request, response }) => {
// request is the same object undici:request:create
console.log('statusCode', response.statusCode)
console.log(response.statusText)
// response.headers are buffers.
console.log(response.headers.map((x) => x.toString()))
})
```
## `undici:request:bodyChunkReceived`
This message is published after a chunk of the response body has been received.
```js
import diagnosticsChannel from 'diagnostics_channel'
diagnosticsChannel.channel('undici:request:bodyChunkReceived').subscribe(({ request, chunk }) => {
// request is the same object undici:request:create
})
```
## `undici:request:trailers`
This message is published after the response body and trailers have been received, i.e. the response has been completed.
```js
import diagnosticsChannel from 'diagnostics_channel'
diagnosticsChannel.channel('undici:request:trailers').subscribe(({ request, trailers }) => {
// request is the same object undici:request:create
console.log('completed', request.completed)
// trailers are buffers.
console.log(trailers.map((x) => x.toString()))
})
```
## `undici:request:error`
This message is published if the request is going to error, but it has not errored yet.
```js
import diagnosticsChannel from 'diagnostics_channel'
diagnosticsChannel.channel('undici:request:error').subscribe(({ request, error }) => {
// request is the same object undici:request:create
})
```
## `undici:client:sendHeaders`
This message is published right before the first byte of the request is written to the socket.
*Note*: It will publish the exact headers that will be sent to the server in raw format.
```js
import diagnosticsChannel from 'diagnostics_channel'
diagnosticsChannel.channel('undici:client:sendHeaders').subscribe(({ request, headers, socket }) => {
// request is the same object undici:request:create
console.log(`Full headers list ${headers.split('\r\n')}`);
})
```
## `undici:client:beforeConnect`
This message is published before creating a new connection for **any** request.
You can not assume that this event is related to any specific request.
```js
import diagnosticsChannel from 'diagnostics_channel'
diagnosticsChannel.channel('undici:client:beforeConnect').subscribe(({ connectParams, connector }) => {
// const { host, hostname, protocol, port, servername, version } = connectParams
// connector is a function that creates the socket
})
```
## `undici:client:connected`
This message is published after a connection is established.
```js
import diagnosticsChannel from 'diagnostics_channel'
diagnosticsChannel.channel('undici:client:connected').subscribe(({ socket, connectParams, connector }) => {
// const { host, hostname, protocol, port, servername, version } = connectParams
// connector is a function that creates the socket
})
```
## `undici:client:connectError`
This message is published if it did not succeed to create new connection
```js
import diagnosticsChannel from 'diagnostics_channel'
diagnosticsChannel.channel('undici:client:connectError').subscribe(({ error, socket, connectParams, connector }) => {
// const { host, hostname, protocol, port, servername, version } = connectParams
// connector is a function that creates the socket
console.log(`Connect failed with ${error.message}`)
})
```
## `undici:websocket:open`
This message is published after the client has successfully connected to a server.
```js
import diagnosticsChannel from 'diagnostics_channel'
diagnosticsChannel.channel('undici:websocket:open').subscribe(({
address, // { address: string, family: string, port: number }
protocol, // string - negotiated subprotocol
extensions, // string - negotiated extensions
websocket, // WebSocket - the WebSocket instance
handshakeResponse // object - HTTP response that upgraded the connection
}) => {
console.log(address) // address, family, and port
console.log(protocol) // negotiated subprotocols
console.log(extensions) // negotiated extensions
console.log(websocket) // the WebSocket instance
// Handshake response details
console.log(handshakeResponse.status) // 101 for successful WebSocket upgrade
console.log(handshakeResponse.statusText) // 'Switching Protocols'
console.log(handshakeResponse.headers) // Object containing response headers
})
```
### Handshake Response Object
The `handshakeResponse` object contains the HTTP response that upgraded the connection to WebSocket:
- `status` (number): The HTTP status code (101 for successful WebSocket upgrade)
- `statusText` (string): The HTTP status message ('Switching Protocols' for successful upgrade)
- `headers` (object): The HTTP response headers from the server, including:
- `upgrade: 'websocket'`
- `connection: 'upgrade'`
- `sec-websocket-accept` and other WebSocket-related headers
This information is particularly useful for debugging and monitoring WebSocket connections, as it provides access to the initial HTTP handshake response that established the WebSocket connection.
## `undici:websocket:close`
This message is published after the connection has closed.
```js
import diagnosticsChannel from 'diagnostics_channel'
diagnosticsChannel.channel('undici:websocket:close').subscribe(({ websocket, code, reason }) => {
console.log(websocket) // the WebSocket instance
console.log(code) // the closing status code
console.log(reason) // the closing reason
})
```
## `undici:websocket:socket_error`
This message is published if the socket experiences an error.
```js
import diagnosticsChannel from 'diagnostics_channel'
diagnosticsChannel.channel('undici:websocket:socket_error').subscribe((error) => {
console.log(error)
})
```
## `undici:websocket:ping`
This message is published after the client receives a ping frame, if the connection is not closing.
```js
import diagnosticsChannel from 'diagnostics_channel'
diagnosticsChannel.channel('undici:websocket:ping').subscribe(({ payload, websocket }) => {
// a Buffer or undefined, containing the optional application data of the frame
console.log(payload)
console.log(websocket) // the WebSocket instance
})
```
## `undici:websocket:pong`
This message is published after the client receives a pong frame.
```js
import diagnosticsChannel from 'diagnostics_channel'
diagnosticsChannel.channel('undici:websocket:pong').subscribe(({ payload, websocket }) => {
// a Buffer or undefined, containing the optional application data of the frame
console.log(payload)
console.log(websocket) // the WebSocket instance
})
```
## `undici:proxy:connected`
This message is published after the `ProxyAgent` establishes a connection to the proxy server.
```js
import diagnosticsChannel from 'diagnostics_channel'
diagnosticsChannel.channel('undici:proxy:connected').subscribe(({ socket, connectParams }) => {
console.log(socket)
console.log(connectParams)
// const { origin, port, path, signal, headers, servername } = connectParams
})
```
## `undici:request:pending-requests`
This message is published when the deduplicate interceptor's pending request map changes. This is useful for monitoring and debugging request deduplication behavior.
The deduplicate interceptor automatically deduplicates concurrent requests for the same resource. When multiple identical requests are made while one is already in-flight, only one request is sent to the origin server, and all waiting handlers receive the same response.
```js
import diagnosticsChannel from 'diagnostics_channel'
diagnosticsChannel.channel('undici:request:pending-requests').subscribe(({ type, size, key }) => {
console.log(type) // 'added' or 'removed'
console.log(size) // current number of pending requests
console.log(key) // the deduplication key for this request
})
```
### Event Properties
- `type` (`string`): Either `'added'` when a new pending request is registered, or `'removed'` when a pending request completes (successfully or with an error).
- `size` (`number`): The current number of pending requests after the change.
- `key` (`string`): The deduplication key for the request, composed of the origin, method, path, and request headers.
### Example: Monitoring Request Deduplication
```js
import diagnosticsChannel from 'diagnostics_channel'
const channel = diagnosticsChannel.channel('undici:request:pending-requests')
channel.subscribe(({ type, size, key }) => {
if (type === 'added') {
console.log(`New pending request: ${key} (${size} total pending)`)
} else {
console.log(`Request completed: ${key} (${size} remaining)`)
}
})
```
This can be useful for:
- Verifying that request deduplication is working as expected
- Monitoring the number of concurrent in-flight requests
- Debugging deduplication behavior in production environments
================================================
FILE: docs/docs/api/Dispatcher.md
================================================
# Dispatcher
Extends: `events.EventEmitter`
Dispatcher is the core API used to dispatch requests.
Requests are not guaranteed to be dispatched in order of invocation.
## Instance Methods
### `Dispatcher.close([callback]): Promise`
Closes the dispatcher and gracefully waits for enqueued requests to complete before resolving.
Arguments:
* **callback** `(error: Error | null, data: null) => void` (optional)
Returns: `void | Promise` - Only returns a `Promise` if no `callback` argument was passed
```js
dispatcher.close() // -> Promise
dispatcher.close(() => {}) // -> void
```
#### Example - Request resolves before Client closes
```js
import { createServer } from 'http'
import { Client } from 'undici'
import { once } from 'events'
const server = createServer((request, response) => {
response.end('undici')
}).listen()
await once(server, 'listening')
const client = new Client(`http://localhost:${server.address().port}`)
try {
const { body } = await client.request({
path: '/',
method: 'GET'
})
body.setEncoding('utf8')
body.on('data', console.log)
} catch (error) {}
await client.close()
console.log('Client closed')
server.close()
```
### `Dispatcher.connect(options[, callback])`
Starts two-way communications with the requested resource using [HTTP CONNECT](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/CONNECT).
Arguments:
* **options** `ConnectOptions`
* **callback** `(err: Error | null, data: ConnectData | null) => void` (optional)
Returns: `void | Promise` - Only returns a `Promise` if no `callback` argument was passed
#### Parameter: `ConnectOptions`
* **path** `string`
* **headers** `UndiciHeaders` (optional) - Default: `null`
* **signal** `AbortSignal | events.EventEmitter | null` (optional) - Default: `null`
* **opaque** `unknown` (optional) - This argument parameter is passed through to `ConnectData`
#### Parameter: `ConnectData`
* **statusCode** `number`
* **headers** `Record`
* **socket** `stream.Duplex`
* **opaque** `unknown`
#### Example - Connect request with echo
```js
import { createServer } from 'http'
import { Client } from 'undici'
import { once } from 'events'
const server = createServer((request, response) => {
throw Error('should never get here')
}).listen()
server.on('connect', (req, socket, head) => {
socket.write('HTTP/1.1 200 Connection established\r\n\r\n')
let data = head.toString()
socket.on('data', (buf) => {
data += buf.toString()
})
socket.on('end', () => {
socket.end(data)
})
})
await once(server, 'listening')
const client = new Client(`http://localhost:${server.address().port}`)
try {
const { socket } = await client.connect({
path: '/'
})
const wanted = 'Body'
let data = ''
socket.on('data', d => { data += d })
socket.on('end', () => {
console.log(`Data received: ${data.toString()} | Data wanted: ${wanted}`)
client.close()
server.close()
})
socket.write(wanted)
socket.end()
} catch (error) { }
```
### `Dispatcher.destroy([error, callback]): Promise`
Destroy the dispatcher abruptly with the given error. All the pending and running requests will be asynchronously aborted and error. Since this operation is asynchronously dispatched there might still be some progress on dispatched requests.
Both arguments are optional; the method can be called in four different ways:
Arguments:
* **error** `Error | null` (optional)
* **callback** `(error: Error | null, data: null) => void` (optional)
Returns: `void | Promise` - Only returns a `Promise` if no `callback` argument was passed
```js
dispatcher.destroy() // -> Promise
dispatcher.destroy(new Error()) // -> Promise
dispatcher.destroy(() => {}) // -> void
dispatcher.destroy(new Error(), () => {}) // -> void
```
#### Example - Request is aborted when Client is destroyed
```js
import { createServer } from 'http'
import { Client } from 'undici'
import { once } from 'events'
const server = createServer((request, response) => {
response.end()
}).listen()
await once(server, 'listening')
const client = new Client(`http://localhost:${server.address().port}`)
try {
const request = client.request({
path: '/',
method: 'GET'
})
client.destroy()
.then(() => {
console.log('Client destroyed')
server.close()
})
await request
} catch (error) {
console.error(error)
}
```
### `Dispatcher.dispatch(options, handler)`
This is the low level API which all the preceding APIs are implemented on top of.
This API is expected to evolve through semver-major versions and is less stable than the preceding higher level APIs.
It is primarily intended for library developers who implement higher level APIs on top of this.
Arguments:
* **options** `DispatchOptions`
* **handler** `DispatchHandler`
Returns: `Boolean` - `false` if dispatcher is busy and further dispatch calls won't make any progress until the `'drain'` event has been emitted.
#### Parameter: `DispatchOptions`
* **origin** `string | URL`
* **path** `string`
* **method** `string`
* **reset** `boolean` (optional) - Default: `false` - If `false`, the request will attempt to create a long-living connection by sending the `connection: keep-alive` header,otherwise will attempt to close it immediately after response by sending `connection: close` within the request and closing the socket afterwards.
* **body** `string | Buffer | Uint8Array | stream.Readable | Iterable | AsyncIterable | null` (optional) - Default: `null`
* **headers** `UndiciHeaders` (optional) - Default: `null`.
* **query** `Record | null` (optional) - Default: `null` - Query string params to be embedded in the request URL. Note that both keys and values of query are encoded using `encodeURIComponent`. If for some reason you need to send them unencoded, embed query params into path directly instead.
* **idempotent** `boolean` (optional) - Default: `true` if `method` is `'HEAD'` or `'GET'` - Whether the requests can be safely retried or not. If `false` the request won't be sent until all preceding requests in the pipeline has completed.
* **blocking** `boolean` (optional) - Default: `method !== 'HEAD'` - Whether the response is expected to take a long time and would end up blocking the pipeline. When this is set to `true` further pipelining will be avoided on the same connection until headers have been received.
* **upgrade** `string | null` (optional) - Default: `null` - Upgrade the request. Should be used to specify the kind of upgrade i.e. `'Websocket'`.
* **bodyTimeout** `number | null` (optional) - The timeout after which a request will time out, in milliseconds. Monitors time between receiving body data. Use `0` to disable it entirely. Defaults to 300 seconds.
* **headersTimeout** `number | null` (optional) - The amount of time, in milliseconds, the parser will wait to receive the complete HTTP headers while not sending the request. Defaults to 300 seconds.
* **expectContinue** `boolean` (optional) - Default: `false` - For H2, it appends the expect: 100-continue header, and halts the request body until a 100-continue is received from the remote server
#### Parameter: `DispatchHandler`
* **onRequestStart** `(controller: DispatchController, context: object) => void` - Invoked before request is dispatched on socket. May be invoked multiple times when a request is retried when the request at the head of the pipeline fails.
* **onRequestUpgrade** `(controller: DispatchController, statusCode: number, headers: Record, socket: Duplex) => void` (optional) - Invoked when request is upgraded. Required if `DispatchOptions.upgrade` is defined or `DispatchOptions.method === 'CONNECT'`.
* **onResponseStart** `(controller: DispatchController, statusCode: number, headers: Record, statusMessage?: string) => void` - Invoked when statusCode and headers have been received. May be invoked multiple times due to 1xx informational headers. Not required for `upgrade` requests. Any return value is ignored.
* **onResponseData** `(controller: DispatchController, chunk: Buffer) => void` - Invoked when response payload data is received. Not required for `upgrade` requests.
* **onResponseEnd** `(controller: DispatchController, trailers: Record) => void` - Invoked when response payload and trailers have been received and the request has completed. Not required for `upgrade` requests.
* **onResponseError** `(controller: DispatchController, error: Error) => void` - Invoked when an error has occurred. May not throw.
#### Example 1 - Dispatch GET request
```js
import { createServer } from 'http'
import { Client } from 'undici'
import { once } from 'events'
const server = createServer((request, response) => {
response.end('Hello, World!')
}).listen()
await once(server, 'listening')
const client = new Client(`http://localhost:${server.address().port}`)
const data = []
client.dispatch({
path: '/',
method: 'GET',
headers: {
'x-foo': 'bar'
}
}, {
onConnect: () => {
console.log('Connected!')
},
onError: (error) => {
console.error(error)
},
onHeaders: (statusCode, headers) => {
console.log(`onHeaders | statusCode: ${statusCode} | headers: ${headers}`)
},
onData: (chunk) => {
console.log('onData: chunk received')
data.push(chunk)
},
onComplete: (trailers) => {
console.log(`onComplete | trailers: ${trailers}`)
const res = Buffer.concat(data).toString('utf8')
console.log(`Data: ${res}`)
client.close()
server.close()
}
})
```
#### Example 2 - Dispatch Upgrade Request
```js
import { createServer } from 'http'
import { Client } from 'undici'
import { once } from 'events'
const server = createServer((request, response) => {
response.end()
}).listen()
await once(server, 'listening')
server.on('upgrade', (request, socket, head) => {
console.log('Node.js Server - upgrade event')
socket.write('HTTP/1.1 101 Web Socket Protocol Handshake\r\n')
socket.write('Upgrade: WebSocket\r\n')
socket.write('Connection: Upgrade\r\n')
socket.write('\r\n')
socket.end()
})
const client = new Client(`http://localhost:${server.address().port}`)
client.dispatch({
path: '/',
method: 'GET',
upgrade: 'websocket'
}, {
onConnect: () => {
console.log('Undici Client - onConnect')
},
onError: (error) => {
console.log('onError') // shouldn't print
},
onUpgrade: (statusCode, headers, socket) => {
console.log('Undici Client - onUpgrade')
console.log(`onUpgrade Headers: ${headers}`)
socket.on('data', buffer => {
console.log(buffer.toString('utf8'))
})
socket.on('end', () => {
client.close()
server.close()
})
socket.end()
}
})
```
#### Example 3 - Dispatch POST request
```js
import { createServer } from 'http'
import { Client } from 'undici'
import { once } from 'events'
const server = createServer((request, response) => {
request.on('data', (data) => {
console.log(`Request Data: ${data.toString('utf8')}`)
const body = JSON.parse(data)
body.message = 'World'
response.end(JSON.stringify(body))
})
}).listen()
await once(server, 'listening')
const client = new Client(`http://localhost:${server.address().port}`)
const data = []
client.dispatch({
path: '/',
method: 'POST',
headers: {
'content-type': 'application/json'
},
body: JSON.stringify({ message: 'Hello' })
}, {
onConnect: () => {
console.log('Connected!')
},
onError: (error) => {
console.error(error)
},
onHeaders: (statusCode, headers) => {
console.log(`onHeaders | statusCode: ${statusCode} | headers: ${headers}`)
},
onData: (chunk) => {
console.log('onData: chunk received')
data.push(chunk)
},
onComplete: (trailers) => {
console.log(`onComplete | trailers: ${trailers}`)
const res = Buffer.concat(data).toString('utf8')
console.log(`Response Data: ${res}`)
client.close()
server.close()
}
})
```
### `Dispatcher.pipeline(options, handler)`
For easy use with [stream.pipeline](https://nodejs.org/api/stream.html#stream_stream_pipeline_source_transforms_destination_callback). The `handler` argument should return a `Readable` from which the result will be read. Usually it should just return the `body` argument unless some kind of transformation needs to be performed based on e.g. `headers` or `statusCode`. The `handler` should validate the response and save any required state. If there is an error, it should be thrown. The function returns a `Duplex` which writes to the request and reads from the response.
Arguments:
* **options** `PipelineOptions`
* **handler** `(data: PipelineHandlerData) => stream.Readable`
Returns: `stream.Duplex`
#### Parameter: PipelineOptions
Extends: [`RequestOptions`](/docs/docs/api/Dispatcher.md#parameter-requestoptions)
* **objectMode** `boolean` (optional) - Default: `false` - Set to `true` if the `handler` will return an object stream.
#### Parameter: PipelineHandlerData
* **statusCode** `number`
* **headers** `Record`
* **opaque** `unknown`
* **body** `stream.Readable`
* **context** `object`
* **onInfo** `({statusCode: number, headers: Record}) => void | null` (optional) - Default: `null` - Callback collecting all the info headers (HTTP 100-199) received.
#### Example 1 - Pipeline Echo
```js
import { Readable, Writable, PassThrough, pipeline } from 'stream'
import { createServer } from 'http'
import { Client } from 'undici'
import { once } from 'events'
const server = createServer((request, response) => {
request.pipe(response)
}).listen()
await once(server, 'listening')
const client = new Client(`http://localhost:${server.address().port}`)
let res = ''
pipeline(
new Readable({
read () {
this.push(Buffer.from('undici'))
this.push(null)
}
}),
client.pipeline({
path: '/',
method: 'GET'
}, ({ statusCode, headers, body }) => {
console.log(`response received ${statusCode}`)
console.log('headers', headers)
return pipeline(body, new PassThrough(), () => {})
}),
new Writable({
write (chunk, _, callback) {
res += chunk.toString()
callback()
},
final (callback) {
console.log(`Response pipelined to writable: ${res}`)
callback()
}
}),
error => {
if (error) {
console.error(error)
}
client.close()
server.close()
}
)
```
### `Dispatcher.request(options[, callback])`
Performs a HTTP request.
Non-idempotent requests will not be pipelined in order
to avoid indirect failures.
Idempotent requests will be automatically retried if
they fail due to indirect failure from the request
at the head of the pipeline. This does not apply to
idempotent requests with a stream request body.
All response bodies must always be fully consumed or destroyed.
Arguments:
* **options** `RequestOptions`
* **callback** `(error: Error | null, data: ResponseData) => void` (optional)
Returns: `void | Promise` - Only returns a `Promise` if no `callback` argument was passed.
#### Parameter: `RequestOptions`
Extends: [`DispatchOptions`](/docs/docs/api/Dispatcher.md#parameter-dispatchoptions)
* **opaque** `unknown` (optional) - Default: `null` - Used for passing through context to `ResponseData`.
* **signal** `AbortSignal | events.EventEmitter | null` (optional) - Default: `null`.
* **onInfo** `({statusCode: number, headers: Record}) => void | null` (optional) - Default: `null` - Callback collecting all the info headers (HTTP 100-199) received.
The `RequestOptions.method` property should not be value `'CONNECT'`.
#### Parameter: `ResponseData`
* **statusCode** `number`
* **statusText** `string` - The status message from the response (e.g., "OK", "Not Found").
* **headers** `Record` - Note that all header keys are lower-cased, e.g. `content-type`.
* **body** `stream.Readable` which also implements [the body mixin from the Fetch Standard](https://fetch.spec.whatwg.org/#body-mixin).
* **trailers** `Record` - This object starts out
as empty and will be mutated to contain trailers after `body` has emitted `'end'`.
* **opaque** `unknown`
* **context** `object`
`body` contains the following additional [body mixin](https://fetch.spec.whatwg.org/#body-mixin) methods and properties:
* [`.arrayBuffer()`](https://fetch.spec.whatwg.org/#dom-body-arraybuffer)
* [`.blob()`](https://fetch.spec.whatwg.org/#dom-body-blob)
* [`.bytes()`](https://fetch.spec.whatwg.org/#dom-body-bytes)
* [`.json()`](https://fetch.spec.whatwg.org/#dom-body-json)
* [`.text()`](https://fetch.spec.whatwg.org/#dom-body-text)
* `body`
* `bodyUsed`
`body` can not be consumed twice. For example, calling `text()` after `json()` throws `TypeError`.
`body` contains the following additional extensions:
- `dump({ limit: Integer })`, dump the response by reading up to `limit` bytes without killing the socket (optional) - Default: 262144.
Note that body will still be a `Readable` even if it is empty, but attempting to deserialize it with `json()` will result in an exception. Recommended way to ensure there is a body to deserialize is to check if status code is not 204, and `content-type` header starts with `application/json`.
#### Example 1 - Basic GET Request
```js
import { createServer } from 'http'
import { Client } from 'undici'
import { once } from 'events'
const server = createServer((request, response) => {
response.end('Hello, World!')
}).listen()
await once(server, 'listening')
const client = new Client(`http://localhost:${server.address().port}`)
try {
const { body, headers, statusCode, statusText, trailers } = await client.request({
path: '/',
method: 'GET'
})
console.log(`response received ${statusCode}`)
console.log('headers', headers)
body.setEncoding('utf8')
body.on('data', console.log)
body.on('error', console.error)
body.on('end', () => {
console.log('trailers', trailers)
})
client.close()
server.close()
} catch (error) {
console.error(error)
}
```
#### Example 2 - Aborting a request
> Node.js v15+ is required to run this example
```js
import { createServer } from 'http'
import { Client } from 'undici'
import { once } from 'events'
const server = createServer((request, response) => {
response.end('Hello, World!')
}).listen()
await once(server, 'listening')
const client = new Client(`http://localhost:${server.address().port}`)
const abortController = new AbortController()
try {
client.request({
path: '/',
method: 'GET',
signal: abortController.signal
})
} catch (error) {
console.error(error) // should print an RequestAbortedError
client.close()
server.close()
}
abortController.abort()
```
Alternatively, any `EventEmitter` that emits an `'abort'` event may be used as an abort controller:
```js
import { createServer } from 'http'
import { Client } from 'undici'
import EventEmitter, { once } from 'events'
const server = createServer((request, response) => {
response.end('Hello, World!')
}).listen()
await once(server, 'listening')
const client = new Client(`http://localhost:${server.address().port}`)
const ee = new EventEmitter()
try {
client.request({
path: '/',
method: 'GET',
signal: ee
})
} catch (error) {
console.error(error) // should print an RequestAbortedError
client.close()
server.close()
}
ee.emit('abort')
```
Destroying the request or response body will have the same effect.
```js
import { createServer } from 'http'
import { Client } from 'undici'
import { once } from 'events'
const server = createServer((request, response) => {
response.end('Hello, World!')
}).listen()
await once(server, 'listening')
const client = new Client(`http://localhost:${server.address().port}`)
try {
const { body } = await client.request({
path: '/',
method: 'GET'
})
body.destroy()
} catch (error) {
console.error(error) // should print an RequestAbortedError
client.close()
server.close()
}
```
#### Example 3 - Conditionally reading the body
Remember to fully consume the body even in the case when it is not read.
```js
const { body, statusCode } = await client.request({
path: '/',
method: 'GET'
})
if (statusCode === 200) {
return await body.arrayBuffer()
}
await body.dump()
return null
```
### `Dispatcher.stream(options, factory[, callback])`
A faster version of `Dispatcher.request`. This method expects the second argument `factory` to return a [`stream.Writable`](https://nodejs.org/api/stream.html#stream_class_stream_writable) stream which the response will be written to. This improves performance by avoiding creating an intermediate [`stream.Readable`](https://nodejs.org/api/stream.html#stream_readable_streams) stream when the user expects to directly pipe the response body to a [`stream.Writable`](https://nodejs.org/api/stream.html#stream_class_stream_writable) stream.
As demonstrated in [Example 1 - Basic GET stream request](/docs/docs/api/Dispatcher.md#example-1-basic-get-stream-request), it is recommended to use the `option.opaque` property to avoid creating a closure for the `factory` method. This pattern works well with Node.js Web Frameworks such as [Fastify](https://fastify.io). See [Example 2 - Stream to Fastify Response](/docs/docs/api/Dispatch.md#example-2-stream-to-fastify-response) for more details.
Arguments:
* **options** `RequestOptions`
* **factory** `(data: StreamFactoryData) => stream.Writable`
* **callback** `(error: Error | null, data: StreamData) => void` (optional)
Returns: `void | Promise` - Only returns a `Promise` if no `callback` argument was passed
#### Parameter: `StreamFactoryData`
* **statusCode** `number`
* **headers** `Record`
* **opaque** `unknown`
* **onInfo** `({statusCode: number, headers: Record}) => void | null` (optional) - Default: `null` - Callback collecting all the info headers (HTTP 100-199) received.
#### Parameter: `StreamData`
* **opaque** `unknown`
* **trailers** `Record`
* **context** `object`
#### Example 1 - Basic GET stream request
```js
import { createServer } from 'http'
import { Client } from 'undici'
import { once } from 'events'
import { Writable } from 'stream'
const server = createServer((request, response) => {
response.end('Hello, World!')
}).listen()
await once(server, 'listening')
const client = new Client(`http://localhost:${server.address().port}`)
const bufs = []
try {
await client.stream({
path: '/',
method: 'GET',
opaque: { bufs }
}, ({ statusCode, headers, opaque: { bufs } }) => {
console.log(`response received ${statusCode}`)
console.log('headers', headers)
return new Writable({
write (chunk, encoding, callback) {
bufs.push(chunk)
callback()
}
})
})
console.log(Buffer.concat(bufs).toString('utf-8'))
client.close()
server.close()
} catch (error) {
console.error(error)
}
```
#### Example 2 - Stream to Fastify Response
In this example, a (fake) request is made to the fastify server using `fastify.inject()`. This request then executes the fastify route handler which makes a subsequent request to the raw Node.js http server using `undici.dispatcher.stream()`. The fastify response is passed to the `opaque` option so that undici can tap into the underlying writable stream using `response.raw`. This methodology demonstrates how one could use undici and fastify together to create fast-as-possible requests from one backend server to another.
```js
import { createServer } from 'http'
import { Client } from 'undici'
import { once } from 'events'
import fastify from 'fastify'
const nodeServer = createServer((request, response) => {
response.end('Hello, World! From Node.js HTTP Server')
}).listen()
await once(nodeServer, 'listening')
console.log('Node Server listening')
const nodeServerUndiciClient = new Client(`http://localhost:${nodeServer.address().port}`)
const fastifyServer = fastify()
fastifyServer.route({
url: '/',
method: 'GET',
handler: (request, response) => {
nodeServerUndiciClient.stream({
path: '/',
method: 'GET',
opaque: response
}, ({ opaque }) => opaque.raw)
}
})
await fastifyServer.listen()
console.log('Fastify Server listening')
const fastifyServerUndiciClient = new Client(`http://localhost:${fastifyServer.server.address().port}`)
try {
const { statusCode, body } = await fastifyServerUndiciClient.request({
path: '/',
method: 'GET'
})
console.log(`response received ${statusCode}`)
body.setEncoding('utf8')
body.on('data', console.log)
nodeServerUndiciClient.close()
fastifyServerUndiciClient.close()
fastifyServer.close()
nodeServer.close()
} catch (error) { }
```
### `Dispatcher.upgrade(options[, callback])`
Upgrade to a different protocol. Visit [MDN - HTTP - Protocol upgrade mechanism](https://developer.mozilla.org/en-US/docs/Web/HTTP/Protocol_upgrade_mechanism) for more details.
Arguments:
* **options** `UpgradeOptions`
* **callback** `(error: Error | null, data: UpgradeData) => void` (optional)
Returns: `void | Promise` - Only returns a `Promise` if no `callback` argument was passed
#### Parameter: `UpgradeOptions`
* **path** `string`
* **method** `string` (optional) - Default: `'GET'`
* **headers** `UndiciHeaders` (optional) - Default: `null`
* **protocol** `string` (optional) - Default: `'Websocket'` - A string of comma separated protocols, in descending preference order.
* **signal** `AbortSignal | EventEmitter | null` (optional) - Default: `null`
#### Parameter: `UpgradeData`
* **headers** `http.IncomingHeaders`
* **socket** `stream.Duplex`
* **opaque** `unknown`
#### Example 1 - Basic Upgrade Request
```js
import { createServer } from 'http'
import { Client } from 'undici'
import { once } from 'events'
const server = createServer((request, response) => {
response.statusCode = 101
response.setHeader('connection', 'upgrade')
response.setHeader('upgrade', request.headers.upgrade)
response.end()
}).listen()
await once(server, 'listening')
const client = new Client(`http://localhost:${server.address().port}`)
try {
const { headers, socket } = await client.upgrade({
path: '/',
})
socket.on('end', () => {
console.log(`upgrade: ${headers.upgrade}`) // upgrade: Websocket
client.close()
server.close()
})
socket.end()
} catch (error) {
console.error(error)
client.close()
server.close()
}
```
### `Dispatcher.compose(interceptors[, interceptor])`
Compose a new dispatcher from the current dispatcher and the given interceptors.
> _Notes_:
> - The order of the interceptors matters. The last interceptor will be the first to be called.
> - It is important to note that the `interceptor` function should return a function that follows the `Dispatcher.dispatch` signature.
> - Any fork of the chain of `interceptors` can lead to unexpected results.
>
> **Interceptor Stack Visualization:**
> ```
> compose([interceptor1, interceptor2, interceptor3])
>
> Request Flow:
> ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐
> │ Request │───▶│interceptor3 │───▶│interceptor2 │───▶│interceptor1 │───▶│ dispatcher │
> └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ │ .dispatch │
> ▲ ▲ ▲ └─────────────┘
> │ │ │ ▲
> (called first) (called second) (called last) │
> │
> ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
> │ Response │◀───│interceptor3 │◀───│interceptor2 │◀───│interceptor1 │◀─────────┘
> └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘
>
> The interceptors are composed in reverse order due to function composition.
> ```
Arguments:
* **interceptors** `Interceptor[interceptor[]]`: It is an array of `Interceptor` functions passed as only argument, or several interceptors passed as separate arguments.
Returns: `Dispatcher`.
#### Parameter: `Interceptor`
A function that takes a `dispatch` method and returns a `dispatch`-like function.
#### Example 1 - Basic Compose
```js
const { Client, RedirectHandler } = require('undici')
const redirectInterceptor = dispatch => {
return (opts, handler) => {
const { maxRedirections } = opts
if (!maxRedirections) {
return dispatch(opts, handler)
}
const redirectHandler = new RedirectHandler(
dispatch,
maxRedirections,
opts,
handler
)
opts = { ...opts, maxRedirections: 0 } // Stop sub dispatcher from also redirecting.
return dispatch(opts, redirectHandler)
}
}
const client = new Client('http://localhost:3000')
.compose(redirectInterceptor)
await client.request({ path: '/', method: 'GET' })
```
#### Example 2 - Chained Compose
```js
const { Client, RedirectHandler, RetryHandler } = require('undici')
const redirectInterceptor = dispatch => {
return (opts, handler) => {
const { maxRedirections } = opts
if (!maxRedirections) {
return dispatch(opts, handler)
}
const redirectHandler = new RedirectHandler(
dispatch,
maxRedirections,
opts,
handler
)
opts = { ...opts, maxRedirections: 0 }
return dispatch(opts, redirectHandler)
}
}
const retryInterceptor = dispatch => {
return function retryInterceptor (opts, handler) {
return dispatch(
opts,
new RetryHandler(opts, {
handler,
dispatch
})
)
}
}
const client = new Client('http://localhost:3000')
.compose(redirectInterceptor)
.compose(retryInterceptor)
await client.request({ path: '/', method: 'GET' })
```
#### Pre-built interceptors
##### `redirect`
The `redirect` interceptor allows you to customize the way your dispatcher handles redirects.
It accepts the same arguments as the [`RedirectHandler` constructor](/docs/docs/api/RedirectHandler.md).
**Example - Basic Redirect Interceptor**
```js
const { Client, interceptors } = require("undici");
const { redirect } = interceptors;
const client = new Client("http://service.example").compose(
redirect({ maxRedirections: 3, throwOnMaxRedirects: true })
);
client.request({ path: "/" })
```
##### `retry`
The `retry` interceptor allows you to customize the way your dispatcher handles retries.
It accepts the same arguments as the [`RetryHandler` constructor](/docs/docs/api/RetryHandler.md).
**Example - Basic Redirect Interceptor**
```js
const { Client, interceptors } = require("undici");
const { retry } = interceptors;
const client = new Client("http://service.example").compose(
retry({
maxRetries: 3,
minTimeout: 1000,
maxTimeout: 10000,
timeoutFactor: 2,
retryAfter: true,
})
);
```
##### `dump`
The `dump` interceptor enables you to dump the response body from a request upon a given limit.
**Options**
- `maxSize` - The maximum size (in bytes) of the response body to dump. If the size of the request's body exceeds this value then the connection will be closed. Default: `1048576`.
> The `Dispatcher#options` also gets extended with the options `dumpMaxSize`, `abortOnDumped`, and `waitForTrailers` which can be used to configure the interceptor at a request-per-request basis.
**Example - Basic Dump Interceptor**
```js
const { Client, interceptors } = require("undici");
const { dump } = interceptors;
const client = new Client("http://service.example").compose(
dump({
maxSize: 1024,
})
);
// or
client.dispatch(
{
path: "/",
method: "GET",
dumpMaxSize: 1024,
},
handler
);
```
##### `dns`
The `dns` interceptor enables you to cache DNS lookups for a given duration, per origin.
>It is well suited for scenarios where you want to cache DNS lookups to avoid the overhead of resolving the same domain multiple times
**Options**
- `maxTTL` - The maximum time-to-live (in milliseconds) of the DNS cache. It should be a positive integer. Default: `10000`.
- Set `0` to disable TTL.
- `maxItems` - The maximum number of items to cache. It should be a positive integer. Default: `Infinity`.
- `dualStack` - Whether to resolve both IPv4 and IPv6 addresses. Default: `true`.
- It will also attempt a happy-eyeballs-like approach to connect to the available addresses in case of a connection failure.
- `affinity` - Whether to use IPv4 or IPv6 addresses. Default: `4`.
- It can be either `'4` or `6`.
- It will only take effect if `dualStack` is `false`.
- `lookup: (hostname: string, options: LookupOptions, callback: (err: NodeJS.ErrnoException | null, addresses: DNSInterceptorRecord[]) => void) => void` - Custom lookup function. Default: `dns.lookup`.
- For more info see [dns.lookup](https://nodejs.org/api/dns.html#dns_dns_lookup_hostname_options_callback).
- `pick: (origin: URL, records: DNSInterceptorRecords, affinity: 4 | 6) => DNSInterceptorRecord` - Custom pick function. Default: `RoundRobin`.
- The function should return a single record from the records array.
- By default a simplified version of Round Robin is used.
- The `records` property can be mutated to store the state of the balancing algorithm.
- `storage: DNSStorage` - Custom storage for resolved DNS records
> The `Dispatcher#options` also gets extended with the options `dns.affinity`, `dns.dualStack`, `dns.lookup` and `dns.pick` which can be used to configure the interceptor at a request-per-request basis.
**DNSInterceptorRecord**
It represents a DNS record.
- `family` - (`number`) The IP family of the address. It can be either `4` or `6`.
- `address` - (`string`) The IP address.
**DNSInterceptorOriginRecords**
It represents a map of DNS IP addresses records for a single origin.
- `4.ips` - (`DNSInterceptorRecord[] | null`) The IPv4 addresses.
- `6.ips` - (`DNSInterceptorRecord[] | null`) The IPv6 addresses.
**DNSStorage**
It represents a storage object for resolved DNS records.
- `size` - (`number`) current size of the storage.
- `get` - (`(origin: string) => DNSInterceptorOriginRecords | null`) method to get the records for a given origin.
- `set` - (`(origin: string, records: DNSInterceptorOriginRecords | null, options: { ttl: number }) => void`) method to set the records for a given origin.
- `delete` - (`(origin: string) => void`) method to delete records for a given origin.
- `full` - (`() => boolean`) method to check if the storage is full, if returns `true`, DNS lookup will be skipped in this interceptor and new records will not be stored.
**Example - Basic DNS Interceptor**
```js
const { Client, interceptors } = require("undici");
const { dns } = interceptors;
const client = new Agent().compose([
dns({ ...opts })
])
const response = await client.request({
origin: `http://localhost:3030`,
...requestOpts
})
```
**Example - DNS Interceptor and LRU cache as a storage**
```js
const { Client, interceptors } = require("undici");
const QuickLRU = require("quick-lru");
const { dns } = interceptors;
const lru = new QuickLRU({ maxSize: 100 });
const lruAdapter = {
get size() {
return lru.size;
},
get(origin) {
return lru.get(origin);
},
set(origin, records, { ttl }) {
lru.set(origin, records, { maxAge: ttl });
},
delete(origin) {
lru.delete(origin);
},
full() {
// For LRU cache, we can always store new records,
// old records will be evicted automatically
return false;
}
}
const client = new Agent().compose([
dns({ storage: lruAdapter })
])
const response = await client.request({
origin: `http://localhost:3030`,
...requestOpts
})
```
##### `responseError`
The `responseError` interceptor throws an error for responses with status code errors (>= 400).
**Example**
```js
const { Client, interceptors } = require("undici");
const { responseError } = interceptors;
const client = new Client("http://service.example").compose(
responseError()
);
// Will throw a ResponseError for status codes >= 400
await client.request({
method: "GET",
path: "/"
});
```
##### `decompress`
⚠️ The decompress interceptor is experimental and subject to change.
The `decompress` interceptor automatically decompresses response bodies that are compressed with gzip, deflate, brotli, or zstd compression. It removes the `content-encoding` and `content-length` headers from decompressed responses and supports RFC-9110 compliant multiple encodings.
**Options**
- `skipErrorResponses` - Whether to skip decompression for error responses (status codes >= 400). Default: `true`.
- `skipStatusCodes` - Array of status codes to skip decompression for. Default: `[204, 304]`.
**Example - Basic Decompress Interceptor**
```js
const { Client, interceptors } = require("undici");
const { decompress } = interceptors;
const client = new Client("http://service.example").compose(
decompress()
);
// Automatically decompresses gzip/deflate/brotli/zstd responses
const response = await client.request({
method: "GET",
path: "/"
});
```
**Example - Custom Options**
```js
const { Client, interceptors } = require("undici");
const { decompress } = interceptors;
const client = new Client("http://service.example").compose(
decompress({
skipErrorResponses: false, // Decompress 5xx responses
skipStatusCodes: [204, 304, 201] // Skip these status codes
})
);
```
**Supported Encodings**
- `gzip` / `x-gzip` - GZIP compression
- `deflate` / `x-compress` - DEFLATE compression
- `br` - Brotli compression
- `zstd` - Zstandard compression
- Multiple encodings (e.g., `gzip, deflate`) are supported per RFC-9110
**Behavior**
- Skips decompression for status codes < 200 or >= 400 (configurable)
- Skips decompression for 204 No Content and 304 Not Modified by default
- Removes `content-encoding` and `content-length` headers when decompressing
- Passes through unsupported encodings unchanged
- Handles case-insensitive encoding names
- Supports streaming decompression without buffering
##### `Cache Interceptor`
The `cache` interceptor implements client-side response caching as described in
[RFC9111](https://www.rfc-editor.org/rfc/rfc9111.html).
**Options**
- `store` - The [`CacheStore`](/docs/docs/api/CacheStore.md) to store and retrieve responses from. Default is [`MemoryCacheStore`](/docs/docs/api/CacheStore.md#memorycachestore).
- `methods` - The [**safe** HTTP methods](https://www.rfc-editor.org/rfc/rfc9110#section-9.2.1) to cache the response of.
- `cacheByDefault` - The default expiration time to cache responses by if they don't have an explicit expiration and cannot have an heuristic expiry computed. If this isn't present, responses neither with an explicit expiration nor heuristically cacheable will not be cached. Default `undefined`.
- `type` - The [type of cache](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Caching#types_of_caches) for Undici to act as. Can be `shared` or `private`. Default `shared`. `private` implies privately cacheable responses will be cached and potentially shared with other users of your application.
**Usage with `fetch`**
```js
const { Agent, cacheStores, interceptors, setGlobalDispatcher } = require('undici')
const client = new Agent().compose(interceptors.cache({
store: new cacheStores.MemoryCacheStore({
maxSize: 100 * 1024 * 1024, // 100MB
maxCount: 1000,
maxEntrySize: 5 * 1024 * 1024 // 5MB
})
}))
setGlobalDispatcher(client)
// First request goes to the network and is cached when cache headers allow it.
const first = await fetch('https://example.com/data')
// Second request can be served from cache according to RFC9111 rules.
const second = await fetch('https://example.com/data')
```
##### `Deduplicate Interceptor`
The `deduplicate` interceptor deduplicates concurrent identical requests. When multiple identical requests are made while one is already in-flight, only one request is sent to the origin server, and all waiting handlers receive the same response. This reduces server load and improves performance.
**Options**
- `methods` - The [**safe** HTTP methods](https://www.rfc-editor.org/rfc/rfc9110#section-9.2.1) to deduplicate. Default `['GET']`.
- `skipHeaderNames` - Header names that, if present in a request, will cause the request to skip deduplication entirely. Useful for headers like `idempotency-key` where presence indicates unique processing. Header name matching is case-insensitive. Default `[]`.
- `excludeHeaderNames` - Header names to exclude from the deduplication key. Requests with different values for these headers will still be deduplicated together. Useful for headers like `x-request-id` that vary per request but shouldn't affect deduplication. Header name matching is case-insensitive. Default `[]`.
- `maxBufferSize` - Maximum bytes buffered per paused waiting deduplicated handler. If a waiting handler remains paused and exceeds this threshold, it is failed with an abort error to prevent unbounded memory growth. Default `5 * 1024 * 1024`.
**Usage**
```js
const { Client, interceptors } = require("undici");
const { deduplicate, cache } = interceptors;
// Deduplicate only
const client = new Client("http://service.example").compose(
deduplicate()
);
// Deduplicate with caching
const clientWithCache = new Client("http://service.example").compose(
deduplicate(),
cache()
);
```
Requests are considered identical if they have the same:
- Origin
- HTTP method
- Path
- Request headers (excluding any headers specified in `excludeHeaderNames`)
All deduplicated requests receive the complete response including status code, headers, and body.
For observability, request deduplication events are published to the `undici:request:pending-requests` [diagnostic channel](/docs/docs/api/DiagnosticsChannel.md#undicirequestpending-requests).
## Instance Events
### Event: `'connect'`
Parameters:
* **origin** `URL`
* **targets** `Array`
### Event: `'disconnect'`
Parameters:
* **origin** `URL`
* **targets** `Array`
* **error** `Error`
Emitted when the dispatcher has been disconnected from the origin.
> **Note**: For HTTP/2, this event is also emitted when the dispatcher has received the [GOAWAY Frame](https://webconcepts.info/concepts/http2-frame-type/0x7) with an Error with the message `HTTP/2: "GOAWAY" frame received` and the code `UND_ERR_INFO`.
> Due to nature of the protocol of using binary frames, it is possible that requests gets hanging as a frame can be received between the `HEADER` and `DATA` frames.
> It is recommended to handle this event and close the dispatcher to create a new HTTP/2 session.
### Event: `'connectionError'`
Parameters:
* **origin** `URL`
* **targets** `Array`
* **error** `Error`
Emitted when dispatcher fails to connect to
origin.
### Event: `'drain'`
Parameters:
* **origin** `URL`
Emitted when dispatcher is no longer busy.
## Parameter: `UndiciHeaders`
* `Record | string[] | Iterable<[string, string | string[] | undefined]> | null`
Header arguments such as `options.headers` in [`Client.dispatch`](/docs/docs/api/Client.md#clientdispatchoptions-handlers) can be specified in three forms:
* As an object specified by the `Record` (`IncomingHttpHeaders`) type.
* As an array of strings. An array representation of a header list must have an even length, or an `InvalidArgumentError` will be thrown.
* As an iterable that can encompass `Headers`, `Map`, or a custom iterator returning key-value pairs.
Keys are lowercase and values are not modified.
Undici validates header syntax at the protocol level (for example, invalid header names and invalid control characters in string values), but it does not sanitize untrusted application input. Validate and sanitize any user-provided header names and values before passing them to Undici to prevent header/body injection vulnerabilities.
When using the array header format (`string[]`), Undici processes only indexed elements. Additional properties assigned to the array object are ignored.
Response headers will derive a `host` from the `url` of the [Client](/docs/docs/api/Client.md#class-client) instance if no `host` header was previously specified.
### Example 1 - Object
```js
{
'content-length': '123',
'content-type': 'text/plain',
connection: 'keep-alive',
host: 'mysite.com',
accept: '*/*'
}
```
### Example 2 - Array
```js
[
'content-length', '123',
'content-type', 'text/plain',
'connection', 'keep-alive',
'host', 'mysite.com',
'accept', '*/*'
]
```
### Example 3 - Iterable
```js
new Headers({
'content-length': '123',
'content-type': 'text/plain',
connection: 'keep-alive',
host: 'mysite.com',
accept: '*/*'
})
```
or
```js
new Map([
['content-length', '123'],
['content-type', 'text/plain'],
['connection', 'keep-alive'],
['host', 'mysite.com'],
['accept', '*/*']
])
```
or
```js
{
*[Symbol.iterator] () {
yield ['content-length', '123']
yield ['content-type', 'text/plain']
yield ['connection', 'keep-alive']
yield ['host', 'mysite.com']
yield ['accept', '*/*']
}
}
```
================================================
FILE: docs/docs/api/EnvHttpProxyAgent.md
================================================
# Class: EnvHttpProxyAgent
Extends: `undici.Dispatcher`
EnvHttpProxyAgent automatically reads the proxy configuration from the environment variables `http_proxy`, `https_proxy`, and `no_proxy` and sets up the proxy agents accordingly. When `http_proxy` and `https_proxy` are set, `http_proxy` is used for HTTP requests and `https_proxy` is used for HTTPS requests. If only `http_proxy` is set, `http_proxy` is used for both HTTP and HTTPS requests. If only `https_proxy` is set, it is only used for HTTPS requests.
`no_proxy` is a comma or space-separated list of hostnames that should not be proxied. The list may contain leading wildcard characters (`*`). If `no_proxy` is set, the EnvHttpProxyAgent will bypass the proxy for requests to hosts that match the list. If `no_proxy` is set to `"*"`, the EnvHttpProxyAgent will bypass the proxy for all requests.
Uppercase environment variables are also supported: `HTTP_PROXY`, `HTTPS_PROXY`, and `NO_PROXY`. However, if both the lowercase and uppercase environment variables are set, the uppercase environment variables will be ignored.
## `new EnvHttpProxyAgent([options])`
Arguments:
* **options** `EnvHttpProxyAgentOptions` (optional) - extends the `Agent` options.
Returns: `EnvHttpProxyAgent`
### Parameter: `EnvHttpProxyAgentOptions`
Extends: [`AgentOptions`](/docs/docs/api/Agent.md#parameter-agentoptions)
* **httpProxy** `string` (optional) - When set, it will override the `HTTP_PROXY` environment variable.
* **httpsProxy** `string` (optional) - When set, it will override the `HTTPS_PROXY` environment variable.
* **noProxy** `string` (optional) - When set, it will override the `NO_PROXY` environment variable.
Examples:
```js
import { EnvHttpProxyAgent } from 'undici'
const envHttpProxyAgent = new EnvHttpProxyAgent()
// or
const envHttpProxyAgent = new EnvHttpProxyAgent({ httpProxy: 'my.proxy.server:8080', httpsProxy: 'my.proxy.server:8443', noProxy: 'localhost' })
```
#### Example - EnvHttpProxyAgent instantiation
This will instantiate the EnvHttpProxyAgent. It will not do anything until registered as the agent to use with requests.
```js
import { EnvHttpProxyAgent } from 'undici'
const envHttpProxyAgent = new EnvHttpProxyAgent()
```
#### Example - Basic Proxy Fetch with global agent dispatcher
```js
import { setGlobalDispatcher, fetch, EnvHttpProxyAgent } from 'undici'
const envHttpProxyAgent = new EnvHttpProxyAgent()
setGlobalDispatcher(envHttpProxyAgent)
const { status, json } = await fetch('http://localhost:3000/foo')
console.log('response received', status) // response received 200
const data = await json() // data { foo: "bar" }
```
#### Example - Basic Proxy Request with global agent dispatcher
```js
import { setGlobalDispatcher, request, EnvHttpProxyAgent } from 'undici'
const envHttpProxyAgent = new EnvHttpProxyAgent()
setGlobalDispatcher(envHttpProxyAgent)
const { statusCode, body } = await request('http://localhost:3000/foo')
console.log('response received', statusCode) // response received 200
for await (const data of body) {
console.log('data', data.toString('utf8')) // data foo
}
```
#### Example - Basic Proxy Request with local agent dispatcher
```js
import { EnvHttpProxyAgent, request } from 'undici'
const envHttpProxyAgent = new EnvHttpProxyAgent()
const {
statusCode,
body
} = await request('http://localhost:3000/foo', { dispatcher: envHttpProxyAgent })
console.log('response received', statusCode) // response received 200
for await (const data of body) {
console.log('data', data.toString('utf8')) // data foo
}
```
#### Example - Basic Proxy Fetch with local agent dispatcher
```js
import { EnvHttpProxyAgent, fetch } from 'undici'
const envHttpProxyAgent = new EnvHttpProxyAgent()
const {
status,
json
} = await fetch('http://localhost:3000/foo', { dispatcher: envHttpProxyAgent })
console.log('response received', status) // response received 200
const data = await json() // data { foo: "bar" }
```
## Instance Methods
### `EnvHttpProxyAgent.close([callback])`
Implements [`Dispatcher.close([callback])`](/docs/docs/api/Dispatcher.md#dispatcherclosecallback-promise).
### `EnvHttpProxyAgent.destroy([error, callback])`
Implements [`Dispatcher.destroy([error, callback])`](/docs/docs/api/Dispatcher.md#dispatcherdestroyerror-callback-promise).
### `EnvHttpProxyAgent.dispatch(options, handler: AgentDispatchOptions)`
Implements [`Dispatcher.dispatch(options, handler)`](/docs/docs/api/Dispatcher.md#dispatcherdispatchoptions-handler).
#### Parameter: `AgentDispatchOptions`
Extends: [`DispatchOptions`](/docs/docs/api/Dispatcher.md#parameter-dispatchoptions)
* **origin** `string | URL`
Implements [`Dispatcher.destroy([error, callback])`](/docs/docs/api/Dispatcher.md#dispatcherdestroyerror-callback-promise).
### `EnvHttpProxyAgent.connect(options[, callback])`
See [`Dispatcher.connect(options[, callback])`](/docs/docs/api/Dispatcher.md#dispatcherconnectoptions-callback).
### `EnvHttpProxyAgent.dispatch(options, handler)`
Implements [`Dispatcher.dispatch(options, handler)`](/docs/docs/api/Dispatcher.md#dispatcherdispatchoptions-handler).
### `EnvHttpProxyAgent.pipeline(options, handler)`
See [`Dispatcher.pipeline(options, handler)`](/docs/docs/api/Dispatcher.md#dispatcherpipelineoptions-handler).
### `EnvHttpProxyAgent.request(options[, callback])`
See [`Dispatcher.request(options [, callback])`](/docs/docs/api/Dispatcher.md#dispatcherrequestoptions-callback).
### `EnvHttpProxyAgent.stream(options, factory[, callback])`
See [`Dispatcher.stream(options, factory[, callback])`](/docs/docs/api/Dispatcher.md#dispatcherstreamoptions-factory-callback).
### `EnvHttpProxyAgent.upgrade(options[, callback])`
See [`Dispatcher.upgrade(options[, callback])`](/docs/docs/api/Dispatcher.md#dispatcherupgradeoptions-callback).
================================================
FILE: docs/docs/api/Errors.md
================================================
# Errors
Undici exposes a variety of error objects that you can use to enhance your error handling.
You can find all the error objects inside the `errors` key.
```js
import { errors } from 'undici'
```
| Error | Error Codes | Description |
| ------------------------------------ | ------------------------------------- | ------------------------------------------------------------------------- |
| `UndiciError` | `UND_ERR` | all errors below are extended from `UndiciError`. |
| `ConnectTimeoutError` | `UND_ERR_CONNECT_TIMEOUT` | socket is destroyed due to connect timeout. |
| `HeadersTimeoutError` | `UND_ERR_HEADERS_TIMEOUT` | socket is destroyed due to headers timeout. |
| `HeadersOverflowError` | `UND_ERR_HEADERS_OVERFLOW` | socket is destroyed due to headers' max size being exceeded. |
| `BodyTimeoutError` | `UND_ERR_BODY_TIMEOUT` | socket is destroyed due to body timeout. |
| `InvalidArgumentError` | `UND_ERR_INVALID_ARG` | passed an invalid argument. |
| `InvalidReturnValueError` | `UND_ERR_INVALID_RETURN_VALUE` | returned an invalid value. |
| `RequestAbortedError` | `UND_ERR_ABORTED` | the request has been aborted by the user |
| `ClientDestroyedError` | `UND_ERR_DESTROYED` | trying to use a destroyed client. |
| `ClientClosedError` | `UND_ERR_CLOSED` | trying to use a closed client. |
| `SocketError` | `UND_ERR_SOCKET` | there is an error with the socket. |
| `NotSupportedError` | `UND_ERR_NOT_SUPPORTED` | encountered unsupported functionality. |
| `RequestContentLengthMismatchError` | `UND_ERR_REQ_CONTENT_LENGTH_MISMATCH` | request body does not match content-length header |
| `ResponseContentLengthMismatchError` | `UND_ERR_RES_CONTENT_LENGTH_MISMATCH` | response body does not match content-length header |
| `InformationalError` | `UND_ERR_INFO` | expected error with reason |
| `ResponseExceededMaxSizeError` | `UND_ERR_RES_EXCEEDED_MAX_SIZE` | response body exceed the max size allowed |
| `SecureProxyConnectionError` | `UND_ERR_PRX_TLS` | tls connection to a proxy failed |
| `MessageSizeExceededError` | `UND_ERR_WS_MESSAGE_SIZE_EXCEEDED` | WebSocket decompressed message exceeded the maximum allowed size |
Be aware of the possible difference between the global dispatcher version and the actual undici version you might be using. We recommend to avoid the check `instanceof errors.UndiciError` and seek for the `error.code === ''` instead to avoid inconsistencies.
### `SocketError`
The `SocketError` has a `.socket` property which holds socket metadata:
```ts
interface SocketInfo {
localAddress?: string
localPort?: number
remoteAddress?: string
remotePort?: number
remoteFamily?: string
timeout?: number
bytesWritten?: number
bytesRead?: number
}
```
Be aware that in some cases the `.socket` property can be `null`.
================================================
FILE: docs/docs/api/EventSource.md
================================================
# EventSource
> ⚠️ Warning: the EventSource API is experimental.
Undici exposes a WHATWG spec-compliant implementation of [EventSource](https://developer.mozilla.org/en-US/docs/Web/API/EventSource)
for [Server-Sent Events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events).
## Instantiating EventSource
Undici exports a EventSource class. You can instantiate the EventSource as
follows:
```mjs
import { EventSource } from 'undici'
const eventSource = new EventSource('http://localhost:3000')
eventSource.onmessage = (event) => {
console.log(event.data)
}
```
## Using a custom Dispatcher
undici allows you to set your own Dispatcher in the EventSource constructor.
An example which allows you to modify the request headers is:
```mjs
import { EventSource, Agent } from 'undici'
class CustomHeaderAgent extends Agent {
dispatch (opts) {
opts.headers['x-custom-header'] = 'hello world'
return super.dispatch(...arguments)
}
}
const eventSource = new EventSource('http://localhost:3000', {
dispatcher: new CustomHeaderAgent()
})
```
More information about the EventSource API can be found on
[MDN](https://developer.mozilla.org/en-US/docs/Web/API/EventSource).
================================================
FILE: docs/docs/api/Fetch.md
================================================
# Fetch
Undici exposes a fetch() method starts the process of fetching a resource from the network.
Documentation and examples can be found on [MDN](https://developer.mozilla.org/en-US/docs/Web/API/fetch).
## FormData
This API is implemented as per the standard, you can find documentation on [MDN](https://developer.mozilla.org/en-US/docs/Web/API/FormData).
If any parameters are passed to the FormData constructor other than `undefined`, an error will be thrown. Other parameters are ignored.
## Response
This API is implemented as per the standard, you can find documentation on [MDN](https://developer.mozilla.org/en-US/docs/Web/API/Response)
## Request
This API is implemented as per the standard, you can find documentation on [MDN](https://developer.mozilla.org/en-US/docs/Web/API/Request)
## Header
This API is implemented as per the standard, you can find documentation on [MDN](https://developer.mozilla.org/en-US/docs/Web/API/Headers)
# Body Mixins
`Response` and `Request` body inherit body mixin methods. These methods include:
- [`.arrayBuffer()`](https://fetch.spec.whatwg.org/#dom-body-arraybuffer)
- [`.blob()`](https://fetch.spec.whatwg.org/#dom-body-blob)
- [`.bytes()`](https://fetch.spec.whatwg.org/#dom-body-bytes)
- [`.formData()`](https://fetch.spec.whatwg.org/#dom-body-formdata)
- [`.json()`](https://fetch.spec.whatwg.org/#dom-body-json)
- [`.text()`](https://fetch.spec.whatwg.org/#dom-body-text)
There is an ongoing discussion regarding `.formData()` and its usefulness and performance in server environments. It is recommended to use a dedicated library for parsing `multipart/form-data` bodies, such as [Busboy](https://www.npmjs.com/package/busboy) or [@fastify/busboy](https://www.npmjs.com/package/@fastify/busboy).
These libraries can be interfaced with fetch with the following example code:
```mjs
import { Busboy } from '@fastify/busboy'
import { Readable } from 'node:stream'
const response = await fetch('...')
const busboy = new Busboy({
headers: {
'content-type': response.headers.get('content-type')
}
})
Readable.fromWeb(response.body).pipe(busboy)
```
================================================
FILE: docs/docs/api/GlobalInstallation.md
================================================
# Global Installation
Undici provides an `install()` function to add all WHATWG fetch classes to `globalThis`, making them available globally without requiring imports.
## `install()`
Install all WHATWG fetch classes globally on `globalThis`.
**Example:**
```js
import { install } from 'undici'
// Install all WHATWG fetch classes globally
install()
// Now you can use fetch classes globally without importing
const response = await fetch('https://api.example.com/data')
const data = await response.json()
// All classes are available globally:
const headers = new Headers([['content-type', 'application/json']])
const request = new Request('https://example.com')
const formData = new FormData()
const ws = new WebSocket('wss://example.com')
const eventSource = new EventSource('https://example.com/events')
```
## Installed Classes
The `install()` function adds the following classes to `globalThis`:
| Class | Description |
|-------|-------------|
| `fetch` | The fetch function for making HTTP requests |
| `Headers` | HTTP headers management |
| `Response` | HTTP response representation |
| `Request` | HTTP request representation |
| `FormData` | Form data handling |
| `WebSocket` | WebSocket client |
| `CloseEvent` | WebSocket close event |
| `ErrorEvent` | WebSocket error event |
| `MessageEvent` | WebSocket message event |
| `EventSource` | Server-sent events client |
## Use Cases
Global installation is useful for:
- **Polyfilling environments** that don't have native fetch support
- **Ensuring consistent behavior** across different Node.js versions
- **Library compatibility** when third-party libraries expect global fetch
- **Migration scenarios** where you want to replace built-in implementations
- **Testing environments** where you need predictable fetch behavior
## Example: Polyfilling an Environment
```js
import { install } from 'undici'
// Check if fetch is available and install if needed
if (typeof globalThis.fetch === 'undefined') {
install()
console.log('Undici fetch installed globally')
}
// Now fetch is guaranteed to be available
const response = await fetch('https://api.example.com')
```
## Example: Testing Environment
```js
import { install } from 'undici'
// In test setup, ensure consistent fetch behavior
install()
// Now all tests use undici's implementations
test('fetch API test', async () => {
const response = await fetch('https://example.com')
expect(response).toBeInstanceOf(Response)
})
```
## Notes
- The `install()` function overwrites any existing global implementations
- Classes installed are undici's implementations, not Node.js built-ins
- This provides access to undici's latest features and performance improvements
- The global installation persists for the lifetime of the process
================================================
FILE: docs/docs/api/H2CClient.md
================================================
# Class: H2CClient
Extends: `undici.Dispatcher`
A basic H2C client.
**Example**
```js
const { createServer } = require('node:http2')
const { once } = require('node:events')
const { H2CClient } = require('undici')
const server = createServer((req, res) => {
res.writeHead(200)
res.end('Hello, world!')
})
server.listen()
once(server, 'listening').then(() => {
const client = new H2CClient(`http://localhost:${server.address().port}/`)
const response = await client.request({ path: '/', method: 'GET' })
console.log(response.statusCode) // 200
response.body.text.then((text) => {
console.log(text) // Hello, world!
})
})
```
## `new H2CClient(url[, options])`
Arguments:
- **url** `URL | string` - Should only include the **protocol, hostname, and port**. It only supports `http` protocol.
- **options** `H2CClientOptions` (optional)
Returns: `H2CClient`
### Parameter: `H2CClientOptions`
- **bodyTimeout** `number | null` (optional) - Default: `300e3` - The timeout after which a request will time out, in milliseconds. Monitors time between receiving body data. Use `0` to disable it entirely. Defaults to 300 seconds. Please note the `timeout` will be reset if you keep writing data to the socket everytime.
- **headersTimeout** `number | null` (optional) - Default: `300e3` - The amount of time, in milliseconds, the parser will wait to receive the complete HTTP headers while not sending the request. Defaults to 300 seconds.
- **keepAliveMaxTimeout** `number | null` (optional) - Default: `600e3` - The maximum allowed `keepAliveTimeout`, in milliseconds, when overridden by _keep-alive_ hints from the server. Defaults to 10 minutes.
- **keepAliveTimeout** `number | null` (optional) - Default: `4e3` - The timeout, in milliseconds, after which a socket without active requests will time out. Monitors time between activity on a connected socket. This value may be overridden by _keep-alive_ hints from the server. See [MDN: HTTP - Headers - Keep-Alive directives](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Keep-Alive#directives) for more details. Defaults to 4 seconds.
- **keepAliveTimeoutThreshold** `number | null` (optional) - Default: `2e3` - A number of milliseconds subtracted from server _keep-alive_ hints when overriding `keepAliveTimeout` to account for timing inaccuracies caused by e.g. transport latency. Defaults to 2 seconds.
- **maxHeaderSize** `number | null` (optional) - Default: `--max-http-header-size` or `16384` - The maximum length of request headers in bytes. Defaults to Node.js' --max-http-header-size or 16KiB.
- **maxResponseSize** `number | null` (optional) - Default: `-1` - The maximum length of response body in bytes. Set to `-1` to disable.
- **maxConcurrentStreams**: `number` - Default: `100`. Dictates the maximum number of concurrent streams for a single H2 session. It can be overridden by a SETTINGS remote frame.
- **pipelining** `number | null` (optional) - Default to `maxConcurrentStreams` - The amount of concurrent requests sent over a single HTTP/2 session in accordance with [RFC-7540](https://httpwg.org/specs/rfc7540.html#StreamsLayer) Stream specification. Streams can be closed up by remote server at any time.
- **pingInterval**: `number` - Default: `60e3`. The time interval in milliseconds between PING frames sent to the server. Set to `0` to disable PING frames. This is only applicable for HTTP/2 connections.
- **connect** `ConnectOptions | null` (optional) - Default: `null`.
- **strictContentLength** `Boolean` (optional) - Default: `true` - Whether to treat request content length mismatches as errors. If true, an error is thrown when the request content-length header doesn't match the length of the request body. **Security Warning:** Disabling this option can expose your application to HTTP Request Smuggling attacks, where mismatched content-length headers cause servers and proxies to interpret request boundaries differently. This can lead to cache poisoning, credential hijacking, and bypassing security controls. Only disable this in controlled environments where you fully trust the request source.
- **autoSelectFamily**: `boolean` (optional) - Default: depends on local Node version, on Node 18.13.0 and above is `false`. Enables a family autodetection algorithm that loosely implements section 5 of [RFC 8305](https://tools.ietf.org/html/rfc8305#section-5). See [here](https://nodejs.org/api/net.html#socketconnectoptions-connectlistener) for more details. This option is ignored if not supported by the current Node version.
- **autoSelectFamilyAttemptTimeout**: `number` - Default: depends on local Node version, on Node 18.13.0 and above is `250`. The amount of time in milliseconds to wait for a connection attempt to finish before trying the next address when using the `autoSelectFamily` option. See [here](https://nodejs.org/api/net.html#socketconnectoptions-connectlistener) for more details.
#### Parameter: `H2CConnectOptions`
- **socketPath** `string | null` (optional) - Default: `null` - An IPC endpoint, either Unix domain socket or Windows named pipe.
- **timeout** `number | null` (optional) - In milliseconds, Default `10e3`.
- **servername** `string | null` (optional)
- **keepAlive** `boolean | null` (optional) - Default: `true` - TCP keep-alive enabled
- **keepAliveInitialDelay** `number | null` (optional) - Default: `60000` - TCP keep-alive interval for the socket in milliseconds
### Example - Basic Client instantiation
This will instantiate the undici H2CClient, but it will not connect to the origin until something is queued. Consider using `client.connect` to prematurely connect to the origin, or just call `client.request`.
```js
"use strict";
import { H2CClient } from "undici";
const client = new H2CClient("http://localhost:3000");
```
## Instance Methods
### `H2CClient.close([callback])`
Implements [`Dispatcher.close([callback])`](/docs/docs/api/Dispatcher.md#dispatcherclosecallback-promise).
### `H2CClient.destroy([error, callback])`
Implements [`Dispatcher.destroy([error, callback])`](/docs/docs/api/Dispatcher.md#dispatcherdestroyerror-callback-promise).
Waits until socket is closed before invoking the callback (or returning a promise if no callback is provided).
### `H2CClient.connect(options[, callback])`
See [`Dispatcher.connect(options[, callback])`](/docs/docs/api/Dispatcher.md#dispatcherconnectoptions-callback).
### `H2CClient.dispatch(options, handlers)`
Implements [`Dispatcher.dispatch(options, handlers)`](/docs/docs/api/Dispatcher.md#dispatcherdispatchoptions-handler).
### `H2CClient.pipeline(options, handler)`
See [`Dispatcher.pipeline(options, handler)`](/docs/docs/api/Dispatcher.md#dispatcherpipelineoptions-handler).
### `H2CClient.request(options[, callback])`
See [`Dispatcher.request(options [, callback])`](/docs/docs/api/Dispatcher.md#dispatcherrequestoptions-callback).
### `H2CClient.stream(options, factory[, callback])`
See [`Dispatcher.stream(options, factory[, callback])`](/docs/docs/api/Dispatcher.md#dispatcherstreamoptions-factory-callback).
### `H2CClient.upgrade(options[, callback])`
See [`Dispatcher.upgrade(options[, callback])`](/docs/docs/api/Dispatcher.md#dispatcherupgradeoptions-callback).
## Instance Properties
### `H2CClient.closed`
- `boolean`
`true` after `H2CClient.close()` has been called.
### `H2CClient.destroyed`
- `boolean`
`true` after `client.destroyed()` has been called or `client.close()` has been called and the client shutdown has completed.
### `H2CClient.pipelining`
- `number`
Property to get and set the pipelining factor.
## Instance Events
### Event: `'connect'`
See [Dispatcher Event: `'connect'`](/docs/docs/api/Dispatcher.md#event-connect).
Parameters:
- **origin** `URL`
- **targets** `Array`
Emitted when a socket has been created and connected. The client will connect once `client.size > 0`.
#### Example - Client connect event
```js
import { createServer } from "node:http2";
import { H2CClient } from "undici";
import { once } from "events";
const server = createServer((request, response) => {
response.end("Hello, World!");
}).listen();
await once(server, "listening");
const client = new H2CClient(`http://localhost:${server.address().port}`);
client.on("connect", (origin) => {
console.log(`Connected to ${origin}`); // should print before the request body statement
});
try {
const { body } = await client.request({
path: "/",
method: "GET",
});
body.setEncoding("utf-8");
body.on("data", console.log);
client.close();
server.close();
} catch (error) {
console.error(error);
client.close();
server.close();
}
```
### Event: `'disconnect'`
See [Dispatcher Event: `'disconnect'`](/docs/docs/api/Dispatcher.md#event-disconnect).
Parameters:
- **origin** `URL`
- **targets** `Array`
- **error** `Error`
Emitted when socket has disconnected. The error argument of the event is the error which caused the socket to disconnect. The client will reconnect if or once `client.size > 0`.
#### Example - Client disconnect event
```js
import { createServer } from "node:http2";
import { H2CClient } from "undici";
import { once } from "events";
const server = createServer((request, response) => {
response.destroy();
}).listen();
await once(server, "listening");
const client = new H2CClient(`http://localhost:${server.address().port}`);
client.on("disconnect", (origin) => {
console.log(`Disconnected from ${origin}`);
});
try {
await client.request({
path: "/",
method: "GET",
});
} catch (error) {
console.error(error.message);
client.close();
server.close();
}
```
### Event: `'drain'`
Emitted when pipeline is no longer busy.
See [Dispatcher Event: `'drain'`](/docs/docs/api/Dispatcher.md#event-drain).
#### Example - Client drain event
```js
import { createServer } from "node:http2";
import { H2CClient } from "undici";
import { once } from "events";
const server = createServer((request, response) => {
response.end("Hello, World!");
}).listen();
await once(server, "listening");
const client = new H2CClient(`http://localhost:${server.address().port}`);
client.on("drain", () => {
console.log("drain event");
client.close();
server.close();
});
const requests = [
client.request({ path: "/", method: "GET" }),
client.request({ path: "/", method: "GET" }),
client.request({ path: "/", method: "GET" }),
];
await Promise.all(requests);
console.log("requests completed");
```
### Event: `'error'`
Invoked for users errors such as throwing in the `onError` handler.
================================================
FILE: docs/docs/api/MockAgent.md
================================================
# Class: MockAgent
Extends: `undici.Dispatcher`
A mocked Agent class that implements the Agent API. It allows one to intercept HTTP requests made through undici and return mocked responses instead.
## `new MockAgent([options])`
Arguments:
* **options** `MockAgentOptions` (optional) - It extends the `Agent` options.
Returns: `MockAgent`
### Parameter: `MockAgentOptions`
Extends: [`AgentOptions`](/docs/docs/api/Agent.md#parameter-agentoptions)
* **agent** `Agent` (optional) - Default: `new Agent([options])` - a custom agent encapsulated by the MockAgent.
* **ignoreTrailingSlash** `boolean` (optional) - Default: `false` - set the default value for `ignoreTrailingSlash` for interceptors.
* **acceptNonStandardSearchParameters** `boolean` (optional) - Default: `false` - set to `true` if the matcher should also accept non standard search parameters such as multi-value items specified with `[]` (e.g. `param[]=1¶m[]=2¶m[]=3`) and multi-value items which values are comma separated (e.g. `param=1,2,3`).
### Example - Basic MockAgent instantiation
This will instantiate the MockAgent. It will not do anything until registered as the agent to use with requests and mock interceptions are added.
```js
import { MockAgent } from 'undici'
const mockAgent = new MockAgent()
```
### Example - Basic MockAgent instantiation with custom agent
```js
import { Agent, MockAgent } from 'undici'
const agent = new Agent()
const mockAgent = new MockAgent({ agent })
```
## Instance Methods
### `MockAgent.get(origin)`
This method creates and retrieves MockPool or MockClient instances which can then be used to intercept HTTP requests. If the number of connections on the mock agent is set to 1, a MockClient instance is returned. Otherwise a MockPool instance is returned.
For subsequent `MockAgent.get` calls on the same origin, the same mock instance will be returned.
Arguments:
* **origin** `string | RegExp | (value) => boolean` - a matcher for the pool origin to be retrieved from the MockAgent.
| Matcher type | Condition to pass |
|:------------:| -------------------------- |
| `string` | Exact match against string |
| `RegExp` | Regex must pass |
| `Function` | Function must return true |
Returns: `MockClient | MockPool`.
| `MockAgentOptions` | Mock instance returned |
| -------------------- | ---------------------- |
| `connections === 1` | `MockClient` |
| `connections` > `1` | `MockPool` |
#### Example - Basic Mocked Request
```js
import { MockAgent, setGlobalDispatcher, request } from 'undici'
const mockAgent = new MockAgent()
setGlobalDispatcher(mockAgent)
const mockPool = mockAgent.get('http://localhost:3000')
mockPool.intercept({ path: '/foo' }).reply(200, 'foo')
const { statusCode, body } = await request('http://localhost:3000/foo')
console.log('response received', statusCode) // response received 200
for await (const data of body) {
console.log('data', data.toString('utf8')) // data foo
}
```
#### Example - Basic Mocked Request with local mock agent dispatcher
```js
import { MockAgent, request } from 'undici'
const mockAgent = new MockAgent()
const mockPool = mockAgent.get('http://localhost:3000')
mockPool.intercept({ path: '/foo' }).reply(200, 'foo')
const {
statusCode,
body
} = await request('http://localhost:3000/foo', { dispatcher: mockAgent })
console.log('response received', statusCode) // response received 200
for await (const data of body) {
console.log('data', data.toString('utf8')) // data foo
}
```
#### Example - Basic Mocked Request with local mock pool dispatcher
```js
import { MockAgent, request } from 'undici'
const mockAgent = new MockAgent()
const mockPool = mockAgent.get('http://localhost:3000')
mockPool.intercept({ path: '/foo' }).reply(200, 'foo')
const {
statusCode,
body
} = await request('http://localhost:3000/foo', { dispatcher: mockPool })
console.log('response received', statusCode) // response received 200
for await (const data of body) {
console.log('data', data.toString('utf8')) // data foo
}
```
#### Example - Basic Mocked Request with local mock client dispatcher
```js
import { MockAgent, request } from 'undici'
const mockAgent = new MockAgent({ connections: 1 })
const mockClient = mockAgent.get('http://localhost:3000')
mockClient.intercept({ path: '/foo' }).reply(200, 'foo')
const {
statusCode,
body
} = await request('http://localhost:3000/foo', { dispatcher: mockClient })
console.log('response received', statusCode) // response received 200
for await (const data of body) {
console.log('data', data.toString('utf8')) // data foo
}
```
#### Example - Basic Mocked requests with multiple intercepts
```js
import { MockAgent, setGlobalDispatcher, request } from 'undici'
const mockAgent = new MockAgent()
setGlobalDispatcher(mockAgent)
const mockPool = mockAgent.get('http://localhost:3000')
mockPool.intercept({ path: '/foo' }).reply(200, 'foo')
mockPool.intercept({ path: '/hello'}).reply(200, 'hello')
const result1 = await request('http://localhost:3000/foo')
console.log('response received', result1.statusCode) // response received 200
for await (const data of result1.body) {
console.log('data', data.toString('utf8')) // data foo
}
const result2 = await request('http://localhost:3000/hello')
console.log('response received', result2.statusCode) // response received 200
for await (const data of result2.body) {
console.log('data', data.toString('utf8')) // data hello
}
```
#### Example - Mock different requests within the same file
```js
const { MockAgent, setGlobalDispatcher } = require('undici');
const agent = new MockAgent();
agent.disableNetConnect();
setGlobalDispatcher(agent);
describe('Test', () => {
it('200', async () => {
const mockAgent = agent.get('http://test.com');
// your test
});
it('200', async () => {
const mockAgent = agent.get('http://testing.com');
// your test
});
});
```
#### Example - Mocked request with query body, headers and trailers
```js
import { MockAgent, setGlobalDispatcher, request } from 'undici'
const mockAgent = new MockAgent()
setGlobalDispatcher(mockAgent)
const mockPool = mockAgent.get('http://localhost:3000')
mockPool.intercept({
path: '/foo?hello=there&see=ya',
method: 'POST',
body: 'form1=data1&form2=data2'
}).reply(200, { foo: 'bar' }, {
headers: { 'content-type': 'application/json' },
trailers: { 'Content-MD5': 'test' }
})
const {
statusCode,
headers,
trailers,
body
} = await request('http://localhost:3000/foo?hello=there&see=ya', {
method: 'POST',
body: 'form1=data1&form2=data2'
})
console.log('response received', statusCode) // response received 200
console.log('headers', headers) // { 'content-type': 'application/json' }
for await (const data of body) {
console.log('data', data.toString('utf8')) // '{"foo":"bar"}'
}
console.log('trailers', trailers) // { 'content-md5': 'test' }
```
#### Example - Mocked request with origin regex
```js
import { MockAgent, setGlobalDispatcher, request } from 'undici'
const mockAgent = new MockAgent()
setGlobalDispatcher(mockAgent)
const mockPool = mockAgent.get(new RegExp('http://localhost:3000'))
mockPool.intercept({ path: '/foo' }).reply(200, 'foo')
const {
statusCode,
body
} = await request('http://localhost:3000/foo')
console.log('response received', statusCode) // response received 200
for await (const data of body) {
console.log('data', data.toString('utf8')) // data foo
}
```
#### Example - Mocked request with origin function
```js
import { MockAgent, setGlobalDispatcher, request } from 'undici'
const mockAgent = new MockAgent()
setGlobalDispatcher(mockAgent)
const mockPool = mockAgent.get((origin) => origin === 'http://localhost:3000')
mockPool.intercept({ path: '/foo' }).reply(200, 'foo')
const {
statusCode,
body
} = await request('http://localhost:3000/foo')
console.log('response received', statusCode) // response received 200
for await (const data of body) {
console.log('data', data.toString('utf8')) // data foo
}
```
### `MockAgent.close()`
Closes the mock agent and waits for registered mock pools and clients to also close before resolving.
Returns: `Promise`
#### Example - clean up after tests are complete
```js
import { MockAgent, setGlobalDispatcher } from 'undici'
const mockAgent = new MockAgent()
setGlobalDispatcher(mockAgent)
await mockAgent.close()
```
### `MockAgent.dispatch(options, handlers)`
Implements [`Agent.dispatch(options, handlers)`](/docs/docs/api/Agent.md#parameter-agentdispatchoptions).
### `MockAgent.request(options[, callback])`
See [`Dispatcher.request(options [, callback])`](/docs/docs/api/Dispatcher.md#dispatcherrequestoptions-callback).
#### Example - MockAgent request
```js
import { MockAgent } from 'undici'
const mockAgent = new MockAgent()
const mockPool = mockAgent.get('http://localhost:3000')
mockPool.intercept({ path: '/foo' }).reply(200, 'foo')
const {
statusCode,
body
} = await mockAgent.request({
origin: 'http://localhost:3000',
path: '/foo',
method: 'GET'
})
console.log('response received', statusCode) // response received 200
for await (const data of body) {
console.log('data', data.toString('utf8')) // data foo
}
```
### `MockAgent.deactivate()`
This method disables mocking in MockAgent.
Returns: `void`
#### Example - Deactivate Mocking
```js
import { MockAgent, setGlobalDispatcher } from 'undici'
const mockAgent = new MockAgent()
setGlobalDispatcher(mockAgent)
mockAgent.deactivate()
```
### `MockAgent.activate()`
This method enables mocking in a MockAgent instance. When instantiated, a MockAgent is automatically activated. Therefore, this method is only effective after `MockAgent.deactivate` has been called.
Returns: `void`
#### Example - Activate Mocking
```js
import { MockAgent, setGlobalDispatcher } from 'undici'
const mockAgent = new MockAgent()
setGlobalDispatcher(mockAgent)
mockAgent.deactivate()
// No mocking will occur
// Later
mockAgent.activate()
```
### `MockAgent.enableNetConnect([host])`
When requests are not matched in a MockAgent intercept, a real HTTP request is attempted. We can control this further through the use of `enableNetConnect`. This is achieved by defining host matchers so only matching requests will be attempted.
When using a string, it should only include the **hostname and optionally, the port**. In addition, calling this method multiple times with a string will allow all HTTP requests that match these values.
Arguments:
* **host** `string | RegExp | (value) => boolean` - (optional)
Returns: `void`
#### Example - Allow all non-matching urls to be dispatched in a real HTTP request
```js
import { MockAgent, setGlobalDispatcher, request } from 'undici'
const mockAgent = new MockAgent()
setGlobalDispatcher(mockAgent)
mockAgent.enableNetConnect()
await request('http://example.com')
// A real request is made
```
#### Example - Allow requests matching a host string to make real requests
```js
import { MockAgent, setGlobalDispatcher, request } from 'undici'
const mockAgent = new MockAgent()
setGlobalDispatcher(mockAgent)
mockAgent.enableNetConnect('example-1.com')
mockAgent.enableNetConnect('example-2.com:8080')
await request('http://example-1.com')
// A real request is made
await request('http://example-2.com:8080')
// A real request is made
await request('http://example-3.com')
// Will throw
```
#### Example - Allow requests matching a host regex to make real requests
```js
import { MockAgent, setGlobalDispatcher, request } from 'undici'
const mockAgent = new MockAgent()
setGlobalDispatcher(mockAgent)
mockAgent.enableNetConnect(new RegExp('example.com'))
await request('http://example.com')
// A real request is made
```
#### Example - Allow requests matching a host function to make real requests
```js
import { MockAgent, setGlobalDispatcher, request } from 'undici'
const mockAgent = new MockAgent()
setGlobalDispatcher(mockAgent)
mockAgent.enableNetConnect((value) => value === 'example.com')
await request('http://example.com')
// A real request is made
```
### `MockAgent.disableNetConnect()`
This method causes all requests to throw when requests are not matched in a MockAgent intercept.
Returns: `void`
#### Example - Disable all non-matching requests by throwing an error for each
```js
import { MockAgent, request } from 'undici'
const mockAgent = new MockAgent()
mockAgent.disableNetConnect()
await request('http://example.com')
// Will throw
```
### `MockAgent.pendingInterceptors()`
This method returns any pending interceptors registered on a mock agent. A pending interceptor meets one of the following criteria:
- Is registered with neither `.times()` nor `.persist()`, and has not been invoked;
- Is persistent (i.e., registered with `.persist()`) and has not been invoked;
- Is registered with `.times()` and has not been invoked `` of times.
Returns: `PendingInterceptor[]` (where `PendingInterceptor` is a `MockDispatch` with an additional `origin: string`)
#### Example - List all pending interceptors
```js
const agent = new MockAgent()
agent.disableNetConnect()
agent
.get('https://example.com')
.intercept({ method: 'GET', path: '/' })
.reply(200)
const pendingInterceptors = agent.pendingInterceptors()
// Returns [
// {
// timesInvoked: 0,
// times: 1,
// persist: false,
// consumed: false,
// pending: true,
// path: '/',
// method: 'GET',
// body: undefined,
// headers: undefined,
// data: {
// error: null,
// statusCode: 200,
// data: '',
// headers: {},
// trailers: {}
// },
// origin: 'https://example.com'
// }
// ]
```
### `MockAgent.assertNoPendingInterceptors([options])`
This method throws if the mock agent has any pending interceptors. A pending interceptor meets one of the following criteria:
- Is registered with neither `.times()` nor `.persist()`, and has not been invoked;
- Is persistent (i.e., registered with `.persist()`) and has not been invoked;
- Is registered with `.times()` and has not been invoked `` of times.
#### Example - Check that there are no pending interceptors
```js
const agent = new MockAgent()
agent.disableNetConnect()
agent
.get('https://example.com')
.intercept({ method: 'GET', path: '/' })
.reply(200)
agent.assertNoPendingInterceptors()
// Throws an UndiciError with the following message:
//
// 1 interceptor is pending:
//
// ┌─────────┬────────┬───────────────────────┬──────┬─────────────┬────────────┬─────────────┬───────────┐
// │ (index) │ Method │ Origin │ Path │ Status code │ Persistent │ Invocations │ Remaining │
// ├─────────┼────────┼───────────────────────┼──────┼─────────────┼────────────┼─────────────┼───────────┤
// │ 0 │ 'GET' │ 'https://example.com' │ '/' │ 200 │ '❌' │ 0 │ 1 │
// └─────────┴────────┴───────────────────────┴──────┴─────────────┴────────────┴─────────────┴───────────┘
```
#### Example - access call history on MockAgent
You can register every call made within a MockAgent to be able to retrieve the body, headers and so on.
This is not enabled by default.
```js
import { MockAgent, setGlobalDispatcher, request } from 'undici'
const mockAgent = new MockAgent({ enableCallHistory: true })
setGlobalDispatcher(mockAgent)
await request('http://example.com', { query: { item: 1 }})
mockAgent.getCallHistory()?.firstCall()
// Returns
// MockCallHistoryLog {
// body: undefined,
// headers: undefined,
// method: 'GET',
// origin: 'http://example.com',
// fullUrl: 'http://example.com/?item=1',
// path: '/',
// searchParams: { item: '1' },
// protocol: 'http:',
// host: 'example.com',
// port: ''
// }
```
#### Example - clear call history
```js
const mockAgent = new MockAgent()
mockAgent.clearAllCallHistory()
```
#### Example - call history instance class method
```js
const mockAgent = new MockAgent()
const mockAgentHistory = mockAgent.getCallHistory()
mockAgentHistory?.calls() // returns an array of MockCallHistoryLogs
mockAgentHistory?.firstCall() // returns the first MockCallHistoryLogs or undefined
mockAgentHistory?.lastCall() // returns the last MockCallHistoryLogs or undefined
mockAgentHistory?.nthCall(3) // returns the third MockCallHistoryLogs or undefined
mockAgentHistory?.filterCalls({ path: '/endpoint', hash: '#hash-value' }) // returns an Array of MockCallHistoryLogs WHERE path === /endpoint OR hash === #hash-value
mockAgentHistory?.filterCalls({ path: '/endpoint', hash: '#hash-value' }, { operator: 'AND' }) // returns an Array of MockCallHistoryLogs WHERE path === /endpoint AND hash === #hash-value
mockAgentHistory?.filterCalls(/"data": "{}"/) // returns an Array of MockCallHistoryLogs where any value match regexp
mockAgentHistory?.filterCalls('application/json') // returns an Array of MockCallHistoryLogs where any value === 'application/json'
mockAgentHistory?.filterCalls((log) => log.path === '/endpoint') // returns an Array of MockCallHistoryLogs when given function returns true
mockAgentHistory?.clear() // clear the history
```
================================================
FILE: docs/docs/api/MockCallHistory.md
================================================
# Class: MockCallHistory
Access to an instance with :
```js
const mockAgent = new MockAgent({ enableCallHistory: true })
mockAgent.getCallHistory()
// or
const mockAgent = new MockAgent()
mockAgent.enableMockHistory()
mockAgent.getCallHistory()
```
a MockCallHistory instance implements a **Symbol.iterator** letting you iterate on registered logs :
```ts
for (const log of mockAgent.getCallHistory()) {
//...
}
const array: Array = [...mockAgent.getCallHistory()]
const set: Set = new Set(mockAgent.getCallHistory())
```
## class methods
### clear
Clear all MockCallHistoryLog registered. This is automatically done when calling `mockAgent.close()`
```js
mockAgent.clearCallHistory()
// same as
mockAgent.getCallHistory()?.clear()
```
### calls
Get all MockCallHistoryLog registered as an array
```js
mockAgent.getCallHistory()?.calls()
```
### firstCall
Get the first MockCallHistoryLog registered or undefined
```js
mockAgent.getCallHistory()?.firstCall()
```
### lastCall
Get the last MockCallHistoryLog registered or undefined
```js
mockAgent.getCallHistory()?.lastCall()
```
### nthCall
Get the nth MockCallHistoryLog registered or undefined
```js
mockAgent.getCallHistory()?.nthCall(3) // the third MockCallHistoryLog registered
```
### filterCallsByProtocol
Filter MockCallHistoryLog by protocol.
> more details for the first parameter can be found [here](/docs/docs/api/MockCallHistory.md#filter-parameter)
```js
mockAgent.getCallHistory()?.filterCallsByProtocol(/https/)
mockAgent.getCallHistory()?.filterCallsByProtocol('https:')
```
### filterCallsByHost
Filter MockCallHistoryLog by host.
> more details for the first parameter can be found [here](/docs/docs/api/MockCallHistory.md#filter-parameter)
```js
mockAgent.getCallHistory()?.filterCallsByHost(/localhost/)
mockAgent.getCallHistory()?.filterCallsByHost('localhost:3000')
```
### filterCallsByPort
Filter MockCallHistoryLog by port.
> more details for the first parameter can be found [here](/docs/docs/api/MockCallHistory.md#filter-parameter)
```js
mockAgent.getCallHistory()?.filterCallsByPort(/3000/)
mockAgent.getCallHistory()?.filterCallsByPort('3000')
mockAgent.getCallHistory()?.filterCallsByPort('')
```
### filterCallsByOrigin
Filter MockCallHistoryLog by origin.
> more details for the first parameter can be found [here](/docs/docs/api/MockCallHistory.md#filter-parameter)
```js
mockAgent.getCallHistory()?.filterCallsByOrigin(/http:\/\/localhost:3000/)
mockAgent.getCallHistory()?.filterCallsByOrigin('http://localhost:3000')
```
### filterCallsByPath
Filter MockCallHistoryLog by path.
> more details for the first parameter can be found [here](/docs/docs/api/MockCallHistory.md#filter-parameter)
```js
mockAgent.getCallHistory()?.filterCallsByPath(/api\/v1\/graphql/)
mockAgent.getCallHistory()?.filterCallsByPath('/api/v1/graphql')
```
### filterCallsByHash
Filter MockCallHistoryLog by hash.
> more details for the first parameter can be found [here](/docs/docs/api/MockCallHistory.md#filter-parameter)
```js
mockAgent.getCallHistory()?.filterCallsByPath(/hash/)
mockAgent.getCallHistory()?.filterCallsByPath('#hash')
```
### filterCallsByFullUrl
Filter MockCallHistoryLog by fullUrl. fullUrl contains protocol, host, port, path, hash, and query params
> more details for the first parameter can be found [here](/docs/docs/api/MockCallHistory.md#filter-parameter)
```js
mockAgent.getCallHistory()?.filterCallsByFullUrl(/https:\/\/localhost:3000\/\?query=value#hash/)
mockAgent.getCallHistory()?.filterCallsByFullUrl('https://localhost:3000/?query=value#hash')
```
### filterCallsByMethod
Filter MockCallHistoryLog by method.
> more details for the first parameter can be found [here](/docs/docs/api/MockCallHistory.md#filter-parameter)
```js
mockAgent.getCallHistory()?.filterCallsByMethod(/POST/)
mockAgent.getCallHistory()?.filterCallsByMethod('POST')
```
### filterCalls
This class method is a meta function / alias to apply complex filtering in a single way.
Parameters :
- criteria : the first parameter. a function, regexp or object.
- function : filter MockCallHistoryLog when the function returns false
- regexp : filter MockCallHistoryLog when the regexp does not match on MockCallHistoryLog.toString() ([see](./MockCallHistoryLog.md#to-string))
- object : an object with MockCallHistoryLog properties as keys to apply multiple filters. each values are a [filter parameter](/docs/docs/api/MockCallHistory.md#filter-parameter)
- options : the second parameter. an object.
- options.operator : `'AND'` or `'OR'` (default `'OR'`). Used only if criteria is an object. see below
```js
mockAgent.getCallHistory()?.filterCalls((log) => log.hash === value && log.headers?.['authorization'] !== undefined)
mockAgent.getCallHistory()?.filterCalls(/"data": "{ "errors": "wrong body" }"/)
// returns an Array of MockCallHistoryLog which all have
// - a hash containing my-hash
// - OR
// - a path equal to /endpoint
mockAgent.getCallHistory()?.filterCalls({ hash: /my-hash/, path: '/endpoint' })
// returns an Array of MockCallHistoryLog which all have
// - a hash containing my-hash
// - AND
// - a path equal to /endpoint
mockAgent.getCallHistory()?.filterCalls({ hash: /my-hash/, path: '/endpoint' }, { operator: 'AND' })
```
## filter parameter
Can be :
- string. MockCallHistoryLog filtered if `value !== parameterValue`
- null. MockCallHistoryLog filtered if `value !== parameterValue`
- undefined. MockCallHistoryLog filtered if `value !== parameterValue`
- regexp. MockCallHistoryLog filtered if `!parameterValue.test(value)`
================================================
FILE: docs/docs/api/MockCallHistoryLog.md
================================================
# Class: MockCallHistoryLog
Access to an instance with :
```js
const mockAgent = new MockAgent({ enableCallHistory: true })
mockAgent.getCallHistory()?.firstCall()
```
## class properties
- body `mockAgent.getCallHistory()?.firstCall()?.body`
- headers `mockAgent.getCallHistory()?.firstCall()?.headers` an object
- method `mockAgent.getCallHistory()?.firstCall()?.method` a string
- fullUrl `mockAgent.getCallHistory()?.firstCall()?.fullUrl` a string containing the protocol, origin, path, query and hash
- origin `mockAgent.getCallHistory()?.firstCall()?.origin` a string containing the protocol and the host
- headers `mockAgent.getCallHistory()?.firstCall()?.headers` an object
- path `mockAgent.getCallHistory()?.firstCall()?.path` a string always starting with `/`
- searchParams `mockAgent.getCallHistory()?.firstCall()?.searchParams` an object
- protocol `mockAgent.getCallHistory()?.firstCall()?.protocol` a string (`https:`)
- host `mockAgent.getCallHistory()?.firstCall()?.host` a string
- port `mockAgent.getCallHistory()?.firstCall()?.port` an empty string or a string containing numbers
- hash `mockAgent.getCallHistory()?.firstCall()?.hash` an empty string or a string starting with `#`
## class methods
### toMap
Returns a Map instance
```js
mockAgent.getCallHistory()?.firstCall()?.toMap()?.get('hash')
// #hash
```
### toString
Returns a string computed with any class property name and value pair
```js
mockAgent.getCallHistory()?.firstCall()?.toString()
// protocol->https:|host->localhost:4000|port->4000|origin->https://localhost:4000|path->/endpoint|hash->#here|searchParams->{"query":"value"}|fullUrl->https://localhost:4000/endpoint?query=value#here|method->PUT|body->"{ "data": "hello" }"|headers->{"content-type":"application/json"}
```
================================================
FILE: docs/docs/api/MockClient.md
================================================
# Class: MockClient
Extends: `undici.Client`
A mock client class that implements the same api as [MockPool](/docs/docs/api/MockPool.md).
## `new MockClient(origin, [options])`
Arguments:
* **origin** `string` - It should only include the **protocol, hostname, and port**.
* **options** `MockClientOptions` - It extends the `Client` options.
Returns: `MockClient`
### Parameter: `MockClientOptions`
Extends: `ClientOptions`
* **agent** `Agent` - the agent to associate this MockClient with.
### Example - Basic MockClient instantiation
We can use MockAgent to instantiate a MockClient ready to be used to intercept specified requests. It will not do anything until registered as the agent to use and any mock request are registered.
```js
import { MockAgent } from 'undici'
// Connections must be set to 1 to return a MockClient instance
const mockAgent = new MockAgent({ connections: 1 })
const mockClient = mockAgent.get('http://localhost:3000')
```
## Instance Methods
### `MockClient.intercept(options)`
Implements: [`MockPool.intercept(options)`](/docs/docs/api/MockPool.md#mockpoolinterceptoptions)
### `MockClient.cleanMocks()`
Implements: [`MockPool.cleanMocks()`](/docs/docs/api/MockPool.md#mockpoolcleanmocks)
### `MockClient.close()`
Implements: [`MockPool.close()`](/docs/docs/api/MockPool.md#mockpoolclose)
### `MockClient.dispatch(options, handlers)`
Implements [`Dispatcher.dispatch(options, handlers)`](/docs/docs/api/Dispatcher.md#dispatcherdispatchoptions-handler).
### `MockClient.request(options[, callback])`
See [`Dispatcher.request(options [, callback])`](/docs/docs/api/Dispatcher.md#dispatcherrequestoptions-callback).
#### Example - MockClient request
```js
import { MockAgent } from 'undici'
const mockAgent = new MockAgent({ connections: 1 })
const mockClient = mockAgent.get('http://localhost:3000')
mockClient.intercept({ path: '/foo' }).reply(200, 'foo')
const {
statusCode,
body
} = await mockClient.request({
origin: 'http://localhost:3000',
path: '/foo',
method: 'GET'
})
console.log('response received', statusCode) // response received 200
for await (const data of body) {
console.log('data', data.toString('utf8')) // data foo
}
```
================================================
FILE: docs/docs/api/MockErrors.md
================================================
# MockErrors
Undici exposes a variety of mock error objects that you can use to enhance your mock error handling.
You can find all the mock error objects inside the `mockErrors` key.
```js
import { mockErrors } from 'undici'
```
| Mock Error | Mock Error Codes | Description |
| --------------------- | ------------------------------- | ---------------------------------------------------------- |
| `MockNotMatchedError` | `UND_MOCK_ERR_MOCK_NOT_MATCHED` | The request does not match any registered mock dispatches. |
================================================
FILE: docs/docs/api/MockPool.md
================================================
# Class: MockPool
Extends: `undici.Pool`
A mock Pool class that implements the Pool API and is used by MockAgent to intercept real requests and return mocked responses.
## `new MockPool(origin, [options])`
Arguments:
* **origin** `string` - It should only include the **protocol, hostname, and port**.
* **options** `MockPoolOptions` - It extends the `Pool` options.
Returns: `MockPool`
### Parameter: `MockPoolOptions`
Extends: `PoolOptions`
* **agent** `Agent` - the agent to associate this MockPool with.
### Example - Basic MockPool instantiation
We can use MockAgent to instantiate a MockPool ready to be used to intercept specified requests. It will not do anything until registered as the agent to use and any mock request are registered.
```js
import { MockAgent } from 'undici'
const mockAgent = new MockAgent()
const mockPool = mockAgent.get('http://localhost:3000')
```
## Instance Methods
### `MockPool.intercept(options)`
This method defines the interception rules for matching against requests for a MockPool or MockPool. We can intercept multiple times on a single instance, but each intercept is only used once. For example if you expect to make 2 requests inside a test, you need to call `intercept()` twice. Assuming you use `disableNetConnect()` you will get `MockNotMatchedError` on the second request when you only call `intercept()` once.
When defining interception rules, all the rules must pass for a request to be intercepted. If a request is not intercepted, a real request will be attempted.
| Matcher type | Condition to pass |
|:------------:| -------------------------- |
| `string` | Exact match against string |
| `RegExp` | Regex must pass |
| `Function` | Function must return true |
Arguments:
* **options** `MockPoolInterceptOptions` - Interception options.
Returns: `MockInterceptor` corresponding to the input options.
### Parameter: `MockPoolInterceptOptions`
* **path** `string | RegExp | (path: string) => boolean` - a matcher for the HTTP request path. When a `RegExp` or callback is used, it will match against the request path including all query parameters in alphabetical order. When a `string` is provided, the query parameters can be conveniently specified through the `MockPoolInterceptOptions.query` setting.
* **method** `string | RegExp | (method: string) => boolean` - (optional) - a matcher for the HTTP request method. Defaults to `GET`.
* **body** `string | RegExp | (body: string) => boolean` - (optional) - a matcher for the HTTP request body.
* **headers** `Record boolean`> - (optional) - a matcher for the HTTP request headers. To be intercepted, a request must match all defined headers. Extra headers not defined here may (or may not) be included in the request and do not affect the interception in any way.
* **query** `Record | null` - (optional) - a matcher for the HTTP request query string params. Only applies when a `string` was provided for `MockPoolInterceptOptions.path`.
* **ignoreTrailingSlash** `boolean` - (optional) - set to `true` if the matcher should also match by ignoring potential trailing slashes in `MockPoolInterceptOptions.path`.
### Return: `MockInterceptor`
We can define the behaviour of an intercepted request with the following options.
* **reply** `(statusCode: number, replyData: string | Buffer | object | MockInterceptor.MockResponseDataHandler, responseOptions?: MockResponseOptions) => MockScope` - define a reply for a matching request. You can define the replyData as a callback to read incoming request data. Default for `responseOptions` is `{}`.
* **reply** `(callback: MockInterceptor.MockReplyOptionsCallback) => MockScope` - define a reply for a matching request, allowing dynamic mocking of all reply options rather than just the data.
* **replyWithError** `(error: Error) => MockScope` - define an error for a matching request to throw.
* **defaultReplyHeaders** `(headers: Record) => MockInterceptor` - define default headers to be included in subsequent replies. These are in addition to headers on a specific reply.
* **defaultReplyTrailers** `(trailers: Record) => MockInterceptor` - define default trailers to be included in subsequent replies. These are in addition to trailers on a specific reply.
* **replyContentLength** `() => MockInterceptor` - define automatically calculated `content-length` headers to be included in subsequent replies.
The reply data of an intercepted request may either be a string, buffer, or JavaScript object. Objects are converted to JSON while strings and buffers are sent as-is.
By default, `reply` and `replyWithError` define the behaviour for the first matching request only. Subsequent requests will not be affected (this can be changed using the returned `MockScope`).
### Parameter: `MockResponseOptions`
* **headers** `Record` - headers to be included on the mocked reply.
* **trailers** `Record` - trailers to be included on the mocked reply.
### Return: `MockScope`
A `MockScope` is associated with a single `MockInterceptor`. With this, we can configure the default behaviour of an intercepted reply.
* **delay** `(waitInMs: number) => MockScope` - delay the associated reply by a set amount in ms.
* **persist** `() => MockScope` - any matching request will always reply with the defined response indefinitely.
* **times** `(repeatTimes: number) => MockScope` - any matching request will reply with the defined response a fixed amount of times. This is overridden by **persist**.
#### Example - Basic Mocked Request
```js
import { MockAgent, setGlobalDispatcher, request } from 'undici'
const mockAgent = new MockAgent()
setGlobalDispatcher(mockAgent)
// MockPool
const mockPool = mockAgent.get('http://localhost:3000')
mockPool.intercept({ path: '/foo' }).reply(200, 'foo')
const {
statusCode,
body
} = await request('http://localhost:3000/foo')
console.log('response received', statusCode) // response received 200
for await (const data of body) {
console.log('data', data.toString('utf8')) // data foo
}
```
#### Example - Mocked request using reply data callbacks
```js
import { MockAgent, setGlobalDispatcher, request } from 'undici'
const mockAgent = new MockAgent()
setGlobalDispatcher(mockAgent)
const mockPool = mockAgent.get('http://localhost:3000')
mockPool.intercept({
path: '/echo',
method: 'GET',
headers: {
'User-Agent': 'undici',
Host: 'example.com'
}
}).reply(200, ({ headers }) => ({ message: headers.get('message') }))
const { statusCode, body, headers } = await request('http://localhost:3000', {
headers: {
message: 'hello world!'
}
})
console.log('response received', statusCode) // response received 200
console.log('headers', headers) // { 'content-type': 'application/json' }
for await (const data of body) {
console.log('data', data.toString('utf8')) // { "message":"hello world!" }
}
```
#### Example - Mocked request using reply options callback
```js
import { MockAgent, setGlobalDispatcher, request } from 'undici'
const mockAgent = new MockAgent()
setGlobalDispatcher(mockAgent)
const mockPool = mockAgent.get('http://localhost:3000')
mockPool.intercept({
path: '/echo',
method: 'GET',
headers: {
'User-Agent': 'undici',
Host: 'example.com'
}
}).reply(({ headers }) => ({ statusCode: 200, data: { message: headers.get('message') }})))
const { statusCode, body, headers } = await request('http://localhost:3000', {
headers: {
message: 'hello world!'
}
})
console.log('response received', statusCode) // response received 200
console.log('headers', headers) // { 'content-type': 'application/json' }
for await (const data of body) {
console.log('data', data.toString('utf8')) // { "message":"hello world!" }
}
```
#### Example - Basic Mocked requests with multiple intercepts
```js
import { MockAgent, setGlobalDispatcher, request } from 'undici'
const mockAgent = new MockAgent()
setGlobalDispatcher(mockAgent)
const mockPool = mockAgent.get('http://localhost:3000')
mockPool.intercept({
path: '/foo',
method: 'GET'
}).reply(200, 'foo')
mockPool.intercept({
path: '/hello',
method: 'GET',
}).reply(200, 'hello')
const result1 = await request('http://localhost:3000/foo')
console.log('response received', result1.statusCode) // response received 200
for await (const data of result1.body) {
console.log('data', data.toString('utf8')) // data foo
}
const result2 = await request('http://localhost:3000/hello')
console.log('response received', result2.statusCode) // response received 200
for await (const data of result2.body) {
console.log('data', data.toString('utf8')) // data hello
}
```
#### Example - Mocked request with query body, request headers and response headers and trailers
```js
import { MockAgent, setGlobalDispatcher, request } from 'undici'
const mockAgent = new MockAgent()
setGlobalDispatcher(mockAgent)
const mockPool = mockAgent.get('http://localhost:3000')
mockPool.intercept({
path: '/foo?hello=there&see=ya',
method: 'POST',
body: 'form1=data1&form2=data2',
headers: {
'User-Agent': 'undici',
Host: 'example.com'
}
}).reply(200, { foo: 'bar' }, {
headers: { 'content-type': 'application/json' },
trailers: { 'Content-MD5': 'test' }
})
const {
statusCode,
headers,
trailers,
body
} = await request('http://localhost:3000/foo?hello=there&see=ya', {
method: 'POST',
body: 'form1=data1&form2=data2',
headers: {
foo: 'bar',
'User-Agent': 'undici',
Host: 'example.com'
}
})
console.log('response received', statusCode) // response received 200
console.log('headers', headers) // { 'content-type': 'application/json' }
for await (const data of body) {
console.log('data', data.toString('utf8')) // '{"foo":"bar"}'
}
console.log('trailers', trailers) // { 'content-md5': 'test' }
```
#### Example - Mocked request using different matchers
```js
import { MockAgent, setGlobalDispatcher, request } from 'undici'
const mockAgent = new MockAgent()
setGlobalDispatcher(mockAgent)
const mockPool = mockAgent.get('http://localhost:3000')
mockPool.intercept({
path: '/foo',
method: /^GET$/,
body: (value) => value === 'form=data',
headers: {
'User-Agent': 'undici',
Host: /^example.com$/
}
}).reply(200, 'foo')
const {
statusCode,
body
} = await request('http://localhost:3000/foo', {
method: 'GET',
body: 'form=data',
headers: {
foo: 'bar',
'User-Agent': 'undici',
Host: 'example.com'
}
})
console.log('response received', statusCode) // response received 200
for await (const data of body) {
console.log('data', data.toString('utf8')) // data foo
}
```
#### Example - Mocked request with reply with a defined error
```js
import { MockAgent, setGlobalDispatcher, request } from 'undici'
const mockAgent = new MockAgent()
setGlobalDispatcher(mockAgent)
const mockPool = mockAgent.get('http://localhost:3000')
mockPool.intercept({
path: '/foo',
method: 'GET'
}).replyWithError(new Error('kaboom'))
try {
await request('http://localhost:3000/foo', {
method: 'GET'
})
} catch (error) {
console.error(error) // TypeError: fetch failed
console.error(error.cause) // Error: kaboom
}
```
#### Example - Mocked request with defaultReplyHeaders
```js
import { MockAgent, setGlobalDispatcher, request } from 'undici'
const mockAgent = new MockAgent()
setGlobalDispatcher(mockAgent)
const mockPool = mockAgent.get('http://localhost:3000')
mockPool.intercept({
path: '/foo',
method: 'GET'
}).defaultReplyHeaders({ foo: 'bar' })
.reply(200, 'foo')
const { headers } = await request('http://localhost:3000/foo')
console.log('headers', headers) // headers { foo: 'bar' }
```
#### Example - Mocked request with defaultReplyTrailers
```js
import { MockAgent, setGlobalDispatcher, request } from 'undici'
const mockAgent = new MockAgent()
setGlobalDispatcher(mockAgent)
const mockPool = mockAgent.get('http://localhost:3000')
mockPool.intercept({
path: '/foo',
method: 'GET'
}).defaultReplyTrailers({ foo: 'bar' })
.reply(200, 'foo')
const { trailers } = await request('http://localhost:3000/foo')
console.log('trailers', trailers) // trailers { foo: 'bar' }
```
#### Example - Mocked request with automatic content-length calculation
```js
import { MockAgent, setGlobalDispatcher, request } from 'undici'
const mockAgent = new MockAgent()
setGlobalDispatcher(mockAgent)
const mockPool = mockAgent.get('http://localhost:3000')
mockPool.intercept({
path: '/foo',
method: 'GET'
}).replyContentLength().reply(200, 'foo')
const { headers } = await request('http://localhost:3000/foo')
console.log('headers', headers) // headers { 'content-length': '3' }
```
#### Example - Mocked request with automatic content-length calculation on an object
```js
import { MockAgent, setGlobalDispatcher, request } from 'undici'
const mockAgent = new MockAgent()
setGlobalDispatcher(mockAgent)
const mockPool = mockAgent.get('http://localhost:3000')
mockPool.intercept({
path: '/foo',
method: 'GET'
}).replyContentLength().reply(200, { foo: 'bar' })
const { headers } = await request('http://localhost:3000/foo')
console.log('headers', headers) // headers { 'content-length': '13' }
```
#### Example - Mocked request with persist enabled
```js
import { MockAgent, setGlobalDispatcher, request } from 'undici'
const mockAgent = new MockAgent()
setGlobalDispatcher(mockAgent)
const mockPool = mockAgent.get('http://localhost:3000')
mockPool.intercept({
path: '/foo',
method: 'GET'
}).reply(200, 'foo').persist()
const result1 = await request('http://localhost:3000/foo')
// Will match and return mocked data
const result2 = await request('http://localhost:3000/foo')
// Will match and return mocked data
// Etc
```
#### Example - Mocked request with times enabled
```js
import { MockAgent, setGlobalDispatcher, request } from 'undici'
const mockAgent = new MockAgent()
setGlobalDispatcher(mockAgent)
const mockPool = mockAgent.get('http://localhost:3000')
mockPool.intercept({
path: '/foo',
method: 'GET'
}).reply(200, 'foo').times(2)
const result1 = await request('http://localhost:3000/foo')
// Will match and return mocked data
const result2 = await request('http://localhost:3000/foo')
// Will match and return mocked data
const result3 = await request('http://localhost:3000/foo')
// Will not match and make attempt a real request
```
#### Example - Mocked request with path callback
```js
import { MockAgent, setGlobalDispatcher, request } from 'undici'
import querystring from 'querystring'
const mockAgent = new MockAgent()
setGlobalDispatcher(mockAgent)
const mockPool = mockAgent.get('http://localhost:3000')
const matchPath = requestPath => {
const [pathname, search] = requestPath.split('?')
const requestQuery = querystring.parse(search)
if (!pathname.startsWith('/foo')) {
return false
}
if (!Object.keys(requestQuery).includes('foo') || requestQuery.foo !== 'bar') {
return false
}
return true
}
mockPool.intercept({
path: matchPath,
method: 'GET'
}).reply(200, 'foo')
const result = await request('http://localhost:3000/foo?foo=bar')
// Will match and return mocked data
```
### `MockPool.close()`
Closes the mock pool and de-registers from associated MockAgent.
Returns: `Promise`
#### Example - clean up after tests are complete
```js
import { MockAgent } from 'undici'
const mockAgent = new MockAgent()
const mockPool = mockAgent.get('http://localhost:3000')
await mockPool.close()
```
### `MockPool.dispatch(options, handlers)`
Implements [`Dispatcher.dispatch(options, handlers)`](/docs/docs/api/Dispatcher.md#dispatcherdispatchoptions-handler).
### `MockPool.request(options[, callback])`
See [`Dispatcher.request(options [, callback])`](/docs/docs/api/Dispatcher.md#dispatcherrequestoptions-callback).
#### Example - MockPool request
```js
import { MockAgent } from 'undici'
const mockAgent = new MockAgent()
const mockPool = mockAgent.get('http://localhost:3000')
mockPool.intercept({
path: '/foo',
method: 'GET',
}).reply(200, 'foo')
const {
statusCode,
body
} = await mockPool.request({
origin: 'http://localhost:3000',
path: '/foo',
method: 'GET'
})
console.log('response received', statusCode) // response received 200
for await (const data of body) {
console.log('data', data.toString('utf8')) // data foo
}
```
### `MockPool.cleanMocks()`
This method cleans up all the prepared mocks.
Returns: `void`
================================================
FILE: docs/docs/api/Pool.md
================================================
# Class: Pool
Extends: `undici.Dispatcher`
A pool of [Client](/docs/docs/api/Client.md) instances connected to the same upstream target.
Requests are not guaranteed to be dispatched in order of invocation.
## `new Pool(url[, options])`
Arguments:
* **url** `URL | string` - It should only include the **protocol, hostname, and port**.
* **options** `PoolOptions` (optional)
### Parameter: `PoolOptions`
Extends: [`ClientOptions`](/docs/docs/api/Client.md#parameter-clientoptions)
* **factory** `(origin: URL, opts: Object) => Dispatcher` - Default: `(origin, opts) => new Client(origin, opts)`
* **connections** `number | null` (optional) - Default: `null` - The number of `Client` instances to create. When set to `null`, the `Pool` instance will create an unlimited amount of `Client` instances.
* **clientTtl** `number | null` (optional) - Default: `null` - The amount of time before a `Client` instance is removed from the `Pool` and closed. When set to `null`, `Client` instances will not be removed or closed based on age.
## Instance Properties
### `Pool.closed`
Implements [Client.closed](/docs/docs/api/Client.md#clientclosed)
### `Pool.destroyed`
Implements [Client.destroyed](/docs/docs/api/Client.md#clientdestroyed)
### `Pool.stats`
Returns [`PoolStats`](PoolStats.md) instance for this pool.
## Instance Methods
### `Pool.close([callback])`
Implements [`Dispatcher.close([callback])`](/docs/docs/api/Dispatcher.md#dispatcherclosecallback-promise).
### `Pool.destroy([error, callback])`
Implements [`Dispatcher.destroy([error, callback])`](/docs/docs/api/Dispatcher.md#dispatcherdestroyerror-callback-promise).
### `Pool.connect(options[, callback])`
See [`Dispatcher.connect(options[, callback])`](/docs/docs/api/Dispatcher.md#dispatcherconnectoptions-callback).
### `Pool.dispatch(options, handler)`
Implements [`Dispatcher.dispatch(options, handler)`](/docs/docs/api/Dispatcher.md#dispatcherdispatchoptions-handler).
### `Pool.pipeline(options, handler)`
See [`Dispatcher.pipeline(options, handler)`](/docs/docs/api/Dispatcher.md#dispatcherpipelineoptions-handler).
### `Pool.request(options[, callback])`
See [`Dispatcher.request(options [, callback])`](/docs/docs/api/Dispatcher.md#dispatcherrequestoptions-callback).
### `Pool.stream(options, factory[, callback])`
See [`Dispatcher.stream(options, factory[, callback])`](/docs/docs/api/Dispatcher.md#dispatcherstreamoptions-factory-callback).
### `Pool.upgrade(options[, callback])`
See [`Dispatcher.upgrade(options[, callback])`](/docs/docs/api/Dispatcher.md#dispatcherupgradeoptions-callback).
## Instance Events
### Event: `'connect'`
See [Dispatcher Event: `'connect'`](/docs/docs/api/Dispatcher.md#event-connect).
### Event: `'disconnect'`
See [Dispatcher Event: `'disconnect'`](/docs/docs/api/Dispatcher.md#event-disconnect).
### Event: `'drain'`
See [Dispatcher Event: `'drain'`](/docs/docs/api/Dispatcher.md#event-drain).
================================================
FILE: docs/docs/api/PoolStats.md
================================================
# Class: PoolStats
Aggregate stats for a [Pool](/docs/docs/api/Pool.md) or [BalancedPool](/docs/docs/api/BalancedPool.md).
## `new PoolStats(pool)`
Arguments:
* **pool** `Pool` - Pool or BalancedPool from which to return stats.
## Instance Properties
### `PoolStats.connected`
Number of open socket connections in this pool.
### `PoolStats.free`
Number of open socket connections in this pool that do not have an active request.
### `PoolStats.pending`
Number of pending requests across all clients in this pool.
### `PoolStats.queued`
Number of queued requests across all clients in this pool.
### `PoolStats.running`
Number of currently active requests across all clients in this pool.
### `PoolStats.size`
Number of active, pending, or queued requests across all clients in this pool.
================================================
FILE: docs/docs/api/ProxyAgent.md
================================================
# Class: ProxyAgent
Extends: `undici.Dispatcher`
A Proxy Agent class that implements the Agent API. It allows the connection through proxy in a simple way.
## `new ProxyAgent([options])`
Arguments:
* **options** `ProxyAgentOptions` (required) - It extends the `Agent` options.
Returns: `ProxyAgent`
### Parameter: `ProxyAgentOptions`
Extends: [`AgentOptions`](/docs/docs/api/Agent.md#parameter-agentoptions)
> It ommits `AgentOptions#connect`.
> **Note:** When `AgentOptions#connections` is set, and different from `0`, the non-standard [`proxy-connection` header](https://udger.com/resources/http-request-headers-detail?header=Proxy-Connection) will be set to `keep-alive` in the request.
* **uri** `string | URL` (required) - The URI of the proxy server. This can be provided as a string, as an instance of the URL class, or as an object with a `uri` property of type string.
If the `uri` is provided as a string or `uri` is an object with an `uri` property of type string, then it will be parsed into a `URL` object according to the [WHATWG URL Specification](https://url.spec.whatwg.org).
For detailed information on the parsing process and potential validation errors, please refer to the ["Writing" section](https://url.spec.whatwg.org/#writing) of the WHATWG URL Specification.
* **token** `string` (optional) - It can be passed by a string of token for authentication.
* **auth** `string` (**deprecated**) - Use token.
* **clientFactory** `(origin: URL, opts: Object) => Dispatcher` (optional) - Default: `(origin, opts) => new Pool(origin, opts)`
* **requestTls** `BuildOptions` (optional) - Options object passed when creating the underlying socket via the connector builder for the request. It extends from [`Client#ConnectOptions`](/docs/docs/api/Client.md#parameter-connectoptions).
* **proxyTls** `BuildOptions` (optional) - Options object passed when creating the underlying socket via the connector builder for the proxy server. It extends from [`Client#ConnectOptions`](/docs/docs/api/Client.md#parameter-connectoptions).
* **proxyTunnel** `boolean` (optional) - For connections involving secure protocols, Undici will always establish a tunnel via the HTTP2 CONNECT extension. If proxyTunnel is set to true, this will occur for unsecured proxy/endpoint connections as well. Currently, there is no way to facilitate HTTP1 IP tunneling as described in https://www.rfc-editor.org/rfc/rfc9484.html#name-http-11-request. If proxyTunnel is set to false (the default), ProxyAgent connections where both the Proxy and Endpoint are unsecured will issue all requests to the Proxy, and prefix the endpoint request path with the endpoint origin address.
Examples:
```js
import { ProxyAgent } from 'undici'
const proxyAgent = new ProxyAgent('my.proxy.server')
// or
const proxyAgent = new ProxyAgent(new URL('my.proxy.server'))
// or
const proxyAgent = new ProxyAgent({ uri: 'my.proxy.server' })
// or
const proxyAgent = new ProxyAgent({
uri: new URL('my.proxy.server'),
proxyTls: {
signal: AbortSignal.timeout(1000)
}
})
```
#### Example - Basic ProxyAgent instantiation
This will instantiate the ProxyAgent. It will not do anything until registered as the agent to use with requests.
```js
import { ProxyAgent } from 'undici'
const proxyAgent = new ProxyAgent('my.proxy.server')
```
#### Example - Basic Proxy Request with global agent dispatcher
```js
import { setGlobalDispatcher, request, ProxyAgent } from 'undici'
const proxyAgent = new ProxyAgent('my.proxy.server')
setGlobalDispatcher(proxyAgent)
const { statusCode, body } = await request('http://localhost:3000/foo')
console.log('response received', statusCode) // response received 200
for await (const data of body) {
console.log('data', data.toString('utf8')) // data foo
}
```
#### Example - Basic Proxy Request with local agent dispatcher
```js
import { ProxyAgent, request } from 'undici'
const proxyAgent = new ProxyAgent('my.proxy.server')
const {
statusCode,
body
} = await request('http://localhost:3000/foo', { dispatcher: proxyAgent })
console.log('response received', statusCode) // response received 200
for await (const data of body) {
console.log('data', data.toString('utf8')) // data foo
}
```
#### Example - Basic Proxy Request with authentication
```js
import { setGlobalDispatcher, request, ProxyAgent } from 'undici';
const proxyAgent = new ProxyAgent({
uri: 'my.proxy.server',
// token: 'Bearer xxxx'
token: `Basic ${Buffer.from('username:password').toString('base64')}`
});
setGlobalDispatcher(proxyAgent);
const { statusCode, body } = await request('http://localhost:3000/foo');
console.log('response received', statusCode); // response received 200
for await (const data of body) {
console.log('data', data.toString('utf8')); // data foo
}
```
### `ProxyAgent.close()`
Closes the proxy agent and waits for registered pools and clients to also close before resolving.
Returns: `Promise`
#### Example - clean up after tests are complete
```js
import { ProxyAgent, setGlobalDispatcher } from 'undici'
const proxyAgent = new ProxyAgent('my.proxy.server')
setGlobalDispatcher(proxyAgent)
await proxyAgent.close()
```
### `ProxyAgent.dispatch(options, handlers)`
Implements [`Agent.dispatch(options, handlers)`](/docs/docs/api/Agent.md#parameter-agentdispatchoptions).
### `ProxyAgent.request(options[, callback])`
See [`Dispatcher.request(options [, callback])`](/docs/docs/api/Dispatcher.md#dispatcherrequestoptions-callback).
#### Example - ProxyAgent with Fetch
This example demonstrates how to use `fetch` with a proxy via `ProxyAgent`. It is particularly useful for scenarios requiring proxy tunneling.
```javascript
import { ProxyAgent, fetch } from 'undici';
// Define the ProxyAgent
const proxyAgent = new ProxyAgent('http://localhost:8000');
// Make a GET request through the proxy
const response = await fetch('http://localhost:3000/foo', {
dispatcher: proxyAgent,
method: 'GET',
});
console.log('Response status:', response.status);
console.log('Response data:', await response.text());
```
---
#### Example - ProxyAgent with a Custom Proxy Server
This example shows how to create a custom proxy server and use it with `ProxyAgent`.
```javascript
import * as http from 'node:http';
import { createProxy } from 'proxy';
import { ProxyAgent, fetch } from 'undici';
// Create a proxy server
const proxyServer = createProxy(http.createServer());
proxyServer.listen(8000, () => {
console.log('Proxy server running on port 8000');
});
// Define and use the ProxyAgent
const proxyAgent = new ProxyAgent('http://localhost:8000');
const response = await fetch('http://example.com', {
dispatcher: proxyAgent,
method: 'GET',
});
console.log('Response status:', response.status);
console.log('Response data:', await response.text());
```
---
#### Example - ProxyAgent with HTTPS Tunneling
This example demonstrates how to perform HTTPS tunneling using a proxy.
```javascript
import { ProxyAgent, fetch } from 'undici';
// Define a ProxyAgent for HTTPS proxy
const proxyAgent = new ProxyAgent('https://secure.proxy.server');
// Make a request to an HTTPS endpoint via the proxy
const response = await fetch('https://secure.endpoint.com/api/data', {
dispatcher: proxyAgent,
method: 'GET',
});
console.log('Response status:', response.status);
console.log('Response data:', await response.json());
```
#### Example - ProxyAgent as a Global Dispatcher
`ProxyAgent` can be configured as a global dispatcher, making it available for all requests without explicitly passing it. This simplifies code and is useful when a single proxy configuration applies to all requests.
```javascript
import { ProxyAgent, setGlobalDispatcher, fetch } from 'undici';
// Define and configure the ProxyAgent
const proxyAgent = new ProxyAgent('http://localhost:8000');
setGlobalDispatcher(proxyAgent);
// Make requests without specifying the dispatcher
const response = await fetch('http://example.com');
console.log('Response status:', response.status);
console.log('Response data:', await response.text());
================================================
FILE: docs/docs/api/RedirectHandler.md
================================================
# Class: RedirectHandler
A class that handles redirection logic for HTTP requests.
## `new RedirectHandler(dispatch, maxRedirections, opts, handler, redirectionLimitReached)`
Arguments:
- **dispatch** `function` - The dispatch function to be called after every retry.
- **maxRedirections** `number` - Maximum number of redirections allowed.
- **opts** `object` - Options for handling redirection.
- **handler** `object` - An object containing handlers for different stages of the request lifecycle.
- **redirectionLimitReached** `boolean` (default: `false`) - A flag that the implementer can provide to enable or disable the feature. If set to `false`, it indicates that the caller doesn't want to use the feature and prefers the old behavior.
Returns: `RedirectHandler`
### Parameters
- **dispatch** `(options: Dispatch.DispatchOptions, handlers: Dispatch.DispatchHandler) => Promise` (required) - Dispatch function to be called after every redirection.
- **maxRedirections** `number` (required) - Maximum number of redirections allowed.
- **opts** `object` (required) - Options for handling redirection.
- **handler** `object` (required) - Handlers for different stages of the request lifecycle.
- **redirectionLimitReached** `boolean` (default: `false`) - A flag that the implementer can provide to enable or disable the feature. If set to `false`, it indicates that the caller doesn't want to use the feature and prefers the old behavior.
### Properties
- **location** `string` - The current redirection location.
- **abort** `function` - The abort function.
- **opts** `object` - The options for handling redirection.
- **maxRedirections** `number` - Maximum number of redirections allowed.
- **handler** `object` - Handlers for different stages of the request lifecycle.
- **history** `Array` - An array representing the history of URLs during redirection.
- **redirectionLimitReached** `boolean` - Indicates whether the redirection limit has been reached.
### Methods
#### `onConnect(abort)`
Called when the connection is established.
Parameters:
- **abort** `function` - The abort function.
#### `onUpgrade(statusCode, headers, socket)`
Called when an upgrade is requested.
Parameters:
- **statusCode** `number` - The HTTP status code.
- **headers** `object` - The headers received in the response.
- **socket** `object` - The socket object.
#### `onError(error)`
Called when an error occurs.
Parameters:
- **error** `Error` - The error that occurred.
#### `onHeaders(statusCode, headers, resume, statusText)`
Called when headers are received.
Parameters:
- **statusCode** `number` - The HTTP status code.
- **headers** `object` - The headers received in the response.
- **resume** `function` - The resume function.
- **statusText** `string` - The status text.
#### `onData(chunk)`
Called when data is received.
Parameters:
- **chunk** `Buffer` - The data chunk received.
#### `onComplete(trailers)`
Called when the request is complete.
Parameters:
- **trailers** `object` - The trailers received.
#### `onBodySent(chunk)`
Called when the request body is sent.
Parameters:
- **chunk** `Buffer` - The chunk of the request body sent.
================================================
FILE: docs/docs/api/RetryAgent.md
================================================
# Class: RetryAgent
Extends: `undici.Dispatcher`
A `undici.Dispatcher` that allows to automatically retry a request.
Wraps a `undici.RetryHandler`.
## `new RetryAgent(dispatcher, [options])`
Arguments:
* **dispatcher** `undici.Dispatcher` (required) - the dispatcher to wrap
* **options** `RetryHandlerOptions` (optional) - the options
Returns: `ProxyAgent`
### Parameter: `RetryHandlerOptions`
- **throwOnError** `boolean` (optional) - Disable to prevent throwing error on last retry attept, useful if you need the body on errors from server or if you have custom error handler. Default: `true`
- **retry** `(err: Error, context: RetryContext, callback: (err?: Error | null) => void) => void` (optional) - Function to be called after every retry. It should pass error if no more retries should be performed.
- **maxRetries** `number` (optional) - Maximum number of retries. Default: `5`
- **maxTimeout** `number` (optional) - Maximum number of milliseconds to wait before retrying. Default: `30000` (30 seconds)
- **minTimeout** `number` (optional) - Minimum number of milliseconds to wait before retrying. Default: `500` (half a second)
- **timeoutFactor** `number` (optional) - Factor to multiply the timeout by for each retry attempt. Default: `2`
- **retryAfter** `boolean` (optional) - It enables automatic retry after the `Retry-After` header is received. Default: `true`
-
- **methods** `string[]` (optional) - Array of HTTP methods to retry. Default: `['GET', 'PUT', 'HEAD', 'OPTIONS', 'DELETE']`
- **statusCodes** `number[]` (optional) - Array of HTTP status codes to retry. Default: `[429, 500, 502, 503, 504]`
- **errorCodes** `string[]` (optional) - Array of Error codes to retry. Default: `['ECONNRESET', 'ECONNREFUSED', 'ENOTFOUND', 'ENETDOWN','ENETUNREACH', 'EHOSTDOWN', 'UND_ERR_SOCKET']`
**`RetryContext`**
- `state`: `RetryState` - Current retry state. It can be mutated.
- `opts`: `Dispatch.DispatchOptions & RetryOptions` - Options passed to the retry handler.
Example:
```js
import { Agent, RetryAgent } from 'undici'
const agent = new RetryAgent(new Agent())
const res = await agent.request({
method: 'GET',
origin: 'http://example.com',
path: '/',
})
console.log(res.statusCode)
console.log(await res.body.text())
```
================================================
FILE: docs/docs/api/RetryHandler.md
================================================
# Class: RetryHandler
Extends: `undici.DispatcherHandlers`
A handler class that implements the retry logic for a request.
## `new RetryHandler(dispatchOptions, retryHandlers, [retryOptions])`
Arguments:
- **options** `Dispatch.DispatchOptions & RetryOptions` (required) - It is an intersection of `Dispatcher.DispatchOptions` and `RetryOptions`.
- **retryHandlers** `RetryHandlers` (required) - Object containing the `dispatch` to be used on every retry, and `handler` for handling the `dispatch` lifecycle.
Returns: `retryHandler`
### Parameter: `Dispatch.DispatchOptions & RetryOptions`
Extends: [`Dispatch.DispatchOptions`](/docs/docs/api/Dispatcher.md#parameter-dispatchoptions).
#### `RetryOptions`
- **throwOnError** `boolean` (optional) - Disable to prevent throwing error on last retry attept, useful if you need the body on errors from server or if you have custom error handler.
- **retry** `(err: Error, context: RetryContext, callback: (err?: Error | null) => void) => number | null` (optional) - Function to be called after every retry. It should pass error if no more retries should be performed.
- **maxRetries** `number` (optional) - Maximum number of retries. Default: `5`
- **maxTimeout** `number` (optional) - Maximum number of milliseconds to wait before retrying. Default: `30000` (30 seconds)
- **minTimeout** `number` (optional) - Minimum number of milliseconds to wait before retrying. Default: `500` (half a second)
- **timeoutFactor** `number` (optional) - Factor to multiply the timeout by for each retry attempt. Default: `2`
- **retryAfter** `boolean` (optional) - It enables automatic retry after the `Retry-After` header is received. Default: `true`
-
- **methods** `string[]` (optional) - Array of HTTP methods to retry. Default: `['GET', 'PUT', 'HEAD', 'OPTIONS', 'DELETE']`
- **statusCodes** `number[]` (optional) - Array of HTTP status codes to retry. Default: `[429, 500, 502, 503, 504]`
- **errorCodes** `string[]` (optional) - Array of Error codes to retry. Default: `['ECONNRESET', 'ECONNREFUSED', 'ENOTFOUND', 'ENETDOWN','ENETUNREACH', 'EHOSTDOWN', 'UND_ERR_SOCKET']`
**`RetryContext`**
- `state`: `RetryState` - Current retry state. It can be mutated.
- `opts`: `Dispatch.DispatchOptions & RetryOptions` - Options passed to the retry handler.
**`RetryState`**
It represents the retry state for a given request.
- `counter`: `number` - Current retry attempt.
### Parameter `RetryHandlers`
- **dispatch** `(options: Dispatch.DispatchOptions, handlers: Dispatch.DispatchHandler) => Promise` (required) - Dispatch function to be called after every retry.
- **handler** Extends [`Dispatch.DispatchHandler`](/docs/docs/api/Dispatcher.md#dispatcherdispatchoptions-handler) (required) - Handler function to be called after the request is successful or the retries are exhausted.
>__Note__: The `RetryHandler` does not retry over stateful bodies (e.g. streams, AsyncIterable) as those, once consumed, are left in a state that cannot be reutilized. For these situations the `RetryHandler` will identify
>the body as stateful and will not retry the request rejecting with the error `UND_ERR_REQ_RETRY`.
Examples:
```js
const client = new Client(`http://localhost:${server.address().port}`);
const chunks = [];
const handler = new RetryHandler(
{
...dispatchOptions,
retryOptions: {
// custom retry function
retry: function (err, state, callback) {
counter++;
if (err.code && err.code === "UND_ERR_DESTROYED") {
callback(err);
return;
}
if (err.statusCode === 206) {
callback(err);
return;
}
setTimeout(() => callback(null), 1000);
},
},
},
{
dispatch: (...args) => {
return client.dispatch(...args);
},
handler: {
onConnect() {},
onBodySent() {},
onHeaders(status, _rawHeaders, resume, _statusMessage) {
// do something with headers
},
onData(chunk) {
chunks.push(chunk);
return true;
},
onComplete() {},
onError() {
// handle error properly
},
},
}
);
```
#### Example - Basic RetryHandler with defaults
```js
const client = new Client(`http://localhost:${server.address().port}`);
const handler = new RetryHandler(dispatchOptions, {
dispatch: client.dispatch.bind(client),
handler: {
onConnect() {},
onBodySent() {},
onHeaders(status, _rawHeaders, resume, _statusMessage) {},
onData(chunk) {},
onComplete() {},
onError(err) {},
},
});
```
================================================
FILE: docs/docs/api/RoundRobinPool.md
================================================
# Class: RoundRobinPool
Extends: `undici.Dispatcher`
A pool of [Client](/docs/docs/api/Client.md) instances connected to the same upstream target with round-robin client selection.
Unlike [`Pool`](/docs/docs/api/Pool.md), which always selects the first available client, `RoundRobinPool` cycles through clients in a round-robin fashion. This ensures even distribution of requests across all connections, which is particularly useful when the upstream target is behind a load balancer that round-robins TCP connections across multiple backend servers (e.g., Kubernetes Services).
Requests are not guaranteed to be dispatched in order of invocation.
## `new RoundRobinPool(url[, options])`
Arguments:
* **url** `URL | string` - It should only include the **protocol, hostname, and port**.
* **options** `RoundRobinPoolOptions` (optional)
### Parameter: `RoundRobinPoolOptions`
Extends: [`ClientOptions`](/docs/docs/api/Client.md#parameter-clientoptions)
* **factory** `(origin: URL, opts: Object) => Dispatcher` - Default: `(origin, opts) => new Client(origin, opts)`
* **connections** `number | null` (optional) - Default: `null` - The number of `Client` instances to create. When set to `null`, the `RoundRobinPool` instance will create an unlimited amount of `Client` instances.
* **clientTtl** `number | null` (optional) - Default: `null` - The amount of time before a `Client` instance is removed from the `RoundRobinPool` and closed. When set to `null`, `Client` instances will not be removed or closed based on age.
## Use Case
`RoundRobinPool` is designed for scenarios where:
1. You connect to a single origin (e.g., `http://my-service.namespace.svc`)
2. That origin is backed by a load balancer distributing TCP connections across multiple servers
3. You want requests evenly distributed across all backend servers
**Example**: In Kubernetes, when using a Service DNS name with multiple Pod replicas, kube-proxy load balances TCP connections. `RoundRobinPool` ensures each connection (and thus each Pod) receives an equal share of requests.
### Important: Backend Distribution Considerations
`RoundRobinPool` distributes **HTTP requests** evenly across **TCP connections**. Whether this translates to even backend server distribution depends on the load balancer's behavior:
**✓ Works when the load balancer**:
- Assigns different backends to different TCP connections from the same client
- Uses algorithms like: round-robin, random, least-connections (without client affinity)
- Example: Default Kubernetes Services without `sessionAffinity`
**✗ Does NOT work when**:
- Load balancer has client/source IP affinity (all connections from one IP → same backend)
- Load balancer uses source-IP-hash or sticky sessions
**How it works:**
1. `RoundRobinPool` creates N TCP connections to the load balancer endpoint
2. Load balancer assigns each TCP connection to a backend (per its algorithm)
3. `RoundRobinPool` cycles HTTP requests across those N connections
4. Result: Requests distributed proportionally to how the LB distributed the connections
If the load balancer assigns all connections to the same backend (e.g., due to session affinity), `RoundRobinPool` cannot overcome this. In such cases, consider using [`BalancedPool`](/docs/docs/api/BalancedPool.md) with direct backend addresses (e.g., individual pod IPs) instead of a load-balanced endpoint.
## Instance Properties
### `RoundRobinPool.closed`
Implements [Client.closed](/docs/docs/api/Client.md#clientclosed)
### `RoundRobinPool.destroyed`
Implements [Client.destroyed](/docs/docs/api/Client.md#clientdestroyed)
### `RoundRobinPool.stats`
Returns [`PoolStats`](PoolStats.md) instance for this pool.
## Instance Methods
### `RoundRobinPool.close([callback])`
Implements [`Dispatcher.close([callback])`](/docs/docs/api/Dispatcher.md#dispatcherclosecallback-promise).
### `RoundRobinPool.destroy([error, callback])`
Implements [`Dispatcher.destroy([error, callback])`](/docs/docs/api/Dispatcher.md#dispatcherdestroyerror-callback-promise).
### `RoundRobinPool.connect(options[, callback])`
See [`Dispatcher.connect(options[, callback])`](/docs/docs/api/Dispatcher.md#dispatcherconnectoptions-callback).
### `RoundRobinPool.dispatch(options, handler)`
Implements [`Dispatcher.dispatch(options, handler)`](/docs/docs/api/Dispatcher.md#dispatcherdispatchoptions-handler).
### `RoundRobinPool.pipeline(options, handler)`
See [`Dispatcher.pipeline(options, handler)`](/docs/docs/api/Dispatcher.md#dispatcherpipelineoptions-handler).
### `RoundRobinPool.request(options[, callback])`
See [`Dispatcher.request(options [, callback])`](/docs/docs/api/Dispatcher.md#dispatcherrequestoptions-callback).
### `RoundRobinPool.stream(options, factory[, callback])`
See [`Dispatcher.stream(options, factory[, callback])`](/docs/docs/api/Dispatcher.md#dispatcherstreamoptions-factory-callback).
### `RoundRobinPool.upgrade(options[, callback])`
See [`Dispatcher.upgrade(options[, callback])`](/docs/docs/api/Dispatcher.md#dispatcherupgradeoptions-callback).
## Instance Events
### Event: `'connect'`
See [Dispatcher Event: `'connect'`](/docs/docs/api/Dispatcher.md#event-connect).
### Event: `'disconnect'`
See [Dispatcher Event: `'disconnect'`](/docs/docs/api/Dispatcher.md#event-disconnect).
### Event: `'drain'`
See [Dispatcher Event: `'drain'`](/docs/docs/api/Dispatcher.md#event-drain).
## Example
```javascript
import { RoundRobinPool } from 'undici'
const pool = new RoundRobinPool('http://my-service.default.svc.cluster.local', {
connections: 10
})
// Requests will be distributed evenly across all 10 connections
for (let i = 0; i < 100; i++) {
const { body } = await pool.request({
path: '/api/data',
method: 'GET'
})
console.log(await body.json())
}
await pool.close()
```
## See Also
- [Pool](/docs/docs/api/Pool.md) - Connection pool without round-robin
- [BalancedPool](/docs/docs/api/BalancedPool.md) - Load balancing across multiple origins
- [Issue #3648](https://github.com/nodejs/undici/issues/3648) - Original issue describing uneven distribution
================================================
FILE: docs/docs/api/SnapshotAgent.md
================================================
# SnapshotAgent
The `SnapshotAgent` provides a powerful way to record and replay HTTP requests for testing purposes. It extends `MockAgent` to enable automatic snapshot testing, eliminating the need to manually define mock responses.
## Use Cases
- **Integration Testing**: Record real API interactions and replay them in tests
- **Offline Development**: Work with APIs without network connectivity
- **Consistent Test Data**: Ensure tests use the same responses across runs
- **API Contract Testing**: Capture and validate API behavior over time
## Constructor
```javascript
new SnapshotAgent([options])
```
### Parameters
- **options** `Object` (optional)
- **mode** `String` - The snapshot mode: `'record'`, `'playback'`, or `'update'`. Default: `'record'`
- **snapshotPath** `String` - Path to the snapshot file for loading/saving
- **maxSnapshots** `Number` - Maximum number of snapshots to keep in memory. Default: `Infinity`
- **autoFlush** `Boolean` - Whether to automatically save snapshots to disk. Default: `false`
- **flushInterval** `Number` - Interval in milliseconds for auto-flush. Default: `30000`
- **matchHeaders** `Array` - Specific headers to include in request matching. Default: all headers
- **ignoreHeaders** `Array` - Headers to ignore during request matching
- **excludeHeaders** `Array` - Headers to exclude from snapshots (for security)
- **matchBody** `Boolean` - Whether to include request body in matching. Default: `true`
- **matchQuery** `Boolean` - Whether to include query parameters in matching. Default: `true`
- **caseSensitive** `Boolean` - Whether header matching is case-sensitive. Default: `false`
- **shouldRecord** `Function` - Callback to determine if a request should be recorded
- **shouldPlayback** `Function` - Callback to determine if a request should be played back
- **excludeUrls** `Array` - URL patterns (strings or RegExp) to exclude from recording/playback
- All other options from `MockAgent` are supported
### Modes
#### Record Mode (`'record'`)
Makes real HTTP requests and saves the responses to snapshots.
```javascript
import { SnapshotAgent, setGlobalDispatcher } from 'undici'
const agent = new SnapshotAgent({
mode: 'record',
snapshotPath: './test/snapshots/api-calls.json'
})
setGlobalDispatcher(agent)
// Makes real requests and records them
const response = await fetch('https://api.example.com/users')
const users = await response.json()
// Save recorded snapshots
await agent.saveSnapshots()
```
#### Playback Mode (`'playback'`)
Replays recorded responses without making real HTTP requests.
```javascript
import { SnapshotAgent, setGlobalDispatcher } from 'undici'
const agent = new SnapshotAgent({
mode: 'playback',
snapshotPath: './test/snapshots/api-calls.json'
})
setGlobalDispatcher(agent)
// Uses recorded response instead of real request
const response = await fetch('https://api.example.com/users')
```
#### Update Mode (`'update'`)
Uses existing snapshots when available, but records new ones for missing requests.
```javascript
import { SnapshotAgent, setGlobalDispatcher } from 'undici'
const agent = new SnapshotAgent({
mode: 'update',
snapshotPath: './test/snapshots/api-calls.json'
})
setGlobalDispatcher(agent)
// Uses snapshot if exists, otherwise makes real request and records it
const response = await fetch('https://api.example.com/new-endpoint')
```
## Instance Methods
### `agent.saveSnapshots([filePath])`
Saves all recorded snapshots to a file.
#### Parameters
- **filePath** `String` (optional) - Path to save snapshots. Uses constructor `snapshotPath` if not provided.
#### Returns
`Promise`
```javascript
await agent.saveSnapshots('./custom-snapshots.json')
```
## Advanced Configuration
### Header Filtering
Control which headers are used for request matching and what gets stored in snapshots:
```javascript
const agent = new SnapshotAgent({
mode: 'record',
snapshotPath: './snapshots.json',
// Only match these specific headers
matchHeaders: ['content-type', 'accept'],
// Ignore these headers during matching (but still store them)
ignoreHeaders: ['user-agent', 'date'],
// Exclude sensitive headers from snapshots entirely
excludeHeaders: ['authorization', 'x-api-key', 'cookie']
})
```
### Custom Request/Response Filtering
Use callback functions to determine what gets recorded or played back:
```javascript
const agent = new SnapshotAgent({
mode: 'record',
snapshotPath: './snapshots.json',
// Only record GET requests to specific endpoints
shouldRecord: (requestOpts) => {
const url = new URL(requestOpts.path, requestOpts.origin)
return requestOpts.method === 'GET' && url.pathname.startsWith('/api/v1/')
},
// Skip authentication endpoints during playback
shouldPlayback: (requestOpts) => {
const url = new URL(requestOpts.path, requestOpts.origin)
return !url.pathname.includes('/auth/')
}
})
```
### URL Pattern Exclusion
Exclude specific URLs from recording/playback using patterns:
```javascript
const agent = new SnapshotAgent({
mode: 'record',
snapshotPath: './snapshots.json',
excludeUrls: [
'https://analytics.example.com', // String match
/\/api\/v\d+\/health/, // Regex pattern
'telemetry' // Substring match
]
})
```
### Memory Management
Configure automatic memory and disk management:
```javascript
const agent = new SnapshotAgent({
mode: 'record',
snapshotPath: './snapshots.json',
// Keep only 1000 snapshots in memory
maxSnapshots: 1000,
// Automatically save to disk every 30 seconds
autoFlush: true,
flushInterval: 30000
})
```
### Sequential Response Handling
Handle multiple responses for the same request (similar to nock):
```javascript
// In record mode, multiple identical requests get recorded as separate responses
const agent = new SnapshotAgent({ mode: 'record', snapshotPath: './sequential.json' })
// First call returns response A
await fetch('https://api.example.com/random')
// Second call returns response B
await fetch('https://api.example.com/random')
await agent.saveSnapshots()
// In playback mode, calls return responses in sequence
const playbackAgent = new SnapshotAgent({ mode: 'playback', snapshotPath: './sequential.json' })
// Returns response A
const first = await fetch('https://api.example.com/random')
// Returns response B
const second = await fetch('https://api.example.com/random')
// Third call repeats the last response (B)
const third = await fetch('https://api.example.com/random')
```
## Managing Snapshots
### Replacing Existing Snapshots
```javascript
// Load existing snapshots
await agent.loadSnapshots('./old-snapshots.json')
// Get snapshot data
const recorder = agent.getRecorder()
const snapshots = recorder.getSnapshots()
// Modify or filter snapshots
const filteredSnapshots = snapshots.filter(s =>
!s.request.url.includes('deprecated')
)
// Replace all snapshots
agent.replaceSnapshots(filteredSnapshots.map((snapshot, index) => ({
hash: `new-hash-${index}`,
snapshot
})))
// Save updated snapshots
await agent.saveSnapshots('./updated-snapshots.json')
```
### `agent.loadSnapshots([filePath])`
Loads snapshots from a file.
#### Parameters
- **filePath** `String` (optional) - Path to load snapshots from. Uses constructor `snapshotPath` if not provided.
#### Returns
`Promise`
```javascript
await agent.loadSnapshots('./existing-snapshots.json')
```
### `agent.getRecorder()`
Gets the underlying `SnapshotRecorder` instance.
#### Returns
`SnapshotRecorder`
```javascript
const recorder = agent.getRecorder()
console.log(`Recorded ${recorder.size()} interactions`)
```
### `agent.getMode()`
Gets the current snapshot mode.
#### Returns
`String` - The current mode (`'record'`, `'playback'`, or `'update'`)
### `agent.clearSnapshots()`
Clears all recorded snapshots from memory.
```javascript
agent.clearSnapshots()
```
## Working with Different Request Types
### GET Requests
```javascript
// Record mode
const agent = new SnapshotAgent({ mode: 'record', snapshotPath: './get-snapshots.json' })
setGlobalDispatcher(agent)
const response = await fetch('https://jsonplaceholder.typicode.com/posts/1')
const post = await response.json()
await agent.saveSnapshots()
```
### POST Requests with Body
```javascript
// Record mode
const agent = new SnapshotAgent({ mode: 'record', snapshotPath: './post-snapshots.json' })
setGlobalDispatcher(agent)
const response = await fetch('https://jsonplaceholder.typicode.com/posts', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title: 'Test Post', body: 'Content' })
})
await agent.saveSnapshots()
```
### Using with `undici.request`
SnapshotAgent works with all undici APIs, not just fetch:
```javascript
import { SnapshotAgent, request, setGlobalDispatcher } from 'undici'
const agent = new SnapshotAgent({ mode: 'record', snapshotPath: './request-snapshots.json' })
setGlobalDispatcher(agent)
const { statusCode, headers, body } = await request('https://api.example.com/data')
const data = await body.json()
await agent.saveSnapshots()
```
## Test Integration
### Basic Test Setup
```javascript
import { test } from 'node:test'
import { SnapshotAgent, setGlobalDispatcher, getGlobalDispatcher } from 'undici'
test('API integration test', async (t) => {
const originalDispatcher = getGlobalDispatcher()
const agent = new SnapshotAgent({
mode: 'playback',
snapshotPath: './test/snapshots/api-test.json'
})
setGlobalDispatcher(agent)
t.after(() => setGlobalDispatcher(originalDispatcher))
// This will use recorded data
const response = await fetch('https://api.example.com/users')
const users = await response.json()
assert(Array.isArray(users))
assert(users.length > 0)
})
```
### Environment-Based Mode Selection
```javascript
const mode = process.env.SNAPSHOT_MODE || 'playback'
const agent = new SnapshotAgent({
mode,
snapshotPath: './test/snapshots/integration.json'
})
// Run with: SNAPSHOT_MODE=record npm test (to record)
// Run with: npm test (to playback)
```
### Test Helper Function
```javascript
function createSnapshotAgent(testName, mode = 'playback') {
return new SnapshotAgent({
mode,
snapshotPath: `./test/snapshots/${testName}.json`
})
}
test('user API test', async (t) => {
const agent = createSnapshotAgent('user-api')
setGlobalDispatcher(agent)
// Test implementation...
})
```
## Snapshot File Format
Snapshots are stored as JSON with the following structure:
```json
[
{
"hash": "dGVzdC1oYXNo...",
"snapshot": {
"request": {
"method": "GET",
"url": "https://api.example.com/users",
"headers": {
"authorization": "Bearer token"
},
"body": undefined
},
"response": {
"statusCode": 200,
"headers": {
"content-type": "application/json"
},
"body": "eyJkYXRhIjoidGVzdCJ9", // base64 encoded
"trailers": {}
},
"timestamp": "2024-01-01T00:00:00.000Z"
}
}
]
```
## Security Considerations
### Sensitive Data in Snapshots
By default, SnapshotAgent records all headers and request/response data. For production use, always exclude sensitive information:
```javascript
const agent = new SnapshotAgent({
mode: 'record',
snapshotPath: './snapshots.json',
// Exclude sensitive headers from snapshots
excludeHeaders: [
'authorization',
'x-api-key',
'cookie',
'set-cookie',
'x-auth-token',
'x-csrf-token'
],
// Filter out requests with sensitive data
shouldRecord: (requestOpts) => {
const url = new URL(requestOpts.path, requestOpts.origin)
// Don't record authentication endpoints
if (url.pathname.includes('/auth/') || url.pathname.includes('/login')) {
return false
}
// Don't record if request contains sensitive body data
if (requestOpts.body && typeof requestOpts.body === 'string') {
const body = requestOpts.body.toLowerCase()
if (body.includes('password') || body.includes('secret')) {
return false
}
}
return true
}
})
```
### Snapshot File Security
**Important**: Snapshot files may contain sensitive data. Handle them securely:
- ✅ Add snapshot files to `.gitignore` if they contain real API data
- ✅ Use environment-specific snapshots (dev/staging/prod)
- ✅ Regularly review snapshot contents for sensitive information
- ✅ Use the `excludeHeaders` option for production snapshots
- ❌ Never commit snapshots with real authentication tokens
- ❌ Don't share snapshot files containing personal data
```gitignore
# Exclude snapshots with real data
/test/snapshots/production-*.json
/test/snapshots/*-real-data.json
# Include sanitized test snapshots
!/test/snapshots/mock-*.json
```
## Error Handling
### Missing Snapshots in Playback Mode
```javascript
try {
const response = await fetch('https://api.example.com/nonexistent')
} catch (error) {
if (error.message.includes('No snapshot found')) {
// Handle missing snapshot
console.log('Snapshot not found for this request')
}
}
```
### Handling Network Errors in Record Mode
```javascript
const agent = new SnapshotAgent({ mode: 'record', snapshotPath: './snapshots.json' })
try {
const response = await fetch('https://nonexistent-api.example.com/data')
} catch (error) {
// Network errors are not recorded as snapshots
console.log('Network error:', error.message)
}
```
## Best Practices
### 1. Organize Snapshots by Test Suite
```javascript
// Use descriptive snapshot file names
const agent = new SnapshotAgent({
mode: 'playback',
snapshotPath: `./test/snapshots/${testSuiteName}-${testName}.json`
})
```
### 2. Version Control Snapshots
Add snapshot files to version control to ensure consistent test behavior across environments:
```gitignore
# Include snapshots in version control
!/test/snapshots/*.json
```
### 3. Clean Up Test Data
```javascript
test('API test', async (t) => {
const agent = new SnapshotAgent({
mode: 'playback',
snapshotPath: './test/snapshots/temp-test.json'
})
// Clean up after test
t.after(() => {
agent.clearSnapshots()
})
})
```
### 4. Snapshot Validation
```javascript
test('validate snapshot contents', async (t) => {
const agent = new SnapshotAgent({
mode: 'playback',
snapshotPath: './test/snapshots/validation.json'
})
const recorder = agent.getRecorder()
const snapshots = recorder.getSnapshots()
// Validate snapshot structure
assert(snapshots.length > 0, 'Should have recorded snapshots')
assert(snapshots[0].request.url.startsWith('https://'), 'Should use HTTPS')
})
```
## Comparison with Other Tools
### vs Manual MockAgent Setup
**Manual MockAgent:**
```javascript
const mockAgent = new MockAgent()
const mockPool = mockAgent.get('https://api.example.com')
mockPool.intercept({
path: '/users',
method: 'GET'
}).reply(200, [
{ id: 1, name: 'User 1' },
{ id: 2, name: 'User 2' }
])
```
**SnapshotAgent:**
```javascript
// Record once
const agent = new SnapshotAgent({ mode: 'record', snapshotPath: './snapshots.json' })
// Real API call gets recorded automatically
// Use in tests
const agent = new SnapshotAgent({ mode: 'playback', snapshotPath: './snapshots.json' })
// Automatically replays recorded response
```
### vs nock
SnapshotAgent provides similar functionality to nock but is specifically designed for undici:
- ✅ Works with all undici APIs (`request`, `stream`, `pipeline`, etc.)
- ✅ Supports undici-specific features (RetryAgent, connection pooling)
- ✅ Better TypeScript integration
- ✅ More efficient for high-performance scenarios
## See Also
- [MockAgent](./MockAgent.md) - Manual mocking for more control
- [MockCallHistory](./MockCallHistory.md) - Inspecting request history
- [Testing Best Practices](../best-practices/writing-tests.md) - General testing guidance
================================================
FILE: docs/docs/api/Socks5ProxyAgent.md
================================================
# Class: Socks5ProxyAgent
Extends: `undici.Dispatcher`
A SOCKS5 proxy wrapper class that implements the Dispatcher API. It enables HTTP requests to be routed through a SOCKS5 proxy server, providing connection tunneling and authentication support.
## `new Socks5ProxyAgent(proxyUrl[, options])`
Arguments:
* **proxyUrl** `string | URL` (required) - The SOCKS5 proxy server URL. Must use `socks5://` or `socks://` protocol.
* **options** `Socks5ProxyAgent.Options` (optional) - Additional configuration options.
Returns: `Socks5ProxyAgent`
### Parameter: `Socks5ProxyAgent.Options`
Extends: [`PoolOptions`](/docs/docs/api/Pool.md#parameter-pooloptions)
* **headers** `IncomingHttpHeaders` (optional) - Additional headers to send with proxy connections.
* **username** `string` (optional) - SOCKS5 proxy username for authentication. Can also be provided in the proxy URL.
* **password** `string` (optional) - SOCKS5 proxy password for authentication. Can also be provided in the proxy URL.
* **connect** `Function` (optional) - Custom connector function for the proxy connection.
* **proxyTls** `BuildOptions` (optional) - TLS options for the proxy connection (when using SOCKS5 over TLS).
Examples:
```js
import { Socks5ProxyAgent } from 'undici'
const socks5Proxy = new Socks5ProxyAgent('socks5://localhost:1080')
// or with authentication
const socks5ProxyWithAuth = new Socks5ProxyAgent('socks5://user:pass@localhost:1080')
// or with options
const socks5ProxyWithOptions = new Socks5ProxyAgent('socks5://localhost:1080', {
username: 'user',
password: 'pass',
connections: 10
})
```
#### Example - Basic SOCKS5 Proxy instantiation
This will instantiate the Socks5ProxyAgent. It will not do anything until registered as the dispatcher to use with requests.
```js
import { Socks5ProxyAgent } from 'undici'
const socks5Proxy = new Socks5ProxyAgent('socks5://localhost:1080')
```
#### Example - Basic SOCKS5 Proxy Request with global dispatcher
```js
import { setGlobalDispatcher, request, Socks5ProxyAgent } from 'undici'
const socks5Proxy = new Socks5ProxyAgent('socks5://localhost:1080')
setGlobalDispatcher(socks5Proxy)
const { statusCode, body } = await request('http://localhost:3000/foo')
console.log('response received', statusCode) // response received 200
for await (const data of body) {
console.log('data', data.toString('utf8')) // data foo
}
```
#### Example - Basic SOCKS5 Proxy Request with local dispatcher
```js
import { Socks5ProxyAgent, request } from 'undici'
const socks5Proxy = new Socks5ProxyAgent('socks5://localhost:1080')
const {
statusCode,
body
} = await request('http://localhost:3000/foo', { dispatcher: socks5Proxy })
console.log('response received', statusCode) // response received 200
for await (const data of body) {
console.log('data', data.toString('utf8')) // data foo
}
```
#### Example - SOCKS5 Proxy Request with authentication
```js
import { setGlobalDispatcher, request, Socks5ProxyAgent } from 'undici'
// Authentication via URL
const socks5Proxy = new Socks5ProxyAgent('socks5://username:password@localhost:1080')
// Or authentication via options
// const socks5Proxy = new Socks5ProxyAgent('socks5://localhost:1080', {
// username: 'username',
// password: 'password'
// })
setGlobalDispatcher(socks5Proxy)
const { statusCode, body } = await request('http://localhost:3000/foo')
console.log('response received', statusCode) // response received 200
for await (const data of body) {
console.log('data', data.toString('utf8')) // data foo
}
```
#### Example - SOCKS5 Proxy with HTTPS requests
SOCKS5 proxy supports both HTTP and HTTPS requests through tunneling:
```js
import { Socks5ProxyAgent, request } from 'undici'
const socks5Proxy = new Socks5ProxyAgent('socks5://localhost:1080')
const response = await request('https://api.example.com/data', {
dispatcher: socks5Proxy,
method: 'GET'
})
console.log('Response status:', response.statusCode)
console.log('Response data:', await response.body.json())
```
#### Example - SOCKS5 Proxy with Fetch
```js
import { Socks5ProxyAgent, fetch } from 'undici'
const socks5Proxy = new Socks5ProxyAgent('socks5://localhost:1080')
const response = await fetch('http://localhost:3000/api/users', {
dispatcher: socks5Proxy,
method: 'GET'
})
console.log('Response status:', response.status)
console.log('Response data:', await response.text())
```
#### Example - Connection Pooling
SOCKS5ProxyWrapper automatically manages connection pooling for better performance:
```js
import { Socks5ProxyAgent, request } from 'undici'
const socks5Proxy = new Socks5ProxyAgent('socks5://localhost:1080', {
connections: 10, // Allow up to 10 concurrent connections
pipelining: 1 // Enable HTTP/1.1 pipelining
})
// Multiple requests will reuse connections through the SOCKS5 tunnel
const responses = await Promise.all([
request('http://api.example.com/endpoint1', { dispatcher: socks5Proxy }),
request('http://api.example.com/endpoint2', { dispatcher: socks5Proxy }),
request('http://api.example.com/endpoint3', { dispatcher: socks5Proxy })
])
console.log('All requests completed through the same SOCKS5 proxy')
```
### `Socks5ProxyAgent.close()`
Closes the SOCKS5 proxy wrapper and waits for all underlying pools and connections to close before resolving.
Returns: `Promise`
#### Example - clean up after tests are complete
```js
import { Socks5ProxyAgent, setGlobalDispatcher } from 'undici'
const socks5Proxy = new Socks5ProxyAgent('socks5://localhost:1080')
setGlobalDispatcher(socks5Proxy)
// ... make requests
await socks5Proxy.close()
```
### `Socks5ProxyAgent.destroy([err])`
Destroys the SOCKS5 proxy wrapper and all underlying connections immediately.
Arguments:
* **err** `Error` (optional) - The error that caused the destruction.
Returns: `Promise`
#### Example - force close all connections
```js
import { Socks5ProxyAgent } from 'undici'
const socks5Proxy = new Socks5ProxyAgent('socks5://localhost:1080')
// Force close all connections
await socks5Proxy.destroy()
```
### `Socks5ProxyAgent.dispatch(options, handlers)`
Implements [`Dispatcher.dispatch(options, handlers)`](/docs/docs/api/Dispatcher.md#dispatcherdispatchoptions-handlers).
### `Socks5ProxyAgent.request(options[, callback])`
See [`Dispatcher.request(options [, callback])`](/docs/docs/api/Dispatcher.md#dispatcherrequestoptions-callback).
## Debugging
SOCKS5 proxy connections can be debugged using Node.js diagnostics:
```sh
NODE_DEBUG=undici:socks5 node script.js
```
This will output detailed information about the SOCKS5 handshake, authentication, and connection establishment.
## SOCKS5 Protocol Support
The Socks5ProxyAgent supports the following SOCKS5 features:
### Authentication Methods
- **No Authentication** (`0x00`) - For public or internal proxies
- **Username/Password** (`0x02`) - RFC 1929 authentication
### Address Types
- **IPv4** (`0x01`) - Standard IPv4 addresses
- **Domain Name** (`0x03`) - Domain names (recommended for flexibility)
- **IPv6** (`0x04`) - IPv6 addresses (full support for standard and compressed notation)
### Commands
- **CONNECT** (`0x01`) - Establish TCP connection (primary use case for HTTP)
### Error Handling
The wrapper handles various SOCKS5 error conditions:
- Connection refused by proxy
- Authentication failures
- Network unreachable
- Host unreachable
- Unsupported address types or commands
## Performance Considerations
- **Connection Pooling**: Automatically pools connections through the SOCKS5 tunnel for better performance
- **HTTP/1.1 Pipelining**: Supports pipelining when enabled
- **DNS Resolution**: Domain names are resolved by the SOCKS5 proxy, reducing local DNS queries
- **TLS Termination**: HTTPS connections are encrypted end-to-end, with the SOCKS5 proxy only handling the TCP tunnel
## Security Notes
1. **Authentication**: Credentials are sent to the SOCKS5 proxy in plaintext unless using SOCKS5 over TLS
2. **DNS Leaks**: All DNS resolution happens on the proxy server, preventing DNS leaks
3. **End-to-end Encryption**: HTTPS traffic remains encrypted between client and final destination
4. **Connection Security**: Consider using authenticated proxies and secure networks
## Compatibility
- **Protocol**: SOCKS5 (RFC 1928) with Username/Password Authentication (RFC 1929)
- **Transport**: TCP only (UDP support not implemented)
- **Node.js**: Compatible with all supported Node.js versions
- **HTTP Versions**: Works with HTTP/1.1 and HTTP/2 over the tunnel
================================================
FILE: docs/docs/api/Util.md
================================================
# Util
Utility API for third-party implementations of the dispatcher API.
## `parseHeaders(headers, [obj])`
Receives a header object and returns the parsed value.
Arguments:
- **headers** `(Buffer | string | (Buffer | string)[])[]` (required) - Header object.
- **obj** `Record` (optional) - Object to specify a proxy object. The parsed value is assigned to this object. But, if **headers** is an object, it is not used.
Returns: `Record` If **obj** is specified, it is equivalent to **obj**.
## `headerNameToString(value)`
Retrieves a header name and returns its lowercase value.
Arguments:
- **value** `string | Buffer` (required) - Header name.
Returns: `string`
================================================
FILE: docs/docs/api/WebSocket.md
================================================
# Class: WebSocket
Extends: [`EventTarget`](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget)
The WebSocket object provides a way to manage a WebSocket connection to a server, allowing bidirectional communication. The API follows the [WebSocket spec](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket) and [RFC 6455](https://datatracker.ietf.org/doc/html/rfc6455).
## `new WebSocket(url[, protocol])`
Arguments:
* **url** `URL | string`
* **protocol** `string | string[] | WebSocketInit` (optional) - Subprotocol(s) to request the server use, or a [`Dispatcher`](/docs/docs/api/Dispatcher.md).
### WebSocketInit
When passing an object as the second argument, the following options are available:
* **protocols** `string | string[]` (optional) - Subprotocol(s) to request the server use.
* **dispatcher** `Dispatcher` (optional) - A custom [`Dispatcher`](/docs/docs/api/Dispatcher.md) to use for the connection.
* **headers** `HeadersInit` (optional) - Custom headers to include in the WebSocket handshake request.
### Example:
This example will not work in browsers or other platforms that don't allow passing an object.
```js
import { WebSocket, ProxyAgent } from 'undici'
const proxyAgent = new ProxyAgent('my.proxy.server')
const ws = new WebSocket('wss://echo.websocket.events', {
dispatcher: proxyAgent,
protocols: ['echo', 'chat']
})
```
If you do not need a custom Dispatcher, it's recommended to use the following pattern:
```js
import { WebSocket } from 'undici'
const ws = new WebSocket('wss://echo.websocket.events', ['echo', 'chat'])
```
### Example with HTTP/2:
> ⚠️ Warning: WebSocket over HTTP/2 is experimental, it is likely to change in the future.
> 🗒️ Note: WebSocket over HTTP/2 may be enabled by default in a future version,
> this will happen by enabling HTTP/2 connections as the default behavior of Undici's Agent as well the global dispatcher.
> Stay tuned to the changelog for more information.
This example will not work in browsers or other platforms that don't allow passing an object.
```js
import { Agent } from 'undici'
const agent = new Agent({ allowH2: true })
const ws = new WebSocket('wss://echo.websocket.events', {
dispatcher: agent,
protocols: ['echo', 'chat']
})
```
# Class: WebSocketStream
> ⚠️ Warning: the WebSocketStream API has not been finalized and is likely to change.
See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/WebSocketStream) for more information.
## `new WebSocketStream(url[, protocol])`
Arguments:
* **url** `URL | string`
* **options** `WebSocketStreamOptions` (optional)
### WebSocketStream Example
```js
const stream = new WebSocketStream('https://echo.websocket.org/')
const { readable, writable } = await stream.opened
async function read () {
/** @type {ReadableStreamReader} */
const reader = readable.getReader()
while (true) {
const { done, value } = await reader.read()
if (done) break
// do something with value
}
}
async function write () {
/** @type {WritableStreamDefaultWriter} */
const writer = writable.getWriter()
writer.write('Hello, world!')
writer.releaseLock()
}
read()
setInterval(() => write(), 5000)
```
## ping(websocket, payload)
Arguments:
* **websocket** `WebSocket` - The WebSocket instance to send the ping frame on
* **payload** `Buffer|undefined` (optional) - Optional payload data to include with the ping frame. Must not exceed 125 bytes.
Sends a ping frame to the WebSocket server. The server must respond with a pong frame containing the same payload data. This can be used for keepalive purposes or to verify that the connection is still active.
### Example:
```js
import { WebSocket, ping } from 'undici'
const ws = new WebSocket('wss://echo.websocket.events')
ws.addEventListener('open', () => {
// Send ping with no payload
ping(ws)
// Send ping with payload
const payload = Buffer.from('hello')
ping(ws, payload)
})
```
**Note**: A ping frame cannot have a payload larger than 125 bytes. The ping will only be sent if the WebSocket connection is in the OPEN state.
## Read More
- [MDN - WebSocket](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket)
- [The WebSocket Specification](https://www.rfc-editor.org/rfc/rfc6455)
- [The WHATWG WebSocket Specification](https://websockets.spec.whatwg.org/)
================================================
FILE: docs/docs/api/api-lifecycle.md
================================================
# Client Lifecycle
An Undici [Client](/docs/docs/api/Client.md) can be best described as a state machine. The following list is a summary of the various state transitions the `Client` will go through in its lifecycle. This document also contains detailed breakdowns of each state.
> This diagram is not a perfect representation of the undici Client. Since the Client class is not actually implemented as a state-machine, actual execution may deviate slightly from what is described below. Consider this as a general resource for understanding the inner workings of the Undici client rather than some kind of formal specification.
## State Transition Overview
* A `Client` begins in the **idle** state with no socket connection and no requests in queue.
* The *connect* event transitions the `Client` to the **pending** state where requests can be queued prior to processing.
* The *close* and *destroy* events transition the `Client` to the **destroyed** state. Since there are no requests in the queue, the *close* event immediately transitions to the **destroyed** state.
* The **pending** state indicates the underlying socket connection has been successfully established and requests are queueing.
* The *process* event transitions the `Client` to the **processing** state where requests are processed.
* If requests are queued, the *close* event transitions to the **processing** state; otherwise, it transitions to the **destroyed** state.
* The *destroy* event transitions to the **destroyed** state.
* The **processing** state initializes to the **processing.running** state.
* If the current request requires draining, the *needDrain* event transitions the `Client` into the **processing.busy** state which will return to the **processing.running** state with the *drainComplete* event.
* After all queued requests are completed, the *keepalive* event transitions the `Client` back to the **pending** state. If no requests are queued during the timeout, the **close** event transitions the `Client` to the **destroyed** state.
* If the *close* event is fired while the `Client` still has queued requests, the `Client` transitions to the **process.closing** state where it will complete all existing requests before firing the *done* event.
* The *done* event gracefully transitions the `Client` to the **destroyed** state.
* At any point in time, the *destroy* event will transition the `Client` from the **processing** state to the **destroyed** state, destroying any queued requests.
* The **destroyed** state is a final state and the `Client` is no longer functional.
A state diagram representing an Undici Client instance:
```mermaid
stateDiagram-v2
[*] --> idle
idle --> pending : connect
idle --> destroyed : destroy/close
pending --> idle : timeout
pending --> destroyed : destroy
state close_fork <>
pending --> close_fork : close
close_fork --> processing
close_fork --> destroyed
pending --> processing : process
processing --> pending : keepalive
processing --> destroyed : done
processing --> destroyed : destroy
destroyed --> [*]
state processing {
[*] --> running
running --> closing : close
running --> busy : needDrain
busy --> running : drainComplete
running --> [*] : keepalive
closing --> [*] : done
}
```
## State details
### idle
The **idle** state is the initial state of a `Client` instance. While an `origin` is required for instantiating a `Client` instance, the underlying socket connection will not be established until a request is queued using [`Client.dispatch()`](/docs/docs/api/Client.md#clientdispatchoptions-handlers). By calling `Client.dispatch()` directly or using one of the multiple implementations ([`Client.connect()`](Client.md#clientconnectoptions-callback), [`Client.pipeline()`](Client.md#clientpipelineoptions-handler), [`Client.request()`](Client.md#clientrequestoptions-callback), [`Client.stream()`](Client.md#clientstreamoptions-factory-callback), and [`Client.upgrade()`](/docs/docs/api/Client.md#clientupgradeoptions-callback)), the `Client` instance will transition from **idle** to [**pending**](/docs/docs/api/Client.md#pending) and then most likely directly to [**processing**](/docs/docs/api/Client.md#processing).
Calling [`Client.close()`](/docs/docs/api/Client.md#clientclosecallback) or [`Client.destroy()`](Client.md#clientdestroyerror-callback) transitions directly to the [**destroyed**](/docs/docs/api/Client.md#destroyed) state since the `Client` instance will have no queued requests in this state.
### pending
The **pending** state signifies a non-processing `Client`. Upon entering this state, the `Client` establishes a socket connection and emits the [`'connect'`](/docs/docs/api/Client.md#event-connect) event signalling a connection was successfully established with the `origin` provided during `Client` instantiation. The internal queue is initially empty, and requests can start queueing.
Calling [`Client.close()`](/docs/docs/api/Client.md#clientclosecallback) with queued requests, transitions the `Client` to the [**processing**](/docs/docs/api/Client.md#processing) state. Without queued requests, it transitions to the [**destroyed**](/docs/docs/api/Client.md#destroyed) state.
Calling [`Client.destroy()`](/docs/docs/api/Client.md#clientdestroyerror-callback) transitions directly to the [**destroyed**](/docs/docs/api/Client.md#destroyed) state regardless of existing requests.
### processing
The **processing** state is a state machine within itself. It initializes to the [**processing.running**](/docs/docs/api/Client.md#running) state. The [`Client.dispatch()`](/docs/docs/api/Client.md#clientdispatchoptions-handlers), [`Client.close()`](Client.md#clientclosecallback), and [`Client.destroy()`](Client.md#clientdestroyerror-callback) can be called at any time while the `Client` is in this state. `Client.dispatch()` will add more requests to the queue while existing requests continue to be processed. `Client.close()` will transition to the [**processing.closing**](/docs/docs/api/Client.md#closing) state. And `Client.destroy()` will transition to [**destroyed**](/docs/docs/api/Client.md#destroyed).
#### running
In the **processing.running** sub-state, queued requests are being processed in a FIFO order. If a request body requires draining, the *needDrain* event transitions to the [**processing.busy**](/docs/docs/api/Client.md#busy) sub-state. The *close* event transitions the Client to the [**process.closing**](/docs/docs/api/Client.md#closing) sub-state. If all queued requests are processed and neither [`Client.close()`](/docs/docs/api/Client.md#clientclosecallback) nor [`Client.destroy()`](Client.md#clientdestroyerror-callback) are called, then the [**processing**](/docs/docs/api/Client.md#processing) machine will trigger a *keepalive* event transitioning the `Client` back to the [**pending**](/docs/docs/api/Client.md#pending) state. During this time, the `Client` is waiting for the socket connection to timeout, and once it does, it triggers the *timeout* event and transitions to the [**idle**](/docs/docs/api/Client.md#idle) state.
#### busy
This sub-state is only entered when a request body is an instance of [Stream](https://nodejs.org/api/stream.html) and requires draining. The `Client` cannot process additional requests while in this state and must wait until the currently processing request body is completely drained before transitioning back to [**processing.running**](/docs/docs/api/Client.md#running).
#### closing
This sub-state is only entered when a `Client` instance has queued requests and the [`Client.close()`](/docs/docs/api/Client.md#clientclosecallback) method is called. In this state, the `Client` instance continues to process requests as usual, with the one exception that no additional requests can be queued. Once all of the queued requests are processed, the `Client` will trigger the *done* event gracefully entering the [**destroyed**](/docs/docs/api/Client.md#destroyed) state without an error.
### destroyed
The **destroyed** state is a final state for the `Client` instance. Once in this state, a `Client` is nonfunctional. Calling any other `Client` methods will result in an `ClientDestroyedError`.
================================================
FILE: docs/docs/best-practices/client-certificate.md
================================================
# Client certificate
Client certificate authentication can be configured with the `Client`, the required options are passed along through the `connect` option.
The client certificates must be signed by a trusted CA. The Node.js default is to trust the well-known CAs curated by Mozilla.
Setting the server option `requestCert: true` tells the server to request the client certificate.
The server option `rejectUnauthorized: false` allows us to handle any invalid certificate errors in client code. The `authorized` property on the socket of the incoming request will show if the client certificate was valid. The `authorizationError` property will give the reason if the certificate was not valid.
### Client Certificate Authentication
```js
const { readFileSync } = require('node:fs')
const { join } = require('node:path')
const { createServer } = require('node:https')
const { Client } = require('undici')
const serverOptions = {
ca: [
readFileSync(join(__dirname, 'client-ca-crt.pem'), 'utf8')
],
key: readFileSync(join(__dirname, 'server-key.pem'), 'utf8'),
cert: readFileSync(join(__dirname, 'server-crt.pem'), 'utf8'),
requestCert: true,
rejectUnauthorized: false
}
const server = createServer(serverOptions, (req, res) => {
// true if client cert is valid
if(req.client.authorized === true) {
console.log('valid')
} else {
console.error(req.client.authorizationError)
}
res.end()
})
server.listen(0, function () {
const tls = {
ca: [
readFileSync(join(__dirname, 'server-ca-crt.pem'), 'utf8')
],
key: readFileSync(join(__dirname, 'client-key.pem'), 'utf8'),
cert: readFileSync(join(__dirname, 'client-crt.pem'), 'utf8'),
rejectUnauthorized: false,
servername: 'agent1'
}
const client = new Client(`https://localhost:${server.address().port}`, {
connect: tls
})
client.request({
path: '/',
method: 'GET'
}, (err, { body }) => {
body.on('data', (buf) => {})
body.on('end', () => {
client.close()
server.close()
})
})
})
```
================================================
FILE: docs/docs/best-practices/crawling.md
================================================
# Crawling
[RFC 9309](https://datatracker.ietf.org/doc/html/rfc9309) defines crawlers as automated clients.
Some web servers may reject requests that omit the `User-Agent` header or that use common defaults such as `'curl/7.79.1'`.
In **undici**, the default user agent is `'undici'`. Since undici is integrated into Node.js core as the implementation of `fetch()`, requests made via `fetch()` use `'node'` as the default user agent.
It is recommended to specify a **custom `User-Agent` header** when implementing crawlers. Providing a descriptive user agent allows servers to correctly identify the client and reduces the likelihood of requests being denied.
A user agent string should include sufficient detail to identify the crawler and provide contact information. For example:
```
AcmeCo Crawler - acme.co - contact@acme.co
```
When adding contact details, avoid using personal identifiers such as your own name or a private email address—especially in a professional or employment context. Instead, use a role-based or organizational contact (e.g., crawler-team@company.com) to protect individual privacy while still enabling communication.
If a crawler behaves unexpectedly—for example, due to misconfiguration or implementation errors—server administrators can use the information in the user agent to contact the operator and coordinate an appropriate resolution.
The `User-Agent` header can be set on individual requests or applied globally by configuring a custom dispatcher.
**Example: setting a `User-Agent` per request**
```js
import { fetch } from 'undici'
const headers = {
'User-Agent': 'AcmeCo Crawler - acme.co - contact@acme.co'
}
const res = await fetch('https://example.com', { headers })
```
## Best Practices for Crawlers
When developing a crawler, the following practices are recommended in addition to setting a descriptive `User-Agent` header:
* **Respect `robots.txt`**
Follow the directives defined in the target site’s `robots.txt` file, including disallowed paths and optional crawl-delay settings (see [W3C guidelines](https://www.w3.org/wiki/Write_Web_Crawler)).
* **Rate limiting**
Regulate request frequency to avoid imposing excessive load on servers. Introduce delays between requests or limit the number of concurrent requests. The W3C suggests at least one second between requests.
* **Error handling**
Implement retry logic with exponential backoff for transient failures, and stop requests when persistent errors occur (e.g., HTTP 403 or 429).
* **Monitoring and logging**
Track request volume, response codes, and error rates to detect misbehavior and address issues proactively.
* **Contact information**
Always include valid and current contact details in the `User-Agent` string so that administrators can reach the crawler operator if necessary.
## References and Further Reading
* [RFC 9309: The Robots Exclusion Protocol](https://datatracker.ietf.org/doc/html/rfc9309)
* [W3C Wiki: Write Web Crawler](https://www.w3.org/wiki/Write_Web_Crawler)
* [Ethical Web Crawling (WWW 2010 Conference Paper)](https://archives.iw3c2.org/www2010/proceedings/www/p1101.pdf)
================================================
FILE: docs/docs/best-practices/mocking-request.md
================================================
# Mocking Request
Undici has its own mocking [utility](/docs/docs/api/MockAgent.md). It allow us to intercept undici HTTP requests and return mocked values instead. It can be useful for testing purposes.
Example:
```js
// bank.mjs
import { request } from 'undici'
export async function bankTransfer(recipient, amount) {
const { body } = await request('http://localhost:3000/bank-transfer',
{
method: 'POST',
headers: {
'X-TOKEN-SECRET': 'SuperSecretToken',
},
body: JSON.stringify({
recipient,
amount
})
}
)
return await body.json()
}
```
And this is what the test file looks like:
```js
// index.test.mjs
import { strict as assert } from 'node:assert'
import { MockAgent, setGlobalDispatcher, } from 'undici'
import { bankTransfer } from './bank.mjs'
const mockAgent = new MockAgent();
setGlobalDispatcher(mockAgent);
// Provide the base url to the request
const mockPool = mockAgent.get('http://localhost:3000');
// intercept the request
mockPool.intercept({
path: '/bank-transfer',
method: 'POST',
headers: {
'X-TOKEN-SECRET': 'SuperSecretToken',
},
body: JSON.stringify({
recipient: '1234567890',
amount: '100'
})
}).reply(200, {
message: 'transaction processed'
})
const success = await bankTransfer('1234567890', '100')
assert.deepEqual(success, { message: 'transaction processed' })
// if you dont want to check whether the body or the headers contain the same value
// just remove it from interceptor
mockPool.intercept({
path: '/bank-transfer',
method: 'POST',
}).reply(400, {
message: 'bank account not found'
})
const badRequest = await bankTransfer('1234567890', '100')
assert.deepEqual(badRequest, { message: 'bank account not found' })
```
Explore other MockAgent functionality [here](/docs/docs/api/MockAgent.md)
## Access agent call history
Using a MockAgent also allows you to make assertions on the configuration used to make your request in your application.
Here is an example :
```js
// index.test.mjs
import { strict as assert } from 'node:assert'
import { MockAgent, setGlobalDispatcher, fetch } from 'undici'
import { app } from './app.mjs'
// given an application server running on http://localhost:3000
await app.start()
// enable call history at instantiation
const mockAgent = new MockAgent({ enableCallHistory: true })
// or after instantiation
mockAgent.enableCallHistory()
setGlobalDispatcher(mockAgent)
// this call is made (not intercepted)
await fetch(`http://localhost:3000/endpoint?query='hello'`, {
method: 'POST',
headers: { 'content-type': 'application/json' }
body: JSON.stringify({ data: '' })
})
// access to the call history of the MockAgent (which register every call made intercepted or not)
assert.ok(mockAgent.getCallHistory()?.calls().length === 1)
assert.strictEqual(mockAgent.getCallHistory()?.firstCall()?.fullUrl, `http://localhost:3000/endpoint?query='hello'`)
assert.strictEqual(mockAgent.getCallHistory()?.firstCall()?.body, JSON.stringify({ data: '' }))
assert.deepStrictEqual(mockAgent.getCallHistory()?.firstCall()?.searchParams, { query: 'hello' })
assert.strictEqual(mockAgent.getCallHistory()?.firstCall()?.port, '3000')
assert.strictEqual(mockAgent.getCallHistory()?.firstCall()?.host, 'localhost:3000')
assert.strictEqual(mockAgent.getCallHistory()?.firstCall()?.method, 'POST')
assert.strictEqual(mockAgent.getCallHistory()?.firstCall()?.path, '/endpoint')
assert.deepStrictEqual(mockAgent.getCallHistory()?.firstCall()?.headers, { 'content-type': 'application/json' })
// clear all call history logs
mockAgent.clearCallHistory()
assert.ok(mockAgent.getCallHistory()?.calls().length === 0)
```
Calling `mockAgent.close()` will automatically clear and delete every call history for you.
Explore other MockAgent functionality [here](/docs/docs/api/MockAgent.md)
Explore other MockCallHistory functionality [here](/docs/docs/api/MockCallHistory.md)
Explore other MockCallHistoryLog functionality [here](/docs/docs/api/MockCallHistoryLog.md)
## Debug Mock Value
When the interceptor and the request options are not the same, undici will automatically make a real HTTP request. To prevent real requests from being made, use `mockAgent.disableNetConnect()`:
```js
const mockAgent = new MockAgent();
setGlobalDispatcher(mockAgent);
mockAgent.disableNetConnect()
// Provide the base url to the request
const mockPool = mockAgent.get('http://localhost:3000');
mockPool.intercept({
path: '/bank-transfer',
method: 'POST',
}).reply(200, {
message: 'transaction processed'
})
const badRequest = await bankTransfer('1234567890', '100')
// Will throw an error
// MockNotMatchedError: Mock dispatch not matched for path '/bank-transfer':
// subsequent request to origin http://localhost:3000 was not allowed (net.connect disabled)
```
## Reply with data based on request
If the mocked response needs to be dynamically derived from the request parameters, you can provide a function instead of an object to `reply`:
```js
mockPool.intercept({
path: '/bank-transfer',
method: 'POST',
headers: {
'X-TOKEN-SECRET': 'SuperSecretToken',
},
body: JSON.stringify({
recipient: '1234567890',
amount: '100'
})
}).reply(200, (opts) => {
// do something with opts
return { message: 'transaction processed' }
})
```
in this case opts will be
```
{
method: 'POST',
headers: { 'X-TOKEN-SECRET': 'SuperSecretToken' },
body: '{"recipient":"1234567890","amount":"100"}',
origin: 'http://localhost:3000',
path: '/bank-transfer'
}
```
================================================
FILE: docs/docs/best-practices/proxy.md
================================================
# Connecting through a proxy
Connecting through a proxy is possible by:
- Using [ProxyAgent](/docs/docs/api/ProxyAgent.md).
- Configuring `Client` or `Pool` constructor.
The proxy url should be passed to the `Client` or `Pool` constructor, while the upstream server url
should be added to every request call in the `path`.
For instance, if you need to send a request to the `/hello` route of your upstream server,
the `path` should be `path: 'http://upstream.server:port/hello?foo=bar'`.
If you proxy requires basic authentication, you can send it via the `proxy-authorization` header.
### Connect without authentication
```js
import { Client } from 'undici'
import { createServer } from 'http'
import { createProxy } from 'proxy'
const server = await buildServer()
const proxyServer = await buildProxy()
const serverUrl = `http://localhost:${server.address().port}`
const proxyUrl = `http://localhost:${proxyServer.address().port}`
server.on('request', (req, res) => {
console.log(req.url) // '/hello?foo=bar'
res.setHeader('content-type', 'application/json')
res.end(JSON.stringify({ hello: 'world' }))
})
const client = new Client(proxyUrl)
const response = await client.request({
method: 'GET',
path: serverUrl + '/hello?foo=bar'
})
response.body.setEncoding('utf8')
let data = ''
for await (const chunk of response.body) {
data += chunk
}
console.log(response.statusCode) // 200
console.log(JSON.parse(data)) // { hello: 'world' }
server.close()
proxyServer.close()
client.close()
function buildServer () {
return new Promise((resolve, reject) => {
const server = createServer()
server.listen(0, () => resolve(server))
})
}
function buildProxy () {
return new Promise((resolve, reject) => {
const server = createProxy(createServer())
server.listen(0, () => resolve(server))
})
}
```
### Connect with authentication
```js
import { Client } from 'undici'
import { createServer } from 'http'
import { createProxy } from 'proxy'
const server = await buildServer()
const proxyServer = await buildProxy()
const serverUrl = `http://localhost:${server.address().port}`
const proxyUrl = `http://localhost:${proxyServer.address().port}`
proxyServer.authenticate = function (req) {
return req.headers['proxy-authorization'] === `Basic ${Buffer.from('user:pass').toString('base64')}`
}
server.on('request', (req, res) => {
console.log(req.url) // '/hello?foo=bar'
res.setHeader('content-type', 'application/json')
res.end(JSON.stringify({ hello: 'world' }))
})
const client = new Client(proxyUrl)
const response = await client.request({
method: 'GET',
path: serverUrl + '/hello?foo=bar',
headers: {
'proxy-authorization': `Basic ${Buffer.from('user:pass').toString('base64')}`
}
})
response.body.setEncoding('utf8')
let data = ''
for await (const chunk of response.body) {
data += chunk
}
console.log(response.statusCode) // 200
console.log(JSON.parse(data)) // { hello: 'world' }
server.close()
proxyServer.close()
client.close()
function buildServer () {
return new Promise((resolve, reject) => {
const server = createServer()
server.listen(0, () => resolve(server))
})
}
function buildProxy () {
return new Promise((resolve, reject) => {
const server = createProxy(createServer())
server.listen(0, () => resolve(server))
})
}
```
================================================
FILE: docs/docs/best-practices/undici-vs-builtin-fetch.md
================================================
# Undici Module vs. Node.js Built-in Fetch
Node.js has shipped a built-in `fetch()` implementation powered by undici since
Node.js v18. This guide explains the relationship between the `undici` npm
package and the built-in `fetch`, and when you should install one versus relying
on the other.
## Background
The `fetch()`, `Request`, `Response`, `Headers`, and `FormData` globals in
Node.js v18+ are provided by a version of undici that is bundled into Node.js
itself. You can check which version is bundled with:
```js
console.log(process.versions.undici); // e.g., "7.5.0"
```
When you install undici from npm, you get the full library with all of its
additional APIs, and potentially a newer release than what your Node.js version
bundles.
## When you do NOT need to install undici
If all of the following are true, you can rely on the built-in globals and skip
adding undici to your dependencies:
- You only need the standard Fetch API (`fetch`, `Request`, `Response`,
`Headers`, `FormData`).
- You are running Node.js v18 or later.
- You do not depend on features or bug fixes introduced in a version of undici
newer than the one bundled with your Node.js release.
- You want zero additional runtime dependencies.
- You want cross-platform interoperability with browsers and other runtimes
(Deno, Bun, Cloudflare Workers, etc.) using the same Fetch API surface.
This is common in applications that make straightforward HTTP requests or in
libraries that target multiple JavaScript runtimes.
## When you SHOULD install undici
Install undici from npm when you need capabilities beyond the standard Fetch API:
### Advanced HTTP APIs
undici exposes `request`, `stream`, `pipeline`, and `connect` methods that
provide lower-level control and significantly better performance than `fetch`:
```js
import { request } from 'undici';
const { statusCode, headers, body } = await request('https://example.com');
const data = await body.json();
```
### Connection pooling and dispatchers
`Client`, `Pool`, `BalancedPool`, `Agent`, and their configuration options
let you manage connection lifecycle, keep-alive behavior, pipelining depth,
and concurrency limits:
```js
import { Pool } from 'undici';
const pool = new Pool('https://example.com', { connections: 10 });
const { body } = await pool.request({ path: '/', method: 'GET' });
```
### Proxy support
`ProxyAgent` and `EnvHttpProxyAgent` handle HTTP(S) proxying. Note that
Node.js v22.21.0+ and v24.0.0+ support environment-variable-based proxy
configuration for the built-in `fetch` via the `--use-env-proxy` flag (or
`NODE_USE_ENV_PROXY=1`). However, undici's `ProxyAgent` still provides
programmatic control through the dispatcher API:
```js
import { ProxyAgent, fetch } from 'undici';
const proxyAgent = new ProxyAgent('https://my-proxy.example.com:8080');
const response = await fetch('https://example.com', { dispatcher: proxyAgent });
```
### Testing and mocking
`MockAgent`, `MockClient`, and `MockPool` let you intercept and mock HTTP
requests without patching globals or depending on external libraries:
```js
import { MockAgent, setGlobalDispatcher, fetch } from 'undici';
const mockAgent = new MockAgent();
setGlobalDispatcher(mockAgent);
const pool = mockAgent.get('https://example.com');
pool.intercept({ path: '/api' }).reply(200, { message: 'mocked' });
```
### Interceptors and middleware
Custom dispatchers and interceptors (retry, redirect, cache, DNS) give you
fine-grained control over how requests are processed.
### Newer version than what Node.js bundles
The npm package often includes features, performance improvements, and bug fixes
that have not yet landed in a Node.js release. If you need a specific fix or
feature, you can install a newer version directly.
## Version compatibility
| Node.js version | Bundled undici version | Notes |
|---|---|---|
| v18.x | ~5.x | `fetch` is experimental (behind `--experimental-fetch` in early v18) |
| v20.x | ~6.x | `fetch` is stable |
| v22.x | ~6.x / ~7.x | `fetch` is stable |
| v24.x | ~7.x | `fetch` is stable; env-proxy support via `--use-env-proxy` |
You can always check the exact bundled version at runtime with
`process.versions.undici`.
Installing undici from npm does not replace the built-in globals. If you want
your installed version to override the global `fetch`, use
[`setGlobalDispatcher`](/docs/api/GlobalInstallation.md) or import `fetch`
directly from `'undici'`:
```js
import { fetch } from 'undici'; // uses your installed version, not the built-in
```
## Further reading
- [API Reference: Fetch](/docs/api/Fetch.md)
- [API Reference: Client](/docs/api/Client.md)
- [API Reference: Pool](/docs/api/Pool.md)
- [API Reference: ProxyAgent](/docs/api/ProxyAgent.md)
- [API Reference: MockAgent](/docs/api/MockAgent.md)
- [API Reference: Global Installation](/docs/api/GlobalInstallation.md)
================================================
FILE: docs/docs/best-practices/writing-tests.md
================================================
# Writing tests
Undici is tuned for a production use case and its default will keep
a socket open for a few seconds after an HTTP request is completed to
remove the overhead of opening up a new socket. These settings that makes
Undici shine in production are not a good fit for using Undici in automated
tests, as it will result in longer execution times.
The following are good defaults that will keep the socket open for only 10ms:
```js
import { request, setGlobalDispatcher, Agent } from 'undici'
const agent = new Agent({
keepAliveTimeout: 10, // milliseconds
keepAliveMaxTimeout: 10 // milliseconds
})
setGlobalDispatcher(agent)
```
## Guarding against unexpected disconnects
Undici's `Client` automatically reconnects after a socket error. This means
a test can silently disconnect, reconnect, and still pass. Unfortunately,
this could mask bugs like unexpected parser errors or protocol violations.
To catch these silent reconnections, add a disconnect guard after creating
a `Client`:
```js
const { Client } = require('undici')
const { test, after } = require('node:test')
const { tspl } = require('@matteo.collina/tspl')
test('example with disconnect guard', async (t) => {
t = tspl(t, { plan: 1 })
const client = new Client('http://localhost:3000')
after(() => client.close())
client.on('disconnect', () => {
if (!client.closed && !client.destroyed) {
t.fail('unexpected disconnect')
}
})
// ... test logic ...
})
```
`client.close()` and `client.destroy()` both emit `'disconnect'` events, but
those are expected. The guard only fails when a disconnect happens during the
active test (i.e., `!client.closed && !client.destroyed` is true).
Skip the guard for tests where a disconnect is expected behavior, such as:
- Signal aborts (`signal.emit('abort')`, `ac.abort()`)
- Server-side destruction (`res.destroy()`, `req.socket.destroy()`)
- Client-side body destruction mid-stream (`data.body.destroy()`)
- Timeout errors (`HeadersTimeoutError`, `BodyTimeoutError`)
- Successful upgrades (the socket is detached from the `Client`)
- Retry/reconnect tests where the disconnect triggers the retry
- HTTP parser errors from malformed responses (`HTTPParserError`)
================================================
FILE: docs/docsify/sidebar.md
================================================
* [**Home**](/ "Node.js Undici")
* API
* [Dispatcher](/docs/api/Dispatcher.md "Undici API - Dispatcher")
* [Client](/docs/api/Client.md "Undici API - Client")
* [H2CClient](/docs/api/H2CClient.md "Undici H2C API - Client")
* [Pool](/docs/api/Pool.md "Undici API - Pool")
* [BalancedPool](/docs/api/BalancedPool.md "Undici API - BalancedPool")
* [RoundRobinPool](/docs/api/RoundRobinPool.md "Undici API - RoundRobinPool")
* [Agent](/docs/api/Agent.md "Undici API - Agent")
* [ProxyAgent](/docs/api/ProxyAgent.md "Undici API - ProxyAgent")
* [Socks5Agent](/docs/api/Socks5Agent.md "Undici API - SOCKS5 Agent")
* [RetryAgent](/docs/api/RetryAgent.md "Undici API - RetryAgent")
* [Connector](/docs/api/Connector.md "Custom connector")
* [Errors](/docs/api/Errors.md "Undici API - Errors")
* [EventSource](/docs/api/EventSource.md "Undici API - EventSource")
* [Fetch](/docs/api/Fetch.md "Undici API - Fetch")
* [Global Installation](/docs/api/GlobalInstallation.md "Undici API - Global Installation")
* [Cookies](/docs/api/Cookies.md "Undici API - Cookies")
* [MockClient](/docs/api/MockClient.md "Undici API - MockClient")
* [MockPool](/docs/api/MockPool.md "Undici API - MockPool")
* [MockAgent](/docs/api/MockAgent.md "Undici API - MockAgent")
* [SnapshotAgent](/docs/api/SnapshotAgent.md "Undici API - SnapshotAgent")
* [MockCallHistory](/docs/api/MockCallHistory.md "Undici API - MockCallHistory")
* [MockCallHistoryLog](/docs/api/MockCallHistoryLog.md "Undici API - MockCallHistoryLog")
* [MockErrors](/docs/api/MockErrors.md "Undici API - MockErrors")
* [API Lifecycle](/docs/api/api-lifecycle.md "Undici API - Lifecycle")
* [Diagnostics Channel Support](/docs/api/DiagnosticsChannel.md "Diagnostics Channel Support")
* [Debug](/docs/api/Debug.md "Undici API - Debugging Undici")
* [WebSocket](/docs/api/WebSocket.md "Undici API - WebSocket")
* [MIME Type Parsing](/docs/api/ContentType.md "Undici API - MIME Type Parsing")
* [CacheStorage](/docs/api/CacheStorage.md "Undici API - CacheStorage")
* [Util](/docs/api/Util.md "Undici API - Util")
* [RedirectHandler](/docs/api/RedirectHandler.md "Undici API - RedirectHandler")
* [RetryHandler](/docs/api/RetryHandler.md "Undici API - RetryHandler")
* [DiagnosticsChannel](/docs/api/DiagnosticsChannel.md "Undici API - DiagnosticsChannel")
* [EnvHttpProxyAgent](/docs/api/EnvHttpProxyAgent.md "Undici API - EnvHttpProxyAgent")
* [PoolStats](/docs/api/PoolStats.md "Undici API - PoolStats")
* Examples
* [Undici Examples](/examples/ "Undici Examples")
* Best Practices
* [Undici vs. Built-in Fetch](/docs/best-practices/undici-vs-builtin-fetch.md "When to install undici vs using Node.js built-in fetch")
* [Proxy](/docs/best-practices/proxy.md "Connecting through a proxy")
* [Client Certificate](/docs/best-practices/client-certificate.md "Connect using a client certificate")
* [Writing Tests](/docs/best-practices/writing-tests.md "Using Undici inside tests")
* [Mocking Request](/docs/best-practices/mocking-request.md "Using Undici inside tests")
* [Crawling](/docs/best-practices/crawling.md "Crawling")
================================================
FILE: docs/examples/README.md
================================================
## undici.request() examples
### A simple GET request, read the response body as text:
```js
const { request } = require('undici')
async function getRequest (port = 3001) {
// A simple GET request
const {
statusCode,
headers,
body
} = await request(`http://localhost:${port}/`)
const data = await body.text()
console.log('response received', statusCode)
console.log('headers', headers)
console.log('data', data)
}
```
### A JSON POST request, read the response body as json:
```js
const { request } = require('undici')
async function postJSONRequest (port = 3001) {
const requestBody = {
hello: 'JSON POST Example body'
}
const {
statusCode,
headers,
body
} = await request(
`http://localhost:${port}/json`,
{ method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(requestBody) }
)
// .json() will fail if we did not receive a valid json body in response:
const decodedJson = await body.json()
console.log('response received', statusCode)
console.log('headers', headers)
console.log('data', decodedJson)
}
```
### A Form POST request, read the response body as text:
```js
const { request } = require('undici')
async function postFormRequest (port = 3001) {
// Make a URL-encoded form POST request:
const qs = require('node:querystring')
const requestBody = {
hello: 'URL Encoded Example body'
}
const {
statusCode,
headers,
body
} = await request(
`http://localhost:${port}/form`,
{ method: 'POST', headers: { 'content-type': 'application/x-www-form-urlencoded' }, body: qs.stringify(requestBody) }
)
const data = await body.text()
console.log('response received', statusCode)
console.log('headers', headers)
console.log('data', data)
}
```
### A FormData request with file stream, read the response body as text
```js
const { request } = require('undici')
const { openAsBlob } = require('fs')
async function formDataBlobRequest () {
// Make a FormData request with file stream:
const formData = new FormData()
formData.append('field', 42)
formData.set('file', await openAsBlob('./index.mjs'))
const {
statusCode,
headers,
body
} = await request('http://127.0.0.1:3000', {
method: 'POST',
body: formData
})
const data = await body.text()
console.log('response received', statusCode)
console.log('headers', headers)
console.log('data', data)
}
```
### A DELETE request
```js
const { request } = require('undici')
async function deleteRequest (port = 3001) {
// Make a DELETE request
const {
statusCode,
headers,
body
} = await request(
`http://localhost:${port}/something`,
{ method: 'DELETE' }
)
console.log('response received', statusCode)
console.log('headers', headers)
// For a DELETE request we expect a 204 response with no body if successful, in which case getting the body content with .json() will fail
if (statusCode === 204) {
console.log('delete successful')
// always consume the body if there is one:
await body.dump()
} else {
const data = await body.text()
console.log('received unexpected data', data)
}
}
```
## Production configuration
### Using interceptors to add response caching, DNS lookup caching and connection retries
```js
import { Agent, interceptors, setGlobalDispatcher } from 'undici'
// Interceptors to add response caching, DNS caching and retrying to the dispatcher
const { cache, dns, retry } = interceptors
const defaultDispatcher = new Agent({
connections: 100, // Limit concurrent kept-alive connections to not run out of resources
headersTimeout: 10_000, // 10 seconds; set as appropriate for the remote servers you plan to connect to
bodyTimeout: 10_000,
}).compose(cache(), dns(), retry())
setGlobalDispatcher(defaultDispatcher) // Add these interceptors to all `fetch` and Undici `request` calls
```
### Cache interceptor with `fetch`
```js
import { Agent, interceptors, setGlobalDispatcher, fetch } from 'undici'
import { createServer } from 'node:http'
const { cache } = interceptors
const server = createServer((req, res) => {
// Cache this response for 60 seconds
res.setHeader('cache-control', 'public, max-age=60')
res.end(JSON.stringify({ now: Date.now() }))
})
await new Promise((resolve) => server.listen(0, resolve))
const { port } = server.address()
const dispatcher = new Agent().compose(cache())
setGlobalDispatcher(dispatcher)
// First request goes to the origin server
const first = await fetch(`http://localhost:${port}`)
const firstBody = await first.json()
// Second request is served from cache if still fresh
const second = await fetch(`http://localhost:${port}`)
const secondBody = await second.json()
console.log(firstBody.now === secondBody.now) // true
await dispatcher.close()
await new Promise((resolve) => server.close(resolve))
```
## Connecting via Unix domain sockets (UDS)
### request() over UDS (per-call dispatcher)
```js
const { Agent, request } = require('undici')
async function requestOverUds () {
const uds = new Agent({ connect: { socketPath: '/var/run/docker.sock' } })
try {
const { statusCode, headers, body } = await request('http://localhost/_ping', {
dispatcher: uds
})
console.log(statusCode, headers, await body.text())
} finally {
await uds.close()
}
}
```
### fetch() over UDS (per-call dispatcher)
```js
const { Agent, fetch } = require('undici')
async function fetchOverUds () {
const uds = new Agent({ connect: { socketPath: '/var/run/docker.sock' } })
try {
const res = await fetch('http://localhost/containers/json', { dispatcher: uds })
console.log(res.status, await res.text())
} finally {
await uds.close()
}
}
```
> Note
> - `connect.socketPath` must be the exact filesystem path your server listens on (e.g., Docker: `/var/run/docker.sock`)..
> - Not supported on Windows (uses named pipes instead).
================================================
FILE: docs/examples/ca-fingerprint/index.js
================================================
'use strict'
const crypto = require('node:crypto')
const https = require('node:https')
const { Client, buildConnector } = require('../../../')
const pem = require('@metcoder95/https-pem')
const caFingerprint = getFingerprint(pem.cert.toString()
.split('\n')
.slice(1, -1)
.map(line => line.trim())
.join('')
)
const server = https.createServer(pem, (req, res) => {
res.setHeader('Content-Type', 'text/plain')
res.end('hello')
})
server.listen(0, function () {
const connector = buildConnector({ rejectUnauthorized: false })
const client = new Client(`https://localhost:${server.address().port}`, {
connect (opts, cb) {
connector(opts, (err, socket) => {
if (err) {
cb(err)
} else if (getIssuerCertificate(socket).fingerprint256 !== caFingerprint) {
socket.destroy()
cb(new Error('Fingerprint does not match or malformed certificate'))
} else {
cb(null, socket)
}
})
}
})
client.request({
path: '/',
method: 'GET'
}, (err, data) => {
if (err) throw err
const bufs = []
data.body.on('data', (buf) => {
bufs.push(buf)
})
data.body.on('end', () => {
console.log(Buffer.concat(bufs).toString('utf8'))
client.close()
server.close()
})
})
})
function getIssuerCertificate (socket) {
let certificate = socket.getPeerCertificate(true)
while (certificate && Object.keys(certificate).length > 0) {
// invalid certificate
if (certificate.issuerCertificate == null) {
return null
}
// We have reached the root certificate.
// In case of self-signed certificates, `issuerCertificate` may be a circular reference.
if (certificate.fingerprint256 === certificate.issuerCertificate.fingerprint256) {
break
}
// continue the loop
certificate = certificate.issuerCertificate
}
return certificate
}
function getFingerprint (content, inputEncoding = 'base64', outputEncoding = 'hex') {
const shasum = crypto.createHash('sha256')
shasum.update(content, inputEncoding)
const res = shasum.digest(outputEncoding)
return res.toUpperCase().match(/.{1,2}/g).join(':')
}
================================================
FILE: docs/examples/eventsource.js
================================================
'use strict'
const { randomBytes } = require('node:crypto')
const { EventSource } = require('../../')
async function main () {
const url = `https://smee.io/${randomBytes(8).toString('base64url')}`
console.log(`Connecting to event source server ${url}`)
const ev = new EventSource(url)
ev.onmessage = console.log
ev.onerror = console.log
ev.onopen = console.log
// Special event of smee.io
ev.addEventListener('ready', console.log)
// Ping event is sent every 30 seconds by smee.io
ev.addEventListener('ping', console.log)
}
main()
================================================
FILE: docs/examples/fetch.js
================================================
'use strict'
const { fetch } = require('../../')
async function main () {
const res = await fetch('http://localhost:3001/')
const data = await res.text()
console.log('response received', res.status)
console.log('headers', res.headers)
console.log('data', data)
}
main()
================================================
FILE: docs/examples/proxy/fetch.mjs
================================================
import * as http from 'node:http'
import { once } from 'node:events'
import { createProxy } from 'proxy'
import { fetch, ProxyAgent } from '../../../'
const proxyServer = createProxy(http.createServer())
const server = http.createServer((req, res) => {
res.writeHead(200, { 'Content-Type': 'text/plain' })
res.end('okay')
})
proxyServer.on('request', (req, res) => {
console.log(`Incoming request to ${req.url}`)
})
await once(proxyServer.listen(0), 'listening')
await once(server.listen(0), 'listening')
const { port: proxyPort } = proxyServer.address()
const { port } = server.address()
console.log(`Proxy listening on port ${proxyPort}`)
console.log(`Server listening on port ${port}`)
try {
// undici does a tunneling to the proxy server using CONNECT.
const agent = new ProxyAgent(`http://localhost:${proxyPort}`)
const response = await fetch(`http://localhost:${port}`, {
dispatcher: agent,
method: 'GET'
})
const data = await response.text()
console.log('Response data:', data)
} catch (e) {
console.log(e)
}
================================================
FILE: docs/examples/proxy/index.js
================================================
'use strict'
const { Pool, Client } = require('../../../')
const http = require('node:http')
const proxy = require('./proxy')
const pool = new Pool('http://localhost:4001', {
connections: 256,
pipelining: 1
})
async function run () {
await Promise.all([
new Promise(resolve => {
// Proxy
http.createServer((req, res) => {
proxy({ req, res, proxyName: 'example' }, pool).catch(err => {
if (res.headersSent) {
res.destroy(err)
} else {
for (const name of res.getHeaderNames()) {
res.removeHeader(name)
}
res.statusCode = err.statusCode || 500
res.end()
}
})
}).listen(4000, resolve)
}),
new Promise(resolve => {
// Upstream
http.createServer((req, res) => {
res.end('hello world')
}).listen(4001, resolve)
})
])
const client = new Client('http://localhost:4000')
const { body } = await client.request({
method: 'GET',
path: '/'
})
for await (const chunk of body) {
console.log(String(chunk))
}
}
run()
================================================
FILE: docs/examples/proxy/proxy.js
================================================
'use strict'
const net = require('node:net')
const { pipeline } = require('node:stream')
const { STATUS_CODES } = require('node:http')
module.exports = async function proxy (ctx, client) {
const { req, socket, proxyName } = ctx
const headers = getHeaders({
headers: req.rawHeaders,
httpVersion: req.httpVersion,
socket: req.socket,
proxyName
})
if (socket) {
const handler = new WSHandler(ctx)
client.dispatch({
method: req.method,
path: req.url,
headers,
upgrade: 'Websocket'
}, handler)
return handler.promise
} else {
const handler = new HTTPHandler(ctx)
client.dispatch({
method: req.method,
path: req.url,
headers,
body: req
}, handler)
return handler.promise
}
}
class HTTPHandler {
constructor (ctx) {
const { req, res, proxyName } = ctx
this.proxyName = proxyName
this.req = req
this.res = res
this.resume = null
this.abort = null
this.promise = new Promise((resolve, reject) => {
this.callback = err => err ? reject(err) : resolve()
})
}
onConnect (abort) {
if (this.req.aborted) {
abort()
} else {
this.abort = abort
this.res.on('close', abort)
}
}
onHeaders (statusCode, headers, resume) {
if (statusCode < 200) {
return
}
this.resume = resume
this.res.on('drain', resume)
this.res.writeHead(statusCode, getHeaders({
headers,
proxyName: this.proxyName,
httpVersion: this.httpVersion
}))
}
onData (chunk) {
return this.res.write(chunk)
}
onComplete () {
this.res.off('close', this.abort)
this.res.off('drain', this.resume)
this.res.end()
this.callback()
}
onError (err) {
this.res.off('close', this.abort)
this.res.off('drain', this.resume)
this.callback(err)
}
}
class WSHandler {
constructor (ctx) {
const { req, socket, proxyName, head } = ctx
setupSocket(socket)
this.proxyName = proxyName
this.httpVersion = req.httpVersion
this.socket = socket
this.head = head
this.abort = null
this.promise = new Promise((resolve, reject) => {
this.callback = err => err ? reject(err) : resolve()
})
}
onConnect (abort) {
if (this.socket.destroyed) {
abort()
} else {
this.abort = abort
this.socket.on('close', abort)
}
}
onUpgrade (statusCode, headers, socket) {
this.socket.off('close', this.abort)
// TODO: Check statusCode?
if (this.head && this.head.length) {
socket.unshift(this.head)
}
setupSocket(socket)
headers = getHeaders({
headers,
proxyName: this.proxyName,
httpVersion: this.httpVersion
})
let head = ''
for (let n = 0; n < headers.length; n += 2) {
head += `\r\n${headers[n]}: ${headers[n + 1]}`
}
this.socket.write(`HTTP/1.1 101 Switching Protocols\r\nconnection: upgrade\r\nupgrade: websocket${head}\r\n\r\n`)
pipeline(socket, this.socket, socket, this.callback)
}
onError (err) {
this.socket.off('close', this.abort)
this.callback(err)
}
}
// This expression matches hop-by-hop headers.
// These headers are meaningful only for a single transport-level connection,
// and must not be retransmitted by proxies or cached.
const HOP_EXPR = /^(te|host|upgrade|trailers|connection|keep-alive|http2-settings|transfer-encoding|proxy-connection|proxy-authenticate|proxy-authorization)$/i
// Removes hop-by-hop and pseudo headers.
// Updates via and forwarded headers.
// Only hop-by-hop headers may be set using the Connection general header.
function getHeaders ({
headers,
proxyName,
httpVersion,
socket
}) {
let via = ''
let forwarded = ''
let host = ''
let authority = ''
let connection = ''
for (let n = 0; n < headers.length; n += 2) {
const key = headers[n]
const val = headers[n + 1]
if (!via && key.length === 3 && key.toLowerCase() === 'via') {
via = val
} else if (!host && key.length === 4 && key.toLowerCase() === 'host') {
host = val
} else if (!forwarded && key.length === 9 && key.toLowerCase() === 'forwarded') {
forwarded = val
} else if (!connection && key.length === 10 && key.toLowerCase() === 'connection') {
connection = val
} else if (!authority && key.length === 10 && key === ':authority') {
authority = val
}
}
let remove
if (connection && !HOP_EXPR.test(connection)) {
remove = connection.split(/,\s*/)
}
const result = []
for (let n = 0; n < headers.length; n += 2) {
const key = headers[n]
const val = headers[n + 1]
if (
key.charAt(0) !== ':' &&
!HOP_EXPR.test(key) &&
(!remove || !remove.includes(key))
) {
result.push(key, val)
}
}
if (socket) {
result.push('forwarded', (forwarded ? forwarded + ', ' : '') + [
`by=${printIp(socket.localAddress, socket.localPort)}`,
`for=${printIp(socket.remoteAddress, socket.remotePort)}`,
`proto=${socket.encrypted ? 'https' : 'http'}`,
`host=${printIp(authority || host || '')}`
].join(';'))
} else if (forwarded) {
// The forwarded header should not be included in response.
throw new BadGateway()
}
if (proxyName) {
if (via) {
if (via.split(',').some(name => name.endsWith(proxyName))) {
throw new LoopDetected()
}
via += ', '
}
via += `${httpVersion} ${proxyName}`
}
if (via) {
result.push('via', via)
}
return result
}
function setupSocket (socket) {
socket.setTimeout(0)
socket.setNoDelay(true)
socket.setKeepAlive(true, 0)
}
function printIp (address, port) {
const isIPv6 = net.isIPv6(address)
let str = `${address}`
if (isIPv6) {
str = `[${str}]`
}
if (port) {
str = `${str}:${port}`
}
if (isIPv6 || port) {
str = `"${str}"`
}
return str
}
class BadGateway extends Error {
constructor (message = STATUS_CODES[502]) {
super(message)
}
toString () {
return `BadGatewayError: ${this.message}`
}
get name () {
return 'BadGatewayError'
}
get status () {
return 502
}
get statusCode () {
return 502
}
get expose () {
return false
}
get headers () {
return undefined
}
}
class LoopDetected extends Error {
constructor (message = STATUS_CODES[508]) {
super(message)
}
toString () {
return `LoopDetectedError: ${this.message}`
}
get name () {
return 'LoopDetectedError'
}
get status () {
return 508
}
get statusCode () {
return 508
}
get expose () {
return false
}
get headers () {
return undefined
}
}
================================================
FILE: docs/examples/proxy/websocket.js
================================================
'use strict'
const { Pool, Client } = require('../../../')
const http = require('node:http')
const proxy = require('./proxy')
const WebSocket = require('ws')
const pool = new Pool('http://localhost:4001', {
connections: 256,
pipelining: 1
})
function createWebSocketServer () {
const wss = new WebSocket.Server({ noServer: true })
wss.on('connection', ws => {
ws.on('message', message => {
console.log(`Received message: ${message}`)
ws.send('Received your message!')
})
})
return wss
}
async function run () {
await Promise.all([
new Promise(resolve => {
// Proxy
http.createServer((req, res) => {
proxy({ req, res, proxyName: 'example' }, pool).catch(err => {
if (res.headersSent) {
res.destroy(err)
} else {
for (const name of res.getHeaderNames()) {
res.removeHeader(name)
}
res.statusCode = err.statusCode || 500
res.end()
}
})
}).listen(4000, resolve)
}),
new Promise(resolve => {
// Upstream
http.createServer((req, res) => {
res.end('hello world')
}).listen(4001, resolve)
}),
new Promise(resolve => {
// WebSocket server
const server = http.createServer((req, res) => {
res.writeHead(200, { 'Content-Type': 'text/plain' })
res.end('WebSocket server is running!')
})
const wss = createWebSocketServer()
server.on('upgrade', (request, socket, head) => {
wss.handleUpgrade(request, socket, head, ws => {
wss.emit('connection', ws, request)
})
})
server.listen(4002, resolve)
})
])
const client = new Client('http://localhost:4000')
const { body } = await client.request({
method: 'GET',
path: '/'
})
for await (const chunk of body) {
console.log(String(chunk))
}
// WebSocket client
const ws = new WebSocket('ws://localhost:4002')
ws.on('open', () => {
ws.send('Hello, WebSocket Server!')
})
ws.on('message', message => {
console.log(`WebSocket Server says: ${message}`)
ws.close()
})
}
run()
================================================
FILE: docs/examples/proxy-agent.js
================================================
'use strict'
const { request, setGlobalDispatcher, ProxyAgent } = require('../..')
setGlobalDispatcher(new ProxyAgent('http://localhost:8000/'))
async function main () {
const {
statusCode,
headers,
trailers,
body
// send the request via the http://localhost:8000/ HTTP proxy
} = await request('http://localhost:3000/undici')
console.log('response received', statusCode)
console.log('headers', headers)
for await (const data of body) {
console.log('data', data)
}
console.log('trailers', trailers)
}
main()
================================================
FILE: docs/examples/request.js
================================================
'use strict'
const { request } = require('../../')
async function getRequest (port = 3001) {
// A simple GET request
const {
statusCode,
headers,
body
} = await request(`http://localhost:${port}/`)
const data = await body.text()
console.log('response received', statusCode)
console.log('headers', headers)
console.log('data', data)
}
async function postJSONRequest (port = 3001) {
// Make a JSON POST request:
const requestBody = {
hello: 'JSON POST Example body'
}
const {
statusCode,
headers,
body
} = await request(
`http://localhost:${port}/json`,
{ method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(requestBody) }
)
// .json() will fail if we did not receive a valid json body in response:
const decodedJson = await body.json()
console.log('response received', statusCode)
console.log('headers', headers)
console.log('data', decodedJson)
}
async function postFormRequest (port = 3001) {
// Make a URL-encoded form POST request:
const qs = require('node:querystring')
const requestBody = {
hello: 'URL Encoded Example body'
}
const {
statusCode,
headers,
body
} = await request(
`http://localhost:${port}/form`,
{ method: 'POST', headers: { 'content-type': 'application/x-www-form-urlencoded' }, body: qs.stringify(requestBody) }
)
const data = await body.text()
console.log('response received', statusCode)
console.log('headers', headers)
console.log('data', data)
}
async function deleteRequest (port = 3001) {
// Make a DELETE request
const {
statusCode,
headers,
body
} = await request(
`http://localhost:${port}/something`,
{ method: 'DELETE' }
)
console.log('response received', statusCode)
console.log('headers', headers)
// For a DELETE request we expect a 204 response with no body if successful, in which case getting the body content with .json() will fail
if (statusCode === 204) {
console.log('delete successful')
// always consume the body if there is one:
await body.dump()
} else {
const data = await body.text()
console.log('received unexpected data', data)
}
}
module.exports = {
getRequest,
postJSONRequest,
postFormRequest,
deleteRequest
}
================================================
FILE: docs/examples/snapshot-testing.js
================================================
const { SnapshotAgent, setGlobalDispatcher, getGlobalDispatcher, request } = require('../../index.js')
const { createServer } = require('node:http')
const { promisify } = require('node:util')
const { tmpdir } = require('node:os')
const { join } = require('node:path')
/**
* Example: Basic Snapshot Testing
*
* This example demonstrates how to use SnapshotAgent to record API
* interactions and replay them in tests for consistent, offline testing.
*/
async function basicSnapshotExample () {
console.log('🚀 Basic Snapshot Testing Example\n')
// Create a temporary snapshot file path
const snapshotPath = join(tmpdir(), `snapshot-example-${Date.now()}.json`)
console.log(`📁 Using temporary snapshot file: ${snapshotPath}\n`)
// Create a local test server
const server = createServer((req, res) => {
res.writeHead(200, { 'Content-Type': 'application/json' })
res.end(JSON.stringify({
message: 'Hello from test server!',
timestamp: new Date().toISOString(),
path: req.url
}))
})
await promisify(server.listen.bind(server))(0)
const { port } = server.address()
const origin = `http://localhost:${port}`
try {
// Step 1: Record mode - capture API responses
console.log('📹 Step 1: Recording API response...')
const recordingAgent = new SnapshotAgent({
mode: 'record',
snapshotPath
})
const originalDispatcher = getGlobalDispatcher()
setGlobalDispatcher(recordingAgent)
try {
// Make an API call that will be recorded
const response = await request(`${origin}/api/test`)
const data = await response.body.json()
console.log(`✅ Recorded response: ${data.message}`)
// Save the recorded snapshots
await recordingAgent.saveSnapshots()
console.log('💾 Snapshot saved to temporary file\n')
} finally {
setGlobalDispatcher(originalDispatcher)
recordingAgent.close()
}
// Step 2: Playback mode - use recorded responses (server can be down)
console.log('🎬 Step 2: Playing back recorded response...')
server.close() // Close server to prove we're using snapshots
const playbackAgent = new SnapshotAgent({
mode: 'playback',
snapshotPath
})
setGlobalDispatcher(playbackAgent)
try {
// This will use the recorded response instead of making a real request
const response = await request(`${origin}/api/test`)
const data = await response.body.json()
console.log(`✅ Playback response: ${data.message}`)
console.log('🎉 Successfully used recorded data instead of live server!')
} finally {
setGlobalDispatcher(originalDispatcher)
playbackAgent.close()
}
} finally {
// Ensure server is closed
if (server.listening) {
server.close()
}
// Clean up temporary file
try {
const { unlink } = require('node:fs/promises')
await unlink(snapshotPath)
console.log('\n🗑️ Cleaned up temporary snapshot file')
} catch {
// File might not exist or already be deleted
}
}
}
// Main execution
async function main () {
await basicSnapshotExample()
}
// Run if called directly
if (require.main === module) {
main().catch(console.error)
}
================================================
FILE: docs/examples/socks5-proxy.js
================================================
'use strict'
const { Socks5Agent, request, fetch } = require('undici')
// Basic example demonstrating SOCKS5 proxy usage
async function basicSocks5Example () {
console.log('=== Basic SOCKS5 Proxy Example ===')
try {
// Create SOCKS5 proxy wrapper
const socks5Proxy = new Socks5Agent('socks5://localhost:1080')
// Make request through SOCKS5 proxy
const response = await request('http://httpbin.org/ip', {
dispatcher: socks5Proxy
})
console.log('Status:', response.statusCode)
const body = await response.body.json()
console.log('Response:', body)
await socks5Proxy.close()
} catch (error) {
console.error('Error:', error.message)
}
}
// Example with authentication
async function authenticatedSocks5Example () {
console.log('\n=== Authenticated SOCKS5 Proxy Example ===')
try {
// Using credentials in URL
const socks5Proxy = new Socks5Agent('socks5://username:password@localhost:1080')
// Alternative: using options
// const socks5Proxy = new Socks5Agent('socks5://localhost:1080', {
// username: 'username',
// password: 'password'
// })
const response = await request('http://httpbin.org/headers', {
dispatcher: socks5Proxy
})
console.log('Status:', response.statusCode)
const body = await response.body.json()
console.log('Headers seen by server:', body.headers)
await socks5Proxy.close()
} catch (error) {
console.error('Error:', error.message)
}
}
// Example with fetch API
async function fetchWithSocks5Example () {
console.log('\n=== Fetch with SOCKS5 Proxy Example ===')
try {
const socks5Proxy = new Socks5Agent('socks5://localhost:1080')
const response = await fetch('http://httpbin.org/json', {
dispatcher: socks5Proxy
})
console.log('Status:', response.status)
const data = await response.json()
console.log('JSON data:', data)
await socks5Proxy.close()
} catch (error) {
console.error('Error:', error.message)
}
}
// Example with HTTPS
async function httpsWithSocks5Example () {
console.log('\n=== HTTPS with SOCKS5 Proxy Example ===')
try {
const socks5Proxy = new Socks5Agent('socks5://localhost:1080')
const response = await request('https://httpbin.org/ip', {
dispatcher: socks5Proxy
})
console.log('Status:', response.statusCode)
const body = await response.body.json()
console.log('HTTPS Response:', body)
await socks5Proxy.close()
} catch (error) {
console.error('Error:', error.message)
}
}
// Example with connection pooling
async function connectionPoolingExample () {
console.log('\n=== Connection Pooling Example ===')
try {
const socks5Proxy = new Socks5Agent('socks5://localhost:1080', {
connections: 5, // Allow up to 5 concurrent connections
pipelining: 1 // Enable HTTP/1.1 pipelining
})
// Make multiple concurrent requests
const requests = []
for (let i = 0; i < 3; i++) {
requests.push(
request(`http://httpbin.org/delay/${i}`, {
dispatcher: socks5Proxy
})
)
}
console.log('Making 3 concurrent requests...')
const responses = await Promise.all(requests)
for (let i = 0; i < responses.length; i++) {
console.log(`Request ${i + 1} status:`, responses[i].statusCode)
// Consume body to avoid warnings
await responses[i].body.dump()
}
await socks5Proxy.close()
} catch (error) {
console.error('Error:', error.message)
}
}
// Example with error handling
async function errorHandlingExample () {
console.log('\n=== Error Handling Example ===')
try {
// Intentionally use a non-existent proxy
const socks5Proxy = new Socks5Agent('socks5://localhost:9999')
await request('http://httpbin.org/ip', {
dispatcher: socks5Proxy
})
} catch (error) {
console.log('Caught expected error:', error.message)
console.log('Error code:', error.code)
}
}
// Global dispatcher example
async function globalDispatcherExample () {
console.log('\n=== Global Dispatcher Example ===')
const { setGlobalDispatcher, getGlobalDispatcher } = require('undici')
try {
const socks5Proxy = new Socks5Agent('socks5://localhost:1080')
// Save original dispatcher
const originalDispatcher = getGlobalDispatcher()
// Set SOCKS5 proxy as global dispatcher
setGlobalDispatcher(socks5Proxy)
// All requests now go through SOCKS5 proxy automatically
const response = await request('http://httpbin.org/ip')
console.log('Status:', response.statusCode)
const body = await response.body.json()
console.log('Response through global SOCKS5 proxy:', body)
// Restore original dispatcher
setGlobalDispatcher(originalDispatcher)
await socks5Proxy.close()
} catch (error) {
console.error('Error:', error.message)
}
}
// Run examples
async function runExamples () {
console.log('SOCKS5 Proxy Examples for Undici')
console.log('================================')
console.log('Note: These examples require a SOCKS5 proxy running on localhost:1080')
console.log('You can use tools like dante-server, shadowsocks, or SSH tunneling.\n')
await basicSocks5Example()
await authenticatedSocks5Example()
await fetchWithSocks5Example()
await httpsWithSocks5Example()
await connectionPoolingExample()
await errorHandlingExample()
await globalDispatcherExample()
console.log('\n=== All examples completed ===')
}
// Only run if this file is executed directly
if (require.main === module) {
runExamples().catch(console.error)
}
module.exports = {
basicSocks5Example,
authenticatedSocks5Example,
fetchWithSocks5Example,
httpsWithSocks5Example,
connectionPoolingExample,
errorHandlingExample,
globalDispatcherExample
}
================================================
FILE: docs/index.html
================================================
Node.js Undici
================================================
FILE: docs/package.json
================================================
{
"private": true,
"name": "@undici/documentation",
"description": "Documentation site for the `undici` package.",
"scripts": {
"serve": "docsify serve ."
},
"dependencies": {
"docsify-cli": "^4.4.4"
}
}
================================================
FILE: eslint.config.js
================================================
'use strict'
const neo = require('neostandard')
const { installedExports } = require('./lib/global')
module.exports = [
...neo({
ignores: [
'lib/llhttp',
'test/fixtures/cache-tests',
'undici-fetch.js',
'test/web-platform-tests/wpt'
],
noJsx: true,
ts: true
}),
{
rules: {
'n/prefer-node-protocol': ['error'],
'n/no-process-exit': 'error',
'@stylistic/comma-dangle': ['error', {
arrays: 'never',
objects: 'never',
imports: 'never',
exports: 'never',
functions: 'never'
}],
'@typescript-eslint/no-redeclare': 'off',
'no-restricted-globals': ['error',
...installedExports.map(name => {
return {
name,
message: `Use undici-own ${name} instead of the global.`
}
})
]
}
}
]
================================================
FILE: index-fetch.js
================================================
'use strict'
const { getGlobalDispatcher, setGlobalDispatcher } = require('./lib/global')
const EnvHttpProxyAgent = require('./lib/dispatcher/env-http-proxy-agent')
const fetchImpl = require('./lib/web/fetch').fetch
// Capture __filename at module load time for stack trace augmentation.
// This may be undefined when bundled in environments like Node.js internals.
const currentFilename = typeof __filename !== 'undefined' ? __filename : undefined
function appendFetchStackTrace (err, filename) {
if (!err || typeof err !== 'object') {
return
}
const stack = typeof err.stack === 'string' ? err.stack : ''
const normalizedFilename = filename.replace(/\\/g, '/')
if (stack && (stack.includes(filename) || stack.includes(normalizedFilename))) {
return
}
const capture = {}
Error.captureStackTrace(capture, appendFetchStackTrace)
if (!capture.stack) {
return
}
const captureLines = capture.stack.split('\n').slice(1).join('\n')
err.stack = stack ? `${stack}\n${captureLines}` : capture.stack
}
module.exports.fetch = function fetch (init, options = undefined) {
return fetchImpl(init, options).catch(err => {
if (currentFilename) {
appendFetchStackTrace(err, currentFilename)
} else if (err && typeof err === 'object') {
Error.captureStackTrace(err, module.exports.fetch)
}
throw err
})
}
module.exports.FormData = require('./lib/web/fetch/formdata').FormData
module.exports.Headers = require('./lib/web/fetch/headers').Headers
module.exports.Response = require('./lib/web/fetch/response').Response
module.exports.Request = require('./lib/web/fetch/request').Request
const { CloseEvent, ErrorEvent, MessageEvent, createFastMessageEvent } = require('./lib/web/websocket/events')
module.exports.WebSocket = require('./lib/web/websocket/websocket').WebSocket
module.exports.CloseEvent = CloseEvent
module.exports.ErrorEvent = ErrorEvent
module.exports.MessageEvent = MessageEvent
module.exports.createFastMessageEvent = createFastMessageEvent
module.exports.EventSource = require('./lib/web/eventsource/eventsource').EventSource
const api = require('./lib/api')
const Dispatcher = require('./lib/dispatcher/dispatcher')
Object.assign(Dispatcher.prototype, api)
// Expose the fetch implementation to be enabled in Node.js core via a flag
module.exports.EnvHttpProxyAgent = EnvHttpProxyAgent
module.exports.getGlobalDispatcher = getGlobalDispatcher
module.exports.setGlobalDispatcher = setGlobalDispatcher
================================================
FILE: index.d.ts
================================================
import Undici from './types/index'
export default Undici
export * from './types/index'
================================================
FILE: index.js
================================================
'use strict'
const Client = require('./lib/dispatcher/client')
const Dispatcher = require('./lib/dispatcher/dispatcher')
const Pool = require('./lib/dispatcher/pool')
const BalancedPool = require('./lib/dispatcher/balanced-pool')
const RoundRobinPool = require('./lib/dispatcher/round-robin-pool')
const Agent = require('./lib/dispatcher/agent')
const ProxyAgent = require('./lib/dispatcher/proxy-agent')
const Socks5ProxyAgent = require('./lib/dispatcher/socks5-proxy-agent')
const EnvHttpProxyAgent = require('./lib/dispatcher/env-http-proxy-agent')
const RetryAgent = require('./lib/dispatcher/retry-agent')
const H2CClient = require('./lib/dispatcher/h2c-client')
const errors = require('./lib/core/errors')
const util = require('./lib/core/util')
const { InvalidArgumentError } = errors
const api = require('./lib/api')
const buildConnector = require('./lib/core/connect')
const MockClient = require('./lib/mock/mock-client')
const { MockCallHistory, MockCallHistoryLog } = require('./lib/mock/mock-call-history')
const MockAgent = require('./lib/mock/mock-agent')
const MockPool = require('./lib/mock/mock-pool')
const SnapshotAgent = require('./lib/mock/snapshot-agent')
const mockErrors = require('./lib/mock/mock-errors')
const RetryHandler = require('./lib/handler/retry-handler')
const { getGlobalDispatcher, setGlobalDispatcher } = require('./lib/global')
const DecoratorHandler = require('./lib/handler/decorator-handler')
const RedirectHandler = require('./lib/handler/redirect-handler')
Object.assign(Dispatcher.prototype, api)
module.exports.Dispatcher = Dispatcher
module.exports.Client = Client
module.exports.Pool = Pool
module.exports.BalancedPool = BalancedPool
module.exports.RoundRobinPool = RoundRobinPool
module.exports.Agent = Agent
module.exports.ProxyAgent = ProxyAgent
module.exports.Socks5ProxyAgent = Socks5ProxyAgent
module.exports.EnvHttpProxyAgent = EnvHttpProxyAgent
module.exports.RetryAgent = RetryAgent
module.exports.H2CClient = H2CClient
module.exports.RetryHandler = RetryHandler
module.exports.DecoratorHandler = DecoratorHandler
module.exports.RedirectHandler = RedirectHandler
module.exports.interceptors = {
redirect: require('./lib/interceptor/redirect'),
responseError: require('./lib/interceptor/response-error'),
retry: require('./lib/interceptor/retry'),
dump: require('./lib/interceptor/dump'),
dns: require('./lib/interceptor/dns'),
cache: require('./lib/interceptor/cache'),
decompress: require('./lib/interceptor/decompress'),
deduplicate: require('./lib/interceptor/deduplicate')
}
module.exports.cacheStores = {
MemoryCacheStore: require('./lib/cache/memory-cache-store')
}
const SqliteCacheStore = require('./lib/cache/sqlite-cache-store')
module.exports.cacheStores.SqliteCacheStore = SqliteCacheStore
module.exports.buildConnector = buildConnector
module.exports.errors = errors
module.exports.util = {
parseHeaders: util.parseHeaders,
headerNameToString: util.headerNameToString
}
function makeDispatcher (fn) {
return (url, opts, handler) => {
if (typeof opts === 'function') {
handler = opts
opts = null
}
if (!url || (typeof url !== 'string' && typeof url !== 'object' && !(url instanceof URL))) {
throw new InvalidArgumentError('invalid url')
}
if (opts != null && typeof opts !== 'object') {
throw new InvalidArgumentError('invalid opts')
}
if (opts && opts.path != null) {
if (typeof opts.path !== 'string') {
throw new InvalidArgumentError('invalid opts.path')
}
let path = opts.path
if (!opts.path.startsWith('/')) {
path = `/${path}`
}
url = new URL(util.parseOrigin(url).origin + path)
} else {
if (!opts) {
opts = typeof url === 'object' ? url : {}
}
url = util.parseURL(url)
}
const { agent, dispatcher = getGlobalDispatcher() } = opts
if (agent) {
throw new InvalidArgumentError('unsupported opts.agent. Did you mean opts.client?')
}
return fn.call(dispatcher, {
...opts,
origin: url.origin,
path: url.search ? `${url.pathname}${url.search}` : url.pathname,
method: opts.method || (opts.body ? 'PUT' : 'GET')
}, handler)
}
}
module.exports.setGlobalDispatcher = setGlobalDispatcher
module.exports.getGlobalDispatcher = getGlobalDispatcher
const fetchImpl = require('./lib/web/fetch').fetch
// Capture __filename at module load time for stack trace augmentation.
// This may be undefined when bundled in environments like Node.js internals.
const currentFilename = typeof __filename !== 'undefined' ? __filename : undefined
function appendFetchStackTrace (err, filename) {
if (!err || typeof err !== 'object') {
return
}
const stack = typeof err.stack === 'string' ? err.stack : ''
const normalizedFilename = filename.replace(/\\/g, '/')
if (stack && (stack.includes(filename) || stack.includes(normalizedFilename))) {
return
}
const capture = {}
Error.captureStackTrace(capture, appendFetchStackTrace)
if (!capture.stack) {
return
}
const captureLines = capture.stack.split('\n').slice(1).join('\n')
err.stack = stack ? `${stack}\n${captureLines}` : capture.stack
}
module.exports.fetch = function fetch (init, options = undefined) {
return fetchImpl(init, options).catch(err => {
if (currentFilename) {
appendFetchStackTrace(err, currentFilename)
} else if (err && typeof err === 'object') {
Error.captureStackTrace(err, module.exports.fetch)
}
throw err
})
}
module.exports.Headers = require('./lib/web/fetch/headers').Headers
module.exports.Response = require('./lib/web/fetch/response').Response
module.exports.Request = require('./lib/web/fetch/request').Request
module.exports.FormData = require('./lib/web/fetch/formdata').FormData
const { setGlobalOrigin, getGlobalOrigin } = require('./lib/web/fetch/global')
module.exports.setGlobalOrigin = setGlobalOrigin
module.exports.getGlobalOrigin = getGlobalOrigin
const { CacheStorage } = require('./lib/web/cache/cachestorage')
const { kConstruct } = require('./lib/core/symbols')
module.exports.caches = new CacheStorage(kConstruct)
const { deleteCookie, getCookies, getSetCookies, setCookie, parseCookie } = require('./lib/web/cookies')
module.exports.deleteCookie = deleteCookie
module.exports.getCookies = getCookies
module.exports.getSetCookies = getSetCookies
module.exports.setCookie = setCookie
module.exports.parseCookie = parseCookie
const { parseMIMEType, serializeAMimeType } = require('./lib/web/fetch/data-url')
module.exports.parseMIMEType = parseMIMEType
module.exports.serializeAMimeType = serializeAMimeType
const { CloseEvent, ErrorEvent, MessageEvent } = require('./lib/web/websocket/events')
const { WebSocket, ping } = require('./lib/web/websocket/websocket')
module.exports.WebSocket = WebSocket
module.exports.CloseEvent = CloseEvent
module.exports.ErrorEvent = ErrorEvent
module.exports.MessageEvent = MessageEvent
module.exports.ping = ping
module.exports.WebSocketStream = require('./lib/web/websocket/stream/websocketstream').WebSocketStream
module.exports.WebSocketError = require('./lib/web/websocket/stream/websocketerror').WebSocketError
module.exports.request = makeDispatcher(api.request)
module.exports.stream = makeDispatcher(api.stream)
module.exports.pipeline = makeDispatcher(api.pipeline)
module.exports.connect = makeDispatcher(api.connect)
module.exports.upgrade = makeDispatcher(api.upgrade)
module.exports.MockClient = MockClient
module.exports.MockCallHistory = MockCallHistory
module.exports.MockCallHistoryLog = MockCallHistoryLog
module.exports.MockPool = MockPool
module.exports.MockAgent = MockAgent
module.exports.SnapshotAgent = SnapshotAgent
module.exports.mockErrors = mockErrors
const { EventSource } = require('./lib/web/eventsource/eventsource')
module.exports.EventSource = EventSource
function install () {
globalThis.fetch = module.exports.fetch
globalThis.Headers = module.exports.Headers
globalThis.Response = module.exports.Response
globalThis.Request = module.exports.Request
globalThis.FormData = module.exports.FormData
globalThis.WebSocket = module.exports.WebSocket
globalThis.CloseEvent = module.exports.CloseEvent
globalThis.ErrorEvent = module.exports.ErrorEvent
globalThis.MessageEvent = module.exports.MessageEvent
globalThis.EventSource = module.exports.EventSource
}
module.exports.install = install
================================================
FILE: lib/api/abort-signal.js
================================================
'use strict'
const { addAbortListener } = require('../core/util')
const { RequestAbortedError } = require('../core/errors')
const kListener = Symbol('kListener')
const kSignal = Symbol('kSignal')
function abort (self) {
if (self.abort) {
self.abort(self[kSignal]?.reason)
} else {
self.reason = self[kSignal]?.reason ?? new RequestAbortedError()
}
removeSignal(self)
}
function addSignal (self, signal) {
self.reason = null
self[kSignal] = null
self[kListener] = null
if (!signal) {
return
}
if (signal.aborted) {
abort(self)
return
}
self[kSignal] = signal
self[kListener] = () => {
abort(self)
}
addAbortListener(self[kSignal], self[kListener])
}
function removeSignal (self) {
if (!self[kSignal]) {
return
}
if ('removeEventListener' in self[kSignal]) {
self[kSignal].removeEventListener('abort', self[kListener])
} else {
self[kSignal].removeListener('abort', self[kListener])
}
self[kSignal] = null
self[kListener] = null
}
module.exports = {
addSignal,
removeSignal
}
================================================
FILE: lib/api/api-connect.js
================================================
'use strict'
const assert = require('node:assert')
const { AsyncResource } = require('node:async_hooks')
const { InvalidArgumentError, SocketError } = require('../core/errors')
const util = require('../core/util')
const { addSignal, removeSignal } = require('./abort-signal')
class ConnectHandler extends AsyncResource {
constructor (opts, callback) {
if (!opts || typeof opts !== 'object') {
throw new InvalidArgumentError('invalid opts')
}
if (typeof callback !== 'function') {
throw new InvalidArgumentError('invalid callback')
}
const { signal, opaque, responseHeaders } = opts
if (signal && typeof signal.on !== 'function' && typeof signal.addEventListener !== 'function') {
throw new InvalidArgumentError('signal must be an EventEmitter or EventTarget')
}
super('UNDICI_CONNECT')
this.opaque = opaque || null
this.responseHeaders = responseHeaders || null
this.callback = callback
this.abort = null
addSignal(this, signal)
}
onConnect (abort, context) {
if (this.reason) {
abort(this.reason)
return
}
assert(this.callback)
this.abort = abort
this.context = context
}
onHeaders () {
throw new SocketError('bad connect', null)
}
onUpgrade (statusCode, rawHeaders, socket) {
const { callback, opaque, context } = this
removeSignal(this)
this.callback = null
let headers = rawHeaders
// Indicates is an HTTP2Session
if (headers != null) {
headers = this.responseHeaders === 'raw' ? util.parseRawHeaders(rawHeaders) : util.parseHeaders(rawHeaders)
}
this.runInAsyncScope(callback, null, null, {
statusCode,
headers,
socket,
opaque,
context
})
}
onError (err) {
const { callback, opaque } = this
removeSignal(this)
if (callback) {
this.callback = null
queueMicrotask(() => {
this.runInAsyncScope(callback, null, err, { opaque })
})
}
}
}
function connect (opts, callback) {
if (callback === undefined) {
return new Promise((resolve, reject) => {
connect.call(this, opts, (err, data) => {
return err ? reject(err) : resolve(data)
})
})
}
try {
const connectHandler = new ConnectHandler(opts, callback)
const connectOptions = { ...opts, method: 'CONNECT' }
this.dispatch(connectOptions, connectHandler)
} catch (err) {
if (typeof callback !== 'function') {
throw err
}
const opaque = opts?.opaque
queueMicrotask(() => callback(err, { opaque }))
}
}
module.exports = connect
================================================
FILE: lib/api/api-pipeline.js
================================================
'use strict'
const {
Readable,
Duplex,
PassThrough
} = require('node:stream')
const assert = require('node:assert')
const { AsyncResource } = require('node:async_hooks')
const {
InvalidArgumentError,
InvalidReturnValueError,
RequestAbortedError
} = require('../core/errors')
const util = require('../core/util')
const { addSignal, removeSignal } = require('./abort-signal')
function noop () {}
const kResume = Symbol('resume')
class PipelineRequest extends Readable {
constructor () {
super({ autoDestroy: true })
this[kResume] = null
}
_read () {
const { [kResume]: resume } = this
if (resume) {
this[kResume] = null
resume()
}
}
_destroy (err, callback) {
this._read()
callback(err)
}
}
class PipelineResponse extends Readable {
constructor (resume) {
super({ autoDestroy: true })
this[kResume] = resume
}
_read () {
this[kResume]()
}
_destroy (err, callback) {
if (!err && !this._readableState.endEmitted) {
err = new RequestAbortedError()
}
callback(err)
}
}
class PipelineHandler extends AsyncResource {
constructor (opts, handler) {
if (!opts || typeof opts !== 'object') {
throw new InvalidArgumentError('invalid opts')
}
if (typeof handler !== 'function') {
throw new InvalidArgumentError('invalid handler')
}
const { signal, method, opaque, onInfo, responseHeaders } = opts
if (signal && typeof signal.on !== 'function' && typeof signal.addEventListener !== 'function') {
throw new InvalidArgumentError('signal must be an EventEmitter or EventTarget')
}
if (method === 'CONNECT') {
throw new InvalidArgumentError('invalid method')
}
if (onInfo && typeof onInfo !== 'function') {
throw new InvalidArgumentError('invalid onInfo callback')
}
super('UNDICI_PIPELINE')
this.opaque = opaque || null
this.responseHeaders = responseHeaders || null
this.handler = handler
this.abort = null
this.context = null
this.onInfo = onInfo || null
this.req = new PipelineRequest().on('error', noop)
this.ret = new Duplex({
readableObjectMode: opts.objectMode,
autoDestroy: true,
read: () => {
const { body } = this
if (body?.resume) {
body.resume()
}
},
write: (chunk, encoding, callback) => {
const { req } = this
if (req.push(chunk, encoding) || req._readableState.destroyed) {
callback()
} else {
req[kResume] = callback
}
},
destroy: (err, callback) => {
const { body, req, res, ret, abort } = this
if (!err && !ret._readableState.endEmitted) {
err = new RequestAbortedError()
}
if (abort && err) {
abort()
}
util.destroy(body, err)
util.destroy(req, err)
util.destroy(res, err)
removeSignal(this)
callback(err)
}
}).on('prefinish', () => {
const { req } = this
// Node < 15 does not call _final in same tick.
req.push(null)
})
this.res = null
addSignal(this, signal)
}
onConnect (abort, context) {
const { res } = this
if (this.reason) {
abort(this.reason)
return
}
assert(!res, 'pipeline cannot be retried')
this.abort = abort
this.context = context
}
onHeaders (statusCode, rawHeaders, resume) {
const { opaque, handler, context } = this
if (statusCode < 200) {
if (this.onInfo) {
const headers = this.responseHeaders === 'raw' ? util.parseRawHeaders(rawHeaders) : util.parseHeaders(rawHeaders)
this.onInfo({ statusCode, headers })
}
return
}
this.res = new PipelineResponse(resume)
let body
try {
this.handler = null
const headers = this.responseHeaders === 'raw' ? util.parseRawHeaders(rawHeaders) : util.parseHeaders(rawHeaders)
body = this.runInAsyncScope(handler, null, {
statusCode,
headers,
opaque,
body: this.res,
context
})
} catch (err) {
this.res.on('error', noop)
throw err
}
if (!body || typeof body.on !== 'function') {
throw new InvalidReturnValueError('expected Readable')
}
body
.on('data', (chunk) => {
const { ret, body } = this
if (!ret.push(chunk) && body.pause) {
body.pause()
}
})
.on('error', (err) => {
const { ret } = this
util.destroy(ret, err)
})
.on('end', () => {
const { ret } = this
ret.push(null)
})
.on('close', () => {
const { ret } = this
if (!ret._readableState.ended) {
util.destroy(ret, new RequestAbortedError())
}
})
this.body = body
}
onData (chunk) {
const { res } = this
return res.push(chunk)
}
onComplete (trailers) {
const { res } = this
res.push(null)
}
onError (err) {
const { ret } = this
this.handler = null
util.destroy(ret, err)
}
}
function pipeline (opts, handler) {
try {
const pipelineHandler = new PipelineHandler(opts, handler)
this.dispatch({ ...opts, body: pipelineHandler.req }, pipelineHandler)
return pipelineHandler.ret
} catch (err) {
return new PassThrough().destroy(err)
}
}
module.exports = pipeline
================================================
FILE: lib/api/api-request.js
================================================
'use strict'
const assert = require('node:assert')
const { AsyncResource } = require('node:async_hooks')
const { Readable } = require('./readable')
const { InvalidArgumentError, RequestAbortedError } = require('../core/errors')
const util = require('../core/util')
function noop () {}
class RequestHandler extends AsyncResource {
constructor (opts, callback) {
if (!opts || typeof opts !== 'object') {
throw new InvalidArgumentError('invalid opts')
}
const { signal, method, opaque, body, onInfo, responseHeaders, highWaterMark } = opts
try {
if (typeof callback !== 'function') {
throw new InvalidArgumentError('invalid callback')
}
if (highWaterMark && (typeof highWaterMark !== 'number' || highWaterMark < 0)) {
throw new InvalidArgumentError('invalid highWaterMark')
}
if (signal && typeof signal.on !== 'function' && typeof signal.addEventListener !== 'function') {
throw new InvalidArgumentError('signal must be an EventEmitter or EventTarget')
}
if (method === 'CONNECT') {
throw new InvalidArgumentError('invalid method')
}
if (onInfo && typeof onInfo !== 'function') {
throw new InvalidArgumentError('invalid onInfo callback')
}
super('UNDICI_REQUEST')
} catch (err) {
if (util.isStream(body)) {
util.destroy(body.on('error', noop), err)
}
throw err
}
this.method = method
this.responseHeaders = responseHeaders || null
this.opaque = opaque || null
this.callback = callback
this.res = null
this.abort = null
this.body = body
this.trailers = {}
this.context = null
this.onInfo = onInfo || null
this.highWaterMark = highWaterMark
this.reason = null
this.removeAbortListener = null
if (signal?.aborted) {
this.reason = signal.reason ?? new RequestAbortedError()
} else if (signal) {
this.removeAbortListener = util.addAbortListener(signal, () => {
this.reason = signal.reason ?? new RequestAbortedError()
if (this.res) {
util.destroy(this.res.on('error', noop), this.reason)
} else if (this.abort) {
this.abort(this.reason)
}
})
}
}
onConnect (abort, context) {
if (this.reason) {
abort(this.reason)
return
}
assert(this.callback)
this.abort = abort
this.context = context
}
onHeaders (statusCode, rawHeaders, resume, statusMessage) {
const { callback, opaque, abort, context, responseHeaders, highWaterMark } = this
const headers = responseHeaders === 'raw' ? util.parseRawHeaders(rawHeaders) : util.parseHeaders(rawHeaders)
if (statusCode < 200) {
if (this.onInfo) {
this.onInfo({ statusCode, headers })
}
return
}
const parsedHeaders = responseHeaders === 'raw' ? util.parseHeaders(rawHeaders) : headers
const contentType = parsedHeaders['content-type']
const contentLength = parsedHeaders['content-length']
const res = new Readable({
resume,
abort,
contentType,
contentLength: this.method !== 'HEAD' && contentLength
? Number(contentLength)
: null,
highWaterMark
})
if (this.removeAbortListener) {
res.on('close', this.removeAbortListener)
this.removeAbortListener = null
}
this.callback = null
this.res = res
if (callback !== null) {
try {
this.runInAsyncScope(callback, null, null, {
statusCode,
statusText: statusMessage,
headers,
trailers: this.trailers,
opaque,
body: res,
context
})
} catch (err) {
// If the callback throws synchronously, we need to handle it
// Remove reference to res to allow res being garbage collected
this.res = null
// Destroy the response stream
util.destroy(res.on('error', noop), err)
// Use queueMicrotask to re-throw the error so it reaches uncaughtException
queueMicrotask(() => {
throw err
})
}
}
}
onData (chunk) {
return this.res.push(chunk)
}
onComplete (trailers) {
util.parseHeaders(trailers, this.trailers)
this.res.push(null)
}
onError (err) {
const { res, callback, body, opaque } = this
if (callback) {
// TODO: Does this need queueMicrotask?
this.callback = null
queueMicrotask(() => {
this.runInAsyncScope(callback, null, err, { opaque })
})
}
if (res) {
this.res = null
// Ensure all queued handlers are invoked before destroying res.
queueMicrotask(() => {
util.destroy(res.on('error', noop), err)
})
}
if (body) {
this.body = null
if (util.isStream(body)) {
body.on('error', noop)
util.destroy(body, err)
}
}
if (this.removeAbortListener) {
this.removeAbortListener()
this.removeAbortListener = null
}
}
}
function request (opts, callback) {
if (callback === undefined) {
return new Promise((resolve, reject) => {
request.call(this, opts, (err, data) => {
return err ? reject(err) : resolve(data)
})
})
}
try {
const handler = new RequestHandler(opts, callback)
this.dispatch(opts, handler)
} catch (err) {
if (typeof callback !== 'function') {
throw err
}
const opaque = opts?.opaque
queueMicrotask(() => callback(err, { opaque }))
}
}
module.exports = request
module.exports.RequestHandler = RequestHandler
================================================
FILE: lib/api/api-stream.js
================================================
'use strict'
const assert = require('node:assert')
const { finished } = require('node:stream')
const { AsyncResource } = require('node:async_hooks')
const { InvalidArgumentError, InvalidReturnValueError } = require('../core/errors')
const util = require('../core/util')
const { addSignal, removeSignal } = require('./abort-signal')
function noop () {}
class StreamHandler extends AsyncResource {
constructor (opts, factory, callback) {
if (!opts || typeof opts !== 'object') {
throw new InvalidArgumentError('invalid opts')
}
const { signal, method, opaque, body, onInfo, responseHeaders } = opts
try {
if (typeof callback !== 'function') {
throw new InvalidArgumentError('invalid callback')
}
if (typeof factory !== 'function') {
throw new InvalidArgumentError('invalid factory')
}
if (signal && typeof signal.on !== 'function' && typeof signal.addEventListener !== 'function') {
throw new InvalidArgumentError('signal must be an EventEmitter or EventTarget')
}
if (method === 'CONNECT') {
throw new InvalidArgumentError('invalid method')
}
if (onInfo && typeof onInfo !== 'function') {
throw new InvalidArgumentError('invalid onInfo callback')
}
super('UNDICI_STREAM')
} catch (err) {
if (util.isStream(body)) {
util.destroy(body.on('error', noop), err)
}
throw err
}
this.responseHeaders = responseHeaders || null
this.opaque = opaque || null
this.factory = factory
this.callback = callback
this.res = null
this.abort = null
this.context = null
this.trailers = null
this.body = body
this.onInfo = onInfo || null
if (util.isStream(body)) {
body.on('error', (err) => {
this.onError(err)
})
}
addSignal(this, signal)
}
onConnect (abort, context) {
if (this.reason) {
abort(this.reason)
return
}
assert(this.callback)
this.abort = abort
this.context = context
}
onHeaders (statusCode, rawHeaders, resume, statusMessage) {
const { factory, opaque, context, responseHeaders } = this
const headers = responseHeaders === 'raw' ? util.parseRawHeaders(rawHeaders) : util.parseHeaders(rawHeaders)
if (statusCode < 200) {
if (this.onInfo) {
this.onInfo({ statusCode, headers })
}
return
}
this.factory = null
if (factory === null) {
return
}
const res = this.runInAsyncScope(factory, null, {
statusCode,
headers,
opaque,
context
})
if (
!res ||
typeof res.write !== 'function' ||
typeof res.end !== 'function' ||
typeof res.on !== 'function'
) {
throw new InvalidReturnValueError('expected Writable')
}
// TODO: Avoid finished. It registers an unnecessary amount of listeners.
finished(res, { readable: false }, (err) => {
const { callback, res, opaque, trailers, abort } = this
this.res = null
if (err || !res?.readable) {
util.destroy(res, err)
}
this.callback = null
this.runInAsyncScope(callback, null, err || null, { opaque, trailers })
if (err) {
abort()
}
})
res.on('drain', resume)
this.res = res
const needDrain = res.writableNeedDrain !== undefined
? res.writableNeedDrain
: res._writableState?.needDrain
return needDrain !== true
}
onData (chunk) {
const { res } = this
return res ? res.write(chunk) : true
}
onComplete (trailers) {
const { res } = this
removeSignal(this)
if (!res) {
return
}
this.trailers = util.parseHeaders(trailers)
res.end()
}
onError (err) {
const { res, callback, opaque, body } = this
removeSignal(this)
this.factory = null
if (res) {
this.res = null
util.destroy(res, err)
} else if (callback) {
this.callback = null
queueMicrotask(() => {
this.runInAsyncScope(callback, null, err, { opaque })
})
}
if (body) {
this.body = null
util.destroy(body, err)
}
}
}
function stream (opts, factory, callback) {
if (callback === undefined) {
return new Promise((resolve, reject) => {
stream.call(this, opts, factory, (err, data) => {
return err ? reject(err) : resolve(data)
})
})
}
try {
const handler = new StreamHandler(opts, factory, callback)
this.dispatch(opts, handler)
} catch (err) {
if (typeof callback !== 'function') {
throw err
}
const opaque = opts?.opaque
queueMicrotask(() => callback(err, { opaque }))
}
}
module.exports = stream
================================================
FILE: lib/api/api-upgrade.js
================================================
'use strict'
const { InvalidArgumentError, SocketError } = require('../core/errors')
const { AsyncResource } = require('node:async_hooks')
const assert = require('node:assert')
const util = require('../core/util')
const { kHTTP2Stream } = require('../core/symbols')
const { addSignal, removeSignal } = require('./abort-signal')
class UpgradeHandler extends AsyncResource {
constructor (opts, callback) {
if (!opts || typeof opts !== 'object') {
throw new InvalidArgumentError('invalid opts')
}
if (typeof callback !== 'function') {
throw new InvalidArgumentError('invalid callback')
}
const { signal, opaque, responseHeaders } = opts
if (signal && typeof signal.on !== 'function' && typeof signal.addEventListener !== 'function') {
throw new InvalidArgumentError('signal must be an EventEmitter or EventTarget')
}
super('UNDICI_UPGRADE')
this.responseHeaders = responseHeaders || null
this.opaque = opaque || null
this.callback = callback
this.abort = null
this.context = null
addSignal(this, signal)
}
onConnect (abort, context) {
if (this.reason) {
abort(this.reason)
return
}
assert(this.callback)
this.abort = abort
this.context = null
}
onHeaders () {
throw new SocketError('bad upgrade', null)
}
onUpgrade (statusCode, rawHeaders, socket) {
assert(socket[kHTTP2Stream] === true ? statusCode === 200 : statusCode === 101)
const { callback, opaque, context } = this
removeSignal(this)
this.callback = null
const headers = this.responseHeaders === 'raw' ? util.parseRawHeaders(rawHeaders) : util.parseHeaders(rawHeaders)
this.runInAsyncScope(callback, null, null, {
headers,
socket,
opaque,
context
})
}
onError (err) {
const { callback, opaque } = this
removeSignal(this)
if (callback) {
this.callback = null
queueMicrotask(() => {
this.runInAsyncScope(callback, null, err, { opaque })
})
}
}
}
function upgrade (opts, callback) {
if (callback === undefined) {
return new Promise((resolve, reject) => {
upgrade.call(this, opts, (err, data) => {
return err ? reject(err) : resolve(data)
})
})
}
try {
const upgradeHandler = new UpgradeHandler(opts, callback)
const upgradeOpts = {
...opts,
method: opts.method || 'GET',
upgrade: opts.protocol || 'Websocket'
}
this.dispatch(upgradeOpts, upgradeHandler)
} catch (err) {
if (typeof callback !== 'function') {
throw err
}
const opaque = opts?.opaque
queueMicrotask(() => callback(err, { opaque }))
}
}
module.exports = upgrade
================================================
FILE: lib/api/index.js
================================================
'use strict'
module.exports.request = require('./api-request')
module.exports.stream = require('./api-stream')
module.exports.pipeline = require('./api-pipeline')
module.exports.upgrade = require('./api-upgrade')
module.exports.connect = require('./api-connect')
================================================
FILE: lib/api/readable.js
================================================
'use strict'
const assert = require('node:assert')
const { Readable } = require('node:stream')
const { RequestAbortedError, NotSupportedError, InvalidArgumentError, AbortError } = require('../core/errors')
const util = require('../core/util')
const { ReadableStreamFrom } = require('../core/util')
const kConsume = Symbol('kConsume')
const kReading = Symbol('kReading')
const kBody = Symbol('kBody')
const kAbort = Symbol('kAbort')
const kContentType = Symbol('kContentType')
const kContentLength = Symbol('kContentLength')
const kUsed = Symbol('kUsed')
const kBytesRead = Symbol('kBytesRead')
const noop = () => {}
/**
* @class
* @extends {Readable}
* @see https://fetch.spec.whatwg.org/#body
*/
class BodyReadable extends Readable {
/**
* @param {object} opts
* @param {(this: Readable, size: number) => void} opts.resume
* @param {() => (void | null)} opts.abort
* @param {string} [opts.contentType = '']
* @param {number} [opts.contentLength]
* @param {number} [opts.highWaterMark = 64 * 1024]
*/
constructor ({
resume,
abort,
contentType = '',
contentLength,
highWaterMark = 64 * 1024 // Same as nodejs fs streams.
}) {
super({
autoDestroy: true,
read: resume,
highWaterMark
})
this._readableState.dataEmitted = false
this[kAbort] = abort
/** @type {Consume | null} */
this[kConsume] = null
/** @type {number} */
this[kBytesRead] = 0
/** @type {ReadableStream|null} */
this[kBody] = null
/** @type {boolean} */
this[kUsed] = false
/** @type {string} */
this[kContentType] = contentType
/** @type {number|null} */
this[kContentLength] = Number.isFinite(contentLength) ? contentLength : null
/**
* Is stream being consumed through Readable API?
* This is an optimization so that we avoid checking
* for 'data' and 'readable' listeners in the hot path
* inside push().
*
* @type {boolean}
*/
this[kReading] = false
}
/**
* @param {Error|null} err
* @param {(error:(Error|null)) => void} callback
* @returns {void}
*/
_destroy (err, callback) {
if (!err && !this._readableState.endEmitted) {
err = new RequestAbortedError()
}
if (err) {
this[kAbort]()
}
// Workaround for Node "bug". If the stream is destroyed in same
// tick as it is created, then a user who is waiting for a
// promise (i.e micro tick) for installing an 'error' listener will
// never get a chance and will always encounter an unhandled exception.
if (!this[kUsed]) {
setImmediate(callback, err)
} else {
callback(err)
}
}
/**
* @param {string|symbol} event
* @param {(...args: any[]) => void} listener
* @returns {this}
*/
on (event, listener) {
if (event === 'data' || event === 'readable') {
this[kReading] = true
this[kUsed] = true
}
return super.on(event, listener)
}
/**
* @param {string|symbol} event
* @param {(...args: any[]) => void} listener
* @returns {this}
*/
addListener (event, listener) {
return this.on(event, listener)
}
/**
* @param {string|symbol} event
* @param {(...args: any[]) => void} listener
* @returns {this}
*/
off (event, listener) {
const ret = super.off(event, listener)
if (event === 'data' || event === 'readable') {
this[kReading] = (
this.listenerCount('data') > 0 ||
this.listenerCount('readable') > 0
)
}
return ret
}
/**
* @param {string|symbol} event
* @param {(...args: any[]) => void} listener
* @returns {this}
*/
removeListener (event, listener) {
return this.off(event, listener)
}
/**
* @param {Buffer|null} chunk
* @returns {boolean}
*/
push (chunk) {
if (chunk) {
this[kBytesRead] += chunk.length
if (this[kConsume]) {
consumePush(this[kConsume], chunk)
return this[kReading] ? super.push(chunk) : true
}
}
return super.push(chunk)
}
/**
* Consumes and returns the body as a string.
*
* @see https://fetch.spec.whatwg.org/#dom-body-text
* @returns {Promise}
*/
text () {
return consume(this, 'text')
}
/**
* Consumes and returns the body as a JavaScript Object.
*
* @see https://fetch.spec.whatwg.org/#dom-body-json
* @returns {Promise}
*/
json () {
return consume(this, 'json')
}
/**
* Consumes and returns the body as a Blob
*
* @see https://fetch.spec.whatwg.org/#dom-body-blob
* @returns {Promise}
*/
blob () {
return consume(this, 'blob')
}
/**
* Consumes and returns the body as an Uint8Array.
*
* @see https://fetch.spec.whatwg.org/#dom-body-bytes
* @returns {Promise}
*/
bytes () {
return consume(this, 'bytes')
}
/**
* Consumes and returns the body as an ArrayBuffer.
*
* @see https://fetch.spec.whatwg.org/#dom-body-arraybuffer
* @returns {Promise}
*/
arrayBuffer () {
return consume(this, 'arrayBuffer')
}
/**
* Not implemented
*
* @see https://fetch.spec.whatwg.org/#dom-body-formdata
* @throws {NotSupportedError}
*/
async formData () {
// TODO: Implement.
throw new NotSupportedError()
}
/**
* Returns true if the body is not null and the body has been consumed.
* Otherwise, returns false.
*
* @see https://fetch.spec.whatwg.org/#dom-body-bodyused
* @readonly
* @returns {boolean}
*/
get bodyUsed () {
return util.isDisturbed(this)
}
/**
* @see https://fetch.spec.whatwg.org/#dom-body-body
* @readonly
* @returns {ReadableStream}
*/
get body () {
if (!this[kBody]) {
this[kBody] = ReadableStreamFrom(this)
if (this[kConsume]) {
// TODO: Is this the best way to force a lock?
this[kBody].getReader() // Ensure stream is locked.
assert(this[kBody].locked)
}
}
return this[kBody]
}
/**
* Dumps the response body by reading `limit` number of bytes.
* @param {object} opts
* @param {number} [opts.limit = 131072] Number of bytes to read.
* @param {AbortSignal} [opts.signal] An AbortSignal to cancel the dump.
* @returns {Promise}
*/
dump (opts) {
const signal = opts?.signal
if (signal != null && (typeof signal !== 'object' || !('aborted' in signal))) {
return Promise.reject(new InvalidArgumentError('signal must be an AbortSignal'))
}
const limit = opts?.limit && Number.isFinite(opts.limit)
? opts.limit
: 128 * 1024
if (signal?.aborted) {
return Promise.reject(signal.reason ?? new AbortError())
}
if (this._readableState.closeEmitted) {
return Promise.resolve(null)
}
return new Promise((resolve, reject) => {
if (
(this[kContentLength] && (this[kContentLength] > limit)) ||
this[kBytesRead] > limit
) {
this.destroy(new AbortError())
}
if (signal) {
const onAbort = () => {
this.destroy(signal.reason ?? new AbortError())
}
signal.addEventListener('abort', onAbort)
this
.on('close', function () {
signal.removeEventListener('abort', onAbort)
if (signal.aborted) {
reject(signal.reason ?? new AbortError())
} else {
resolve(null)
}
})
} else {
this.on('close', resolve)
}
this
.on('error', noop)
.on('data', () => {
if (this[kBytesRead] > limit) {
this.destroy()
}
})
.resume()
})
}
/**
* @param {BufferEncoding} encoding
* @returns {this}
*/
setEncoding (encoding) {
if (Buffer.isEncoding(encoding)) {
this._readableState.encoding = encoding
}
return this
}
}
/**
* @see https://streams.spec.whatwg.org/#readablestream-locked
* @param {BodyReadable} bodyReadable
* @returns {boolean}
*/
function isLocked (bodyReadable) {
// Consume is an implicit lock.
return bodyReadable[kBody]?.locked === true || bodyReadable[kConsume] !== null
}
/**
* @see https://fetch.spec.whatwg.org/#body-unusable
* @param {BodyReadable} bodyReadable
* @returns {boolean}
*/
function isUnusable (bodyReadable) {
return util.isDisturbed(bodyReadable) || isLocked(bodyReadable)
}
/**
* @typedef {'text' | 'json' | 'blob' | 'bytes' | 'arrayBuffer'} ConsumeType
*/
/**
* @template {ConsumeType} T
* @typedef {T extends 'text' ? string :
* T extends 'json' ? unknown :
* T extends 'blob' ? Blob :
* T extends 'arrayBuffer' ? ArrayBuffer :
* T extends 'bytes' ? Uint8Array :
* never
* } ConsumeReturnType
*/
/**
* @typedef {object} Consume
* @property {ConsumeType} type
* @property {BodyReadable} stream
* @property {((value?: any) => void)} resolve
* @property {((err: Error) => void)} reject
* @property {number} length
* @property {Buffer[]} body
*/
/**
* @template {ConsumeType} T
* @param {BodyReadable} stream
* @param {T} type
* @returns {Promise>}
*/
function consume (stream, type) {
assert(!stream[kConsume])
return new Promise((resolve, reject) => {
if (isUnusable(stream)) {
const rState = stream._readableState
if (rState.destroyed && rState.closeEmitted === false) {
stream
.on('error', reject)
.on('close', () => {
reject(new TypeError('unusable'))
})
} else {
reject(rState.errored ?? new TypeError('unusable'))
}
} else {
queueMicrotask(() => {
stream[kConsume] = {
type,
stream,
resolve,
reject,
length: 0,
body: []
}
stream
.on('error', function (err) {
consumeFinish(this[kConsume], err)
})
.on('close', function () {
if (this[kConsume].body !== null) {
consumeFinish(this[kConsume], new RequestAbortedError())
}
})
consumeStart(stream[kConsume])
})
}
})
}
/**
* @param {Consume} consume
* @returns {void}
*/
function consumeStart (consume) {
if (consume.body === null) {
return
}
const { _readableState: state } = consume.stream
if (state.bufferIndex) {
const start = state.bufferIndex
const end = state.buffer.length
for (let n = start; n < end; n++) {
consumePush(consume, state.buffer[n])
}
} else {
for (const chunk of state.buffer) {
consumePush(consume, chunk)
}
}
if (state.endEmitted) {
consumeEnd(this[kConsume], this._readableState.encoding)
} else {
consume.stream.on('end', function () {
consumeEnd(this[kConsume], this._readableState.encoding)
})
}
consume.stream.resume()
while (consume.stream.read() != null) {
// Loop
}
}
/**
* @param {Buffer[]} chunks
* @param {number} length
* @param {BufferEncoding} [encoding='utf8']
* @returns {string}
*/
function chunksDecode (chunks, length, encoding) {
if (chunks.length === 0 || length === 0) {
return ''
}
const buffer = chunks.length === 1 ? chunks[0] : Buffer.concat(chunks, length)
const bufferLength = buffer.length
// Skip BOM.
const start =
bufferLength > 2 &&
buffer[0] === 0xef &&
buffer[1] === 0xbb &&
buffer[2] === 0xbf
? 3
: 0
if (!encoding || encoding === 'utf8' || encoding === 'utf-8') {
return buffer.utf8Slice(start, bufferLength)
} else {
return buffer.subarray(start, bufferLength).toString(encoding)
}
}
/**
* @param {Buffer[]} chunks
* @param {number} length
* @returns {Uint8Array}
*/
function chunksConcat (chunks, length) {
if (chunks.length === 0 || length === 0) {
return new Uint8Array(0)
}
if (chunks.length === 1) {
// fast-path
return new Uint8Array(chunks[0])
}
const buffer = new Uint8Array(Buffer.allocUnsafeSlow(length).buffer)
let offset = 0
for (let i = 0; i < chunks.length; ++i) {
const chunk = chunks[i]
buffer.set(chunk, offset)
offset += chunk.length
}
return buffer
}
/**
* @param {Consume} consume
* @param {BufferEncoding} encoding
* @returns {void}
*/
function consumeEnd (consume, encoding) {
const { type, body, resolve, stream, length } = consume
try {
if (type === 'text') {
resolve(chunksDecode(body, length, encoding))
} else if (type === 'json') {
resolve(JSON.parse(chunksDecode(body, length, encoding)))
} else if (type === 'arrayBuffer') {
resolve(chunksConcat(body, length).buffer)
} else if (type === 'blob') {
resolve(new Blob(body, { type: stream[kContentType] }))
} else if (type === 'bytes') {
resolve(chunksConcat(body, length))
}
consumeFinish(consume)
} catch (err) {
stream.destroy(err)
}
}
/**
* @param {Consume} consume
* @param {Buffer} chunk
* @returns {void}
*/
function consumePush (consume, chunk) {
consume.length += chunk.length
consume.body.push(chunk)
}
/**
* @param {Consume} consume
* @param {Error} [err]
* @returns {void}
*/
function consumeFinish (consume, err) {
if (consume.body === null) {
return
}
if (err) {
consume.reject(err)
} else {
consume.resolve()
}
// Reset the consume object to allow for garbage collection.
consume.type = null
consume.stream = null
consume.resolve = null
consume.reject = null
consume.length = 0
consume.body = null
}
module.exports = {
Readable: BodyReadable,
chunksDecode
}
================================================
FILE: lib/cache/memory-cache-store.js
================================================
'use strict'
const { Writable } = require('node:stream')
const { EventEmitter } = require('node:events')
const { assertCacheKey, assertCacheValue } = require('../util/cache.js')
/**
* @typedef {import('../../types/cache-interceptor.d.ts').default.CacheKey} CacheKey
* @typedef {import('../../types/cache-interceptor.d.ts').default.CacheValue} CacheValue
* @typedef {import('../../types/cache-interceptor.d.ts').default.CacheStore} CacheStore
* @typedef {import('../../types/cache-interceptor.d.ts').default.GetResult} GetResult
*/
/**
* @implements {CacheStore}
* @extends {EventEmitter}
*/
class MemoryCacheStore extends EventEmitter {
#maxCount = 1024
#maxSize = 104857600 // 100MB
#maxEntrySize = 5242880 // 5MB
#size = 0
#count = 0
#entries = new Map()
#hasEmittedMaxSizeEvent = false
/**
* @param {import('../../types/cache-interceptor.d.ts').default.MemoryCacheStoreOpts | undefined} [opts]
*/
constructor (opts) {
super()
if (opts) {
if (typeof opts !== 'object') {
throw new TypeError('MemoryCacheStore options must be an object')
}
if (opts.maxCount !== undefined) {
if (
typeof opts.maxCount !== 'number' ||
!Number.isInteger(opts.maxCount) ||
opts.maxCount < 0
) {
throw new TypeError('MemoryCacheStore options.maxCount must be a non-negative integer')
}
this.#maxCount = opts.maxCount
}
if (opts.maxSize !== undefined) {
if (
typeof opts.maxSize !== 'number' ||
!Number.isInteger(opts.maxSize) ||
opts.maxSize < 0
) {
throw new TypeError('MemoryCacheStore options.maxSize must be a non-negative integer')
}
this.#maxSize = opts.maxSize
}
if (opts.maxEntrySize !== undefined) {
if (
typeof opts.maxEntrySize !== 'number' ||
!Number.isInteger(opts.maxEntrySize) ||
opts.maxEntrySize < 0
) {
throw new TypeError('MemoryCacheStore options.maxEntrySize must be a non-negative integer')
}
this.#maxEntrySize = opts.maxEntrySize
}
}
}
/**
* Get the current size of the cache in bytes
* @returns {number} The current size of the cache in bytes
*/
get size () {
return this.#size
}
/**
* Check if the cache is full (either max size or max count reached)
* @returns {boolean} True if the cache is full, false otherwise
*/
isFull () {
return this.#size >= this.#maxSize || this.#count >= this.#maxCount
}
/**
* @param {import('../../types/cache-interceptor.d.ts').default.CacheKey} req
* @returns {import('../../types/cache-interceptor.d.ts').default.GetResult | undefined}
*/
get (key) {
assertCacheKey(key)
const topLevelKey = `${key.origin}:${key.path}`
const now = Date.now()
const entries = this.#entries.get(topLevelKey)
const entry = entries ? findEntry(key, entries, now) : null
return entry == null
? undefined
: {
statusMessage: entry.statusMessage,
statusCode: entry.statusCode,
headers: entry.headers,
body: entry.body,
vary: entry.vary ? entry.vary : undefined,
etag: entry.etag,
cacheControlDirectives: entry.cacheControlDirectives,
cachedAt: entry.cachedAt,
staleAt: entry.staleAt,
deleteAt: entry.deleteAt
}
}
/**
* @param {import('../../types/cache-interceptor.d.ts').default.CacheKey} key
* @param {import('../../types/cache-interceptor.d.ts').default.CacheValue} val
* @returns {Writable | undefined}
*/
createWriteStream (key, val) {
assertCacheKey(key)
assertCacheValue(val)
const topLevelKey = `${key.origin}:${key.path}`
const store = this
const entry = { ...key, ...val, body: [], size: 0 }
return new Writable({
write (chunk, encoding, callback) {
if (typeof chunk === 'string') {
chunk = Buffer.from(chunk, encoding)
}
entry.size += chunk.byteLength
if (entry.size >= store.#maxEntrySize) {
this.destroy()
} else {
entry.body.push(chunk)
}
callback(null)
},
final (callback) {
let entries = store.#entries.get(topLevelKey)
if (!entries) {
entries = []
store.#entries.set(topLevelKey, entries)
}
const previousEntry = findEntry(key, entries, Date.now())
if (previousEntry) {
const index = entries.indexOf(previousEntry)
entries.splice(index, 1, entry)
store.#size -= previousEntry.size
} else {
entries.push(entry)
store.#count += 1
}
store.#size += entry.size
// Check if cache is full and emit event if needed
if (store.#size > store.#maxSize || store.#count > store.#maxCount) {
// Emit maxSizeExceeded event if we haven't already
if (!store.#hasEmittedMaxSizeEvent) {
store.emit('maxSizeExceeded', {
size: store.#size,
maxSize: store.#maxSize,
count: store.#count,
maxCount: store.#maxCount
})
store.#hasEmittedMaxSizeEvent = true
}
// Perform eviction
for (const [key, entries] of store.#entries) {
for (const entry of entries.splice(0, entries.length / 2)) {
store.#size -= entry.size
store.#count -= 1
}
if (entries.length === 0) {
store.#entries.delete(key)
}
}
// Reset the event flag after eviction
if (store.#size < store.#maxSize && store.#count < store.#maxCount) {
store.#hasEmittedMaxSizeEvent = false
}
}
callback(null)
}
})
}
/**
* @param {CacheKey} key
*/
delete (key) {
if (typeof key !== 'object') {
throw new TypeError(`expected key to be object, got ${typeof key}`)
}
const topLevelKey = `${key.origin}:${key.path}`
for (const entry of this.#entries.get(topLevelKey) ?? []) {
this.#size -= entry.size
this.#count -= 1
}
this.#entries.delete(topLevelKey)
}
}
function findEntry (key, entries, now) {
return entries.find((entry) => (
entry.deleteAt > now &&
entry.method === key.method &&
(entry.vary == null || Object.keys(entry.vary).every(headerName => {
if (entry.vary[headerName] === null) {
return key.headers[headerName] === undefined
}
return entry.vary[headerName] === key.headers[headerName]
}))
))
}
module.exports = MemoryCacheStore
================================================
FILE: lib/cache/sqlite-cache-store.js
================================================
'use strict'
const { Writable } = require('node:stream')
const { assertCacheKey, assertCacheValue } = require('../util/cache.js')
let DatabaseSync
const VERSION = 3
// 2gb
const MAX_ENTRY_SIZE = 2 * 1000 * 1000 * 1000
/**
* @typedef {import('../../types/cache-interceptor.d.ts').default.CacheStore} CacheStore
* @implements {CacheStore}
*
* @typedef {{
* id: Readonly,
* body?: Uint8Array
* statusCode: number
* statusMessage: string
* headers?: string
* vary?: string
* etag?: string
* cacheControlDirectives?: string
* cachedAt: number
* staleAt: number
* deleteAt: number
* }} SqliteStoreValue
*/
module.exports = class SqliteCacheStore {
#maxEntrySize = MAX_ENTRY_SIZE
#maxCount = Infinity
/**
* @type {import('node:sqlite').DatabaseSync}
*/
#db
/**
* @type {import('node:sqlite').StatementSync}
*/
#getValuesQuery
/**
* @type {import('node:sqlite').StatementSync}
*/
#updateValueQuery
/**
* @type {import('node:sqlite').StatementSync}
*/
#insertValueQuery
/**
* @type {import('node:sqlite').StatementSync}
*/
#deleteExpiredValuesQuery
/**
* @type {import('node:sqlite').StatementSync}
*/
#deleteByUrlQuery
/**
* @type {import('node:sqlite').StatementSync}
*/
#countEntriesQuery
/**
* @type {import('node:sqlite').StatementSync | null}
*/
#deleteOldValuesQuery
/**
* @param {import('../../types/cache-interceptor.d.ts').default.SqliteCacheStoreOpts | undefined} opts
*/
constructor (opts) {
if (opts) {
if (typeof opts !== 'object') {
throw new TypeError('SqliteCacheStore options must be an object')
}
if (opts.maxEntrySize !== undefined) {
if (
typeof opts.maxEntrySize !== 'number' ||
!Number.isInteger(opts.maxEntrySize) ||
opts.maxEntrySize < 0
) {
throw new TypeError('SqliteCacheStore options.maxEntrySize must be a non-negative integer')
}
if (opts.maxEntrySize > MAX_ENTRY_SIZE) {
throw new TypeError('SqliteCacheStore options.maxEntrySize must be less than 2gb')
}
this.#maxEntrySize = opts.maxEntrySize
}
if (opts.maxCount !== undefined) {
if (
typeof opts.maxCount !== 'number' ||
!Number.isInteger(opts.maxCount) ||
opts.maxCount < 0
) {
throw new TypeError('SqliteCacheStore options.maxCount must be a non-negative integer')
}
this.#maxCount = opts.maxCount
}
}
if (!DatabaseSync) {
DatabaseSync = require('node:sqlite').DatabaseSync
}
this.#db = new DatabaseSync(opts?.location ?? ':memory:')
this.#db.exec(`
PRAGMA journal_mode = WAL;
PRAGMA synchronous = NORMAL;
PRAGMA temp_store = memory;
PRAGMA optimize;
CREATE TABLE IF NOT EXISTS cacheInterceptorV${VERSION} (
-- Data specific to us
id INTEGER PRIMARY KEY AUTOINCREMENT,
url TEXT NOT NULL,
method TEXT NOT NULL,
-- Data returned to the interceptor
body BUF NULL,
deleteAt INTEGER NOT NULL,
statusCode INTEGER NOT NULL,
statusMessage TEXT NOT NULL,
headers TEXT NULL,
cacheControlDirectives TEXT NULL,
etag TEXT NULL,
vary TEXT NULL,
cachedAt INTEGER NOT NULL,
staleAt INTEGER NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_cacheInterceptorV${VERSION}_getValuesQuery ON cacheInterceptorV${VERSION}(url, method, deleteAt);
CREATE INDEX IF NOT EXISTS idx_cacheInterceptorV${VERSION}_deleteByUrlQuery ON cacheInterceptorV${VERSION}(deleteAt);
`)
this.#getValuesQuery = this.#db.prepare(`
SELECT
id,
body,
deleteAt,
statusCode,
statusMessage,
headers,
etag,
cacheControlDirectives,
vary,
cachedAt,
staleAt
FROM cacheInterceptorV${VERSION}
WHERE
url = ?
AND method = ?
ORDER BY
deleteAt ASC
`)
this.#updateValueQuery = this.#db.prepare(`
UPDATE cacheInterceptorV${VERSION} SET
body = ?,
deleteAt = ?,
statusCode = ?,
statusMessage = ?,
headers = ?,
etag = ?,
cacheControlDirectives = ?,
cachedAt = ?,
staleAt = ?
WHERE
id = ?
`)
this.#insertValueQuery = this.#db.prepare(`
INSERT INTO cacheInterceptorV${VERSION} (
url,
method,
body,
deleteAt,
statusCode,
statusMessage,
headers,
etag,
cacheControlDirectives,
vary,
cachedAt,
staleAt
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`)
this.#deleteByUrlQuery = this.#db.prepare(
`DELETE FROM cacheInterceptorV${VERSION} WHERE url = ?`
)
this.#countEntriesQuery = this.#db.prepare(
`SELECT COUNT(*) AS total FROM cacheInterceptorV${VERSION}`
)
this.#deleteExpiredValuesQuery = this.#db.prepare(
`DELETE FROM cacheInterceptorV${VERSION} WHERE deleteAt <= ?`
)
this.#deleteOldValuesQuery = this.#maxCount === Infinity
? null
: this.#db.prepare(`
DELETE FROM cacheInterceptorV${VERSION}
WHERE id IN (
SELECT
id
FROM cacheInterceptorV${VERSION}
ORDER BY cachedAt DESC
LIMIT ?
)
`)
}
close () {
this.#db.close()
}
/**
* @param {import('../../types/cache-interceptor.d.ts').default.CacheKey} key
* @returns {(import('../../types/cache-interceptor.d.ts').default.GetResult & { body?: Buffer }) | undefined}
*/
get (key) {
assertCacheKey(key)
const value = this.#findValue(key)
return value
? {
body: value.body ? Buffer.from(value.body.buffer, value.body.byteOffset, value.body.byteLength) : undefined,
statusCode: value.statusCode,
statusMessage: value.statusMessage,
headers: value.headers ? JSON.parse(value.headers) : undefined,
etag: value.etag ? value.etag : undefined,
vary: value.vary ? JSON.parse(value.vary) : undefined,
cacheControlDirectives: value.cacheControlDirectives
? JSON.parse(value.cacheControlDirectives)
: undefined,
cachedAt: value.cachedAt,
staleAt: value.staleAt,
deleteAt: value.deleteAt
}
: undefined
}
/**
* @param {import('../../types/cache-interceptor.d.ts').default.CacheKey} key
* @param {import('../../types/cache-interceptor.d.ts').default.CacheValue & { body: null | Buffer | Array}} value
*/
set (key, value) {
assertCacheKey(key)
const url = this.#makeValueUrl(key)
const body = Array.isArray(value.body) ? Buffer.concat(value.body) : value.body
const size = body?.byteLength
if (size && size > this.#maxEntrySize) {
return
}
const existingValue = this.#findValue(key, true)
if (existingValue) {
// Updating an existing response, let's overwrite it
this.#updateValueQuery.run(
body,
value.deleteAt,
value.statusCode,
value.statusMessage,
value.headers ? JSON.stringify(value.headers) : null,
value.etag ? value.etag : null,
value.cacheControlDirectives ? JSON.stringify(value.cacheControlDirectives) : null,
value.cachedAt,
value.staleAt,
existingValue.id
)
} else {
this.#prune()
// New response, let's insert it
this.#insertValueQuery.run(
url,
key.method,
body,
value.deleteAt,
value.statusCode,
value.statusMessage,
value.headers ? JSON.stringify(value.headers) : null,
value.etag ? value.etag : null,
value.cacheControlDirectives ? JSON.stringify(value.cacheControlDirectives) : null,
value.vary ? JSON.stringify(value.vary) : null,
value.cachedAt,
value.staleAt
)
}
}
/**
* @param {import('../../types/cache-interceptor.d.ts').default.CacheKey} key
* @param {import('../../types/cache-interceptor.d.ts').default.CacheValue} value
* @returns {Writable | undefined}
*/
createWriteStream (key, value) {
assertCacheKey(key)
assertCacheValue(value)
let size = 0
/**
* @type {Buffer[] | null}
*/
const body = []
const store = this
return new Writable({
decodeStrings: true,
write (chunk, encoding, callback) {
size += chunk.byteLength
if (size < store.#maxEntrySize) {
body.push(chunk)
} else {
this.destroy()
}
callback()
},
final (callback) {
store.set(key, { ...value, body })
callback()
}
})
}
/**
* @param {import('../../types/cache-interceptor.d.ts').default.CacheKey} key
*/
delete (key) {
if (typeof key !== 'object') {
throw new TypeError(`expected key to be object, got ${typeof key}`)
}
this.#deleteByUrlQuery.run(this.#makeValueUrl(key))
}
#prune () {
if (Number.isFinite(this.#maxCount) && this.size <= this.#maxCount) {
return 0
}
{
const removed = this.#deleteExpiredValuesQuery.run(Date.now()).changes
if (removed) {
return removed
}
}
{
const removed = this.#deleteOldValuesQuery?.run(Math.max(Math.floor(this.#maxCount * 0.1), 1)).changes
if (removed) {
return removed
}
}
return 0
}
/**
* Counts the number of rows in the cache
* @returns {Number}
*/
get size () {
const { total } = this.#countEntriesQuery.get()
return total
}
/**
* @param {import('../../types/cache-interceptor.d.ts').default.CacheKey} key
* @returns {string}
*/
#makeValueUrl (key) {
return `${key.origin}/${key.path}`
}
/**
* @param {import('../../types/cache-interceptor.d.ts').default.CacheKey} key
* @param {boolean} [canBeExpired=false]
* @returns {SqliteStoreValue | undefined}
*/
#findValue (key, canBeExpired = false) {
const url = this.#makeValueUrl(key)
const { headers, method } = key
/**
* @type {SqliteStoreValue[]}
*/
const values = this.#getValuesQuery.all(url, method)
if (values.length === 0) {
return undefined
}
const now = Date.now()
for (const value of values) {
if (now >= value.deleteAt && !canBeExpired) {
return undefined
}
let matches = true
if (value.vary) {
const vary = JSON.parse(value.vary)
for (const header in vary) {
if (!headerValueEquals(headers[header], vary[header])) {
matches = false
break
}
}
}
if (matches) {
return value
}
}
return undefined
}
}
/**
* @param {string|string[]|null|undefined} lhs
* @param {string|string[]|null|undefined} rhs
* @returns {boolean}
*/
function headerValueEquals (lhs, rhs) {
if (lhs == null && rhs == null) {
return true
}
if ((lhs == null && rhs != null) ||
(lhs != null && rhs == null)) {
return false
}
if (Array.isArray(lhs) && Array.isArray(rhs)) {
if (lhs.length !== rhs.length) {
return false
}
return lhs.every((x, i) => x === rhs[i])
}
return lhs === rhs
}
================================================
FILE: lib/core/connect.js
================================================
'use strict'
const net = require('node:net')
const assert = require('node:assert')
const util = require('./util')
const { InvalidArgumentError } = require('./errors')
let tls // include tls conditionally since it is not always available
// TODO: session re-use does not wait for the first
// connection to resolve the session and might therefore
// resolve the same servername multiple times even when
// re-use is enabled.
const SessionCache = class WeakSessionCache {
constructor (maxCachedSessions) {
this._maxCachedSessions = maxCachedSessions
this._sessionCache = new Map()
this._sessionRegistry = new FinalizationRegistry((key) => {
if (this._sessionCache.size < this._maxCachedSessions) {
return
}
const ref = this._sessionCache.get(key)
if (ref !== undefined && ref.deref() === undefined) {
this._sessionCache.delete(key)
}
})
}
get (sessionKey) {
const ref = this._sessionCache.get(sessionKey)
return ref ? ref.deref() : null
}
set (sessionKey, session) {
if (this._maxCachedSessions === 0) {
return
}
this._sessionCache.set(sessionKey, new WeakRef(session))
this._sessionRegistry.register(session, sessionKey)
}
}
function buildConnector ({ allowH2, useH2c, maxCachedSessions, socketPath, timeout, session: customSession, ...opts }) {
if (maxCachedSessions != null && (!Number.isInteger(maxCachedSessions) || maxCachedSessions < 0)) {
throw new InvalidArgumentError('maxCachedSessions must be a positive integer or zero')
}
const options = { path: socketPath, ...opts }
const sessionCache = new SessionCache(maxCachedSessions == null ? 100 : maxCachedSessions)
timeout = timeout == null ? 10e3 : timeout
allowH2 = allowH2 != null ? allowH2 : false
return function connect ({ hostname, host, protocol, port, servername, localAddress, httpSocket }, callback) {
let socket
if (protocol === 'https:') {
if (!tls) {
tls = require('node:tls')
}
servername = servername || options.servername || util.getServerName(host) || null
const sessionKey = servername || hostname
assert(sessionKey)
const session = customSession || sessionCache.get(sessionKey) || null
port = port || 443
socket = tls.connect({
highWaterMark: 16384, // TLS in node can't have bigger HWM anyway...
...options,
servername,
session,
localAddress,
ALPNProtocols: allowH2 ? ['http/1.1', 'h2'] : ['http/1.1'],
socket: httpSocket, // upgrade socket connection
port,
host: hostname
})
socket
.on('session', function (session) {
// TODO (fix): Can a session become invalid once established? Don't think so?
sessionCache.set(sessionKey, session)
})
} else {
assert(!httpSocket, 'httpSocket can only be sent on TLS update')
port = port || 80
socket = net.connect({
highWaterMark: 64 * 1024, // Same as nodejs fs streams.
...options,
localAddress,
port,
host: hostname
})
if (useH2c === true) {
socket.alpnProtocol = 'h2'
}
}
// Set TCP keep alive options on the socket here instead of in connect() for the case of assigning the socket
if (options.keepAlive == null || options.keepAlive) {
const keepAliveInitialDelay = options.keepAliveInitialDelay === undefined ? 60e3 : options.keepAliveInitialDelay
socket.setKeepAlive(true, keepAliveInitialDelay)
}
const clearConnectTimeout = util.setupConnectTimeout(new WeakRef(socket), { timeout, hostname, port })
socket
.setNoDelay(true)
.once(protocol === 'https:' ? 'secureConnect' : 'connect', function () {
queueMicrotask(clearConnectTimeout)
if (callback) {
const cb = callback
callback = null
cb(null, this)
}
})
.on('error', function (err) {
queueMicrotask(clearConnectTimeout)
if (callback) {
const cb = callback
callback = null
cb(err)
}
})
return socket
}
}
module.exports = buildConnector
================================================
FILE: lib/core/constants.js
================================================
'use strict'
/**
* @see https://developer.mozilla.org/docs/Web/HTTP/Headers
*/
const wellknownHeaderNames = /** @type {const} */ ([
'Accept',
'Accept-Encoding',
'Accept-Language',
'Accept-Ranges',
'Access-Control-Allow-Credentials',
'Access-Control-Allow-Headers',
'Access-Control-Allow-Methods',
'Access-Control-Allow-Origin',
'Access-Control-Expose-Headers',
'Access-Control-Max-Age',
'Access-Control-Request-Headers',
'Access-Control-Request-Method',
'Age',
'Allow',
'Alt-Svc',
'Alt-Used',
'Authorization',
'Cache-Control',
'Clear-Site-Data',
'Connection',
'Content-Disposition',
'Content-Encoding',
'Content-Language',
'Content-Length',
'Content-Location',
'Content-Range',
'Content-Security-Policy',
'Content-Security-Policy-Report-Only',
'Content-Type',
'Cookie',
'Cross-Origin-Embedder-Policy',
'Cross-Origin-Opener-Policy',
'Cross-Origin-Resource-Policy',
'Date',
'Device-Memory',
'Downlink',
'ECT',
'ETag',
'Expect',
'Expect-CT',
'Expires',
'Forwarded',
'From',
'Host',
'If-Match',
'If-Modified-Since',
'If-None-Match',
'If-Range',
'If-Unmodified-Since',
'Keep-Alive',
'Last-Modified',
'Link',
'Location',
'Max-Forwards',
'Origin',
'Permissions-Policy',
'Pragma',
'Proxy-Authenticate',
'Proxy-Authorization',
'RTT',
'Range',
'Referer',
'Referrer-Policy',
'Refresh',
'Retry-After',
'Sec-WebSocket-Accept',
'Sec-WebSocket-Extensions',
'Sec-WebSocket-Key',
'Sec-WebSocket-Protocol',
'Sec-WebSocket-Version',
'Server',
'Server-Timing',
'Service-Worker-Allowed',
'Service-Worker-Navigation-Preload',
'Set-Cookie',
'SourceMap',
'Strict-Transport-Security',
'Supports-Loading-Mode',
'TE',
'Timing-Allow-Origin',
'Trailer',
'Transfer-Encoding',
'Upgrade',
'Upgrade-Insecure-Requests',
'User-Agent',
'Vary',
'Via',
'WWW-Authenticate',
'X-Content-Type-Options',
'X-DNS-Prefetch-Control',
'X-Frame-Options',
'X-Permitted-Cross-Domain-Policies',
'X-Powered-By',
'X-Requested-With',
'X-XSS-Protection'
])
/** @type {Record, string>} */
const headerNameLowerCasedRecord = {}
// Note: object prototypes should not be able to be referenced. e.g. `Object#hasOwnProperty`.
Object.setPrototypeOf(headerNameLowerCasedRecord, null)
/**
* @type {Record, Buffer>}
*/
const wellknownHeaderNameBuffers = {}
// Note: object prototypes should not be able to be referenced. e.g. `Object#hasOwnProperty`.
Object.setPrototypeOf(wellknownHeaderNameBuffers, null)
/**
* @param {string} header Lowercased header
* @returns {Buffer}
*/
function getHeaderNameAsBuffer (header) {
let buffer = wellknownHeaderNameBuffers[header]
if (buffer === undefined) {
buffer = Buffer.from(header)
}
return buffer
}
for (let i = 0; i < wellknownHeaderNames.length; ++i) {
const key = wellknownHeaderNames[i]
const lowerCasedKey = key.toLowerCase()
headerNameLowerCasedRecord[key] = headerNameLowerCasedRecord[lowerCasedKey] =
lowerCasedKey
}
module.exports = {
wellknownHeaderNames,
headerNameLowerCasedRecord,
getHeaderNameAsBuffer
}
================================================
FILE: lib/core/diagnostics.js
================================================
'use strict'
const diagnosticsChannel = require('node:diagnostics_channel')
const util = require('node:util')
const undiciDebugLog = util.debuglog('undici')
const fetchDebuglog = util.debuglog('fetch')
const websocketDebuglog = util.debuglog('websocket')
const channels = {
// Client
beforeConnect: diagnosticsChannel.channel('undici:client:beforeConnect'),
connected: diagnosticsChannel.channel('undici:client:connected'),
connectError: diagnosticsChannel.channel('undici:client:connectError'),
sendHeaders: diagnosticsChannel.channel('undici:client:sendHeaders'),
// Request
create: diagnosticsChannel.channel('undici:request:create'),
bodySent: diagnosticsChannel.channel('undici:request:bodySent'),
bodyChunkSent: diagnosticsChannel.channel('undici:request:bodyChunkSent'),
bodyChunkReceived: diagnosticsChannel.channel('undici:request:bodyChunkReceived'),
headers: diagnosticsChannel.channel('undici:request:headers'),
trailers: diagnosticsChannel.channel('undici:request:trailers'),
error: diagnosticsChannel.channel('undici:request:error'),
// WebSocket
open: diagnosticsChannel.channel('undici:websocket:open'),
close: diagnosticsChannel.channel('undici:websocket:close'),
socketError: diagnosticsChannel.channel('undici:websocket:socket_error'),
ping: diagnosticsChannel.channel('undici:websocket:ping'),
pong: diagnosticsChannel.channel('undici:websocket:pong'),
// ProxyAgent
proxyConnected: diagnosticsChannel.channel('undici:proxy:connected')
}
let isTrackingClientEvents = false
function trackClientEvents (debugLog = undiciDebugLog) {
if (isTrackingClientEvents) {
return
}
// Check if any of the channels already have subscribers to prevent duplicate subscriptions
// This can happen when both Node.js built-in undici and undici as a dependency are present
if (channels.beforeConnect.hasSubscribers || channels.connected.hasSubscribers ||
channels.connectError.hasSubscribers || channels.sendHeaders.hasSubscribers) {
isTrackingClientEvents = true
return
}
isTrackingClientEvents = true
diagnosticsChannel.subscribe('undici:client:beforeConnect',
evt => {
const {
connectParams: { version, protocol, port, host }
} = evt
debugLog(
'connecting to %s%s using %s%s',
host,
port ? `:${port}` : '',
protocol,
version
)
})
diagnosticsChannel.subscribe('undici:client:connected',
evt => {
const {
connectParams: { version, protocol, port, host }
} = evt
debugLog(
'connected to %s%s using %s%s',
host,
port ? `:${port}` : '',
protocol,
version
)
})
diagnosticsChannel.subscribe('undici:client:connectError',
evt => {
const {
connectParams: { version, protocol, port, host },
error
} = evt
debugLog(
'connection to %s%s using %s%s errored - %s',
host,
port ? `:${port}` : '',
protocol,
version,
error.message
)
})
diagnosticsChannel.subscribe('undici:client:sendHeaders',
evt => {
const {
request: { method, path, origin }
} = evt
debugLog('sending request to %s %s%s', method, origin, path)
})
}
let isTrackingRequestEvents = false
function trackRequestEvents (debugLog = undiciDebugLog) {
if (isTrackingRequestEvents) {
return
}
// Check if any of the channels already have subscribers to prevent duplicate subscriptions
// This can happen when both Node.js built-in undici and undici as a dependency are present
if (channels.headers.hasSubscribers || channels.trailers.hasSubscribers ||
channels.error.hasSubscribers) {
isTrackingRequestEvents = true
return
}
isTrackingRequestEvents = true
diagnosticsChannel.subscribe('undici:request:headers',
evt => {
const {
request: { method, path, origin },
response: { statusCode }
} = evt
debugLog(
'received response to %s %s%s - HTTP %d',
method,
origin,
path,
statusCode
)
})
diagnosticsChannel.subscribe('undici:request:trailers',
evt => {
const {
request: { method, path, origin }
} = evt
debugLog('trailers received from %s %s%s', method, origin, path)
})
diagnosticsChannel.subscribe('undici:request:error',
evt => {
const {
request: { method, path, origin },
error
} = evt
debugLog(
'request to %s %s%s errored - %s',
method,
origin,
path,
error.message
)
})
}
let isTrackingWebSocketEvents = false
function trackWebSocketEvents (debugLog = websocketDebuglog) {
if (isTrackingWebSocketEvents) {
return
}
// Check if any of the channels already have subscribers to prevent duplicate subscriptions
// This can happen when both Node.js built-in undici and undici as a dependency are present
if (channels.open.hasSubscribers || channels.close.hasSubscribers ||
channels.socketError.hasSubscribers || channels.ping.hasSubscribers ||
channels.pong.hasSubscribers) {
isTrackingWebSocketEvents = true
return
}
isTrackingWebSocketEvents = true
diagnosticsChannel.subscribe('undici:websocket:open',
evt => {
const {
address: { address, port }
} = evt
debugLog('connection opened %s%s', address, port ? `:${port}` : '')
})
diagnosticsChannel.subscribe('undici:websocket:close',
evt => {
const { websocket, code, reason } = evt
debugLog(
'closed connection to %s - %s %s',
websocket.url,
code,
reason
)
})
diagnosticsChannel.subscribe('undici:websocket:socket_error',
err => {
debugLog('connection errored - %s', err.message)
})
diagnosticsChannel.subscribe('undici:websocket:ping',
evt => {
debugLog('ping received')
})
diagnosticsChannel.subscribe('undici:websocket:pong',
evt => {
debugLog('pong received')
})
}
if (undiciDebugLog.enabled || fetchDebuglog.enabled) {
trackClientEvents(fetchDebuglog.enabled ? fetchDebuglog : undiciDebugLog)
trackRequestEvents(fetchDebuglog.enabled ? fetchDebuglog : undiciDebugLog)
}
if (websocketDebuglog.enabled) {
trackClientEvents(undiciDebugLog.enabled ? undiciDebugLog : websocketDebuglog)
trackWebSocketEvents(websocketDebuglog)
}
module.exports = {
channels
}
================================================
FILE: lib/core/errors.js
================================================
'use strict'
const kUndiciError = Symbol.for('undici.error.UND_ERR')
class UndiciError extends Error {
constructor (message, options) {
super(message, options)
this.name = 'UndiciError'
this.code = 'UND_ERR'
}
static [Symbol.hasInstance] (instance) {
return instance && instance[kUndiciError] === true
}
get [kUndiciError] () {
return true
}
}
const kConnectTimeoutError = Symbol.for('undici.error.UND_ERR_CONNECT_TIMEOUT')
class ConnectTimeoutError extends UndiciError {
constructor (message) {
super(message)
this.name = 'ConnectTimeoutError'
this.message = message || 'Connect Timeout Error'
this.code = 'UND_ERR_CONNECT_TIMEOUT'
}
static [Symbol.hasInstance] (instance) {
return instance && instance[kConnectTimeoutError] === true
}
get [kConnectTimeoutError] () {
return true
}
}
const kHeadersTimeoutError = Symbol.for('undici.error.UND_ERR_HEADERS_TIMEOUT')
class HeadersTimeoutError extends UndiciError {
constructor (message) {
super(message)
this.name = 'HeadersTimeoutError'
this.message = message || 'Headers Timeout Error'
this.code = 'UND_ERR_HEADERS_TIMEOUT'
}
static [Symbol.hasInstance] (instance) {
return instance && instance[kHeadersTimeoutError] === true
}
get [kHeadersTimeoutError] () {
return true
}
}
const kHeadersOverflowError = Symbol.for('undici.error.UND_ERR_HEADERS_OVERFLOW')
class HeadersOverflowError extends UndiciError {
constructor (message) {
super(message)
this.name = 'HeadersOverflowError'
this.message = message || 'Headers Overflow Error'
this.code = 'UND_ERR_HEADERS_OVERFLOW'
}
static [Symbol.hasInstance] (instance) {
return instance && instance[kHeadersOverflowError] === true
}
get [kHeadersOverflowError] () {
return true
}
}
const kBodyTimeoutError = Symbol.for('undici.error.UND_ERR_BODY_TIMEOUT')
class BodyTimeoutError extends UndiciError {
constructor (message) {
super(message)
this.name = 'BodyTimeoutError'
this.message = message || 'Body Timeout Error'
this.code = 'UND_ERR_BODY_TIMEOUT'
}
static [Symbol.hasInstance] (instance) {
return instance && instance[kBodyTimeoutError] === true
}
get [kBodyTimeoutError] () {
return true
}
}
const kInvalidArgumentError = Symbol.for('undici.error.UND_ERR_INVALID_ARG')
class InvalidArgumentError extends UndiciError {
constructor (message) {
super(message)
this.name = 'InvalidArgumentError'
this.message = message || 'Invalid Argument Error'
this.code = 'UND_ERR_INVALID_ARG'
}
static [Symbol.hasInstance] (instance) {
return instance && instance[kInvalidArgumentError] === true
}
get [kInvalidArgumentError] () {
return true
}
}
const kInvalidReturnValueError = Symbol.for('undici.error.UND_ERR_INVALID_RETURN_VALUE')
class InvalidReturnValueError extends UndiciError {
constructor (message) {
super(message)
this.name = 'InvalidReturnValueError'
this.message = message || 'Invalid Return Value Error'
this.code = 'UND_ERR_INVALID_RETURN_VALUE'
}
static [Symbol.hasInstance] (instance) {
return instance && instance[kInvalidReturnValueError] === true
}
get [kInvalidReturnValueError] () {
return true
}
}
const kAbortError = Symbol.for('undici.error.UND_ERR_ABORT')
class AbortError extends UndiciError {
constructor (message) {
super(message)
this.name = 'AbortError'
this.message = message || 'The operation was aborted'
this.code = 'UND_ERR_ABORT'
}
static [Symbol.hasInstance] (instance) {
return instance && instance[kAbortError] === true
}
get [kAbortError] () {
return true
}
}
const kRequestAbortedError = Symbol.for('undici.error.UND_ERR_ABORTED')
class RequestAbortedError extends AbortError {
constructor (message) {
super(message)
this.name = 'AbortError'
this.message = message || 'Request aborted'
this.code = 'UND_ERR_ABORTED'
}
static [Symbol.hasInstance] (instance) {
return instance && instance[kRequestAbortedError] === true
}
get [kRequestAbortedError] () {
return true
}
}
const kInformationalError = Symbol.for('undici.error.UND_ERR_INFO')
class InformationalError extends UndiciError {
constructor (message) {
super(message)
this.name = 'InformationalError'
this.message = message || 'Request information'
this.code = 'UND_ERR_INFO'
}
static [Symbol.hasInstance] (instance) {
return instance && instance[kInformationalError] === true
}
get [kInformationalError] () {
return true
}
}
const kRequestContentLengthMismatchError = Symbol.for('undici.error.UND_ERR_REQ_CONTENT_LENGTH_MISMATCH')
class RequestContentLengthMismatchError extends UndiciError {
constructor (message) {
super(message)
this.name = 'RequestContentLengthMismatchError'
this.message = message || 'Request body length does not match content-length header'
this.code = 'UND_ERR_REQ_CONTENT_LENGTH_MISMATCH'
}
static [Symbol.hasInstance] (instance) {
return instance && instance[kRequestContentLengthMismatchError] === true
}
get [kRequestContentLengthMismatchError] () {
return true
}
}
const kResponseContentLengthMismatchError = Symbol.for('undici.error.UND_ERR_RES_CONTENT_LENGTH_MISMATCH')
class ResponseContentLengthMismatchError extends UndiciError {
constructor (message) {
super(message)
this.name = 'ResponseContentLengthMismatchError'
this.message = message || 'Response body length does not match content-length header'
this.code = 'UND_ERR_RES_CONTENT_LENGTH_MISMATCH'
}
static [Symbol.hasInstance] (instance) {
return instance && instance[kResponseContentLengthMismatchError] === true
}
get [kResponseContentLengthMismatchError] () {
return true
}
}
const kClientDestroyedError = Symbol.for('undici.error.UND_ERR_DESTROYED')
class ClientDestroyedError extends UndiciError {
constructor (message) {
super(message)
this.name = 'ClientDestroyedError'
this.message = message || 'The client is destroyed'
this.code = 'UND_ERR_DESTROYED'
}
static [Symbol.hasInstance] (instance) {
return instance && instance[kClientDestroyedError] === true
}
get [kClientDestroyedError] () {
return true
}
}
const kClientClosedError = Symbol.for('undici.error.UND_ERR_CLOSED')
class ClientClosedError extends UndiciError {
constructor (message) {
super(message)
this.name = 'ClientClosedError'
this.message = message || 'The client is closed'
this.code = 'UND_ERR_CLOSED'
}
static [Symbol.hasInstance] (instance) {
return instance && instance[kClientClosedError] === true
}
get [kClientClosedError] () {
return true
}
}
const kSocketError = Symbol.for('undici.error.UND_ERR_SOCKET')
class SocketError extends UndiciError {
constructor (message, socket) {
super(message)
this.name = 'SocketError'
this.message = message || 'Socket error'
this.code = 'UND_ERR_SOCKET'
this.socket = socket
}
static [Symbol.hasInstance] (instance) {
return instance && instance[kSocketError] === true
}
get [kSocketError] () {
return true
}
}
const kNotSupportedError = Symbol.for('undici.error.UND_ERR_NOT_SUPPORTED')
class NotSupportedError extends UndiciError {
constructor (message) {
super(message)
this.name = 'NotSupportedError'
this.message = message || 'Not supported error'
this.code = 'UND_ERR_NOT_SUPPORTED'
}
static [Symbol.hasInstance] (instance) {
return instance && instance[kNotSupportedError] === true
}
get [kNotSupportedError] () {
return true
}
}
const kBalancedPoolMissingUpstreamError = Symbol.for('undici.error.UND_ERR_BPL_MISSING_UPSTREAM')
class BalancedPoolMissingUpstreamError extends UndiciError {
constructor (message) {
super(message)
this.name = 'MissingUpstreamError'
this.message = message || 'No upstream has been added to the BalancedPool'
this.code = 'UND_ERR_BPL_MISSING_UPSTREAM'
}
static [Symbol.hasInstance] (instance) {
return instance && instance[kBalancedPoolMissingUpstreamError] === true
}
get [kBalancedPoolMissingUpstreamError] () {
return true
}
}
const kHTTPParserError = Symbol.for('undici.error.UND_ERR_HTTP_PARSER')
class HTTPParserError extends Error {
constructor (message, code, data) {
super(message)
this.name = 'HTTPParserError'
this.code = code ? `HPE_${code}` : undefined
this.data = data ? data.toString() : undefined
}
static [Symbol.hasInstance] (instance) {
return instance && instance[kHTTPParserError] === true
}
get [kHTTPParserError] () {
return true
}
}
const kResponseExceededMaxSizeError = Symbol.for('undici.error.UND_ERR_RES_EXCEEDED_MAX_SIZE')
class ResponseExceededMaxSizeError extends UndiciError {
constructor (message) {
super(message)
this.name = 'ResponseExceededMaxSizeError'
this.message = message || 'Response content exceeded max size'
this.code = 'UND_ERR_RES_EXCEEDED_MAX_SIZE'
}
static [Symbol.hasInstance] (instance) {
return instance && instance[kResponseExceededMaxSizeError] === true
}
get [kResponseExceededMaxSizeError] () {
return true
}
}
const kRequestRetryError = Symbol.for('undici.error.UND_ERR_REQ_RETRY')
class RequestRetryError extends UndiciError {
constructor (message, code, { headers, data }) {
super(message)
this.name = 'RequestRetryError'
this.message = message || 'Request retry error'
this.code = 'UND_ERR_REQ_RETRY'
this.statusCode = code
this.data = data
this.headers = headers
}
static [Symbol.hasInstance] (instance) {
return instance && instance[kRequestRetryError] === true
}
get [kRequestRetryError] () {
return true
}
}
const kResponseError = Symbol.for('undici.error.UND_ERR_RESPONSE')
class ResponseError extends UndiciError {
constructor (message, code, { headers, body }) {
super(message)
this.name = 'ResponseError'
this.message = message || 'Response error'
this.code = 'UND_ERR_RESPONSE'
this.statusCode = code
this.body = body
this.headers = headers
}
static [Symbol.hasInstance] (instance) {
return instance && instance[kResponseError] === true
}
get [kResponseError] () {
return true
}
}
const kSecureProxyConnectionError = Symbol.for('undici.error.UND_ERR_PRX_TLS')
class SecureProxyConnectionError extends UndiciError {
constructor (cause, message, options = {}) {
super(message, { cause, ...options })
this.name = 'SecureProxyConnectionError'
this.message = message || 'Secure Proxy Connection failed'
this.code = 'UND_ERR_PRX_TLS'
this.cause = cause
}
static [Symbol.hasInstance] (instance) {
return instance && instance[kSecureProxyConnectionError] === true
}
get [kSecureProxyConnectionError] () {
return true
}
}
const kMaxOriginsReachedError = Symbol.for('undici.error.UND_ERR_MAX_ORIGINS_REACHED')
class MaxOriginsReachedError extends UndiciError {
constructor (message) {
super(message)
this.name = 'MaxOriginsReachedError'
this.message = message || 'Maximum allowed origins reached'
this.code = 'UND_ERR_MAX_ORIGINS_REACHED'
}
static [Symbol.hasInstance] (instance) {
return instance && instance[kMaxOriginsReachedError] === true
}
get [kMaxOriginsReachedError] () {
return true
}
}
class Socks5ProxyError extends UndiciError {
constructor (message, code) {
super(message)
this.name = 'Socks5ProxyError'
this.message = message || 'SOCKS5 proxy error'
this.code = code || 'UND_ERR_SOCKS5'
}
}
const kMessageSizeExceededError = Symbol.for('undici.error.UND_ERR_WS_MESSAGE_SIZE_EXCEEDED')
class MessageSizeExceededError extends UndiciError {
constructor (message) {
super(message)
this.name = 'MessageSizeExceededError'
this.message = message || 'Max decompressed message size exceeded'
this.code = 'UND_ERR_WS_MESSAGE_SIZE_EXCEEDED'
}
static [Symbol.hasInstance] (instance) {
return instance && instance[kMessageSizeExceededError] === true
}
get [kMessageSizeExceededError] () {
return true
}
}
module.exports = {
AbortError,
HTTPParserError,
UndiciError,
HeadersTimeoutError,
HeadersOverflowError,
BodyTimeoutError,
RequestContentLengthMismatchError,
ConnectTimeoutError,
InvalidArgumentError,
InvalidReturnValueError,
RequestAbortedError,
ClientDestroyedError,
ClientClosedError,
InformationalError,
SocketError,
NotSupportedError,
ResponseContentLengthMismatchError,
BalancedPoolMissingUpstreamError,
ResponseExceededMaxSizeError,
RequestRetryError,
ResponseError,
SecureProxyConnectionError,
MaxOriginsReachedError,
Socks5ProxyError,
MessageSizeExceededError
}
================================================
FILE: lib/core/request.js
================================================
'use strict'
const {
InvalidArgumentError,
NotSupportedError
} = require('./errors')
const assert = require('node:assert')
const {
isValidHTTPToken,
isValidHeaderValue,
isStream,
destroy,
isBuffer,
isFormDataLike,
isIterable,
hasSafeIterator,
isBlobLike,
serializePathWithQuery,
assertRequestHandler,
getServerName,
normalizedMethodRecords,
getProtocolFromUrlString
} = require('./util')
const { channels } = require('./diagnostics.js')
const { headerNameLowerCasedRecord } = require('./constants')
// Verifies that a given path is valid does not contain control chars \x00 to \x20
const invalidPathRegex = /[^\u0021-\u00ff]/
const kHandler = Symbol('handler')
class Request {
constructor (origin, {
path,
method,
body,
headers,
query,
idempotent,
blocking,
upgrade,
headersTimeout,
bodyTimeout,
reset,
expectContinue,
servername,
throwOnError,
maxRedirections,
typeOfService
}, handler) {
if (typeof path !== 'string') {
throw new InvalidArgumentError('path must be a string')
} else if (
path[0] !== '/' &&
!(path.startsWith('http://') || path.startsWith('https://')) &&
method !== 'CONNECT'
) {
throw new InvalidArgumentError('path must be an absolute URL or start with a slash')
} else if (invalidPathRegex.test(path)) {
throw new InvalidArgumentError('invalid request path')
}
if (typeof method !== 'string') {
throw new InvalidArgumentError('method must be a string')
} else if (normalizedMethodRecords[method] === undefined && !isValidHTTPToken(method)) {
throw new InvalidArgumentError('invalid request method')
}
if (upgrade && typeof upgrade !== 'string') {
throw new InvalidArgumentError('upgrade must be a string')
}
if (upgrade && !isValidHeaderValue(upgrade)) {
throw new InvalidArgumentError('invalid upgrade header')
}
if (headersTimeout != null && (!Number.isFinite(headersTimeout) || headersTimeout < 0)) {
throw new InvalidArgumentError('invalid headersTimeout')
}
if (bodyTimeout != null && (!Number.isFinite(bodyTimeout) || bodyTimeout < 0)) {
throw new InvalidArgumentError('invalid bodyTimeout')
}
if (reset != null && typeof reset !== 'boolean') {
throw new InvalidArgumentError('invalid reset')
}
if (expectContinue != null && typeof expectContinue !== 'boolean') {
throw new InvalidArgumentError('invalid expectContinue')
}
if (throwOnError != null) {
throw new InvalidArgumentError('invalid throwOnError')
}
if (maxRedirections != null && maxRedirections !== 0) {
throw new InvalidArgumentError('maxRedirections is not supported, use the redirect interceptor')
}
if (typeOfService != null && (!Number.isInteger(typeOfService) || typeOfService < 0 || typeOfService > 255)) {
throw new InvalidArgumentError('typeOfService must be an integer between 0 and 255')
}
this.headersTimeout = headersTimeout
this.bodyTimeout = bodyTimeout
this.method = method
this.typeOfService = typeOfService ?? 0
this.abort = null
if (body == null) {
this.body = null
} else if (isStream(body)) {
this.body = body
const rState = this.body._readableState
if (!rState || !rState.autoDestroy) {
this.endHandler = function autoDestroy () {
destroy(this)
}
this.body.on('end', this.endHandler)
}
this.errorHandler = err => {
if (this.abort) {
this.abort(err)
} else {
this.error = err
}
}
this.body.on('error', this.errorHandler)
} else if (isBuffer(body)) {
this.body = body.byteLength ? body : null
} else if (ArrayBuffer.isView(body)) {
this.body = body.buffer.byteLength ? Buffer.from(body.buffer, body.byteOffset, body.byteLength) : null
} else if (body instanceof ArrayBuffer) {
this.body = body.byteLength ? Buffer.from(body) : null
} else if (typeof body === 'string') {
this.body = body.length ? Buffer.from(body) : null
} else if (isFormDataLike(body) || isIterable(body) || isBlobLike(body)) {
this.body = body
} else {
throw new InvalidArgumentError('body must be a string, a Buffer, a Readable stream, an iterable, or an async iterable')
}
this.completed = false
this.aborted = false
this.upgrade = upgrade || null
this.path = query ? serializePathWithQuery(path, query) : path
// TODO: shall we maybe standardize it to an URL object?
this.origin = origin
this.protocol = getProtocolFromUrlString(origin)
this.idempotent = idempotent == null
? method === 'HEAD' || method === 'GET'
: idempotent
this.blocking = blocking ?? this.method !== 'HEAD'
this.reset = reset == null ? null : reset
this.host = null
this.contentLength = null
this.contentType = null
this.headers = []
// Only for H2
this.expectContinue = expectContinue != null ? expectContinue : false
if (Array.isArray(headers)) {
if (headers.length % 2 !== 0) {
throw new InvalidArgumentError('headers array must be even')
}
for (let i = 0; i < headers.length; i += 2) {
processHeader(this, headers[i], headers[i + 1])
}
} else if (headers && typeof headers === 'object') {
if (hasSafeIterator(headers)) {
for (const header of headers) {
if (!Array.isArray(header) || header.length !== 2) {
throw new InvalidArgumentError('headers must be in key-value pair format')
}
processHeader(this, header[0], header[1])
}
} else {
const keys = Object.keys(headers)
for (let i = 0; i < keys.length; ++i) {
processHeader(this, keys[i], headers[keys[i]])
}
}
} else if (headers != null) {
throw new InvalidArgumentError('headers must be an object or an array')
}
assertRequestHandler(handler, method, upgrade)
this.servername = servername || getServerName(this.host) || null
this[kHandler] = handler
if (channels.create.hasSubscribers) {
channels.create.publish({ request: this })
}
}
onBodySent (chunk) {
if (channels.bodyChunkSent.hasSubscribers) {
channels.bodyChunkSent.publish({ request: this, chunk })
}
if (this[kHandler].onBodySent) {
try {
return this[kHandler].onBodySent(chunk)
} catch (err) {
this.abort(err)
}
}
}
onRequestSent () {
if (channels.bodySent.hasSubscribers) {
channels.bodySent.publish({ request: this })
}
if (this[kHandler].onRequestSent) {
try {
return this[kHandler].onRequestSent()
} catch (err) {
this.abort(err)
}
}
}
onConnect (abort) {
assert(!this.aborted)
assert(!this.completed)
if (this.error) {
abort(this.error)
} else {
this.abort = abort
return this[kHandler].onConnect(abort)
}
}
onResponseStarted () {
return this[kHandler].onResponseStarted?.()
}
onHeaders (statusCode, headers, resume, statusText) {
assert(!this.aborted)
assert(!this.completed)
if (channels.headers.hasSubscribers) {
channels.headers.publish({ request: this, response: { statusCode, headers, statusText } })
}
try {
return this[kHandler].onHeaders(statusCode, headers, resume, statusText)
} catch (err) {
this.abort(err)
}
}
onData (chunk) {
assert(!this.aborted)
assert(!this.completed)
if (channels.bodyChunkReceived.hasSubscribers) {
channels.bodyChunkReceived.publish({ request: this, chunk })
}
try {
return this[kHandler].onData(chunk)
} catch (err) {
this.abort(err)
return false
}
}
onUpgrade (statusCode, headers, socket) {
assert(!this.aborted)
assert(!this.completed)
return this[kHandler].onUpgrade(statusCode, headers, socket)
}
onComplete (trailers) {
this.onFinally()
assert(!this.aborted)
assert(!this.completed)
this.completed = true
if (channels.trailers.hasSubscribers) {
channels.trailers.publish({ request: this, trailers })
}
try {
return this[kHandler].onComplete(trailers)
} catch (err) {
// TODO (fix): This might be a bad idea?
this.onError(err)
}
}
onError (error) {
this.onFinally()
if (channels.error.hasSubscribers) {
channels.error.publish({ request: this, error })
}
if (this.aborted) {
return
}
this.aborted = true
return this[kHandler].onError(error)
}
onFinally () {
if (this.errorHandler) {
this.body.off('error', this.errorHandler)
this.errorHandler = null
}
if (this.endHandler) {
this.body.off('end', this.endHandler)
this.endHandler = null
}
}
addHeader (key, value) {
processHeader(this, key, value)
return this
}
}
function processHeader (request, key, val) {
if (val && (typeof val === 'object' && !Array.isArray(val))) {
throw new InvalidArgumentError(`invalid ${key} header`)
} else if (val === undefined) {
return
}
let headerName = headerNameLowerCasedRecord[key]
if (headerName === undefined) {
headerName = key.toLowerCase()
if (headerNameLowerCasedRecord[headerName] === undefined && !isValidHTTPToken(headerName)) {
throw new InvalidArgumentError('invalid header key')
}
}
if (Array.isArray(val)) {
const arr = []
for (let i = 0; i < val.length; i++) {
if (typeof val[i] === 'string') {
if (!isValidHeaderValue(val[i])) {
throw new InvalidArgumentError(`invalid ${key} header`)
}
arr.push(val[i])
} else if (val[i] === null) {
arr.push('')
} else if (typeof val[i] === 'object') {
throw new InvalidArgumentError(`invalid ${key} header`)
} else {
arr.push(`${val[i]}`)
}
}
val = arr
} else if (typeof val === 'string') {
if (!isValidHeaderValue(val)) {
throw new InvalidArgumentError(`invalid ${key} header`)
}
} else if (val === null) {
val = ''
} else {
val = `${val}`
}
if (headerName === 'host') {
if (request.host !== null) {
throw new InvalidArgumentError('duplicate host header')
}
if (typeof val !== 'string') {
throw new InvalidArgumentError('invalid host header')
}
// Consumed by Client
request.host = val
} else if (headerName === 'content-length') {
if (request.contentLength !== null) {
throw new InvalidArgumentError('duplicate content-length header')
}
request.contentLength = parseInt(val, 10)
if (!Number.isFinite(request.contentLength)) {
throw new InvalidArgumentError('invalid content-length header')
}
} else if (request.contentType === null && headerName === 'content-type') {
request.contentType = val
request.headers.push(key, val)
} else if (headerName === 'transfer-encoding' || headerName === 'keep-alive' || headerName === 'upgrade') {
throw new InvalidArgumentError(`invalid ${headerName} header`)
} else if (headerName === 'connection') {
const value = typeof val === 'string' ? val.toLowerCase() : null
if (value !== 'close' && value !== 'keep-alive') {
throw new InvalidArgumentError('invalid connection header')
}
if (value === 'close') {
request.reset = true
}
} else if (headerName === 'expect') {
throw new NotSupportedError('expect header not supported')
} else {
request.headers.push(key, val)
}
}
module.exports = Request
================================================
FILE: lib/core/socks5-client.js
================================================
'use strict'
const { EventEmitter } = require('node:events')
const { Buffer } = require('node:buffer')
const { InvalidArgumentError, Socks5ProxyError } = require('./errors')
const { debuglog } = require('node:util')
const { parseAddress } = require('./socks5-utils')
const debug = debuglog('undici:socks5')
// SOCKS5 constants
const SOCKS_VERSION = 0x05
// Authentication methods
const AUTH_METHODS = {
NO_AUTH: 0x00,
GSSAPI: 0x01,
USERNAME_PASSWORD: 0x02,
NO_ACCEPTABLE: 0xFF
}
// SOCKS5 commands
const COMMANDS = {
CONNECT: 0x01,
BIND: 0x02,
UDP_ASSOCIATE: 0x03
}
// Address types
const ADDRESS_TYPES = {
IPV4: 0x01,
DOMAIN: 0x03,
IPV6: 0x04
}
// Reply codes
const REPLY_CODES = {
SUCCEEDED: 0x00,
GENERAL_FAILURE: 0x01,
CONNECTION_NOT_ALLOWED: 0x02,
NETWORK_UNREACHABLE: 0x03,
HOST_UNREACHABLE: 0x04,
CONNECTION_REFUSED: 0x05,
TTL_EXPIRED: 0x06,
COMMAND_NOT_SUPPORTED: 0x07,
ADDRESS_TYPE_NOT_SUPPORTED: 0x08
}
// State machine states
const STATES = {
INITIAL: 'initial',
HANDSHAKING: 'handshaking',
AUTHENTICATING: 'authenticating',
CONNECTING: 'connecting',
CONNECTED: 'connected',
ERROR: 'error',
CLOSED: 'closed'
}
/**
* SOCKS5 client implementation
* Handles SOCKS5 protocol negotiation and connection establishment
*/
class Socks5Client extends EventEmitter {
constructor (socket, options = {}) {
super()
if (!socket) {
throw new InvalidArgumentError('socket is required')
}
this.socket = socket
this.options = options
this.state = STATES.INITIAL
this.buffer = Buffer.alloc(0)
// Authentication settings
this.authMethods = []
if (options.username && options.password) {
this.authMethods.push(AUTH_METHODS.USERNAME_PASSWORD)
}
this.authMethods.push(AUTH_METHODS.NO_AUTH)
// Socket event handlers
this.socket.on('data', this.onData.bind(this))
this.socket.on('error', this.onError.bind(this))
this.socket.on('close', this.onClose.bind(this))
}
/**
* Handle incoming data from the socket
*/
onData (data) {
debug('received data', data.length, 'bytes in state', this.state)
this.buffer = Buffer.concat([this.buffer, data])
try {
switch (this.state) {
case STATES.HANDSHAKING:
this.handleHandshakeResponse()
break
case STATES.AUTHENTICATING:
this.handleAuthResponse()
break
case STATES.CONNECTING:
this.handleConnectResponse()
break
}
} catch (err) {
this.onError(err)
}
}
/**
* Handle socket errors
*/
onError (err) {
debug('socket error', err)
this.state = STATES.ERROR
this.emit('error', err)
this.destroy()
}
/**
* Handle socket close
*/
onClose () {
debug('socket closed')
this.state = STATES.CLOSED
this.emit('close')
}
/**
* Destroy the client and underlying socket
*/
destroy () {
if (this.socket && !this.socket.destroyed) {
this.socket.destroy()
}
}
/**
* Start the SOCKS5 handshake
*/
handshake () {
if (this.state !== STATES.INITIAL) {
throw new InvalidArgumentError('Handshake already started')
}
debug('starting handshake with', this.authMethods.length, 'auth methods')
this.state = STATES.HANDSHAKING
// Build handshake request
// +----+----------+----------+
// |VER | NMETHODS | METHODS |
// +----+----------+----------+
// | 1 | 1 | 1 to 255 |
// +----+----------+----------+
const request = Buffer.alloc(2 + this.authMethods.length)
request[0] = SOCKS_VERSION
request[1] = this.authMethods.length
this.authMethods.forEach((method, i) => {
request[2 + i] = method
})
this.socket.write(request)
}
/**
* Handle handshake response from server
*/
handleHandshakeResponse () {
if (this.buffer.length < 2) {
return // Not enough data yet
}
const version = this.buffer[0]
const method = this.buffer[1]
if (version !== SOCKS_VERSION) {
throw new Socks5ProxyError(`Invalid SOCKS version: ${version}`, 'UND_ERR_SOCKS5_VERSION')
}
if (method === AUTH_METHODS.NO_ACCEPTABLE) {
throw new Socks5ProxyError('No acceptable authentication method', 'UND_ERR_SOCKS5_AUTH_REJECTED')
}
this.buffer = this.buffer.subarray(2)
debug('server selected auth method', method)
if (method === AUTH_METHODS.NO_AUTH) {
this.emit('authenticated')
} else if (method === AUTH_METHODS.USERNAME_PASSWORD) {
this.state = STATES.AUTHENTICATING
this.sendAuthRequest()
} else {
throw new Socks5ProxyError(`Unsupported authentication method: ${method}`, 'UND_ERR_SOCKS5_AUTH_METHOD')
}
}
/**
* Send username/password authentication request
*/
sendAuthRequest () {
const { username, password } = this.options
if (!username || !password) {
throw new InvalidArgumentError('Username and password required for authentication')
}
debug('sending username/password auth')
// Username/Password authentication request (RFC 1929)
// +----+------+----------+------+----------+
// |VER | ULEN | UNAME | PLEN | PASSWD |
// +----+------+----------+------+----------+
// | 1 | 1 | 1 to 255 | 1 | 1 to 255 |
// +----+------+----------+------+----------+
const usernameBuffer = Buffer.from(username)
const passwordBuffer = Buffer.from(password)
if (usernameBuffer.length > 255 || passwordBuffer.length > 255) {
throw new InvalidArgumentError('Username or password too long')
}
const request = Buffer.alloc(3 + usernameBuffer.length + passwordBuffer.length)
request[0] = 0x01 // Sub-negotiation version
request[1] = usernameBuffer.length
usernameBuffer.copy(request, 2)
request[2 + usernameBuffer.length] = passwordBuffer.length
passwordBuffer.copy(request, 3 + usernameBuffer.length)
this.socket.write(request)
}
/**
* Handle authentication response
*/
handleAuthResponse () {
if (this.buffer.length < 2) {
return // Not enough data yet
}
const version = this.buffer[0]
const status = this.buffer[1]
if (version !== 0x01) {
throw new Socks5ProxyError(`Invalid auth sub-negotiation version: ${version}`, 'UND_ERR_SOCKS5_AUTH_VERSION')
}
if (status !== 0x00) {
throw new Socks5ProxyError('Authentication failed', 'UND_ERR_SOCKS5_AUTH_FAILED')
}
this.buffer = this.buffer.subarray(2)
debug('authentication successful')
this.emit('authenticated')
}
/**
* Send CONNECT command
* @param {string} address - Target address (IP or domain)
* @param {number} port - Target port
*/
connect (address, port) {
if (this.state === STATES.CONNECTED) {
throw new InvalidArgumentError('Already connected')
}
debug('connecting to', address, port)
this.state = STATES.CONNECTING
const request = this.buildConnectRequest(COMMANDS.CONNECT, address, port)
this.socket.write(request)
}
/**
* Build a SOCKS5 request
*/
buildConnectRequest (command, address, port) {
// Parse address to determine type and buffer
const { type: addressType, buffer: addressBuffer } = parseAddress(address)
// Build request
// +----+-----+-------+------+----------+----------+
// |VER | CMD | RSV | ATYP | DST.ADDR | DST.PORT |
// +----+-----+-------+------+----------+----------+
// | 1 | 1 | X'00' | 1 | Variable | 2 |
// +----+-----+-------+------+----------+----------+
const request = Buffer.alloc(4 + addressBuffer.length + 2)
request[0] = SOCKS_VERSION
request[1] = command
request[2] = 0x00 // Reserved
request[3] = addressType
addressBuffer.copy(request, 4)
request.writeUInt16BE(port, 4 + addressBuffer.length)
return request
}
/**
* Handle CONNECT response
*/
handleConnectResponse () {
if (this.buffer.length < 4) {
return // Not enough data for header
}
const version = this.buffer[0]
const reply = this.buffer[1]
const addressType = this.buffer[3]
if (version !== SOCKS_VERSION) {
throw new Socks5ProxyError(`Invalid SOCKS version in reply: ${version}`, 'UND_ERR_SOCKS5_REPLY_VERSION')
}
// Calculate the expected response length
let responseLength = 4 // VER + REP + RSV + ATYP
if (addressType === ADDRESS_TYPES.IPV4) {
responseLength += 4 + 2 // IPv4 + port
} else if (addressType === ADDRESS_TYPES.DOMAIN) {
if (this.buffer.length < 5) {
return // Need domain length byte
}
responseLength += 1 + this.buffer[4] + 2 // length byte + domain + port
} else if (addressType === ADDRESS_TYPES.IPV6) {
responseLength += 16 + 2 // IPv6 + port
} else {
throw new Socks5ProxyError(`Invalid address type in reply: ${addressType}`, 'UND_ERR_SOCKS5_ADDR_TYPE')
}
if (this.buffer.length < responseLength) {
return // Not enough data for full response
}
if (reply !== REPLY_CODES.SUCCEEDED) {
const errorMessage = this.getReplyErrorMessage(reply)
throw new Socks5ProxyError(`SOCKS5 connection failed: ${errorMessage}`, `UND_ERR_SOCKS5_REPLY_${reply}`)
}
// Parse bound address and port
let boundAddress
let offset = 4
if (addressType === ADDRESS_TYPES.IPV4) {
boundAddress = Array.from(this.buffer.subarray(offset, offset + 4)).join('.')
offset += 4
} else if (addressType === ADDRESS_TYPES.DOMAIN) {
const domainLength = this.buffer[offset]
offset += 1
boundAddress = this.buffer.subarray(offset, offset + domainLength).toString()
offset += domainLength
} else if (addressType === ADDRESS_TYPES.IPV6) {
// Parse IPv6 address from 16-byte buffer
const parts = []
for (let i = 0; i < 8; i++) {
const value = this.buffer.readUInt16BE(offset + i * 2)
parts.push(value.toString(16))
}
boundAddress = parts.join(':')
offset += 16
}
const boundPort = this.buffer.readUInt16BE(offset)
this.buffer = this.buffer.subarray(responseLength)
this.state = STATES.CONNECTED
debug('connected, bound address:', boundAddress, 'port:', boundPort)
this.emit('connected', { address: boundAddress, port: boundPort })
}
/**
* Get human-readable error message for reply code
*/
getReplyErrorMessage (reply) {
switch (reply) {
case REPLY_CODES.GENERAL_FAILURE:
return 'General SOCKS server failure'
case REPLY_CODES.CONNECTION_NOT_ALLOWED:
return 'Connection not allowed by ruleset'
case REPLY_CODES.NETWORK_UNREACHABLE:
return 'Network unreachable'
case REPLY_CODES.HOST_UNREACHABLE:
return 'Host unreachable'
case REPLY_CODES.CONNECTION_REFUSED:
return 'Connection refused'
case REPLY_CODES.TTL_EXPIRED:
return 'TTL expired'
case REPLY_CODES.COMMAND_NOT_SUPPORTED:
return 'Command not supported'
case REPLY_CODES.ADDRESS_TYPE_NOT_SUPPORTED:
return 'Address type not supported'
default:
return `Unknown error code: ${reply}`
}
}
}
module.exports = {
Socks5Client,
AUTH_METHODS,
COMMANDS,
ADDRESS_TYPES,
REPLY_CODES,
STATES
}
================================================
FILE: lib/core/socks5-utils.js
================================================
'use strict'
const { Buffer } = require('node:buffer')
const net = require('node:net')
const { InvalidArgumentError } = require('./errors')
/**
* Parse an address and determine its type
* @param {string} address - The address to parse
* @returns {{type: number, buffer: Buffer}} Address type and buffer
*/
function parseAddress (address) {
// Check if it's an IPv4 address
if (net.isIPv4(address)) {
const parts = address.split('.').map(Number)
return {
type: 0x01, // IPv4
buffer: Buffer.from(parts)
}
}
// Check if it's an IPv6 address
if (net.isIPv6(address)) {
return {
type: 0x04, // IPv6
buffer: parseIPv6(address)
}
}
// Otherwise, treat as domain name
const domainBuffer = Buffer.from(address, 'utf8')
if (domainBuffer.length > 255) {
throw new InvalidArgumentError('Domain name too long (max 255 bytes)')
}
return {
type: 0x03, // Domain
buffer: Buffer.concat([Buffer.from([domainBuffer.length]), domainBuffer])
}
}
/**
* Parse IPv6 address to buffer
* @param {string} address - IPv6 address string
* @returns {Buffer} 16-byte buffer
*/
function parseIPv6 (address) {
const buffer = Buffer.alloc(16)
const parts = address.split(':')
let partIndex = 0
let bufferIndex = 0
// Handle compressed notation (::)
const doubleColonIndex = address.indexOf('::')
if (doubleColonIndex !== -1) {
// Count non-empty parts
const nonEmptyParts = parts.filter(p => p.length > 0).length
const skipParts = 8 - nonEmptyParts
for (let i = 0; i < parts.length; i++) {
if (parts[i] === '' && i === doubleColonIndex / 3) {
// Skip empty parts for ::
bufferIndex += skipParts * 2
} else if (parts[i] !== '') {
const value = parseInt(parts[i], 16)
buffer.writeUInt16BE(value, bufferIndex)
bufferIndex += 2
}
}
} else {
// No compression, parse normally
for (const part of parts) {
if (part === '') continue
const value = parseInt(part, 16)
buffer.writeUInt16BE(value, partIndex * 2)
partIndex++
}
}
return buffer
}
/**
* Build a SOCKS5 address buffer
* @param {number} type - Address type (1=IPv4, 3=Domain, 4=IPv6)
* @param {Buffer} addressBuffer - The address data
* @param {number} port - Port number
* @returns {Buffer} Complete address buffer including type, address, and port
*/
function buildAddressBuffer (type, addressBuffer, port) {
const portBuffer = Buffer.allocUnsafe(2)
portBuffer.writeUInt16BE(port, 0)
return Buffer.concat([
Buffer.from([type]),
addressBuffer,
portBuffer
])
}
/**
* Parse address from SOCKS5 response
* @param {Buffer} buffer - Buffer containing the address
* @param {number} offset - Starting offset in buffer
* @returns {{address: string, port: number, bytesRead: number}}
*/
function parseResponseAddress (buffer, offset = 0) {
if (buffer.length < offset + 1) {
throw new InvalidArgumentError('Buffer too small to contain address type')
}
const addressType = buffer[offset]
let address
let currentOffset = offset + 1
switch (addressType) {
case 0x01: { // IPv4
if (buffer.length < currentOffset + 6) {
throw new InvalidArgumentError('Buffer too small for IPv4 address')
}
address = Array.from(buffer.subarray(currentOffset, currentOffset + 4)).join('.')
currentOffset += 4
break
}
case 0x03: { // Domain
if (buffer.length < currentOffset + 1) {
throw new InvalidArgumentError('Buffer too small for domain length')
}
const domainLength = buffer[currentOffset]
currentOffset += 1
if (buffer.length < currentOffset + domainLength + 2) {
throw new InvalidArgumentError('Buffer too small for domain address')
}
address = buffer.subarray(currentOffset, currentOffset + domainLength).toString('utf8')
currentOffset += domainLength
break
}
case 0x04: { // IPv6
if (buffer.length < currentOffset + 18) {
throw new InvalidArgumentError('Buffer too small for IPv6 address')
}
// Convert buffer to IPv6 string
const parts = []
for (let i = 0; i < 8; i++) {
const value = buffer.readUInt16BE(currentOffset + i * 2)
parts.push(value.toString(16))
}
address = parts.join(':')
currentOffset += 16
break
}
default:
throw new InvalidArgumentError(`Invalid address type: ${addressType}`)
}
// Parse port
if (buffer.length < currentOffset + 2) {
throw new InvalidArgumentError('Buffer too small for port')
}
const port = buffer.readUInt16BE(currentOffset)
currentOffset += 2
return {
address,
port,
bytesRead: currentOffset - offset
}
}
/**
* Create error for SOCKS5 reply code
* @param {number} replyCode - SOCKS5 reply code
* @returns {Error} Appropriate error object
*/
function createReplyError (replyCode) {
const messages = {
0x01: 'General SOCKS server failure',
0x02: 'Connection not allowed by ruleset',
0x03: 'Network unreachable',
0x04: 'Host unreachable',
0x05: 'Connection refused',
0x06: 'TTL expired',
0x07: 'Command not supported',
0x08: 'Address type not supported'
}
const message = messages[replyCode] || `Unknown SOCKS5 error code: ${replyCode}`
const error = new Error(message)
error.code = `SOCKS5_${replyCode}`
return error
}
module.exports = {
parseAddress,
parseIPv6,
buildAddressBuffer,
parseResponseAddress,
createReplyError
}
================================================
FILE: lib/core/symbols.js
================================================
'use strict'
module.exports = {
kClose: Symbol('close'),
kDestroy: Symbol('destroy'),
kDispatch: Symbol('dispatch'),
kUrl: Symbol('url'),
kWriting: Symbol('writing'),
kResuming: Symbol('resuming'),
kQueue: Symbol('queue'),
kConnect: Symbol('connect'),
kConnecting: Symbol('connecting'),
kKeepAliveDefaultTimeout: Symbol('default keep alive timeout'),
kKeepAliveMaxTimeout: Symbol('max keep alive timeout'),
kKeepAliveTimeoutThreshold: Symbol('keep alive timeout threshold'),
kKeepAliveTimeoutValue: Symbol('keep alive timeout'),
kKeepAlive: Symbol('keep alive'),
kHeadersTimeout: Symbol('headers timeout'),
kBodyTimeout: Symbol('body timeout'),
kServerName: Symbol('server name'),
kLocalAddress: Symbol('local address'),
kHost: Symbol('host'),
kNoRef: Symbol('no ref'),
kBodyUsed: Symbol('used'),
kBody: Symbol('abstracted request body'),
kRunning: Symbol('running'),
kBlocking: Symbol('blocking'),
kPending: Symbol('pending'),
kSize: Symbol('size'),
kBusy: Symbol('busy'),
kQueued: Symbol('queued'),
kFree: Symbol('free'),
kConnected: Symbol('connected'),
kClosed: Symbol('closed'),
kNeedDrain: Symbol('need drain'),
kReset: Symbol('reset'),
kDestroyed: Symbol.for('nodejs.stream.destroyed'),
kResume: Symbol('resume'),
kOnError: Symbol('on error'),
kMaxHeadersSize: Symbol('max headers size'),
kRunningIdx: Symbol('running index'),
kPendingIdx: Symbol('pending index'),
kError: Symbol('error'),
kClients: Symbol('clients'),
kClient: Symbol('client'),
kParser: Symbol('parser'),
kOnDestroyed: Symbol('destroy callbacks'),
kPipelining: Symbol('pipelining'),
kSocket: Symbol('socket'),
kHostHeader: Symbol('host header'),
kConnector: Symbol('connector'),
kStrictContentLength: Symbol('strict content length'),
kMaxRedirections: Symbol('maxRedirections'),
kMaxRequests: Symbol('maxRequestsPerClient'),
kProxy: Symbol('proxy agent options'),
kCounter: Symbol('socket request counter'),
kMaxResponseSize: Symbol('max response size'),
kHTTP2Session: Symbol('http2Session'),
kHTTP2SessionState: Symbol('http2Session state'),
kRetryHandlerDefaultRetry: Symbol('retry agent default retry'),
kConstruct: Symbol('constructable'),
kListeners: Symbol('listeners'),
kHTTPContext: Symbol('http context'),
kMaxConcurrentStreams: Symbol('max concurrent streams'),
kHTTP2InitialWindowSize: Symbol('http2 initial window size'),
kHTTP2ConnectionWindowSize: Symbol('http2 connection window size'),
kEnableConnectProtocol: Symbol('http2session connect protocol'),
kRemoteSettings: Symbol('http2session remote settings'),
kHTTP2Stream: Symbol('http2session client stream'),
kPingInterval: Symbol('ping interval'),
kNoProxyAgent: Symbol('no proxy agent'),
kHttpProxyAgent: Symbol('http proxy agent'),
kHttpsProxyAgent: Symbol('https proxy agent'),
kSocks5ProxyAgent: Symbol('socks5 proxy agent')
}
================================================
FILE: lib/core/tree.js
================================================
'use strict'
const {
wellknownHeaderNames,
headerNameLowerCasedRecord
} = require('./constants')
class TstNode {
/** @type {any} */
value = null
/** @type {null | TstNode} */
left = null
/** @type {null | TstNode} */
middle = null
/** @type {null | TstNode} */
right = null
/** @type {number} */
code
/**
* @param {string} key
* @param {any} value
* @param {number} index
*/
constructor (key, value, index) {
if (index === undefined || index >= key.length) {
throw new TypeError('Unreachable')
}
const code = this.code = key.charCodeAt(index)
// check code is ascii string
if (code > 0x7F) {
throw new TypeError('key must be ascii string')
}
if (key.length !== ++index) {
this.middle = new TstNode(key, value, index)
} else {
this.value = value
}
}
/**
* @param {string} key
* @param {any} value
* @returns {void}
*/
add (key, value) {
const length = key.length
if (length === 0) {
throw new TypeError('Unreachable')
}
let index = 0
/**
* @type {TstNode}
*/
let node = this
while (true) {
const code = key.charCodeAt(index)
// check code is ascii string
if (code > 0x7F) {
throw new TypeError('key must be ascii string')
}
if (node.code === code) {
if (length === ++index) {
node.value = value
break
} else if (node.middle !== null) {
node = node.middle
} else {
node.middle = new TstNode(key, value, index)
break
}
} else if (node.code < code) {
if (node.left !== null) {
node = node.left
} else {
node.left = new TstNode(key, value, index)
break
}
} else if (node.right !== null) {
node = node.right
} else {
node.right = new TstNode(key, value, index)
break
}
}
}
/**
* @param {Uint8Array} key
* @returns {TstNode | null}
*/
search (key) {
const keylength = key.length
let index = 0
/**
* @type {TstNode|null}
*/
let node = this
while (node !== null && index < keylength) {
let code = key[index]
// A-Z
// First check if it is bigger than 0x5a.
// Lowercase letters have higher char codes than uppercase ones.
// Also we assume that headers will mostly contain lowercase characters.
if (code <= 0x5a && code >= 0x41) {
// Lowercase for uppercase.
code |= 32
}
while (node !== null) {
if (code === node.code) {
if (keylength === ++index) {
// Returns Node since it is the last key.
return node
}
node = node.middle
break
}
node = node.code < code ? node.left : node.right
}
}
return null
}
}
class TernarySearchTree {
/** @type {TstNode | null} */
node = null
/**
* @param {string} key
* @param {any} value
* @returns {void}
* */
insert (key, value) {
if (this.node === null) {
this.node = new TstNode(key, value, 0)
} else {
this.node.add(key, value)
}
}
/**
* @param {Uint8Array} key
* @returns {any}
*/
lookup (key) {
return this.node?.search(key)?.value ?? null
}
}
const tree = new TernarySearchTree()
for (let i = 0; i < wellknownHeaderNames.length; ++i) {
const key = headerNameLowerCasedRecord[wellknownHeaderNames[i]]
tree.insert(key, key)
}
module.exports = {
TernarySearchTree,
tree
}
================================================
FILE: lib/core/util.js
================================================
'use strict'
const assert = require('node:assert')
const { kDestroyed, kBodyUsed, kListeners, kBody } = require('./symbols')
const { IncomingMessage } = require('node:http')
const stream = require('node:stream')
const net = require('node:net')
const { stringify } = require('node:querystring')
const { EventEmitter: EE } = require('node:events')
const timers = require('../util/timers')
const { InvalidArgumentError, ConnectTimeoutError } = require('./errors')
const { headerNameLowerCasedRecord } = require('./constants')
const { tree } = require('./tree')
const [nodeMajor, nodeMinor] = process.versions.node.split('.', 2).map(v => Number(v))
class BodyAsyncIterable {
constructor (body) {
this[kBody] = body
this[kBodyUsed] = false
}
async * [Symbol.asyncIterator] () {
assert(!this[kBodyUsed], 'disturbed')
this[kBodyUsed] = true
yield * this[kBody]
}
}
function noop () {}
/**
* @param {*} body
* @returns {*}
*/
function wrapRequestBody (body) {
if (isStream(body)) {
// TODO (fix): Provide some way for the user to cache the file to e.g. /tmp
// so that it can be dispatched again?
// TODO (fix): Do we need 100-expect support to provide a way to do this properly?
if (bodyLength(body) === 0) {
body
.on('data', function () {
assert(false)
})
}
if (typeof body.readableDidRead !== 'boolean') {
body[kBodyUsed] = false
EE.prototype.on.call(body, 'data', function () {
this[kBodyUsed] = true
})
}
return body
} else if (body && typeof body.pipeTo === 'function') {
// TODO (fix): We can't access ReadableStream internal state
// to determine whether or not it has been disturbed. This is just
// a workaround.
return new BodyAsyncIterable(body)
} else if (body && isFormDataLike(body)) {
return body
} else if (
body &&
typeof body !== 'string' &&
!ArrayBuffer.isView(body) &&
isIterable(body)
) {
// TODO: Should we allow re-using iterable if !this.opts.idempotent
// or through some other flag?
return new BodyAsyncIterable(body)
} else {
return body
}
}
/**
* @param {*} obj
* @returns {obj is import('node:stream').Stream}
*/
function isStream (obj) {
return obj && typeof obj === 'object' && typeof obj.pipe === 'function' && typeof obj.on === 'function'
}
/**
* @param {*} object
* @returns {object is Blob}
* based on https://github.com/node-fetch/fetch-blob/blob/8ab587d34080de94140b54f07168451e7d0b655e/index.js#L229-L241 (MIT License)
*/
function isBlobLike (object) {
if (object === null) {
return false
} else if (object instanceof Blob) {
return true
} else if (typeof object !== 'object') {
return false
} else {
const sTag = object[Symbol.toStringTag]
return (sTag === 'Blob' || sTag === 'File') && (
('stream' in object && typeof object.stream === 'function') ||
('arrayBuffer' in object && typeof object.arrayBuffer === 'function')
)
}
}
/**
* @param {string} url The path to check for query strings or fragments.
* @returns {boolean} Returns true if the path contains a query string or fragment.
*/
function pathHasQueryOrFragment (url) {
return (
url.includes('?') ||
url.includes('#')
)
}
/**
* @param {string} url The URL to add the query params to
* @param {import('node:querystring').ParsedUrlQueryInput} queryParams The object to serialize into a URL query string
* @returns {string} The URL with the query params added
*/
function serializePathWithQuery (url, queryParams) {
if (pathHasQueryOrFragment(url)) {
throw new Error('Query params cannot be passed when url already contains "?" or "#".')
}
const stringified = stringify(queryParams)
if (stringified) {
url += '?' + stringified
}
return url
}
/**
* @param {number|string|undefined} port
* @returns {boolean}
*/
function isValidPort (port) {
const value = parseInt(port, 10)
return (
value === Number(port) &&
value >= 0 &&
value <= 65535
)
}
/**
* Check if the value is a valid http or https prefixed string.
*
* @param {string} value
* @returns {boolean}
*/
function isHttpOrHttpsPrefixed (value) {
return (
value != null &&
value[0] === 'h' &&
value[1] === 't' &&
value[2] === 't' &&
value[3] === 'p' &&
(
value[4] === ':' ||
(
value[4] === 's' &&
value[5] === ':'
)
)
)
}
/**
* @param {string|URL|Record} url
* @returns {URL}
*/
function parseURL (url) {
if (typeof url === 'string') {
/**
* @type {URL}
*/
url = new URL(url)
if (!isHttpOrHttpsPrefixed(url.origin || url.protocol)) {
throw new InvalidArgumentError('Invalid URL protocol: the URL must start with `http:` or `https:`.')
}
return url
}
if (!url || typeof url !== 'object') {
throw new InvalidArgumentError('Invalid URL: The URL argument must be a non-null object.')
}
if (!(url instanceof URL)) {
if (url.port != null && url.port !== '' && isValidPort(url.port) === false) {
throw new InvalidArgumentError('Invalid URL: port must be a valid integer or a string representation of an integer.')
}
if (url.path != null && typeof url.path !== 'string') {
throw new InvalidArgumentError('Invalid URL path: the path must be a string or null/undefined.')
}
if (url.pathname != null && typeof url.pathname !== 'string') {
throw new InvalidArgumentError('Invalid URL pathname: the pathname must be a string or null/undefined.')
}
if (url.hostname != null && typeof url.hostname !== 'string') {
throw new InvalidArgumentError('Invalid URL hostname: the hostname must be a string or null/undefined.')
}
if (url.origin != null && typeof url.origin !== 'string') {
throw new InvalidArgumentError('Invalid URL origin: the origin must be a string or null/undefined.')
}
if (!isHttpOrHttpsPrefixed(url.origin || url.protocol)) {
throw new InvalidArgumentError('Invalid URL protocol: the URL must start with `http:` or `https:`.')
}
const port = url.port != null
? url.port
: (url.protocol === 'https:' ? 443 : 80)
let origin = url.origin != null
? url.origin
: `${url.protocol || ''}//${url.hostname || ''}:${port}`
let path = url.path != null
? url.path
: `${url.pathname || ''}${url.search || ''}`
if (origin[origin.length - 1] === '/') {
origin = origin.slice(0, origin.length - 1)
}
if (path && path[0] !== '/') {
path = `/${path}`
}
// new URL(path, origin) is unsafe when `path` contains an absolute URL
// From https://developer.mozilla.org/en-US/docs/Web/API/URL/URL:
// If first parameter is a relative URL, second param is required, and will be used as the base URL.
// If first parameter is an absolute URL, a given second param will be ignored.
return new URL(`${origin}${path}`)
}
if (!isHttpOrHttpsPrefixed(url.origin || url.protocol)) {
throw new InvalidArgumentError('Invalid URL protocol: the URL must start with `http:` or `https:`.')
}
return url
}
/**
* @param {string|URL|Record} url
* @returns {URL}
*/
function parseOrigin (url) {
url = parseURL(url)
if (url.pathname !== '/' || url.search || url.hash) {
throw new InvalidArgumentError('invalid url')
}
return url
}
/**
* @param {string} host
* @returns {string}
*/
function getHostname (host) {
if (host[0] === '[') {
const idx = host.indexOf(']')
assert(idx !== -1)
return host.substring(1, idx)
}
const idx = host.indexOf(':')
if (idx === -1) return host
return host.substring(0, idx)
}
/**
* IP addresses are not valid server names per RFC6066
* Currently, the only server names supported are DNS hostnames
* @param {string|null} host
* @returns {string|null}
*/
function getServerName (host) {
if (!host) {
return null
}
assert(typeof host === 'string')
const servername = getHostname(host)
if (net.isIP(servername)) {
return ''
}
return servername
}
/**
* @function
* @template T
* @param {T} obj
* @returns {T}
*/
function deepClone (obj) {
return JSON.parse(JSON.stringify(obj))
}
/**
* @param {*} obj
* @returns {obj is AsyncIterable}
*/
function isAsyncIterable (obj) {
return !!(obj != null && typeof obj[Symbol.asyncIterator] === 'function')
}
/**
* @param {*} obj
* @returns {obj is Iterable}
*/
function isIterable (obj) {
return !!(obj != null && (typeof obj[Symbol.iterator] === 'function' || typeof obj[Symbol.asyncIterator] === 'function'))
}
/**
* Checks whether an object has a safe Symbol.iterator — i.e. one that is
* either own or inherited from a non-Object.prototype chain. This prevents
* prototype-pollution attacks from injecting a fake iterator on
* Object.prototype.
* @param {object} obj
* @returns {boolean}
*/
function hasSafeIterator (obj) {
const prototype = Object.getPrototypeOf(obj)
const ownIterator = Object.prototype.hasOwnProperty.call(obj, Symbol.iterator)
return ownIterator || (prototype != null && prototype !== Object.prototype && typeof obj[Symbol.iterator] === 'function')
}
/**
* @param {Blob|Buffer|import ('stream').Stream} body
* @returns {number|null}
*/
function bodyLength (body) {
if (body == null) {
return 0
} else if (isStream(body)) {
const state = body._readableState
return state && state.objectMode === false && state.ended === true && Number.isFinite(state.length)
? state.length
: null
} else if (isBlobLike(body)) {
return body.size != null ? body.size : null
} else if (isBuffer(body)) {
return body.byteLength
}
return null
}
/**
* @param {import ('stream').Stream} body
* @returns {boolean}
*/
function isDestroyed (body) {
return body && !!(body.destroyed || body[kDestroyed] || (stream.isDestroyed?.(body)))
}
/**
* @param {import ('stream').Stream} stream
* @param {Error} [err]
* @returns {void}
*/
function destroy (stream, err) {
if (stream == null || !isStream(stream) || isDestroyed(stream)) {
return
}
if (typeof stream.destroy === 'function') {
if (Object.getPrototypeOf(stream).constructor === IncomingMessage) {
// See: https://github.com/nodejs/node/pull/38505/files
stream.socket = null
}
stream.destroy(err)
} else if (err) {
queueMicrotask(() => {
stream.emit('error', err)
})
}
if (stream.destroyed !== true) {
stream[kDestroyed] = true
}
}
const KEEPALIVE_TIMEOUT_EXPR = /timeout=(\d+)/
/**
* @param {string} val
* @returns {number | null}
*/
function parseKeepAliveTimeout (val) {
const m = val.match(KEEPALIVE_TIMEOUT_EXPR)
return m ? parseInt(m[1], 10) * 1000 : null
}
/**
* Retrieves a header name and returns its lowercase value.
* @param {string | Buffer} value Header name
* @returns {string}
*/
function headerNameToString (value) {
return typeof value === 'string'
? headerNameLowerCasedRecord[value] ?? value.toLowerCase()
: tree.lookup(value) ?? value.toString('latin1').toLowerCase()
}
/**
* Receive the buffer as a string and return its lowercase value.
* @param {Buffer} value Header name
* @returns {string}
*/
function bufferToLowerCasedHeaderName (value) {
return tree.lookup(value) ?? value.toString('latin1').toLowerCase()
}
/**
* @param {(Buffer | string)[]} headers
* @param {Record} [obj]
* @returns {Record}
*/
function parseHeaders (headers, obj) {
if (obj === undefined) obj = {}
for (let i = 0; i < headers.length; i += 2) {
const key = headerNameToString(headers[i])
let val = obj[key]
if (val) {
if (typeof val === 'string') {
val = [val]
obj[key] = val
}
val.push(headers[i + 1].toString('latin1'))
} else {
const headersValue = headers[i + 1]
if (typeof headersValue === 'string') {
obj[key] = headersValue
} else {
obj[key] = Array.isArray(headersValue) ? headersValue.map(x => x.toString('latin1')) : headersValue.toString('latin1')
}
}
}
return obj
}
/**
* @param {Buffer[]} headers
* @returns {string[]}
*/
function parseRawHeaders (headers) {
const headersLength = headers.length
/**
* @type {string[]}
*/
const ret = new Array(headersLength)
let key
let val
for (let n = 0; n < headersLength; n += 2) {
key = headers[n]
val = headers[n + 1]
typeof key !== 'string' && (key = key.toString())
typeof val !== 'string' && (val = val.toString('latin1'))
ret[n] = key
ret[n + 1] = val
}
return ret
}
/**
* @param {string[]} headers
* @param {Buffer[]} headers
*/
function encodeRawHeaders (headers) {
if (!Array.isArray(headers)) {
throw new TypeError('expected headers to be an array')
}
return headers.map(x => Buffer.from(x))
}
/**
* @param {*} buffer
* @returns {buffer is Buffer}
*/
function isBuffer (buffer) {
// See, https://github.com/mcollina/undici/pull/319
return buffer instanceof Uint8Array || Buffer.isBuffer(buffer)
}
/**
* Asserts that the handler object is a request handler.
*
* @param {object} handler
* @param {string} method
* @param {string} [upgrade]
* @returns {asserts handler is import('../api/api-request').RequestHandler}
*/
function assertRequestHandler (handler, method, upgrade) {
if (!handler || typeof handler !== 'object') {
throw new InvalidArgumentError('handler must be an object')
}
if (typeof handler.onRequestStart === 'function') {
// TODO (fix): More checks...
return
}
if (typeof handler.onConnect !== 'function') {
throw new InvalidArgumentError('invalid onConnect method')
}
if (typeof handler.onError !== 'function') {
throw new InvalidArgumentError('invalid onError method')
}
if (typeof handler.onBodySent !== 'function' && handler.onBodySent !== undefined) {
throw new InvalidArgumentError('invalid onBodySent method')
}
if (upgrade || method === 'CONNECT') {
if (typeof handler.onUpgrade !== 'function') {
throw new InvalidArgumentError('invalid onUpgrade method')
}
} else {
if (typeof handler.onHeaders !== 'function') {
throw new InvalidArgumentError('invalid onHeaders method')
}
if (typeof handler.onData !== 'function') {
throw new InvalidArgumentError('invalid onData method')
}
if (typeof handler.onComplete !== 'function') {
throw new InvalidArgumentError('invalid onComplete method')
}
}
}
/**
* A body is disturbed if it has been read from and it cannot be re-used without
* losing state or data.
* @param {import('node:stream').Readable} body
* @returns {boolean}
*/
function isDisturbed (body) {
// TODO (fix): Why is body[kBodyUsed] needed?
return !!(body && (stream.isDisturbed(body) || body[kBodyUsed]))
}
/**
* @typedef {object} SocketInfo
* @property {string} [localAddress]
* @property {number} [localPort]
* @property {string} [remoteAddress]
* @property {number} [remotePort]
* @property {string} [remoteFamily]
* @property {number} [timeout]
* @property {number} bytesWritten
* @property {number} bytesRead
*/
/**
* @param {import('net').Socket} socket
* @returns {SocketInfo}
*/
function getSocketInfo (socket) {
return {
localAddress: socket.localAddress,
localPort: socket.localPort,
remoteAddress: socket.remoteAddress,
remotePort: socket.remotePort,
remoteFamily: socket.remoteFamily,
timeout: socket.timeout,
bytesWritten: socket.bytesWritten,
bytesRead: socket.bytesRead
}
}
/**
* @param {Iterable} iterable
* @returns {ReadableStream}
*/
function ReadableStreamFrom (iterable) {
// We cannot use ReadableStream.from here because it does not return a byte stream.
let iterator
return new ReadableStream(
{
start () {
iterator = iterable[Symbol.asyncIterator]()
},
pull (controller) {
return iterator.next().then(({ done, value }) => {
if (done) {
return queueMicrotask(() => {
controller.close()
controller.byobRequest?.respond(0)
})
} else {
const buf = Buffer.isBuffer(value) ? value : Buffer.from(value)
if (buf.byteLength) {
return controller.enqueue(new Uint8Array(buf))
} else {
return this.pull(controller)
}
}
})
},
cancel () {
return iterator.return()
},
type: 'bytes'
}
)
}
/**
* The object should be a FormData instance and contains all the required
* methods.
* @param {*} object
* @returns {object is FormData}
*/
function isFormDataLike (object) {
return (
object &&
typeof object === 'object' &&
typeof object.append === 'function' &&
typeof object.delete === 'function' &&
typeof object.get === 'function' &&
typeof object.getAll === 'function' &&
typeof object.has === 'function' &&
typeof object.set === 'function' &&
object[Symbol.toStringTag] === 'FormData'
)
}
function addAbortListener (signal, listener) {
if ('addEventListener' in signal) {
signal.addEventListener('abort', listener, { once: true })
return () => signal.removeEventListener('abort', listener)
}
signal.once('abort', listener)
return () => signal.removeListener('abort', listener)
}
const validTokenChars = new Uint8Array([
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0-15
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 16-31
0, 1, 0, 1, 1, 1, 1, 1, 0, 0, 1, 1, 0, 1, 1, 0, // 32-47 (!"#$%&'()*+,-./)
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, // 48-63 (0-9:;<=>?)
0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 64-79 (@A-O)
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, // 80-95 (P-Z[\]^_)
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 96-111 (`a-o)
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1, 0, // 112-127 (p-z{|}~)
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 128-143
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 144-159
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 160-175
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 176-191
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 192-207
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 208-223
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 224-239
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 // 240-255
])
/**
* @see https://tools.ietf.org/html/rfc7230#section-3.2.6
* @param {number} c
* @returns {boolean}
*/
function isTokenCharCode (c) {
return (validTokenChars[c] === 1)
}
const tokenRegExp = /^[\^_`a-zA-Z\-0-9!#$%&'*+.|~]+$/
/**
* @param {string} characters
* @returns {boolean}
*/
function isValidHTTPToken (characters) {
if (characters.length >= 12) return tokenRegExp.test(characters)
if (characters.length === 0) return false
for (let i = 0; i < characters.length; i++) {
if (validTokenChars[characters.charCodeAt(i)] !== 1) {
return false
}
}
return true
}
// headerCharRegex have been lifted from
// https://github.com/nodejs/node/blob/main/lib/_http_common.js
/**
* Matches if val contains an invalid field-vchar
* field-value = *( field-content / obs-fold )
* field-content = field-vchar [ 1*( SP / HTAB ) field-vchar ]
* field-vchar = VCHAR / obs-text
*/
const headerCharRegex = /[^\t\x20-\x7e\x80-\xff]/
/**
* @param {string} characters
* @returns {boolean}
*/
function isValidHeaderValue (characters) {
return !headerCharRegex.test(characters)
}
const rangeHeaderRegex = /^bytes (\d+)-(\d+)\/(\d+)?$/
/**
* @typedef {object} RangeHeader
* @property {number} start
* @property {number | null} end
* @property {number | null} size
*/
/**
* Parse accordingly to RFC 9110
* @see https://www.rfc-editor.org/rfc/rfc9110#field.content-range
* @param {string} [range]
* @returns {RangeHeader|null}
*/
function parseRangeHeader (range) {
if (range == null || range === '') return { start: 0, end: null, size: null }
const m = range ? range.match(rangeHeaderRegex) : null
return m
? {
start: parseInt(m[1]),
end: m[2] ? parseInt(m[2]) : null,
size: m[3] ? parseInt(m[3]) : null
}
: null
}
/**
* @template {import("events").EventEmitter} T
* @param {T} obj
* @param {string} name
* @param {(...args: any[]) => void} listener
* @returns {T}
*/
function addListener (obj, name, listener) {
const listeners = (obj[kListeners] ??= [])
listeners.push([name, listener])
obj.on(name, listener)
return obj
}
/**
* @template {import("events").EventEmitter} T
* @param {T} obj
* @returns {T}
*/
function removeAllListeners (obj) {
if (obj[kListeners] != null) {
for (const [name, listener] of obj[kListeners]) {
obj.removeListener(name, listener)
}
obj[kListeners] = null
}
return obj
}
/**
* @param {import ('../dispatcher/client')} client
* @param {import ('../core/request')} request
* @param {Error} err
*/
function errorRequest (client, request, err) {
try {
request.onError(err)
assert(request.aborted)
} catch (err) {
client.emit('error', err)
}
}
/**
* @param {WeakRef} socketWeakRef
* @param {object} opts
* @param {number} opts.timeout
* @param {string} opts.hostname
* @param {number} opts.port
* @returns {() => void}
*/
const setupConnectTimeout = process.platform === 'win32'
? (socketWeakRef, opts) => {
if (!opts.timeout) {
return noop
}
let s1 = null
let s2 = null
const fastTimer = timers.setFastTimeout(() => {
// setImmediate is added to make sure that we prioritize socket error events over timeouts
s1 = setImmediate(() => {
// Windows needs an extra setImmediate probably due to implementation differences in the socket logic
s2 = setImmediate(() => onConnectTimeout(socketWeakRef.deref(), opts))
})
}, opts.timeout)
return () => {
timers.clearFastTimeout(fastTimer)
clearImmediate(s1)
clearImmediate(s2)
}
}
: (socketWeakRef, opts) => {
if (!opts.timeout) {
return noop
}
let s1 = null
const fastTimer = timers.setFastTimeout(() => {
// setImmediate is added to make sure that we prioritize socket error events over timeouts
s1 = setImmediate(() => {
onConnectTimeout(socketWeakRef.deref(), opts)
})
}, opts.timeout)
return () => {
timers.clearFastTimeout(fastTimer)
clearImmediate(s1)
}
}
/**
* @param {net.Socket} socket
* @param {object} opts
* @param {number} opts.timeout
* @param {string} opts.hostname
* @param {number} opts.port
*/
function onConnectTimeout (socket, opts) {
// The socket could be already garbage collected
if (socket == null) {
return
}
let message = 'Connect Timeout Error'
if (Array.isArray(socket.autoSelectFamilyAttemptedAddresses)) {
message += ` (attempted addresses: ${socket.autoSelectFamilyAttemptedAddresses.join(', ')},`
} else {
message += ` (attempted address: ${opts.hostname}:${opts.port},`
}
message += ` timeout: ${opts.timeout}ms)`
destroy(socket, new ConnectTimeoutError(message))
}
/**
* @param {string} urlString
* @returns {string}
*/
function getProtocolFromUrlString (urlString) {
if (
urlString[0] === 'h' &&
urlString[1] === 't' &&
urlString[2] === 't' &&
urlString[3] === 'p'
) {
switch (urlString[4]) {
case ':':
return 'http:'
case 's':
if (urlString[5] === ':') {
return 'https:'
}
}
}
// fallback if none of the usual suspects
return urlString.slice(0, urlString.indexOf(':') + 1)
}
const kEnumerableProperty = Object.create(null)
kEnumerableProperty.enumerable = true
const normalizedMethodRecordsBase = {
delete: 'DELETE',
DELETE: 'DELETE',
get: 'GET',
GET: 'GET',
head: 'HEAD',
HEAD: 'HEAD',
options: 'OPTIONS',
OPTIONS: 'OPTIONS',
post: 'POST',
POST: 'POST',
put: 'PUT',
PUT: 'PUT'
}
const normalizedMethodRecords = {
...normalizedMethodRecordsBase,
patch: 'patch',
PATCH: 'PATCH'
}
// Note: object prototypes should not be able to be referenced. e.g. `Object#hasOwnProperty`.
Object.setPrototypeOf(normalizedMethodRecordsBase, null)
Object.setPrototypeOf(normalizedMethodRecords, null)
module.exports = {
kEnumerableProperty,
isDisturbed,
isBlobLike,
parseOrigin,
parseURL,
getServerName,
isStream,
isIterable,
hasSafeIterator,
isAsyncIterable,
isDestroyed,
headerNameToString,
bufferToLowerCasedHeaderName,
addListener,
removeAllListeners,
errorRequest,
parseRawHeaders,
encodeRawHeaders,
parseHeaders,
parseKeepAliveTimeout,
destroy,
bodyLength,
deepClone,
ReadableStreamFrom,
isBuffer,
assertRequestHandler,
getSocketInfo,
isFormDataLike,
pathHasQueryOrFragment,
serializePathWithQuery,
addAbortListener,
isValidHTTPToken,
isValidHeaderValue,
isTokenCharCode,
parseRangeHeader,
normalizedMethodRecordsBase,
normalizedMethodRecords,
isValidPort,
isHttpOrHttpsPrefixed,
nodeMajor,
nodeMinor,
safeHTTPMethods: Object.freeze(['GET', 'HEAD', 'OPTIONS', 'TRACE']),
wrapRequestBody,
setupConnectTimeout,
getProtocolFromUrlString
}
================================================
FILE: lib/dispatcher/agent.js
================================================
'use strict'
const { InvalidArgumentError, MaxOriginsReachedError } = require('../core/errors')
const { kClients, kRunning, kClose, kDestroy, kDispatch, kUrl } = require('../core/symbols')
const DispatcherBase = require('./dispatcher-base')
const Pool = require('./pool')
const Client = require('./client')
const util = require('../core/util')
const kOnConnect = Symbol('onConnect')
const kOnDisconnect = Symbol('onDisconnect')
const kOnConnectionError = Symbol('onConnectionError')
const kOnDrain = Symbol('onDrain')
const kFactory = Symbol('factory')
const kOptions = Symbol('options')
const kOrigins = Symbol('origins')
function defaultFactory (origin, opts) {
return opts && opts.connections === 1
? new Client(origin, opts)
: new Pool(origin, opts)
}
class Agent extends DispatcherBase {
constructor ({ factory = defaultFactory, maxOrigins = Infinity, connect, ...options } = {}) {
if (typeof factory !== 'function') {
throw new InvalidArgumentError('factory must be a function.')
}
if (connect != null && typeof connect !== 'function' && typeof connect !== 'object') {
throw new InvalidArgumentError('connect must be a function or an object')
}
if (typeof maxOrigins !== 'number' || Number.isNaN(maxOrigins) || maxOrigins <= 0) {
throw new InvalidArgumentError('maxOrigins must be a number greater than 0')
}
super()
if (connect && typeof connect !== 'function') {
connect = { ...connect }
}
this[kOptions] = { ...util.deepClone(options), maxOrigins, connect }
this[kFactory] = factory
this[kClients] = new Map()
this[kOrigins] = new Set()
this[kOnDrain] = (origin, targets) => {
this.emit('drain', origin, [this, ...targets])
}
this[kOnConnect] = (origin, targets) => {
this.emit('connect', origin, [this, ...targets])
}
this[kOnDisconnect] = (origin, targets, err) => {
this.emit('disconnect', origin, [this, ...targets], err)
}
this[kOnConnectionError] = (origin, targets, err) => {
this.emit('connectionError', origin, [this, ...targets], err)
}
}
get [kRunning] () {
let ret = 0
for (const { dispatcher } of this[kClients].values()) {
ret += dispatcher[kRunning]
}
return ret
}
[kDispatch] (opts, handler) {
let key
if (opts.origin && (typeof opts.origin === 'string' || opts.origin instanceof URL)) {
key = String(opts.origin)
} else {
throw new InvalidArgumentError('opts.origin must be a non-empty string or URL.')
}
if (this[kOrigins].size >= this[kOptions].maxOrigins && !this[kOrigins].has(key)) {
throw new MaxOriginsReachedError()
}
const result = this[kClients].get(key)
let dispatcher = result && result.dispatcher
if (!dispatcher) {
const closeClientIfUnused = (connected) => {
const result = this[kClients].get(key)
if (result) {
if (connected) result.count -= 1
if (result.count <= 0) {
this[kClients].delete(key)
if (!result.dispatcher.destroyed) {
result.dispatcher.close()
}
}
this[kOrigins].delete(key)
}
}
dispatcher = this[kFactory](opts.origin, this[kOptions])
.on('drain', this[kOnDrain])
.on('connect', (origin, targets) => {
const result = this[kClients].get(key)
if (result) {
result.count += 1
}
this[kOnConnect](origin, targets)
})
.on('disconnect', (origin, targets, err) => {
closeClientIfUnused(true)
this[kOnDisconnect](origin, targets, err)
})
.on('connectionError', (origin, targets, err) => {
closeClientIfUnused(false)
this[kOnConnectionError](origin, targets, err)
})
this[kClients].set(key, { count: 0, dispatcher })
this[kOrigins].add(key)
}
return dispatcher.dispatch(opts, handler)
}
[kClose] () {
const closePromises = []
for (const { dispatcher } of this[kClients].values()) {
closePromises.push(dispatcher.close())
}
this[kClients].clear()
return Promise.all(closePromises)
}
[kDestroy] (err) {
const destroyPromises = []
for (const { dispatcher } of this[kClients].values()) {
destroyPromises.push(dispatcher.destroy(err))
}
this[kClients].clear()
return Promise.all(destroyPromises)
}
get stats () {
const allClientStats = {}
for (const { dispatcher } of this[kClients].values()) {
if (dispatcher.stats) {
allClientStats[dispatcher[kUrl].origin] = dispatcher.stats
}
}
return allClientStats
}
}
module.exports = Agent
================================================
FILE: lib/dispatcher/balanced-pool.js
================================================
'use strict'
const {
BalancedPoolMissingUpstreamError,
InvalidArgumentError
} = require('../core/errors')
const {
PoolBase,
kClients,
kNeedDrain,
kAddClient,
kRemoveClient,
kGetDispatcher
} = require('./pool-base')
const Pool = require('./pool')
const { kUrl } = require('../core/symbols')
const util = require('../core/util')
const kFactory = Symbol('factory')
const kOptions = Symbol('options')
const kGreatestCommonDivisor = Symbol('kGreatestCommonDivisor')
const kCurrentWeight = Symbol('kCurrentWeight')
const kIndex = Symbol('kIndex')
const kWeight = Symbol('kWeight')
const kMaxWeightPerServer = Symbol('kMaxWeightPerServer')
const kErrorPenalty = Symbol('kErrorPenalty')
/**
* Calculate the greatest common divisor of two numbers by
* using the Euclidean algorithm.
*
* @param {number} a
* @param {number} b
* @returns {number}
*/
function getGreatestCommonDivisor (a, b) {
if (a === 0) return b
while (b !== 0) {
const t = b
b = a % b
a = t
}
return a
}
function defaultFactory (origin, opts) {
return new Pool(origin, opts)
}
class BalancedPool extends PoolBase {
constructor (upstreams = [], { factory = defaultFactory, ...opts } = {}) {
if (typeof factory !== 'function') {
throw new InvalidArgumentError('factory must be a function.')
}
super()
this[kOptions] = { ...util.deepClone(opts) }
this[kOptions].interceptors = opts.interceptors
? { ...opts.interceptors }
: undefined
this[kIndex] = -1
this[kCurrentWeight] = 0
this[kMaxWeightPerServer] = this[kOptions].maxWeightPerServer || 100
this[kErrorPenalty] = this[kOptions].errorPenalty || 15
if (!Array.isArray(upstreams)) {
upstreams = [upstreams]
}
this[kFactory] = factory
for (const upstream of upstreams) {
this.addUpstream(upstream)
}
this._updateBalancedPoolStats()
}
addUpstream (upstream) {
const upstreamOrigin = util.parseOrigin(upstream).origin
if (this[kClients].find((pool) => (
pool[kUrl].origin === upstreamOrigin &&
pool.closed !== true &&
pool.destroyed !== true
))) {
return this
}
const pool = this[kFactory](upstreamOrigin, this[kOptions])
this[kAddClient](pool)
pool.on('connect', () => {
pool[kWeight] = Math.min(this[kMaxWeightPerServer], pool[kWeight] + this[kErrorPenalty])
})
pool.on('connectionError', () => {
pool[kWeight] = Math.max(1, pool[kWeight] - this[kErrorPenalty])
this._updateBalancedPoolStats()
})
pool.on('disconnect', (...args) => {
const err = args[2]
if (err && err.code === 'UND_ERR_SOCKET') {
// decrease the weight of the pool.
pool[kWeight] = Math.max(1, pool[kWeight] - this[kErrorPenalty])
this._updateBalancedPoolStats()
}
})
for (const client of this[kClients]) {
client[kWeight] = this[kMaxWeightPerServer]
}
this._updateBalancedPoolStats()
return this
}
_updateBalancedPoolStats () {
let result = 0
for (let i = 0; i < this[kClients].length; i++) {
result = getGreatestCommonDivisor(this[kClients][i][kWeight], result)
}
this[kGreatestCommonDivisor] = result
}
removeUpstream (upstream) {
const upstreamOrigin = util.parseOrigin(upstream).origin
const pool = this[kClients].find((pool) => (
pool[kUrl].origin === upstreamOrigin &&
pool.closed !== true &&
pool.destroyed !== true
))
if (pool) {
this[kRemoveClient](pool)
}
return this
}
getUpstream (upstream) {
const upstreamOrigin = util.parseOrigin(upstream).origin
return this[kClients].find((pool) => (
pool[kUrl].origin === upstreamOrigin &&
pool.closed !== true &&
pool.destroyed !== true
))
}
get upstreams () {
return this[kClients]
.filter(dispatcher => dispatcher.closed !== true && dispatcher.destroyed !== true)
.map((p) => p[kUrl].origin)
}
[kGetDispatcher] () {
// We validate that pools is greater than 0,
// otherwise we would have to wait until an upstream
// is added, which might never happen.
if (this[kClients].length === 0) {
throw new BalancedPoolMissingUpstreamError()
}
const dispatcher = this[kClients].find(dispatcher => (
!dispatcher[kNeedDrain] &&
dispatcher.closed !== true &&
dispatcher.destroyed !== true
))
if (!dispatcher) {
return
}
const allClientsBusy = this[kClients].map(pool => pool[kNeedDrain]).reduce((a, b) => a && b, true)
if (allClientsBusy) {
return
}
let counter = 0
let maxWeightIndex = this[kClients].findIndex(pool => !pool[kNeedDrain])
while (counter++ < this[kClients].length) {
this[kIndex] = (this[kIndex] + 1) % this[kClients].length
const pool = this[kClients][this[kIndex]]
// find pool index with the largest weight
if (pool[kWeight] > this[kClients][maxWeightIndex][kWeight] && !pool[kNeedDrain]) {
maxWeightIndex = this[kIndex]
}
// decrease the current weight every `this[kClients].length`.
if (this[kIndex] === 0) {
// Set the current weight to the next lower weight.
this[kCurrentWeight] = this[kCurrentWeight] - this[kGreatestCommonDivisor]
if (this[kCurrentWeight] <= 0) {
this[kCurrentWeight] = this[kMaxWeightPerServer]
}
}
if (pool[kWeight] >= this[kCurrentWeight] && (!pool[kNeedDrain])) {
return pool
}
}
this[kCurrentWeight] = this[kClients][maxWeightIndex][kWeight]
this[kIndex] = maxWeightIndex
return this[kClients][maxWeightIndex]
}
}
module.exports = BalancedPool
================================================
FILE: lib/dispatcher/client-h1.js
================================================
'use strict'
/* global WebAssembly */
const assert = require('node:assert')
const util = require('../core/util.js')
const { channels } = require('../core/diagnostics.js')
const timers = require('../util/timers.js')
const {
RequestContentLengthMismatchError,
ResponseContentLengthMismatchError,
RequestAbortedError,
HeadersTimeoutError,
HeadersOverflowError,
SocketError,
InformationalError,
BodyTimeoutError,
HTTPParserError,
ResponseExceededMaxSizeError
} = require('../core/errors.js')
const {
kUrl,
kReset,
kClient,
kParser,
kBlocking,
kRunning,
kPending,
kSize,
kWriting,
kQueue,
kNoRef,
kKeepAliveDefaultTimeout,
kHostHeader,
kPendingIdx,
kRunningIdx,
kError,
kPipelining,
kSocket,
kKeepAliveTimeoutValue,
kMaxHeadersSize,
kKeepAliveMaxTimeout,
kKeepAliveTimeoutThreshold,
kHeadersTimeout,
kBodyTimeout,
kStrictContentLength,
kMaxRequests,
kCounter,
kMaxResponseSize,
kOnError,
kResume,
kHTTPContext,
kClosed
} = require('../core/symbols.js')
const constants = require('../llhttp/constants.js')
const EMPTY_BUF = Buffer.alloc(0)
const FastBuffer = Buffer[Symbol.species]
const removeAllListeners = util.removeAllListeners
let extractBody
function lazyllhttp () {
const llhttpWasmData = process.env.JEST_WORKER_ID ? require('../llhttp/llhttp-wasm.js') : undefined
let mod
// We disable wasm SIMD on ppc64 as it seems to be broken on Power 9 architectures.
let useWasmSIMD = process.arch !== 'ppc64'
// The Env Variable UNDICI_NO_WASM_SIMD allows explicitly overriding the default behavior
if (process.env.UNDICI_NO_WASM_SIMD === '1') {
useWasmSIMD = true
} else if (process.env.UNDICI_NO_WASM_SIMD === '0') {
useWasmSIMD = false
}
if (useWasmSIMD) {
try {
mod = new WebAssembly.Module(require('../llhttp/llhttp_simd-wasm.js'))
} catch {
}
}
if (!mod) {
// We could check if the error was caused by the simd option not
// being enabled, but the occurring of this other error
// * https://github.com/emscripten-core/emscripten/issues/11495
// got me to remove that check to avoid breaking Node 12.
mod = new WebAssembly.Module(llhttpWasmData || require('../llhttp/llhttp-wasm.js'))
}
return new WebAssembly.Instance(mod, {
env: {
/**
* @param {number} p
* @param {number} at
* @param {number} len
* @returns {number}
*/
wasm_on_url: (p, at, len) => {
return 0
},
/**
* @param {number} p
* @param {number} at
* @param {number} len
* @returns {number}
*/
wasm_on_status: (p, at, len) => {
assert(currentParser.ptr === p)
const start = at - currentBufferPtr + currentBufferRef.byteOffset
return currentParser.onStatus(new FastBuffer(currentBufferRef.buffer, start, len))
},
/**
* @param {number} p
* @returns {number}
*/
wasm_on_message_begin: (p) => {
assert(currentParser.ptr === p)
return currentParser.onMessageBegin()
},
/**
* @param {number} p
* @param {number} at
* @param {number} len
* @returns {number}
*/
wasm_on_header_field: (p, at, len) => {
assert(currentParser.ptr === p)
const start = at - currentBufferPtr + currentBufferRef.byteOffset
return currentParser.onHeaderField(new FastBuffer(currentBufferRef.buffer, start, len))
},
/**
* @param {number} p
* @param {number} at
* @param {number} len
* @returns {number}
*/
wasm_on_header_value: (p, at, len) => {
assert(currentParser.ptr === p)
const start = at - currentBufferPtr + currentBufferRef.byteOffset
return currentParser.onHeaderValue(new FastBuffer(currentBufferRef.buffer, start, len))
},
/**
* @param {number} p
* @param {number} statusCode
* @param {0|1} upgrade
* @param {0|1} shouldKeepAlive
* @returns {number}
*/
wasm_on_headers_complete: (p, statusCode, upgrade, shouldKeepAlive) => {
assert(currentParser.ptr === p)
return currentParser.onHeadersComplete(statusCode, upgrade === 1, shouldKeepAlive === 1)
},
/**
* @param {number} p
* @param {number} at
* @param {number} len
* @returns {number}
*/
wasm_on_body: (p, at, len) => {
assert(currentParser.ptr === p)
const start = at - currentBufferPtr + currentBufferRef.byteOffset
return currentParser.onBody(new FastBuffer(currentBufferRef.buffer, start, len))
},
/**
* @param {number} p
* @returns {number}
*/
wasm_on_message_complete: (p) => {
assert(currentParser.ptr === p)
return currentParser.onMessageComplete()
}
}
})
}
let llhttpInstance = null
/**
* @type {Parser|null}
*/
let currentParser = null
let currentBufferRef = null
/**
* @type {number}
*/
let currentBufferSize = 0
let currentBufferPtr = null
const USE_NATIVE_TIMER = 0
const USE_FAST_TIMER = 1
// Use fast timers for headers and body to take eventual event loop
// latency into account.
const TIMEOUT_HEADERS = 2 | USE_FAST_TIMER
const TIMEOUT_BODY = 4 | USE_FAST_TIMER
// Use native timers to ignore event loop latency for keep-alive
// handling.
const TIMEOUT_KEEP_ALIVE = 8 | USE_NATIVE_TIMER
class Parser {
/**
* @param {import('./client.js')} client
* @param {import('net').Socket} socket
* @param {*} llhttp
*/
constructor (client, socket, { exports }) {
this.llhttp = exports
this.ptr = this.llhttp.llhttp_alloc(constants.TYPE.RESPONSE)
this.client = client
/**
* @type {import('net').Socket}
*/
this.socket = socket
this.timeout = null
this.timeoutValue = null
this.timeoutType = null
this.statusCode = 0
this.statusText = ''
this.upgrade = false
this.headers = []
this.headersSize = 0
this.headersMaxSize = client[kMaxHeadersSize]
this.shouldKeepAlive = false
this.paused = false
this.resume = this.resume.bind(this)
this.bytesRead = 0
this.keepAlive = ''
this.contentLength = ''
this.connection = ''
this.maxResponseSize = client[kMaxResponseSize]
}
setTimeout (delay, type) {
// If the existing timer and the new timer are of different timer type
// (fast or native) or have different delay, we need to clear the existing
// timer and set a new one.
if (
delay !== this.timeoutValue ||
(type & USE_FAST_TIMER) ^ (this.timeoutType & USE_FAST_TIMER)
) {
// If a timeout is already set, clear it with clearTimeout of the fast
// timer implementation, as it can clear fast and native timers.
if (this.timeout) {
timers.clearTimeout(this.timeout)
this.timeout = null
}
if (delay) {
if (type & USE_FAST_TIMER) {
this.timeout = timers.setFastTimeout(onParserTimeout, delay, new WeakRef(this))
} else {
this.timeout = setTimeout(onParserTimeout, delay, new WeakRef(this))
this.timeout?.unref()
}
}
this.timeoutValue = delay
} else if (this.timeout) {
if (this.timeout.refresh) {
this.timeout.refresh()
}
}
this.timeoutType = type
}
resume () {
if (this.socket.destroyed || !this.paused) {
return
}
assert(this.ptr != null)
assert(currentParser === null)
this.llhttp.llhttp_resume(this.ptr)
assert(this.timeoutType === TIMEOUT_BODY)
if (this.timeout) {
if (this.timeout.refresh) {
this.timeout.refresh()
}
}
this.paused = false
this.execute(this.socket.read() || EMPTY_BUF) // Flush parser.
this.readMore()
}
readMore () {
while (!this.paused && this.ptr) {
const chunk = this.socket.read()
if (chunk === null) {
break
}
this.execute(chunk)
}
}
/**
* @param {Buffer} chunk
*/
execute (chunk) {
assert(currentParser === null)
assert(this.ptr != null)
assert(!this.paused)
const { socket, llhttp } = this
// Allocate a new buffer if the current buffer is too small.
if (chunk.length > currentBufferSize) {
if (currentBufferPtr) {
llhttp.free(currentBufferPtr)
}
// Allocate a buffer that is a multiple of 4096 bytes.
currentBufferSize = Math.ceil(chunk.length / 4096) * 4096
currentBufferPtr = llhttp.malloc(currentBufferSize)
}
new Uint8Array(llhttp.memory.buffer, currentBufferPtr, currentBufferSize).set(chunk)
// Call `execute` on the wasm parser.
// We pass the `llhttp_parser` pointer address, the pointer address of buffer view data,
// and finally the length of bytes to parse.
// The return value is an error code or `constants.ERROR.OK`.
try {
let ret
try {
currentBufferRef = chunk
currentParser = this
ret = llhttp.llhttp_execute(this.ptr, currentBufferPtr, chunk.length)
} finally {
currentParser = null
currentBufferRef = null
}
if (ret !== constants.ERROR.OK) {
const data = chunk.subarray(llhttp.llhttp_get_error_pos(this.ptr) - currentBufferPtr)
if (ret === constants.ERROR.PAUSED_UPGRADE) {
this.onUpgrade(data)
} else if (ret === constants.ERROR.PAUSED) {
this.paused = true
socket.unshift(data)
} else {
const ptr = llhttp.llhttp_get_error_reason(this.ptr)
let message = ''
if (ptr) {
const len = new Uint8Array(llhttp.memory.buffer, ptr).indexOf(0)
message =
'Response does not match the HTTP/1.1 protocol (' +
Buffer.from(llhttp.memory.buffer, ptr, len).toString() +
')'
}
throw new HTTPParserError(message, constants.ERROR[ret], data)
}
}
} catch (err) {
util.destroy(socket, err)
}
}
destroy () {
assert(currentParser === null)
assert(this.ptr != null)
this.llhttp.llhttp_free(this.ptr)
this.ptr = null
this.timeout && timers.clearTimeout(this.timeout)
this.timeout = null
this.timeoutValue = null
this.timeoutType = null
this.paused = false
}
/**
* @param {Buffer} buf
* @returns {0}
*/
onStatus (buf) {
this.statusText = buf.toString()
return 0
}
/**
* @returns {0|-1}
*/
onMessageBegin () {
const { socket, client } = this
if (socket.destroyed) {
return -1
}
const request = client[kQueue][client[kRunningIdx]]
if (!request) {
return -1
}
request.onResponseStarted()
return 0
}
/**
* @param {Buffer} buf
* @returns {number}
*/
onHeaderField (buf) {
const len = this.headers.length
if ((len & 1) === 0) {
this.headers.push(buf)
} else {
this.headers[len - 1] = Buffer.concat([this.headers[len - 1], buf])
}
this.trackHeader(buf.length)
return 0
}
/**
* @param {Buffer} buf
* @returns {number}
*/
onHeaderValue (buf) {
let len = this.headers.length
if ((len & 1) === 1) {
this.headers.push(buf)
len += 1
} else {
this.headers[len - 1] = Buffer.concat([this.headers[len - 1], buf])
}
const key = this.headers[len - 2]
if (key.length === 10) {
const headerName = util.bufferToLowerCasedHeaderName(key)
if (headerName === 'keep-alive') {
this.keepAlive += buf.toString()
} else if (headerName === 'connection') {
this.connection += buf.toString()
}
} else if (key.length === 14 && util.bufferToLowerCasedHeaderName(key) === 'content-length') {
this.contentLength += buf.toString()
}
this.trackHeader(buf.length)
return 0
}
/**
* @param {number} len
*/
trackHeader (len) {
this.headersSize += len
if (this.headersSize >= this.headersMaxSize) {
util.destroy(this.socket, new HeadersOverflowError())
}
}
/**
* @param {Buffer} head
*/
onUpgrade (head) {
const { upgrade, client, socket, headers, statusCode } = this
assert(upgrade)
assert(client[kSocket] === socket)
assert(!socket.destroyed)
assert(!this.paused)
assert((headers.length & 1) === 0)
const request = client[kQueue][client[kRunningIdx]]
assert(request)
assert(request.upgrade || request.method === 'CONNECT')
this.statusCode = 0
this.statusText = ''
this.shouldKeepAlive = false
this.headers = []
this.headersSize = 0
socket.unshift(head)
socket[kParser].destroy()
socket[kParser] = null
socket[kClient] = null
socket[kError] = null
removeAllListeners(socket)
client[kSocket] = null
client[kHTTPContext] = null // TODO (fix): This is hacky...
client[kQueue][client[kRunningIdx]++] = null
client.emit('disconnect', client[kUrl], [client], new InformationalError('upgrade'))
try {
request.onUpgrade(statusCode, headers, socket)
} catch (err) {
util.destroy(socket, err)
}
client[kResume]()
}
/**
* @param {number} statusCode
* @param {boolean} upgrade
* @param {boolean} shouldKeepAlive
* @returns {number}
*/
onHeadersComplete (statusCode, upgrade, shouldKeepAlive) {
const { client, socket, headers, statusText } = this
if (socket.destroyed) {
return -1
}
const request = client[kQueue][client[kRunningIdx]]
if (!request) {
return -1
}
assert(!this.upgrade)
assert(this.statusCode < 200)
if (statusCode === 100) {
util.destroy(socket, new SocketError('bad response', util.getSocketInfo(socket)))
return -1
}
/* this can only happen if server is misbehaving */
if (upgrade && !request.upgrade) {
util.destroy(socket, new SocketError('bad upgrade', util.getSocketInfo(socket)))
return -1
}
assert(this.timeoutType === TIMEOUT_HEADERS)
this.statusCode = statusCode
this.shouldKeepAlive = (
shouldKeepAlive ||
// Override llhttp value which does not allow keepAlive for HEAD.
(request.method === 'HEAD' && !socket[kReset] && this.connection.toLowerCase() === 'keep-alive')
)
if (this.statusCode >= 200) {
const bodyTimeout = request.bodyTimeout != null
? request.bodyTimeout
: client[kBodyTimeout]
this.setTimeout(bodyTimeout, TIMEOUT_BODY)
} else if (this.timeout) {
if (this.timeout.refresh) {
this.timeout.refresh()
}
}
if (request.method === 'CONNECT') {
assert(client[kRunning] === 1)
this.upgrade = true
return 2
}
if (upgrade) {
assert(client[kRunning] === 1)
this.upgrade = true
return 2
}
assert((this.headers.length & 1) === 0)
this.headers = []
this.headersSize = 0
if (this.shouldKeepAlive && client[kPipelining]) {
const keepAliveTimeout = this.keepAlive ? util.parseKeepAliveTimeout(this.keepAlive) : null
if (keepAliveTimeout != null) {
const timeout = Math.min(
keepAliveTimeout - client[kKeepAliveTimeoutThreshold],
client[kKeepAliveMaxTimeout]
)
if (timeout <= 0) {
socket[kReset] = true
} else {
client[kKeepAliveTimeoutValue] = timeout
}
} else {
client[kKeepAliveTimeoutValue] = client[kKeepAliveDefaultTimeout]
}
} else {
// Stop more requests from being dispatched.
socket[kReset] = true
}
const pause = request.onHeaders(statusCode, headers, this.resume, statusText) === false
if (request.aborted) {
return -1
}
if (request.method === 'HEAD') {
return 1
}
if (statusCode < 200) {
return 1
}
if (socket[kBlocking]) {
socket[kBlocking] = false
client[kResume]()
}
return pause ? constants.ERROR.PAUSED : 0
}
/**
* @param {Buffer} buf
* @returns {number}
*/
onBody (buf) {
const { client, socket, statusCode, maxResponseSize } = this
if (socket.destroyed) {
return -1
}
const request = client[kQueue][client[kRunningIdx]]
assert(request)
assert(this.timeoutType === TIMEOUT_BODY)
if (this.timeout) {
if (this.timeout.refresh) {
this.timeout.refresh()
}
}
assert(statusCode >= 200)
if (maxResponseSize > -1 && this.bytesRead + buf.length > maxResponseSize) {
util.destroy(socket, new ResponseExceededMaxSizeError())
return -1
}
this.bytesRead += buf.length
if (request.onData(buf) === false) {
return constants.ERROR.PAUSED
}
return 0
}
/**
* @returns {number}
*/
onMessageComplete () {
const { client, socket, statusCode, upgrade, headers, contentLength, bytesRead, shouldKeepAlive } = this
if (socket.destroyed && (!statusCode || shouldKeepAlive)) {
return -1
}
if (upgrade) {
return 0
}
assert(statusCode >= 100)
assert((this.headers.length & 1) === 0)
const request = client[kQueue][client[kRunningIdx]]
assert(request)
this.statusCode = 0
this.statusText = ''
this.bytesRead = 0
this.contentLength = ''
this.keepAlive = ''
this.connection = ''
this.headers = []
this.headersSize = 0
if (statusCode < 200) {
return 0
}
if (request.method !== 'HEAD' && contentLength && bytesRead !== parseInt(contentLength, 10)) {
util.destroy(socket, new ResponseContentLengthMismatchError())
return -1
}
request.onComplete(headers)
client[kQueue][client[kRunningIdx]++] = null
if (socket[kWriting]) {
assert(client[kRunning] === 0)
// Response completed before request.
util.destroy(socket, new InformationalError('reset'))
return constants.ERROR.PAUSED
} else if (!shouldKeepAlive) {
util.destroy(socket, new InformationalError('reset'))
return constants.ERROR.PAUSED
} else if (socket[kReset] && client[kRunning] === 0) {
// Destroy socket once all requests have completed.
// The request at the tail of the pipeline is the one
// that requested reset and no further requests should
// have been queued since then.
util.destroy(socket, new InformationalError('reset'))
return constants.ERROR.PAUSED
} else if (client[kPipelining] == null || client[kPipelining] === 1) {
// We must wait a full event loop cycle to reuse this socket to make sure
// that non-spec compliant servers are not closing the connection even if they
// said they won't.
setImmediate(client[kResume])
} else {
client[kResume]()
}
return 0
}
}
function onParserTimeout (parserWeakRef) {
const parser = parserWeakRef.deref()
if (!parser) {
return
}
const { socket, timeoutType, client, paused } = parser
if (timeoutType === TIMEOUT_HEADERS) {
if (!socket[kWriting] || socket.writableNeedDrain || client[kRunning] > 1) {
assert(!paused, 'cannot be paused while waiting for headers')
util.destroy(socket, new HeadersTimeoutError())
}
} else if (timeoutType === TIMEOUT_BODY) {
if (!paused) {
util.destroy(socket, new BodyTimeoutError())
}
} else if (timeoutType === TIMEOUT_KEEP_ALIVE) {
assert(client[kRunning] === 0 && client[kKeepAliveTimeoutValue])
util.destroy(socket, new InformationalError('socket idle timeout'))
}
}
/**
* @param {import ('./client.js')} client
* @param {import('net').Socket} socket
* @returns
*/
function connectH1 (client, socket) {
client[kSocket] = socket
if (!llhttpInstance) {
llhttpInstance = lazyllhttp()
}
if (socket.errored) {
throw socket.errored
}
if (socket.destroyed) {
throw new SocketError('destroyed')
}
socket[kNoRef] = false
socket[kWriting] = false
socket[kReset] = false
socket[kBlocking] = false
socket[kParser] = new Parser(client, socket, llhttpInstance)
util.addListener(socket, 'error', onHttpSocketError)
util.addListener(socket, 'readable', onHttpSocketReadable)
util.addListener(socket, 'end', onHttpSocketEnd)
util.addListener(socket, 'close', onHttpSocketClose)
socket[kClosed] = false
socket.on('close', onSocketClose)
return {
version: 'h1',
defaultPipelining: 1,
write (request) {
return writeH1(client, request)
},
resume () {
resumeH1(client)
},
/**
* @param {Error|undefined} err
* @param {() => void} callback
*/
destroy (err, callback) {
if (socket[kClosed]) {
queueMicrotask(callback)
} else {
socket.on('close', callback)
socket.destroy(err)
}
},
/**
* @returns {boolean}
*/
get destroyed () {
return socket.destroyed
},
/**
* @param {import('../core/request.js')} request
* @returns {boolean}
*/
busy (request) {
if (socket[kWriting] || socket[kReset] || socket[kBlocking]) {
return true
}
if (request) {
if (client[kRunning] > 0 && !request.idempotent) {
// Non-idempotent request cannot be retried.
// Ensure that no other requests are inflight and
// could cause failure.
return true
}
if (client[kRunning] > 0 && (request.upgrade || request.method === 'CONNECT')) {
// Don't dispatch an upgrade until all preceding requests have completed.
// A misbehaving server might upgrade the connection before all pipelined
// request has completed.
return true
}
if (client[kRunning] > 0 && util.bodyLength(request.body) !== 0 &&
(util.isStream(request.body) || util.isAsyncIterable(request.body) || util.isFormDataLike(request.body))) {
// Request with stream or iterator body can error while other requests
// are inflight and indirectly error those as well.
// Ensure this doesn't happen by waiting for inflight
// to complete before dispatching.
// Request with stream or iterator body cannot be retried.
// Ensure that no other requests are inflight and
// could cause failure.
return true
}
}
return false
}
}
}
function onHttpSocketError (err) {
assert(err.code !== 'ERR_TLS_CERT_ALTNAME_INVALID')
const parser = this[kParser]
// On Mac OS, we get an ECONNRESET even if there is a full body to be forwarded
// to the user.
if (err.code === 'ECONNRESET' && parser.statusCode && !parser.shouldKeepAlive) {
// We treat all incoming data so for as a valid response.
parser.onMessageComplete()
return
}
this[kError] = err
this[kClient][kOnError](err)
}
function onHttpSocketReadable () {
this[kParser]?.readMore()
}
function onHttpSocketEnd () {
const parser = this[kParser]
if (parser.statusCode && !parser.shouldKeepAlive) {
// We treat all incoming data so far as a valid response.
parser.onMessageComplete()
return
}
util.destroy(this, new SocketError('other side closed', util.getSocketInfo(this)))
}
function onHttpSocketClose () {
const parser = this[kParser]
if (parser) {
if (!this[kError] && parser.statusCode && !parser.shouldKeepAlive) {
// We treat all incoming data so far as a valid response.
parser.onMessageComplete()
}
this[kParser].destroy()
this[kParser] = null
}
const err = this[kError] || new SocketError('closed', util.getSocketInfo(this))
const client = this[kClient]
client[kSocket] = null
client[kHTTPContext] = null // TODO (fix): This is hacky...
if (client.destroyed) {
assert(client[kPending] === 0)
// Fail entire queue.
const requests = client[kQueue].splice(client[kRunningIdx])
for (let i = 0; i < requests.length; i++) {
const request = requests[i]
util.errorRequest(client, request, err)
}
} else if (client[kRunning] > 0 && err.code !== 'UND_ERR_INFO') {
// Fail head of pipeline.
const request = client[kQueue][client[kRunningIdx]]
client[kQueue][client[kRunningIdx]++] = null
util.errorRequest(client, request, err)
}
client[kPendingIdx] = client[kRunningIdx]
assert(client[kRunning] === 0)
client.emit('disconnect', client[kUrl], [client], err)
client[kResume]()
}
function onSocketClose () {
this[kClosed] = true
}
/**
* @param {import('./client.js')} client
*/
function resumeH1 (client) {
const socket = client[kSocket]
if (socket && !socket.destroyed) {
if (client[kSize] === 0) {
if (!socket[kNoRef] && socket.unref) {
socket.unref()
socket[kNoRef] = true
}
} else if (socket[kNoRef] && socket.ref) {
socket.ref()
socket[kNoRef] = false
}
if (client[kSize] === 0) {
if (socket[kParser].timeoutType !== TIMEOUT_KEEP_ALIVE) {
socket[kParser].setTimeout(client[kKeepAliveTimeoutValue], TIMEOUT_KEEP_ALIVE)
}
} else if (client[kRunning] > 0 && socket[kParser].statusCode < 200) {
if (socket[kParser].timeoutType !== TIMEOUT_HEADERS) {
const request = client[kQueue][client[kRunningIdx]]
const headersTimeout = request.headersTimeout != null
? request.headersTimeout
: client[kHeadersTimeout]
socket[kParser].setTimeout(headersTimeout, TIMEOUT_HEADERS)
}
}
}
}
// https://www.rfc-editor.org/rfc/rfc7230#section-3.3.2
function shouldSendContentLength (method) {
return method !== 'GET' && method !== 'HEAD' && method !== 'OPTIONS' && method !== 'TRACE' && method !== 'CONNECT'
}
/**
* @param {import('./client.js')} client
* @param {import('../core/request.js')} request
* @returns
*/
function writeH1 (client, request) {
const { method, path, host, upgrade, blocking, reset } = request
let { body, headers, contentLength } = request
// https://tools.ietf.org/html/rfc7231#section-4.3.1
// https://tools.ietf.org/html/rfc7231#section-4.3.2
// https://tools.ietf.org/html/rfc7231#section-4.3.5
// Sending a payload body on a request that does not
// expect it can cause undefined behavior on some
// servers and corrupt connection state. Do not
// re-use the connection for further requests.
const expectsPayload = (
method === 'PUT' ||
method === 'POST' ||
method === 'PATCH' ||
method === 'QUERY' ||
method === 'PROPFIND' ||
method === 'PROPPATCH'
)
if (util.isFormDataLike(body)) {
if (!extractBody) {
extractBody = require('../web/fetch/body.js').extractBody
}
const [bodyStream, contentType] = extractBody(body)
if (request.contentType == null) {
headers.push('content-type', contentType)
}
body = bodyStream.stream
contentLength = bodyStream.length
} else if (util.isBlobLike(body) && request.contentType == null && body.type) {
headers.push('content-type', body.type)
}
if (body && typeof body.read === 'function') {
// Try to read EOF in order to get length.
body.read(0)
}
const bodyLength = util.bodyLength(body)
contentLength = bodyLength ?? contentLength
if (contentLength === null) {
contentLength = request.contentLength
}
if (contentLength === 0 && !expectsPayload) {
// https://tools.ietf.org/html/rfc7230#section-3.3.2
// A user agent SHOULD NOT send a Content-Length header field when
// the request message does not contain a payload body and the method
// semantics do not anticipate such a body.
contentLength = null
}
// https://github.com/nodejs/undici/issues/2046
// A user agent may send a Content-Length header with 0 value, this should be allowed.
if (shouldSendContentLength(method) && contentLength > 0 && request.contentLength !== null && request.contentLength !== contentLength) {
if (client[kStrictContentLength]) {
util.errorRequest(client, request, new RequestContentLengthMismatchError())
return false
}
process.emitWarning(new RequestContentLengthMismatchError())
}
const socket = client[kSocket]
/**
* @param {Error} [err]
* @returns {void}
*/
const abort = (err) => {
if (request.aborted || request.completed) {
return
}
util.errorRequest(client, request, err || new RequestAbortedError())
util.destroy(body)
util.destroy(socket, new InformationalError('aborted'))
}
try {
request.onConnect(abort)
} catch (err) {
util.errorRequest(client, request, err)
}
if (request.aborted) {
return false
}
if (method === 'HEAD') {
// https://github.com/mcollina/undici/issues/258
// Close after a HEAD request to interop with misbehaving servers
// that may send a body in the response.
socket[kReset] = true
}
if (upgrade || method === 'CONNECT') {
// On CONNECT or upgrade, block pipeline from dispatching further
// requests on this connection.
socket[kReset] = true
}
if (reset != null) {
socket[kReset] = reset
}
if (client[kMaxRequests] && socket[kCounter]++ >= client[kMaxRequests]) {
socket[kReset] = true
}
if (blocking) {
socket[kBlocking] = true
}
if (socket.setTypeOfService) {
socket.setTypeOfService(request.typeOfService)
}
let header = `${method} ${path} HTTP/1.1\r\n`
if (typeof host === 'string') {
header += `host: ${host}\r\n`
} else {
header += client[kHostHeader]
}
if (upgrade) {
header += `connection: upgrade\r\nupgrade: ${upgrade}\r\n`
} else if (client[kPipelining] && !socket[kReset]) {
header += 'connection: keep-alive\r\n'
} else {
header += 'connection: close\r\n'
}
if (Array.isArray(headers)) {
for (let n = 0; n < headers.length; n += 2) {
const key = headers[n + 0]
const val = headers[n + 1]
if (Array.isArray(val)) {
for (let i = 0; i < val.length; i++) {
header += `${key}: ${val[i]}\r\n`
}
} else {
header += `${key}: ${val}\r\n`
}
}
}
if (channels.sendHeaders.hasSubscribers) {
channels.sendHeaders.publish({ request, headers: header, socket })
}
if (!body || bodyLength === 0) {
writeBuffer(abort, null, client, request, socket, contentLength, header, expectsPayload)
} else if (util.isBuffer(body)) {
writeBuffer(abort, body, client, request, socket, contentLength, header, expectsPayload)
} else if (util.isBlobLike(body)) {
if (typeof body.stream === 'function') {
writeIterable(abort, body.stream(), client, request, socket, contentLength, header, expectsPayload)
} else {
writeBlob(abort, body, client, request, socket, contentLength, header, expectsPayload)
}
} else if (util.isStream(body)) {
writeStream(abort, body, client, request, socket, contentLength, header, expectsPayload)
} else if (util.isIterable(body)) {
writeIterable(abort, body, client, request, socket, contentLength, header, expectsPayload)
} else {
assert(false)
}
return true
}
/**
* @param {AbortCallback} abort
* @param {import('stream').Stream} body
* @param {import('./client.js')} client
* @param {import('../core/request.js')} request
* @param {import('net').Socket} socket
* @param {number} contentLength
* @param {string} header
* @param {boolean} expectsPayload
*/
function writeStream (abort, body, client, request, socket, contentLength, header, expectsPayload) {
assert(contentLength !== 0 || client[kRunning] === 0, 'stream body cannot be pipelined')
let finished = false
const writer = new AsyncWriter({ abort, socket, request, contentLength, client, expectsPayload, header })
/**
* @param {Buffer} chunk
* @returns {void}
*/
const onData = function (chunk) {
if (finished) {
return
}
try {
if (!writer.write(chunk) && this.pause) {
this.pause()
}
} catch (err) {
util.destroy(this, err)
}
}
/**
* @returns {void}
*/
const onDrain = function () {
if (finished) {
return
}
if (body.resume) {
body.resume()
}
}
/**
* @returns {void}
*/
const onClose = function () {
// 'close' might be emitted *before* 'error' for
// broken streams. Wait a tick to avoid this case.
queueMicrotask(() => {
// It's only safe to remove 'error' listener after
// 'close'.
body.removeListener('error', onFinished)
})
if (!finished) {
const err = new RequestAbortedError()
queueMicrotask(() => onFinished(err))
}
}
/**
* @param {Error} [err]
* @returns
*/
const onFinished = function (err) {
if (finished) {
return
}
finished = true
assert(socket.destroyed || (socket[kWriting] && client[kRunning] <= 1))
socket
.off('drain', onDrain)
.off('error', onFinished)
body
.removeListener('data', onData)
.removeListener('end', onFinished)
.removeListener('close', onClose)
if (!err) {
try {
writer.end()
} catch (er) {
err = er
}
}
writer.destroy(err)
if (err && (err.code !== 'UND_ERR_INFO' || err.message !== 'reset')) {
util.destroy(body, err)
} else {
util.destroy(body)
}
}
body
.on('data', onData)
.on('end', onFinished)
.on('error', onFinished)
.on('close', onClose)
if (body.resume) {
body.resume()
}
socket
.on('drain', onDrain)
.on('error', onFinished)
if (body.errorEmitted ?? body.errored) {
setImmediate(onFinished, body.errored)
} else if (body.endEmitted ?? body.readableEnded) {
setImmediate(onFinished, null)
}
if (body.closeEmitted ?? body.closed) {
setImmediate(onClose)
}
}
/**
* @typedef AbortCallback
* @type {Function}
* @param {Error} [err]
* @returns {void}
*/
/**
* @param {AbortCallback} abort
* @param {Uint8Array|null} body
* @param {import('./client.js')} client
* @param {import('../core/request.js')} request
* @param {import('net').Socket} socket
* @param {number} contentLength
* @param {string} header
* @param {boolean} expectsPayload
* @returns {void}
*/
function writeBuffer (abort, body, client, request, socket, contentLength, header, expectsPayload) {
try {
if (!body) {
if (contentLength === 0) {
socket.write(`${header}content-length: 0\r\n\r\n`, 'latin1')
} else {
assert(contentLength === null, 'no body must not have content length')
socket.write(`${header}\r\n`, 'latin1')
}
} else if (util.isBuffer(body)) {
assert(contentLength === body.byteLength, 'buffer body must have content length')
socket.cork()
socket.write(`${header}content-length: ${contentLength}\r\n\r\n`, 'latin1')
socket.write(body)
socket.uncork()
request.onBodySent(body)
if (!expectsPayload && request.reset !== false) {
socket[kReset] = true
}
}
request.onRequestSent()
client[kResume]()
} catch (err) {
abort(err)
}
}
/**
* @param {AbortCallback} abort
* @param {Blob} body
* @param {import('./client.js')} client
* @param {import('../core/request.js')} request
* @param {import('net').Socket} socket
* @param {number} contentLength
* @param {string} header
* @param {boolean} expectsPayload
* @returns {Promise}
*/
async function writeBlob (abort, body, client, request, socket, contentLength, header, expectsPayload) {
assert(contentLength === body.size, 'blob body must have content length')
try {
if (contentLength != null && contentLength !== body.size) {
throw new RequestContentLengthMismatchError()
}
const buffer = Buffer.from(await body.arrayBuffer())
socket.cork()
socket.write(`${header}content-length: ${contentLength}\r\n\r\n`, 'latin1')
socket.write(buffer)
socket.uncork()
request.onBodySent(buffer)
request.onRequestSent()
if (!expectsPayload && request.reset !== false) {
socket[kReset] = true
}
client[kResume]()
} catch (err) {
abort(err)
}
}
/**
* @param {AbortCallback} abort
* @param {Iterable} body
* @param {import('./client.js')} client
* @param {import('../core/request.js')} request
* @param {import('net').Socket} socket
* @param {number} contentLength
* @param {string} header
* @param {boolean} expectsPayload
* @returns {Promise}
*/
async function writeIterable (abort, body, client, request, socket, contentLength, header, expectsPayload) {
assert(contentLength !== 0 || client[kRunning] === 0, 'iterator body cannot be pipelined')
let callback = null
function onDrain () {
if (callback) {
const cb = callback
callback = null
cb()
}
}
const waitForDrain = () => new Promise((resolve, reject) => {
assert(callback === null)
if (socket[kError]) {
reject(socket[kError])
} else {
callback = resolve
}
})
socket
.on('close', onDrain)
.on('drain', onDrain)
const writer = new AsyncWriter({ abort, socket, request, contentLength, client, expectsPayload, header })
try {
// It's up to the user to somehow abort the async iterable.
for await (const chunk of body) {
if (socket[kError]) {
throw socket[kError]
}
if (!writer.write(chunk)) {
await waitForDrain()
}
}
writer.end()
} catch (err) {
writer.destroy(err)
} finally {
socket
.off('close', onDrain)
.off('drain', onDrain)
}
}
class AsyncWriter {
/**
*
* @param {object} arg
* @param {AbortCallback} arg.abort
* @param {import('net').Socket} arg.socket
* @param {import('../core/request.js')} arg.request
* @param {number} arg.contentLength
* @param {import('./client.js')} arg.client
* @param {boolean} arg.expectsPayload
* @param {string} arg.header
*/
constructor ({ abort, socket, request, contentLength, client, expectsPayload, header }) {
this.socket = socket
this.request = request
this.contentLength = contentLength
this.client = client
this.bytesWritten = 0
this.expectsPayload = expectsPayload
this.header = header
this.abort = abort
socket[kWriting] = true
}
/**
* @param {Buffer} chunk
* @returns
*/
write (chunk) {
const { socket, request, contentLength, client, bytesWritten, expectsPayload, header } = this
if (socket[kError]) {
throw socket[kError]
}
if (socket.destroyed) {
return false
}
const len = Buffer.byteLength(chunk)
if (!len) {
return true
}
// We should defer writing chunks.
if (contentLength !== null && bytesWritten + len > contentLength) {
if (client[kStrictContentLength]) {
throw new RequestContentLengthMismatchError()
}
process.emitWarning(new RequestContentLengthMismatchError())
}
socket.cork()
if (bytesWritten === 0) {
if (!expectsPayload && request.reset !== false) {
socket[kReset] = true
}
if (contentLength === null) {
socket.write(`${header}transfer-encoding: chunked\r\n`, 'latin1')
} else {
socket.write(`${header}content-length: ${contentLength}\r\n\r\n`, 'latin1')
}
}
if (contentLength === null) {
socket.write(`\r\n${len.toString(16)}\r\n`, 'latin1')
}
this.bytesWritten += len
const ret = socket.write(chunk)
socket.uncork()
request.onBodySent(chunk)
if (!ret) {
if (socket[kParser].timeout && socket[kParser].timeoutType === TIMEOUT_HEADERS) {
if (socket[kParser].timeout.refresh) {
socket[kParser].timeout.refresh()
}
}
}
return ret
}
/**
* @returns {void}
*/
end () {
const { socket, contentLength, client, bytesWritten, expectsPayload, header, request } = this
request.onRequestSent()
socket[kWriting] = false
if (socket[kError]) {
throw socket[kError]
}
if (socket.destroyed) {
return
}
if (bytesWritten === 0) {
if (expectsPayload) {
// https://tools.ietf.org/html/rfc7230#section-3.3.2
// A user agent SHOULD send a Content-Length in a request message when
// no Transfer-Encoding is sent and the request method defines a meaning
// for an enclosed payload body.
socket.write(`${header}content-length: 0\r\n\r\n`, 'latin1')
} else {
socket.write(`${header}\r\n`, 'latin1')
}
} else if (contentLength === null) {
socket.write('\r\n0\r\n\r\n', 'latin1')
}
if (contentLength !== null && bytesWritten !== contentLength) {
if (client[kStrictContentLength]) {
throw new RequestContentLengthMismatchError()
} else {
process.emitWarning(new RequestContentLengthMismatchError())
}
}
if (socket[kParser].timeout && socket[kParser].timeoutType === TIMEOUT_HEADERS) {
if (socket[kParser].timeout.refresh) {
socket[kParser].timeout.refresh()
}
}
client[kResume]()
}
/**
* @param {Error} [err]
* @returns {void}
*/
destroy (err) {
const { socket, client, abort } = this
socket[kWriting] = false
if (err) {
assert(client[kRunning] <= 1, 'pipeline should only contain this request')
abort(err)
}
}
}
module.exports = connectH1
================================================
FILE: lib/dispatcher/client-h2.js
================================================
'use strict'
const assert = require('node:assert')
const { pipeline } = require('node:stream')
const util = require('../core/util.js')
const {
RequestContentLengthMismatchError,
RequestAbortedError,
SocketError,
InformationalError,
InvalidArgumentError
} = require('../core/errors.js')
const {
kUrl,
kReset,
kClient,
kRunning,
kPending,
kQueue,
kPendingIdx,
kRunningIdx,
kError,
kSocket,
kStrictContentLength,
kOnError,
kMaxConcurrentStreams,
kPingInterval,
kHTTP2Session,
kHTTP2InitialWindowSize,
kHTTP2ConnectionWindowSize,
kResume,
kSize,
kHTTPContext,
kClosed,
kBodyTimeout,
kEnableConnectProtocol,
kRemoteSettings,
kHTTP2Stream,
kHTTP2SessionState
} = require('../core/symbols.js')
const { channels } = require('../core/diagnostics.js')
const kOpenStreams = Symbol('open streams')
let extractBody
/** @type {import('http2')} */
let http2
try {
http2 = require('node:http2')
} catch {
// @ts-ignore
http2 = { constants: {} }
}
const {
constants: {
HTTP2_HEADER_AUTHORITY,
HTTP2_HEADER_METHOD,
HTTP2_HEADER_PATH,
HTTP2_HEADER_SCHEME,
HTTP2_HEADER_CONTENT_LENGTH,
HTTP2_HEADER_EXPECT,
HTTP2_HEADER_STATUS,
HTTP2_HEADER_PROTOCOL,
NGHTTP2_REFUSED_STREAM,
NGHTTP2_CANCEL
}
} = http2
function parseH2Headers (headers) {
const result = []
for (const [name, value] of Object.entries(headers)) {
// h2 may concat the header value by array
// e.g. Set-Cookie
if (Array.isArray(value)) {
for (const subvalue of value) {
// we need to provide each header value of header name
// because the headers handler expect name-value pair
result.push(Buffer.from(name), Buffer.from(subvalue))
}
} else {
result.push(Buffer.from(name), Buffer.from(value))
}
}
return result
}
function connectH2 (client, socket) {
client[kSocket] = socket
const http2InitialWindowSize = client[kHTTP2InitialWindowSize]
const http2ConnectionWindowSize = client[kHTTP2ConnectionWindowSize]
const session = http2.connect(client[kUrl], {
createConnection: () => socket,
peerMaxConcurrentStreams: client[kMaxConcurrentStreams],
settings: {
// TODO(metcoder95): add support for PUSH
enablePush: false,
...(http2InitialWindowSize != null ? { initialWindowSize: http2InitialWindowSize } : null)
}
})
client[kSocket] = socket
session[kOpenStreams] = 0
session[kClient] = client
session[kSocket] = socket
session[kHTTP2SessionState] = {
ping: {
interval: client[kPingInterval] === 0 ? null : setInterval(onHttp2SendPing, client[kPingInterval], session).unref()
}
}
// We set it to true by default in a best-effort; however once connected to an H2 server
// we will check if extended CONNECT protocol is supported or not
// and set this value accordingly.
session[kEnableConnectProtocol] = false
// States whether or not we have received the remote settings from the server
session[kRemoteSettings] = false
// Apply connection-level flow control once connected (if supported).
if (http2ConnectionWindowSize) {
util.addListener(session, 'connect', applyConnectionWindowSize.bind(session, http2ConnectionWindowSize))
}
util.addListener(session, 'error', onHttp2SessionError)
util.addListener(session, 'frameError', onHttp2FrameError)
util.addListener(session, 'end', onHttp2SessionEnd)
util.addListener(session, 'goaway', onHttp2SessionGoAway)
util.addListener(session, 'close', onHttp2SessionClose)
util.addListener(session, 'remoteSettings', onHttp2RemoteSettings)
// TODO (@metcoder95): implement SETTINGS support
// util.addListener(session, 'localSettings', onHttp2RemoteSettings)
session.unref()
client[kHTTP2Session] = session
socket[kHTTP2Session] = session
util.addListener(socket, 'error', onHttp2SocketError)
util.addListener(socket, 'end', onHttp2SocketEnd)
util.addListener(socket, 'close', onHttp2SocketClose)
socket[kClosed] = false
socket.on('close', onSocketClose)
return {
version: 'h2',
defaultPipelining: Infinity,
/**
* @param {import('../core/request.js')} request
* @returns {boolean}
*/
write (request) {
return writeH2(client, request)
},
/**
* @returns {void}
*/
resume () {
resumeH2(client)
},
/**
* @param {Error | null} err
* @param {() => void} callback
*/
destroy (err, callback) {
if (socket[kClosed]) {
queueMicrotask(callback)
} else {
socket.destroy(err).on('close', callback)
}
},
/**
* @type {boolean}
*/
get destroyed () {
return socket.destroyed
},
/**
* @param {import('../core/request.js')} request
* @returns {boolean}
*/
busy (request) {
if (request != null) {
if (client[kRunning] > 0) {
// We are already processing requests
// Non-idempotent request cannot be retried.
// Ensure that no other requests are inflight and
// could cause failure.
if (request.idempotent === false) return true
// Don't dispatch an upgrade until all preceding requests have completed.
// Possibly, we do not have remote settings confirmed yet.
if ((request.upgrade === 'websocket' || request.method === 'CONNECT') && session[kRemoteSettings] === false) return true
// Request with stream or iterator body can error while other requests
// are inflight and indirectly error those as well.
// Ensure this doesn't happen by waiting for inflight
// to complete before dispatching.
// Request with stream or iterator body cannot be retried.
// Ensure that no other requests are inflight and
// could cause failure.
if (util.bodyLength(request.body) !== 0 &&
(util.isStream(request.body) || util.isAsyncIterable(request.body) || util.isFormDataLike(request.body))) return true
} else {
return (request.upgrade === 'websocket' || request.method === 'CONNECT') && session[kRemoteSettings] === false
}
}
return false
}
}
}
function resumeH2 (client) {
const socket = client[kSocket]
if (socket?.destroyed === false) {
if (client[kSize] === 0 || client[kMaxConcurrentStreams] === 0) {
socket.unref()
client[kHTTP2Session].unref()
} else {
socket.ref()
client[kHTTP2Session].ref()
}
}
}
function applyConnectionWindowSize (connectionWindowSize) {
try {
if (typeof this.setLocalWindowSize === 'function') {
this.setLocalWindowSize(connectionWindowSize)
}
} catch {
// Best-effort only.
}
}
function onHttp2RemoteSettings (settings) {
// Fallbacks are a safe bet, remote setting will always override
this[kClient][kMaxConcurrentStreams] = settings.maxConcurrentStreams ?? this[kClient][kMaxConcurrentStreams]
/**
* From RFC-8441
* A sender MUST NOT send a SETTINGS_ENABLE_CONNECT_PROTOCOL parameter
* with the value of 0 after previously sending a value of 1.
*/
// Note: Cannot be tested in Node, it does not supports disabling the extended CONNECT protocol once enabled
if (this[kRemoteSettings] === true && this[kEnableConnectProtocol] === true && settings.enableConnectProtocol === false) {
const err = new InformationalError('HTTP/2: Server disabled extended CONNECT protocol against RFC-8441')
this[kSocket][kError] = err
this[kClient][kOnError](err)
return
}
this[kEnableConnectProtocol] = settings.enableConnectProtocol ?? this[kEnableConnectProtocol]
this[kRemoteSettings] = true
this[kClient][kResume]()
}
function onHttp2SendPing (session) {
const state = session[kHTTP2SessionState]
if ((session.closed || session.destroyed) && state.ping.interval != null) {
clearInterval(state.ping.interval)
state.ping.interval = null
return
}
// If no ping sent, do nothing
session.ping(onPing.bind(session))
function onPing (err, duration) {
const client = this[kClient]
const socket = this[kClient]
if (err != null) {
const error = new InformationalError(`HTTP/2: "PING" errored - type ${err.message}`)
socket[kError] = error
client[kOnError](error)
} else {
client.emit('ping', duration)
}
}
}
function onHttp2SessionError (err) {
assert(err.code !== 'ERR_TLS_CERT_ALTNAME_INVALID')
this[kSocket][kError] = err
this[kClient][kOnError](err)
}
function onHttp2FrameError (type, code, id) {
if (id === 0) {
const err = new InformationalError(`HTTP/2: "frameError" received - type ${type}, code ${code}`)
this[kSocket][kError] = err
this[kClient][kOnError](err)
}
}
function onHttp2SessionEnd () {
const err = new SocketError('other side closed', util.getSocketInfo(this[kSocket]))
this.destroy(err)
util.destroy(this[kSocket], err)
}
/**
* This is the root cause of #3011
* We need to handle GOAWAY frames properly, and trigger the session close
* along with the socket right away
*
* @this {import('http2').ClientHttp2Session}
* @param {number} errorCode
*/
function onHttp2SessionGoAway (errorCode) {
// TODO(mcollina): Verify if GOAWAY implements the spec correctly:
// https://datatracker.ietf.org/doc/html/rfc7540#section-6.8
// Specifically, we do not verify the "valid" stream id.
const err = this[kError] || new SocketError(`HTTP/2: "GOAWAY" frame received with code ${errorCode}`, util.getSocketInfo(this[kSocket]))
const client = this[kClient]
client[kSocket] = null
client[kHTTPContext] = null
// this is an HTTP2 session
this.close()
this[kHTTP2Session] = null
util.destroy(this[kSocket], err)
// Fail head of pipeline.
if (client[kRunningIdx] < client[kQueue].length) {
const request = client[kQueue][client[kRunningIdx]]
client[kQueue][client[kRunningIdx]++] = null
util.errorRequest(client, request, err)
client[kPendingIdx] = client[kRunningIdx]
}
assert(client[kRunning] === 0)
client.emit('disconnect', client[kUrl], [client], err)
client.emit('connectionError', client[kUrl], [client], err)
client[kResume]()
}
function onHttp2SessionClose () {
const { [kClient]: client, [kHTTP2SessionState]: state } = this
const { [kSocket]: socket } = client
const err = this[kSocket][kError] || this[kError] || new SocketError('closed', util.getSocketInfo(socket))
client[kSocket] = null
client[kHTTPContext] = null
if (state.ping.interval != null) {
clearInterval(state.ping.interval)
state.ping.interval = null
}
if (client.destroyed) {
assert(client[kPending] === 0)
// Fail entire queue.
const requests = client[kQueue].splice(client[kRunningIdx])
for (let i = 0; i < requests.length; i++) {
const request = requests[i]
util.errorRequest(client, request, err)
}
}
}
function onHttp2SocketClose () {
const err = this[kError] || new SocketError('closed', util.getSocketInfo(this))
const client = this[kHTTP2Session][kClient]
client[kSocket] = null
client[kHTTPContext] = null
if (this[kHTTP2Session] !== null) {
this[kHTTP2Session].destroy(err)
}
client[kPendingIdx] = client[kRunningIdx]
assert(client[kRunning] === 0)
client.emit('disconnect', client[kUrl], [client], err)
client[kResume]()
}
function onHttp2SocketError (err) {
assert(err.code !== 'ERR_TLS_CERT_ALTNAME_INVALID')
this[kError] = err
this[kClient][kOnError](err)
}
function onHttp2SocketEnd () {
util.destroy(this, new SocketError('other side closed', util.getSocketInfo(this)))
}
function onSocketClose () {
this[kClosed] = true
}
// https://www.rfc-editor.org/rfc/rfc7230#section-3.3.2
function shouldSendContentLength (method) {
return method !== 'GET' && method !== 'HEAD' && method !== 'OPTIONS' && method !== 'TRACE' && method !== 'CONNECT'
}
function writeH2 (client, request) {
const requestTimeout = request.bodyTimeout ?? client[kBodyTimeout]
const session = client[kHTTP2Session]
const { method, path, host, upgrade, expectContinue, signal, protocol, headers: reqHeaders } = request
let { body } = request
if (upgrade != null && upgrade !== 'websocket') {
util.errorRequest(client, request, new InvalidArgumentError(`Custom upgrade "${upgrade}" not supported over HTTP/2`))
return false
}
const headers = {}
for (let n = 0; n < reqHeaders.length; n += 2) {
const key = reqHeaders[n + 0]
const val = reqHeaders[n + 1]
if (key === 'cookie') {
if (headers[key] != null) {
headers[key] = Array.isArray(headers[key]) ? (headers[key].push(val), headers[key]) : [headers[key], val]
} else {
headers[key] = val
}
continue
}
if (Array.isArray(val)) {
for (let i = 0; i < val.length; i++) {
if (headers[key]) {
headers[key] += `, ${val[i]}`
} else {
headers[key] = val[i]
}
}
} else if (headers[key]) {
headers[key] += `, ${val}`
} else {
headers[key] = val
}
}
/** @type {import('node:http2').ClientHttp2Stream} */
let stream = null
const { hostname, port } = client[kUrl]
headers[HTTP2_HEADER_AUTHORITY] = host || `${hostname}${port ? `:${port}` : ''}`
headers[HTTP2_HEADER_METHOD] = method
const abort = (err) => {
if (request.aborted || request.completed) {
return
}
err = err || new RequestAbortedError()
util.errorRequest(client, request, err)
if (stream != null) {
// Some chunks might still come after abort,
// let's ignore them
stream.removeAllListeners('data')
// On Abort, we close the stream to send RST_STREAM frame
stream.close()
// We move the running index to the next request
client[kOnError](err)
client[kResume]()
}
// We do not destroy the socket as we can continue using the session
// the stream gets destroyed and the session remains to create new streams
util.destroy(body, err)
}
try {
// We are already connected, streams are pending.
// We can call on connect, and wait for abort
request.onConnect(abort)
} catch (err) {
util.errorRequest(client, request, err)
}
if (request.aborted) {
return false
}
if (upgrade || method === 'CONNECT') {
session.ref()
if (upgrade === 'websocket') {
// We cannot upgrade to websocket if extended CONNECT protocol is not supported
if (session[kEnableConnectProtocol] === false) {
util.errorRequest(client, request, new InformationalError('HTTP/2: Extended CONNECT protocol not supported by server'))
session.unref()
return false
}
// We force the method to CONNECT
// as per RFC-8441
// https://datatracker.ietf.org/doc/html/rfc8441#section-4
headers[HTTP2_HEADER_METHOD] = 'CONNECT'
headers[HTTP2_HEADER_PROTOCOL] = 'websocket'
// :path and :scheme headers must be omitted when sending CONNECT but set if extended-CONNECT
headers[HTTP2_HEADER_PATH] = path
if (protocol === 'ws:' || protocol === 'wss:') {
headers[HTTP2_HEADER_SCHEME] = protocol === 'ws:' ? 'http' : 'https'
} else {
headers[HTTP2_HEADER_SCHEME] = protocol === 'http:' ? 'http' : 'https'
}
stream = session.request(headers, { endStream: false, signal })
stream[kHTTP2Stream] = true
stream.once('response', (headers, _flags) => {
const { [HTTP2_HEADER_STATUS]: statusCode, ...realHeaders } = headers
request.onUpgrade(statusCode, parseH2Headers(realHeaders), stream)
++session[kOpenStreams]
client[kQueue][client[kRunningIdx]++] = null
})
stream.on('error', () => {
if (stream.rstCode === NGHTTP2_REFUSED_STREAM || stream.rstCode === NGHTTP2_CANCEL) {
// NGHTTP2_REFUSED_STREAM (7) or NGHTTP2_CANCEL (8)
// We do not treat those as errors as the server might
// not support websockets and refuse the stream
abort(new InformationalError(`HTTP/2: "stream error" received - code ${stream.rstCode}`))
}
})
stream.once('close', () => {
session[kOpenStreams] -= 1
if (session[kOpenStreams] === 0) session.unref()
})
stream.setTimeout(requestTimeout)
return true
}
// TODO: consolidate once we support CONNECT properly
// NOTE: We are already connected, streams are pending, first request
// will create a new stream. We trigger a request to create the stream and wait until
// `ready` event is triggered
// We disabled endStream to allow the user to write to the stream
stream = session.request(headers, { endStream: false, signal })
stream[kHTTP2Stream] = true
stream.on('response', headers => {
const { [HTTP2_HEADER_STATUS]: statusCode, ...realHeaders } = headers
request.onUpgrade(statusCode, parseH2Headers(realHeaders), stream)
++session[kOpenStreams]
client[kQueue][client[kRunningIdx]++] = null
})
stream.once('close', () => {
session[kOpenStreams] -= 1
if (session[kOpenStreams] === 0) session.unref()
})
stream.setTimeout(requestTimeout)
return true
}
// https://tools.ietf.org/html/rfc7540#section-8.3
// :path and :scheme headers must be omitted when sending CONNECT
headers[HTTP2_HEADER_PATH] = path
headers[HTTP2_HEADER_SCHEME] = protocol === 'http:' ? 'http' : 'https'
// https://tools.ietf.org/html/rfc7231#section-4.3.1
// https://tools.ietf.org/html/rfc7231#section-4.3.2
// https://tools.ietf.org/html/rfc7231#section-4.3.5
// Sending a payload body on a request that does not
// expect it can cause undefined behavior on some
// servers and corrupt connection state. Do not
// re-use the connection for further requests.
const expectsPayload = (
method === 'PUT' ||
method === 'POST' ||
method === 'PATCH'
)
if (body && typeof body.read === 'function') {
// Try to read EOF in order to get length.
body.read(0)
}
let contentLength = util.bodyLength(body)
if (util.isFormDataLike(body)) {
extractBody ??= require('../web/fetch/body.js').extractBody
const [bodyStream, contentType] = extractBody(body)
headers['content-type'] = contentType
body = bodyStream.stream
contentLength = bodyStream.length
}
if (contentLength == null) {
contentLength = request.contentLength
}
if (!expectsPayload) {
// https://tools.ietf.org/html/rfc7230#section-3.3.2
// A user agent SHOULD NOT send a Content-Length header field when
// the request message does not contain a payload body and the method
// semantics do not anticipate such a body.
// And for methods that don't expect a payload, omit Content-Length.
contentLength = null
}
// https://github.com/nodejs/undici/issues/2046
// A user agent may send a Content-Length header with 0 value, this should be allowed.
if (shouldSendContentLength(method) && contentLength > 0 && request.contentLength != null && request.contentLength !== contentLength) {
if (client[kStrictContentLength]) {
util.errorRequest(client, request, new RequestContentLengthMismatchError())
return false
}
process.emitWarning(new RequestContentLengthMismatchError())
}
if (contentLength != null) {
assert(body || contentLength === 0, 'no body must not have content length')
headers[HTTP2_HEADER_CONTENT_LENGTH] = `${contentLength}`
}
session.ref()
if (channels.sendHeaders.hasSubscribers) {
let header = ''
for (const key in headers) {
header += `${key}: ${headers[key]}\r\n`
}
channels.sendHeaders.publish({ request, headers: header, socket: session[kSocket] })
}
// TODO(metcoder95): add support for sending trailers
const shouldEndStream = method === 'GET' || method === 'HEAD' || body === null
if (expectContinue) {
headers[HTTP2_HEADER_EXPECT] = '100-continue'
stream = session.request(headers, { endStream: shouldEndStream, signal })
stream[kHTTP2Stream] = true
stream.once('continue', writeBodyH2)
} else {
stream = session.request(headers, {
endStream: shouldEndStream,
signal
})
stream[kHTTP2Stream] = true
writeBodyH2()
}
// Increment counter as we have new streams open
++session[kOpenStreams]
stream.setTimeout(requestTimeout)
// Track whether we received a response (headers)
let responseReceived = false
stream.once('response', headers => {
const { [HTTP2_HEADER_STATUS]: statusCode, ...realHeaders } = headers
request.onResponseStarted()
responseReceived = true
// Due to the stream nature, it is possible we face a race condition
// where the stream has been assigned, but the request has been aborted
// the request remains in-flight and headers hasn't been received yet
// for those scenarios, best effort is to destroy the stream immediately
// as there's no value to keep it open.
if (request.aborted) {
stream.removeAllListeners('data')
return
}
if (request.onHeaders(Number(statusCode), parseH2Headers(realHeaders), stream.resume.bind(stream), '') === false) {
stream.pause()
}
stream.on('data', (chunk) => {
if (request.aborted || request.completed) {
return
}
if (request.onData(chunk) === false) {
stream.pause()
}
})
})
stream.once('end', () => {
stream.removeAllListeners('data')
// If we received a response, this is a normal completion
if (responseReceived) {
if (!request.aborted && !request.completed) {
request.onComplete({})
}
client[kQueue][client[kRunningIdx]++] = null
client[kResume]()
} else {
// Stream ended without receiving a response - this is an error
// (e.g., server destroyed the stream before sending headers)
abort(new InformationalError('HTTP/2: stream half-closed (remote)'))
client[kQueue][client[kRunningIdx]++] = null
client[kPendingIdx] = client[kRunningIdx]
client[kResume]()
}
})
stream.once('close', () => {
stream.removeAllListeners('data')
session[kOpenStreams] -= 1
if (session[kOpenStreams] === 0) {
session.unref()
}
})
stream.once('error', function (err) {
stream.removeAllListeners('data')
abort(err)
})
stream.once('frameError', (type, code) => {
stream.removeAllListeners('data')
abort(new InformationalError(`HTTP/2: "frameError" received - type ${type}, code ${code}`))
})
stream.on('aborted', () => {
stream.removeAllListeners('data')
})
stream.on('timeout', () => {
const err = new InformationalError(`HTTP/2: "stream timeout after ${requestTimeout}"`)
stream.removeAllListeners('data')
session[kOpenStreams] -= 1
if (session[kOpenStreams] === 0) {
session.unref()
}
abort(err)
})
stream.once('trailers', trailers => {
if (request.aborted || request.completed) {
return
}
stream.removeAllListeners('data')
request.onComplete(trailers)
})
return true
function writeBodyH2 () {
if (!body || contentLength === 0) {
writeBuffer(
abort,
stream,
null,
client,
request,
client[kSocket],
contentLength,
expectsPayload
)
} else if (util.isBuffer(body)) {
writeBuffer(
abort,
stream,
body,
client,
request,
client[kSocket],
contentLength,
expectsPayload
)
} else if (util.isBlobLike(body)) {
if (typeof body.stream === 'function') {
writeIterable(
abort,
stream,
body.stream(),
client,
request,
client[kSocket],
contentLength,
expectsPayload
)
} else {
writeBlob(
abort,
stream,
body,
client,
request,
client[kSocket],
contentLength,
expectsPayload
)
}
} else if (util.isStream(body)) {
writeStream(
abort,
client[kSocket],
expectsPayload,
stream,
body,
client,
request,
contentLength
)
} else if (util.isIterable(body)) {
writeIterable(
abort,
stream,
body,
client,
request,
client[kSocket],
contentLength,
expectsPayload
)
} else {
assert(false)
}
}
}
function writeBuffer (abort, h2stream, body, client, request, socket, contentLength, expectsPayload) {
try {
if (body != null && util.isBuffer(body)) {
assert(contentLength === body.byteLength, 'buffer body must have content length')
h2stream.cork()
h2stream.write(body)
h2stream.uncork()
h2stream.end()
request.onBodySent(body)
}
if (!expectsPayload) {
socket[kReset] = true
}
request.onRequestSent()
client[kResume]()
} catch (error) {
abort(error)
}
}
function writeStream (abort, socket, expectsPayload, h2stream, body, client, request, contentLength) {
assert(contentLength !== 0 || client[kRunning] === 0, 'stream body cannot be pipelined')
// For HTTP/2, is enough to pipe the stream
const pipe = pipeline(
body,
h2stream,
(err) => {
if (err) {
util.destroy(pipe, err)
abort(err)
} else {
util.removeAllListeners(pipe)
request.onRequestSent()
if (!expectsPayload) {
socket[kReset] = true
}
client[kResume]()
}
}
)
util.addListener(pipe, 'data', onPipeData)
function onPipeData (chunk) {
request.onBodySent(chunk)
}
}
async function writeBlob (abort, h2stream, body, client, request, socket, contentLength, expectsPayload) {
assert(contentLength === body.size, 'blob body must have content length')
try {
if (contentLength != null && contentLength !== body.size) {
throw new RequestContentLengthMismatchError()
}
const buffer = Buffer.from(await body.arrayBuffer())
h2stream.cork()
h2stream.write(buffer)
h2stream.uncork()
h2stream.end()
request.onBodySent(buffer)
request.onRequestSent()
if (!expectsPayload) {
socket[kReset] = true
}
client[kResume]()
} catch (err) {
abort(err)
}
}
async function writeIterable (abort, h2stream, body, client, request, socket, contentLength, expectsPayload) {
assert(contentLength !== 0 || client[kRunning] === 0, 'iterator body cannot be pipelined')
let callback = null
function onDrain () {
if (callback) {
const cb = callback
callback = null
cb()
}
}
const waitForDrain = () => new Promise((resolve, reject) => {
assert(callback === null)
if (socket[kError]) {
reject(socket[kError])
} else {
callback = resolve
}
})
h2stream
.on('close', onDrain)
.on('drain', onDrain)
try {
// It's up to the user to somehow abort the async iterable.
for await (const chunk of body) {
if (socket[kError]) {
throw socket[kError]
}
const res = h2stream.write(chunk)
request.onBodySent(chunk)
if (!res) {
await waitForDrain()
}
}
h2stream.end()
request.onRequestSent()
if (!expectsPayload) {
socket[kReset] = true
}
client[kResume]()
} catch (err) {
abort(err)
} finally {
h2stream
.off('close', onDrain)
.off('drain', onDrain)
}
}
module.exports = connectH2
================================================
FILE: lib/dispatcher/client.js
================================================
'use strict'
const assert = require('node:assert')
const net = require('node:net')
const http = require('node:http')
const util = require('../core/util.js')
const { ClientStats } = require('../util/stats.js')
const { channels } = require('../core/diagnostics.js')
const Request = require('../core/request.js')
const DispatcherBase = require('./dispatcher-base')
const {
InvalidArgumentError,
InformationalError,
ClientDestroyedError
} = require('../core/errors.js')
const buildConnector = require('../core/connect.js')
const {
kUrl,
kServerName,
kClient,
kBusy,
kConnect,
kResuming,
kRunning,
kPending,
kSize,
kQueue,
kConnected,
kConnecting,
kNeedDrain,
kKeepAliveDefaultTimeout,
kHostHeader,
kPendingIdx,
kRunningIdx,
kError,
kPipelining,
kKeepAliveTimeoutValue,
kMaxHeadersSize,
kKeepAliveMaxTimeout,
kKeepAliveTimeoutThreshold,
kHeadersTimeout,
kBodyTimeout,
kStrictContentLength,
kConnector,
kMaxRequests,
kCounter,
kClose,
kDestroy,
kDispatch,
kLocalAddress,
kMaxResponseSize,
kOnError,
kHTTPContext,
kMaxConcurrentStreams,
kHTTP2InitialWindowSize,
kHTTP2ConnectionWindowSize,
kResume,
kPingInterval
} = require('../core/symbols.js')
const connectH1 = require('./client-h1.js')
const connectH2 = require('./client-h2.js')
const kClosedResolve = Symbol('kClosedResolve')
const getDefaultNodeMaxHeaderSize = http &&
http.maxHeaderSize &&
Number.isInteger(http.maxHeaderSize) &&
http.maxHeaderSize > 0
? () => http.maxHeaderSize
: () => { throw new InvalidArgumentError('http module not available or http.maxHeaderSize invalid') }
const noop = () => { }
function getPipelining (client) {
return client[kPipelining] ?? client[kHTTPContext]?.defaultPipelining ?? 1
}
/**
* @type {import('../../types/client.js').default}
*/
class Client extends DispatcherBase {
/**
*
* @param {string|URL} url
* @param {import('../../types/client.js').Client.Options} options
*/
constructor (url, {
maxHeaderSize,
headersTimeout,
socketTimeout,
requestTimeout,
connectTimeout,
bodyTimeout,
idleTimeout,
keepAlive,
keepAliveTimeout,
maxKeepAliveTimeout,
keepAliveMaxTimeout,
keepAliveTimeoutThreshold,
socketPath,
pipelining,
tls,
strictContentLength,
maxCachedSessions,
connect,
maxRequestsPerClient,
localAddress,
maxResponseSize,
autoSelectFamily,
autoSelectFamilyAttemptTimeout,
// h2
maxConcurrentStreams,
allowH2,
useH2c,
initialWindowSize,
connectionWindowSize,
pingInterval
} = {}) {
if (keepAlive !== undefined) {
throw new InvalidArgumentError('unsupported keepAlive, use pipelining=0 instead')
}
if (socketTimeout !== undefined) {
throw new InvalidArgumentError('unsupported socketTimeout, use headersTimeout & bodyTimeout instead')
}
if (requestTimeout !== undefined) {
throw new InvalidArgumentError('unsupported requestTimeout, use headersTimeout & bodyTimeout instead')
}
if (idleTimeout !== undefined) {
throw new InvalidArgumentError('unsupported idleTimeout, use keepAliveTimeout instead')
}
if (maxKeepAliveTimeout !== undefined) {
throw new InvalidArgumentError('unsupported maxKeepAliveTimeout, use keepAliveMaxTimeout instead')
}
if (maxHeaderSize != null) {
if (!Number.isInteger(maxHeaderSize) || maxHeaderSize < 1) {
throw new InvalidArgumentError('invalid maxHeaderSize')
}
} else {
// If maxHeaderSize is not provided, use the default value from the http module
// or if that is not available, throw an error.
maxHeaderSize = getDefaultNodeMaxHeaderSize()
}
if (socketPath != null && typeof socketPath !== 'string') {
throw new InvalidArgumentError('invalid socketPath')
}
if (connectTimeout != null && (!Number.isFinite(connectTimeout) || connectTimeout < 0)) {
throw new InvalidArgumentError('invalid connectTimeout')
}
if (keepAliveTimeout != null && (!Number.isFinite(keepAliveTimeout) || keepAliveTimeout <= 0)) {
throw new InvalidArgumentError('invalid keepAliveTimeout')
}
if (keepAliveMaxTimeout != null && (!Number.isFinite(keepAliveMaxTimeout) || keepAliveMaxTimeout <= 0)) {
throw new InvalidArgumentError('invalid keepAliveMaxTimeout')
}
if (keepAliveTimeoutThreshold != null && !Number.isFinite(keepAliveTimeoutThreshold)) {
throw new InvalidArgumentError('invalid keepAliveTimeoutThreshold')
}
if (headersTimeout != null && (!Number.isInteger(headersTimeout) || headersTimeout < 0)) {
throw new InvalidArgumentError('headersTimeout must be a positive integer or zero')
}
if (bodyTimeout != null && (!Number.isInteger(bodyTimeout) || bodyTimeout < 0)) {
throw new InvalidArgumentError('bodyTimeout must be a positive integer or zero')
}
if (connect != null && typeof connect !== 'function' && typeof connect !== 'object') {
throw new InvalidArgumentError('connect must be a function or an object')
}
if (maxRequestsPerClient != null && (!Number.isInteger(maxRequestsPerClient) || maxRequestsPerClient < 0)) {
throw new InvalidArgumentError('maxRequestsPerClient must be a positive number')
}
if (localAddress != null && (typeof localAddress !== 'string' || net.isIP(localAddress) === 0)) {
throw new InvalidArgumentError('localAddress must be valid string IP address')
}
if (maxResponseSize != null && (!Number.isInteger(maxResponseSize) || maxResponseSize < -1)) {
throw new InvalidArgumentError('maxResponseSize must be a positive number')
}
if (
autoSelectFamilyAttemptTimeout != null &&
(!Number.isInteger(autoSelectFamilyAttemptTimeout) || autoSelectFamilyAttemptTimeout < -1)
) {
throw new InvalidArgumentError('autoSelectFamilyAttemptTimeout must be a positive number')
}
// h2
if (allowH2 != null && typeof allowH2 !== 'boolean') {
throw new InvalidArgumentError('allowH2 must be a valid boolean value')
}
if (maxConcurrentStreams != null && (typeof maxConcurrentStreams !== 'number' || maxConcurrentStreams < 1)) {
throw new InvalidArgumentError('maxConcurrentStreams must be a positive integer, greater than 0')
}
if (useH2c != null && typeof useH2c !== 'boolean') {
throw new InvalidArgumentError('useH2c must be a valid boolean value')
}
if (initialWindowSize != null && (!Number.isInteger(initialWindowSize) || initialWindowSize < 1)) {
throw new InvalidArgumentError('initialWindowSize must be a positive integer, greater than 0')
}
if (connectionWindowSize != null && (!Number.isInteger(connectionWindowSize) || connectionWindowSize < 1)) {
throw new InvalidArgumentError('connectionWindowSize must be a positive integer, greater than 0')
}
if (pingInterval != null && (typeof pingInterval !== 'number' || !Number.isInteger(pingInterval) || pingInterval < 0)) {
throw new InvalidArgumentError('pingInterval must be a positive integer, greater or equal to 0')
}
super()
if (typeof connect !== 'function') {
connect = buildConnector({
...tls,
maxCachedSessions,
allowH2,
useH2c,
socketPath,
timeout: connectTimeout,
...(typeof autoSelectFamily === 'boolean' ? { autoSelectFamily, autoSelectFamilyAttemptTimeout } : undefined),
...connect
})
} else if (socketPath != null) {
const customConnect = connect
connect = (opts, callback) => customConnect({ ...opts, socketPath }, callback)
}
this[kUrl] = util.parseOrigin(url)
this[kConnector] = connect
this[kPipelining] = pipelining != null ? pipelining : 1
this[kMaxHeadersSize] = maxHeaderSize
this[kKeepAliveDefaultTimeout] = keepAliveTimeout == null ? 4e3 : keepAliveTimeout
this[kKeepAliveMaxTimeout] = keepAliveMaxTimeout == null ? 600e3 : keepAliveMaxTimeout
this[kKeepAliveTimeoutThreshold] = keepAliveTimeoutThreshold == null ? 2e3 : keepAliveTimeoutThreshold
this[kKeepAliveTimeoutValue] = this[kKeepAliveDefaultTimeout]
this[kServerName] = null
this[kLocalAddress] = localAddress != null ? localAddress : null
this[kResuming] = 0 // 0, idle, 1, scheduled, 2 resuming
this[kNeedDrain] = 0 // 0, idle, 1, scheduled, 2 resuming
this[kHostHeader] = `host: ${this[kUrl].hostname}${this[kUrl].port ? `:${this[kUrl].port}` : ''}\r\n`
this[kBodyTimeout] = bodyTimeout != null ? bodyTimeout : 300e3
this[kHeadersTimeout] = headersTimeout != null ? headersTimeout : 300e3
this[kStrictContentLength] = strictContentLength == null ? true : strictContentLength
this[kMaxRequests] = maxRequestsPerClient
this[kClosedResolve] = null
this[kMaxResponseSize] = maxResponseSize > -1 ? maxResponseSize : -1
this[kHTTPContext] = null
// h2
this[kMaxConcurrentStreams] = maxConcurrentStreams != null ? maxConcurrentStreams : 100 // Max peerConcurrentStreams for a Node h2 server
// HTTP/2 window sizes are set to higher defaults than Node.js core for better performance:
// - initialWindowSize: 262144 (256KB) vs Node.js default 65535 (64KB - 1)
// Allows more data to be sent before requiring acknowledgment, improving throughput
// especially on high-latency networks. This matches common production HTTP/2 servers.
// - connectionWindowSize: 524288 (512KB) vs Node.js default (none set)
// Provides better flow control for the entire connection across multiple streams.
this[kHTTP2InitialWindowSize] = initialWindowSize != null ? initialWindowSize : 262144
this[kHTTP2ConnectionWindowSize] = connectionWindowSize != null ? connectionWindowSize : 524288
this[kPingInterval] = pingInterval != null ? pingInterval : 60e3 // Default ping interval for h2 - 1 minute
// kQueue is built up of 3 sections separated by
// the kRunningIdx and kPendingIdx indices.
// | complete | running | pending |
// ^ kRunningIdx ^ kPendingIdx ^ kQueue.length
// kRunningIdx points to the first running element.
// kPendingIdx points to the first pending element.
// This implements a fast queue with an amortized
// time of O(1).
this[kQueue] = []
this[kRunningIdx] = 0
this[kPendingIdx] = 0
this[kResume] = (sync) => resume(this, sync)
this[kOnError] = (err) => onError(this, err)
}
get pipelining () {
return this[kPipelining]
}
set pipelining (value) {
this[kPipelining] = value
this[kResume](true)
}
get stats () {
return new ClientStats(this)
}
get [kPending] () {
return this[kQueue].length - this[kPendingIdx]
}
get [kRunning] () {
return this[kPendingIdx] - this[kRunningIdx]
}
get [kSize] () {
return this[kQueue].length - this[kRunningIdx]
}
get [kConnected] () {
return !!this[kHTTPContext] && !this[kConnecting] && !this[kHTTPContext].destroyed
}
get [kBusy] () {
return Boolean(
this[kHTTPContext]?.busy(null) ||
(this[kSize] >= (getPipelining(this) || 1)) ||
this[kPending] > 0
)
}
[kConnect] (cb) {
connect(this)
this.once('connect', cb)
}
[kDispatch] (opts, handler) {
const request = new Request(this[kUrl].origin, opts, handler)
this[kQueue].push(request)
if (this[kResuming]) {
// Do nothing.
} else if (util.bodyLength(request.body) == null && util.isIterable(request.body)) {
// Wait a tick in case stream/iterator is ended in the same tick.
this[kResuming] = 1
queueMicrotask(() => resume(this))
} else {
this[kResume](true)
}
if (this[kResuming] && this[kNeedDrain] !== 2 && this[kBusy]) {
this[kNeedDrain] = 2
}
return this[kNeedDrain] < 2
}
[kClose] () {
// TODO: for H2 we need to gracefully flush the remaining enqueued
// request and close each stream.
return new Promise((resolve) => {
if (this[kSize]) {
this[kClosedResolve] = resolve
} else {
resolve(null)
}
})
}
[kDestroy] (err) {
return new Promise((resolve) => {
const requests = this[kQueue].splice(this[kPendingIdx])
for (let i = 0; i < requests.length; i++) {
const request = requests[i]
util.errorRequest(this, request, err)
}
const callback = () => {
if (this[kClosedResolve]) {
// TODO (fix): Should we error here with ClientDestroyedError?
this[kClosedResolve]()
this[kClosedResolve] = null
}
resolve(null)
}
if (this[kHTTPContext]) {
this[kHTTPContext].destroy(err, callback)
this[kHTTPContext] = null
} else {
queueMicrotask(callback)
}
this[kResume]()
})
}
}
function onError (client, err) {
if (
client[kRunning] === 0 &&
err.code !== 'UND_ERR_INFO' &&
err.code !== 'UND_ERR_SOCKET'
) {
// Error is not caused by running request and not a recoverable
// socket error.
assert(client[kPendingIdx] === client[kRunningIdx])
const requests = client[kQueue].splice(client[kRunningIdx])
for (let i = 0; i < requests.length; i++) {
const request = requests[i]
util.errorRequest(client, request, err)
}
assert(client[kSize] === 0)
}
}
/**
* @param {Client} client
* @returns {void}
*/
function connect (client) {
assert(!client[kConnecting])
assert(!client[kHTTPContext])
let { host, hostname, protocol, port } = client[kUrl]
// Resolve ipv6
if (hostname[0] === '[') {
const idx = hostname.indexOf(']')
assert(idx !== -1)
const ip = hostname.substring(1, idx)
assert(net.isIPv6(ip))
hostname = ip
}
client[kConnecting] = true
if (channels.beforeConnect.hasSubscribers) {
channels.beforeConnect.publish({
connectParams: {
host,
hostname,
protocol,
port,
version: client[kHTTPContext]?.version,
servername: client[kServerName],
localAddress: client[kLocalAddress]
},
connector: client[kConnector]
})
}
client[kConnector]({
host,
hostname,
protocol,
port,
servername: client[kServerName],
localAddress: client[kLocalAddress]
}, (err, socket) => {
if (err) {
handleConnectError(client, err, { host, hostname, protocol, port })
client[kResume]()
return
}
if (client.destroyed) {
util.destroy(socket.on('error', noop), new ClientDestroyedError())
client[kResume]()
return
}
assert(socket)
try {
client[kHTTPContext] = socket.alpnProtocol === 'h2'
? connectH2(client, socket)
: connectH1(client, socket)
} catch (err) {
socket.destroy().on('error', noop)
handleConnectError(client, err, { host, hostname, protocol, port })
client[kResume]()
return
}
client[kConnecting] = false
socket[kCounter] = 0
socket[kMaxRequests] = client[kMaxRequests]
socket[kClient] = client
socket[kError] = null
if (channels.connected.hasSubscribers) {
channels.connected.publish({
connectParams: {
host,
hostname,
protocol,
port,
version: client[kHTTPContext]?.version,
servername: client[kServerName],
localAddress: client[kLocalAddress]
},
connector: client[kConnector],
socket
})
}
client.emit('connect', client[kUrl], [client])
client[kResume]()
})
}
function handleConnectError (client, err, { host, hostname, protocol, port }) {
if (client.destroyed) {
return
}
client[kConnecting] = false
if (channels.connectError.hasSubscribers) {
channels.connectError.publish({
connectParams: {
host,
hostname,
protocol,
port,
version: client[kHTTPContext]?.version,
servername: client[kServerName],
localAddress: client[kLocalAddress]
},
connector: client[kConnector],
error: err
})
}
if (err.code === 'ERR_TLS_CERT_ALTNAME_INVALID') {
assert(client[kRunning] === 0)
while (client[kPending] > 0 && client[kQueue][client[kPendingIdx]].servername === client[kServerName]) {
const request = client[kQueue][client[kPendingIdx]++]
util.errorRequest(client, request, err)
}
} else {
onError(client, err)
}
client.emit('connectionError', client[kUrl], [client], err)
}
function emitDrain (client) {
client[kNeedDrain] = 0
client.emit('drain', client[kUrl], [client])
}
function resume (client, sync) {
if (client[kResuming] === 2) {
return
}
client[kResuming] = 2
_resume(client, sync)
client[kResuming] = 0
if (client[kRunningIdx] > 256) {
client[kQueue].splice(0, client[kRunningIdx])
client[kPendingIdx] -= client[kRunningIdx]
client[kRunningIdx] = 0
}
}
function _resume (client, sync) {
while (true) {
if (client.destroyed) {
assert(client[kPending] === 0)
return
}
if (client[kClosedResolve] && !client[kSize]) {
client[kClosedResolve]()
client[kClosedResolve] = null
return
}
if (client[kHTTPContext]) {
client[kHTTPContext].resume()
}
if (client[kBusy]) {
client[kNeedDrain] = 2
} else if (client[kNeedDrain] === 2) {
if (sync) {
client[kNeedDrain] = 1
queueMicrotask(() => emitDrain(client))
} else {
emitDrain(client)
}
continue
}
if (client[kPending] === 0) {
return
}
if (client[kRunning] >= (getPipelining(client) || 1)) {
return
}
const request = client[kQueue][client[kPendingIdx]]
if (request === null) {
return
}
if (client[kUrl].protocol === 'https:' && client[kServerName] !== request.servername) {
if (client[kRunning] > 0) {
return
}
client[kServerName] = request.servername
client[kHTTPContext]?.destroy(new InformationalError('servername changed'), () => {
client[kHTTPContext] = null
resume(client)
})
}
if (client[kConnecting]) {
return
}
if (!client[kHTTPContext]) {
connect(client)
return
}
if (client[kHTTPContext].destroyed) {
return
}
if (client[kHTTPContext].busy(request)) {
return
}
if (!request.aborted && client[kHTTPContext].write(request)) {
client[kPendingIdx]++
} else {
client[kQueue].splice(client[kPendingIdx], 1)
}
}
}
module.exports = Client
================================================
FILE: lib/dispatcher/dispatcher-base.js
================================================
'use strict'
const Dispatcher = require('./dispatcher')
const UnwrapHandler = require('../handler/unwrap-handler')
const {
ClientDestroyedError,
ClientClosedError,
InvalidArgumentError
} = require('../core/errors')
const { kDestroy, kClose, kClosed, kDestroyed, kDispatch } = require('../core/symbols')
const kOnDestroyed = Symbol('onDestroyed')
const kOnClosed = Symbol('onClosed')
class DispatcherBase extends Dispatcher {
/** @type {boolean} */
[kDestroyed] = false;
/** @type {Array|null} */
[kOnClosed] = null
/** @returns {boolean} */
get destroyed () {
return this[kDestroyed]
}
/** @returns {boolean} */
get closed () {
return this[kClosed]
}
close (callback) {
if (callback === undefined) {
return new Promise((resolve, reject) => {
this.close((err, data) => {
return err ? reject(err) : resolve(data)
})
})
}
if (typeof callback !== 'function') {
throw new InvalidArgumentError('invalid callback')
}
if (this[kDestroyed]) {
const err = new ClientDestroyedError()
queueMicrotask(() => callback(err, null))
return
}
if (this[kClosed]) {
if (this[kOnClosed]) {
this[kOnClosed].push(callback)
} else {
queueMicrotask(() => callback(null, null))
}
return
}
this[kClosed] = true
this[kOnClosed] ??= []
this[kOnClosed].push(callback)
const onClosed = () => {
const callbacks = this[kOnClosed]
this[kOnClosed] = null
for (let i = 0; i < callbacks.length; i++) {
callbacks[i](null, null)
}
}
// Should not error.
this[kClose]()
.then(() => this.destroy())
.then(() => queueMicrotask(onClosed))
}
destroy (err, callback) {
if (typeof err === 'function') {
callback = err
err = null
}
if (callback === undefined) {
return new Promise((resolve, reject) => {
this.destroy(err, (err, data) => {
return err ? reject(err) : resolve(data)
})
})
}
if (typeof callback !== 'function') {
throw new InvalidArgumentError('invalid callback')
}
if (this[kDestroyed]) {
if (this[kOnDestroyed]) {
this[kOnDestroyed].push(callback)
} else {
queueMicrotask(() => callback(null, null))
}
return
}
if (!err) {
err = new ClientDestroyedError()
}
this[kDestroyed] = true
this[kOnDestroyed] ??= []
this[kOnDestroyed].push(callback)
const onDestroyed = () => {
const callbacks = this[kOnDestroyed]
this[kOnDestroyed] = null
for (let i = 0; i < callbacks.length; i++) {
callbacks[i](null, null)
}
}
// Should not error.
this[kDestroy](err)
.then(() => queueMicrotask(onDestroyed))
}
dispatch (opts, handler) {
if (!handler || typeof handler !== 'object') {
throw new InvalidArgumentError('handler must be an object')
}
handler = UnwrapHandler.unwrap(handler)
try {
if (!opts || typeof opts !== 'object') {
throw new InvalidArgumentError('opts must be an object.')
}
if (this[kDestroyed] || this[kOnDestroyed]) {
throw new ClientDestroyedError()
}
if (this[kClosed]) {
throw new ClientClosedError()
}
return this[kDispatch](opts, handler)
} catch (err) {
if (typeof handler.onError !== 'function') {
throw err
}
handler.onError(err)
return false
}
}
}
module.exports = DispatcherBase
================================================
FILE: lib/dispatcher/dispatcher.js
================================================
'use strict'
const EventEmitter = require('node:events')
const WrapHandler = require('../handler/wrap-handler')
const wrapInterceptor = (dispatch) => (opts, handler) => dispatch(opts, WrapHandler.wrap(handler))
class Dispatcher extends EventEmitter {
dispatch () {
throw new Error('not implemented')
}
close () {
throw new Error('not implemented')
}
destroy () {
throw new Error('not implemented')
}
compose (...args) {
// So we handle [interceptor1, interceptor2] or interceptor1, interceptor2, ...
const interceptors = Array.isArray(args[0]) ? args[0] : args
let dispatch = this.dispatch.bind(this)
for (const interceptor of interceptors) {
if (interceptor == null) {
continue
}
if (typeof interceptor !== 'function') {
throw new TypeError(`invalid interceptor, expected function received ${typeof interceptor}`)
}
dispatch = interceptor(dispatch)
dispatch = wrapInterceptor(dispatch)
if (dispatch == null || typeof dispatch !== 'function' || dispatch.length !== 2) {
throw new TypeError('invalid interceptor')
}
}
return new Proxy(this, {
get: (target, key) => key === 'dispatch' ? dispatch : target[key]
})
}
}
module.exports = Dispatcher
================================================
FILE: lib/dispatcher/env-http-proxy-agent.js
================================================
'use strict'
const DispatcherBase = require('./dispatcher-base')
const { kClose, kDestroy, kClosed, kDestroyed, kDispatch, kNoProxyAgent, kHttpProxyAgent, kHttpsProxyAgent } = require('../core/symbols')
const ProxyAgent = require('./proxy-agent')
const Agent = require('./agent')
const DEFAULT_PORTS = {
'http:': 80,
'https:': 443
}
class EnvHttpProxyAgent extends DispatcherBase {
#noProxyValue = null
#noProxyEntries = null
#opts = null
constructor (opts = {}) {
super()
this.#opts = opts
const { httpProxy, httpsProxy, noProxy, ...agentOpts } = opts
this[kNoProxyAgent] = new Agent(agentOpts)
const HTTP_PROXY = httpProxy ?? process.env.http_proxy ?? process.env.HTTP_PROXY
if (HTTP_PROXY) {
this[kHttpProxyAgent] = new ProxyAgent({ ...agentOpts, uri: HTTP_PROXY })
} else {
this[kHttpProxyAgent] = this[kNoProxyAgent]
}
const HTTPS_PROXY = httpsProxy ?? process.env.https_proxy ?? process.env.HTTPS_PROXY
if (HTTPS_PROXY) {
this[kHttpsProxyAgent] = new ProxyAgent({ ...agentOpts, uri: HTTPS_PROXY })
} else {
this[kHttpsProxyAgent] = this[kHttpProxyAgent]
}
this.#parseNoProxy()
}
[kDispatch] (opts, handler) {
const url = new URL(opts.origin)
const agent = this.#getProxyAgentForUrl(url)
return agent.dispatch(opts, handler)
}
[kClose] () {
return Promise.all([
this[kNoProxyAgent].close(),
!this[kHttpProxyAgent][kClosed] && this[kHttpProxyAgent].close(),
!this[kHttpsProxyAgent][kClosed] && this[kHttpsProxyAgent].close()
])
}
[kDestroy] (err) {
return Promise.all([
this[kNoProxyAgent].destroy(err),
!this[kHttpProxyAgent][kDestroyed] && this[kHttpProxyAgent].destroy(err),
!this[kHttpsProxyAgent][kDestroyed] && this[kHttpsProxyAgent].destroy(err)
])
}
#getProxyAgentForUrl (url) {
let { protocol, host: hostname, port } = url
// Stripping ports in this way instead of using parsedUrl.hostname to make
// sure that the brackets around IPv6 addresses are kept.
hostname = hostname.replace(/:\d*$/, '').toLowerCase()
port = Number.parseInt(port, 10) || DEFAULT_PORTS[protocol] || 0
if (!this.#shouldProxy(hostname, port)) {
return this[kNoProxyAgent]
}
if (protocol === 'https:') {
return this[kHttpsProxyAgent]
}
return this[kHttpProxyAgent]
}
#shouldProxy (hostname, port) {
if (this.#noProxyChanged) {
this.#parseNoProxy()
}
if (this.#noProxyEntries.length === 0) {
return true // Always proxy if NO_PROXY is not set or empty.
}
if (this.#noProxyValue === '*') {
return false // Never proxy if wildcard is set.
}
for (let i = 0; i < this.#noProxyEntries.length; i++) {
const entry = this.#noProxyEntries[i]
if (entry.port && entry.port !== port) {
continue // Skip if ports don't match.
}
// Don't proxy if the hostname is equal with the no_proxy host.
if (hostname === entry.hostname) {
return false
}
// Don't proxy if the hostname is the subdomain of the no_proxy host.
// Reference - https://github.com/denoland/deno/blob/6fbce91e40cc07fc6da74068e5cc56fdd40f7b4c/ext/fetch/proxy.rs#L485
if (hostname.slice(-(entry.hostname.length + 1)) === `.${entry.hostname}`) {
return false
}
}
return true
}
#parseNoProxy () {
const noProxyValue = this.#opts.noProxy ?? this.#noProxyEnv
const noProxySplit = noProxyValue.split(/[,\s]/)
const noProxyEntries = []
for (let i = 0; i < noProxySplit.length; i++) {
const entry = noProxySplit[i]
if (!entry) {
continue
}
const parsed = entry.match(/^(.+):(\d+)$/)
noProxyEntries.push({
// strip leading dot or asterisk with dot
hostname: (parsed ? parsed[1] : entry).replace(/^\*?\./, '').toLowerCase(),
port: parsed ? Number.parseInt(parsed[2], 10) : 0
})
}
this.#noProxyValue = noProxyValue
this.#noProxyEntries = noProxyEntries
}
get #noProxyChanged () {
if (this.#opts.noProxy !== undefined) {
return false
}
return this.#noProxyValue !== this.#noProxyEnv
}
get #noProxyEnv () {
return process.env.no_proxy ?? process.env.NO_PROXY ?? ''
}
}
module.exports = EnvHttpProxyAgent
================================================
FILE: lib/dispatcher/fixed-queue.js
================================================
'use strict'
// Extracted from node/lib/internal/fixed_queue.js
// Currently optimal queue size, tested on V8 6.0 - 6.6. Must be power of two.
const kSize = 2048
const kMask = kSize - 1
// The FixedQueue is implemented as a singly-linked list of fixed-size
// circular buffers. It looks something like this:
//
// head tail
// | |
// v v
// +-----------+ <-----\ +-----------+ <------\ +-----------+
// | [null] | \----- | next | \------- | next |
// +-----------+ +-----------+ +-----------+
// | item | <-- bottom | item | <-- bottom | undefined |
// | item | | item | | undefined |
// | item | | item | | undefined |
// | item | | item | | undefined |
// | item | | item | bottom --> | item |
// | item | | item | | item |
// | ... | | ... | | ... |
// | item | | item | | item |
// | item | | item | | item |
// | undefined | <-- top | item | | item |
// | undefined | | item | | item |
// | undefined | | undefined | <-- top top --> | undefined |
// +-----------+ +-----------+ +-----------+
//
// Or, if there is only one circular buffer, it looks something
// like either of these:
//
// head tail head tail
// | | | |
// v v v v
// +-----------+ +-----------+
// | [null] | | [null] |
// +-----------+ +-----------+
// | undefined | | item |
// | undefined | | item |
// | item | <-- bottom top --> | undefined |
// | item | | undefined |
// | undefined | <-- top bottom --> | item |
// | undefined | | item |
// +-----------+ +-----------+
//
// Adding a value means moving `top` forward by one, removing means
// moving `bottom` forward by one. After reaching the end, the queue
// wraps around.
//
// When `top === bottom` the current queue is empty and when
// `top + 1 === bottom` it's full. This wastes a single space of storage
// but allows much quicker checks.
/**
* @type {FixedCircularBuffer}
* @template T
*/
class FixedCircularBuffer {
/** @type {number} */
bottom = 0
/** @type {number} */
top = 0
/** @type {Array} */
list = new Array(kSize).fill(undefined)
/** @type {T|null} */
next = null
/** @returns {boolean} */
isEmpty () {
return this.top === this.bottom
}
/** @returns {boolean} */
isFull () {
return ((this.top + 1) & kMask) === this.bottom
}
/**
* @param {T} data
* @returns {void}
*/
push (data) {
this.list[this.top] = data
this.top = (this.top + 1) & kMask
}
/** @returns {T|null} */
shift () {
const nextItem = this.list[this.bottom]
if (nextItem === undefined) { return null }
this.list[this.bottom] = undefined
this.bottom = (this.bottom + 1) & kMask
return nextItem
}
}
/**
* @template T
*/
module.exports = class FixedQueue {
constructor () {
/** @type {FixedCircularBuffer} */
this.head = this.tail = new FixedCircularBuffer()
}
/** @returns {boolean} */
isEmpty () {
return this.head.isEmpty()
}
/** @param {T} data */
push (data) {
if (this.head.isFull()) {
// Head is full: Creates a new queue, sets the old queue's `.next` to it,
// and sets it as the new main queue.
this.head = this.head.next = new FixedCircularBuffer()
}
this.head.push(data)
}
/** @returns {T|null} */
shift () {
const tail = this.tail
const next = tail.shift()
if (tail.isEmpty() && tail.next !== null) {
// If there is another queue, it forms the new tail.
this.tail = tail.next
tail.next = null
}
return next
}
}
================================================
FILE: lib/dispatcher/h2c-client.js
================================================
'use strict'
const { InvalidArgumentError } = require('../core/errors')
const Client = require('./client')
class H2CClient extends Client {
constructor (origin, clientOpts) {
if (typeof origin === 'string') {
origin = new URL(origin)
}
if (origin.protocol !== 'http:') {
throw new InvalidArgumentError(
'h2c-client: Only h2c protocol is supported'
)
}
const { connect, maxConcurrentStreams, pipelining, ...opts } =
clientOpts ?? {}
let defaultMaxConcurrentStreams = 100
let defaultPipelining = 100
if (
maxConcurrentStreams != null &&
Number.isInteger(maxConcurrentStreams) &&
maxConcurrentStreams > 0
) {
defaultMaxConcurrentStreams = maxConcurrentStreams
}
if (pipelining != null && Number.isInteger(pipelining) && pipelining > 0) {
defaultPipelining = pipelining
}
if (defaultPipelining > defaultMaxConcurrentStreams) {
throw new InvalidArgumentError(
'h2c-client: pipelining cannot be greater than maxConcurrentStreams'
)
}
super(origin, {
...opts,
maxConcurrentStreams: defaultMaxConcurrentStreams,
pipelining: defaultPipelining,
allowH2: true,
useH2c: true
})
}
}
module.exports = H2CClient
================================================
FILE: lib/dispatcher/pool-base.js
================================================
'use strict'
const { PoolStats } = require('../util/stats.js')
const DispatcherBase = require('./dispatcher-base')
const FixedQueue = require('./fixed-queue')
const { kConnected, kSize, kRunning, kPending, kQueued, kBusy, kFree, kUrl, kClose, kDestroy, kDispatch } = require('../core/symbols')
const kClients = Symbol('clients')
const kNeedDrain = Symbol('needDrain')
const kQueue = Symbol('queue')
const kClosedResolve = Symbol('closed resolve')
const kOnDrain = Symbol('onDrain')
const kOnConnect = Symbol('onConnect')
const kOnDisconnect = Symbol('onDisconnect')
const kOnConnectionError = Symbol('onConnectionError')
const kGetDispatcher = Symbol('get dispatcher')
const kAddClient = Symbol('add client')
const kRemoveClient = Symbol('remove client')
class PoolBase extends DispatcherBase {
[kQueue] = new FixedQueue();
[kQueued] = 0;
[kClients] = [];
[kNeedDrain] = false;
[kOnDrain] (client, origin, targets) {
const queue = this[kQueue]
let needDrain = false
while (!needDrain) {
const item = queue.shift()
if (!item) {
break
}
this[kQueued]--
needDrain = !client.dispatch(item.opts, item.handler)
}
client[kNeedDrain] = needDrain
if (!needDrain && this[kNeedDrain]) {
this[kNeedDrain] = false
this.emit('drain', origin, [this, ...targets])
}
if (this[kClosedResolve] && queue.isEmpty()) {
const closeAll = []
for (let i = 0; i < this[kClients].length; i++) {
const client = this[kClients][i]
if (!client.destroyed) {
closeAll.push(client.close())
}
}
return Promise.all(closeAll)
.then(this[kClosedResolve])
}
}
[kOnConnect] = (origin, targets) => {
this.emit('connect', origin, [this, ...targets])
};
[kOnDisconnect] = (origin, targets, err) => {
this.emit('disconnect', origin, [this, ...targets], err)
};
[kOnConnectionError] = (origin, targets, err) => {
this.emit('connectionError', origin, [this, ...targets], err)
}
get [kBusy] () {
return this[kNeedDrain]
}
get [kConnected] () {
let ret = 0
for (const { [kConnected]: connected } of this[kClients]) {
ret += connected
}
return ret
}
get [kFree] () {
let ret = 0
for (const { [kConnected]: connected, [kNeedDrain]: needDrain } of this[kClients]) {
ret += connected && !needDrain
}
return ret
}
get [kPending] () {
let ret = this[kQueued]
for (const { [kPending]: pending } of this[kClients]) {
ret += pending
}
return ret
}
get [kRunning] () {
let ret = 0
for (const { [kRunning]: running } of this[kClients]) {
ret += running
}
return ret
}
get [kSize] () {
let ret = this[kQueued]
for (const { [kSize]: size } of this[kClients]) {
ret += size
}
return ret
}
get stats () {
return new PoolStats(this)
}
[kClose] () {
if (this[kQueue].isEmpty()) {
const closeAll = []
for (let i = 0; i < this[kClients].length; i++) {
const client = this[kClients][i]
if (!client.destroyed) {
closeAll.push(client.close())
}
}
return Promise.all(closeAll)
} else {
return new Promise((resolve) => {
this[kClosedResolve] = resolve
})
}
}
[kDestroy] (err) {
while (true) {
const item = this[kQueue].shift()
if (!item) {
break
}
item.handler.onError(err)
}
const destroyAll = new Array(this[kClients].length)
for (let i = 0; i < this[kClients].length; i++) {
destroyAll[i] = this[kClients][i].destroy(err)
}
return Promise.all(destroyAll)
}
[kDispatch] (opts, handler) {
const dispatcher = this[kGetDispatcher]()
if (!dispatcher) {
this[kNeedDrain] = true
this[kQueue].push({ opts, handler })
this[kQueued]++
} else if (!dispatcher.dispatch(opts, handler)) {
dispatcher[kNeedDrain] = true
this[kNeedDrain] = !this[kGetDispatcher]()
}
return !this[kNeedDrain]
}
[kAddClient] (client) {
client
.on('drain', this[kOnDrain].bind(this, client))
.on('connect', this[kOnConnect])
.on('disconnect', this[kOnDisconnect])
.on('connectionError', this[kOnConnectionError])
this[kClients].push(client)
if (this[kNeedDrain]) {
queueMicrotask(() => {
if (this[kNeedDrain]) {
this[kOnDrain](client, client[kUrl], [client, this])
}
})
}
return this
}
[kRemoveClient] (client) {
client.close(() => {
const idx = this[kClients].indexOf(client)
if (idx !== -1) {
this[kClients].splice(idx, 1)
}
})
this[kNeedDrain] = this[kClients].some(dispatcher => (
!dispatcher[kNeedDrain] &&
dispatcher.closed !== true &&
dispatcher.destroyed !== true
))
}
}
module.exports = {
PoolBase,
kClients,
kNeedDrain,
kAddClient,
kRemoveClient,
kGetDispatcher
}
================================================
FILE: lib/dispatcher/pool.js
================================================
'use strict'
const {
PoolBase,
kClients,
kNeedDrain,
kAddClient,
kGetDispatcher,
kRemoveClient
} = require('./pool-base')
const Client = require('./client')
const {
InvalidArgumentError
} = require('../core/errors')
const util = require('../core/util')
const { kUrl } = require('../core/symbols')
const buildConnector = require('../core/connect')
const kOptions = Symbol('options')
const kConnections = Symbol('connections')
const kFactory = Symbol('factory')
function defaultFactory (origin, opts) {
return new Client(origin, opts)
}
class Pool extends PoolBase {
constructor (origin, {
connections,
factory = defaultFactory,
connect,
connectTimeout,
tls,
maxCachedSessions,
socketPath,
autoSelectFamily,
autoSelectFamilyAttemptTimeout,
allowH2,
clientTtl,
...options
} = {}) {
if (connections != null && (!Number.isFinite(connections) || connections < 0)) {
throw new InvalidArgumentError('invalid connections')
}
if (typeof factory !== 'function') {
throw new InvalidArgumentError('factory must be a function.')
}
if (connect != null && typeof connect !== 'function' && typeof connect !== 'object') {
throw new InvalidArgumentError('connect must be a function or an object')
}
if (typeof connect !== 'function') {
connect = buildConnector({
...tls,
maxCachedSessions,
allowH2,
socketPath,
timeout: connectTimeout,
...(typeof autoSelectFamily === 'boolean' ? { autoSelectFamily, autoSelectFamilyAttemptTimeout } : undefined),
...connect
})
}
super()
this[kConnections] = connections || null
this[kUrl] = util.parseOrigin(origin)
this[kOptions] = { ...util.deepClone(options), connect, allowH2, clientTtl, socketPath }
this[kOptions].interceptors = options.interceptors
? { ...options.interceptors }
: undefined
this[kFactory] = factory
this.on('connect', (origin, targets) => {
if (clientTtl != null && clientTtl > 0) {
for (const target of targets) {
Object.assign(target, { ttl: Date.now() })
}
}
})
this.on('connectionError', (origin, targets, error) => {
// If a connection error occurs, we remove the client from the pool,
// and emit a connectionError event. They will not be re-used.
// Fixes https://github.com/nodejs/undici/issues/3895
for (const target of targets) {
// Do not use kRemoveClient here, as it will close the client,
// but the client cannot be closed in this state.
const idx = this[kClients].indexOf(target)
if (idx !== -1) {
this[kClients].splice(idx, 1)
}
}
})
}
[kGetDispatcher] () {
const clientTtlOption = this[kOptions].clientTtl
for (const client of this[kClients]) {
// check ttl of client and if it's stale, remove it from the pool
if (clientTtlOption != null && clientTtlOption > 0 && client.ttl && ((Date.now() - client.ttl) > clientTtlOption)) {
this[kRemoveClient](client)
} else if (!client[kNeedDrain]) {
return client
}
}
if (!this[kConnections] || this[kClients].length < this[kConnections]) {
const dispatcher = this[kFactory](this[kUrl], this[kOptions])
this[kAddClient](dispatcher)
return dispatcher
}
}
}
module.exports = Pool
================================================
FILE: lib/dispatcher/proxy-agent.js
================================================
'use strict'
const { kProxy, kClose, kDestroy, kDispatch } = require('../core/symbols')
const Agent = require('./agent')
const Pool = require('./pool')
const DispatcherBase = require('./dispatcher-base')
const { InvalidArgumentError, RequestAbortedError, SecureProxyConnectionError } = require('../core/errors')
const buildConnector = require('../core/connect')
const Client = require('./client')
const { channels } = require('../core/diagnostics')
const Socks5ProxyAgent = require('./socks5-proxy-agent')
const kAgent = Symbol('proxy agent')
const kClient = Symbol('proxy client')
const kProxyHeaders = Symbol('proxy headers')
const kRequestTls = Symbol('request tls settings')
const kProxyTls = Symbol('proxy tls settings')
const kConnectEndpoint = Symbol('connect endpoint function')
const kTunnelProxy = Symbol('tunnel proxy')
function defaultProtocolPort (protocol) {
return protocol === 'https:' ? 443 : 80
}
function defaultFactory (origin, opts) {
return new Pool(origin, opts)
}
const noop = () => {}
function defaultAgentFactory (origin, opts) {
if (opts.connections === 1) {
return new Client(origin, opts)
}
return new Pool(origin, opts)
}
class Http1ProxyWrapper extends DispatcherBase {
#client
constructor (proxyUrl, { headers = {}, connect, factory }) {
if (!proxyUrl) {
throw new InvalidArgumentError('Proxy URL is mandatory')
}
super()
this[kProxyHeaders] = headers
if (factory) {
this.#client = factory(proxyUrl, { connect })
} else {
this.#client = new Client(proxyUrl, { connect })
}
}
[kDispatch] (opts, handler) {
const onHeaders = handler.onHeaders
handler.onHeaders = function (statusCode, data, resume) {
if (statusCode === 407) {
if (typeof handler.onError === 'function') {
handler.onError(new InvalidArgumentError('Proxy Authentication Required (407)'))
}
return
}
if (onHeaders) onHeaders.call(this, statusCode, data, resume)
}
// Rewrite request as an HTTP1 Proxy request, without tunneling.
const {
origin,
path = '/',
headers = {}
} = opts
opts.path = origin + path
if (!('host' in headers) && !('Host' in headers)) {
const { host } = new URL(origin)
headers.host = host
}
opts.headers = { ...this[kProxyHeaders], ...headers }
return this.#client[kDispatch](opts, handler)
}
[kClose] () {
return this.#client.close()
}
[kDestroy] (err) {
return this.#client.destroy(err)
}
}
class ProxyAgent extends DispatcherBase {
constructor (opts) {
if (!opts || (typeof opts === 'object' && !(opts instanceof URL) && !opts.uri)) {
throw new InvalidArgumentError('Proxy uri is mandatory')
}
const { clientFactory = defaultFactory } = opts
if (typeof clientFactory !== 'function') {
throw new InvalidArgumentError('Proxy opts.clientFactory must be a function.')
}
const { proxyTunnel = true } = opts
super()
const url = this.#getUrl(opts)
const { href, origin, port, protocol, username, password, hostname: proxyHostname } = url
this[kProxy] = { uri: href, protocol }
this[kRequestTls] = opts.requestTls
this[kProxyTls] = opts.proxyTls
this[kProxyHeaders] = opts.headers || {}
this[kTunnelProxy] = proxyTunnel
if (opts.auth && opts.token) {
throw new InvalidArgumentError('opts.auth cannot be used in combination with opts.token')
} else if (opts.auth) {
/* @deprecated in favour of opts.token */
this[kProxyHeaders]['proxy-authorization'] = `Basic ${opts.auth}`
} else if (opts.token) {
this[kProxyHeaders]['proxy-authorization'] = opts.token
} else if (username && password) {
this[kProxyHeaders]['proxy-authorization'] = `Basic ${Buffer.from(`${decodeURIComponent(username)}:${decodeURIComponent(password)}`).toString('base64')}`
}
const connect = buildConnector({ ...opts.proxyTls })
this[kConnectEndpoint] = buildConnector({ ...opts.requestTls })
const agentFactory = opts.factory || defaultAgentFactory
const factory = (origin, options) => {
const { protocol } = new URL(origin)
// Handle SOCKS5 proxy
if (this[kProxy].protocol === 'socks5:' || this[kProxy].protocol === 'socks:') {
return new Socks5ProxyAgent(this[kProxy].uri, {
headers: this[kProxyHeaders],
connect,
factory: agentFactory,
username: opts.username || username,
password: opts.password || password,
proxyTls: opts.proxyTls
})
}
if (!this[kTunnelProxy] && protocol === 'http:' && this[kProxy].protocol === 'http:') {
return new Http1ProxyWrapper(this[kProxy].uri, {
headers: this[kProxyHeaders],
connect,
factory: agentFactory
})
}
return agentFactory(origin, options)
}
// For SOCKS5 proxies, we don't need a client to the proxy itself
// The SOCKS5 connection is handled within Socks5ProxyAgent
if (protocol === 'socks5:' || protocol === 'socks:') {
this[kClient] = null
} else {
this[kClient] = clientFactory(url, { connect })
}
this[kAgent] = new Agent({
...opts,
factory,
connect: async (opts, callback) => {
// SOCKS5 proxies handle their own connections via Socks5ProxyAgent,
// so this connect function should never be called for them.
if (!this[kClient]) {
callback(new InvalidArgumentError('Cannot establish tunnel connection without a proxy client'))
return
}
let requestedPath = opts.host
if (!opts.port) {
requestedPath += `:${defaultProtocolPort(opts.protocol)}`
}
try {
const connectParams = {
origin,
port,
path: requestedPath,
signal: opts.signal,
headers: {
...this[kProxyHeaders],
host: opts.host,
...(opts.connections == null || opts.connections > 0 ? { 'proxy-connection': 'keep-alive' } : {})
},
servername: this[kProxyTls]?.servername || proxyHostname
}
const { socket, statusCode } = await this[kClient].connect(connectParams)
if (statusCode !== 200) {
socket.on('error', noop).destroy()
callback(new RequestAbortedError(`Proxy response (${statusCode}) !== 200 when HTTP Tunneling`))
return
}
if (channels.proxyConnected.hasSubscribers) {
channels.proxyConnected.publish({
socket,
connectParams
})
}
if (opts.protocol !== 'https:') {
callback(null, socket)
return
}
let servername
if (this[kRequestTls]) {
servername = this[kRequestTls].servername
} else {
servername = opts.servername
}
this[kConnectEndpoint]({ ...opts, servername, httpSocket: socket }, callback)
} catch (err) {
if (err.code === 'ERR_TLS_CERT_ALTNAME_INVALID') {
// Throw a custom error to avoid loop in client.js#connect
callback(new SecureProxyConnectionError(err))
} else {
callback(err)
}
}
}
})
}
dispatch (opts, handler) {
const headers = buildHeaders(opts.headers)
throwIfProxyAuthIsSent(headers)
if (headers && !('host' in headers) && !('Host' in headers)) {
const { host } = new URL(opts.origin)
headers.host = host
}
return this[kAgent].dispatch(
{
...opts,
headers
},
handler
)
}
/**
* @param {import('../../types/proxy-agent').ProxyAgent.Options | string | URL} opts
* @returns {URL}
*/
#getUrl (opts) {
if (typeof opts === 'string') {
return new URL(opts)
} else if (opts instanceof URL) {
return opts
} else {
return new URL(opts.uri)
}
}
[kClose] () {
const promises = [this[kAgent].close()]
if (this[kClient]) {
promises.push(this[kClient].close())
}
return Promise.all(promises)
}
[kDestroy] () {
const promises = [this[kAgent].destroy()]
if (this[kClient]) {
promises.push(this[kClient].destroy())
}
return Promise.all(promises)
}
}
/**
* @param {string[] | Record} headers
* @returns {Record}
*/
function buildHeaders (headers) {
// When using undici.fetch, the headers list is stored
// as an array.
if (Array.isArray(headers)) {
/** @type {Record} */
const headersPair = {}
for (let i = 0; i < headers.length; i += 2) {
headersPair[headers[i]] = headers[i + 1]
}
return headersPair
}
return headers
}
/**
* @param {Record} headers
*
* Previous versions of ProxyAgent suggests the Proxy-Authorization in request headers
* Nevertheless, it was changed and to avoid a security vulnerability by end users
* this check was created.
* It should be removed in the next major version for performance reasons
*/
function throwIfProxyAuthIsSent (headers) {
const existProxyAuth = headers && Object.keys(headers)
.find((key) => key.toLowerCase() === 'proxy-authorization')
if (existProxyAuth) {
throw new InvalidArgumentError('Proxy-Authorization should be sent in ProxyAgent constructor')
}
}
module.exports = ProxyAgent
================================================
FILE: lib/dispatcher/retry-agent.js
================================================
'use strict'
const Dispatcher = require('./dispatcher')
const RetryHandler = require('../handler/retry-handler')
class RetryAgent extends Dispatcher {
#agent = null
#options = null
constructor (agent, options = {}) {
super(options)
this.#agent = agent
this.#options = options
}
dispatch (opts, handler) {
const retry = new RetryHandler({
...opts,
retryOptions: this.#options
}, {
dispatch: this.#agent.dispatch.bind(this.#agent),
handler
})
return this.#agent.dispatch(opts, retry)
}
close () {
return this.#agent.close()
}
destroy () {
return this.#agent.destroy()
}
}
module.exports = RetryAgent
================================================
FILE: lib/dispatcher/round-robin-pool.js
================================================
'use strict'
const {
PoolBase,
kClients,
kNeedDrain,
kAddClient,
kGetDispatcher,
kRemoveClient
} = require('./pool-base')
const Client = require('./client')
const {
InvalidArgumentError
} = require('../core/errors')
const util = require('../core/util')
const { kUrl } = require('../core/symbols')
const buildConnector = require('../core/connect')
const kOptions = Symbol('options')
const kConnections = Symbol('connections')
const kFactory = Symbol('factory')
const kIndex = Symbol('index')
function defaultFactory (origin, opts) {
return new Client(origin, opts)
}
class RoundRobinPool extends PoolBase {
constructor (origin, {
connections,
factory = defaultFactory,
connect,
connectTimeout,
tls,
maxCachedSessions,
socketPath,
autoSelectFamily,
autoSelectFamilyAttemptTimeout,
allowH2,
clientTtl,
...options
} = {}) {
if (connections != null && (!Number.isFinite(connections) || connections < 0)) {
throw new InvalidArgumentError('invalid connections')
}
if (typeof factory !== 'function') {
throw new InvalidArgumentError('factory must be a function.')
}
if (connect != null && typeof connect !== 'function' && typeof connect !== 'object') {
throw new InvalidArgumentError('connect must be a function or an object')
}
if (typeof connect !== 'function') {
connect = buildConnector({
...tls,
maxCachedSessions,
allowH2,
socketPath,
timeout: connectTimeout,
...(typeof autoSelectFamily === 'boolean' ? { autoSelectFamily, autoSelectFamilyAttemptTimeout } : undefined),
...connect
})
}
super()
this[kConnections] = connections || null
this[kUrl] = util.parseOrigin(origin)
this[kOptions] = { ...util.deepClone(options), connect, allowH2, clientTtl, socketPath }
this[kOptions].interceptors = options.interceptors
? { ...options.interceptors }
: undefined
this[kFactory] = factory
this[kIndex] = -1
this.on('connect', (origin, targets) => {
if (clientTtl != null && clientTtl > 0) {
for (const target of targets) {
Object.assign(target, { ttl: Date.now() })
}
}
})
this.on('connectionError', (origin, targets, error) => {
for (const target of targets) {
const idx = this[kClients].indexOf(target)
if (idx !== -1) {
this[kClients].splice(idx, 1)
}
}
})
}
[kGetDispatcher] () {
const clientTtlOption = this[kOptions].clientTtl
const clientsLength = this[kClients].length
// If we have no clients yet, create one
if (clientsLength === 0) {
const dispatcher = this[kFactory](this[kUrl], this[kOptions])
this[kAddClient](dispatcher)
return dispatcher
}
// Round-robin through existing clients
let checked = 0
while (checked < clientsLength) {
this[kIndex] = (this[kIndex] + 1) % clientsLength
const client = this[kClients][this[kIndex]]
// Check if client is stale (TTL expired)
if (clientTtlOption != null && clientTtlOption > 0 && client.ttl && ((Date.now() - client.ttl) > clientTtlOption)) {
this[kRemoveClient](client)
checked++
continue
}
// Return client if it's not draining
if (!client[kNeedDrain]) {
return client
}
checked++
}
// All clients are busy, create a new one if we haven't reached the limit
if (!this[kConnections] || clientsLength < this[kConnections]) {
const dispatcher = this[kFactory](this[kUrl], this[kOptions])
this[kAddClient](dispatcher)
return dispatcher
}
}
}
module.exports = RoundRobinPool
================================================
FILE: lib/dispatcher/socks5-proxy-agent.js
================================================
'use strict'
const net = require('node:net')
const { URL } = require('node:url')
let tls // include tls conditionally since it is not always available
const DispatcherBase = require('./dispatcher-base')
const { InvalidArgumentError } = require('../core/errors')
const { Socks5Client } = require('../core/socks5-client')
const { kDispatch, kClose, kDestroy } = require('../core/symbols')
const Pool = require('./pool')
const buildConnector = require('../core/connect')
const { debuglog } = require('node:util')
const debug = debuglog('undici:socks5-proxy')
const kProxyUrl = Symbol('proxy url')
const kProxyHeaders = Symbol('proxy headers')
const kProxyAuth = Symbol('proxy auth')
const kPool = Symbol('pool')
const kConnector = Symbol('connector')
// Static flag to ensure warning is only emitted once per process
let experimentalWarningEmitted = false
/**
* SOCKS5 proxy agent for dispatching requests through a SOCKS5 proxy
*/
class Socks5ProxyAgent extends DispatcherBase {
constructor (proxyUrl, options = {}) {
super()
// Emit experimental warning only once
if (!experimentalWarningEmitted) {
process.emitWarning(
'SOCKS5 proxy support is experimental and subject to change',
'ExperimentalWarning'
)
experimentalWarningEmitted = true
}
if (!proxyUrl) {
throw new InvalidArgumentError('Proxy URL is mandatory')
}
// Parse proxy URL
const url = typeof proxyUrl === 'string' ? new URL(proxyUrl) : proxyUrl
if (url.protocol !== 'socks5:' && url.protocol !== 'socks:') {
throw new InvalidArgumentError('Proxy URL must use socks5:// or socks:// protocol')
}
this[kProxyUrl] = url
this[kProxyHeaders] = options.headers || {}
// Extract auth from URL or options
this[kProxyAuth] = {
username: options.username || (url.username ? decodeURIComponent(url.username) : null),
password: options.password || (url.password ? decodeURIComponent(url.password) : null)
}
// Create connector for proxy connection
this[kConnector] = options.connect || buildConnector({
...options.proxyTls,
servername: options.proxyTls?.servername || url.hostname
})
// Pool for the actual HTTP connections (with SOCKS5 tunnel connect function)
this[kPool] = null
}
/**
* Create a SOCKS5 connection to the proxy
*/
async createSocks5Connection (targetHost, targetPort) {
const proxyHost = this[kProxyUrl].hostname
const proxyPort = parseInt(this[kProxyUrl].port) || 1080
debug('creating SOCKS5 connection to', proxyHost, proxyPort)
// Connect to the SOCKS5 proxy
const socket = await new Promise((resolve, reject) => {
const onConnect = () => {
socket.removeListener('error', onError)
resolve(socket)
}
const onError = (err) => {
socket.removeListener('connect', onConnect)
reject(err)
}
const socket = net.connect({
host: proxyHost,
port: proxyPort
})
socket.once('connect', onConnect)
socket.once('error', onError)
})
// Create SOCKS5 client
const socks5Client = new Socks5Client(socket, this[kProxyAuth])
// Handle SOCKS5 errors
socks5Client.on('error', (err) => {
debug('SOCKS5 error:', err)
socket.destroy()
})
// Perform SOCKS5 handshake
await socks5Client.handshake()
// Wait for authentication (if required)
await new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
reject(new Error('SOCKS5 authentication timeout'))
}, 5000)
const onAuthenticated = () => {
clearTimeout(timeout)
socks5Client.removeListener('error', onError)
resolve()
}
const onError = (err) => {
clearTimeout(timeout)
socks5Client.removeListener('authenticated', onAuthenticated)
reject(err)
}
// Check if already authenticated (for NO_AUTH method)
if (socks5Client.state === 'authenticated') {
clearTimeout(timeout)
resolve()
} else {
socks5Client.once('authenticated', onAuthenticated)
socks5Client.once('error', onError)
}
})
// Send CONNECT command
await socks5Client.connect(targetHost, targetPort)
// Wait for connection
await new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
reject(new Error('SOCKS5 connection timeout'))
}, 5000)
const onConnected = (info) => {
debug('SOCKS5 tunnel established to', targetHost, targetPort, 'via', info)
clearTimeout(timeout)
socks5Client.removeListener('error', onError)
resolve()
}
const onError = (err) => {
clearTimeout(timeout)
socks5Client.removeListener('connected', onConnected)
reject(err)
}
socks5Client.once('connected', onConnected)
socks5Client.once('error', onError)
})
return socket
}
/**
* Dispatch a request through the SOCKS5 proxy
*/
async [kDispatch] (opts, handler) {
const { origin } = opts
debug('dispatching request to', origin, 'via SOCKS5')
try {
// Create Pool with custom connect function if we don't have one yet
if (!this[kPool] || this[kPool].destroyed || this[kPool].closed) {
this[kPool] = new Pool(origin, {
pipelining: opts.pipelining,
connections: opts.connections,
connect: async (connectOpts, callback) => {
try {
const url = new URL(origin)
const targetHost = url.hostname
const targetPort = parseInt(url.port) || (url.protocol === 'https:' ? 443 : 80)
debug('establishing SOCKS5 connection to', targetHost, targetPort)
// Create SOCKS5 tunnel
const socket = await this.createSocks5Connection(targetHost, targetPort)
// Handle TLS if needed
let finalSocket = socket
if (url.protocol === 'https:') {
if (!tls) {
tls = require('node:tls')
}
debug('upgrading to TLS')
finalSocket = tls.connect({
socket,
servername: targetHost,
...connectOpts.tls || {}
})
await new Promise((resolve, reject) => {
finalSocket.once('secureConnect', resolve)
finalSocket.once('error', reject)
})
}
callback(null, finalSocket)
} catch (err) {
debug('SOCKS5 connection error:', err)
callback(err)
}
}
})
}
// Dispatch the request through the pool
return this[kPool][kDispatch](opts, handler)
} catch (err) {
debug('dispatch error:', err)
if (typeof handler.onError === 'function') {
handler.onError(err)
} else {
throw err
}
}
}
async [kClose] () {
if (this[kPool]) {
await this[kPool].close()
}
}
async [kDestroy] (err) {
if (this[kPool]) {
await this[kPool].destroy(err)
}
}
}
module.exports = Socks5ProxyAgent
================================================
FILE: lib/encoding/index.js
================================================
'use strict'
const textDecoder = new TextDecoder()
/**
* @see https://encoding.spec.whatwg.org/#utf-8-decode
* @param {Uint8Array} buffer
*/
function utf8DecodeBytes (buffer) {
if (buffer.length === 0) {
return ''
}
// 1. Let buffer be the result of peeking three bytes from
// ioQueue, converted to a byte sequence.
// 2. If buffer is 0xEF 0xBB 0xBF, then read three
// bytes from ioQueue. (Do nothing with those bytes.)
if (buffer[0] === 0xEF && buffer[1] === 0xBB && buffer[2] === 0xBF) {
buffer = buffer.subarray(3)
}
// 3. Process a queue with an instance of UTF-8’s
// decoder, ioQueue, output, and "replacement".
const output = textDecoder.decode(buffer)
// 4. Return output.
return output
}
module.exports = {
utf8DecodeBytes
}
================================================
FILE: lib/global.js
================================================
'use strict'
// We include a version number for the Dispatcher API. In case of breaking changes,
// this version number must be increased to avoid conflicts.
const globalDispatcher = Symbol.for('undici.globalDispatcher.1')
const { InvalidArgumentError } = require('./core/errors')
const Agent = require('./dispatcher/agent')
if (getGlobalDispatcher() === undefined) {
setGlobalDispatcher(new Agent())
}
function setGlobalDispatcher (agent) {
if (!agent || typeof agent.dispatch !== 'function') {
throw new InvalidArgumentError('Argument agent must implement Agent')
}
Object.defineProperty(globalThis, globalDispatcher, {
value: agent,
writable: true,
enumerable: false,
configurable: false
})
}
function getGlobalDispatcher () {
return globalThis[globalDispatcher]
}
// These are the globals that can be installed by undici.install().
// Not exported by index.js to avoid use outside of this module.
const installedExports = /** @type {const} */ (
[
'fetch',
'Headers',
'Response',
'Request',
'FormData',
'WebSocket',
'CloseEvent',
'ErrorEvent',
'MessageEvent',
'EventSource'
]
)
module.exports = {
setGlobalDispatcher,
getGlobalDispatcher,
installedExports
}
================================================
FILE: lib/handler/cache-handler.js
================================================
'use strict'
const util = require('../core/util')
const {
parseCacheControlHeader,
parseVaryHeader,
isEtagUsable
} = require('../util/cache')
const { parseHttpDate } = require('../util/date.js')
function noop () {}
// Status codes that we can use some heuristics on to cache
const HEURISTICALLY_CACHEABLE_STATUS_CODES = [
200, 203, 204, 206, 300, 301, 308, 404, 405, 410, 414, 501
]
// Status codes which semantic is not handled by the cache
// https://datatracker.ietf.org/doc/html/rfc9111#section-3
// This list should not grow beyond 206 unless the RFC is updated
// by a newer one including more. Please introduce another list if
// implementing caching of responses with the 'must-understand' directive.
const NOT_UNDERSTOOD_STATUS_CODES = [
206
]
const MAX_RESPONSE_AGE = 2147483647000
/**
* @typedef {import('../../types/dispatcher.d.ts').default.DispatchHandler} DispatchHandler
*
* @implements {DispatchHandler}
*/
class CacheHandler {
/**
* @type {import('../../types/cache-interceptor.d.ts').default.CacheKey}
*/
#cacheKey
/**
* @type {import('../../types/cache-interceptor.d.ts').default.CacheHandlerOptions['type']}
*/
#cacheType
/**
* @type {number | undefined}
*/
#cacheByDefault
/**
* @type {import('../../types/cache-interceptor.d.ts').default.CacheStore}
*/
#store
/**
* @type {import('../../types/dispatcher.d.ts').default.DispatchHandler}
*/
#handler
/**
* @type {import('node:stream').Writable | undefined}
*/
#writeStream
/**
* @param {import('../../types/cache-interceptor.d.ts').default.CacheHandlerOptions} opts
* @param {import('../../types/cache-interceptor.d.ts').default.CacheKey} cacheKey
* @param {import('../../types/dispatcher.d.ts').default.DispatchHandler} handler
*/
constructor ({ store, type, cacheByDefault }, cacheKey, handler) {
this.#store = store
this.#cacheType = type
this.#cacheByDefault = cacheByDefault
this.#cacheKey = cacheKey
this.#handler = handler
}
onRequestStart (controller, context) {
this.#writeStream?.destroy()
this.#writeStream = undefined
this.#handler.onRequestStart?.(controller, context)
}
onRequestUpgrade (controller, statusCode, headers, socket) {
this.#handler.onRequestUpgrade?.(controller, statusCode, headers, socket)
}
/**
* @param {import('../../types/dispatcher.d.ts').default.DispatchController} controller
* @param {number} statusCode
* @param {import('../../types/header.d.ts').IncomingHttpHeaders} resHeaders
* @param {string} statusMessage
*/
onResponseStart (
controller,
statusCode,
resHeaders,
statusMessage
) {
const downstreamOnHeaders = () =>
this.#handler.onResponseStart?.(
controller,
statusCode,
resHeaders,
statusMessage
)
const handler = this
if (
!util.safeHTTPMethods.includes(this.#cacheKey.method) &&
statusCode >= 200 &&
statusCode <= 399
) {
// Successful response to an unsafe method, delete it from cache
// https://www.rfc-editor.org/rfc/rfc9111.html#name-invalidating-stored-response
try {
this.#store.delete(this.#cacheKey)?.catch?.(noop)
} catch {
// Fail silently
}
return downstreamOnHeaders()
}
const cacheControlHeader = resHeaders['cache-control']
const heuristicallyCacheable = resHeaders['last-modified'] && HEURISTICALLY_CACHEABLE_STATUS_CODES.includes(statusCode)
if (
!cacheControlHeader &&
!resHeaders['expires'] &&
!heuristicallyCacheable &&
!this.#cacheByDefault
) {
// Don't have anything to tell us this response is cachable and we're not
// caching by default
return downstreamOnHeaders()
}
const cacheControlDirectives = cacheControlHeader ? parseCacheControlHeader(cacheControlHeader) : {}
if (!canCacheResponse(this.#cacheType, statusCode, resHeaders, cacheControlDirectives)) {
return downstreamOnHeaders()
}
const now = Date.now()
const resAge = resHeaders.age ? getAge(resHeaders.age) : undefined
if (resAge && resAge >= MAX_RESPONSE_AGE) {
// Response considered stale
return downstreamOnHeaders()
}
const resDate = typeof resHeaders.date === 'string'
? parseHttpDate(resHeaders.date)
: undefined
const staleAt =
determineStaleAt(this.#cacheType, now, resAge, resHeaders, resDate, cacheControlDirectives) ??
this.#cacheByDefault
if (staleAt === undefined || (resAge && resAge > staleAt)) {
return downstreamOnHeaders()
}
const baseTime = resDate ? resDate.getTime() : now
const absoluteStaleAt = staleAt + baseTime
if (now >= absoluteStaleAt) {
// Response is already stale
return downstreamOnHeaders()
}
let varyDirectives
if (this.#cacheKey.headers && resHeaders.vary) {
varyDirectives = parseVaryHeader(resHeaders.vary, this.#cacheKey.headers)
if (!varyDirectives) {
// Parse error
return downstreamOnHeaders()
}
}
const deleteAt = determineDeleteAt(baseTime, cacheControlDirectives, absoluteStaleAt)
const strippedHeaders = stripNecessaryHeaders(resHeaders, cacheControlDirectives)
/**
* @type {import('../../types/cache-interceptor.d.ts').default.CacheValue}
*/
const value = {
statusCode,
statusMessage,
headers: strippedHeaders,
vary: varyDirectives,
cacheControlDirectives,
cachedAt: resAge ? now - resAge : now,
staleAt: absoluteStaleAt,
deleteAt
}
// Not modified, re-use the cached value
// https://www.rfc-editor.org/rfc/rfc9111.html#name-handling-304-not-modified
if (statusCode === 304) {
const handle304 = (cachedValue) => {
if (!cachedValue) {
// Do not create a new cache entry, as a 304 won't have a body - so cannot be cached.
return downstreamOnHeaders()
}
// Re-use the cached value: statuscode, statusmessage, headers and body
value.statusCode = cachedValue.statusCode
value.statusMessage = cachedValue.statusMessage
value.etag = cachedValue.etag
value.headers = { ...cachedValue.headers, ...strippedHeaders }
downstreamOnHeaders()
this.#writeStream = this.#store.createWriteStream(this.#cacheKey, value)
if (!this.#writeStream || !cachedValue?.body) {
return
}
if (typeof cachedValue.body.values === 'function') {
const bodyIterator = cachedValue.body.values()
const streamCachedBody = () => {
for (const chunk of bodyIterator) {
const full = this.#writeStream.write(chunk) === false
this.#handler.onResponseData?.(controller, chunk)
// when stream is full stop writing until we get a 'drain' event
if (full) {
break
}
}
}
this.#writeStream
.on('error', function () {
handler.#writeStream = undefined
handler.#store.delete(handler.#cacheKey)
})
.on('drain', () => {
streamCachedBody()
})
.on('close', function () {
if (handler.#writeStream === this) {
handler.#writeStream = undefined
}
})
streamCachedBody()
} else if (typeof cachedValue.body.on === 'function') {
// Readable stream body (e.g. from async/remote cache stores)
cachedValue.body
.on('data', (chunk) => {
this.#writeStream.write(chunk)
this.#handler.onResponseData?.(controller, chunk)
})
.on('end', () => {
this.#writeStream.end()
})
.on('error', () => {
this.#writeStream = undefined
this.#store.delete(this.#cacheKey)
})
this.#writeStream
.on('error', function () {
handler.#writeStream = undefined
handler.#store.delete(handler.#cacheKey)
})
.on('close', function () {
if (handler.#writeStream === this) {
handler.#writeStream = undefined
}
})
}
}
/**
* @type {import('../../types/cache-interceptor.d.ts').default.CacheValue}
*/
const result = this.#store.get(this.#cacheKey)
if (result && typeof result.then === 'function') {
result.then(handle304)
} else {
handle304(result)
}
} else {
if (typeof resHeaders.etag === 'string' && isEtagUsable(resHeaders.etag)) {
value.etag = resHeaders.etag
}
this.#writeStream = this.#store.createWriteStream(this.#cacheKey, value)
if (!this.#writeStream) {
return downstreamOnHeaders()
}
this.#writeStream
.on('drain', () => controller.resume())
.on('error', function () {
// TODO (fix): Make error somehow observable?
handler.#writeStream = undefined
// Delete the value in case the cache store is holding onto state from
// the call to createWriteStream
handler.#store.delete(handler.#cacheKey)
})
.on('close', function () {
if (handler.#writeStream === this) {
handler.#writeStream = undefined
}
// TODO (fix): Should we resume even if was paused downstream?
controller.resume()
})
downstreamOnHeaders()
}
}
onResponseData (controller, chunk) {
if (this.#writeStream?.write(chunk) === false) {
controller.pause()
}
this.#handler.onResponseData?.(controller, chunk)
}
onResponseEnd (controller, trailers) {
this.#writeStream?.end()
this.#handler.onResponseEnd?.(controller, trailers)
}
onResponseError (controller, err) {
this.#writeStream?.destroy(err)
this.#writeStream = undefined
this.#handler.onResponseError?.(controller, err)
}
}
/**
* @see https://www.rfc-editor.org/rfc/rfc9111.html#name-storing-responses-to-authen
*
* @param {import('../../types/cache-interceptor.d.ts').default.CacheOptions['type']} cacheType
* @param {number} statusCode
* @param {import('../../types/header.d.ts').IncomingHttpHeaders} resHeaders
* @param {import('../../types/cache-interceptor.d.ts').default.CacheControlDirectives} cacheControlDirectives
*/
function canCacheResponse (cacheType, statusCode, resHeaders, cacheControlDirectives) {
// Status code must be final and understood.
if (statusCode < 200 || NOT_UNDERSTOOD_STATUS_CODES.includes(statusCode)) {
return false
}
// Responses with neither status codes that are heuristically cacheable, nor "explicit enough" caching
// directives, are not cacheable. "Explicit enough": see https://www.rfc-editor.org/rfc/rfc9111.html#section-3
if (!HEURISTICALLY_CACHEABLE_STATUS_CODES.includes(statusCode) && !resHeaders['expires'] &&
!cacheControlDirectives.public &&
cacheControlDirectives['max-age'] === undefined &&
// RFC 9111: a private response directive, if the cache is not shared
!(cacheControlDirectives.private && cacheType === 'private') &&
!(cacheControlDirectives['s-maxage'] !== undefined && cacheType === 'shared')
) {
return false
}
if (cacheControlDirectives['no-store']) {
return false
}
if (cacheType === 'shared' && cacheControlDirectives.private === true) {
return false
}
// https://www.rfc-editor.org/rfc/rfc9111.html#section-4.1-5
if (resHeaders.vary?.includes('*')) {
return false
}
// https://www.rfc-editor.org/rfc/rfc9111.html#name-storing-responses-to-authen
if (resHeaders.authorization) {
if (!cacheControlDirectives.public || typeof resHeaders.authorization !== 'string') {
return false
}
if (
Array.isArray(cacheControlDirectives['no-cache']) &&
cacheControlDirectives['no-cache'].includes('authorization')
) {
return false
}
if (
Array.isArray(cacheControlDirectives['private']) &&
cacheControlDirectives['private'].includes('authorization')
) {
return false
}
}
return true
}
/**
* @param {string | string[]} ageHeader
* @returns {number | undefined}
*/
function getAge (ageHeader) {
const age = parseInt(Array.isArray(ageHeader) ? ageHeader[0] : ageHeader)
return isNaN(age) ? undefined : age * 1000
}
/**
* @param {import('../../types/cache-interceptor.d.ts').default.CacheOptions['type']} cacheType
* @param {number} now
* @param {number | undefined} age
* @param {import('../../types/header.d.ts').IncomingHttpHeaders} resHeaders
* @param {Date | undefined} responseDate
* @param {import('../../types/cache-interceptor.d.ts').default.CacheControlDirectives} cacheControlDirectives
*
* @returns {number | undefined} time that the value is stale at in seconds or undefined if it shouldn't be cached
*/
function determineStaleAt (cacheType, now, age, resHeaders, responseDate, cacheControlDirectives) {
if (cacheType === 'shared') {
// Prioritize s-maxage since we're a shared cache
// s-maxage > max-age > Expire
// https://www.rfc-editor.org/rfc/rfc9111.html#section-5.2.2.10-3
const sMaxAge = cacheControlDirectives['s-maxage']
if (sMaxAge !== undefined) {
return sMaxAge > 0 ? sMaxAge * 1000 : undefined
}
}
const maxAge = cacheControlDirectives['max-age']
if (maxAge !== undefined) {
return maxAge > 0 ? maxAge * 1000 : undefined
}
if (typeof resHeaders.expires === 'string') {
// https://www.rfc-editor.org/rfc/rfc9111.html#section-5.3
const expiresDate = parseHttpDate(resHeaders.expires)
if (expiresDate) {
if (now >= expiresDate.getTime()) {
return undefined
}
if (responseDate) {
if (responseDate >= expiresDate) {
return undefined
}
if (age !== undefined && age > (expiresDate - responseDate)) {
return undefined
}
}
return expiresDate.getTime() - now
}
}
if (typeof resHeaders['last-modified'] === 'string') {
// https://www.rfc-editor.org/rfc/rfc9111.html#name-calculating-heuristic-fresh
const lastModified = new Date(resHeaders['last-modified'])
if (isValidDate(lastModified)) {
if (lastModified.getTime() >= now) {
return undefined
}
const responseAge = now - lastModified.getTime()
return responseAge * 0.1
}
}
if (cacheControlDirectives.immutable) {
// https://www.rfc-editor.org/rfc/rfc8246.html#section-2.2
return 31536000
}
return undefined
}
/**
* @param {number} now
* @param {import('../../types/cache-interceptor.d.ts').default.CacheControlDirectives} cacheControlDirectives
* @param {number} staleAt
*/
function determineDeleteAt (now, cacheControlDirectives, staleAt) {
let staleWhileRevalidate = -Infinity
let staleIfError = -Infinity
let immutable = -Infinity
if (cacheControlDirectives['stale-while-revalidate']) {
staleWhileRevalidate = staleAt + (cacheControlDirectives['stale-while-revalidate'] * 1000)
}
if (cacheControlDirectives['stale-if-error']) {
staleIfError = staleAt + (cacheControlDirectives['stale-if-error'] * 1000)
}
if (cacheControlDirectives.immutable && staleWhileRevalidate === -Infinity && staleIfError === -Infinity) {
immutable = now + 31536000000
}
// When no stale directives or immutable flag, add a revalidation buffer
// equal to the freshness lifetime so the entry survives past staleAt long
// enough to be revalidated instead of silently disappearing.
if (staleWhileRevalidate === -Infinity && staleIfError === -Infinity && immutable === -Infinity) {
const freshnessLifetime = staleAt - now
return staleAt + freshnessLifetime
}
return Math.max(staleAt, staleWhileRevalidate, staleIfError, immutable)
}
/**
* Strips headers required to be removed in cached responses
* @param {import('../../types/header.d.ts').IncomingHttpHeaders} resHeaders
* @param {import('../../types/cache-interceptor.d.ts').default.CacheControlDirectives} cacheControlDirectives
* @returns {Record}
*/
function stripNecessaryHeaders (resHeaders, cacheControlDirectives) {
const headersToRemove = [
'connection',
'proxy-authenticate',
'proxy-authentication-info',
'proxy-authorization',
'proxy-connection',
'te',
'transfer-encoding',
'upgrade',
// We'll add age back when serving it
'age'
]
if (resHeaders['connection']) {
if (Array.isArray(resHeaders['connection'])) {
// connection: a
// connection: b
headersToRemove.push(...resHeaders['connection'].map(header => header.trim()))
} else {
// connection: a, b
headersToRemove.push(...resHeaders['connection'].split(',').map(header => header.trim()))
}
}
if (Array.isArray(cacheControlDirectives['no-cache'])) {
headersToRemove.push(...cacheControlDirectives['no-cache'])
}
if (Array.isArray(cacheControlDirectives['private'])) {
headersToRemove.push(...cacheControlDirectives['private'])
}
let strippedHeaders
for (const headerName of headersToRemove) {
if (resHeaders[headerName]) {
strippedHeaders ??= { ...resHeaders }
delete strippedHeaders[headerName]
}
}
return strippedHeaders ?? resHeaders
}
/**
* @param {Date} date
* @returns {boolean}
*/
function isValidDate (date) {
return date instanceof Date && Number.isFinite(date.valueOf())
}
module.exports = CacheHandler
================================================
FILE: lib/handler/cache-revalidation-handler.js
================================================
'use strict'
const assert = require('node:assert')
/**
* This takes care of revalidation requests we send to the origin. If we get
* a response indicating that what we have is cached (via a HTTP 304), we can
* continue using the cached value. Otherwise, we'll receive the new response
* here, which we then just pass on to the next handler (most likely a
* CacheHandler). Note that this assumes the proper headers were already
* included in the request to tell the origin that we want to revalidate the
* response (i.e. if-modified-since or if-none-match).
*
* @see https://www.rfc-editor.org/rfc/rfc9111.html#name-validation
*
* @implements {import('../../types/dispatcher.d.ts').default.DispatchHandler}
*/
class CacheRevalidationHandler {
#successful = false
/**
* @type {((boolean, any) => void) | null}
*/
#callback
/**
* @type {(import('../../types/dispatcher.d.ts').default.DispatchHandler)}
*/
#handler
#context
/**
* @type {boolean}
*/
#allowErrorStatusCodes
/**
* @param {(boolean) => void} callback Function to call if the cached value is valid
* @param {import('../../types/dispatcher.d.ts').default.DispatchHandlers} handler
* @param {boolean} allowErrorStatusCodes
*/
constructor (callback, handler, allowErrorStatusCodes) {
if (typeof callback !== 'function') {
throw new TypeError('callback must be a function')
}
this.#callback = callback
this.#handler = handler
this.#allowErrorStatusCodes = allowErrorStatusCodes
}
onRequestStart (_, context) {
this.#successful = false
this.#context = context
}
onRequestUpgrade (controller, statusCode, headers, socket) {
this.#handler.onRequestUpgrade?.(controller, statusCode, headers, socket)
}
onResponseStart (
controller,
statusCode,
headers,
statusMessage
) {
assert(this.#callback != null)
// https://www.rfc-editor.org/rfc/rfc9111.html#name-handling-a-validation-respo
// https://datatracker.ietf.org/doc/html/rfc5861#section-4
this.#successful = statusCode === 304 ||
(this.#allowErrorStatusCodes && statusCode >= 500 && statusCode <= 504)
this.#callback(this.#successful, this.#context)
this.#callback = null
if (this.#successful) {
return true
}
this.#handler.onRequestStart?.(controller, this.#context)
this.#handler.onResponseStart?.(
controller,
statusCode,
headers,
statusMessage
)
}
onResponseData (controller, chunk) {
if (this.#successful) {
return
}
return this.#handler.onResponseData?.(controller, chunk)
}
onResponseEnd (controller, trailers) {
if (this.#successful) {
return
}
this.#handler.onResponseEnd?.(controller, trailers)
}
onResponseError (controller, err) {
if (this.#successful) {
return
}
if (this.#callback) {
this.#callback(false)
this.#callback = null
}
if (typeof this.#handler.onResponseError === 'function') {
this.#handler.onResponseError(controller, err)
} else {
throw err
}
}
}
module.exports = CacheRevalidationHandler
================================================
FILE: lib/handler/decorator-handler.js
================================================
'use strict'
const assert = require('node:assert')
const WrapHandler = require('./wrap-handler')
/**
* @deprecated
*/
module.exports = class DecoratorHandler {
#handler
#onCompleteCalled = false
#onErrorCalled = false
#onResponseStartCalled = false
constructor (handler) {
if (typeof handler !== 'object' || handler === null) {
throw new TypeError('handler must be an object')
}
this.#handler = WrapHandler.wrap(handler)
}
onRequestStart (...args) {
this.#handler.onRequestStart?.(...args)
}
onRequestUpgrade (...args) {
assert(!this.#onCompleteCalled)
assert(!this.#onErrorCalled)
return this.#handler.onRequestUpgrade?.(...args)
}
onResponseStart (...args) {
assert(!this.#onCompleteCalled)
assert(!this.#onErrorCalled)
assert(!this.#onResponseStartCalled)
this.#onResponseStartCalled = true
return this.#handler.onResponseStart?.(...args)
}
onResponseData (...args) {
assert(!this.#onCompleteCalled)
assert(!this.#onErrorCalled)
return this.#handler.onResponseData?.(...args)
}
onResponseEnd (...args) {
assert(!this.#onCompleteCalled)
assert(!this.#onErrorCalled)
this.#onCompleteCalled = true
return this.#handler.onResponseEnd?.(...args)
}
onResponseError (...args) {
this.#onErrorCalled = true
return this.#handler.onResponseError?.(...args)
}
/**
* @deprecated
*/
onBodySent () {}
}
================================================
FILE: lib/handler/deduplication-handler.js
================================================
'use strict'
const { RequestAbortedError } = require('../core/errors')
/**
* @typedef {import('../../types/dispatcher.d.ts').default.DispatchHandler} DispatchHandler
*/
const DEFAULT_MAX_BUFFER_SIZE = 5 * 1024 * 1024
/**
* @typedef {Object} WaitingHandler
* @property {DispatchHandler} handler
* @property {import('../../types/dispatcher.d.ts').default.DispatchController} controller
* @property {Buffer[]} bufferedChunks
* @property {number} bufferedBytes
* @property {object | null} pendingTrailers
* @property {boolean} done
*/
/**
* Handler that forwards response events to multiple waiting handlers.
* Used for request deduplication.
*
* @implements {DispatchHandler}
*/
class DeduplicationHandler {
/**
* @type {DispatchHandler}
*/
#primaryHandler
/**
* @type {WaitingHandler[]}
*/
#waitingHandlers = []
/**
* @type {number}
*/
#maxBufferSize = DEFAULT_MAX_BUFFER_SIZE
/**
* @type {number}
*/
#statusCode = 0
/**
* @type {Record}
*/
#headers = {}
/**
* @type {string}
*/
#statusMessage = ''
/**
* @type {boolean}
*/
#aborted = false
/**
* @type {boolean}
*/
#responseStarted = false
/**
* @type {boolean}
*/
#responseDataStarted = false
/**
* @type {boolean}
*/
#completed = false
/**
* @type {import('../../types/dispatcher.d.ts').default.DispatchController | null}
*/
#controller = null
/**
* @type {(() => void) | null}
*/
#onComplete = null
/**
* @param {DispatchHandler} primaryHandler The primary handler
* @param {() => void} onComplete Callback when request completes
* @param {number} [maxBufferSize] Maximum paused buffer size per waiting handler
*/
constructor (primaryHandler, onComplete, maxBufferSize = DEFAULT_MAX_BUFFER_SIZE) {
this.#primaryHandler = primaryHandler
this.#onComplete = onComplete
this.#maxBufferSize = maxBufferSize
}
/**
* Add a waiting handler that will receive response events.
* Returns false if deduplication can no longer safely attach this handler.
*
* @param {DispatchHandler} handler
* @returns {boolean}
*/
addWaitingHandler (handler) {
if (this.#completed || this.#responseDataStarted) {
return false
}
const waitingHandler = this.#createWaitingHandler(handler)
const waitingController = waitingHandler.controller
try {
handler.onRequestStart?.(waitingController, null)
if (waitingController.aborted) {
waitingHandler.done = true
return true
}
if (this.#responseStarted) {
handler.onResponseStart?.(
waitingController,
this.#statusCode,
this.#headers,
this.#statusMessage
)
}
} catch {
// Ignore errors from waiting handlers
waitingHandler.done = true
return true
}
if (!waitingController.aborted) {
this.#waitingHandlers.push(waitingHandler)
}
return true
}
/**
* @param {import('../../types/dispatcher.d.ts').default.DispatchController} controller
* @param {any} context
*/
onRequestStart (controller, context) {
this.#controller = controller
this.#primaryHandler.onRequestStart?.(controller, context)
}
/**
* @param {import('../../types/dispatcher.d.ts').default.DispatchController} controller
* @param {number} statusCode
* @param {import('../../types/header.d.ts').IncomingHttpHeaders} headers
* @param {Socket} socket
*/
onRequestUpgrade (controller, statusCode, headers, socket) {
this.#primaryHandler.onRequestUpgrade?.(controller, statusCode, headers, socket)
}
/**
* @param {import('../../types/dispatcher.d.ts').default.DispatchController} controller
* @param {number} statusCode
* @param {Record} headers
* @param {string} statusMessage
*/
onResponseStart (controller, statusCode, headers, statusMessage) {
this.#responseStarted = true
this.#statusCode = statusCode
this.#headers = headers
this.#statusMessage = statusMessage
this.#primaryHandler.onResponseStart?.(controller, statusCode, headers, statusMessage)
for (const waitingHandler of this.#waitingHandlers) {
const { handler, controller: waitingController } = waitingHandler
if (waitingHandler.done || waitingController.aborted) {
waitingHandler.done = true
continue
}
try {
handler.onResponseStart?.(
waitingController,
statusCode,
headers,
statusMessage
)
} catch {
// Ignore errors from waiting handlers
}
if (waitingController.aborted) {
waitingHandler.done = true
}
}
this.#pruneDoneWaitingHandlers()
}
/**
* @param {import('../../types/dispatcher.d.ts').default.DispatchController} controller
* @param {Buffer} chunk
*/
onResponseData (controller, chunk) {
if (this.#aborted || this.#completed) {
return
}
this.#responseDataStarted = true
this.#primaryHandler.onResponseData?.(controller, chunk)
for (const waitingHandler of this.#waitingHandlers) {
const { handler, controller: waitingController } = waitingHandler
if (waitingHandler.done || waitingController.aborted) {
waitingHandler.done = true
continue
}
if (waitingController.paused) {
this.#bufferWaitingChunk(waitingHandler, chunk)
continue
}
try {
handler.onResponseData?.(waitingController, chunk)
} catch {
// Ignore errors from waiting handlers
}
if (waitingController.aborted) {
waitingHandler.done = true
waitingHandler.bufferedChunks = []
waitingHandler.bufferedBytes = 0
}
}
this.#pruneDoneWaitingHandlers()
}
/**
* @param {import('../../types/dispatcher.d.ts').default.DispatchController} controller
* @param {object} trailers
*/
onResponseEnd (controller, trailers) {
if (this.#aborted || this.#completed) {
return
}
this.#completed = true
this.#primaryHandler.onResponseEnd?.(controller, trailers)
for (const waitingHandler of this.#waitingHandlers) {
if (waitingHandler.done || waitingHandler.controller.aborted) {
waitingHandler.done = true
continue
}
this.#flushWaitingHandler(waitingHandler)
if (waitingHandler.done || waitingHandler.controller.aborted) {
waitingHandler.done = true
continue
}
if (waitingHandler.controller.paused && waitingHandler.bufferedChunks.length > 0) {
waitingHandler.pendingTrailers = trailers
continue
}
try {
waitingHandler.handler.onResponseEnd?.(waitingHandler.controller, trailers)
} catch {
// Ignore errors from waiting handlers
}
waitingHandler.done = true
}
this.#pruneDoneWaitingHandlers()
this.#onComplete?.()
}
/**
* @param {import('../../types/dispatcher.d.ts').default.DispatchController} controller
* @param {Error} err
*/
onResponseError (controller, err) {
if (this.#completed) {
return
}
this.#aborted = true
this.#completed = true
this.#primaryHandler.onResponseError?.(controller, err)
for (const waitingHandler of this.#waitingHandlers) {
this.#errorWaitingHandler(waitingHandler, err)
}
this.#waitingHandlers = []
this.#onComplete?.()
}
/**
* @param {DispatchHandler} handler
* @returns {WaitingHandler}
*/
#createWaitingHandler (handler) {
/** @type {WaitingHandler} */
const waitingHandler = {
handler,
controller: null,
bufferedChunks: [],
bufferedBytes: 0,
pendingTrailers: null,
done: false
}
const state = {
aborted: false,
paused: false,
reason: null
}
waitingHandler.controller = {
resume: () => {
if (state.aborted) {
return
}
state.paused = false
this.#flushWaitingHandler(waitingHandler)
if (
this.#completed &&
waitingHandler.pendingTrailers &&
waitingHandler.bufferedChunks.length === 0 &&
!state.paused &&
!state.aborted
) {
try {
waitingHandler.handler.onResponseEnd?.(waitingHandler.controller, waitingHandler.pendingTrailers)
} catch {
// Ignore errors from waiting handlers
}
waitingHandler.pendingTrailers = null
waitingHandler.done = true
}
this.#pruneDoneWaitingHandlers()
},
pause: () => {
if (!state.aborted) {
state.paused = true
}
},
get paused () { return state.paused },
get aborted () { return state.aborted },
get reason () { return state.reason },
abort: (reason) => {
state.aborted = true
state.reason = reason ?? null
waitingHandler.done = true
waitingHandler.pendingTrailers = null
waitingHandler.bufferedChunks = []
waitingHandler.bufferedBytes = 0
}
}
return waitingHandler
}
/**
* @param {WaitingHandler} waitingHandler
* @param {Buffer} chunk
*/
#bufferWaitingChunk (waitingHandler, chunk) {
if (waitingHandler.done || waitingHandler.controller.aborted) {
waitingHandler.done = true
waitingHandler.bufferedChunks = []
waitingHandler.bufferedBytes = 0
return
}
const bufferedChunk = Buffer.from(chunk)
waitingHandler.bufferedChunks.push(bufferedChunk)
waitingHandler.bufferedBytes += bufferedChunk.length
if (waitingHandler.bufferedBytes > this.#maxBufferSize) {
const err = new RequestAbortedError(`Deduplicated waiting handler exceeded maxBufferSize (${this.#maxBufferSize} bytes) while paused`)
this.#errorWaitingHandler(waitingHandler, err)
}
}
/**
* @param {WaitingHandler} waitingHandler
*/
#flushWaitingHandler (waitingHandler) {
const { handler, controller } = waitingHandler
while (
!waitingHandler.done &&
!controller.aborted &&
!controller.paused &&
waitingHandler.bufferedChunks.length > 0
) {
const bufferedChunk = waitingHandler.bufferedChunks.shift()
waitingHandler.bufferedBytes -= bufferedChunk.length
try {
handler.onResponseData?.(controller, bufferedChunk)
} catch {
// Ignore errors from waiting handlers
}
if (controller.aborted) {
waitingHandler.done = true
waitingHandler.pendingTrailers = null
waitingHandler.bufferedChunks = []
waitingHandler.bufferedBytes = 0
break
}
}
}
/**
* @param {WaitingHandler} waitingHandler
* @param {Error} err
*/
#errorWaitingHandler (waitingHandler, err) {
if (waitingHandler.done) {
return
}
waitingHandler.done = true
waitingHandler.pendingTrailers = null
waitingHandler.bufferedChunks = []
waitingHandler.bufferedBytes = 0
try {
waitingHandler.controller.abort(err)
waitingHandler.handler.onResponseError?.(waitingHandler.controller, err)
} catch {
// Ignore errors from waiting handlers
}
}
#pruneDoneWaitingHandlers () {
this.#waitingHandlers = this.#waitingHandlers.filter(waitingHandler => waitingHandler.done === false)
}
}
module.exports = DeduplicationHandler
================================================
FILE: lib/handler/redirect-handler.js
================================================
'use strict'
const util = require('../core/util')
const { kBodyUsed } = require('../core/symbols')
const assert = require('node:assert')
const { InvalidArgumentError } = require('../core/errors')
const EE = require('node:events')
const redirectableStatusCodes = [300, 301, 302, 303, 307, 308]
const kBody = Symbol('body')
const noop = () => {}
class BodyAsyncIterable {
constructor (body) {
this[kBody] = body
this[kBodyUsed] = false
}
async * [Symbol.asyncIterator] () {
assert(!this[kBodyUsed], 'disturbed')
this[kBodyUsed] = true
yield * this[kBody]
}
}
class RedirectHandler {
static buildDispatch (dispatcher, maxRedirections) {
if (maxRedirections != null && (!Number.isInteger(maxRedirections) || maxRedirections < 0)) {
throw new InvalidArgumentError('maxRedirections must be a positive number')
}
const dispatch = dispatcher.dispatch.bind(dispatcher)
return (opts, originalHandler) => dispatch(opts, new RedirectHandler(dispatch, maxRedirections, opts, originalHandler))
}
constructor (dispatch, maxRedirections, opts, handler) {
if (maxRedirections != null && (!Number.isInteger(maxRedirections) || maxRedirections < 0)) {
throw new InvalidArgumentError('maxRedirections must be a positive number')
}
this.dispatch = dispatch
this.location = null
const { maxRedirections: _, ...cleanOpts } = opts
this.opts = cleanOpts // opts must be a copy, exclude maxRedirections
this.maxRedirections = maxRedirections
this.handler = handler
this.history = []
if (util.isStream(this.opts.body)) {
// TODO (fix): Provide some way for the user to cache the file to e.g. /tmp
// so that it can be dispatched again?
// TODO (fix): Do we need 100-expect support to provide a way to do this properly?
if (util.bodyLength(this.opts.body) === 0) {
this.opts.body
.on('data', function () {
assert(false)
})
}
if (typeof this.opts.body.readableDidRead !== 'boolean') {
this.opts.body[kBodyUsed] = false
EE.prototype.on.call(this.opts.body, 'data', function () {
this[kBodyUsed] = true
})
}
} else if (this.opts.body && typeof this.opts.body.pipeTo === 'function') {
// TODO (fix): We can't access ReadableStream internal state
// to determine whether or not it has been disturbed. This is just
// a workaround.
this.opts.body = new BodyAsyncIterable(this.opts.body)
} else if (
this.opts.body &&
typeof this.opts.body !== 'string' &&
!ArrayBuffer.isView(this.opts.body) &&
util.isIterable(this.opts.body) &&
!util.isFormDataLike(this.opts.body)
) {
// TODO: Should we allow re-using iterable if !this.opts.idempotent
// or through some other flag?
this.opts.body = new BodyAsyncIterable(this.opts.body)
}
}
onRequestStart (controller, context) {
this.handler.onRequestStart?.(controller, { ...context, history: this.history })
}
onRequestUpgrade (controller, statusCode, headers, socket) {
this.handler.onRequestUpgrade?.(controller, statusCode, headers, socket)
}
onResponseStart (controller, statusCode, headers, statusMessage) {
if (this.opts.throwOnMaxRedirect && this.history.length >= this.maxRedirections) {
throw new Error('max redirects')
}
// https://tools.ietf.org/html/rfc7231#section-6.4.2
// https://fetch.spec.whatwg.org/#http-redirect-fetch
// In case of HTTP 301 or 302 with POST, change the method to GET
if ((statusCode === 301 || statusCode === 302) && this.opts.method === 'POST') {
this.opts.method = 'GET'
if (util.isStream(this.opts.body)) {
util.destroy(this.opts.body.on('error', noop))
}
this.opts.body = null
}
// https://tools.ietf.org/html/rfc7231#section-6.4.4
// In case of HTTP 303, always replace method to be either HEAD or GET
if (statusCode === 303 && this.opts.method !== 'HEAD') {
this.opts.method = 'GET'
if (util.isStream(this.opts.body)) {
util.destroy(this.opts.body.on('error', noop))
}
this.opts.body = null
}
this.location = this.history.length >= this.maxRedirections || util.isDisturbed(this.opts.body) || redirectableStatusCodes.indexOf(statusCode) === -1
? null
: headers.location
if (this.opts.origin) {
this.history.push(new URL(this.opts.path, this.opts.origin))
}
if (!this.location) {
this.handler.onResponseStart?.(controller, statusCode, headers, statusMessage)
return
}
const { origin, pathname, search } = util.parseURL(new URL(this.location, this.opts.origin && new URL(this.opts.path, this.opts.origin)))
const path = search ? `${pathname}${search}` : pathname
// Check for redirect loops by seeing if we've already visited this URL in our history
// This catches the case where Client/Pool try to handle cross-origin redirects but fail
// and keep redirecting to the same URL in an infinite loop
const redirectUrlString = `${origin}${path}`
for (const historyUrl of this.history) {
if (historyUrl.toString() === redirectUrlString) {
throw new InvalidArgumentError(`Redirect loop detected. Cannot redirect to ${origin}. This typically happens when using a Client or Pool with cross-origin redirects. Use an Agent for cross-origin redirects.`)
}
}
// Remove headers referring to the original URL.
// By default it is Host only, unless it's a 303 (see below), which removes also all Content-* headers.
// https://tools.ietf.org/html/rfc7231#section-6.4
this.opts.headers = cleanRequestHeaders(this.opts.headers, statusCode === 303, this.opts.origin !== origin)
this.opts.path = path
this.opts.origin = origin
this.opts.query = null
}
onResponseData (controller, chunk) {
if (this.location) {
/*
https://tools.ietf.org/html/rfc7231#section-6.4
TLDR: undici always ignores 3xx response bodies.
Redirection is used to serve the requested resource from another URL, so it assumes that
no body is generated (and thus can be ignored). Even though generating a body is not prohibited.
For status 301, 302, 303, 307 and 308 (the latter from RFC 7238), the specs mention that the body usually
(which means it's optional and not mandated) contain just an hyperlink to the value of
the Location response header, so the body can be ignored safely.
For status 300, which is "Multiple Choices", the spec mentions both generating a Location
response header AND a response body with the other possible location to follow.
Since the spec explicitly chooses not to specify a format for such body and leave it to
servers and browsers implementors, we ignore the body as there is no specified way to eventually parse it.
*/
} else {
this.handler.onResponseData?.(controller, chunk)
}
}
onResponseEnd (controller, trailers) {
if (this.location) {
/*
https://tools.ietf.org/html/rfc7231#section-6.4
TLDR: undici always ignores 3xx response trailers as they are not expected in case of redirections
and neither are useful if present.
See comment on onData method above for more detailed information.
*/
this.dispatch(this.opts, this)
} else {
this.handler.onResponseEnd(controller, trailers)
}
}
onResponseError (controller, error) {
this.handler.onResponseError?.(controller, error)
}
}
// https://tools.ietf.org/html/rfc7231#section-6.4.4
function shouldRemoveHeader (header, removeContent, unknownOrigin) {
if (header.length === 4) {
return util.headerNameToString(header) === 'host'
}
if (removeContent && util.headerNameToString(header).startsWith('content-')) {
return true
}
if (unknownOrigin && (header.length === 13 || header.length === 6 || header.length === 19)) {
const name = util.headerNameToString(header)
return name === 'authorization' || name === 'cookie' || name === 'proxy-authorization'
}
return false
}
// https://tools.ietf.org/html/rfc7231#section-6.4
function cleanRequestHeaders (headers, removeContent, unknownOrigin) {
const ret = []
if (Array.isArray(headers)) {
for (let i = 0; i < headers.length; i += 2) {
if (!shouldRemoveHeader(headers[i], removeContent, unknownOrigin)) {
ret.push(headers[i], headers[i + 1])
}
}
} else if (headers && typeof headers === 'object') {
const entries = util.hasSafeIterator(headers) ? headers : Object.entries(headers)
for (const [key, value] of entries) {
if (!shouldRemoveHeader(key, removeContent, unknownOrigin)) {
ret.push(key, value)
}
}
} else {
assert(headers == null, 'headers must be an object or an array')
}
return ret
}
module.exports = RedirectHandler
================================================
FILE: lib/handler/retry-handler.js
================================================
'use strict'
const assert = require('node:assert')
const { kRetryHandlerDefaultRetry } = require('../core/symbols')
const { RequestRetryError } = require('../core/errors')
const WrapHandler = require('./wrap-handler')
const {
isDisturbed,
parseRangeHeader,
wrapRequestBody
} = require('../core/util')
function calculateRetryAfterHeader (retryAfter) {
const retryTime = new Date(retryAfter).getTime()
return isNaN(retryTime) ? 0 : retryTime - Date.now()
}
class RetryHandler {
constructor (opts, { dispatch, handler }) {
const { retryOptions, ...dispatchOpts } = opts
const {
// Retry scoped
retry: retryFn,
maxRetries,
maxTimeout,
minTimeout,
timeoutFactor,
// Response scoped
methods,
errorCodes,
retryAfter,
statusCodes,
throwOnError
} = retryOptions ?? {}
this.error = null
this.dispatch = dispatch
this.handler = WrapHandler.wrap(handler)
this.opts = { ...dispatchOpts, body: wrapRequestBody(opts.body) }
this.retryOpts = {
throwOnError: throwOnError ?? true,
retry: retryFn ?? RetryHandler[kRetryHandlerDefaultRetry],
retryAfter: retryAfter ?? true,
maxTimeout: maxTimeout ?? 30 * 1000, // 30s,
minTimeout: minTimeout ?? 500, // .5s
timeoutFactor: timeoutFactor ?? 2,
maxRetries: maxRetries ?? 5,
// What errors we should retry
methods: methods ?? ['GET', 'HEAD', 'OPTIONS', 'PUT', 'DELETE', 'TRACE'],
// Indicates which errors to retry
statusCodes: statusCodes ?? [500, 502, 503, 504, 429],
// List of errors to retry
errorCodes: errorCodes ?? [
'ECONNRESET',
'ECONNREFUSED',
'ENOTFOUND',
'ENETDOWN',
'ENETUNREACH',
'EHOSTDOWN',
'EHOSTUNREACH',
'EPIPE',
'UND_ERR_SOCKET'
]
}
this.retryCount = 0
this.retryCountCheckpoint = 0
this.headersSent = false
this.start = 0
this.end = null
this.etag = null
}
onResponseStartWithRetry (controller, statusCode, headers, statusMessage, err) {
if (this.retryOpts.throwOnError) {
// Preserve old behavior for status codes that are not eligible for retry
if (this.retryOpts.statusCodes.includes(statusCode) === false) {
this.headersSent = true
this.handler.onResponseStart?.(controller, statusCode, headers, statusMessage)
} else {
this.error = err
}
return
}
if (isDisturbed(this.opts.body)) {
this.headersSent = true
this.handler.onResponseStart?.(controller, statusCode, headers, statusMessage)
return
}
function shouldRetry (passedErr) {
if (passedErr) {
this.headersSent = true
this.handler.onResponseStart?.(controller, statusCode, headers, statusMessage)
controller.resume()
return
}
this.error = err
controller.resume()
}
controller.pause()
this.retryOpts.retry(
err,
{
state: { counter: this.retryCount },
opts: { retryOptions: this.retryOpts, ...this.opts }
},
shouldRetry.bind(this)
)
}
onRequestStart (controller, context) {
if (!this.headersSent) {
this.handler.onRequestStart?.(controller, context)
}
}
onRequestUpgrade (controller, statusCode, headers, socket) {
this.handler.onRequestUpgrade?.(controller, statusCode, headers, socket)
}
static [kRetryHandlerDefaultRetry] (err, { state, opts }, cb) {
const { statusCode, code, headers } = err
const { method, retryOptions } = opts
const {
maxRetries,
minTimeout,
maxTimeout,
timeoutFactor,
statusCodes,
errorCodes,
methods
} = retryOptions
const { counter } = state
// Any code that is not a Undici's originated and allowed to retry
if (code && code !== 'UND_ERR_REQ_RETRY' && !errorCodes.includes(code)) {
cb(err)
return
}
// If a set of method are provided and the current method is not in the list
if (Array.isArray(methods) && !methods.includes(method)) {
cb(err)
return
}
// If a set of status code are provided and the current status code is not in the list
if (
statusCode != null &&
Array.isArray(statusCodes) &&
!statusCodes.includes(statusCode)
) {
cb(err)
return
}
// If we reached the max number of retries
if (counter > maxRetries) {
cb(err)
return
}
let retryAfterHeader = headers?.['retry-after']
if (retryAfterHeader) {
retryAfterHeader = Number(retryAfterHeader)
retryAfterHeader = Number.isNaN(retryAfterHeader)
? calculateRetryAfterHeader(headers['retry-after'])
: retryAfterHeader * 1e3 // Retry-After is in seconds
}
const retryTimeout =
retryAfterHeader > 0
? Math.min(retryAfterHeader, maxTimeout)
: Math.min(minTimeout * timeoutFactor ** (counter - 1), maxTimeout)
setTimeout(() => cb(null), retryTimeout)
}
onResponseStart (controller, statusCode, headers, statusMessage) {
this.error = null
this.retryCount += 1
if (statusCode >= 300) {
const err = new RequestRetryError('Request failed', statusCode, {
headers,
data: {
count: this.retryCount
}
})
this.onResponseStartWithRetry(controller, statusCode, headers, statusMessage, err)
return
}
// Checkpoint for resume from where we left it
if (this.headersSent) {
// Only Partial Content 206 supposed to provide Content-Range,
// any other status code that partially consumed the payload
// should not be retried because it would result in downstream
// wrongly concatenate multiple responses.
if (statusCode !== 206 && (this.start > 0 || statusCode !== 200)) {
throw new RequestRetryError('server does not support the range header and the payload was partially consumed', statusCode, {
headers,
data: { count: this.retryCount }
})
}
const contentRange = parseRangeHeader(headers['content-range'])
// If no content range
if (!contentRange) {
// We always throw here as we want to indicate that we entred unexpected path
throw new RequestRetryError('Content-Range mismatch', statusCode, {
headers,
data: { count: this.retryCount }
})
}
// Let's start with a weak etag check
if (this.etag != null && this.etag !== headers.etag) {
// We always throw here as we want to indicate that we entred unexpected path
throw new RequestRetryError('ETag mismatch', statusCode, {
headers,
data: { count: this.retryCount }
})
}
const { start, size, end = size ? size - 1 : null } = contentRange
assert(this.start === start, 'content-range mismatch')
assert(this.end == null || this.end === end, 'content-range mismatch')
return
}
if (this.end == null) {
if (statusCode === 206) {
// First time we receive 206
const range = parseRangeHeader(headers['content-range'])
if (range == null) {
this.headersSent = true
this.handler.onResponseStart?.(
controller,
statusCode,
headers,
statusMessage
)
return
}
const { start, size, end = size ? size - 1 : null } = range
assert(
start != null && Number.isFinite(start),
'content-range mismatch'
)
assert(end != null && Number.isFinite(end), 'invalid content-length')
this.start = start
this.end = end
}
// We make our best to checkpoint the body for further range headers
if (this.end == null) {
const contentLength = headers['content-length']
this.end = contentLength != null ? Number(contentLength) - 1 : null
}
assert(Number.isFinite(this.start))
assert(
this.end == null || Number.isFinite(this.end),
'invalid content-length'
)
this.resume = true
this.etag = headers.etag != null ? headers.etag : null
// Weak etags are not useful for comparison nor cache
// for instance not safe to assume if the response is byte-per-byte
// equal
if (
this.etag != null &&
this.etag[0] === 'W' &&
this.etag[1] === '/'
) {
this.etag = null
}
this.headersSent = true
this.handler.onResponseStart?.(
controller,
statusCode,
headers,
statusMessage
)
} else {
throw new RequestRetryError('Request failed', statusCode, {
headers,
data: { count: this.retryCount }
})
}
}
onResponseData (controller, chunk) {
if (this.error) {
return
}
this.start += chunk.length
this.handler.onResponseData?.(controller, chunk)
}
onResponseEnd (controller, trailers) {
if (this.error && this.retryOpts.throwOnError) {
throw this.error
}
if (!this.error) {
this.retryCount = 0
return this.handler.onResponseEnd?.(controller, trailers)
}
this.retry(controller)
}
retry (controller) {
if (this.start !== 0) {
const headers = { range: `bytes=${this.start}-${this.end ?? ''}` }
// Weak etag check - weak etags will make comparison algorithms never match
if (this.etag != null) {
headers['if-match'] = this.etag
}
this.opts = {
...this.opts,
headers: {
...this.opts.headers,
...headers
}
}
}
try {
this.retryCountCheckpoint = this.retryCount
this.dispatch(this.opts, this)
} catch (err) {
this.handler.onResponseError?.(controller, err)
}
}
onResponseError (controller, err) {
if (controller?.aborted || isDisturbed(this.opts.body)) {
this.handler.onResponseError?.(controller, err)
return
}
function shouldRetry (returnedErr) {
if (!returnedErr) {
this.retry(controller)
return
}
this.handler?.onResponseError?.(controller, returnedErr)
}
// We reconcile in case of a mix between network errors
// and server error response
if (this.retryCount - this.retryCountCheckpoint > 0) {
// We count the difference between the last checkpoint and the current retry count
this.retryCount =
this.retryCountCheckpoint +
(this.retryCount - this.retryCountCheckpoint)
} else {
this.retryCount += 1
}
this.retryOpts.retry(
err,
{
state: { counter: this.retryCount },
opts: { retryOptions: this.retryOpts, ...this.opts }
},
shouldRetry.bind(this)
)
}
}
module.exports = RetryHandler
================================================
FILE: lib/handler/unwrap-handler.js
================================================
'use strict'
const { parseHeaders } = require('../core/util')
const { InvalidArgumentError } = require('../core/errors')
const kResume = Symbol('resume')
class UnwrapController {
#paused = false
#reason = null
#aborted = false
#abort
[kResume] = null
constructor (abort) {
this.#abort = abort
}
pause () {
this.#paused = true
}
resume () {
if (this.#paused) {
this.#paused = false
this[kResume]?.()
}
}
abort (reason) {
if (!this.#aborted) {
this.#aborted = true
this.#reason = reason
this.#abort(reason)
}
}
get aborted () {
return this.#aborted
}
get reason () {
return this.#reason
}
get paused () {
return this.#paused
}
}
module.exports = class UnwrapHandler {
#handler
#controller
constructor (handler) {
this.#handler = handler
}
static unwrap (handler) {
// TODO (fix): More checks...
return !handler.onRequestStart ? handler : new UnwrapHandler(handler)
}
onConnect (abort, context) {
this.#controller = new UnwrapController(abort)
this.#handler.onRequestStart?.(this.#controller, context)
}
onResponseStarted () {
return this.#handler.onResponseStarted?.()
}
onUpgrade (statusCode, rawHeaders, socket) {
this.#handler.onRequestUpgrade?.(this.#controller, statusCode, parseHeaders(rawHeaders), socket)
}
onHeaders (statusCode, rawHeaders, resume, statusMessage) {
this.#controller[kResume] = resume
this.#handler.onResponseStart?.(this.#controller, statusCode, parseHeaders(rawHeaders), statusMessage)
return !this.#controller.paused
}
onData (data) {
this.#handler.onResponseData?.(this.#controller, data)
return !this.#controller.paused
}
onComplete (rawTrailers) {
this.#handler.onResponseEnd?.(this.#controller, parseHeaders(rawTrailers))
}
onError (err) {
if (!this.#handler.onResponseError) {
throw new InvalidArgumentError('invalid onError method')
}
this.#handler.onResponseError?.(this.#controller, err)
}
}
================================================
FILE: lib/handler/wrap-handler.js
================================================
'use strict'
const { InvalidArgumentError } = require('../core/errors')
module.exports = class WrapHandler {
#handler
constructor (handler) {
this.#handler = handler
}
static wrap (handler) {
// TODO (fix): More checks...
return handler.onRequestStart ? handler : new WrapHandler(handler)
}
// Unwrap Interface
onConnect (abort, context) {
return this.#handler.onConnect?.(abort, context)
}
onResponseStarted () {
return this.#handler.onResponseStarted?.()
}
onHeaders (statusCode, rawHeaders, resume, statusMessage) {
return this.#handler.onHeaders?.(statusCode, rawHeaders, resume, statusMessage)
}
onUpgrade (statusCode, rawHeaders, socket) {
return this.#handler.onUpgrade?.(statusCode, rawHeaders, socket)
}
onData (data) {
return this.#handler.onData?.(data)
}
onComplete (trailers) {
return this.#handler.onComplete?.(trailers)
}
onError (err) {
if (!this.#handler.onError) {
throw err
}
return this.#handler.onError?.(err)
}
// Wrap Interface
onRequestStart (controller, context) {
this.#handler.onConnect?.((reason) => controller.abort(reason), context)
}
onRequestUpgrade (controller, statusCode, headers, socket) {
const rawHeaders = []
for (const [key, val] of Object.entries(headers)) {
rawHeaders.push(Buffer.from(key, 'latin1'), toRawHeaderValue(val))
}
this.#handler.onUpgrade?.(statusCode, rawHeaders, socket)
}
onResponseStart (controller, statusCode, headers, statusMessage) {
const rawHeaders = []
for (const [key, val] of Object.entries(headers)) {
rawHeaders.push(Buffer.from(key, 'latin1'), toRawHeaderValue(val))
}
if (this.#handler.onHeaders?.(statusCode, rawHeaders, () => controller.resume(), statusMessage) === false) {
controller.pause()
}
}
onResponseData (controller, data) {
if (this.#handler.onData?.(data) === false) {
controller.pause()
}
}
onResponseEnd (controller, trailers) {
const rawTrailers = []
for (const [key, val] of Object.entries(trailers)) {
rawTrailers.push(Buffer.from(key, 'latin1'), toRawHeaderValue(val))
}
this.#handler.onComplete?.(rawTrailers)
}
onResponseError (controller, err) {
if (!this.#handler.onError) {
throw new InvalidArgumentError('invalid onError method')
}
this.#handler.onError?.(err)
}
}
function toRawHeaderValue (value) {
return Array.isArray(value)
? value.map((item) => Buffer.from(item, 'latin1'))
: Buffer.from(value, 'latin1')
}
================================================
FILE: lib/interceptor/cache.js
================================================
'use strict'
const assert = require('node:assert')
const { Readable } = require('node:stream')
const util = require('../core/util')
const CacheHandler = require('../handler/cache-handler')
const MemoryCacheStore = require('../cache/memory-cache-store')
const CacheRevalidationHandler = require('../handler/cache-revalidation-handler')
const { assertCacheStore, assertCacheMethods, makeCacheKey, normalizeHeaders, parseCacheControlHeader } = require('../util/cache.js')
const { AbortError } = require('../core/errors.js')
/**
* @param {(string | RegExp)[] | undefined} origins
* @param {string} name
*/
function assertCacheOrigins (origins, name) {
if (origins === undefined) return
if (!Array.isArray(origins)) {
throw new TypeError(`expected ${name} to be an array or undefined, got ${typeof origins}`)
}
for (let i = 0; i < origins.length; i++) {
const origin = origins[i]
if (typeof origin !== 'string' && !(origin instanceof RegExp)) {
throw new TypeError(`expected ${name}[${i}] to be a string or RegExp, got ${typeof origin}`)
}
}
}
const nop = () => {}
/**
* @typedef {(options: import('../../types/dispatcher.d.ts').default.DispatchOptions, handler: import('../../types/dispatcher.d.ts').default.DispatchHandler) => void} DispatchFn
*/
/**
* @param {import('../../types/cache-interceptor.d.ts').default.GetResult} result
* @param {import('../../types/cache-interceptor.d.ts').default.CacheControlDirectives | undefined} cacheControlDirectives
* @param {import('../../types/dispatcher.d.ts').default.RequestOptions} opts
* @returns {boolean}
*/
function needsRevalidation (result, cacheControlDirectives, { headers = {} }) {
// Always revalidate requests with the no-cache request directive.
if (cacheControlDirectives?.['no-cache']) {
return true
}
// Always revalidate requests with unqualified no-cache response directive.
if (result.cacheControlDirectives?.['no-cache'] && !Array.isArray(result.cacheControlDirectives['no-cache'])) {
return true
}
// Always revalidate requests with conditional headers.
if (headers['if-modified-since'] || headers['if-none-match']) {
return true
}
return false
}
/**
* @param {import('../../types/cache-interceptor.d.ts').default.GetResult} result
* @param {import('../../types/cache-interceptor.d.ts').default.CacheControlDirectives | undefined} cacheControlDirectives
* @returns {boolean}
*/
function isStale (result, cacheControlDirectives) {
const now = Date.now()
if (now > result.staleAt) {
// Response is stale
if (cacheControlDirectives?.['max-stale']) {
// There's a threshold where we can serve stale responses, let's see if
// we're in it
// https://www.rfc-editor.org/rfc/rfc9111.html#name-max-stale
const gracePeriod = result.staleAt + (cacheControlDirectives['max-stale'] * 1000)
return now > gracePeriod
}
return true
}
if (cacheControlDirectives?.['min-fresh']) {
// https://www.rfc-editor.org/rfc/rfc9111.html#section-5.2.1.3
// At this point, staleAt is always > now
const timeLeftTillStale = result.staleAt - now
const threshold = cacheControlDirectives['min-fresh'] * 1000
return timeLeftTillStale <= threshold
}
return false
}
/**
* Check if we're within the stale-while-revalidate window for a stale response
* @param {import('../../types/cache-interceptor.d.ts').default.GetResult} result
* @returns {boolean}
*/
function withinStaleWhileRevalidateWindow (result) {
const staleWhileRevalidate = result.cacheControlDirectives?.['stale-while-revalidate']
if (!staleWhileRevalidate) {
return false
}
const now = Date.now()
const staleWhileRevalidateExpiry = result.staleAt + (staleWhileRevalidate * 1000)
return now <= staleWhileRevalidateExpiry
}
/**
* @param {DispatchFn} dispatch
* @param {import('../../types/cache-interceptor.d.ts').default.CacheHandlerOptions} globalOpts
* @param {import('../../types/cache-interceptor.d.ts').default.CacheKey} cacheKey
* @param {import('../../types/dispatcher.d.ts').default.DispatchHandler} handler
* @param {import('../../types/dispatcher.d.ts').default.RequestOptions} opts
* @param {import('../../types/cache-interceptor.d.ts').default.CacheControlDirectives | undefined} reqCacheControl
*/
function handleUncachedResponse (
dispatch,
globalOpts,
cacheKey,
handler,
opts,
reqCacheControl
) {
if (reqCacheControl?.['only-if-cached']) {
let aborted = false
try {
if (typeof handler.onConnect === 'function') {
handler.onConnect(() => {
aborted = true
})
if (aborted) {
return
}
}
if (typeof handler.onHeaders === 'function') {
handler.onHeaders(504, [], nop, 'Gateway Timeout')
if (aborted) {
return
}
}
if (typeof handler.onComplete === 'function') {
handler.onComplete([])
}
} catch (err) {
if (typeof handler.onError === 'function') {
handler.onError(err)
}
}
return true
}
return dispatch(opts, new CacheHandler(globalOpts, cacheKey, handler))
}
/**
* @param {import('../../types/dispatcher.d.ts').default.DispatchHandler} handler
* @param {import('../../types/dispatcher.d.ts').default.RequestOptions} opts
* @param {import('../../types/cache-interceptor.d.ts').default.GetResult} result
* @param {number} age
* @param {any} context
* @param {boolean} isStale
*/
function sendCachedValue (handler, opts, result, age, context, isStale) {
// TODO (perf): Readable.from path can be optimized...
const stream = util.isStream(result.body)
? result.body
: Readable.from(result.body ?? [])
assert(!stream.destroyed, 'stream should not be destroyed')
assert(!stream.readableDidRead, 'stream should not be readableDidRead')
const controller = {
resume () {
stream.resume()
},
pause () {
stream.pause()
},
get paused () {
return stream.isPaused()
},
get aborted () {
return stream.destroyed
},
get reason () {
return stream.errored
},
abort (reason) {
stream.destroy(reason ?? new AbortError())
}
}
stream
.on('error', function (err) {
if (!this.readableEnded) {
if (typeof handler.onResponseError === 'function') {
handler.onResponseError(controller, err)
} else {
throw err
}
}
})
.on('close', function () {
if (!this.errored) {
handler.onResponseEnd?.(controller, {})
}
})
handler.onRequestStart?.(controller, context)
if (stream.destroyed) {
return
}
// Add the age header
// https://www.rfc-editor.org/rfc/rfc9111.html#name-age
const headers = { ...result.headers, age: String(age) }
if (isStale) {
// Add warning header
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Warning
headers.warning = '110 - "response is stale"'
}
handler.onResponseStart?.(controller, result.statusCode, headers, result.statusMessage)
if (opts.method === 'HEAD') {
stream.destroy()
} else {
stream.on('data', function (chunk) {
handler.onResponseData?.(controller, chunk)
})
}
}
/**
* @param {DispatchFn} dispatch
* @param {import('../../types/cache-interceptor.d.ts').default.CacheHandlerOptions} globalOpts
* @param {import('../../types/cache-interceptor.d.ts').default.CacheKey} cacheKey
* @param {import('../../types/dispatcher.d.ts').default.DispatchHandler} handler
* @param {import('../../types/dispatcher.d.ts').default.RequestOptions} opts
* @param {import('../../types/cache-interceptor.d.ts').default.CacheControlDirectives | undefined} reqCacheControl
* @param {import('../../types/cache-interceptor.d.ts').default.GetResult | undefined} result
*/
function handleResult (
dispatch,
globalOpts,
cacheKey,
handler,
opts,
reqCacheControl,
result
) {
if (!result) {
return handleUncachedResponse(dispatch, globalOpts, cacheKey, handler, opts, reqCacheControl)
}
const now = Date.now()
if (now > result.deleteAt) {
// Response is expired, cache store shouldn't have given this to us
return dispatch(opts, new CacheHandler(globalOpts, cacheKey, handler))
}
const age = Math.round((now - result.cachedAt) / 1000)
if (reqCacheControl?.['max-age'] && age >= reqCacheControl['max-age']) {
// Response is considered expired for this specific request
// https://www.rfc-editor.org/rfc/rfc9111.html#section-5.2.1.1
return dispatch(opts, handler)
}
const stale = isStale(result, reqCacheControl)
const revalidate = needsRevalidation(result, reqCacheControl, opts)
// Check if the response is stale
if (stale || revalidate) {
if (util.isStream(opts.body) && util.bodyLength(opts.body) !== 0) {
// If body is a stream we can't revalidate...
// TODO (fix): This could be less strict...
return dispatch(opts, new CacheHandler(globalOpts, cacheKey, handler))
}
// RFC 5861: If we're within stale-while-revalidate window, serve stale immediately
// and revalidate in background, unless immediate revalidation is necessary
if (!revalidate && withinStaleWhileRevalidateWindow(result)) {
// Serve stale response immediately
sendCachedValue(handler, opts, result, age, null, true)
// Start background revalidation (fire-and-forget)
queueMicrotask(() => {
const headers = {
...opts.headers,
'if-modified-since': new Date(result.cachedAt).toUTCString()
}
if (result.etag) {
headers['if-none-match'] = result.etag
}
if (result.vary) {
for (const key in result.vary) {
if (result.vary[key] != null) {
headers[key] = result.vary[key]
}
}
}
// Background revalidation - update cache if we get new data
dispatch(
{
...opts,
headers
},
new CacheHandler(globalOpts, cacheKey, {
// Silent handler that just updates the cache
onRequestStart () {},
onRequestUpgrade () {},
onResponseStart () {},
onResponseData () {},
onResponseEnd () {},
onResponseError () {}
})
)
})
return true
}
let withinStaleIfErrorThreshold = false
const staleIfErrorExpiry = result.cacheControlDirectives['stale-if-error'] ?? reqCacheControl?.['stale-if-error']
if (staleIfErrorExpiry) {
withinStaleIfErrorThreshold = now < (result.staleAt + (staleIfErrorExpiry * 1000))
}
const headers = {
...opts.headers,
'if-modified-since': new Date(result.cachedAt).toUTCString()
}
if (result.etag) {
headers['if-none-match'] = result.etag
}
if (result.vary) {
for (const key in result.vary) {
if (result.vary[key] != null) {
headers[key] = result.vary[key]
}
}
}
// We need to revalidate the response
return dispatch(
{
...opts,
headers
},
new CacheRevalidationHandler(
(success, context) => {
if (success) {
// TODO: successful revalidation should be considered fresh (not give stale warning).
sendCachedValue(handler, opts, result, age, context, stale)
} else if (util.isStream(result.body)) {
result.body.on('error', nop).destroy()
}
},
new CacheHandler(globalOpts, cacheKey, handler),
withinStaleIfErrorThreshold
)
)
}
// Dump request body.
if (util.isStream(opts.body)) {
opts.body.on('error', nop).destroy()
}
sendCachedValue(handler, opts, result, age, null, false)
}
/**
* @param {import('../../types/cache-interceptor.d.ts').default.CacheOptions} [opts]
* @returns {import('../../types/dispatcher.d.ts').default.DispatcherComposeInterceptor}
*/
module.exports = (opts = {}) => {
const {
store = new MemoryCacheStore(),
methods = ['GET'],
cacheByDefault = undefined,
type = 'shared',
origins = undefined
} = opts
if (typeof opts !== 'object' || opts === null) {
throw new TypeError(`expected type of opts to be an Object, got ${opts === null ? 'null' : typeof opts}`)
}
assertCacheStore(store, 'opts.store')
assertCacheMethods(methods, 'opts.methods')
assertCacheOrigins(origins, 'opts.origins')
if (typeof cacheByDefault !== 'undefined' && typeof cacheByDefault !== 'number') {
throw new TypeError(`expected opts.cacheByDefault to be number or undefined, got ${typeof cacheByDefault}`)
}
if (typeof type !== 'undefined' && type !== 'shared' && type !== 'private') {
throw new TypeError(`expected opts.type to be shared, private, or undefined, got ${typeof type}`)
}
const globalOpts = {
store,
methods,
cacheByDefault,
type
}
const safeMethodsToNotCache = util.safeHTTPMethods.filter(method => methods.includes(method) === false)
return dispatch => {
return (opts, handler) => {
if (!opts.origin || safeMethodsToNotCache.includes(opts.method)) {
// Not a method we want to cache or we don't have the origin, skip
return dispatch(opts, handler)
}
// Check if origin is in whitelist
if (origins !== undefined) {
const requestOrigin = opts.origin.toString().toLowerCase()
let isAllowed = false
for (let i = 0; i < origins.length; i++) {
const allowed = origins[i]
if (typeof allowed === 'string') {
if (allowed.toLowerCase() === requestOrigin) {
isAllowed = true
break
}
} else if (allowed.test(requestOrigin)) {
isAllowed = true
break
}
}
if (!isAllowed) {
return dispatch(opts, handler)
}
}
opts = {
...opts,
headers: normalizeHeaders(opts)
}
const reqCacheControl = opts.headers?.['cache-control']
? parseCacheControlHeader(opts.headers['cache-control'])
: undefined
if (reqCacheControl?.['no-store']) {
return dispatch(opts, handler)
}
/**
* @type {import('../../types/cache-interceptor.d.ts').default.CacheKey}
*/
const cacheKey = makeCacheKey(opts)
const result = store.get(cacheKey)
if (result && typeof result.then === 'function') {
return result
.then(result => handleResult(dispatch,
globalOpts,
cacheKey,
handler,
opts,
reqCacheControl,
result
))
} else {
return handleResult(
dispatch,
globalOpts,
cacheKey,
handler,
opts,
reqCacheControl,
result
)
}
}
}
}
================================================
FILE: lib/interceptor/decompress.js
================================================
'use strict'
const { createInflate, createGunzip, createBrotliDecompress, createZstdDecompress } = require('node:zlib')
const { pipeline } = require('node:stream')
const DecoratorHandler = require('../handler/decorator-handler')
const { runtimeFeatures } = require('../util/runtime-features')
/** @typedef {import('node:stream').Transform} Transform */
/** @typedef {import('node:stream').Transform} Controller */
/** @typedef {Transform&import('node:zlib').Zlib} DecompressorStream */
/** @type {Record DecompressorStream>} */
const supportedEncodings = {
gzip: createGunzip,
'x-gzip': createGunzip,
br: createBrotliDecompress,
deflate: createInflate,
compress: createInflate,
'x-compress': createInflate,
...(runtimeFeatures.has('zstd') ? { zstd: createZstdDecompress } : {})
}
const defaultSkipStatusCodes = /** @type {const} */ ([204, 304])
let warningEmitted = /** @type {boolean} */ (false)
/**
* @typedef {Object} DecompressHandlerOptions
* @property {number[]|Readonly} [skipStatusCodes=[204, 304]] - List of status codes to skip decompression for
* @property {boolean} [skipErrorResponses] - Whether to skip decompression for error responses (status codes >= 400)
*/
class DecompressHandler extends DecoratorHandler {
/** @type {Transform[]} */
#decompressors = []
/** @type {Readonly} */
#skipStatusCodes
/** @type {boolean} */
#skipErrorResponses
constructor (handler, { skipStatusCodes = defaultSkipStatusCodes, skipErrorResponses = true } = {}) {
super(handler)
this.#skipStatusCodes = skipStatusCodes
this.#skipErrorResponses = skipErrorResponses
}
/**
* Determines if decompression should be skipped based on encoding and status code
* @param {string} contentEncoding - Content-Encoding header value
* @param {number} statusCode - HTTP status code of the response
* @returns {boolean} - True if decompression should be skipped
*/
#shouldSkipDecompression (contentEncoding, statusCode) {
if (!contentEncoding || statusCode < 200) return true
if (this.#skipStatusCodes.includes(statusCode)) return true
if (this.#skipErrorResponses && statusCode >= 400) return true
return false
}
/**
* Creates a chain of decompressors for multiple content encodings
*
* @param {string} encodings - Comma-separated list of content encodings
* @returns {Array} - Array of decompressor streams
* @throws {Error} - If the number of content-encodings exceeds the maximum allowed
*/
#createDecompressionChain (encodings) {
const parts = encodings.split(',')
// Limit the number of content-encodings to prevent resource exhaustion.
// CVE fix similar to urllib3 (GHSA-gm62-xv2j-4w53) and curl (CVE-2022-32206).
const maxContentEncodings = 5
if (parts.length > maxContentEncodings) {
throw new Error(`too many content-encodings in response: ${parts.length}, maximum allowed is ${maxContentEncodings}`)
}
/** @type {DecompressorStream[]} */
const decompressors = []
for (let i = parts.length - 1; i >= 0; i--) {
const encoding = parts[i].trim()
if (!encoding) continue
if (!supportedEncodings[encoding]) {
decompressors.length = 0 // Clear if unsupported encoding
return decompressors // Unsupported encoding
}
decompressors.push(supportedEncodings[encoding]())
}
return decompressors
}
/**
* Sets up event handlers for a decompressor stream using readable events
* @param {DecompressorStream} decompressor - The decompressor stream
* @param {Controller} controller - The controller to coordinate with
* @returns {void}
*/
#setupDecompressorEvents (decompressor, controller) {
decompressor.on('readable', () => {
let chunk
while ((chunk = decompressor.read()) !== null) {
const result = super.onResponseData(controller, chunk)
if (result === false) {
break
}
}
})
decompressor.on('error', (error) => {
super.onResponseError(controller, error)
})
}
/**
* Sets up event handling for a single decompressor
* @param {Controller} controller - The controller to handle events
* @returns {void}
*/
#setupSingleDecompressor (controller) {
const decompressor = this.#decompressors[0]
this.#setupDecompressorEvents(decompressor, controller)
decompressor.on('end', () => {
super.onResponseEnd(controller, {})
})
}
/**
* Sets up event handling for multiple chained decompressors using pipeline
* @param {Controller} controller - The controller to handle events
* @returns {void}
*/
#setupMultipleDecompressors (controller) {
const lastDecompressor = this.#decompressors[this.#decompressors.length - 1]
this.#setupDecompressorEvents(lastDecompressor, controller)
pipeline(this.#decompressors, (err) => {
if (err) {
super.onResponseError(controller, err)
return
}
super.onResponseEnd(controller, {})
})
}
/**
* Cleans up decompressor references to prevent memory leaks
* @returns {void}
*/
#cleanupDecompressors () {
this.#decompressors.length = 0
}
/**
* @param {Controller} controller
* @param {number} statusCode
* @param {Record} headers
* @param {string} statusMessage
* @returns {void}
*/
onResponseStart (controller, statusCode, headers, statusMessage) {
const contentEncoding = headers['content-encoding']
// If content encoding is not supported or status code is in skip list
if (this.#shouldSkipDecompression(contentEncoding, statusCode)) {
return super.onResponseStart(controller, statusCode, headers, statusMessage)
}
const decompressors = this.#createDecompressionChain(contentEncoding.toLowerCase())
if (decompressors.length === 0) {
this.#cleanupDecompressors()
return super.onResponseStart(controller, statusCode, headers, statusMessage)
}
this.#decompressors = decompressors
// Remove compression headers since we're decompressing
const { 'content-encoding': _, 'content-length': __, ...newHeaders } = headers
if (this.#decompressors.length === 1) {
this.#setupSingleDecompressor(controller)
} else {
this.#setupMultipleDecompressors(controller)
}
return super.onResponseStart(controller, statusCode, newHeaders, statusMessage)
}
/**
* @param {Controller} controller
* @param {Buffer} chunk
* @returns {void}
*/
onResponseData (controller, chunk) {
if (this.#decompressors.length > 0) {
this.#decompressors[0].write(chunk)
return
}
super.onResponseData(controller, chunk)
}
/**
* @param {Controller} controller
* @param {Record | undefined} trailers
* @returns {void}
*/
onResponseEnd (controller, trailers) {
if (this.#decompressors.length > 0) {
this.#decompressors[0].end()
this.#cleanupDecompressors()
return
}
super.onResponseEnd(controller, trailers)
}
/**
* @param {Controller} controller
* @param {Error} err
* @returns {void}
*/
onResponseError (controller, err) {
if (this.#decompressors.length > 0) {
for (const decompressor of this.#decompressors) {
decompressor.destroy(err)
}
this.#cleanupDecompressors()
}
super.onResponseError(controller, err)
}
}
/**
* Creates a decompression interceptor for HTTP responses
* @param {DecompressHandlerOptions} [options] - Options for the interceptor
* @returns {Function} - Interceptor function
*/
function createDecompressInterceptor (options = {}) {
// Emit experimental warning only once
if (!warningEmitted) {
process.emitWarning(
'DecompressInterceptor is experimental and subject to change',
'ExperimentalWarning'
)
warningEmitted = true
}
return (dispatch) => {
return (opts, handler) => {
const decompressHandler = new DecompressHandler(handler, options)
return dispatch(opts, decompressHandler)
}
}
}
module.exports = createDecompressInterceptor
================================================
FILE: lib/interceptor/deduplicate.js
================================================
'use strict'
const diagnosticsChannel = require('node:diagnostics_channel')
const util = require('../core/util')
const DeduplicationHandler = require('../handler/deduplication-handler')
const { normalizeHeaders, makeCacheKey, makeDeduplicationKey } = require('../util/cache.js')
const pendingRequestsChannel = diagnosticsChannel.channel('undici:request:pending-requests')
/**
* @param {import('../../types/interceptors.d.ts').default.DeduplicateInterceptorOpts} [opts]
* @returns {import('../../types/dispatcher.d.ts').default.DispatcherComposeInterceptor}
*/
module.exports = (opts = {}) => {
const {
methods = ['GET'],
skipHeaderNames = [],
excludeHeaderNames = [],
maxBufferSize = 5 * 1024 * 1024
} = opts
if (typeof opts !== 'object' || opts === null) {
throw new TypeError(`expected type of opts to be an Object, got ${opts === null ? 'null' : typeof opts}`)
}
if (!Array.isArray(methods)) {
throw new TypeError(`expected opts.methods to be an array, got ${typeof methods}`)
}
for (const method of methods) {
if (!util.safeHTTPMethods.includes(method)) {
throw new TypeError(`expected opts.methods to only contain safe HTTP methods, got ${method}`)
}
}
if (!Array.isArray(skipHeaderNames)) {
throw new TypeError(`expected opts.skipHeaderNames to be an array, got ${typeof skipHeaderNames}`)
}
if (!Array.isArray(excludeHeaderNames)) {
throw new TypeError(`expected opts.excludeHeaderNames to be an array, got ${typeof excludeHeaderNames}`)
}
if (!Number.isFinite(maxBufferSize) || maxBufferSize <= 0) {
throw new TypeError(`expected opts.maxBufferSize to be a positive finite number, got ${maxBufferSize}`)
}
// Convert to lowercase Set for case-insensitive header matching
const skipHeaderNamesSet = new Set(skipHeaderNames.map(name => name.toLowerCase()))
// Convert to lowercase Set for case-insensitive header exclusion from deduplication key
const excludeHeaderNamesSet = new Set(excludeHeaderNames.map(name => name.toLowerCase()))
/**
* Map of pending requests for deduplication
* @type {Map}
*/
const pendingRequests = new Map()
return dispatch => {
return (opts, handler) => {
if (!opts.origin || methods.includes(opts.method) === false) {
return dispatch(opts, handler)
}
opts = {
...opts,
headers: normalizeHeaders(opts)
}
// Skip deduplication if request contains any of the specified headers
if (skipHeaderNamesSet.size > 0) {
for (const headerName of Object.keys(opts.headers)) {
if (skipHeaderNamesSet.has(headerName.toLowerCase())) {
return dispatch(opts, handler)
}
}
}
const cacheKey = makeCacheKey(opts)
const dedupeKey = makeDeduplicationKey(cacheKey, excludeHeaderNamesSet)
// Check if there's already a pending request for this key
const pendingHandler = pendingRequests.get(dedupeKey)
if (pendingHandler) {
// Add this handler to the waiting list when safe.
// If body streaming has already started, this request must be sent independently.
if (pendingHandler.addWaitingHandler(handler)) {
return true
}
return dispatch(opts, handler)
}
// Create a new deduplication handler
const deduplicationHandler = new DeduplicationHandler(
handler,
() => {
// Clean up when request completes
pendingRequests.delete(dedupeKey)
if (pendingRequestsChannel.hasSubscribers) {
pendingRequestsChannel.publish({ size: pendingRequests.size, key: dedupeKey, type: 'removed' })
}
},
maxBufferSize
)
// Register the pending request
pendingRequests.set(dedupeKey, deduplicationHandler)
if (pendingRequestsChannel.hasSubscribers) {
pendingRequestsChannel.publish({ size: pendingRequests.size, key: dedupeKey, type: 'added' })
}
return dispatch(opts, deduplicationHandler)
}
}
}
================================================
FILE: lib/interceptor/dns.js
================================================
'use strict'
const { isIP } = require('node:net')
const { lookup } = require('node:dns')
const DecoratorHandler = require('../handler/decorator-handler')
const { InvalidArgumentError, InformationalError } = require('../core/errors')
const maxInt = Math.pow(2, 31) - 1
function hasSafeIterator (headers) {
const prototype = Object.getPrototypeOf(headers)
const ownIterator = Object.prototype.hasOwnProperty.call(headers, Symbol.iterator)
return ownIterator || (prototype != null && prototype !== Object.prototype && typeof headers[Symbol.iterator] === 'function')
}
function isHostHeader (key) {
return typeof key === 'string' && key.toLowerCase() === 'host'
}
function normalizeHeaders (headers) {
if (headers == null) {
return null
}
if (Array.isArray(headers)) {
if (headers.length === 0 || !Array.isArray(headers[0])) {
return headers
}
const normalized = []
for (const header of headers) {
if (Array.isArray(header) && header.length === 2) {
normalized.push(header[0], header[1])
} else {
normalized.push(header)
}
}
return normalized
}
if (typeof headers === 'object' && hasSafeIterator(headers)) {
const normalized = []
for (const header of headers) {
if (Array.isArray(header) && header.length === 2) {
normalized.push(header[0], header[1])
} else {
normalized.push(header)
}
}
return normalized
}
return headers
}
function hasHostHeader (headers) {
if (headers == null) {
return false
}
if (Array.isArray(headers)) {
if (headers.length === 0) {
return false
}
for (let i = 0; i < headers.length; i += 2) {
if (isHostHeader(headers[i])) {
return true
}
}
return false
}
if (typeof headers === 'object') {
for (const key in headers) {
if (isHostHeader(key)) {
return true
}
}
}
return false
}
function withHostHeader (host, headers) {
const normalizedHeaders = normalizeHeaders(headers)
if (hasHostHeader(normalizedHeaders)) {
return normalizedHeaders
}
if (Array.isArray(normalizedHeaders)) {
return ['host', host, ...normalizedHeaders]
}
if (normalizedHeaders && typeof normalizedHeaders === 'object') {
return {
host,
...normalizedHeaders
}
}
return { host }
}
class DNSStorage {
#maxItems = 0
#records = new Map()
constructor (opts) {
this.#maxItems = opts.maxItems
}
get size () {
return this.#records.size
}
get (hostname) {
return this.#records.get(hostname) ?? null
}
set (hostname, records) {
this.#records.set(hostname, records)
}
delete (hostname) {
this.#records.delete(hostname)
}
// Delegate to storage decide can we do more lookups or not
full () {
return this.size >= this.#maxItems
}
}
class DNSInstance {
#maxTTL = 0
#maxItems = 0
dualStack = true
affinity = null
lookup = null
pick = null
storage = null
constructor (opts) {
this.#maxTTL = opts.maxTTL
this.#maxItems = opts.maxItems
this.dualStack = opts.dualStack
this.affinity = opts.affinity
this.lookup = opts.lookup ?? this.#defaultLookup
this.pick = opts.pick ?? this.#defaultPick
this.storage = opts.storage ?? new DNSStorage(opts)
}
runLookup (origin, opts, cb) {
const ips = this.storage.get(origin.hostname)
// If full, we just return the origin
if (ips == null && this.storage.full()) {
cb(null, origin)
return
}
const newOpts = {
affinity: this.affinity,
dualStack: this.dualStack,
lookup: this.lookup,
pick: this.pick,
...opts.dns,
maxTTL: this.#maxTTL,
maxItems: this.#maxItems
}
// If no IPs we lookup
if (ips == null) {
this.lookup(origin, newOpts, (err, addresses) => {
if (err || addresses == null || addresses.length === 0) {
cb(err ?? new InformationalError('No DNS entries found'))
return
}
this.setRecords(origin, addresses)
const records = this.storage.get(origin.hostname)
const ip = this.pick(
origin,
records,
newOpts.affinity
)
let port
if (typeof ip.port === 'number') {
port = `:${ip.port}`
} else if (origin.port !== '') {
port = `:${origin.port}`
} else {
port = ''
}
cb(
null,
new URL(`${origin.protocol}//${
ip.family === 6 ? `[${ip.address}]` : ip.address
}${port}`)
)
})
} else {
// If there's IPs we pick
const ip = this.pick(
origin,
ips,
newOpts.affinity
)
// If no IPs we lookup - deleting old records
if (ip == null) {
this.storage.delete(origin.hostname)
this.runLookup(origin, opts, cb)
return
}
let port
if (typeof ip.port === 'number') {
port = `:${ip.port}`
} else if (origin.port !== '') {
port = `:${origin.port}`
} else {
port = ''
}
cb(
null,
new URL(`${origin.protocol}//${
ip.family === 6 ? `[${ip.address}]` : ip.address
}${port}`)
)
}
}
#defaultLookup (origin, opts, cb) {
lookup(
origin.hostname,
{
all: true,
family: this.dualStack === false ? this.affinity : 0,
order: 'ipv4first'
},
(err, addresses) => {
if (err) {
return cb(err)
}
const results = new Map()
for (const addr of addresses) {
// On linux we found duplicates, we attempt to remove them with
// the latest record
results.set(`${addr.address}:${addr.family}`, addr)
}
cb(null, results.values())
}
)
}
#defaultPick (origin, hostnameRecords, affinity) {
let ip = null
const { records, offset } = hostnameRecords
let family
if (this.dualStack) {
if (affinity == null) {
// Balance between ip families
if (offset == null || offset === maxInt) {
hostnameRecords.offset = 0
affinity = 4
} else {
hostnameRecords.offset++
affinity = (hostnameRecords.offset & 1) === 1 ? 6 : 4
}
}
if (records[affinity] != null && records[affinity].ips.length > 0) {
family = records[affinity]
} else {
family = records[affinity === 4 ? 6 : 4]
}
} else {
family = records[affinity]
}
// If no IPs we return null
if (family == null || family.ips.length === 0) {
return ip
}
if (family.offset == null || family.offset === maxInt) {
family.offset = 0
} else {
family.offset++
}
const position = family.offset % family.ips.length
ip = family.ips[position] ?? null
if (ip == null) {
return ip
}
if (Date.now() - ip.timestamp > ip.ttl) { // record TTL is already in ms
// We delete expired records
// It is possible that they have different TTL, so we manage them individually
family.ips.splice(position, 1)
return this.pick(origin, hostnameRecords, affinity)
}
return ip
}
pickFamily (origin, ipFamily) {
const records = this.storage.get(origin.hostname)?.records
if (!records) {
return null
}
const family = records[ipFamily]
if (!family) {
return null
}
if (family.offset == null || family.offset === maxInt) {
family.offset = 0
} else {
family.offset++
}
const position = family.offset % family.ips.length
const ip = family.ips[position] ?? null
if (ip == null) {
return ip
}
if (Date.now() - ip.timestamp > ip.ttl) { // record TTL is already in ms
// We delete expired records
// It is possible that they have different TTL, so we manage them individually
family.ips.splice(position, 1)
}
return ip
}
setRecords (origin, addresses) {
const timestamp = Date.now()
const records = { records: { 4: null, 6: null } }
let minTTL = this.#maxTTL
for (const record of addresses) {
record.timestamp = timestamp
if (typeof record.ttl === 'number') {
// The record TTL is expected to be in ms
record.ttl = Math.min(record.ttl, this.#maxTTL)
minTTL = Math.min(minTTL, record.ttl)
} else {
record.ttl = this.#maxTTL
}
const familyRecords = records.records[record.family] ?? { ips: [] }
familyRecords.ips.push(record)
records.records[record.family] = familyRecords
}
// We provide a default TTL if external storage will be used without TTL per record-level support
this.storage.set(origin.hostname, records, { ttl: minTTL })
}
deleteRecords (origin) {
this.storage.delete(origin.hostname)
}
getHandler (meta, opts) {
return new DNSDispatchHandler(this, meta, opts)
}
}
class DNSDispatchHandler extends DecoratorHandler {
#state = null
#opts = null
#dispatch = null
#origin = null
#controller = null
#newOrigin = null
#firstTry = true
constructor (state, { origin, handler, dispatch, newOrigin }, opts) {
super(handler)
this.#origin = origin
this.#newOrigin = newOrigin
this.#opts = { ...opts }
this.#state = state
this.#dispatch = dispatch
}
onResponseError (controller, err) {
switch (err.code) {
case 'ETIMEDOUT':
case 'ECONNREFUSED': {
if (this.#state.dualStack) {
if (!this.#firstTry) {
super.onResponseError(controller, err)
return
}
this.#firstTry = false
// Pick an ip address from the other family
const otherFamily = this.#newOrigin.hostname[0] === '[' ? 4 : 6
const ip = this.#state.pickFamily(this.#origin, otherFamily)
if (ip == null) {
super.onResponseError(controller, err)
return
}
let port
if (typeof ip.port === 'number') {
port = `:${ip.port}`
} else if (this.#origin.port !== '') {
port = `:${this.#origin.port}`
} else {
port = ''
}
const dispatchOpts = {
...this.#opts,
origin: `${this.#origin.protocol}//${
ip.family === 6 ? `[${ip.address}]` : ip.address
}${port}`,
headers: withHostHeader(this.#origin.host, this.#opts.headers)
}
this.#dispatch(dispatchOpts, this)
return
}
// if dual-stack disabled, we error out
super.onResponseError(controller, err)
break
}
case 'ENOTFOUND':
this.#state.deleteRecords(this.#origin)
super.onResponseError(controller, err)
break
default:
super.onResponseError(controller, err)
break
}
}
}
module.exports = interceptorOpts => {
if (
interceptorOpts?.maxTTL != null &&
(typeof interceptorOpts?.maxTTL !== 'number' || interceptorOpts?.maxTTL < 0)
) {
throw new InvalidArgumentError('Invalid maxTTL. Must be a positive number')
}
if (
interceptorOpts?.maxItems != null &&
(typeof interceptorOpts?.maxItems !== 'number' ||
interceptorOpts?.maxItems < 1)
) {
throw new InvalidArgumentError(
'Invalid maxItems. Must be a positive number and greater than zero'
)
}
if (
interceptorOpts?.affinity != null &&
interceptorOpts?.affinity !== 4 &&
interceptorOpts?.affinity !== 6
) {
throw new InvalidArgumentError('Invalid affinity. Must be either 4 or 6')
}
if (
interceptorOpts?.dualStack != null &&
typeof interceptorOpts?.dualStack !== 'boolean'
) {
throw new InvalidArgumentError('Invalid dualStack. Must be a boolean')
}
if (
interceptorOpts?.lookup != null &&
typeof interceptorOpts?.lookup !== 'function'
) {
throw new InvalidArgumentError('Invalid lookup. Must be a function')
}
if (
interceptorOpts?.pick != null &&
typeof interceptorOpts?.pick !== 'function'
) {
throw new InvalidArgumentError('Invalid pick. Must be a function')
}
if (
interceptorOpts?.storage != null &&
(typeof interceptorOpts?.storage?.get !== 'function' ||
typeof interceptorOpts?.storage?.set !== 'function' ||
typeof interceptorOpts?.storage?.full !== 'function' ||
typeof interceptorOpts?.storage?.delete !== 'function'
)
) {
throw new InvalidArgumentError('Invalid storage. Must be a object with methods: { get, set, full, delete }')
}
const dualStack = interceptorOpts?.dualStack ?? true
let affinity
if (dualStack) {
affinity = interceptorOpts?.affinity ?? null
} else {
affinity = interceptorOpts?.affinity ?? 4
}
const opts = {
maxTTL: interceptorOpts?.maxTTL ?? 10e3, // Expressed in ms
lookup: interceptorOpts?.lookup ?? null,
pick: interceptorOpts?.pick ?? null,
dualStack,
affinity,
maxItems: interceptorOpts?.maxItems ?? Infinity,
storage: interceptorOpts?.storage
}
const instance = new DNSInstance(opts)
return dispatch => {
return function dnsInterceptor (origDispatchOpts, handler) {
const origin =
origDispatchOpts.origin.constructor === URL
? origDispatchOpts.origin
: new URL(origDispatchOpts.origin)
if (isIP(origin.hostname) !== 0) {
return dispatch(origDispatchOpts, handler)
}
instance.runLookup(origin, origDispatchOpts, (err, newOrigin) => {
if (err) {
return handler.onResponseError(null, err)
}
const dispatchOpts = {
...origDispatchOpts,
servername: origin.hostname, // For SNI on TLS
origin: newOrigin.origin,
headers: withHostHeader(origin.host, origDispatchOpts.headers)
}
dispatch(
dispatchOpts,
instance.getHandler(
{ origin, dispatch, handler, newOrigin },
origDispatchOpts
)
)
})
return true
}
}
}
================================================
FILE: lib/interceptor/dump.js
================================================
'use strict'
const { InvalidArgumentError, RequestAbortedError } = require('../core/errors')
const DecoratorHandler = require('../handler/decorator-handler')
class DumpHandler extends DecoratorHandler {
#maxSize = 1024 * 1024
#dumped = false
#size = 0
#controller = null
aborted = false
reason = false
constructor ({ maxSize, signal }, handler) {
if (maxSize != null && (!Number.isFinite(maxSize) || maxSize < 1)) {
throw new InvalidArgumentError('maxSize must be a number greater than 0')
}
super(handler)
this.#maxSize = maxSize ?? this.#maxSize
// this.#handler = handler
}
#abort (reason) {
this.aborted = true
this.reason = reason
}
onRequestStart (controller, context) {
controller.abort = this.#abort.bind(this)
this.#controller = controller
return super.onRequestStart(controller, context)
}
onResponseStart (controller, statusCode, headers, statusMessage) {
const contentLength = headers['content-length']
if (contentLength != null && contentLength > this.#maxSize) {
throw new RequestAbortedError(
`Response size (${contentLength}) larger than maxSize (${
this.#maxSize
})`
)
}
if (this.aborted === true) {
return true
}
return super.onResponseStart(controller, statusCode, headers, statusMessage)
}
onResponseError (controller, err) {
if (this.#dumped) {
return
}
// On network errors before connect, controller will be null
err = this.#controller?.reason ?? err
super.onResponseError(controller, err)
}
onResponseData (controller, chunk) {
this.#size = this.#size + chunk.length
if (this.#size >= this.#maxSize) {
this.#dumped = true
if (this.aborted === true) {
super.onResponseError(controller, this.reason)
} else {
super.onResponseEnd(controller, {})
}
}
return true
}
onResponseEnd (controller, trailers) {
if (this.#dumped) {
return
}
if (this.#controller.aborted === true) {
super.onResponseError(controller, this.reason)
return
}
super.onResponseEnd(controller, trailers)
}
}
function createDumpInterceptor (
{ maxSize: defaultMaxSize } = {
maxSize: 1024 * 1024
}
) {
return dispatch => {
return function Intercept (opts, handler) {
const { dumpMaxSize = defaultMaxSize } = opts
const dumpHandler = new DumpHandler({ maxSize: dumpMaxSize, signal: opts.signal }, handler)
return dispatch(opts, dumpHandler)
}
}
}
module.exports = createDumpInterceptor
================================================
FILE: lib/interceptor/redirect.js
================================================
'use strict'
const RedirectHandler = require('../handler/redirect-handler')
function createRedirectInterceptor ({ maxRedirections: defaultMaxRedirections } = {}) {
return (dispatch) => {
return function Intercept (opts, handler) {
const { maxRedirections = defaultMaxRedirections, ...rest } = opts
if (maxRedirections == null || maxRedirections === 0) {
return dispatch(opts, handler)
}
const dispatchOpts = { ...rest } // Stop sub dispatcher from also redirecting.
const redirectHandler = new RedirectHandler(dispatch, maxRedirections, dispatchOpts, handler)
return dispatch(dispatchOpts, redirectHandler)
}
}
}
module.exports = createRedirectInterceptor
================================================
FILE: lib/interceptor/response-error.js
================================================
'use strict'
// const { parseHeaders } = require('../core/util')
const DecoratorHandler = require('../handler/decorator-handler')
const { ResponseError } = require('../core/errors')
class ResponseErrorHandler extends DecoratorHandler {
#statusCode
#contentType
#decoder
#headers
#body
constructor (_opts, { handler }) {
super(handler)
}
#checkContentType (contentType) {
return (this.#contentType ?? '').indexOf(contentType) === 0
}
onRequestStart (controller, context) {
this.#statusCode = 0
this.#contentType = null
this.#decoder = null
this.#headers = null
this.#body = ''
return super.onRequestStart(controller, context)
}
onResponseStart (controller, statusCode, headers, statusMessage) {
this.#statusCode = statusCode
this.#headers = headers
this.#contentType = headers['content-type']
if (this.#statusCode < 400) {
return super.onResponseStart(controller, statusCode, headers, statusMessage)
}
if (this.#checkContentType('application/json') || this.#checkContentType('text/plain')) {
this.#decoder = new TextDecoder('utf-8')
}
}
onResponseData (controller, chunk) {
if (this.#statusCode < 400) {
return super.onResponseData(controller, chunk)
}
this.#body += this.#decoder?.decode(chunk, { stream: true }) ?? ''
}
onResponseEnd (controller, trailers) {
if (this.#statusCode >= 400) {
this.#body += this.#decoder?.decode(undefined, { stream: false }) ?? ''
if (this.#checkContentType('application/json')) {
try {
this.#body = JSON.parse(this.#body)
} catch {
// Do nothing...
}
}
let err
const stackTraceLimit = Error.stackTraceLimit
Error.stackTraceLimit = 0
try {
err = new ResponseError('Response Error', this.#statusCode, {
body: this.#body,
headers: this.#headers
})
} finally {
Error.stackTraceLimit = stackTraceLimit
}
super.onResponseError(controller, err)
} else {
super.onResponseEnd(controller, trailers)
}
}
onResponseError (controller, err) {
super.onResponseError(controller, err)
}
}
module.exports = () => {
return (dispatch) => {
return function Intercept (opts, handler) {
return dispatch(opts, new ResponseErrorHandler(opts, { handler }))
}
}
}
================================================
FILE: lib/interceptor/retry.js
================================================
'use strict'
const RetryHandler = require('../handler/retry-handler')
module.exports = globalOpts => {
return dispatch => {
return function retryInterceptor (opts, handler) {
return dispatch(
opts,
new RetryHandler(
{ ...opts, retryOptions: { ...globalOpts, ...opts.retryOptions } },
{
handler,
dispatch
}
)
)
}
}
}
================================================
FILE: lib/llhttp/.gitkeep
================================================
================================================
FILE: lib/llhttp/constants.d.ts
================================================
export type IntDict = Record;
export declare const ERROR: IntDict;
export declare const TYPE: IntDict;
export declare const FLAGS: IntDict;
export declare const LENIENT_FLAGS: IntDict;
export declare const METHODS: IntDict;
export declare const STATUSES: IntDict;
export declare const FINISH: IntDict;
export declare const HEADER_STATE: IntDict;
export declare const METHODS_HTTP: number[];
export declare const METHODS_ICE: number[];
export declare const METHODS_RTSP: number[];
export declare const METHOD_MAP: IntDict;
export declare const H_METHOD_MAP: {
[k: string]: number;
};
export declare const STATUSES_HTTP: number[];
export type CharList = (string | number)[];
export declare const ALPHA: CharList;
export declare const NUM_MAP: {
0: number;
1: number;
2: number;
3: number;
4: number;
5: number;
6: number;
7: number;
8: number;
9: number;
};
export declare const HEX_MAP: {
0: number;
1: number;
2: number;
3: number;
4: number;
5: number;
6: number;
7: number;
8: number;
9: number;
A: number;
B: number;
C: number;
D: number;
E: number;
F: number;
a: number;
b: number;
c: number;
d: number;
e: number;
f: number;
};
export declare const NUM: CharList;
export declare const ALPHANUM: CharList;
export declare const MARK: CharList;
export declare const USERINFO_CHARS: CharList;
export declare const URL_CHAR: CharList;
export declare const HEX: CharList;
export declare const TOKEN: CharList;
export declare const HEADER_CHARS: CharList;
export declare const CONNECTION_TOKEN_CHARS: CharList;
export declare const QUOTED_STRING: CharList;
export declare const HTAB_SP_VCHAR_OBS_TEXT: CharList;
export declare const MAJOR: {
0: number;
1: number;
2: number;
3: number;
4: number;
5: number;
6: number;
7: number;
8: number;
9: number;
};
export declare const MINOR: {
0: number;
1: number;
2: number;
3: number;
4: number;
5: number;
6: number;
7: number;
8: number;
9: number;
};
export declare const SPECIAL_HEADERS: {
connection: number;
'content-length': number;
'proxy-connection': number;
'transfer-encoding': number;
upgrade: number;
};
declare const _default: {
ERROR: IntDict;
TYPE: IntDict;
FLAGS: IntDict;
LENIENT_FLAGS: IntDict;
METHODS: IntDict;
STATUSES: IntDict;
FINISH: IntDict;
HEADER_STATE: IntDict;
ALPHA: CharList;
NUM_MAP: {
0: number;
1: number;
2: number;
3: number;
4: number;
5: number;
6: number;
7: number;
8: number;
9: number;
};
HEX_MAP: {
0: number;
1: number;
2: number;
3: number;
4: number;
5: number;
6: number;
7: number;
8: number;
9: number;
A: number;
B: number;
C: number;
D: number;
E: number;
F: number;
a: number;
b: number;
c: number;
d: number;
e: number;
f: number;
};
NUM: CharList;
ALPHANUM: CharList;
MARK: CharList;
USERINFO_CHARS: CharList;
URL_CHAR: CharList;
HEX: CharList;
TOKEN: CharList;
HEADER_CHARS: CharList;
CONNECTION_TOKEN_CHARS: CharList;
QUOTED_STRING: CharList;
HTAB_SP_VCHAR_OBS_TEXT: CharList;
MAJOR: {
0: number;
1: number;
2: number;
3: number;
4: number;
5: number;
6: number;
7: number;
8: number;
9: number;
};
MINOR: {
0: number;
1: number;
2: number;
3: number;
4: number;
5: number;
6: number;
7: number;
8: number;
9: number;
};
SPECIAL_HEADERS: {
connection: number;
'content-length': number;
'proxy-connection': number;
'transfer-encoding': number;
upgrade: number;
};
METHODS_HTTP: number[];
METHODS_ICE: number[];
METHODS_RTSP: number[];
METHOD_MAP: IntDict;
H_METHOD_MAP: {
[k: string]: number;
};
STATUSES_HTTP: number[];
};
export default _default;
================================================
FILE: lib/llhttp/constants.js
================================================
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.SPECIAL_HEADERS = exports.MINOR = exports.MAJOR = exports.HTAB_SP_VCHAR_OBS_TEXT = exports.QUOTED_STRING = exports.CONNECTION_TOKEN_CHARS = exports.HEADER_CHARS = exports.TOKEN = exports.HEX = exports.URL_CHAR = exports.USERINFO_CHARS = exports.MARK = exports.ALPHANUM = exports.NUM = exports.HEX_MAP = exports.NUM_MAP = exports.ALPHA = exports.STATUSES_HTTP = exports.H_METHOD_MAP = exports.METHOD_MAP = exports.METHODS_RTSP = exports.METHODS_ICE = exports.METHODS_HTTP = exports.HEADER_STATE = exports.FINISH = exports.STATUSES = exports.METHODS = exports.LENIENT_FLAGS = exports.FLAGS = exports.TYPE = exports.ERROR = void 0;
const utils_1 = require("./utils");
// Emums
exports.ERROR = {
OK: 0,
INTERNAL: 1,
STRICT: 2,
CR_EXPECTED: 25,
LF_EXPECTED: 3,
UNEXPECTED_CONTENT_LENGTH: 4,
UNEXPECTED_SPACE: 30,
CLOSED_CONNECTION: 5,
INVALID_METHOD: 6,
INVALID_URL: 7,
INVALID_CONSTANT: 8,
INVALID_VERSION: 9,
INVALID_HEADER_TOKEN: 10,
INVALID_CONTENT_LENGTH: 11,
INVALID_CHUNK_SIZE: 12,
INVALID_STATUS: 13,
INVALID_EOF_STATE: 14,
INVALID_TRANSFER_ENCODING: 15,
CB_MESSAGE_BEGIN: 16,
CB_HEADERS_COMPLETE: 17,
CB_MESSAGE_COMPLETE: 18,
CB_CHUNK_HEADER: 19,
CB_CHUNK_COMPLETE: 20,
PAUSED: 21,
PAUSED_UPGRADE: 22,
PAUSED_H2_UPGRADE: 23,
USER: 24,
CB_URL_COMPLETE: 26,
CB_STATUS_COMPLETE: 27,
CB_METHOD_COMPLETE: 32,
CB_VERSION_COMPLETE: 33,
CB_HEADER_FIELD_COMPLETE: 28,
CB_HEADER_VALUE_COMPLETE: 29,
CB_CHUNK_EXTENSION_NAME_COMPLETE: 34,
CB_CHUNK_EXTENSION_VALUE_COMPLETE: 35,
CB_RESET: 31,
CB_PROTOCOL_COMPLETE: 38,
};
exports.TYPE = {
BOTH: 0, // default
REQUEST: 1,
RESPONSE: 2,
};
exports.FLAGS = {
CONNECTION_KEEP_ALIVE: 1 << 0,
CONNECTION_CLOSE: 1 << 1,
CONNECTION_UPGRADE: 1 << 2,
CHUNKED: 1 << 3,
UPGRADE: 1 << 4,
CONTENT_LENGTH: 1 << 5,
SKIPBODY: 1 << 6,
TRAILING: 1 << 7,
// 1 << 8 is unused
TRANSFER_ENCODING: 1 << 9,
};
exports.LENIENT_FLAGS = {
HEADERS: 1 << 0,
CHUNKED_LENGTH: 1 << 1,
KEEP_ALIVE: 1 << 2,
TRANSFER_ENCODING: 1 << 3,
VERSION: 1 << 4,
DATA_AFTER_CLOSE: 1 << 5,
OPTIONAL_LF_AFTER_CR: 1 << 6,
OPTIONAL_CRLF_AFTER_CHUNK: 1 << 7,
OPTIONAL_CR_BEFORE_LF: 1 << 8,
SPACES_AFTER_CHUNK_SIZE: 1 << 9,
};
exports.METHODS = {
'DELETE': 0,
'GET': 1,
'HEAD': 2,
'POST': 3,
'PUT': 4,
/* pathological */
'CONNECT': 5,
'OPTIONS': 6,
'TRACE': 7,
/* WebDAV */
'COPY': 8,
'LOCK': 9,
'MKCOL': 10,
'MOVE': 11,
'PROPFIND': 12,
'PROPPATCH': 13,
'SEARCH': 14,
'UNLOCK': 15,
'BIND': 16,
'REBIND': 17,
'UNBIND': 18,
'ACL': 19,
/* subversion */
'REPORT': 20,
'MKACTIVITY': 21,
'CHECKOUT': 22,
'MERGE': 23,
/* upnp */
'M-SEARCH': 24,
'NOTIFY': 25,
'SUBSCRIBE': 26,
'UNSUBSCRIBE': 27,
/* RFC-5789 */
'PATCH': 28,
'PURGE': 29,
/* CalDAV */
'MKCALENDAR': 30,
/* RFC-2068, section 19.6.1.2 */
'LINK': 31,
'UNLINK': 32,
/* icecast */
'SOURCE': 33,
/* RFC-7540, section 11.6 */
'PRI': 34,
/* RFC-2326 RTSP */
'DESCRIBE': 35,
'ANNOUNCE': 36,
'SETUP': 37,
'PLAY': 38,
'PAUSE': 39,
'TEARDOWN': 40,
'GET_PARAMETER': 41,
'SET_PARAMETER': 42,
'REDIRECT': 43,
'RECORD': 44,
/* RAOP */
'FLUSH': 45,
/* DRAFT https://www.ietf.org/archive/id/draft-ietf-httpbis-safe-method-w-body-02.html */
'QUERY': 46,
};
exports.STATUSES = {
CONTINUE: 100,
SWITCHING_PROTOCOLS: 101,
PROCESSING: 102,
EARLY_HINTS: 103,
RESPONSE_IS_STALE: 110, // Unofficial
REVALIDATION_FAILED: 111, // Unofficial
DISCONNECTED_OPERATION: 112, // Unofficial
HEURISTIC_EXPIRATION: 113, // Unofficial
MISCELLANEOUS_WARNING: 199, // Unofficial
OK: 200,
CREATED: 201,
ACCEPTED: 202,
NON_AUTHORITATIVE_INFORMATION: 203,
NO_CONTENT: 204,
RESET_CONTENT: 205,
PARTIAL_CONTENT: 206,
MULTI_STATUS: 207,
ALREADY_REPORTED: 208,
TRANSFORMATION_APPLIED: 214, // Unofficial
IM_USED: 226,
MISCELLANEOUS_PERSISTENT_WARNING: 299, // Unofficial
MULTIPLE_CHOICES: 300,
MOVED_PERMANENTLY: 301,
FOUND: 302,
SEE_OTHER: 303,
NOT_MODIFIED: 304,
USE_PROXY: 305,
SWITCH_PROXY: 306, // No longer used
TEMPORARY_REDIRECT: 307,
PERMANENT_REDIRECT: 308,
BAD_REQUEST: 400,
UNAUTHORIZED: 401,
PAYMENT_REQUIRED: 402,
FORBIDDEN: 403,
NOT_FOUND: 404,
METHOD_NOT_ALLOWED: 405,
NOT_ACCEPTABLE: 406,
PROXY_AUTHENTICATION_REQUIRED: 407,
REQUEST_TIMEOUT: 408,
CONFLICT: 409,
GONE: 410,
LENGTH_REQUIRED: 411,
PRECONDITION_FAILED: 412,
PAYLOAD_TOO_LARGE: 413,
URI_TOO_LONG: 414,
UNSUPPORTED_MEDIA_TYPE: 415,
RANGE_NOT_SATISFIABLE: 416,
EXPECTATION_FAILED: 417,
IM_A_TEAPOT: 418,
PAGE_EXPIRED: 419, // Unofficial
ENHANCE_YOUR_CALM: 420, // Unofficial
MISDIRECTED_REQUEST: 421,
UNPROCESSABLE_ENTITY: 422,
LOCKED: 423,
FAILED_DEPENDENCY: 424,
TOO_EARLY: 425,
UPGRADE_REQUIRED: 426,
PRECONDITION_REQUIRED: 428,
TOO_MANY_REQUESTS: 429,
REQUEST_HEADER_FIELDS_TOO_LARGE_UNOFFICIAL: 430, // Unofficial
REQUEST_HEADER_FIELDS_TOO_LARGE: 431,
LOGIN_TIMEOUT: 440, // Unofficial
NO_RESPONSE: 444, // Unofficial
RETRY_WITH: 449, // Unofficial
BLOCKED_BY_PARENTAL_CONTROL: 450, // Unofficial
UNAVAILABLE_FOR_LEGAL_REASONS: 451,
CLIENT_CLOSED_LOAD_BALANCED_REQUEST: 460, // Unofficial
INVALID_X_FORWARDED_FOR: 463, // Unofficial
REQUEST_HEADER_TOO_LARGE: 494, // Unofficial
SSL_CERTIFICATE_ERROR: 495, // Unofficial
SSL_CERTIFICATE_REQUIRED: 496, // Unofficial
HTTP_REQUEST_SENT_TO_HTTPS_PORT: 497, // Unofficial
INVALID_TOKEN: 498, // Unofficial
CLIENT_CLOSED_REQUEST: 499, // Unofficial
INTERNAL_SERVER_ERROR: 500,
NOT_IMPLEMENTED: 501,
BAD_GATEWAY: 502,
SERVICE_UNAVAILABLE: 503,
GATEWAY_TIMEOUT: 504,
HTTP_VERSION_NOT_SUPPORTED: 505,
VARIANT_ALSO_NEGOTIATES: 506,
INSUFFICIENT_STORAGE: 507,
LOOP_DETECTED: 508,
BANDWIDTH_LIMIT_EXCEEDED: 509,
NOT_EXTENDED: 510,
NETWORK_AUTHENTICATION_REQUIRED: 511,
WEB_SERVER_UNKNOWN_ERROR: 520, // Unofficial
WEB_SERVER_IS_DOWN: 521, // Unofficial
CONNECTION_TIMEOUT: 522, // Unofficial
ORIGIN_IS_UNREACHABLE: 523, // Unofficial
TIMEOUT_OCCURED: 524, // Unofficial
SSL_HANDSHAKE_FAILED: 525, // Unofficial
INVALID_SSL_CERTIFICATE: 526, // Unofficial
RAILGUN_ERROR: 527, // Unofficial
SITE_IS_OVERLOADED: 529, // Unofficial
SITE_IS_FROZEN: 530, // Unofficial
IDENTITY_PROVIDER_AUTHENTICATION_ERROR: 561, // Unofficial
NETWORK_READ_TIMEOUT: 598, // Unofficial
NETWORK_CONNECT_TIMEOUT: 599, // Unofficial
};
exports.FINISH = {
SAFE: 0,
SAFE_WITH_CB: 1,
UNSAFE: 2,
};
exports.HEADER_STATE = {
GENERAL: 0,
CONNECTION: 1,
CONTENT_LENGTH: 2,
TRANSFER_ENCODING: 3,
UPGRADE: 4,
CONNECTION_KEEP_ALIVE: 5,
CONNECTION_CLOSE: 6,
CONNECTION_UPGRADE: 7,
TRANSFER_ENCODING_CHUNKED: 8,
};
// C headers
exports.METHODS_HTTP = [
exports.METHODS.DELETE,
exports.METHODS.GET,
exports.METHODS.HEAD,
exports.METHODS.POST,
exports.METHODS.PUT,
exports.METHODS.CONNECT,
exports.METHODS.OPTIONS,
exports.METHODS.TRACE,
exports.METHODS.COPY,
exports.METHODS.LOCK,
exports.METHODS.MKCOL,
exports.METHODS.MOVE,
exports.METHODS.PROPFIND,
exports.METHODS.PROPPATCH,
exports.METHODS.SEARCH,
exports.METHODS.UNLOCK,
exports.METHODS.BIND,
exports.METHODS.REBIND,
exports.METHODS.UNBIND,
exports.METHODS.ACL,
exports.METHODS.REPORT,
exports.METHODS.MKACTIVITY,
exports.METHODS.CHECKOUT,
exports.METHODS.MERGE,
exports.METHODS['M-SEARCH'],
exports.METHODS.NOTIFY,
exports.METHODS.SUBSCRIBE,
exports.METHODS.UNSUBSCRIBE,
exports.METHODS.PATCH,
exports.METHODS.PURGE,
exports.METHODS.MKCALENDAR,
exports.METHODS.LINK,
exports.METHODS.UNLINK,
exports.METHODS.PRI,
// TODO(indutny): should we allow it with HTTP?
exports.METHODS.SOURCE,
exports.METHODS.QUERY,
];
exports.METHODS_ICE = [
exports.METHODS.SOURCE,
];
exports.METHODS_RTSP = [
exports.METHODS.OPTIONS,
exports.METHODS.DESCRIBE,
exports.METHODS.ANNOUNCE,
exports.METHODS.SETUP,
exports.METHODS.PLAY,
exports.METHODS.PAUSE,
exports.METHODS.TEARDOWN,
exports.METHODS.GET_PARAMETER,
exports.METHODS.SET_PARAMETER,
exports.METHODS.REDIRECT,
exports.METHODS.RECORD,
exports.METHODS.FLUSH,
// For AirPlay
exports.METHODS.GET,
exports.METHODS.POST,
];
exports.METHOD_MAP = (0, utils_1.enumToMap)(exports.METHODS);
exports.H_METHOD_MAP = Object.fromEntries(Object.entries(exports.METHODS).filter(([k]) => k.startsWith('H')));
exports.STATUSES_HTTP = [
exports.STATUSES.CONTINUE,
exports.STATUSES.SWITCHING_PROTOCOLS,
exports.STATUSES.PROCESSING,
exports.STATUSES.EARLY_HINTS,
exports.STATUSES.RESPONSE_IS_STALE,
exports.STATUSES.REVALIDATION_FAILED,
exports.STATUSES.DISCONNECTED_OPERATION,
exports.STATUSES.HEURISTIC_EXPIRATION,
exports.STATUSES.MISCELLANEOUS_WARNING,
exports.STATUSES.OK,
exports.STATUSES.CREATED,
exports.STATUSES.ACCEPTED,
exports.STATUSES.NON_AUTHORITATIVE_INFORMATION,
exports.STATUSES.NO_CONTENT,
exports.STATUSES.RESET_CONTENT,
exports.STATUSES.PARTIAL_CONTENT,
exports.STATUSES.MULTI_STATUS,
exports.STATUSES.ALREADY_REPORTED,
exports.STATUSES.TRANSFORMATION_APPLIED,
exports.STATUSES.IM_USED,
exports.STATUSES.MISCELLANEOUS_PERSISTENT_WARNING,
exports.STATUSES.MULTIPLE_CHOICES,
exports.STATUSES.MOVED_PERMANENTLY,
exports.STATUSES.FOUND,
exports.STATUSES.SEE_OTHER,
exports.STATUSES.NOT_MODIFIED,
exports.STATUSES.USE_PROXY,
exports.STATUSES.SWITCH_PROXY,
exports.STATUSES.TEMPORARY_REDIRECT,
exports.STATUSES.PERMANENT_REDIRECT,
exports.STATUSES.BAD_REQUEST,
exports.STATUSES.UNAUTHORIZED,
exports.STATUSES.PAYMENT_REQUIRED,
exports.STATUSES.FORBIDDEN,
exports.STATUSES.NOT_FOUND,
exports.STATUSES.METHOD_NOT_ALLOWED,
exports.STATUSES.NOT_ACCEPTABLE,
exports.STATUSES.PROXY_AUTHENTICATION_REQUIRED,
exports.STATUSES.REQUEST_TIMEOUT,
exports.STATUSES.CONFLICT,
exports.STATUSES.GONE,
exports.STATUSES.LENGTH_REQUIRED,
exports.STATUSES.PRECONDITION_FAILED,
exports.STATUSES.PAYLOAD_TOO_LARGE,
exports.STATUSES.URI_TOO_LONG,
exports.STATUSES.UNSUPPORTED_MEDIA_TYPE,
exports.STATUSES.RANGE_NOT_SATISFIABLE,
exports.STATUSES.EXPECTATION_FAILED,
exports.STATUSES.IM_A_TEAPOT,
exports.STATUSES.PAGE_EXPIRED,
exports.STATUSES.ENHANCE_YOUR_CALM,
exports.STATUSES.MISDIRECTED_REQUEST,
exports.STATUSES.UNPROCESSABLE_ENTITY,
exports.STATUSES.LOCKED,
exports.STATUSES.FAILED_DEPENDENCY,
exports.STATUSES.TOO_EARLY,
exports.STATUSES.UPGRADE_REQUIRED,
exports.STATUSES.PRECONDITION_REQUIRED,
exports.STATUSES.TOO_MANY_REQUESTS,
exports.STATUSES.REQUEST_HEADER_FIELDS_TOO_LARGE_UNOFFICIAL,
exports.STATUSES.REQUEST_HEADER_FIELDS_TOO_LARGE,
exports.STATUSES.LOGIN_TIMEOUT,
exports.STATUSES.NO_RESPONSE,
exports.STATUSES.RETRY_WITH,
exports.STATUSES.BLOCKED_BY_PARENTAL_CONTROL,
exports.STATUSES.UNAVAILABLE_FOR_LEGAL_REASONS,
exports.STATUSES.CLIENT_CLOSED_LOAD_BALANCED_REQUEST,
exports.STATUSES.INVALID_X_FORWARDED_FOR,
exports.STATUSES.REQUEST_HEADER_TOO_LARGE,
exports.STATUSES.SSL_CERTIFICATE_ERROR,
exports.STATUSES.SSL_CERTIFICATE_REQUIRED,
exports.STATUSES.HTTP_REQUEST_SENT_TO_HTTPS_PORT,
exports.STATUSES.INVALID_TOKEN,
exports.STATUSES.CLIENT_CLOSED_REQUEST,
exports.STATUSES.INTERNAL_SERVER_ERROR,
exports.STATUSES.NOT_IMPLEMENTED,
exports.STATUSES.BAD_GATEWAY,
exports.STATUSES.SERVICE_UNAVAILABLE,
exports.STATUSES.GATEWAY_TIMEOUT,
exports.STATUSES.HTTP_VERSION_NOT_SUPPORTED,
exports.STATUSES.VARIANT_ALSO_NEGOTIATES,
exports.STATUSES.INSUFFICIENT_STORAGE,
exports.STATUSES.LOOP_DETECTED,
exports.STATUSES.BANDWIDTH_LIMIT_EXCEEDED,
exports.STATUSES.NOT_EXTENDED,
exports.STATUSES.NETWORK_AUTHENTICATION_REQUIRED,
exports.STATUSES.WEB_SERVER_UNKNOWN_ERROR,
exports.STATUSES.WEB_SERVER_IS_DOWN,
exports.STATUSES.CONNECTION_TIMEOUT,
exports.STATUSES.ORIGIN_IS_UNREACHABLE,
exports.STATUSES.TIMEOUT_OCCURED,
exports.STATUSES.SSL_HANDSHAKE_FAILED,
exports.STATUSES.INVALID_SSL_CERTIFICATE,
exports.STATUSES.RAILGUN_ERROR,
exports.STATUSES.SITE_IS_OVERLOADED,
exports.STATUSES.SITE_IS_FROZEN,
exports.STATUSES.IDENTITY_PROVIDER_AUTHENTICATION_ERROR,
exports.STATUSES.NETWORK_READ_TIMEOUT,
exports.STATUSES.NETWORK_CONNECT_TIMEOUT,
];
exports.ALPHA = [];
for (let i = 'A'.charCodeAt(0); i <= 'Z'.charCodeAt(0); i++) {
// Upper case
exports.ALPHA.push(String.fromCharCode(i));
// Lower case
exports.ALPHA.push(String.fromCharCode(i + 0x20));
}
exports.NUM_MAP = {
0: 0, 1: 1, 2: 2, 3: 3, 4: 4,
5: 5, 6: 6, 7: 7, 8: 8, 9: 9,
};
exports.HEX_MAP = {
0: 0, 1: 1, 2: 2, 3: 3, 4: 4,
5: 5, 6: 6, 7: 7, 8: 8, 9: 9,
A: 0XA, B: 0XB, C: 0XC, D: 0XD, E: 0XE, F: 0XF,
a: 0xa, b: 0xb, c: 0xc, d: 0xd, e: 0xe, f: 0xf,
};
exports.NUM = [
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
];
exports.ALPHANUM = exports.ALPHA.concat(exports.NUM);
exports.MARK = ['-', '_', '.', '!', '~', '*', '\'', '(', ')'];
exports.USERINFO_CHARS = exports.ALPHANUM
.concat(exports.MARK)
.concat(['%', ';', ':', '&', '=', '+', '$', ',']);
// TODO(indutny): use RFC
exports.URL_CHAR = [
'!', '"', '$', '%', '&', '\'',
'(', ')', '*', '+', ',', '-', '.', '/',
':', ';', '<', '=', '>',
'@', '[', '\\', ']', '^', '_',
'`',
'{', '|', '}', '~',
].concat(exports.ALPHANUM);
exports.HEX = exports.NUM.concat(['a', 'b', 'c', 'd', 'e', 'f', 'A', 'B', 'C', 'D', 'E', 'F']);
/* Tokens as defined by rfc 2616. Also lowercases them.
* token = 1*
* separators = "(" | ")" | "<" | ">" | "@"
* | "," | ";" | ":" | "\" | <">
* | "/" | "[" | "]" | "?" | "="
* | "{" | "}" | SP | HT
*/
exports.TOKEN = [
'!', '#', '$', '%', '&', '\'',
'*', '+', '-', '.',
'^', '_', '`',
'|', '~',
].concat(exports.ALPHANUM);
/*
* Verify that a char is a valid visible (printable) US-ASCII
* character or %x80-FF
*/
exports.HEADER_CHARS = ['\t'];
for (let i = 32; i <= 255; i++) {
if (i !== 127) {
exports.HEADER_CHARS.push(i);
}
}
// ',' = \x44
exports.CONNECTION_TOKEN_CHARS = exports.HEADER_CHARS.filter((c) => c !== 44);
exports.QUOTED_STRING = ['\t', ' '];
for (let i = 0x21; i <= 0xff; i++) {
if (i !== 0x22 && i !== 0x5c) { // All characters in ASCII except \ and "
exports.QUOTED_STRING.push(i);
}
}
exports.HTAB_SP_VCHAR_OBS_TEXT = ['\t', ' '];
// VCHAR: https://tools.ietf.org/html/rfc5234#appendix-B.1
for (let i = 0x21; i <= 0x7E; i++) {
exports.HTAB_SP_VCHAR_OBS_TEXT.push(i);
}
// OBS_TEXT: https://datatracker.ietf.org/doc/html/rfc9110#name-collected-abnf
for (let i = 0x80; i <= 0xff; i++) {
exports.HTAB_SP_VCHAR_OBS_TEXT.push(i);
}
exports.MAJOR = exports.NUM_MAP;
exports.MINOR = exports.MAJOR;
exports.SPECIAL_HEADERS = {
'connection': exports.HEADER_STATE.CONNECTION,
'content-length': exports.HEADER_STATE.CONTENT_LENGTH,
'proxy-connection': exports.HEADER_STATE.CONNECTION,
'transfer-encoding': exports.HEADER_STATE.TRANSFER_ENCODING,
'upgrade': exports.HEADER_STATE.UPGRADE,
};
exports.default = {
ERROR: exports.ERROR,
TYPE: exports.TYPE,
FLAGS: exports.FLAGS,
LENIENT_FLAGS: exports.LENIENT_FLAGS,
METHODS: exports.METHODS,
STATUSES: exports.STATUSES,
FINISH: exports.FINISH,
HEADER_STATE: exports.HEADER_STATE,
ALPHA: exports.ALPHA,
NUM_MAP: exports.NUM_MAP,
HEX_MAP: exports.HEX_MAP,
NUM: exports.NUM,
ALPHANUM: exports.ALPHANUM,
MARK: exports.MARK,
USERINFO_CHARS: exports.USERINFO_CHARS,
URL_CHAR: exports.URL_CHAR,
HEX: exports.HEX,
TOKEN: exports.TOKEN,
HEADER_CHARS: exports.HEADER_CHARS,
CONNECTION_TOKEN_CHARS: exports.CONNECTION_TOKEN_CHARS,
QUOTED_STRING: exports.QUOTED_STRING,
HTAB_SP_VCHAR_OBS_TEXT: exports.HTAB_SP_VCHAR_OBS_TEXT,
MAJOR: exports.MAJOR,
MINOR: exports.MINOR,
SPECIAL_HEADERS: exports.SPECIAL_HEADERS,
METHODS_HTTP: exports.METHODS_HTTP,
METHODS_ICE: exports.METHODS_ICE,
METHODS_RTSP: exports.METHODS_RTSP,
METHOD_MAP: exports.METHOD_MAP,
H_METHOD_MAP: exports.H_METHOD_MAP,
STATUSES_HTTP: exports.STATUSES_HTTP,
};
================================================
FILE: lib/llhttp/llhttp-wasm.js
================================================
'use strict'
const { Buffer } = require('node:buffer')
const wasmBase64 = 'AGFzbQEAAAABJwdgAX8Bf2ADf39/AX9gAn9/AGABfwBgBH9/f38Bf2AAAGADf39/AALLAQgDZW52GHdhc21fb25faGVhZGVyc19jb21wbGV0ZQAEA2VudhV3YXNtX29uX21lc3NhZ2VfYmVnaW4AAANlbnYLd2FzbV9vbl91cmwAAQNlbnYOd2FzbV9vbl9zdGF0dXMAAQNlbnYUd2FzbV9vbl9oZWFkZXJfZmllbGQAAQNlbnYUd2FzbV9vbl9oZWFkZXJfdmFsdWUAAQNlbnYMd2FzbV9vbl9ib2R5AAEDZW52GHdhc21fb25fbWVzc2FnZV9jb21wbGV0ZQAAAzU0BQYAAAMAAAAAAAADAQMAAwMDAAACAAAAAAICAgICAgICAgIBAQEBAQEBAQEBAwAAAwAAAAQFAXABExMFAwEAAgYIAX8BQcDZBAsHxQcoBm1lbW9yeQIAC19pbml0aWFsaXplAAgZX19pbmRpcmVjdF9mdW5jdGlvbl90YWJsZQEAC2xsaHR0cF9pbml0AAkYbGxodHRwX3Nob3VsZF9rZWVwX2FsaXZlADcMbGxodHRwX2FsbG9jAAsGbWFsbG9jADkLbGxodHRwX2ZyZWUADARmcmVlAAwPbGxodHRwX2dldF90eXBlAA0VbGxodHRwX2dldF9odHRwX21ham9yAA4VbGxodHRwX2dldF9odHRwX21pbm9yAA8RbGxodHRwX2dldF9tZXRob2QAEBZsbGh0dHBfZ2V0X3N0YXR1c19jb2RlABESbGxodHRwX2dldF91cGdyYWRlABIMbGxodHRwX3Jlc2V0ABMObGxodHRwX2V4ZWN1dGUAFBRsbGh0dHBfc2V0dGluZ3NfaW5pdAAVDWxsaHR0cF9maW5pc2gAFgxsbGh0dHBfcGF1c2UAFw1sbGh0dHBfcmVzdW1lABgbbGxodHRwX3Jlc3VtZV9hZnRlcl91cGdyYWRlABkQbGxodHRwX2dldF9lcnJubwAaF2xsaHR0cF9nZXRfZXJyb3JfcmVhc29uABsXbGxodHRwX3NldF9lcnJvcl9yZWFzb24AHBRsbGh0dHBfZ2V0X2Vycm9yX3BvcwAdEWxsaHR0cF9lcnJub19uYW1lAB4SbGxodHRwX21ldGhvZF9uYW1lAB8SbGxodHRwX3N0YXR1c19uYW1lACAabGxodHRwX3NldF9sZW5pZW50X2hlYWRlcnMAISFsbGh0dHBfc2V0X2xlbmllbnRfY2h1bmtlZF9sZW5ndGgAIh1sbGh0dHBfc2V0X2xlbmllbnRfa2VlcF9hbGl2ZQAjJGxsaHR0cF9zZXRfbGVuaWVudF90cmFuc2Zlcl9lbmNvZGluZwAkGmxsaHR0cF9zZXRfbGVuaWVudF92ZXJzaW9uACUjbGxodHRwX3NldF9sZW5pZW50X2RhdGFfYWZ0ZXJfY2xvc2UAJidsbGh0dHBfc2V0X2xlbmllbnRfb3B0aW9uYWxfbGZfYWZ0ZXJfY3IAJyxsbGh0dHBfc2V0X2xlbmllbnRfb3B0aW9uYWxfY3JsZl9hZnRlcl9jaHVuawAoKGxsaHR0cF9zZXRfbGVuaWVudF9vcHRpb25hbF9jcl9iZWZvcmVfbGYAKSpsbGh0dHBfc2V0X2xlbmllbnRfc3BhY2VzX2FmdGVyX2NodW5rX3NpemUAKhhsbGh0dHBfbWVzc2FnZV9uZWVkc19lb2YANgkYAQBBAQsSAQIDBAUKBgcyNDMuKy8tLDAxCq/ZAjQWAEHA1QAoAgAEQAALQcDVAEEBNgIACxQAIAAQOCAAIAI2AjggACABOgAoCxQAIAAgAC8BNCAALQAwIAAQNxAACx4BAX9BwAAQOiIBEDggAUGACDYCOCABIAA6ACggAQuPDAEHfwJAIABFDQAgAEEIayIBIABBBGsoAgAiAEF4cSIEaiEFAkAgAEEBcQ0AIABBA3FFDQEgASABKAIAIgBrIgFB1NUAKAIASQ0BIAAgBGohBAJAAkBB2NUAKAIAIAFHBEAgAEH/AU0EQCAAQQN2IQMgASgCCCIAIAEoAgwiAkYEQEHE1QBBxNUAKAIAQX4gA3dxNgIADAULIAIgADYCCCAAIAI2AgwMBAsgASgCGCEGIAEgASgCDCIARwRAIAAgASgCCCICNgIIIAIgADYCDAwDCyABQRRqIgMoAgAiAkUEQCABKAIQIgJFDQIgAUEQaiEDCwNAIAMhByACIgBBFGoiAygCACICDQAgAEEQaiEDIAAoAhAiAg0ACyAHQQA2AgAMAgsgBSgCBCIAQQNxQQNHDQIgBSAAQX5xNgIEQczVACAENgIAIAUgBDYCACABIARBAXI2AgQMAwtBACEACyAGRQ0AAkAgASgCHCICQQJ0QfTXAGoiAygCACABRgRAIAMgADYCACAADQFByNUAQcjVACgCAEF+IAJ3cTYCAAwCCyAGQRBBFCAGKAIQIAFGG2ogADYCACAARQ0BCyAAIAY2AhggASgCECICBEAgACACNgIQIAIgADYCGAsgAUEUaigCACICRQ0AIABBFGogAjYCACACIAA2AhgLIAEgBU8NACAFKAIEIgBBAXFFDQACQAJAAkACQCAAQQJxRQRAQdzVACgCACAFRgRAQdzVACABNgIAQdDVAEHQ1QAoAgAgBGoiADYCACABIABBAXI2AgQgAUHY1QAoAgBHDQZBzNUAQQA2AgBB2NUAQQA2AgAMBgtB2NUAKAIAIAVGBEBB2NUAIAE2AgBBzNUAQczVACgCACAEaiIANgIAIAEgAEEBcjYCBCAAIAFqIAA2AgAMBgsgAEF4cSAEaiEEIABB/wFNBEAgAEEDdiEDIAUoAggiACAFKAIMIgJGBEBBxNUAQcTVACgCAEF+IAN3cTYCAAwFCyACIAA2AgggACACNgIMDAQLIAUoAhghBiAFIAUoAgwiAEcEQEHU1QAoAgAaIAAgBSgCCCICNgIIIAIgADYCDAwDCyAFQRRqIgMoAgAiAkUEQCAFKAIQIgJFDQIgBUEQaiEDCwNAIAMhByACIgBBFGoiAygCACICDQAgAEEQaiEDIAAoAhAiAg0ACyAHQQA2AgAMAgsgBSAAQX5xNgIEIAEgBGogBDYCACABIARBAXI2AgQMAwtBACEACyAGRQ0AAkAgBSgCHCICQQJ0QfTXAGoiAygCACAFRgRAIAMgADYCACAADQFByNUAQcjVACgCAEF+IAJ3cTYCAAwCCyAGQRBBFCAGKAIQIAVGG2ogADYCACAARQ0BCyAAIAY2AhggBSgCECICBEAgACACNgIQIAIgADYCGAsgBUEUaigCACICRQ0AIABBFGogAjYCACACIAA2AhgLIAEgBGogBDYCACABIARBAXI2AgQgAUHY1QAoAgBHDQBBzNUAIAQ2AgAMAQsgBEH/AU0EQCAEQXhxQezVAGohAAJ/QcTVACgCACICQQEgBEEDdnQiA3FFBEBBxNUAIAIgA3I2AgAgAAwBCyAAKAIICyICIAE2AgwgACABNgIIIAEgADYCDCABIAI2AggMAQtBHyECIARB////B00EQCAEQSYgBEEIdmciAGt2QQFxIABBAXRrQT5qIQILIAEgAjYCHCABQgA3AhAgAkECdEH01wBqIQACQEHI1QAoAgAiA0EBIAJ0IgdxRQRAIAAgATYCAEHI1QAgAyAHcjYCACABIAA2AhggASABNgIIIAEgATYCDAwBCyAEQRkgAkEBdmtBACACQR9HG3QhAiAAKAIAIQACQANAIAAiAygCBEF4cSAERg0BIAJBHXYhACACQQF0IQIgAyAAQQRxakEQaiIHKAIAIgANAAsgByABNgIAIAEgAzYCGCABIAE2AgwgASABNgIIDAELIAMoAggiACABNgIMIAMgATYCCCABQQA2AhggASADNgIMIAEgADYCCAtB5NUAQeTVACgCAEEBayIAQX8gABs2AgALCwcAIAAtACgLBwAgAC0AKgsHACAALQArCwcAIAAtACkLBwAgAC8BNAsHACAALQAwC0ABBH8gACgCGCEBIAAvAS4hAiAALQAoIQMgACgCOCEEIAAQOCAAIAQ2AjggACADOgAoIAAgAjsBLiAAIAE2AhgL5YUCAgd/A34gASACaiEEAkAgACIDKAIMIgANACADKAIEBEAgAyABNgIECyMAQRBrIgkkAAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAn8CQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkAgAygCHCICQQJrDvwBAfkBAgMEBQYHCAkKCwwNDg8QERL4ARP3ARQV9gEWF/UBGBkaGxwdHh8g/QH7ASH0ASIjJCUmJygpKivzASwtLi8wMTLyAfEBMzTwAe8BNTY3ODk6Ozw9Pj9AQUJDREVGR0hJSktMTU5P+gFQUVJT7gHtAVTsAVXrAVZXWFla6gFbXF1eX2BhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ent8fX5/gAGBAYIBgwGEAYUBhgGHAYgBiQGKAYsBjAGNAY4BjwGQAZEBkgGTAZQBlQGWAZcBmAGZAZoBmwGcAZ0BngGfAaABoQGiAaMBpAGlAaYBpwGoAakBqgGrAawBrQGuAa8BsAGxAbIBswG0AbUBtgG3AbgBuQG6AbsBvAG9Ab4BvwHAAcEBwgHDAcQBxQHGAccByAHJAcoBywHMAc0BzgHpAegBzwHnAdAB5gHRAdIB0wHUAeUB1QHWAdcB2AHZAdoB2wHcAd0B3gHfAeAB4QHiAeMBAPwBC0EADOMBC0EODOIBC0ENDOEBC0EPDOABC0EQDN8BC0ETDN4BC0EUDN0BC0EVDNwBC0EWDNsBC0EXDNoBC0EYDNkBC0EZDNgBC0EaDNcBC0EbDNYBC0EcDNUBC0EdDNQBC0EeDNMBC0EfDNIBC0EgDNEBC0EhDNABC0EIDM8BC0EiDM4BC0EkDM0BC0EjDMwBC0EHDMsBC0ElDMoBC0EmDMkBC0EnDMgBC0EoDMcBC0ESDMYBC0ERDMUBC0EpDMQBC0EqDMMBC0ErDMIBC0EsDMEBC0HeAQzAAQtBLgy/AQtBLwy+AQtBMAy9AQtBMQy8AQtBMgy7AQtBMwy6AQtBNAy5AQtB3wEMuAELQTUMtwELQTkMtgELQQwMtQELQTYMtAELQTcMswELQTgMsgELQT4MsQELQToMsAELQeABDK8BC0ELDK4BC0E/DK0BC0E7DKwBC0EKDKsBC0E8DKoBC0E9DKkBC0HhAQyoAQtBwQAMpwELQcAADKYBC0HCAAylAQtBCQykAQtBLQyjAQtBwwAMogELQcQADKEBC0HFAAygAQtBxgAMnwELQccADJ4BC0HIAAydAQtByQAMnAELQcoADJsBC0HLAAyaAQtBzAAMmQELQc0ADJgBC0HOAAyXAQtBzwAMlgELQdAADJUBC0HRAAyUAQtB0gAMkwELQdMADJIBC0HVAAyRAQtB1AAMkAELQdYADI8BC0HXAAyOAQtB2AAMjQELQdkADIwBC0HaAAyLAQtB2wAMigELQdwADIkBC0HdAAyIAQtB3gAMhwELQd8ADIYBC0HgAAyFAQtB4QAMhAELQeIADIMBC0HjAAyCAQtB5AAMgQELQeUADIABC0HiAQx/C0HmAAx+C0HnAAx9C0EGDHwLQegADHsLQQUMegtB6QAMeQtBBAx4C0HqAAx3C0HrAAx2C0HsAAx1C0HtAAx0C0EDDHMLQe4ADHILQe8ADHELQfAADHALQfIADG8LQfEADG4LQfMADG0LQfQADGwLQfUADGsLQfYADGoLQQIMaQtB9wAMaAtB+AAMZwtB+QAMZgtB+gAMZQtB+wAMZAtB/AAMYwtB/QAMYgtB/gAMYQtB/wAMYAtBgAEMXwtBgQEMXgtBggEMXQtBgwEMXAtBhAEMWwtBhQEMWgtBhgEMWQtBhwEMWAtBiAEMVwtBiQEMVgtBigEMVQtBiwEMVAtBjAEMUwtBjQEMUgtBjgEMUQtBjwEMUAtBkAEMTwtBkQEMTgtBkgEMTQtBkwEMTAtBlAEMSwtBlQEMSgtBlgEMSQtBlwEMSAtBmAEMRwtBmQEMRgtBmgEMRQtBmwEMRAtBnAEMQwtBnQEMQgtBngEMQQtBnwEMQAtBoAEMPwtBoQEMPgtBogEMPQtBowEMPAtBpAEMOwtBpQEMOgtBpgEMOQtBpwEMOAtBqAEMNwtBqQEMNgtBqgEMNQtBqwEMNAtBrAEMMwtBrQEMMgtBrgEMMQtBrwEMMAtBsAEMLwtBsQEMLgtBsgEMLQtBswEMLAtBtAEMKwtBtQEMKgtBtgEMKQtBtwEMKAtBuAEMJwtBuQEMJgtBugEMJQtBuwEMJAtBvAEMIwtBvQEMIgtBvgEMIQtBvwEMIAtBwAEMHwtBwQEMHgtBwgEMHQtBAQwcC0HDAQwbC0HEAQwaC0HFAQwZC0HGAQwYC0HHAQwXC0HIAQwWC0HJAQwVC0HKAQwUC0HLAQwTC0HMAQwSC0HNAQwRC0HOAQwQC0HPAQwPC0HQAQwOC0HRAQwNC0HSAQwMC0HTAQwLC0HUAQwKC0HVAQwJC0HWAQwIC0HjAQwHC0HXAQwGC0HYAQwFC0HZAQwEC0HaAQwDC0HbAQwCC0HdAQwBC0HcAQshAgNAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQCADAn8CQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAn8CQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAn8CQAJAAkACQAJAAkACQAJ/AkACQAJAAn8CQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAIAMCfwJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACfwJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkAgAg7jAQABAgMEBQYHCAkKCwwNDg8QERITFBUWFxgZGhscHR4fICEjJCUnKCmeA5sDmgORA4oDgwOAA/0C+wL4AvIC8QLvAu0C6ALnAuYC5QLkAtwC2wLaAtkC2ALXAtYC1QLPAs4CzALLAsoCyQLIAscCxgLEAsMCvgK8AroCuQK4ArcCtgK1ArQCswKyArECsAKuAq0CqQKoAqcCpgKlAqQCowKiAqECoAKfApgCkAKMAosCigKBAv4B/QH8AfsB+gH5AfgB9wH1AfMB8AHrAekB6AHnAeYB5QHkAeMB4gHhAeAB3wHeAd0B3AHaAdkB2AHXAdYB1QHUAdMB0gHRAdABzwHOAc0BzAHLAcoByQHIAccBxgHFAcQBwwHCAcEBwAG/Ab4BvQG8AbsBugG5AbgBtwG2AbUBtAGzAbIBsQGwAa8BrgGtAawBqwGqAakBqAGnAaYBpQGkAaMBogGfAZ4BmQGYAZcBlgGVAZQBkwGSAZEBkAGPAY0BjAGHAYYBhQGEAYMBggF9fHt6eXZ1dFBRUlNUVQsgASAERw1yQf0BIQIMvgMLIAEgBEcNmAFB2wEhAgy9AwsgASAERw3xAUGOASECDLwDCyABIARHDfwBQYQBIQIMuwMLIAEgBEcNigJB/wAhAgy6AwsgASAERw2RAkH9ACECDLkDCyABIARHDZQCQfsAIQIMuAMLIAEgBEcNHkEeIQIMtwMLIAEgBEcNGUEYIQIMtgMLIAEgBEcNygJBzQAhAgy1AwsgASAERw3VAkHGACECDLQDCyABIARHDdYCQcMAIQIMswMLIAEgBEcN3AJBOCECDLIDCyADLQAwQQFGDa0DDIkDC0EAIQACQAJAAkAgAy0AKkUNACADLQArRQ0AIAMvATIiAkECcUUNAQwCCyADLwEyIgJBAXFFDQELQQEhACADLQAoQQFGDQAgAy8BNCIGQeQAa0HkAEkNACAGQcwBRg0AIAZBsAJGDQAgAkHAAHENAEEAIQAgAkGIBHFBgARGDQAgAkEocUEARyEACyADQQA7ATIgA0EAOgAxAkAgAEUEQCADQQA6ADEgAy0ALkEEcQ0BDLEDCyADQgA3AyALIANBADoAMSADQQE6ADYMSAtBACEAAkAgAygCOCICRQ0AIAIoAjAiAkUNACADIAIRAAAhAAsgAEUNSCAAQRVHDWIgA0EENgIcIAMgATYCFCADQdIbNgIQIANBFTYCDEEAIQIMrwMLIAEgBEYEQEEGIQIMrwMLIAEtAABBCkcNGSABQQFqIQEMGgsgA0IANwMgQRIhAgyUAwsgASAERw2KA0EjIQIMrAMLIAEgBEYEQEEHIQIMrAMLAkACQCABLQAAQQprDgQBGBgAGAsgAUEBaiEBQRAhAgyTAwsgAUEBaiEBIANBL2otAABBAXENF0EAIQIgA0EANgIcIAMgATYCFCADQZkgNgIQIANBGTYCDAyrAwsgAyADKQMgIgwgBCABa60iCn0iC0IAIAsgDFgbNwMgIAogDFoNGEEIIQIMqgMLIAEgBEcEQCADQQk2AgggAyABNgIEQRQhAgyRAwtBCSECDKkDCyADKQMgUA2uAgxDCyABIARGBEBBCyECDKgDCyABLQAAQQpHDRYgAUEBaiEBDBcLIANBL2otAABBAXFFDRkMJgtBACEAAkAgAygCOCICRQ0AIAIoAlAiAkUNACADIAIRAAAhAAsgAA0ZDEILQQAhAAJAIAMoAjgiAkUNACACKAJQIgJFDQAgAyACEQAAIQALIAANGgwkC0EAIQACQCADKAI4IgJFDQAgAigCUCICRQ0AIAMgAhEAACEACyAADRsMMgsgA0Evai0AAEEBcUUNHAwiC0EAIQACQCADKAI4IgJFDQAgAigCVCICRQ0AIAMgAhEAACEACyAADRwMQgtBACEAAkAgAygCOCICRQ0AIAIoAlQiAkUNACADIAIRAAAhAAsgAA0dDCALIAEgBEYEQEETIQIMoAMLAkAgAS0AACIAQQprDgQfIyMAIgsgAUEBaiEBDB8LQQAhAAJAIAMoAjgiAkUNACACKAJUIgJFDQAgAyACEQAAIQALIAANIgxCCyABIARGBEBBFiECDJ4DCyABLQAAQcDBAGotAABBAUcNIwyDAwsCQANAIAEtAABBsDtqLQAAIgBBAUcEQAJAIABBAmsOAgMAJwsgAUEBaiEBQSEhAgyGAwsgBCABQQFqIgFHDQALQRghAgydAwsgAygCBCEAQQAhAiADQQA2AgQgAyAAIAFBAWoiARA0IgANIQxBC0EAIQACQCADKAI4IgJFDQAgAigCVCICRQ0AIAMgAhEAACEACyAADSMMKgsgASAERgRAQRwhAgybAwsgA0EKNgIIIAMgATYCBEEAIQACQCADKAI4IgJFDQAgAigCUCICRQ0AIAMgAhEAACEACyAADSVBJCECDIEDCyABIARHBEADQCABLQAAQbA9ai0AACIAQQNHBEAgAEEBaw4FGBomggMlJgsgBCABQQFqIgFHDQALQRshAgyaAwtBGyECDJkDCwNAIAEtAABBsD9qLQAAIgBBA0cEQCAAQQFrDgUPEScTJicLIAQgAUEBaiIBRw0AC0EeIQIMmAMLIAEgBEcEQCADQQs2AgggAyABNgIEQQchAgz/AgtBHyECDJcDCyABIARGBEBBICECDJcDCwJAIAEtAABBDWsOFC4/Pz8/Pz8/Pz8/Pz8/Pz8/Pz8APwtBACECIANBADYCHCADQb8LNgIQIANBAjYCDCADIAFBAWo2AhQMlgMLIANBL2ohAgNAIAEgBEYEQEEhIQIMlwMLAkACQAJAIAEtAAAiAEEJaw4YAgApKQEpKSkpKSkpKSkpKSkpKSkpKSkCJwsgAUEBaiEBIANBL2otAABBAXFFDQoMGAsgAUEBaiEBDBcLIAFBAWohASACLQAAQQJxDQALQQAhAiADQQA2AhwgAyABNgIUIANBnxU2AhAgA0EMNgIMDJUDCyADLQAuQYABcUUNAQtBACEAAkAgAygCOCICRQ0AIAIoAlwiAkUNACADIAIRAAAhAAsgAEUN5gIgAEEVRgRAIANBJDYCHCADIAE2AhQgA0GbGzYCECADQRU2AgxBACECDJQDC0EAIQIgA0EANgIcIAMgATYCFCADQZAONgIQIANBFDYCDAyTAwtBACECIANBADYCHCADIAE2AhQgA0G+IDYCECADQQI2AgwMkgMLIAMoAgQhAEEAIQIgA0EANgIEIAMgACABIAynaiIBEDIiAEUNKyADQQc2AhwgAyABNgIUIAMgADYCDAyRAwsgAy0ALkHAAHFFDQELQQAhAAJAIAMoAjgiAkUNACACKAJYIgJFDQAgAyACEQAAIQALIABFDSsgAEEVRgRAIANBCjYCHCADIAE2AhQgA0HrGTYCECADQRU2AgxBACECDJADC0EAIQIgA0EANgIcIAMgATYCFCADQZMMNgIQIANBEzYCDAyPAwtBACECIANBADYCHCADIAE2AhQgA0GCFTYCECADQQI2AgwMjgMLQQAhAiADQQA2AhwgAyABNgIUIANB3RQ2AhAgA0EZNgIMDI0DC0EAIQIgA0EANgIcIAMgATYCFCADQeYdNgIQIANBGTYCDAyMAwsgAEEVRg09QQAhAiADQQA2AhwgAyABNgIUIANB0A82AhAgA0EiNgIMDIsDCyADKAIEIQBBACECIANBADYCBCADIAAgARAzIgBFDSggA0ENNgIcIAMgATYCFCADIAA2AgwMigMLIABBFUYNOkEAIQIgA0EANgIcIAMgATYCFCADQdAPNgIQIANBIjYCDAyJAwsgAygCBCEAQQAhAiADQQA2AgQgAyAAIAEQMyIARQRAIAFBAWohAQwoCyADQQ42AhwgAyAANgIMIAMgAUEBajYCFAyIAwsgAEEVRg03QQAhAiADQQA2AhwgAyABNgIUIANB0A82AhAgA0EiNgIMDIcDCyADKAIEIQBBACECIANBADYCBCADIAAgARAzIgBFBEAgAUEBaiEBDCcLIANBDzYCHCADIAA2AgwgAyABQQFqNgIUDIYDC0EAIQIgA0EANgIcIAMgATYCFCADQeIXNgIQIANBGTYCDAyFAwsgAEEVRg0zQQAhAiADQQA2AhwgAyABNgIUIANB1gw2AhAgA0EjNgIMDIQDCyADKAIEIQBBACECIANBADYCBCADIAAgARA0IgBFDSUgA0ERNgIcIAMgATYCFCADIAA2AgwMgwMLIABBFUYNMEEAIQIgA0EANgIcIAMgATYCFCADQdYMNgIQIANBIzYCDAyCAwsgAygCBCEAQQAhAiADQQA2AgQgAyAAIAEQNCIARQRAIAFBAWohAQwlCyADQRI2AhwgAyAANgIMIAMgAUEBajYCFAyBAwsgA0Evai0AAEEBcUUNAQtBFyECDOYCC0EAIQIgA0EANgIcIAMgATYCFCADQeIXNgIQIANBGTYCDAz+AgsgAEE7Rw0AIAFBAWohAQwMC0EAIQIgA0EANgIcIAMgATYCFCADQZIYNgIQIANBAjYCDAz8AgsgAEEVRg0oQQAhAiADQQA2AhwgAyABNgIUIANB1gw2AhAgA0EjNgIMDPsCCyADQRQ2AhwgAyABNgIUIAMgADYCDAz6AgsgAygCBCEAQQAhAiADQQA2AgQgAyAAIAEQNCIARQRAIAFBAWohAQz1AgsgA0EVNgIcIAMgADYCDCADIAFBAWo2AhQM+QILIAMoAgQhAEEAIQIgA0EANgIEIAMgACABEDQiAEUEQCABQQFqIQEM8wILIANBFzYCHCADIAA2AgwgAyABQQFqNgIUDPgCCyAAQRVGDSNBACECIANBADYCHCADIAE2AhQgA0HWDDYCECADQSM2AgwM9wILIAMoAgQhAEEAIQIgA0EANgIEIAMgACABEDQiAEUEQCABQQFqIQEMHQsgA0EZNgIcIAMgADYCDCADIAFBAWo2AhQM9gILIAMoAgQhAEEAIQIgA0EANgIEIAMgACABEDQiAEUEQCABQQFqIQEM7wILIANBGjYCHCADIAA2AgwgAyABQQFqNgIUDPUCCyAAQRVGDR9BACECIANBADYCHCADIAE2AhQgA0HQDzYCECADQSI2AgwM9AILIAMoAgQhACADQQA2AgQgAyAAIAEQMyIARQRAIAFBAWohAQwbCyADQRw2AhwgAyAANgIMIAMgAUEBajYCFEEAIQIM8wILIAMoAgQhACADQQA2AgQgAyAAIAEQMyIARQRAIAFBAWohAQzrAgsgA0EdNgIcIAMgADYCDCADIAFBAWo2AhRBACECDPICCyAAQTtHDQEgAUEBaiEBC0EmIQIM1wILQQAhAiADQQA2AhwgAyABNgIUIANBnxU2AhAgA0EMNgIMDO8CCyABIARHBEADQCABLQAAQSBHDYQCIAQgAUEBaiIBRw0AC0EsIQIM7wILQSwhAgzuAgsgASAERgRAQTQhAgzuAgsCQAJAA0ACQCABLQAAQQprDgQCAAADAAsgBCABQQFqIgFHDQALQTQhAgzvAgsgAygCBCEAIANBADYCBCADIAAgARAxIgBFDZ8CIANBMjYCHCADIAE2AhQgAyAANgIMQQAhAgzuAgsgAygCBCEAIANBADYCBCADIAAgARAxIgBFBEAgAUEBaiEBDJ8CCyADQTI2AhwgAyAANgIMIAMgAUEBajYCFEEAIQIM7QILIAEgBEcEQAJAA0AgAS0AAEEwayIAQf8BcUEKTwRAQTohAgzXAgsgAykDICILQpmz5syZs+bMGVYNASADIAtCCn4iCjcDICAKIACtQv8BgyILQn+FVg0BIAMgCiALfDcDICAEIAFBAWoiAUcNAAtBwAAhAgzuAgsgAygCBCEAIANBADYCBCADIAAgAUEBaiIBEDEiAA0XDOICC0HAACECDOwCCyABIARGBEBByQAhAgzsAgsCQANAAkAgAS0AAEEJaw4YAAKiAqICqQKiAqICogKiAqICogKiAqICogKiAqICogKiAqICogKiAqICogIAogILIAQgAUEBaiIBRw0AC0HJACECDOwCCyABQQFqIQEgA0Evai0AAEEBcQ2lAiADQQA2AhwgAyABNgIUIANBlxA2AhAgA0EKNgIMQQAhAgzrAgsgASAERwRAA0AgAS0AAEEgRw0VIAQgAUEBaiIBRw0AC0H4ACECDOsCC0H4ACECDOoCCyADQQI6ACgMOAtBACECIANBADYCHCADQb8LNgIQIANBAjYCDCADIAFBAWo2AhQM6AILQQAhAgzOAgtBDSECDM0CC0ETIQIMzAILQRUhAgzLAgtBFiECDMoCC0EYIQIMyQILQRkhAgzIAgtBGiECDMcCC0EbIQIMxgILQRwhAgzFAgtBHSECDMQCC0EeIQIMwwILQR8hAgzCAgtBICECDMECC0EiIQIMwAILQSMhAgy/AgtBJSECDL4CC0HlACECDL0CCyADQT02AhwgAyABNgIUIAMgADYCDEEAIQIM1QILIANBGzYCHCADIAE2AhQgA0GkHDYCECADQRU2AgxBACECDNQCCyADQSA2AhwgAyABNgIUIANBmBo2AhAgA0EVNgIMQQAhAgzTAgsgA0ETNgIcIAMgATYCFCADQZgaNgIQIANBFTYCDEEAIQIM0gILIANBCzYCHCADIAE2AhQgA0GYGjYCECADQRU2AgxBACECDNECCyADQRA2AhwgAyABNgIUIANBmBo2AhAgA0EVNgIMQQAhAgzQAgsgA0EgNgIcIAMgATYCFCADQaQcNgIQIANBFTYCDEEAIQIMzwILIANBCzYCHCADIAE2AhQgA0GkHDYCECADQRU2AgxBACECDM4CCyADQQw2AhwgAyABNgIUIANBpBw2AhAgA0EVNgIMQQAhAgzNAgtBACECIANBADYCHCADIAE2AhQgA0HdDjYCECADQRI2AgwMzAILAkADQAJAIAEtAABBCmsOBAACAgACCyAEIAFBAWoiAUcNAAtB/QEhAgzMAgsCQAJAIAMtADZBAUcNAEEAIQACQCADKAI4IgJFDQAgAigCYCICRQ0AIAMgAhEAACEACyAARQ0AIABBFUcNASADQfwBNgIcIAMgATYCFCADQdwZNgIQIANBFTYCDEEAIQIMzQILQdwBIQIMswILIANBADYCHCADIAE2AhQgA0H5CzYCECADQR82AgxBACECDMsCCwJAAkAgAy0AKEEBaw4CBAEAC0HbASECDLICC0HUASECDLECCyADQQI6ADFBACEAAkAgAygCOCICRQ0AIAIoAgAiAkUNACADIAIRAAAhAAsgAEUEQEHdASECDLECCyAAQRVHBEAgA0EANgIcIAMgATYCFCADQbQMNgIQIANBEDYCDEEAIQIMygILIANB+wE2AhwgAyABNgIUIANBgRo2AhAgA0EVNgIMQQAhAgzJAgsgASAERgRAQfoBIQIMyQILIAEtAABByABGDQEgA0EBOgAoC0HAASECDK4CC0HaASECDK0CCyABIARHBEAgA0EMNgIIIAMgATYCBEHZASECDK0CC0H5ASECDMUCCyABIARGBEBB+AEhAgzFAgsgAS0AAEHIAEcNBCABQQFqIQFB2AEhAgyrAgsgASAERgRAQfcBIQIMxAILAkACQCABLQAAQcUAaw4QAAUFBQUFBQUFBQUFBQUFAQULIAFBAWohAUHWASECDKsCCyABQQFqIQFB1wEhAgyqAgtB9gEhAiABIARGDcICIAMoAgAiACAEIAFraiEFIAEgAGtBAmohBgJAA0AgAS0AACAAQbrVAGotAABHDQMgAEECRg0BIABBAWohACAEIAFBAWoiAUcNAAsgAyAFNgIADMMCCyADKAIEIQAgA0IANwMAIAMgACAGQQFqIgEQLiIARQRAQeMBIQIMqgILIANB9QE2AhwgAyABNgIUIAMgADYCDEEAIQIMwgILQfQBIQIgASAERg3BAiADKAIAIgAgBCABa2ohBSABIABrQQFqIQYCQANAIAEtAAAgAEG41QBqLQAARw0CIABBAUYNASAAQQFqIQAgBCABQQFqIgFHDQALIAMgBTYCAAzCAgsgA0GBBDsBKCADKAIEIQAgA0IANwMAIAMgACAGQQFqIgEQLiIADQMMAgsgA0EANgIAC0EAIQIgA0EANgIcIAMgATYCFCADQeUfNgIQIANBCDYCDAy/AgtB1QEhAgylAgsgA0HzATYCHCADIAE2AhQgAyAANgIMQQAhAgy9AgtBACEAAkAgAygCOCICRQ0AIAIoAkAiAkUNACADIAIRAAAhAAsgAEUNbiAAQRVHBEAgA0EANgIcIAMgATYCFCADQYIPNgIQIANBIDYCDEEAIQIMvQILIANBjwE2AhwgAyABNgIUIANB7Bs2AhAgA0EVNgIMQQAhAgy8AgsgASAERwRAIANBDTYCCCADIAE2AgRB0wEhAgyjAgtB8gEhAgy7AgsgASAERgRAQfEBIQIMuwILAkACQAJAIAEtAABByABrDgsAAQgICAgICAgIAggLIAFBAWohAUHQASECDKMCCyABQQFqIQFB0QEhAgyiAgsgAUEBaiEBQdIBIQIMoQILQfABIQIgASAERg25AiADKAIAIgAgBCABa2ohBiABIABrQQJqIQUDQCABLQAAIABBtdUAai0AAEcNBCAAQQJGDQMgAEEBaiEAIAQgAUEBaiIBRw0ACyADIAY2AgAMuQILQe8BIQIgASAERg24AiADKAIAIgAgBCABa2ohBiABIABrQQFqIQUDQCABLQAAIABBs9UAai0AAEcNAyAAQQFGDQIgAEEBaiEAIAQgAUEBaiIBRw0ACyADIAY2AgAMuAILQe4BIQIgASAERg23AiADKAIAIgAgBCABa2ohBiABIABrQQJqIQUDQCABLQAAIABBsNUAai0AAEcNAiAAQQJGDQEgAEEBaiEAIAQgAUEBaiIBRw0ACyADIAY2AgAMtwILIAMoAgQhACADQgA3AwAgAyAAIAVBAWoiARArIgBFDQIgA0HsATYCHCADIAE2AhQgAyAANgIMQQAhAgy2AgsgA0EANgIACyADKAIEIQAgA0EANgIEIAMgACABECsiAEUNnAIgA0HtATYCHCADIAE2AhQgAyAANgIMQQAhAgy0AgtBzwEhAgyaAgtBACEAAkAgAygCOCICRQ0AIAIoAjQiAkUNACADIAIRAAAhAAsCQCAABEAgAEEVRg0BIANBADYCHCADIAE2AhQgA0HqDTYCECADQSY2AgxBACECDLQCC0HOASECDJoCCyADQesBNgIcIAMgATYCFCADQYAbNgIQIANBFTYCDEEAIQIMsgILIAEgBEYEQEHrASECDLICCyABLQAAQS9GBEAgAUEBaiEBDAELIANBADYCHCADIAE2AhQgA0GyODYCECADQQg2AgxBACECDLECC0HNASECDJcCCyABIARHBEAgA0EONgIIIAMgATYCBEHMASECDJcCC0HqASECDK8CCyABIARGBEBB6QEhAgyvAgsgAS0AAEEwayIAQf8BcUEKSQRAIAMgADoAKiABQQFqIQFBywEhAgyWAgsgAygCBCEAIANBADYCBCADIAAgARAvIgBFDZcCIANB6AE2AhwgAyABNgIUIAMgADYCDEEAIQIMrgILIAEgBEYEQEHnASECDK4CCwJAIAEtAABBLkYEQCABQQFqIQEMAQsgAygCBCEAIANBADYCBCADIAAgARAvIgBFDZgCIANB5gE2AhwgAyABNgIUIAMgADYCDEEAIQIMrgILQcoBIQIMlAILIAEgBEYEQEHlASECDK0CC0EAIQBBASEFQQEhB0EAIQICQAJAAkACQAJAAn8CQAJAAkACQAJAAkACQCABLQAAQTBrDgoKCQABAgMEBQYICwtBAgwGC0EDDAULQQQMBAtBBQwDC0EGDAILQQcMAQtBCAshAkEAIQVBACEHDAILQQkhAkEBIQBBACEFQQAhBwwBC0EAIQVBASECCyADIAI6ACsgAUEBaiEBAkACQCADLQAuQRBxDQACQAJAAkAgAy0AKg4DAQACBAsgB0UNAwwCCyAADQEMAgsgBUUNAQsgAygCBCEAIANBADYCBCADIAAgARAvIgBFDQIgA0HiATYCHCADIAE2AhQgAyAANgIMQQAhAgyvAgsgAygCBCEAIANBADYCBCADIAAgARAvIgBFDZoCIANB4wE2AhwgAyABNgIUIAMgADYCDEEAIQIMrgILIAMoAgQhACADQQA2AgQgAyAAIAEQLyIARQ2YAiADQeQBNgIcIAMgATYCFCADIAA2AgwMrQILQckBIQIMkwILQQAhAAJAIAMoAjgiAkUNACACKAJEIgJFDQAgAyACEQAAIQALAkAgAARAIABBFUYNASADQQA2AhwgAyABNgIUIANBpA02AhAgA0EhNgIMQQAhAgytAgtByAEhAgyTAgsgA0HhATYCHCADIAE2AhQgA0HQGjYCECADQRU2AgxBACECDKsCCyABIARGBEBB4QEhAgyrAgsCQCABLQAAQSBGBEAgA0EAOwE0IAFBAWohAQwBCyADQQA2AhwgAyABNgIUIANBmRE2AhAgA0EJNgIMQQAhAgyrAgtBxwEhAgyRAgsgASAERgRAQeABIQIMqgILAkAgAS0AAEEwa0H/AXEiAkEKSQRAIAFBAWohAQJAIAMvATQiAEGZM0sNACADIABBCmwiADsBNCAAQf7/A3EgAkH//wNzSw0AIAMgACACajsBNAwCC0EAIQIgA0EANgIcIAMgATYCFCADQZUeNgIQIANBDTYCDAyrAgsgA0EANgIcIAMgATYCFCADQZUeNgIQIANBDTYCDEEAIQIMqgILQcYBIQIMkAILIAEgBEYEQEHfASECDKkCCwJAIAEtAABBMGtB/wFxIgJBCkkEQCABQQFqIQECQCADLwE0IgBBmTNLDQAgAyAAQQpsIgA7ATQgAEH+/wNxIAJB//8Dc0sNACADIAAgAmo7ATQMAgtBACECIANBADYCHCADIAE2AhQgA0GVHjYCECADQQ02AgwMqgILIANBADYCHCADIAE2AhQgA0GVHjYCECADQQ02AgxBACECDKkCC0HFASECDI8CCyABIARGBEBB3gEhAgyoAgsCQCABLQAAQTBrQf8BcSICQQpJBEAgAUEBaiEBAkAgAy8BNCIAQZkzSw0AIAMgAEEKbCIAOwE0IABB/v8DcSACQf//A3NLDQAgAyAAIAJqOwE0DAILQQAhAiADQQA2AhwgAyABNgIUIANBlR42AhAgA0ENNgIMDKkCCyADQQA2AhwgAyABNgIUIANBlR42AhAgA0ENNgIMQQAhAgyoAgtBxAEhAgyOAgsgASAERgRAQd0BIQIMpwILAkACQAJAAkAgAS0AAEEKaw4XAgMDAAMDAwMDAwMDAwMDAwMDAwMDAwEDCyABQQFqDAULIAFBAWohAUHDASECDI8CCyABQQFqIQEgA0Evai0AAEEBcQ0IIANBADYCHCADIAE2AhQgA0GNCzYCECADQQ02AgxBACECDKcCCyADQQA2AhwgAyABNgIUIANBjQs2AhAgA0ENNgIMQQAhAgymAgsgASAERwRAIANBDzYCCCADIAE2AgRBASECDI0CC0HcASECDKUCCwJAAkADQAJAIAEtAABBCmsOBAIAAAMACyAEIAFBAWoiAUcNAAtB2wEhAgymAgsgAygCBCEAIANBADYCBCADIAAgARAtIgBFBEAgAUEBaiEBDAQLIANB2gE2AhwgAyAANgIMIAMgAUEBajYCFEEAIQIMpQILIAMoAgQhACADQQA2AgQgAyAAIAEQLSIADQEgAUEBagshAUHBASECDIoCCyADQdkBNgIcIAMgADYCDCADIAFBAWo2AhRBACECDKICC0HCASECDIgCCyADQS9qLQAAQQFxDQEgA0EANgIcIAMgATYCFCADQeQcNgIQIANBGTYCDEEAIQIMoAILIAEgBEYEQEHZASECDKACCwJAAkACQCABLQAAQQprDgQBAgIAAgsgAUEBaiEBDAILIAFBAWohAQwBCyADLQAuQcAAcUUNAQtBACEAAkAgAygCOCICRQ0AIAIoAjwiAkUNACADIAIRAAAhAAsgAEUNoAEgAEEVRgRAIANB2QA2AhwgAyABNgIUIANBtxo2AhAgA0EVNgIMQQAhAgyfAgsgA0EANgIcIAMgATYCFCADQYANNgIQIANBGzYCDEEAIQIMngILIANBADYCHCADIAE2AhQgA0HcKDYCECADQQI2AgxBACECDJ0CCyABIARHBEAgA0EMNgIIIAMgATYCBEG/ASECDIQCC0HYASECDJwCCyABIARGBEBB1wEhAgycAgsCQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAIAEtAABBwQBrDhUAAQIDWgQFBlpaWgcICQoLDA0ODxBaCyABQQFqIQFB+wAhAgySAgsgAUEBaiEBQfwAIQIMkQILIAFBAWohAUGBASECDJACCyABQQFqIQFBhQEhAgyPAgsgAUEBaiEBQYYBIQIMjgILIAFBAWohAUGJASECDI0CCyABQQFqIQFBigEhAgyMAgsgAUEBaiEBQY0BIQIMiwILIAFBAWohAUGWASECDIoCCyABQQFqIQFBlwEhAgyJAgsgAUEBaiEBQZgBIQIMiAILIAFBAWohAUGlASECDIcCCyABQQFqIQFBpgEhAgyGAgsgAUEBaiEBQawBIQIMhQILIAFBAWohAUG0ASECDIQCCyABQQFqIQFBtwEhAgyDAgsgAUEBaiEBQb4BIQIMggILIAEgBEYEQEHWASECDJsCCyABLQAAQc4ARw1IIAFBAWohAUG9ASECDIECCyABIARGBEBB1QEhAgyaAgsCQAJAAkAgAS0AAEHCAGsOEgBKSkpKSkpKSkoBSkpKSkpKAkoLIAFBAWohAUG4ASECDIICCyABQQFqIQFBuwEhAgyBAgsgAUEBaiEBQbwBIQIMgAILQdQBIQIgASAERg2YAiADKAIAIgAgBCABa2ohBSABIABrQQdqIQYCQANAIAEtAAAgAEGo1QBqLQAARw1FIABBB0YNASAAQQFqIQAgBCABQQFqIgFHDQALIAMgBTYCAAyZAgsgA0EANgIAIAZBAWohAUEbDEULIAEgBEYEQEHTASECDJgCCwJAAkAgAS0AAEHJAGsOBwBHR0dHRwFHCyABQQFqIQFBuQEhAgz/AQsgAUEBaiEBQboBIQIM/gELQdIBIQIgASAERg2WAiADKAIAIgAgBCABa2ohBSABIABrQQFqIQYCQANAIAEtAAAgAEGm1QBqLQAARw1DIABBAUYNASAAQQFqIQAgBCABQQFqIgFHDQALIAMgBTYCAAyXAgsgA0EANgIAIAZBAWohAUEPDEMLQdEBIQIgASAERg2VAiADKAIAIgAgBCABa2ohBSABIABrQQFqIQYCQANAIAEtAAAgAEGk1QBqLQAARw1CIABBAUYNASAAQQFqIQAgBCABQQFqIgFHDQALIAMgBTYCAAyWAgsgA0EANgIAIAZBAWohAUEgDEILQdABIQIgASAERg2UAiADKAIAIgAgBCABa2ohBSABIABrQQJqIQYCQANAIAEtAAAgAEGh1QBqLQAARw1BIABBAkYNASAAQQFqIQAgBCABQQFqIgFHDQALIAMgBTYCAAyVAgsgA0EANgIAIAZBAWohAUESDEELIAEgBEYEQEHPASECDJQCCwJAAkAgAS0AAEHFAGsODgBDQ0NDQ0NDQ0NDQ0MBQwsgAUEBaiEBQbUBIQIM+wELIAFBAWohAUG2ASECDPoBC0HOASECIAEgBEYNkgIgAygCACIAIAQgAWtqIQUgASAAa0ECaiEGAkADQCABLQAAIABBntUAai0AAEcNPyAAQQJGDQEgAEEBaiEAIAQgAUEBaiIBRw0ACyADIAU2AgAMkwILIANBADYCACAGQQFqIQFBBww/C0HNASECIAEgBEYNkQIgAygCACIAIAQgAWtqIQUgASAAa0EFaiEGAkADQCABLQAAIABBmNUAai0AAEcNPiAAQQVGDQEgAEEBaiEAIAQgAUEBaiIBRw0ACyADIAU2AgAMkgILIANBADYCACAGQQFqIQFBKAw+CyABIARGBEBBzAEhAgyRAgsCQAJAAkAgAS0AAEHFAGsOEQBBQUFBQUFBQUEBQUFBQUECQQsgAUEBaiEBQbEBIQIM+QELIAFBAWohAUGyASECDPgBCyABQQFqIQFBswEhAgz3AQtBywEhAiABIARGDY8CIAMoAgAiACAEIAFraiEFIAEgAGtBBmohBgJAA0AgAS0AACAAQZHVAGotAABHDTwgAEEGRg0BIABBAWohACAEIAFBAWoiAUcNAAsgAyAFNgIADJACCyADQQA2AgAgBkEBaiEBQRoMPAtBygEhAiABIARGDY4CIAMoAgAiACAEIAFraiEFIAEgAGtBA2ohBgJAA0AgAS0AACAAQY3VAGotAABHDTsgAEEDRg0BIABBAWohACAEIAFBAWoiAUcNAAsgAyAFNgIADI8CCyADQQA2AgAgBkEBaiEBQSEMOwsgASAERgRAQckBIQIMjgILAkACQCABLQAAQcEAaw4UAD09PT09PT09PT09PT09PT09PQE9CyABQQFqIQFBrQEhAgz1AQsgAUEBaiEBQbABIQIM9AELIAEgBEYEQEHIASECDI0CCwJAAkAgAS0AAEHVAGsOCwA8PDw8PDw8PDwBPAsgAUEBaiEBQa4BIQIM9AELIAFBAWohAUGvASECDPMBC0HHASECIAEgBEYNiwIgAygCACIAIAQgAWtqIQUgASAAa0EIaiEGAkADQCABLQAAIABBhNUAai0AAEcNOCAAQQhGDQEgAEEBaiEAIAQgAUEBaiIBRw0ACyADIAU2AgAMjAILIANBADYCACAGQQFqIQFBKgw4CyABIARGBEBBxgEhAgyLAgsgAS0AAEHQAEcNOCABQQFqIQFBJQw3C0HFASECIAEgBEYNiQIgAygCACIAIAQgAWtqIQUgASAAa0ECaiEGAkADQCABLQAAIABBgdUAai0AAEcNNiAAQQJGDQEgAEEBaiEAIAQgAUEBaiIBRw0ACyADIAU2AgAMigILIANBADYCACAGQQFqIQFBDgw2CyABIARGBEBBxAEhAgyJAgsgAS0AAEHFAEcNNiABQQFqIQFBqwEhAgzvAQsgASAERgRAQcMBIQIMiAILAkACQAJAAkAgAS0AAEHCAGsODwABAjk5OTk5OTk5OTk5AzkLIAFBAWohAUGnASECDPEBCyABQQFqIQFBqAEhAgzwAQsgAUEBaiEBQakBIQIM7wELIAFBAWohAUGqASECDO4BC0HCASECIAEgBEYNhgIgAygCACIAIAQgAWtqIQUgASAAa0ECaiEGAkADQCABLQAAIABB/tQAai0AAEcNMyAAQQJGDQEgAEEBaiEAIAQgAUEBaiIBRw0ACyADIAU2AgAMhwILIANBADYCACAGQQFqIQFBFAwzC0HBASECIAEgBEYNhQIgAygCACIAIAQgAWtqIQUgASAAa0EEaiEGAkADQCABLQAAIABB+dQAai0AAEcNMiAAQQRGDQEgAEEBaiEAIAQgAUEBaiIBRw0ACyADIAU2AgAMhgILIANBADYCACAGQQFqIQFBKwwyC0HAASECIAEgBEYNhAIgAygCACIAIAQgAWtqIQUgASAAa0ECaiEGAkADQCABLQAAIABB9tQAai0AAEcNMSAAQQJGDQEgAEEBaiEAIAQgAUEBaiIBRw0ACyADIAU2AgAMhQILIANBADYCACAGQQFqIQFBLAwxC0G/ASECIAEgBEYNgwIgAygCACIAIAQgAWtqIQUgASAAa0ECaiEGAkADQCABLQAAIABBodUAai0AAEcNMCAAQQJGDQEgAEEBaiEAIAQgAUEBaiIBRw0ACyADIAU2AgAMhAILIANBADYCACAGQQFqIQFBEQwwC0G+ASECIAEgBEYNggIgAygCACIAIAQgAWtqIQUgASAAa0EDaiEGAkADQCABLQAAIABB8tQAai0AAEcNLyAAQQNGDQEgAEEBaiEAIAQgAUEBaiIBRw0ACyADIAU2AgAMgwILIANBADYCACAGQQFqIQFBLgwvCyABIARGBEBBvQEhAgyCAgsCQAJAAkACQAJAIAEtAABBwQBrDhUANDQ0NDQ0NDQ0NAE0NAI0NAM0NAQ0CyABQQFqIQFBmwEhAgzsAQsgAUEBaiEBQZwBIQIM6wELIAFBAWohAUGdASECDOoBCyABQQFqIQFBogEhAgzpAQsgAUEBaiEBQaQBIQIM6AELIAEgBEYEQEG8ASECDIECCwJAAkAgAS0AAEHSAGsOAwAwATALIAFBAWohAUGjASECDOgBCyABQQFqIQFBBAwtC0G7ASECIAEgBEYN/wEgAygCACIAIAQgAWtqIQUgASAAa0EBaiEGAkADQCABLQAAIABB8NQAai0AAEcNLCAAQQFGDQEgAEEBaiEAIAQgAUEBaiIBRw0ACyADIAU2AgAMgAILIANBADYCACAGQQFqIQFBHQwsCyABIARGBEBBugEhAgz/AQsCQAJAIAEtAABByQBrDgcBLi4uLi4ALgsgAUEBaiEBQaEBIQIM5gELIAFBAWohAUEiDCsLIAEgBEYEQEG5ASECDP4BCyABLQAAQdAARw0rIAFBAWohAUGgASECDOQBCyABIARGBEBBuAEhAgz9AQsCQAJAIAEtAABBxgBrDgsALCwsLCwsLCwsASwLIAFBAWohAUGeASECDOQBCyABQQFqIQFBnwEhAgzjAQtBtwEhAiABIARGDfsBIAMoAgAiACAEIAFraiEFIAEgAGtBA2ohBgJAA0AgAS0AACAAQezUAGotAABHDSggAEEDRg0BIABBAWohACAEIAFBAWoiAUcNAAsgAyAFNgIADPwBCyADQQA2AgAgBkEBaiEBQQ0MKAtBtgEhAiABIARGDfoBIAMoAgAiACAEIAFraiEFIAEgAGtBAmohBgJAA0AgAS0AACAAQaHVAGotAABHDScgAEECRg0BIABBAWohACAEIAFBAWoiAUcNAAsgAyAFNgIADPsBCyADQQA2AgAgBkEBaiEBQQwMJwtBtQEhAiABIARGDfkBIAMoAgAiACAEIAFraiEFIAEgAGtBAWohBgJAA0AgAS0AACAAQerUAGotAABHDSYgAEEBRg0BIABBAWohACAEIAFBAWoiAUcNAAsgAyAFNgIADPoBCyADQQA2AgAgBkEBaiEBQQMMJgtBtAEhAiABIARGDfgBIAMoAgAiACAEIAFraiEFIAEgAGtBAWohBgJAA0AgAS0AACAAQejUAGotAABHDSUgAEEBRg0BIABBAWohACAEIAFBAWoiAUcNAAsgAyAFNgIADPkBCyADQQA2AgAgBkEBaiEBQSYMJQsgASAERgRAQbMBIQIM+AELAkACQCABLQAAQdQAaw4CAAEnCyABQQFqIQFBmQEhAgzfAQsgAUEBaiEBQZoBIQIM3gELQbIBIQIgASAERg32ASADKAIAIgAgBCABa2ohBSABIABrQQFqIQYCQANAIAEtAAAgAEHm1ABqLQAARw0jIABBAUYNASAAQQFqIQAgBCABQQFqIgFHDQALIAMgBTYCAAz3AQsgA0EANgIAIAZBAWohAUEnDCMLQbEBIQIgASAERg31ASADKAIAIgAgBCABa2ohBSABIABrQQFqIQYCQANAIAEtAAAgAEHk1ABqLQAARw0iIABBAUYNASAAQQFqIQAgBCABQQFqIgFHDQALIAMgBTYCAAz2AQsgA0EANgIAIAZBAWohAUEcDCILQbABIQIgASAERg30ASADKAIAIgAgBCABa2ohBSABIABrQQVqIQYCQANAIAEtAAAgAEHe1ABqLQAARw0hIABBBUYNASAAQQFqIQAgBCABQQFqIgFHDQALIAMgBTYCAAz1AQsgA0EANgIAIAZBAWohAUEGDCELQa8BIQIgASAERg3zASADKAIAIgAgBCABa2ohBSABIABrQQRqIQYCQANAIAEtAAAgAEHZ1ABqLQAARw0gIABBBEYNASAAQQFqIQAgBCABQQFqIgFHDQALIAMgBTYCAAz0AQsgA0EANgIAIAZBAWohAUEZDCALIAEgBEYEQEGuASECDPMBCwJAAkACQAJAIAEtAABBLWsOIwAkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJAEkJCQkJAIkJCQDJAsgAUEBaiEBQY4BIQIM3AELIAFBAWohAUGPASECDNsBCyABQQFqIQFBlAEhAgzaAQsgAUEBaiEBQZUBIQIM2QELQa0BIQIgASAERg3xASADKAIAIgAgBCABa2ohBSABIABrQQFqIQYCQANAIAEtAAAgAEHX1ABqLQAARw0eIABBAUYNASAAQQFqIQAgBCABQQFqIgFHDQALIAMgBTYCAAzyAQsgA0EANgIAIAZBAWohAUELDB4LIAEgBEYEQEGsASECDPEBCwJAAkAgAS0AAEHBAGsOAwAgASALIAFBAWohAUGQASECDNgBCyABQQFqIQFBkwEhAgzXAQsgASAERgRAQasBIQIM8AELAkACQCABLQAAQcEAaw4PAB8fHx8fHx8fHx8fHx8BHwsgAUEBaiEBQZEBIQIM1wELIAFBAWohAUGSASECDNYBCyABIARGBEBBqgEhAgzvAQsgAS0AAEHMAEcNHCABQQFqIQFBCgwbC0GpASECIAEgBEYN7QEgAygCACIAIAQgAWtqIQUgASAAa0EFaiEGAkADQCABLQAAIABB0dQAai0AAEcNGiAAQQVGDQEgAEEBaiEAIAQgAUEBaiIBRw0ACyADIAU2AgAM7gELIANBADYCACAGQQFqIQFBHgwaC0GoASECIAEgBEYN7AEgAygCACIAIAQgAWtqIQUgASAAa0EGaiEGAkADQCABLQAAIABBytQAai0AAEcNGSAAQQZGDQEgAEEBaiEAIAQgAUEBaiIBRw0ACyADIAU2AgAM7QELIANBADYCACAGQQFqIQFBFQwZC0GnASECIAEgBEYN6wEgAygCACIAIAQgAWtqIQUgASAAa0ECaiEGAkADQCABLQAAIABBx9QAai0AAEcNGCAAQQJGDQEgAEEBaiEAIAQgAUEBaiIBRw0ACyADIAU2AgAM7AELIANBADYCACAGQQFqIQFBFwwYC0GmASECIAEgBEYN6gEgAygCACIAIAQgAWtqIQUgASAAa0EFaiEGAkADQCABLQAAIABBwdQAai0AAEcNFyAAQQVGDQEgAEEBaiEAIAQgAUEBaiIBRw0ACyADIAU2AgAM6wELIANBADYCACAGQQFqIQFBGAwXCyABIARGBEBBpQEhAgzqAQsCQAJAIAEtAABByQBrDgcAGRkZGRkBGQsgAUEBaiEBQYsBIQIM0QELIAFBAWohAUGMASECDNABC0GkASECIAEgBEYN6AEgAygCACIAIAQgAWtqIQUgASAAa0EBaiEGAkADQCABLQAAIABBptUAai0AAEcNFSAAQQFGDQEgAEEBaiEAIAQgAUEBaiIBRw0ACyADIAU2AgAM6QELIANBADYCACAGQQFqIQFBCQwVC0GjASECIAEgBEYN5wEgAygCACIAIAQgAWtqIQUgASAAa0EBaiEGAkADQCABLQAAIABBpNUAai0AAEcNFCAAQQFGDQEgAEEBaiEAIAQgAUEBaiIBRw0ACyADIAU2AgAM6AELIANBADYCACAGQQFqIQFBHwwUC0GiASECIAEgBEYN5gEgAygCACIAIAQgAWtqIQUgASAAa0ECaiEGAkADQCABLQAAIABBvtQAai0AAEcNEyAAQQJGDQEgAEEBaiEAIAQgAUEBaiIBRw0ACyADIAU2AgAM5wELIANBADYCACAGQQFqIQFBAgwTC0GhASECIAEgBEYN5QEgAygCACIAIAQgAWtqIQUgASAAa0EBaiEGA0AgAS0AACAAQbzUAGotAABHDREgAEEBRg0CIABBAWohACAEIAFBAWoiAUcNAAsgAyAFNgIADOUBCyABIARGBEBBoAEhAgzlAQtBASABLQAAQd8ARw0RGiABQQFqIQFBhwEhAgzLAQsgA0EANgIAIAZBAWohAUGIASECDMoBC0GfASECIAEgBEYN4gEgAygCACIAIAQgAWtqIQUgASAAa0EIaiEGAkADQCABLQAAIABBhNUAai0AAEcNDyAAQQhGDQEgAEEBaiEAIAQgAUEBaiIBRw0ACyADIAU2AgAM4wELIANBADYCACAGQQFqIQFBKQwPC0GeASECIAEgBEYN4QEgAygCACIAIAQgAWtqIQUgASAAa0EDaiEGAkADQCABLQAAIABBuNQAai0AAEcNDiAAQQNGDQEgAEEBaiEAIAQgAUEBaiIBRw0ACyADIAU2AgAM4gELIANBADYCACAGQQFqIQFBLQwOCyABIARGBEBBnQEhAgzhAQsgAS0AAEHFAEcNDiABQQFqIQFBhAEhAgzHAQsgASAERgRAQZwBIQIM4AELAkACQCABLQAAQcwAaw4IAA8PDw8PDwEPCyABQQFqIQFBggEhAgzHAQsgAUEBaiEBQYMBIQIMxgELQZsBIQIgASAERg3eASADKAIAIgAgBCABa2ohBSABIABrQQRqIQYCQANAIAEtAAAgAEGz1ABqLQAARw0LIABBBEYNASAAQQFqIQAgBCABQQFqIgFHDQALIAMgBTYCAAzfAQsgA0EANgIAIAZBAWohAUEjDAsLQZoBIQIgASAERg3dASADKAIAIgAgBCABa2ohBSABIABrQQJqIQYCQANAIAEtAAAgAEGw1ABqLQAARw0KIABBAkYNASAAQQFqIQAgBCABQQFqIgFHDQALIAMgBTYCAAzeAQsgA0EANgIAIAZBAWohAUEADAoLIAEgBEYEQEGZASECDN0BCwJAAkAgAS0AAEHIAGsOCAAMDAwMDAwBDAsgAUEBaiEBQf0AIQIMxAELIAFBAWohAUGAASECDMMBCyABIARGBEBBmAEhAgzcAQsCQAJAIAEtAABBzgBrDgMACwELCyABQQFqIQFB/gAhAgzDAQsgAUEBaiEBQf8AIQIMwgELIAEgBEYEQEGXASECDNsBCyABLQAAQdkARw0IIAFBAWohAUEIDAcLQZYBIQIgASAERg3ZASADKAIAIgAgBCABa2ohBSABIABrQQNqIQYCQANAIAEtAAAgAEGs1ABqLQAARw0GIABBA0YNASAAQQFqIQAgBCABQQFqIgFHDQALIAMgBTYCAAzaAQsgA0EANgIAIAZBAWohAUEFDAYLQZUBIQIgASAERg3YASADKAIAIgAgBCABa2ohBSABIABrQQVqIQYCQANAIAEtAAAgAEGm1ABqLQAARw0FIABBBUYNASAAQQFqIQAgBCABQQFqIgFHDQALIAMgBTYCAAzZAQsgA0EANgIAIAZBAWohAUEWDAULQZQBIQIgASAERg3XASADKAIAIgAgBCABa2ohBSABIABrQQJqIQYCQANAIAEtAAAgAEGh1QBqLQAARw0EIABBAkYNASAAQQFqIQAgBCABQQFqIgFHDQALIAMgBTYCAAzYAQsgA0EANgIAIAZBAWohAUEQDAQLIAEgBEYEQEGTASECDNcBCwJAAkAgAS0AAEHDAGsODAAGBgYGBgYGBgYGAQYLIAFBAWohAUH5ACECDL4BCyABQQFqIQFB+gAhAgy9AQtBkgEhAiABIARGDdUBIAMoAgAiACAEIAFraiEFIAEgAGtBBWohBgJAA0AgAS0AACAAQaDUAGotAABHDQIgAEEFRg0BIABBAWohACAEIAFBAWoiAUcNAAsgAyAFNgIADNYBCyADQQA2AgAgBkEBaiEBQSQMAgsgA0EANgIADAILIAEgBEYEQEGRASECDNQBCyABLQAAQcwARw0BIAFBAWohAUETCzoAKSADKAIEIQAgA0EANgIEIAMgACABEC4iAA0CDAELQQAhAiADQQA2AhwgAyABNgIUIANB/h82AhAgA0EGNgIMDNEBC0H4ACECDLcBCyADQZABNgIcIAMgATYCFCADIAA2AgxBACECDM8BC0EAIQACQCADKAI4IgJFDQAgAigCQCICRQ0AIAMgAhEAACEACyAARQ0AIABBFUYNASADQQA2AhwgAyABNgIUIANBgg82AhAgA0EgNgIMQQAhAgzOAQtB9wAhAgy0AQsgA0GPATYCHCADIAE2AhQgA0HsGzYCECADQRU2AgxBACECDMwBCyABIARGBEBBjwEhAgzMAQsCQCABLQAAQSBGBEAgAUEBaiEBDAELIANBADYCHCADIAE2AhQgA0GbHzYCECADQQY2AgxBACECDMwBC0ECIQIMsgELA0AgAS0AAEEgRw0CIAQgAUEBaiIBRw0AC0GOASECDMoBCyABIARGBEBBjQEhAgzKAQsCQCABLQAAQQlrDgRKAABKAAtB9QAhAgywAQsgAy0AKUEFRgRAQfYAIQIMsAELQfQAIQIMrwELIAEgBEYEQEGMASECDMgBCyADQRA2AgggAyABNgIEDAoLIAEgBEYEQEGLASECDMcBCwJAIAEtAABBCWsOBEcAAEcAC0HzACECDK0BCyABIARHBEAgA0EQNgIIIAMgATYCBEHxACECDK0BC0GKASECDMUBCwJAIAEgBEcEQANAIAEtAABBoNAAai0AACIAQQNHBEACQCAAQQFrDgJJAAQLQfAAIQIMrwELIAQgAUEBaiIBRw0AC0GIASECDMYBC0GIASECDMUBCyADQQA2AhwgAyABNgIUIANB2yA2AhAgA0EHNgIMQQAhAgzEAQsgASAERgRAQYkBIQIMxAELAkACQAJAIAEtAABBoNIAai0AAEEBaw4DRgIAAQtB8gAhAgysAQsgA0EANgIcIAMgATYCFCADQbQSNgIQIANBBzYCDEEAIQIMxAELQeoAIQIMqgELIAEgBEcEQCABQQFqIQFB7wAhAgyqAQtBhwEhAgzCAQsgBCABIgBGBEBBhgEhAgzCAQsgAC0AACIBQS9GBEAgAEEBaiEBQe4AIQIMqQELIAFBCWsiAkEXSw0BIAAhAUEBIAJ0QZuAgARxDUEMAQsgBCABIgBGBEBBhQEhAgzBAQsgAC0AAEEvRw0AIABBAWohAQwDC0EAIQIgA0EANgIcIAMgADYCFCADQdsgNgIQIANBBzYCDAy/AQsCQAJAAkACQAJAA0AgAS0AAEGgzgBqLQAAIgBBBUcEQAJAAkAgAEEBaw4IRwUGBwgABAEIC0HrACECDK0BCyABQQFqIQFB7QAhAgysAQsgBCABQQFqIgFHDQALQYQBIQIMwwELIAFBAWoMFAsgAygCBCEAIANBADYCBCADIAAgARAsIgBFDR4gA0HbADYCHCADIAE2AhQgAyAANgIMQQAhAgzBAQsgAygCBCEAIANBADYCBCADIAAgARAsIgBFDR4gA0HdADYCHCADIAE2AhQgAyAANgIMQQAhAgzAAQsgAygCBCEAIANBADYCBCADIAAgARAsIgBFDR4gA0H6ADYCHCADIAE2AhQgAyAANgIMQQAhAgy/AQsgA0EANgIcIAMgATYCFCADQfkPNgIQIANBBzYCDEEAIQIMvgELIAEgBEYEQEGDASECDL4BCwJAIAEtAABBoM4Aai0AAEEBaw4IPgQFBgAIAgMHCyABQQFqIQELQQMhAgyjAQsgAUEBagwNC0EAIQIgA0EANgIcIANB0RI2AhAgA0EHNgIMIAMgAUEBajYCFAy6AQsgAygCBCEAIANBADYCBCADIAAgARAsIgBFDRYgA0HbADYCHCADIAE2AhQgAyAANgIMQQAhAgy5AQsgAygCBCEAIANBADYCBCADIAAgARAsIgBFDRYgA0HdADYCHCADIAE2AhQgAyAANgIMQQAhAgy4AQsgAygCBCEAIANBADYCBCADIAAgARAsIgBFDRYgA0H6ADYCHCADIAE2AhQgAyAANgIMQQAhAgy3AQsgA0EANgIcIAMgATYCFCADQfkPNgIQIANBBzYCDEEAIQIMtgELQewAIQIMnAELIAEgBEYEQEGCASECDLUBCyABQQFqDAILIAEgBEYEQEGBASECDLQBCyABQQFqDAELIAEgBEYNASABQQFqCyEBQQQhAgyYAQtBgAEhAgywAQsDQCABLQAAQaDMAGotAAAiAEECRwRAIABBAUcEQEHpACECDJkBCwwxCyAEIAFBAWoiAUcNAAtB/wAhAgyvAQsgASAERgRAQf4AIQIMrwELAkAgAS0AAEEJaw43LwMGLwQGBgYGBgYGBgYGBgYGBgYGBgYFBgYCBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGAAYLIAFBAWoLIQFBBSECDJQBCyABQQFqDAYLIAMoAgQhACADQQA2AgQgAyAAIAEQLCIARQ0IIANB2wA2AhwgAyABNgIUIAMgADYCDEEAIQIMqwELIAMoAgQhACADQQA2AgQgAyAAIAEQLCIARQ0IIANB3QA2AhwgAyABNgIUIAMgADYCDEEAIQIMqgELIAMoAgQhACADQQA2AgQgAyAAIAEQLCIARQ0IIANB+gA2AhwgAyABNgIUIAMgADYCDEEAIQIMqQELIANBADYCHCADIAE2AhQgA0GNFDYCECADQQc2AgxBACECDKgBCwJAAkACQAJAA0AgAS0AAEGgygBqLQAAIgBBBUcEQAJAIABBAWsOBi4DBAUGAAYLQegAIQIMlAELIAQgAUEBaiIBRw0AC0H9ACECDKsBCyADKAIEIQAgA0EANgIEIAMgACABECwiAEUNByADQdsANgIcIAMgATYCFCADIAA2AgxBACECDKoBCyADKAIEIQAgA0EANgIEIAMgACABECwiAEUNByADQd0ANgIcIAMgATYCFCADIAA2AgxBACECDKkBCyADKAIEIQAgA0EANgIEIAMgACABECwiAEUNByADQfoANgIcIAMgATYCFCADIAA2AgxBACECDKgBCyADQQA2AhwgAyABNgIUIANB5Ag2AhAgA0EHNgIMQQAhAgynAQsgASAERg0BIAFBAWoLIQFBBiECDIwBC0H8ACECDKQBCwJAAkACQAJAA0AgAS0AAEGgyABqLQAAIgBBBUcEQCAAQQFrDgQpAgMEBQsgBCABQQFqIgFHDQALQfsAIQIMpwELIAMoAgQhACADQQA2AgQgAyAAIAEQLCIARQ0DIANB2wA2AhwgAyABNgIUIAMgADYCDEEAIQIMpgELIAMoAgQhACADQQA2AgQgAyAAIAEQLCIARQ0DIANB3QA2AhwgAyABNgIUIAMgADYCDEEAIQIMpQELIAMoAgQhACADQQA2AgQgAyAAIAEQLCIARQ0DIANB+gA2AhwgAyABNgIUIAMgADYCDEEAIQIMpAELIANBADYCHCADIAE2AhQgA0G8CjYCECADQQc2AgxBACECDKMBC0HPACECDIkBC0HRACECDIgBC0HnACECDIcBCyABIARGBEBB+gAhAgygAQsCQCABLQAAQQlrDgQgAAAgAAsgAUEBaiEBQeYAIQIMhgELIAEgBEYEQEH5ACECDJ8BCwJAIAEtAABBCWsOBB8AAB8AC0EAIQACQCADKAI4IgJFDQAgAigCOCICRQ0AIAMgAhEAACEACyAARQRAQeIBIQIMhgELIABBFUcEQCADQQA2AhwgAyABNgIUIANByQ02AhAgA0EaNgIMQQAhAgyfAQsgA0H4ADYCHCADIAE2AhQgA0HqGjYCECADQRU2AgxBACECDJ4BCyABIARHBEAgA0ENNgIIIAMgATYCBEHkACECDIUBC0H3ACECDJ0BCyABIARGBEBB9gAhAgydAQsCQAJAAkAgAS0AAEHIAGsOCwABCwsLCwsLCwsCCwsgAUEBaiEBQd0AIQIMhQELIAFBAWohAUHgACECDIQBCyABQQFqIQFB4wAhAgyDAQtB9QAhAiABIARGDZsBIAMoAgAiACAEIAFraiEFIAEgAGtBAmohBgJAA0AgAS0AACAAQbXVAGotAABHDQggAEECRg0BIABBAWohACAEIAFBAWoiAUcNAAsgAyAFNgIADJwBCyADKAIEIQAgA0IANwMAIAMgACAGQQFqIgEQKyIABEAgA0H0ADYCHCADIAE2AhQgAyAANgIMQQAhAgycAQtB4gAhAgyCAQtBACEAAkAgAygCOCICRQ0AIAIoAjQiAkUNACADIAIRAAAhAAsCQCAABEAgAEEVRg0BIANBADYCHCADIAE2AhQgA0HqDTYCECADQSY2AgxBACECDJwBC0HhACECDIIBCyADQfMANgIcIAMgATYCFCADQYAbNgIQIANBFTYCDEEAIQIMmgELIAMtACkiAEEja0ELSQ0JAkAgAEEGSw0AQQEgAHRBygBxRQ0ADAoLQQAhAiADQQA2AhwgAyABNgIUIANB7Qk2AhAgA0EINgIMDJkBC0HyACECIAEgBEYNmAEgAygCACIAIAQgAWtqIQUgASAAa0EBaiEGAkADQCABLQAAIABBs9UAai0AAEcNBSAAQQFGDQEgAEEBaiEAIAQgAUEBaiIBRw0ACyADIAU2AgAMmQELIAMoAgQhACADQgA3AwAgAyAAIAZBAWoiARArIgAEQCADQfEANgIcIAMgATYCFCADIAA2AgxBACECDJkBC0HfACECDH8LQQAhAAJAIAMoAjgiAkUNACACKAI0IgJFDQAgAyACEQAAIQALAkAgAARAIABBFUYNASADQQA2AhwgAyABNgIUIANB6g02AhAgA0EmNgIMQQAhAgyZAQtB3gAhAgx/CyADQfAANgIcIAMgATYCFCADQYAbNgIQIANBFTYCDEEAIQIMlwELIAMtAClBIUYNBiADQQA2AhwgAyABNgIUIANBkQo2AhAgA0EINgIMQQAhAgyWAQtB7wAhAiABIARGDZUBIAMoAgAiACAEIAFraiEFIAEgAGtBAmohBgJAA0AgAS0AACAAQbDVAGotAABHDQIgAEECRg0BIABBAWohACAEIAFBAWoiAUcNAAsgAyAFNgIADJYBCyADKAIEIQAgA0IANwMAIAMgACAGQQFqIgEQKyIARQ0CIANB7QA2AhwgAyABNgIUIAMgADYCDEEAIQIMlQELIANBADYCAAsgAygCBCEAIANBADYCBCADIAAgARArIgBFDYABIANB7gA2AhwgAyABNgIUIAMgADYCDEEAIQIMkwELQdwAIQIMeQtBACEAAkAgAygCOCICRQ0AIAIoAjQiAkUNACADIAIRAAAhAAsCQCAABEAgAEEVRg0BIANBADYCHCADIAE2AhQgA0HqDTYCECADQSY2AgxBACECDJMBC0HbACECDHkLIANB7AA2AhwgAyABNgIUIANBgBs2AhAgA0EVNgIMQQAhAgyRAQsgAy0AKSIAQSNJDQAgAEEuRg0AIANBADYCHCADIAE2AhQgA0HJCTYCECADQQg2AgxBACECDJABC0HaACECDHYLIAEgBEYEQEHrACECDI8BCwJAIAEtAABBL0YEQCABQQFqIQEMAQsgA0EANgIcIAMgATYCFCADQbI4NgIQIANBCDYCDEEAIQIMjwELQdkAIQIMdQsgASAERwRAIANBDjYCCCADIAE2AgRB2AAhAgx1C0HqACECDI0BCyABIARGBEBB6QAhAgyNAQsgAS0AAEEwayIAQf8BcUEKSQRAIAMgADoAKiABQQFqIQFB1wAhAgx0CyADKAIEIQAgA0EANgIEIAMgACABEC8iAEUNeiADQegANgIcIAMgATYCFCADIAA2AgxBACECDIwBCyABIARGBEBB5wAhAgyMAQsCQCABLQAAQS5GBEAgAUEBaiEBDAELIAMoAgQhACADQQA2AgQgAyAAIAEQLyIARQ17IANB5gA2AhwgAyABNgIUIAMgADYCDEEAIQIMjAELQdYAIQIMcgsgASAERgRAQeUAIQIMiwELQQAhAEEBIQVBASEHQQAhAgJAAkACQAJAAkACfwJAAkACQAJAAkACQAJAIAEtAABBMGsOCgoJAAECAwQFBggLC0ECDAYLQQMMBQtBBAwEC0EFDAMLQQYMAgtBBwwBC0EICyECQQAhBUEAIQcMAgtBCSECQQEhAEEAIQVBACEHDAELQQAhBUEBIQILIAMgAjoAKyABQQFqIQECQAJAIAMtAC5BEHENAAJAAkACQCADLQAqDgMBAAIECyAHRQ0DDAILIAANAQwCCyAFRQ0BCyADKAIEIQAgA0EANgIEIAMgACABEC8iAEUNAiADQeIANgIcIAMgATYCFCADIAA2AgxBACECDI0BCyADKAIEIQAgA0EANgIEIAMgACABEC8iAEUNfSADQeMANgIcIAMgATYCFCADIAA2AgxBACECDIwBCyADKAIEIQAgA0EANgIEIAMgACABEC8iAEUNeyADQeQANgIcIAMgATYCFCADIAA2AgwMiwELQdQAIQIMcQsgAy0AKUEiRg2GAUHTACECDHALQQAhAAJAIAMoAjgiAkUNACACKAJEIgJFDQAgAyACEQAAIQALIABFBEBB1QAhAgxwCyAAQRVHBEAgA0EANgIcIAMgATYCFCADQaQNNgIQIANBITYCDEEAIQIMiQELIANB4QA2AhwgAyABNgIUIANB0Bo2AhAgA0EVNgIMQQAhAgyIAQsgASAERgRAQeAAIQIMiAELAkACQAJAAkACQCABLQAAQQprDgQBBAQABAsgAUEBaiEBDAELIAFBAWohASADQS9qLQAAQQFxRQ0BC0HSACECDHALIANBADYCHCADIAE2AhQgA0G2ETYCECADQQk2AgxBACECDIgBCyADQQA2AhwgAyABNgIUIANBthE2AhAgA0EJNgIMQQAhAgyHAQsgASAERgRAQd8AIQIMhwELIAEtAABBCkYEQCABQQFqIQEMCQsgAy0ALkHAAHENCCADQQA2AhwgAyABNgIUIANBthE2AhAgA0ECNgIMQQAhAgyGAQsgASAERgRAQd0AIQIMhgELIAEtAAAiAkENRgRAIAFBAWohAUHQACECDG0LIAEhACACQQlrDgQFAQEFAQsgBCABIgBGBEBB3AAhAgyFAQsgAC0AAEEKRw0AIABBAWoMAgtBACECIANBADYCHCADIAA2AhQgA0HKLTYCECADQQc2AgwMgwELIAEgBEYEQEHbACECDIMBCwJAIAEtAABBCWsOBAMAAAMACyABQQFqCyEBQc4AIQIMaAsgASAERgRAQdoAIQIMgQELIAEtAABBCWsOBAABAQABC0EAIQIgA0EANgIcIANBmhI2AhAgA0EHNgIMIAMgAUEBajYCFAx/CyADQYASOwEqQQAhAAJAIAMoAjgiAkUNACACKAI4IgJFDQAgAyACEQAAIQALIABFDQAgAEEVRw0BIANB2QA2AhwgAyABNgIUIANB6ho2AhAgA0EVNgIMQQAhAgx+C0HNACECDGQLIANBADYCHCADIAE2AhQgA0HJDTYCECADQRo2AgxBACECDHwLIAEgBEYEQEHZACECDHwLIAEtAABBIEcNPSABQQFqIQEgAy0ALkEBcQ09IANBADYCHCADIAE2AhQgA0HCHDYCECADQR42AgxBACECDHsLIAEgBEYEQEHYACECDHsLAkACQAJAAkACQCABLQAAIgBBCmsOBAIDAwABCyABQQFqIQFBLCECDGULIABBOkcNASADQQA2AhwgAyABNgIUIANB5xE2AhAgA0EKNgIMQQAhAgx9CyABQQFqIQEgA0Evai0AAEEBcUUNcyADLQAyQYABcUUEQCADQTJqIQIgAxA1QQAhAAJAIAMoAjgiBkUNACAGKAIoIgZFDQAgAyAGEQAAIQALAkACQCAADhZNTEsBAQEBAQEBAQEBAQEBAQEBAQEAAQsgA0EpNgIcIAMgATYCFCADQawZNgIQIANBFTYCDEEAIQIMfgsgA0EANgIcIAMgATYCFCADQeULNgIQIANBETYCDEEAIQIMfQtBACEAAkAgAygCOCICRQ0AIAIoAlwiAkUNACADIAIRAAAhAAsgAEUNWSAAQRVHDQEgA0EFNgIcIAMgATYCFCADQZsbNgIQIANBFTYCDEEAIQIMfAtBywAhAgxiC0EAIQIgA0EANgIcIAMgATYCFCADQZAONgIQIANBFDYCDAx6CyADIAMvATJBgAFyOwEyDDsLIAEgBEcEQCADQRE2AgggAyABNgIEQcoAIQIMYAtB1wAhAgx4CyABIARGBEBB1gAhAgx4CwJAAkACQAJAIAEtAAAiAEEgciAAIABBwQBrQf8BcUEaSRtB/wFxQeMAaw4TAEBAQEBAQEBAQEBAQAFAQEACA0ALIAFBAWohAUHGACECDGELIAFBAWohAUHHACECDGALIAFBAWohAUHIACECDF8LIAFBAWohAUHJACECDF4LQdUAIQIgBCABIgBGDXYgBCABayADKAIAIgFqIQYgACABa0EFaiEHA0AgAUGQyABqLQAAIAAtAAAiBUEgciAFIAVBwQBrQf8BcUEaSRtB/wFxRw0IQQQgAUEFRg0KGiABQQFqIQEgBCAAQQFqIgBHDQALIAMgBjYCAAx2C0HUACECIAQgASIARg11IAQgAWsgAygCACIBaiEGIAAgAWtBD2ohBwNAIAFBgMgAai0AACAALQAAIgVBIHIgBSAFQcEAa0H/AXFBGkkbQf8BcUcNB0EDIAFBD0YNCRogAUEBaiEBIAQgAEEBaiIARw0ACyADIAY2AgAMdQtB0wAhAiAEIAEiAEYNdCAEIAFrIAMoAgAiAWohBiAAIAFrQQ5qIQcDQCABQeLHAGotAAAgAC0AACIFQSByIAUgBUHBAGtB/wFxQRpJG0H/AXFHDQYgAUEORg0HIAFBAWohASAEIABBAWoiAEcNAAsgAyAGNgIADHQLQdIAIQIgBCABIgBGDXMgBCABayADKAIAIgFqIQUgACABa0EBaiEGA0AgAUHgxwBqLQAAIAAtAAAiB0EgciAHIAdBwQBrQf8BcUEaSRtB/wFxRw0FIAFBAUYNAiABQQFqIQEgBCAAQQFqIgBHDQALIAMgBTYCAAxzCyABIARGBEBB0QAhAgxzCwJAAkAgAS0AACIAQSByIAAgAEHBAGtB/wFxQRpJG0H/AXFB7gBrDgcAOTk5OTkBOQsgAUEBaiEBQcMAIQIMWgsgAUEBaiEBQcQAIQIMWQsgA0EANgIAIAZBAWohAUHFACECDFgLQdAAIQIgBCABIgBGDXAgBCABayADKAIAIgFqIQYgACABa0EJaiEHA0AgAUHWxwBqLQAAIAAtAAAiBUEgciAFIAVBwQBrQf8BcUEaSRtB/wFxRw0CQQIgAUEJRg0EGiABQQFqIQEgBCAAQQFqIgBHDQALIAMgBjYCAAxwC0HPACECIAQgASIARg1vIAQgAWsgAygCACIBaiEGIAAgAWtBBWohBwNAIAFB0McAai0AACAALQAAIgVBIHIgBSAFQcEAa0H/AXFBGkkbQf8BcUcNASABQQVGDQIgAUEBaiEBIAQgAEEBaiIARw0ACyADIAY2AgAMbwsgACEBIANBADYCAAwzC0EBCzoALCADQQA2AgAgB0EBaiEBC0EtIQIMUgsCQANAIAEtAABB0MUAai0AAEEBRw0BIAQgAUEBaiIBRw0AC0HNACECDGsLQcIAIQIMUQsgASAERgRAQcwAIQIMagsgAS0AAEE6RgRAIAMoAgQhACADQQA2AgQgAyAAIAEQMCIARQ0zIANBywA2AhwgAyAANgIMIAMgAUEBajYCFEEAIQIMagsgA0EANgIcIAMgATYCFCADQecRNgIQIANBCjYCDEEAIQIMaQsCQAJAIAMtACxBAmsOAgABJwsgA0Ezai0AAEECcUUNJiADLQAuQQJxDSYgA0EANgIcIAMgATYCFCADQaYUNgIQIANBCzYCDEEAIQIMaQsgAy0AMkEgcUUNJSADLQAuQQJxDSUgA0EANgIcIAMgATYCFCADQb0TNgIQIANBDzYCDEEAIQIMaAtBACEAAkAgAygCOCICRQ0AIAIoAkgiAkUNACADIAIRAAAhAAsgAEUEQEHBACECDE8LIABBFUcEQCADQQA2AhwgAyABNgIUIANBpg82AhAgA0EcNgIMQQAhAgxoCyADQcoANgIcIAMgATYCFCADQYUcNgIQIANBFTYCDEEAIQIMZwsgASAERwRAA0AgAS0AAEHAwQBqLQAAQQFHDRcgBCABQQFqIgFHDQALQcQAIQIMZwtBxAAhAgxmCyABIARHBEADQAJAIAEtAAAiAEEgciAAIABBwQBrQf8BcUEaSRtB/wFxIgBBCUYNACAAQSBGDQACQAJAAkACQCAAQeMAaw4TAAMDAwMDAwMBAwMDAwMDAwMDAgMLIAFBAWohAUE2IQIMUgsgAUEBaiEBQTchAgxRCyABQQFqIQFBOCECDFALDBULIAQgAUEBaiIBRw0AC0E8IQIMZgtBPCECDGULIAEgBEYEQEHIACECDGULIANBEjYCCCADIAE2AgQCQAJAAkACQAJAIAMtACxBAWsOBBQAAQIJCyADLQAyQSBxDQNB4AEhAgxPCwJAIAMvATIiAEEIcUUNACADLQAoQQFHDQAgAy0ALkEIcUUNAgsgAyAAQff7A3FBgARyOwEyDAsLIAMgAy8BMkEQcjsBMgwECyADQQA2AgQgAyABIAEQMSIABEAgA0HBADYCHCADIAA2AgwgAyABQQFqNgIUQQAhAgxmCyABQQFqIQEMWAsgA0EANgIcIAMgATYCFCADQfQTNgIQIANBBDYCDEEAIQIMZAtBxwAhAiABIARGDWMgAygCACIAIAQgAWtqIQUgASAAa0EGaiEGAkADQCAAQcDFAGotAAAgAS0AAEEgckcNASAAQQZGDUogAEEBaiEAIAQgAUEBaiIBRw0ACyADIAU2AgAMZAsgA0EANgIADAULAkAgASAERwRAA0AgAS0AAEHAwwBqLQAAIgBBAUcEQCAAQQJHDQMgAUEBaiEBDAULIAQgAUEBaiIBRw0AC0HFACECDGQLQcUAIQIMYwsLIANBADoALAwBC0ELIQIMRwtBPyECDEYLAkACQANAIAEtAAAiAEEgRwRAAkAgAEEKaw4EAwUFAwALIABBLEYNAwwECyAEIAFBAWoiAUcNAAtBxgAhAgxgCyADQQg6ACwMDgsgAy0AKEEBRw0CIAMtAC5BCHENAiADKAIEIQAgA0EANgIEIAMgACABEDEiAARAIANBwgA2AhwgAyAANgIMIAMgAUEBajYCFEEAIQIMXwsgAUEBaiEBDFALQTshAgxECwJAA0AgAS0AACIAQSBHIABBCUdxDQEgBCABQQFqIgFHDQALQcMAIQIMXQsLQTwhAgxCCwJAAkAgASAERwRAA0AgAS0AACIAQSBHBEAgAEEKaw4EAwQEAwQLIAQgAUEBaiIBRw0AC0E/IQIMXQtBPyECDFwLIAMgAy8BMkEgcjsBMgwKCyADKAIEIQAgA0EANgIEIAMgACABEDEiAEUNTiADQT42AhwgAyABNgIUIAMgADYCDEEAIQIMWgsCQCABIARHBEADQCABLQAAQcDDAGotAAAiAEEBRwRAIABBAkYNAwwMCyAEIAFBAWoiAUcNAAtBNyECDFsLQTchAgxaCyABQQFqIQEMBAtBOyECIAQgASIARg1YIAQgAWsgAygCACIBaiEGIAAgAWtBBWohBwJAA0AgAUGQyABqLQAAIAAtAAAiBUEgciAFIAVBwQBrQf8BcUEaSRtB/wFxRw0BIAFBBUYEQEEHIQEMPwsgAUEBaiEBIAQgAEEBaiIARw0ACyADIAY2AgAMWQsgA0EANgIAIAAhAQwFC0E6IQIgBCABIgBGDVcgBCABayADKAIAIgFqIQYgACABa0EIaiEHAkADQCABQbTBAGotAAAgAC0AACIFQSByIAUgBUHBAGtB/wFxQRpJG0H/AXFHDQEgAUEIRgRAQQUhAQw+CyABQQFqIQEgBCAAQQFqIgBHDQALIAMgBjYCAAxYCyADQQA2AgAgACEBDAQLQTkhAiAEIAEiAEYNViAEIAFrIAMoAgAiAWohBiAAIAFrQQNqIQcCQANAIAFBsMEAai0AACAALQAAIgVBIHIgBSAFQcEAa0H/AXFBGkkbQf8BcUcNASABQQNGBEBBBiEBDD0LIAFBAWohASAEIABBAWoiAEcNAAsgAyAGNgIADFcLIANBADYCACAAIQEMAwsCQANAIAEtAAAiAEEgRwRAIABBCmsOBAcEBAcCCyAEIAFBAWoiAUcNAAtBOCECDFYLIABBLEcNASABQQFqIQBBASEBAkACQAJAAkACQCADLQAsQQVrDgQDAQIEAAsgACEBDAQLQQIhAQwBC0EEIQELIANBAToALCADIAMvATIgAXI7ATIgACEBDAELIAMgAy8BMkEIcjsBMiAAIQELQT4hAgw7CyADQQA6ACwLQTkhAgw5CyABIARGBEBBNiECDFILAkACQAJAAkACQCABLQAAQQprDgQAAgIBAgsgAygCBCEAIANBADYCBCADIAAgARAxIgBFDQIgA0EzNgIcIAMgATYCFCADIAA2AgxBACECDFULIAMoAgQhACADQQA2AgQgAyAAIAEQMSIARQRAIAFBAWohAQwGCyADQTI2AhwgAyAANgIMIAMgAUEBajYCFEEAIQIMVAsgAy0ALkEBcQRAQd8BIQIMOwsgAygCBCEAIANBADYCBCADIAAgARAxIgANAQxJC0E0IQIMOQsgA0E1NgIcIAMgATYCFCADIAA2AgxBACECDFELQTUhAgw3CyADQS9qLQAAQQFxDQAgA0EANgIcIAMgATYCFCADQesWNgIQIANBGTYCDEEAIQIMTwtBMyECDDULIAEgBEYEQEEyIQIMTgsCQCABLQAAQQpGBEAgAUEBaiEBDAELIANBADYCHCADIAE2AhQgA0GSFzYCECADQQM2AgxBACECDE4LQTIhAgw0CyABIARGBEBBMSECDE0LAkAgAS0AACIAQQlGDQAgAEEgRg0AQQEhAgJAIAMtACxBBWsOBAYEBQANCyADIAMvATJBCHI7ATIMDAsgAy0ALkEBcUUNASADLQAsQQhHDQAgA0EAOgAsC0E9IQIMMgsgA0EANgIcIAMgATYCFCADQcIWNgIQIANBCjYCDEEAIQIMSgtBAiECDAELQQQhAgsgA0EBOgAsIAMgAy8BMiACcjsBMgwGCyABIARGBEBBMCECDEcLIAEtAABBCkYEQCABQQFqIQEMAQsgAy0ALkEBcQ0AIANBADYCHCADIAE2AhQgA0HcKDYCECADQQI2AgxBACECDEYLQTAhAgwsCyABQQFqIQFBMSECDCsLIAEgBEYEQEEvIQIMRAsgAS0AACIAQQlHIABBIEdxRQRAIAFBAWohASADLQAuQQFxDQEgA0EANgIcIAMgATYCFCADQZcQNgIQIANBCjYCDEEAIQIMRAtBASECAkACQAJAAkACQAJAIAMtACxBAmsOBwUEBAMBAgAECyADIAMvATJBCHI7ATIMAwtBAiECDAELQQQhAgsgA0EBOgAsIAMgAy8BMiACcjsBMgtBLyECDCsLIANBADYCHCADIAE2AhQgA0GEEzYCECADQQs2AgxBACECDEMLQeEBIQIMKQsgASAERgRAQS4hAgxCCyADQQA2AgQgA0ESNgIIIAMgASABEDEiAA0BC0EuIQIMJwsgA0EtNgIcIAMgATYCFCADIAA2AgxBACECDD8LQQAhAAJAIAMoAjgiAkUNACACKAJMIgJFDQAgAyACEQAAIQALIABFDQAgAEEVRw0BIANB2AA2AhwgAyABNgIUIANBsxs2AhAgA0EVNgIMQQAhAgw+C0HMACECDCQLIANBADYCHCADIAE2AhQgA0GzDjYCECADQR02AgxBACECDDwLIAEgBEYEQEHOACECDDwLIAEtAAAiAEEgRg0CIABBOkYNAQsgA0EAOgAsQQkhAgwhCyADKAIEIQAgA0EANgIEIAMgACABEDAiAA0BDAILIAMtAC5BAXEEQEHeASECDCALIAMoAgQhACADQQA2AgQgAyAAIAEQMCIARQ0CIANBKjYCHCADIAA2AgwgAyABQQFqNgIUQQAhAgw4CyADQcsANgIcIAMgADYCDCADIAFBAWo2AhRBACECDDcLIAFBAWohAUHAACECDB0LIAFBAWohAQwsCyABIARGBEBBKyECDDULAkAgAS0AAEEKRgRAIAFBAWohAQwBCyADLQAuQcAAcUUNBgsgAy0AMkGAAXEEQEEAIQACQCADKAI4IgJFDQAgAigCXCICRQ0AIAMgAhEAACEACyAARQ0SIABBFUYEQCADQQU2AhwgAyABNgIUIANBmxs2AhAgA0EVNgIMQQAhAgw2CyADQQA2AhwgAyABNgIUIANBkA42AhAgA0EUNgIMQQAhAgw1CyADQTJqIQIgAxA1QQAhAAJAIAMoAjgiBkUNACAGKAIoIgZFDQAgAyAGEQAAIQALIAAOFgIBAAQEBAQEBAQEBAQEBAQEBAQEBAMECyADQQE6ADALIAIgAi8BAEHAAHI7AQALQSshAgwYCyADQSk2AhwgAyABNgIUIANBrBk2AhAgA0EVNgIMQQAhAgwwCyADQQA2AhwgAyABNgIUIANB5Qs2AhAgA0ERNgIMQQAhAgwvCyADQQA2AhwgAyABNgIUIANBpQs2AhAgA0ECNgIMQQAhAgwuC0EBIQcgAy8BMiIFQQhxRQRAIAMpAyBCAFIhBwsCQCADLQAwBEBBASEAIAMtAClBBUYNASAFQcAAcUUgB3FFDQELAkAgAy0AKCICQQJGBEBBASEAIAMvATQiBkHlAEYNAkEAIQAgBUHAAHENAiAGQeQARg0CIAZB5gBrQQJJDQIgBkHMAUYNAiAGQbACRg0CDAELQQAhACAFQcAAcQ0BC0ECIQAgBUEIcQ0AIAVBgARxBEACQCACQQFHDQAgAy0ALkEKcQ0AQQUhAAwCC0EEIQAMAQsgBUEgcUUEQCADEDZBAEdBAnQhAAwBC0EAQQMgAykDIFAbIQALIABBAWsOBQIABwEDBAtBESECDBMLIANBAToAMQwpC0EAIQICQCADKAI4IgBFDQAgACgCMCIARQ0AIAMgABEAACECCyACRQ0mIAJBFUYEQCADQQM2AhwgAyABNgIUIANB0hs2AhAgA0EVNgIMQQAhAgwrC0EAIQIgA0EANgIcIAMgATYCFCADQd0ONgIQIANBEjYCDAwqCyADQQA2AhwgAyABNgIUIANB+SA2AhAgA0EPNgIMQQAhAgwpC0EAIQACQCADKAI4IgJFDQAgAigCMCICRQ0AIAMgAhEAACEACyAADQELQQ4hAgwOCyAAQRVGBEAgA0ECNgIcIAMgATYCFCADQdIbNgIQIANBFTYCDEEAIQIMJwsgA0EANgIcIAMgATYCFCADQd0ONgIQIANBEjYCDEEAIQIMJgtBKiECDAwLIAEgBEcEQCADQQk2AgggAyABNgIEQSkhAgwMC0EmIQIMJAsgAyADKQMgIgwgBCABa60iCn0iC0IAIAsgDFgbNwMgIAogDFQEQEElIQIMJAsgAygCBCEAIANBADYCBCADIAAgASAMp2oiARAyIgBFDQAgA0EFNgIcIAMgATYCFCADIAA2AgxBACECDCMLQQ8hAgwJC0IAIQoCQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkAgAS0AAEEwaw43FxYAAQIDBAUGBxQUFBQUFBQICQoLDA0UFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFA4PEBESExQLQgIhCgwWC0IDIQoMFQtCBCEKDBQLQgUhCgwTC0IGIQoMEgtCByEKDBELQgghCgwQC0IJIQoMDwtCCiEKDA4LQgshCgwNC0IMIQoMDAtCDSEKDAsLQg4hCgwKC0IPIQoMCQtCCiEKDAgLQgshCgwHC0IMIQoMBgtCDSEKDAULQg4hCgwEC0IPIQoMAwsgA0EANgIcIAMgATYCFCADQZ8VNgIQIANBDDYCDEEAIQIMIQsgASAERgRAQSIhAgwhC0IAIQoCQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAIAEtAABBMGsONxUUAAECAwQFBgcWFhYWFhYWCAkKCwwNFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYODxAREhMWC0ICIQoMFAtCAyEKDBMLQgQhCgwSC0IFIQoMEQtCBiEKDBALQgchCgwPC0IIIQoMDgtCCSEKDA0LQgohCgwMC0ILIQoMCwtCDCEKDAoLQg0hCgwJC0IOIQoMCAtCDyEKDAcLQgohCgwGC0ILIQoMBQtCDCEKDAQLQg0hCgwDC0IOIQoMAgtCDyEKDAELQgEhCgsgAUEBaiEBIAMpAyAiC0L//////////w9YBEAgAyALQgSGIAqENwMgDAILIANBADYCHCADIAE2AhQgA0G1CTYCECADQQw2AgxBACECDB4LQSchAgwEC0EoIQIMAwsgAyABOgAsIANBADYCACAHQQFqIQFBDCECDAILIANBADYCACAGQQFqIQFBCiECDAELIAFBAWohAUEIIQIMAAsAC0EAIQIgA0EANgIcIAMgATYCFCADQbI4NgIQIANBCDYCDAwXC0EAIQIgA0EANgIcIAMgATYCFCADQYMRNgIQIANBCTYCDAwWC0EAIQIgA0EANgIcIAMgATYCFCADQd8KNgIQIANBCTYCDAwVC0EAIQIgA0EANgIcIAMgATYCFCADQe0QNgIQIANBCTYCDAwUC0EAIQIgA0EANgIcIAMgATYCFCADQdIRNgIQIANBCTYCDAwTC0EAIQIgA0EANgIcIAMgATYCFCADQbI4NgIQIANBCDYCDAwSC0EAIQIgA0EANgIcIAMgATYCFCADQYMRNgIQIANBCTYCDAwRC0EAIQIgA0EANgIcIAMgATYCFCADQd8KNgIQIANBCTYCDAwQC0EAIQIgA0EANgIcIAMgATYCFCADQe0QNgIQIANBCTYCDAwPC0EAIQIgA0EANgIcIAMgATYCFCADQdIRNgIQIANBCTYCDAwOC0EAIQIgA0EANgIcIAMgATYCFCADQbkXNgIQIANBDzYCDAwNC0EAIQIgA0EANgIcIAMgATYCFCADQbkXNgIQIANBDzYCDAwMC0EAIQIgA0EANgIcIAMgATYCFCADQZkTNgIQIANBCzYCDAwLC0EAIQIgA0EANgIcIAMgATYCFCADQZ0JNgIQIANBCzYCDAwKC0EAIQIgA0EANgIcIAMgATYCFCADQZcQNgIQIANBCjYCDAwJC0EAIQIgA0EANgIcIAMgATYCFCADQbEQNgIQIANBCjYCDAwIC0EAIQIgA0EANgIcIAMgATYCFCADQbsdNgIQIANBAjYCDAwHC0EAIQIgA0EANgIcIAMgATYCFCADQZYWNgIQIANBAjYCDAwGC0EAIQIgA0EANgIcIAMgATYCFCADQfkYNgIQIANBAjYCDAwFC0EAIQIgA0EANgIcIAMgATYCFCADQcQYNgIQIANBAjYCDAwECyADQQI2AhwgAyABNgIUIANBqR42AhAgA0EWNgIMQQAhAgwDC0HeACECIAEgBEYNAiAJQQhqIQcgAygCACEFAkACQCABIARHBEAgBUGWyABqIQggBCAFaiABayEGIAVBf3NBCmoiBSABaiEAA0AgAS0AACAILQAARwRAQQIhCAwDCyAFRQRAQQAhCCAAIQEMAwsgBUEBayEFIAhBAWohCCAEIAFBAWoiAUcNAAsgBiEFIAQhAQsgB0EBNgIAIAMgBTYCAAwBCyADQQA2AgAgByAINgIACyAHIAE2AgQgCSgCDCEAAkACQCAJKAIIQQFrDgIEAQALIANBADYCHCADQcIeNgIQIANBFzYCDCADIABBAWo2AhRBACECDAMLIANBADYCHCADIAA2AhQgA0HXHjYCECADQQk2AgxBACECDAILIAEgBEYEQEEoIQIMAgsgA0EJNgIIIAMgATYCBEEnIQIMAQsgASAERgRAQQEhAgwBCwNAAkACQAJAIAEtAABBCmsOBAABAQABCyABQQFqIQEMAQsgAUEBaiEBIAMtAC5BIHENAEEAIQIgA0EANgIcIAMgATYCFCADQaEhNgIQIANBBTYCDAwCC0EBIQIgASAERw0ACwsgCUEQaiQAIAJFBEAgAygCDCEADAELIAMgAjYCHEEAIQAgAygCBCIBRQ0AIAMgASAEIAMoAggRAQAiAUUNACADIAQ2AhQgAyABNgIMIAEhAAsgAAu+AgECfyAAQQA6AAAgAEHkAGoiAUEBa0EAOgAAIABBADoAAiAAQQA6AAEgAUEDa0EAOgAAIAFBAmtBADoAACAAQQA6AAMgAUEEa0EAOgAAQQAgAGtBA3EiASAAaiIAQQA2AgBB5AAgAWtBfHEiAiAAaiIBQQRrQQA2AgACQCACQQlJDQAgAEEANgIIIABBADYCBCABQQhrQQA2AgAgAUEMa0EANgIAIAJBGUkNACAAQQA2AhggAEEANgIUIABBADYCECAAQQA2AgwgAUEQa0EANgIAIAFBFGtBADYCACABQRhrQQA2AgAgAUEca0EANgIAIAIgAEEEcUEYciICayIBQSBJDQAgACACaiEAA0AgAEIANwMYIABCADcDECAAQgA3AwggAEIANwMAIABBIGohACABQSBrIgFBH0sNAAsLC1YBAX8CQCAAKAIMDQACQAJAAkACQCAALQAxDgMBAAMCCyAAKAI4IgFFDQAgASgCMCIBRQ0AIAAgAREAACIBDQMLQQAPCwALIABByhk2AhBBDiEBCyABCxoAIAAoAgxFBEAgAEHeHzYCECAAQRU2AgwLCxQAIAAoAgxBFUYEQCAAQQA2AgwLCxQAIAAoAgxBFkYEQCAAQQA2AgwLCwcAIAAoAgwLBwAgACgCEAsJACAAIAE2AhALBwAgACgCFAsrAAJAIABBJ08NAEL//////wkgAK2IQgGDUA0AIABBAnRB0DhqKAIADwsACxcAIABBL08EQAALIABBAnRB7DlqKAIAC78JAQF/QfQtIQECQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQCAAQeQAaw70A2NiAAFhYWFhYWECAwQFYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYQYHCAkKCwwNDg9hYWFhYRBhYWFhYWFhYWFhYRFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWESExQVFhcYGRobYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYRwdHh8gISIjJCUmJygpKissLS4vMDEyMzQ1NmE3ODk6YWFhYWFhYWE7YWFhPGFhYWE9Pj9hYWFhYWFhYUBhYUFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFCQ0RFRkdISUpLTE1OT1BRUlNhYWFhYWFhYVRVVldYWVpbYVxdYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhXmFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYV9gYQtB6iwPC0GYJg8LQe0xDwtBoDcPC0HJKQ8LQbQpDwtBli0PC0HrKw8LQaI1DwtB2zQPC0HgKQ8LQeMkDwtB1SQPC0HuJA8LQeYlDwtByjQPC0HQNw8LQao1DwtB9SwPC0H2Jg8LQYIiDwtB8jMPC0G+KA8LQec3DwtBzSEPC0HAIQ8LQbglDwtByyUPC0GWJA8LQY80DwtBzTUPC0HdKg8LQe4zDwtBnDQPC0GeMQ8LQfQ1DwtB5SIPC0GvJQ8LQZkxDwtBsjYPC0H5Ng8LQcQyDwtB3SwPC0GCMQ8LQcExDwtBjTcPC0HJJA8LQew2DwtB5yoPC0HIIw8LQeIhDwtByTcPC0GlIg8LQZQiDwtB2zYPC0HeNQ8LQYYmDwtBvCsPC0GLMg8LQaAjDwtB9jAPC0GALA8LQYkrDwtBpCYPC0HyIw8LQYEoDwtBqzIPC0HrJw8LQcI2DwtBoiQPC0HPKg8LQdwjDwtBhycPC0HkNA8LQbciDwtBrTEPC0HVIg8LQa80DwtB3iYPC0HWMg8LQfQ0DwtBgTgPC0H0Nw8LQZI2DwtBnScPC0GCKQ8LQY0jDwtB1zEPC0G9NQ8LQbQ3DwtB2DAPC0G2Jw8LQZo4DwtBpyoPC0HEJw8LQa4jDwtB9SIPCwALQcomIQELIAELFwAgACAALwEuQf7/A3EgAUEAR3I7AS4LGgAgACAALwEuQf3/A3EgAUEAR0EBdHI7AS4LGgAgACAALwEuQfv/A3EgAUEAR0ECdHI7AS4LGgAgACAALwEuQff/A3EgAUEAR0EDdHI7AS4LGgAgACAALwEuQe//A3EgAUEAR0EEdHI7AS4LGgAgACAALwEuQd//A3EgAUEAR0EFdHI7AS4LGgAgACAALwEuQb//A3EgAUEAR0EGdHI7AS4LGgAgACAALwEuQf/+A3EgAUEAR0EHdHI7AS4LGgAgACAALwEuQf/9A3EgAUEAR0EIdHI7AS4LGgAgACAALwEuQf/7A3EgAUEAR0EJdHI7AS4LPgECfwJAIAAoAjgiA0UNACADKAIEIgNFDQAgACABIAIgAWsgAxEBACIEQX9HDQAgAEHhEjYCEEEYIQQLIAQLPgECfwJAIAAoAjgiA0UNACADKAIIIgNFDQAgACABIAIgAWsgAxEBACIEQX9HDQAgAEH8ETYCEEEYIQQLIAQLPgECfwJAIAAoAjgiA0UNACADKAIMIgNFDQAgACABIAIgAWsgAxEBACIEQX9HDQAgAEHsCjYCEEEYIQQLIAQLPgECfwJAIAAoAjgiA0UNACADKAIQIgNFDQAgACABIAIgAWsgAxEBACIEQX9HDQAgAEH6HjYCEEEYIQQLIAQLPgECfwJAIAAoAjgiA0UNACADKAIUIgNFDQAgACABIAIgAWsgAxEBACIEQX9HDQAgAEHLEDYCEEEYIQQLIAQLPgECfwJAIAAoAjgiA0UNACADKAIYIgNFDQAgACABIAIgAWsgAxEBACIEQX9HDQAgAEG3HzYCEEEYIQQLIAQLPgECfwJAIAAoAjgiA0UNACADKAIcIgNFDQAgACABIAIgAWsgAxEBACIEQX9HDQAgAEG/FTYCEEEYIQQLIAQLPgECfwJAIAAoAjgiA0UNACADKAIsIgNFDQAgACABIAIgAWsgAxEBACIEQX9HDQAgAEH+CDYCEEEYIQQLIAQLPgECfwJAIAAoAjgiA0UNACADKAIgIgNFDQAgACABIAIgAWsgAxEBACIEQX9HDQAgAEGMHTYCEEEYIQQLIAQLPgECfwJAIAAoAjgiA0UNACADKAIkIgNFDQAgACABIAIgAWsgAxEBACIEQX9HDQAgAEHmFTYCEEEYIQQLIAQLOAAgAAJ/IAAvATJBFHFBFEYEQEEBIAAtAChBAUYNARogAC8BNEHlAEYMAQsgAC0AKUEFRgs6ADALWQECfwJAIAAtAChBAUYNACAALwE0IgFB5ABrQeQASQ0AIAFBzAFGDQAgAUGwAkYNACAALwEyIgBBwABxDQBBASECIABBiARxQYAERg0AIABBKHFFIQILIAILjAEBAn8CQAJAAkAgAC0AKkUNACAALQArRQ0AIAAvATIiAUECcUUNAQwCCyAALwEyIgFBAXFFDQELQQEhAiAALQAoQQFGDQAgAC8BNCIAQeQAa0HkAEkNACAAQcwBRg0AIABBsAJGDQAgAUHAAHENAEEAIQIgAUGIBHFBgARGDQAgAUEocUEARyECCyACC1cAIABBGGpCADcDACAAQgA3AwAgAEE4akIANwMAIABBMGpCADcDACAAQShqQgA3AwAgAEEgakIANwMAIABBEGpCADcDACAAQQhqQgA3AwAgAEH9ATYCHAsGACAAEDoLmi0BC38jAEEQayIKJABB3NUAKAIAIglFBEBBnNkAKAIAIgVFBEBBqNkAQn83AgBBoNkAQoCAhICAgMAANwIAQZzZACAKQQhqQXBxQdiq1aoFcyIFNgIAQbDZAEEANgIAQYDZAEEANgIAC0GE2QBBwNkENgIAQdTVAEHA2QQ2AgBB6NUAIAU2AgBB5NUAQX82AgBBiNkAQcCmAzYCAANAIAFBgNYAaiABQfTVAGoiAjYCACACIAFB7NUAaiIDNgIAIAFB+NUAaiADNgIAIAFBiNYAaiABQfzVAGoiAzYCACADIAI2AgAgAUGQ1gBqIAFBhNYAaiICNgIAIAIgAzYCACABQYzWAGogAjYCACABQSBqIgFBgAJHDQALQczZBEGBpgM2AgBB4NUAQazZACgCADYCAEHQ1QBBgKYDNgIAQdzVAEHI2QQ2AgBBzP8HQTg2AgBByNkEIQkLAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkAgAEHsAU0EQEHE1QAoAgAiBkEQIABBE2pBcHEgAEELSRsiBEEDdiIAdiIBQQNxBEACQCABQQFxIAByQQFzIgJBA3QiAEHs1QBqIgEgAEH01QBqKAIAIgAoAggiA0YEQEHE1QAgBkF+IAJ3cTYCAAwBCyABIAM2AgggAyABNgIMCyAAQQhqIQEgACACQQN0IgJBA3I2AgQgACACaiIAIAAoAgRBAXI2AgQMEQtBzNUAKAIAIgggBE8NASABBEACQEECIAB0IgJBACACa3IgASAAdHFoIgBBA3QiAkHs1QBqIgEgAkH01QBqKAIAIgIoAggiA0YEQEHE1QAgBkF+IAB3cSIGNgIADAELIAEgAzYCCCADIAE2AgwLIAIgBEEDcjYCBCAAQQN0IgAgBGshBSAAIAJqIAU2AgAgAiAEaiIEIAVBAXI2AgQgCARAIAhBeHFB7NUAaiEAQdjVACgCACEDAn9BASAIQQN2dCIBIAZxRQRAQcTVACABIAZyNgIAIAAMAQsgACgCCAsiASADNgIMIAAgAzYCCCADIAA2AgwgAyABNgIICyACQQhqIQFB2NUAIAQ2AgBBzNUAIAU2AgAMEQtByNUAKAIAIgtFDQEgC2hBAnRB9NcAaigCACIAKAIEQXhxIARrIQUgACECA0ACQCACKAIQIgFFBEAgAkEUaigCACIBRQ0BCyABKAIEQXhxIARrIgMgBUkhAiADIAUgAhshBSABIAAgAhshACABIQIMAQsLIAAoAhghCSAAKAIMIgMgAEcEQEHU1QAoAgAaIAMgACgCCCIBNgIIIAEgAzYCDAwQCyAAQRRqIgIoAgAiAUUEQCAAKAIQIgFFDQMgAEEQaiECCwNAIAIhByABIgNBFGoiAigCACIBDQAgA0EQaiECIAMoAhAiAQ0ACyAHQQA2AgAMDwtBfyEEIABBv39LDQAgAEETaiIBQXBxIQRByNUAKAIAIghFDQBBACAEayEFAkACQAJAAn9BACAEQYACSQ0AGkEfIARB////B0sNABogBEEmIAFBCHZnIgBrdkEBcSAAQQF0a0E+agsiBkECdEH01wBqKAIAIgJFBEBBACEBQQAhAwwBC0EAIQEgBEEZIAZBAXZrQQAgBkEfRxt0IQBBACEDA0ACQCACKAIEQXhxIARrIgcgBU8NACACIQMgByIFDQBBACEFIAIhAQwDCyABIAJBFGooAgAiByAHIAIgAEEddkEEcWpBEGooAgAiAkYbIAEgBxshASAAQQF0IQAgAg0ACwsgASADckUEQEEAIQNBAiAGdCIAQQAgAGtyIAhxIgBFDQMgAGhBAnRB9NcAaigCACEBCyABRQ0BCwNAIAEoAgRBeHEgBGsiAiAFSSEAIAIgBSAAGyEFIAEgAyAAGyEDIAEoAhAiAAR/IAAFIAFBFGooAgALIgENAAsLIANFDQAgBUHM1QAoAgAgBGtPDQAgAygCGCEHIAMgAygCDCIARwRAQdTVACgCABogACADKAIIIgE2AgggASAANgIMDA4LIANBFGoiAigCACIBRQRAIAMoAhAiAUUNAyADQRBqIQILA0AgAiEGIAEiAEEUaiICKAIAIgENACAAQRBqIQIgACgCECIBDQALIAZBADYCAAwNC0HM1QAoAgAiAyAETwRAQdjVACgCACEBAkAgAyAEayICQRBPBEAgASAEaiIAIAJBAXI2AgQgASADaiACNgIAIAEgBEEDcjYCBAwBCyABIANBA3I2AgQgASADaiIAIAAoAgRBAXI2AgRBACEAQQAhAgtBzNUAIAI2AgBB2NUAIAA2AgAgAUEIaiEBDA8LQdDVACgCACIDIARLBEAgBCAJaiIAIAMgBGsiAUEBcjYCBEHc1QAgADYCAEHQ1QAgATYCACAJIARBA3I2AgQgCUEIaiEBDA8LQQAhASAEAn9BnNkAKAIABEBBpNkAKAIADAELQajZAEJ/NwIAQaDZAEKAgISAgIDAADcCAEGc2QAgCkEMakFwcUHYqtWqBXM2AgBBsNkAQQA2AgBBgNkAQQA2AgBBgIAECyIAIARBxwBqIgVqIgZBACAAayIHcSICTwRAQbTZAEEwNgIADA8LAkBB/NgAKAIAIgFFDQBB9NgAKAIAIgggAmohACAAIAFNIAAgCEtxDQBBACEBQbTZAEEwNgIADA8LQYDZAC0AAEEEcQ0EAkACQCAJBEBBhNkAIQEDQCABKAIAIgAgCU0EQCAAIAEoAgRqIAlLDQMLIAEoAggiAQ0ACwtBABA7IgBBf0YNBSACIQZBoNkAKAIAIgFBAWsiAyAAcQRAIAIgAGsgACADakEAIAFrcWohBgsgBCAGTw0FIAZB/v///wdLDQVB/NgAKAIAIgMEQEH02AAoAgAiByAGaiEBIAEgB00NBiABIANLDQYLIAYQOyIBIABHDQEMBwsgBiADayAHcSIGQf7///8HSw0EIAYQOyEAIAAgASgCACABKAIEakYNAyAAIQELAkAgBiAEQcgAak8NACABQX9GDQBBpNkAKAIAIgAgBSAGa2pBACAAa3EiAEH+////B0sEQCABIQAMBwsgABA7QX9HBEAgACAGaiEGIAEhAAwHC0EAIAZrEDsaDAQLIAEiAEF/Rw0FDAMLQQAhAwwMC0EAIQAMCgsgAEF/Rw0CC0GA2QBBgNkAKAIAQQRyNgIACyACQf7///8HSw0BIAIQOyEAQQAQOyEBIABBf0YNASABQX9GDQEgACABTw0BIAEgAGsiBiAEQThqTQ0BC0H02ABB9NgAKAIAIAZqIgE2AgBB+NgAKAIAIAFJBEBB+NgAIAE2AgALAkACQAJAQdzVACgCACICBEBBhNkAIQEDQCAAIAEoAgAiAyABKAIEIgVqRg0CIAEoAggiAQ0ACwwCC0HU1QAoAgAiAUEARyAAIAFPcUUEQEHU1QAgADYCAAtBACEBQYjZACAGNgIAQYTZACAANgIAQeTVAEF/NgIAQejVAEGc2QAoAgA2AgBBkNkAQQA2AgADQCABQYDWAGogAUH01QBqIgI2AgAgAiABQezVAGoiAzYCACABQfjVAGogAzYCACABQYjWAGogAUH81QBqIgM2AgAgAyACNgIAIAFBkNYAaiABQYTWAGoiAjYCACACIAM2AgAgAUGM1gBqIAI2AgAgAUEgaiIBQYACRw0AC0F4IABrQQ9xIgEgAGoiAiAGQThrIgMgAWsiAUEBcjYCBEHg1QBBrNkAKAIANgIAQdDVACABNgIAQdzVACACNgIAIAAgA2pBODYCBAwCCyAAIAJNDQAgAiADSQ0AIAEoAgxBCHENAEF4IAJrQQ9xIgAgAmoiA0HQ1QAoAgAgBmoiByAAayIAQQFyNgIEIAEgBSAGajYCBEHg1QBBrNkAKAIANgIAQdDVACAANgIAQdzVACADNgIAIAIgB2pBODYCBAwBCyAAQdTVACgCAEkEQEHU1QAgADYCAAsgACAGaiEDQYTZACEBAkACQAJAA0AgAyABKAIARwRAIAEoAggiAQ0BDAILCyABLQAMQQhxRQ0BC0GE2QAhAQNAIAEoAgAiAyACTQRAIAMgASgCBGoiBSACSw0DCyABKAIIIQEMAAsACyABIAA2AgAgASABKAIEIAZqNgIEIABBeCAAa0EPcWoiCSAEQQNyNgIEIANBeCADa0EPcWoiBiAEIAlqIgRrIQEgAiAGRgRAQdzVACAENgIAQdDVAEHQ1QAoAgAgAWoiADYCACAEIABBAXI2AgQMCAtB2NUAKAIAIAZGBEBB2NUAIAQ2AgBBzNUAQczVACgCACABaiIANgIAIAQgAEEBcjYCBCAAIARqIAA2AgAMCAsgBigCBCIFQQNxQQFHDQYgBUF4cSEIIAVB/wFNBEAgBUEDdiEDIAYoAggiACAGKAIMIgJGBEBBxNUAQcTVACgCAEF+IAN3cTYCAAwHCyACIAA2AgggACACNgIMDAYLIAYoAhghByAGIAYoAgwiAEcEQCAAIAYoAggiAjYCCCACIAA2AgwMBQsgBkEUaiICKAIAIgVFBEAgBigCECIFRQ0EIAZBEGohAgsDQCACIQMgBSIAQRRqIgIoAgAiBQ0AIABBEGohAiAAKAIQIgUNAAsgA0EANgIADAQLQXggAGtBD3EiASAAaiIHIAZBOGsiAyABayIBQQFyNgIEIAAgA2pBODYCBCACIAVBNyAFa0EPcWpBP2siAyADIAJBEGpJGyIDQSM2AgRB4NUAQazZACgCADYCAEHQ1QAgATYCAEHc1QAgBzYCACADQRBqQYzZACkCADcCACADQYTZACkCADcCCEGM2QAgA0EIajYCAEGI2QAgBjYCAEGE2QAgADYCAEGQ2QBBADYCACADQSRqIQEDQCABQQc2AgAgBSABQQRqIgFLDQALIAIgA0YNACADIAMoAgRBfnE2AgQgAyADIAJrIgU2AgAgAiAFQQFyNgIEIAVB/wFNBEAgBUF4cUHs1QBqIQACf0HE1QAoAgAiAUEBIAVBA3Z0IgNxRQRAQcTVACABIANyNgIAIAAMAQsgACgCCAsiASACNgIMIAAgAjYCCCACIAA2AgwgAiABNgIIDAELQR8hASAFQf///wdNBEAgBUEmIAVBCHZnIgBrdkEBcSAAQQF0a0E+aiEBCyACIAE2AhwgAkIANwIQIAFBAnRB9NcAaiEAQcjVACgCACIDQQEgAXQiBnFFBEAgACACNgIAQcjVACADIAZyNgIAIAIgADYCGCACIAI2AgggAiACNgIMDAELIAVBGSABQQF2a0EAIAFBH0cbdCEBIAAoAgAhAwJAA0AgAyIAKAIEQXhxIAVGDQEgAUEddiEDIAFBAXQhASAAIANBBHFqQRBqIgYoAgAiAw0ACyAGIAI2AgAgAiAANgIYIAIgAjYCDCACIAI2AggMAQsgACgCCCIBIAI2AgwgACACNgIIIAJBADYCGCACIAA2AgwgAiABNgIIC0HQ1QAoAgAiASAETQ0AQdzVACgCACIAIARqIgIgASAEayIBQQFyNgIEQdDVACABNgIAQdzVACACNgIAIAAgBEEDcjYCBCAAQQhqIQEMCAtBACEBQbTZAEEwNgIADAcLQQAhAAsgB0UNAAJAIAYoAhwiAkECdEH01wBqIgMoAgAgBkYEQCADIAA2AgAgAA0BQcjVAEHI1QAoAgBBfiACd3E2AgAMAgsgB0EQQRQgBygCECAGRhtqIAA2AgAgAEUNAQsgACAHNgIYIAYoAhAiAgRAIAAgAjYCECACIAA2AhgLIAZBFGooAgAiAkUNACAAQRRqIAI2AgAgAiAANgIYCyABIAhqIQEgBiAIaiIGKAIEIQULIAYgBUF+cTYCBCABIARqIAE2AgAgBCABQQFyNgIEIAFB/wFNBEAgAUF4cUHs1QBqIQACf0HE1QAoAgAiAkEBIAFBA3Z0IgFxRQRAQcTVACABIAJyNgIAIAAMAQsgACgCCAsiASAENgIMIAAgBDYCCCAEIAA2AgwgBCABNgIIDAELQR8hBSABQf///wdNBEAgAUEmIAFBCHZnIgBrdkEBcSAAQQF0a0E+aiEFCyAEIAU2AhwgBEIANwIQIAVBAnRB9NcAaiEAQcjVACgCACICQQEgBXQiA3FFBEAgACAENgIAQcjVACACIANyNgIAIAQgADYCGCAEIAQ2AgggBCAENgIMDAELIAFBGSAFQQF2a0EAIAVBH0cbdCEFIAAoAgAhAAJAA0AgACICKAIEQXhxIAFGDQEgBUEddiEAIAVBAXQhBSACIABBBHFqQRBqIgMoAgAiAA0ACyADIAQ2AgAgBCACNgIYIAQgBDYCDCAEIAQ2AggMAQsgAigCCCIAIAQ2AgwgAiAENgIIIARBADYCGCAEIAI2AgwgBCAANgIICyAJQQhqIQEMAgsCQCAHRQ0AAkAgAygCHCIBQQJ0QfTXAGoiAigCACADRgRAIAIgADYCACAADQFByNUAIAhBfiABd3EiCDYCAAwCCyAHQRBBFCAHKAIQIANGG2ogADYCACAARQ0BCyAAIAc2AhggAygCECIBBEAgACABNgIQIAEgADYCGAsgA0EUaigCACIBRQ0AIABBFGogATYCACABIAA2AhgLAkAgBUEPTQRAIAMgBCAFaiIAQQNyNgIEIAAgA2oiACAAKAIEQQFyNgIEDAELIAMgBGoiAiAFQQFyNgIEIAMgBEEDcjYCBCACIAVqIAU2AgAgBUH/AU0EQCAFQXhxQezVAGohAAJ/QcTVACgCACIBQQEgBUEDdnQiBXFFBEBBxNUAIAEgBXI2AgAgAAwBCyAAKAIICyIBIAI2AgwgACACNgIIIAIgADYCDCACIAE2AggMAQtBHyEBIAVB////B00EQCAFQSYgBUEIdmciAGt2QQFxIABBAXRrQT5qIQELIAIgATYCHCACQgA3AhAgAUECdEH01wBqIQBBASABdCIEIAhxRQRAIAAgAjYCAEHI1QAgBCAIcjYCACACIAA2AhggAiACNgIIIAIgAjYCDAwBCyAFQRkgAUEBdmtBACABQR9HG3QhASAAKAIAIQQCQANAIAQiACgCBEF4cSAFRg0BIAFBHXYhBCABQQF0IQEgACAEQQRxakEQaiIGKAIAIgQNAAsgBiACNgIAIAIgADYCGCACIAI2AgwgAiACNgIIDAELIAAoAggiASACNgIMIAAgAjYCCCACQQA2AhggAiAANgIMIAIgATYCCAsgA0EIaiEBDAELAkAgCUUNAAJAIAAoAhwiAUECdEH01wBqIgIoAgAgAEYEQCACIAM2AgAgAw0BQcjVACALQX4gAXdxNgIADAILIAlBEEEUIAkoAhAgAEYbaiADNgIAIANFDQELIAMgCTYCGCAAKAIQIgEEQCADIAE2AhAgASADNgIYCyAAQRRqKAIAIgFFDQAgA0EUaiABNgIAIAEgAzYCGAsCQCAFQQ9NBEAgACAEIAVqIgFBA3I2AgQgACABaiIBIAEoAgRBAXI2AgQMAQsgACAEaiIHIAVBAXI2AgQgACAEQQNyNgIEIAUgB2ogBTYCACAIBEAgCEF4cUHs1QBqIQFB2NUAKAIAIQMCf0EBIAhBA3Z0IgIgBnFFBEBBxNUAIAIgBnI2AgAgAQwBCyABKAIICyICIAM2AgwgASADNgIIIAMgATYCDCADIAI2AggLQdjVACAHNgIAQczVACAFNgIACyAAQQhqIQELIApBEGokACABC0MAIABFBEA/AEEQdA8LAkAgAEH//wNxDQAgAEEASA0AIABBEHZAACIAQX9GBEBBtNkAQTA2AgBBfw8LIABBEHQPCwALC5lCIgBBgAgLDQEAAAAAAAAAAgAAAAMAQZgICwUEAAAABQBBqAgLCQYAAAAHAAAACABB5AgLwjJJbnZhbGlkIGNoYXIgaW4gdXJsIHF1ZXJ5AFNwYW4gY2FsbGJhY2sgZXJyb3IgaW4gb25fYm9keQBDb250ZW50LUxlbmd0aCBvdmVyZmxvdwBDaHVuayBzaXplIG92ZXJmbG93AEludmFsaWQgbWV0aG9kIGZvciBIVFRQL3gueCByZXF1ZXN0AEludmFsaWQgbWV0aG9kIGZvciBSVFNQL3gueCByZXF1ZXN0AEV4cGVjdGVkIFNPVVJDRSBtZXRob2QgZm9yIElDRS94LnggcmVxdWVzdABJbnZhbGlkIGNoYXIgaW4gdXJsIGZyYWdtZW50IHN0YXJ0AEV4cGVjdGVkIGRvdABTcGFuIGNhbGxiYWNrIGVycm9yIGluIG9uX3N0YXR1cwBJbnZhbGlkIHJlc3BvbnNlIHN0YXR1cwBFeHBlY3RlZCBMRiBhZnRlciBoZWFkZXJzAEludmFsaWQgY2hhcmFjdGVyIGluIGNodW5rIGV4dGVuc2lvbnMAVXNlciBjYWxsYmFjayBlcnJvcgBgb25fcmVzZXRgIGNhbGxiYWNrIGVycm9yAGBvbl9jaHVua19oZWFkZXJgIGNhbGxiYWNrIGVycm9yAGBvbl9tZXNzYWdlX2JlZ2luYCBjYWxsYmFjayBlcnJvcgBgb25fY2h1bmtfZXh0ZW5zaW9uX3ZhbHVlYCBjYWxsYmFjayBlcnJvcgBgb25fc3RhdHVzX2NvbXBsZXRlYCBjYWxsYmFjayBlcnJvcgBgb25fdmVyc2lvbl9jb21wbGV0ZWAgY2FsbGJhY2sgZXJyb3IAYG9uX3VybF9jb21wbGV0ZWAgY2FsbGJhY2sgZXJyb3IAYG9uX3Byb3RvY29sX2NvbXBsZXRlYCBjYWxsYmFjayBlcnJvcgBgb25fY2h1bmtfY29tcGxldGVgIGNhbGxiYWNrIGVycm9yAGBvbl9oZWFkZXJfdmFsdWVfY29tcGxldGVgIGNhbGxiYWNrIGVycm9yAGBvbl9tZXNzYWdlX2NvbXBsZXRlYCBjYWxsYmFjayBlcnJvcgBgb25fbWV0aG9kX2NvbXBsZXRlYCBjYWxsYmFjayBlcnJvcgBgb25faGVhZGVyX2ZpZWxkX2NvbXBsZXRlYCBjYWxsYmFjayBlcnJvcgBgb25fY2h1bmtfZXh0ZW5zaW9uX25hbWVgIGNhbGxiYWNrIGVycm9yAFVuZXhwZWN0ZWQgY2hhciBpbiB1cmwgc2VydmVyAEludmFsaWQgaGVhZGVyIHZhbHVlIGNoYXIASW52YWxpZCBoZWFkZXIgZmllbGQgY2hhcgBTcGFuIGNhbGxiYWNrIGVycm9yIGluIG9uX3ZlcnNpb24ASW52YWxpZCBtaW5vciB2ZXJzaW9uAEludmFsaWQgbWFqb3IgdmVyc2lvbgBFeHBlY3RlZCBzcGFjZSBhZnRlciB2ZXJzaW9uAEV4cGVjdGVkIENSTEYgYWZ0ZXIgdmVyc2lvbgBJbnZhbGlkIEhUVFAgdmVyc2lvbgBJbnZhbGlkIGhlYWRlciB0b2tlbgBTcGFuIGNhbGxiYWNrIGVycm9yIGluIG9uX3VybABJbnZhbGlkIGNoYXJhY3RlcnMgaW4gdXJsAFVuZXhwZWN0ZWQgc3RhcnQgY2hhciBpbiB1cmwARG91YmxlIEAgaW4gdXJsAFNwYW4gY2FsbGJhY2sgZXJyb3IgaW4gb25fcHJvdG9jb2wARW1wdHkgQ29udGVudC1MZW5ndGgASW52YWxpZCBjaGFyYWN0ZXIgaW4gQ29udGVudC1MZW5ndGgAVHJhbnNmZXItRW5jb2RpbmcgY2FuJ3QgYmUgcHJlc2VudCB3aXRoIENvbnRlbnQtTGVuZ3RoAER1cGxpY2F0ZSBDb250ZW50LUxlbmd0aABJbnZhbGlkIGNoYXIgaW4gdXJsIHBhdGgAQ29udGVudC1MZW5ndGggY2FuJ3QgYmUgcHJlc2VudCB3aXRoIFRyYW5zZmVyLUVuY29kaW5nAE1pc3NpbmcgZXhwZWN0ZWQgQ1IgYWZ0ZXIgY2h1bmsgc2l6ZQBFeHBlY3RlZCBMRiBhZnRlciBjaHVuayBzaXplAEludmFsaWQgY2hhcmFjdGVyIGluIGNodW5rIHNpemUAU3BhbiBjYWxsYmFjayBlcnJvciBpbiBvbl9oZWFkZXJfdmFsdWUAU3BhbiBjYWxsYmFjayBlcnJvciBpbiBvbl9jaHVua19leHRlbnNpb25fdmFsdWUASW52YWxpZCBjaGFyYWN0ZXIgaW4gY2h1bmsgZXh0ZW5zaW9ucyB2YWx1ZQBVbmV4cGVjdGVkIHdoaXRlc3BhY2UgYWZ0ZXIgaGVhZGVyIHZhbHVlAE1pc3NpbmcgZXhwZWN0ZWQgQ1IgYWZ0ZXIgaGVhZGVyIHZhbHVlAE1pc3NpbmcgZXhwZWN0ZWQgTEYgYWZ0ZXIgaGVhZGVyIHZhbHVlAEludmFsaWQgYFRyYW5zZmVyLUVuY29kaW5nYCBoZWFkZXIgdmFsdWUATWlzc2luZyBleHBlY3RlZCBDUiBhZnRlciBjaHVuayBleHRlbnNpb24gdmFsdWUASW52YWxpZCBjaGFyYWN0ZXIgaW4gY2h1bmsgZXh0ZW5zaW9ucyBxdW90ZSB2YWx1ZQBJbnZhbGlkIHF1b3RlZC1wYWlyIGluIGNodW5rIGV4dGVuc2lvbnMgcXVvdGVkIHZhbHVlAEludmFsaWQgY2hhcmFjdGVyIGluIGNodW5rIGV4dGVuc2lvbnMgcXVvdGVkIHZhbHVlAFBhdXNlZCBieSBvbl9oZWFkZXJzX2NvbXBsZXRlAEludmFsaWQgRU9GIHN0YXRlAG9uX3Jlc2V0IHBhdXNlAG9uX2NodW5rX2hlYWRlciBwYXVzZQBvbl9tZXNzYWdlX2JlZ2luIHBhdXNlAG9uX2NodW5rX2V4dGVuc2lvbl92YWx1ZSBwYXVzZQBvbl9zdGF0dXNfY29tcGxldGUgcGF1c2UAb25fdmVyc2lvbl9jb21wbGV0ZSBwYXVzZQBvbl91cmxfY29tcGxldGUgcGF1c2UAb25fcHJvdG9jb2xfY29tcGxldGUgcGF1c2UAb25fY2h1bmtfY29tcGxldGUgcGF1c2UAb25faGVhZGVyX3ZhbHVlX2NvbXBsZXRlIHBhdXNlAG9uX21lc3NhZ2VfY29tcGxldGUgcGF1c2UAb25fbWV0aG9kX2NvbXBsZXRlIHBhdXNlAG9uX2hlYWRlcl9maWVsZF9jb21wbGV0ZSBwYXVzZQBvbl9jaHVua19leHRlbnNpb25fbmFtZSBwYXVzZQBVbmV4cGVjdGVkIHNwYWNlIGFmdGVyIHN0YXJ0IGxpbmUATWlzc2luZyBleHBlY3RlZCBDUiBhZnRlciByZXNwb25zZSBsaW5lAFNwYW4gY2FsbGJhY2sgZXJyb3IgaW4gb25fY2h1bmtfZXh0ZW5zaW9uX25hbWUASW52YWxpZCBjaGFyYWN0ZXIgaW4gY2h1bmsgZXh0ZW5zaW9ucyBuYW1lAE1pc3NpbmcgZXhwZWN0ZWQgQ1IgYWZ0ZXIgY2h1bmsgZXh0ZW5zaW9uIG5hbWUASW52YWxpZCBzdGF0dXMgY29kZQBQYXVzZSBvbiBDT05ORUNUL1VwZ3JhZGUAUGF1c2Ugb24gUFJJL1VwZ3JhZGUARXhwZWN0ZWQgSFRUUC8yIENvbm5lY3Rpb24gUHJlZmFjZQBTcGFuIGNhbGxiYWNrIGVycm9yIGluIG9uX21ldGhvZABFeHBlY3RlZCBzcGFjZSBhZnRlciBtZXRob2QAU3BhbiBjYWxsYmFjayBlcnJvciBpbiBvbl9oZWFkZXJfZmllbGQAUGF1c2VkAEludmFsaWQgd29yZCBlbmNvdW50ZXJlZABJbnZhbGlkIG1ldGhvZCBlbmNvdW50ZXJlZABNaXNzaW5nIGV4cGVjdGVkIENSIGFmdGVyIGNodW5rIGRhdGEARXhwZWN0ZWQgTEYgYWZ0ZXIgY2h1bmsgZGF0YQBVbmV4cGVjdGVkIGNoYXIgaW4gdXJsIHNjaGVtYQBSZXF1ZXN0IGhhcyBpbnZhbGlkIGBUcmFuc2Zlci1FbmNvZGluZ2AARGF0YSBhZnRlciBgQ29ubmVjdGlvbjogY2xvc2VgAFNXSVRDSF9QUk9YWQBVU0VfUFJPWFkATUtBQ1RJVklUWQBVTlBST0NFU1NBQkxFX0VOVElUWQBRVUVSWQBDT1BZAE1PVkVEX1BFUk1BTkVOVExZAFRPT19FQVJMWQBOT1RJRlkARkFJTEVEX0RFUEVOREVOQ1kAQkFEX0dBVEVXQVkAUExBWQBQVVQAQ0hFQ0tPVVQAR0FURVdBWV9USU1FT1VUAFJFUVVFU1RfVElNRU9VVABORVRXT1JLX0NPTk5FQ1RfVElNRU9VVABDT05ORUNUSU9OX1RJTUVPVVQATE9HSU5fVElNRU9VVABORVRXT1JLX1JFQURfVElNRU9VVABQT1NUAE1JU0RJUkVDVEVEX1JFUVVFU1QAQ0xJRU5UX0NMT1NFRF9SRVFVRVNUAENMSUVOVF9DTE9TRURfTE9BRF9CQUxBTkNFRF9SRVFVRVNUAEJBRF9SRVFVRVNUAEhUVFBfUkVRVUVTVF9TRU5UX1RPX0hUVFBTX1BPUlQAUkVQT1JUAElNX0FfVEVBUE9UAFJFU0VUX0NPTlRFTlQATk9fQ09OVEVOVABQQVJUSUFMX0NPTlRFTlQASFBFX0lOVkFMSURfQ09OU1RBTlQASFBFX0NCX1JFU0VUAEdFVABIUEVfU1RSSUNUAENPTkZMSUNUAFRFTVBPUkFSWV9SRURJUkVDVABQRVJNQU5FTlRfUkVESVJFQ1QAQ09OTkVDVABNVUxUSV9TVEFUVVMASFBFX0lOVkFMSURfU1RBVFVTAFRPT19NQU5ZX1JFUVVFU1RTAEVBUkxZX0hJTlRTAFVOQVZBSUxBQkxFX0ZPUl9MRUdBTF9SRUFTT05TAE9QVElPTlMAU1dJVENISU5HX1BST1RPQ09MUwBWQVJJQU5UX0FMU09fTkVHT1RJQVRFUwBNVUxUSVBMRV9DSE9JQ0VTAElOVEVSTkFMX1NFUlZFUl9FUlJPUgBXRUJfU0VSVkVSX1VOS05PV05fRVJST1IAUkFJTEdVTl9FUlJPUgBJREVOVElUWV9QUk9WSURFUl9BVVRIRU5USUNBVElPTl9FUlJPUgBTU0xfQ0VSVElGSUNBVEVfRVJST1IASU5WQUxJRF9YX0ZPUldBUkRFRF9GT1IAU0VUX1BBUkFNRVRFUgBHRVRfUEFSQU1FVEVSAEhQRV9VU0VSAFNFRV9PVEhFUgBIUEVfQ0JfQ0hVTktfSEVBREVSAEV4cGVjdGVkIExGIGFmdGVyIENSAE1LQ0FMRU5EQVIAU0VUVVAAV0VCX1NFUlZFUl9JU19ET1dOAFRFQVJET1dOAEhQRV9DTE9TRURfQ09OTkVDVElPTgBIRVVSSVNUSUNfRVhQSVJBVElPTgBESVNDT05ORUNURURfT1BFUkFUSU9OAE5PTl9BVVRIT1JJVEFUSVZFX0lORk9STUFUSU9OAEhQRV9JTlZBTElEX1ZFUlNJT04ASFBFX0NCX01FU1NBR0VfQkVHSU4AU0lURV9JU19GUk9aRU4ASFBFX0lOVkFMSURfSEVBREVSX1RPS0VOAElOVkFMSURfVE9LRU4ARk9SQklEREVOAEVOSEFOQ0VfWU9VUl9DQUxNAEhQRV9JTlZBTElEX1VSTABCTE9DS0VEX0JZX1BBUkVOVEFMX0NPTlRST0wATUtDT0wAQUNMAEhQRV9JTlRFUk5BTABSRVFVRVNUX0hFQURFUl9GSUVMRFNfVE9PX0xBUkdFX1VOT0ZGSUNJQUwASFBFX09LAFVOTElOSwBVTkxPQ0sAUFJJAFJFVFJZX1dJVEgASFBFX0lOVkFMSURfQ09OVEVOVF9MRU5HVEgASFBFX1VORVhQRUNURURfQ09OVEVOVF9MRU5HVEgARkxVU0gAUFJPUFBBVENIAE0tU0VBUkNIAFVSSV9UT09fTE9ORwBQUk9DRVNTSU5HAE1JU0NFTExBTkVPVVNfUEVSU0lTVEVOVF9XQVJOSU5HAE1JU0NFTExBTkVPVVNfV0FSTklORwBIUEVfSU5WQUxJRF9UUkFOU0ZFUl9FTkNPRElORwBFeHBlY3RlZCBDUkxGAEhQRV9JTlZBTElEX0NIVU5LX1NJWkUATU9WRQBDT05USU5VRQBIUEVfQ0JfU1RBVFVTX0NPTVBMRVRFAEhQRV9DQl9IRUFERVJTX0NPTVBMRVRFAEhQRV9DQl9WRVJTSU9OX0NPTVBMRVRFAEhQRV9DQl9VUkxfQ09NUExFVEUASFBFX0NCX1BST1RPQ09MX0NPTVBMRVRFAEhQRV9DQl9DSFVOS19DT01QTEVURQBIUEVfQ0JfSEVBREVSX1ZBTFVFX0NPTVBMRVRFAEhQRV9DQl9DSFVOS19FWFRFTlNJT05fVkFMVUVfQ09NUExFVEUASFBFX0NCX0NIVU5LX0VYVEVOU0lPTl9OQU1FX0NPTVBMRVRFAEhQRV9DQl9NRVNTQUdFX0NPTVBMRVRFAEhQRV9DQl9NRVRIT0RfQ09NUExFVEUASFBFX0NCX0hFQURFUl9GSUVMRF9DT01QTEVURQBERUxFVEUASFBFX0lOVkFMSURfRU9GX1NUQVRFAElOVkFMSURfU1NMX0NFUlRJRklDQVRFAFBBVVNFAE5PX1JFU1BPTlNFAFVOU1VQUE9SVEVEX01FRElBX1RZUEUAR09ORQBOT1RfQUNDRVBUQUJMRQBTRVJWSUNFX1VOQVZBSUxBQkxFAFJBTkdFX05PVF9TQVRJU0ZJQUJMRQBPUklHSU5fSVNfVU5SRUFDSEFCTEUAUkVTUE9OU0VfSVNfU1RBTEUAUFVSR0UATUVSR0UAUkVRVUVTVF9IRUFERVJfRklFTERTX1RPT19MQVJHRQBSRVFVRVNUX0hFQURFUl9UT09fTEFSR0UAUEFZTE9BRF9UT09fTEFSR0UASU5TVUZGSUNJRU5UX1NUT1JBR0UASFBFX1BBVVNFRF9VUEdSQURFAEhQRV9QQVVTRURfSDJfVVBHUkFERQBTT1VSQ0UAQU5OT1VOQ0UAVFJBQ0UASFBFX1VORVhQRUNURURfU1BBQ0UAREVTQ1JJQkUAVU5TVUJTQ1JJQkUAUkVDT1JEAEhQRV9JTlZBTElEX01FVEhPRABOT1RfRk9VTkQAUFJPUEZJTkQAVU5CSU5EAFJFQklORABVTkFVVEhPUklaRUQATUVUSE9EX05PVF9BTExPV0VEAEhUVFBfVkVSU0lPTl9OT1RfU1VQUE9SVEVEAEFMUkVBRFlfUkVQT1JURUQAQUNDRVBURUQATk9UX0lNUExFTUVOVEVEAExPT1BfREVURUNURUQASFBFX0NSX0VYUEVDVEVEAEhQRV9MRl9FWFBFQ1RFRABDUkVBVEVEAElNX1VTRUQASFBFX1BBVVNFRABUSU1FT1VUX09DQ1VSRUQAUEFZTUVOVF9SRVFVSVJFRABQUkVDT05ESVRJT05fUkVRVUlSRUQAUFJPWFlfQVVUSEVOVElDQVRJT05fUkVRVUlSRUQATkVUV09SS19BVVRIRU5USUNBVElPTl9SRVFVSVJFRABMRU5HVEhfUkVRVUlSRUQAU1NMX0NFUlRJRklDQVRFX1JFUVVJUkVEAFVQR1JBREVfUkVRVUlSRUQAUEFHRV9FWFBJUkVEAFBSRUNPTkRJVElPTl9GQUlMRUQARVhQRUNUQVRJT05fRkFJTEVEAFJFVkFMSURBVElPTl9GQUlMRUQAU1NMX0hBTkRTSEFLRV9GQUlMRUQATE9DS0VEAFRSQU5TRk9STUFUSU9OX0FQUExJRUQATk9UX01PRElGSUVEAE5PVF9FWFRFTkRFRABCQU5EV0lEVEhfTElNSVRfRVhDRUVERUQAU0lURV9JU19PVkVSTE9BREVEAEhFQUQARXhwZWN0ZWQgSFRUUC8sIFJUU1AvIG9yIElDRS8A5xUAAK8VAACkEgAAkhoAACYWAACeFAAA2xkAAHkVAAB+EgAA/hQAADYVAAALFgAA2BYAAPMSAABCGAAArBYAABIVAAAUFwAA7xcAAEgUAABxFwAAshoAAGsZAAB+GQAANRQAAIIaAABEFwAA/RYAAB4YAACHFwAAqhkAAJMSAAAHGAAALBcAAMoXAACkFwAA5xUAAOcVAABYFwAAOxgAAKASAAAtHAAAwxEAAEgRAADeEgAAQhMAAKQZAAD9EAAA9xUAAKUVAADvFgAA+BkAAEoWAABWFgAA9RUAAAoaAAAIGgAAARoAAKsVAABCEgAA1xAAAEwRAAAFGQAAVBYAAB4RAADKGQAAyBkAAE4WAAD/GAAAcRQAAPAVAADuFQAAlBkAAPwVAAC/GQAAmxkAAHwUAABDEQAAcBgAAJUUAAAnFAAAGRQAANUSAADUGQAARBYAAPcQAEG5OwsBAQBB0DsL4AEBAQIBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEDAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQBBuj0LBAEAAAIAQdE9C14DBAMDAwMDAAADAwADAwADAwMDAwMDAwMDAAUAAAAAAAMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAAAAAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMAAwADAEG6PwsEAQAAAgBB0T8LXgMAAwMDAwMAAAMDAAMDAAMDAwMDAwMDAwMABAAFAAAAAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMAAAADAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwADAAMAQbDBAAsNbG9zZWVlcC1hbGl2ZQBBycEACwEBAEHgwQAL4AEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQBBycMACwEBAEHgwwAL5wEBAQEBAQEBAQEBAQECAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAWNodW5rZWQAQfHFAAteAQABAQEBAQAAAQEAAQEAAQEBAQEBAQEBAQAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEAAQBB0McACyFlY3Rpb25lbnQtbGVuZ3Rob25yb3h5LWNvbm5lY3Rpb24AQYDIAAsgcmFuc2Zlci1lbmNvZGluZ3BncmFkZQ0KDQpTTQ0KDQoAQanIAAsFAQIAAQMAQcDIAAtfBAUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUAQanKAAsFAQIAAQMAQcDKAAtfBAUFBgUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUAQanMAAsEAQAAAQBBwcwAC14CAgACAgICAgICAgICAgICAgICAgICAgICAgICAgIAAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAEGpzgALBQECAAEDAEHAzgALXwQFAAAFBQUFBQUFBQUFBQYFBQUFBQUFBQUFBQUABQAHCAUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQAFAAUABQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUAAAAFAEGp0AALBQEBAAEBAEHA0AALAQEAQdrQAAtBAgAAAAAAAAMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAAAAAAAAAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMAQanSAAsFAQEAAQEAQcDSAAsBAQBBytIACwYCAAAAAAIAQeHSAAs6AwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMAAAAAAAADAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwBBoNQAC50BTk9VTkNFRUNLT1VUTkVDVEVURUNSSUJFTFVTSEVURUFEU0VBUkNIUkdFQ1RJVklUWUxFTkRBUlZFT1RJRllQVElPTlNDSFNFQVlTVEFUQ0hHRVVFUllPUkRJUkVDVE9SVFJDSFBBUkFNRVRFUlVSQ0VCU0NSSUJFQVJET1dOQUNFSU5ETktDS1VCU0NSSUJFVFRQQ0VUU1BBRFRQLw=='
let wasmBuffer
Object.defineProperty(module, 'exports', {
get: () => {
return wasmBuffer
? wasmBuffer
: (wasmBuffer = Buffer.from(wasmBase64, 'base64'))
}
})
================================================
FILE: lib/llhttp/llhttp_simd-wasm.js
================================================
'use strict'
const { Buffer } = require('node:buffer')
const wasmBase64 = 'AGFzbQEAAAABJwdgAX8Bf2ADf39/AX9gAn9/AGABfwBgBH9/f38Bf2AAAGADf39/AALLAQgDZW52GHdhc21fb25faGVhZGVyc19jb21wbGV0ZQAEA2VudhV3YXNtX29uX21lc3NhZ2VfYmVnaW4AAANlbnYLd2FzbV9vbl91cmwAAQNlbnYOd2FzbV9vbl9zdGF0dXMAAQNlbnYUd2FzbV9vbl9oZWFkZXJfZmllbGQAAQNlbnYUd2FzbV9vbl9oZWFkZXJfdmFsdWUAAQNlbnYMd2FzbV9vbl9ib2R5AAEDZW52GHdhc21fb25fbWVzc2FnZV9jb21wbGV0ZQAAAzU0BQYAAAMAAAAAAAADAQMAAwMDAAACAAAAAAICAgICAgICAgIBAQEBAQEBAQEBAwAAAwAAAAQFAXABExMFAwEAAgYIAX8BQcDZBAsHxQcoBm1lbW9yeQIAC19pbml0aWFsaXplAAgZX19pbmRpcmVjdF9mdW5jdGlvbl90YWJsZQEAC2xsaHR0cF9pbml0AAkYbGxodHRwX3Nob3VsZF9rZWVwX2FsaXZlADcMbGxodHRwX2FsbG9jAAsGbWFsbG9jADkLbGxodHRwX2ZyZWUADARmcmVlAAwPbGxodHRwX2dldF90eXBlAA0VbGxodHRwX2dldF9odHRwX21ham9yAA4VbGxodHRwX2dldF9odHRwX21pbm9yAA8RbGxodHRwX2dldF9tZXRob2QAEBZsbGh0dHBfZ2V0X3N0YXR1c19jb2RlABESbGxodHRwX2dldF91cGdyYWRlABIMbGxodHRwX3Jlc2V0ABMObGxodHRwX2V4ZWN1dGUAFBRsbGh0dHBfc2V0dGluZ3NfaW5pdAAVDWxsaHR0cF9maW5pc2gAFgxsbGh0dHBfcGF1c2UAFw1sbGh0dHBfcmVzdW1lABgbbGxodHRwX3Jlc3VtZV9hZnRlcl91cGdyYWRlABkQbGxodHRwX2dldF9lcnJubwAaF2xsaHR0cF9nZXRfZXJyb3JfcmVhc29uABsXbGxodHRwX3NldF9lcnJvcl9yZWFzb24AHBRsbGh0dHBfZ2V0X2Vycm9yX3BvcwAdEWxsaHR0cF9lcnJub19uYW1lAB4SbGxodHRwX21ldGhvZF9uYW1lAB8SbGxodHRwX3N0YXR1c19uYW1lACAabGxodHRwX3NldF9sZW5pZW50X2hlYWRlcnMAISFsbGh0dHBfc2V0X2xlbmllbnRfY2h1bmtlZF9sZW5ndGgAIh1sbGh0dHBfc2V0X2xlbmllbnRfa2VlcF9hbGl2ZQAjJGxsaHR0cF9zZXRfbGVuaWVudF90cmFuc2Zlcl9lbmNvZGluZwAkGmxsaHR0cF9zZXRfbGVuaWVudF92ZXJzaW9uACUjbGxodHRwX3NldF9sZW5pZW50X2RhdGFfYWZ0ZXJfY2xvc2UAJidsbGh0dHBfc2V0X2xlbmllbnRfb3B0aW9uYWxfbGZfYWZ0ZXJfY3IAJyxsbGh0dHBfc2V0X2xlbmllbnRfb3B0aW9uYWxfY3JsZl9hZnRlcl9jaHVuawAoKGxsaHR0cF9zZXRfbGVuaWVudF9vcHRpb25hbF9jcl9iZWZvcmVfbGYAKSpsbGh0dHBfc2V0X2xlbmllbnRfc3BhY2VzX2FmdGVyX2NodW5rX3NpemUAKhhsbGh0dHBfbWVzc2FnZV9uZWVkc19lb2YANgkYAQBBAQsSAQIDBAUKBgcyNDMuKy8tLDAxCuzaAjQWAEHA1QAoAgAEQAALQcDVAEEBNgIACxQAIAAQOCAAIAI2AjggACABOgAoCxQAIAAgAC8BNCAALQAwIAAQNxAACx4BAX9BwAAQOiIBEDggAUGACDYCOCABIAA6ACggAQuPDAEHfwJAIABFDQAgAEEIayIBIABBBGsoAgAiAEF4cSIEaiEFAkAgAEEBcQ0AIABBA3FFDQEgASABKAIAIgBrIgFB1NUAKAIASQ0BIAAgBGohBAJAAkBB2NUAKAIAIAFHBEAgAEH/AU0EQCAAQQN2IQMgASgCCCIAIAEoAgwiAkYEQEHE1QBBxNUAKAIAQX4gA3dxNgIADAULIAIgADYCCCAAIAI2AgwMBAsgASgCGCEGIAEgASgCDCIARwRAIAAgASgCCCICNgIIIAIgADYCDAwDCyABQRRqIgMoAgAiAkUEQCABKAIQIgJFDQIgAUEQaiEDCwNAIAMhByACIgBBFGoiAygCACICDQAgAEEQaiEDIAAoAhAiAg0ACyAHQQA2AgAMAgsgBSgCBCIAQQNxQQNHDQIgBSAAQX5xNgIEQczVACAENgIAIAUgBDYCACABIARBAXI2AgQMAwtBACEACyAGRQ0AAkAgASgCHCICQQJ0QfTXAGoiAygCACABRgRAIAMgADYCACAADQFByNUAQcjVACgCAEF+IAJ3cTYCAAwCCyAGQRBBFCAGKAIQIAFGG2ogADYCACAARQ0BCyAAIAY2AhggASgCECICBEAgACACNgIQIAIgADYCGAsgAUEUaigCACICRQ0AIABBFGogAjYCACACIAA2AhgLIAEgBU8NACAFKAIEIgBBAXFFDQACQAJAAkACQCAAQQJxRQRAQdzVACgCACAFRgRAQdzVACABNgIAQdDVAEHQ1QAoAgAgBGoiADYCACABIABBAXI2AgQgAUHY1QAoAgBHDQZBzNUAQQA2AgBB2NUAQQA2AgAMBgtB2NUAKAIAIAVGBEBB2NUAIAE2AgBBzNUAQczVACgCACAEaiIANgIAIAEgAEEBcjYCBCAAIAFqIAA2AgAMBgsgAEF4cSAEaiEEIABB/wFNBEAgAEEDdiEDIAUoAggiACAFKAIMIgJGBEBBxNUAQcTVACgCAEF+IAN3cTYCAAwFCyACIAA2AgggACACNgIMDAQLIAUoAhghBiAFIAUoAgwiAEcEQEHU1QAoAgAaIAAgBSgCCCICNgIIIAIgADYCDAwDCyAFQRRqIgMoAgAiAkUEQCAFKAIQIgJFDQIgBUEQaiEDCwNAIAMhByACIgBBFGoiAygCACICDQAgAEEQaiEDIAAoAhAiAg0ACyAHQQA2AgAMAgsgBSAAQX5xNgIEIAEgBGogBDYCACABIARBAXI2AgQMAwtBACEACyAGRQ0AAkAgBSgCHCICQQJ0QfTXAGoiAygCACAFRgRAIAMgADYCACAADQFByNUAQcjVACgCAEF+IAJ3cTYCAAwCCyAGQRBBFCAGKAIQIAVGG2ogADYCACAARQ0BCyAAIAY2AhggBSgCECICBEAgACACNgIQIAIgADYCGAsgBUEUaigCACICRQ0AIABBFGogAjYCACACIAA2AhgLIAEgBGogBDYCACABIARBAXI2AgQgAUHY1QAoAgBHDQBBzNUAIAQ2AgAMAQsgBEH/AU0EQCAEQXhxQezVAGohAAJ/QcTVACgCACICQQEgBEEDdnQiA3FFBEBBxNUAIAIgA3I2AgAgAAwBCyAAKAIICyICIAE2AgwgACABNgIIIAEgADYCDCABIAI2AggMAQtBHyECIARB////B00EQCAEQSYgBEEIdmciAGt2QQFxIABBAXRrQT5qIQILIAEgAjYCHCABQgA3AhAgAkECdEH01wBqIQACQEHI1QAoAgAiA0EBIAJ0IgdxRQRAIAAgATYCAEHI1QAgAyAHcjYCACABIAA2AhggASABNgIIIAEgATYCDAwBCyAEQRkgAkEBdmtBACACQR9HG3QhAiAAKAIAIQACQANAIAAiAygCBEF4cSAERg0BIAJBHXYhACACQQF0IQIgAyAAQQRxakEQaiIHKAIAIgANAAsgByABNgIAIAEgAzYCGCABIAE2AgwgASABNgIIDAELIAMoAggiACABNgIMIAMgATYCCCABQQA2AhggASADNgIMIAEgADYCCAtB5NUAQeTVACgCAEEBayIAQX8gABs2AgALCwcAIAAtACgLBwAgAC0AKgsHACAALQArCwcAIAAtACkLBwAgAC8BNAsHACAALQAwC0ABBH8gACgCGCEBIAAvAS4hAiAALQAoIQMgACgCOCEEIAAQOCAAIAQ2AjggACADOgAoIAAgAjsBLiAAIAE2AhgLhocCAwd/A34BeyABIAJqIQQCQCAAIgMoAgwiAA0AIAMoAgQEQCADIAE2AgQLIwBBEGsiCSQAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACfwJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQCADKAIcIgJBAmsO/AEB+QECAwQFBgcICQoLDA0ODxAREvgBE/cBFBX2ARYX9QEYGRobHB0eHyD9AfsBIfQBIiMkJSYnKCkqK/MBLC0uLzAxMvIB8QEzNPAB7wE1Njc4OTo7PD0+P0BBQkNERUZHSElKS0xNTk/6AVBRUlPuAe0BVOwBVesBVldYWVrqAVtcXV5fYGFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6e3x9fn+AAYEBggGDAYQBhQGGAYcBiAGJAYoBiwGMAY0BjgGPAZABkQGSAZMBlAGVAZYBlwGYAZkBmgGbAZwBnQGeAZ8BoAGhAaIBowGkAaUBpgGnAagBqQGqAasBrAGtAa4BrwGwAbEBsgGzAbQBtQG2AbcBuAG5AboBuwG8Ab0BvgG/AcABwQHCAcMBxAHFAcYBxwHIAckBygHLAcwBzQHOAekB6AHPAecB0AHmAdEB0gHTAdQB5QHVAdYB1wHYAdkB2gHbAdwB3QHeAd8B4AHhAeIB4wEA/AELQQAM4wELQQ4M4gELQQ0M4QELQQ8M4AELQRAM3wELQRMM3gELQRQM3QELQRUM3AELQRYM2wELQRcM2gELQRgM2QELQRkM2AELQRoM1wELQRsM1gELQRwM1QELQR0M1AELQR4M0wELQR8M0gELQSAM0QELQSEM0AELQQgMzwELQSIMzgELQSQMzQELQSMMzAELQQcMywELQSUMygELQSYMyQELQScMyAELQSgMxwELQRIMxgELQREMxQELQSkMxAELQSoMwwELQSsMwgELQSwMwQELQd4BDMABC0EuDL8BC0EvDL4BC0EwDL0BC0ExDLwBC0EyDLsBC0EzDLoBC0E0DLkBC0HfAQy4AQtBNQy3AQtBOQy2AQtBDAy1AQtBNgy0AQtBNwyzAQtBOAyyAQtBPgyxAQtBOgywAQtB4AEMrwELQQsMrgELQT8MrQELQTsMrAELQQoMqwELQTwMqgELQT0MqQELQeEBDKgBC0HBAAynAQtBwAAMpgELQcIADKUBC0EJDKQBC0EtDKMBC0HDAAyiAQtBxAAMoQELQcUADKABC0HGAAyfAQtBxwAMngELQcgADJ0BC0HJAAycAQtBygAMmwELQcsADJoBC0HMAAyZAQtBzQAMmAELQc4ADJcBC0HPAAyWAQtB0AAMlQELQdEADJQBC0HSAAyTAQtB0wAMkgELQdUADJEBC0HUAAyQAQtB1gAMjwELQdcADI4BC0HYAAyNAQtB2QAMjAELQdoADIsBC0HbAAyKAQtB3AAMiQELQd0ADIgBC0HeAAyHAQtB3wAMhgELQeAADIUBC0HhAAyEAQtB4gAMgwELQeMADIIBC0HkAAyBAQtB5QAMgAELQeIBDH8LQeYADH4LQecADH0LQQYMfAtB6AAMewtBBQx6C0HpAAx5C0EEDHgLQeoADHcLQesADHYLQewADHULQe0ADHQLQQMMcwtB7gAMcgtB7wAMcQtB8AAMcAtB8gAMbwtB8QAMbgtB8wAMbQtB9AAMbAtB9QAMawtB9gAMagtBAgxpC0H3AAxoC0H4AAxnC0H5AAxmC0H6AAxlC0H7AAxkC0H8AAxjC0H9AAxiC0H+AAxhC0H/AAxgC0GAAQxfC0GBAQxeC0GCAQxdC0GDAQxcC0GEAQxbC0GFAQxaC0GGAQxZC0GHAQxYC0GIAQxXC0GJAQxWC0GKAQxVC0GLAQxUC0GMAQxTC0GNAQxSC0GOAQxRC0GPAQxQC0GQAQxPC0GRAQxOC0GSAQxNC0GTAQxMC0GUAQxLC0GVAQxKC0GWAQxJC0GXAQxIC0GYAQxHC0GZAQxGC0GaAQxFC0GbAQxEC0GcAQxDC0GdAQxCC0GeAQxBC0GfAQxAC0GgAQw/C0GhAQw+C0GiAQw9C0GjAQw8C0GkAQw7C0GlAQw6C0GmAQw5C0GnAQw4C0GoAQw3C0GpAQw2C0GqAQw1C0GrAQw0C0GsAQwzC0GtAQwyC0GuAQwxC0GvAQwwC0GwAQwvC0GxAQwuC0GyAQwtC0GzAQwsC0G0AQwrC0G1AQwqC0G2AQwpC0G3AQwoC0G4AQwnC0G5AQwmC0G6AQwlC0G7AQwkC0G8AQwjC0G9AQwiC0G+AQwhC0G/AQwgC0HAAQwfC0HBAQweC0HCAQwdC0EBDBwLQcMBDBsLQcQBDBoLQcUBDBkLQcYBDBgLQccBDBcLQcgBDBYLQckBDBULQcoBDBQLQcsBDBMLQcwBDBILQc0BDBELQc4BDBALQc8BDA8LQdABDA4LQdEBDA0LQdIBDAwLQdMBDAsLQdQBDAoLQdUBDAkLQdYBDAgLQeMBDAcLQdcBDAYLQdgBDAULQdkBDAQLQdoBDAMLQdsBDAILQd0BDAELQdwBCyECA0ACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAIAMCfwJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACfwJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACfwJAAkACQAJAAkACQAJAAn8CQAJAAkACfwJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkAgAwJ/AkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJ/AkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQCACDuMBAAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8gISMkJScoKZ4DmwOaA5EDigODA4AD/QL7AvgC8gLxAu8C7QLoAucC5gLlAuQC3ALbAtoC2QLYAtcC1gLVAs8CzgLMAssCygLJAsgCxwLGAsQCwwK+ArwCugK5ArgCtwK2ArUCtAKzArICsQKwAq4CrQKpAqgCpwKmAqUCpAKjAqICoQKgAp8CmAKQAowCiwKKAoEC/gH9AfwB+wH6AfkB+AH3AfUB8wHwAesB6QHoAecB5gHlAeQB4wHiAeEB4AHfAd4B3QHcAdoB2QHYAdcB1gHVAdQB0wHSAdEB0AHPAc4BzQHMAcsBygHJAcgBxwHGAcUBxAHDAcIBwQHAAb8BvgG9AbwBuwG6AbkBuAG3AbYBtQG0AbMBsgGxAbABrwGuAa0BrAGrAaoBqQGoAacBpgGlAaQBowGiAZ8BngGZAZgBlwGWAZUBlAGTAZIBkQGQAY8BjQGMAYcBhgGFAYQBgwGCAX18e3p5dnV0UFFSU1RVCyABIARHDXJB/QEhAgy+AwsgASAERw2YAUHbASECDL0DCyABIARHDfEBQY4BIQIMvAMLIAEgBEcN/AFBhAEhAgy7AwsgASAERw2KAkH/ACECDLoDCyABIARHDZECQf0AIQIMuQMLIAEgBEcNlAJB+wAhAgy4AwsgASAERw0eQR4hAgy3AwsgASAERw0ZQRghAgy2AwsgASAERw3KAkHNACECDLUDCyABIARHDdUCQcYAIQIMtAMLIAEgBEcN1gJBwwAhAgyzAwsgASAERw3cAkE4IQIMsgMLIAMtADBBAUYNrQMMiQMLQQAhAAJAAkACQCADLQAqRQ0AIAMtACtFDQAgAy8BMiICQQJxRQ0BDAILIAMvATIiAkEBcUUNAQtBASEAIAMtAChBAUYNACADLwE0IgZB5ABrQeQASQ0AIAZBzAFGDQAgBkGwAkYNACACQcAAcQ0AQQAhACACQYgEcUGABEYNACACQShxQQBHIQALIANBADsBMiADQQA6ADECQCAARQRAIANBADoAMSADLQAuQQRxDQEMsQMLIANCADcDIAsgA0EAOgAxIANBAToANgxIC0EAIQACQCADKAI4IgJFDQAgAigCMCICRQ0AIAMgAhEAACEACyAARQ1IIABBFUcNYiADQQQ2AhwgAyABNgIUIANB0hs2AhAgA0EVNgIMQQAhAgyvAwsgASAERgRAQQYhAgyvAwsgAS0AAEEKRw0ZIAFBAWohAQwaCyADQgA3AyBBEiECDJQDCyABIARHDYoDQSMhAgysAwsgASAERgRAQQchAgysAwsCQAJAIAEtAABBCmsOBAEYGAAYCyABQQFqIQFBECECDJMDCyABQQFqIQEgA0Evai0AAEEBcQ0XQQAhAiADQQA2AhwgAyABNgIUIANBmSA2AhAgA0EZNgIMDKsDCyADIAMpAyAiDCAEIAFrrSIKfSILQgAgCyAMWBs3AyAgCiAMWg0YQQghAgyqAwsgASAERwRAIANBCTYCCCADIAE2AgRBFCECDJEDC0EJIQIMqQMLIAMpAyBQDa4CDEMLIAEgBEYEQEELIQIMqAMLIAEtAABBCkcNFiABQQFqIQEMFwsgA0Evai0AAEEBcUUNGQwmC0EAIQACQCADKAI4IgJFDQAgAigCUCICRQ0AIAMgAhEAACEACyAADRkMQgtBACEAAkAgAygCOCICRQ0AIAIoAlAiAkUNACADIAIRAAAhAAsgAA0aDCQLQQAhAAJAIAMoAjgiAkUNACACKAJQIgJFDQAgAyACEQAAIQALIAANGwwyCyADQS9qLQAAQQFxRQ0cDCILQQAhAAJAIAMoAjgiAkUNACACKAJUIgJFDQAgAyACEQAAIQALIAANHAxCC0EAIQACQCADKAI4IgJFDQAgAigCVCICRQ0AIAMgAhEAACEACyAADR0MIAsgASAERgRAQRMhAgygAwsCQCABLQAAIgBBCmsOBB8jIwAiCyABQQFqIQEMHwtBACEAAkAgAygCOCICRQ0AIAIoAlQiAkUNACADIAIRAAAhAAsgAA0iDEILIAEgBEYEQEEWIQIMngMLIAEtAABBwMEAai0AAEEBRw0jDIMDCwJAA0AgAS0AAEGwO2otAAAiAEEBRwRAAkAgAEECaw4CAwAnCyABQQFqIQFBISECDIYDCyAEIAFBAWoiAUcNAAtBGCECDJ0DCyADKAIEIQBBACECIANBADYCBCADIAAgAUEBaiIBEDQiAA0hDEELQQAhAAJAIAMoAjgiAkUNACACKAJUIgJFDQAgAyACEQAAIQALIAANIwwqCyABIARGBEBBHCECDJsDCyADQQo2AgggAyABNgIEQQAhAAJAIAMoAjgiAkUNACACKAJQIgJFDQAgAyACEQAAIQALIAANJUEkIQIMgQMLIAEgBEcEQANAIAEtAABBsD1qLQAAIgBBA0cEQCAAQQFrDgUYGiaCAyUmCyAEIAFBAWoiAUcNAAtBGyECDJoDC0EbIQIMmQMLA0AgAS0AAEGwP2otAAAiAEEDRwRAIABBAWsOBQ8RJxMmJwsgBCABQQFqIgFHDQALQR4hAgyYAwsgASAERwRAIANBCzYCCCADIAE2AgRBByECDP8CC0EfIQIMlwMLIAEgBEYEQEEgIQIMlwMLAkAgAS0AAEENaw4ULj8/Pz8/Pz8/Pz8/Pz8/Pz8/PwA/C0EAIQIgA0EANgIcIANBvws2AhAgA0ECNgIMIAMgAUEBajYCFAyWAwsgA0EvaiECA0AgASAERgRAQSEhAgyXAwsCQAJAAkAgAS0AACIAQQlrDhgCACkpASkpKSkpKSkpKSkpKSkpKSkpKQInCyABQQFqIQEgA0Evai0AAEEBcUUNCgwYCyABQQFqIQEMFwsgAUEBaiEBIAItAABBAnENAAtBACECIANBADYCHCADIAE2AhQgA0GfFTYCECADQQw2AgwMlQMLIAMtAC5BgAFxRQ0BC0EAIQACQCADKAI4IgJFDQAgAigCXCICRQ0AIAMgAhEAACEACyAARQ3mAiAAQRVGBEAgA0EkNgIcIAMgATYCFCADQZsbNgIQIANBFTYCDEEAIQIMlAMLQQAhAiADQQA2AhwgAyABNgIUIANBkA42AhAgA0EUNgIMDJMDC0EAIQIgA0EANgIcIAMgATYCFCADQb4gNgIQIANBAjYCDAySAwsgAygCBCEAQQAhAiADQQA2AgQgAyAAIAEgDKdqIgEQMiIARQ0rIANBBzYCHCADIAE2AhQgAyAANgIMDJEDCyADLQAuQcAAcUUNAQtBACEAAkAgAygCOCICRQ0AIAIoAlgiAkUNACADIAIRAAAhAAsgAEUNKyAAQRVGBEAgA0EKNgIcIAMgATYCFCADQesZNgIQIANBFTYCDEEAIQIMkAMLQQAhAiADQQA2AhwgAyABNgIUIANBkww2AhAgA0ETNgIMDI8DC0EAIQIgA0EANgIcIAMgATYCFCADQYIVNgIQIANBAjYCDAyOAwtBACECIANBADYCHCADIAE2AhQgA0HdFDYCECADQRk2AgwMjQMLQQAhAiADQQA2AhwgAyABNgIUIANB5h02AhAgA0EZNgIMDIwDCyAAQRVGDT1BACECIANBADYCHCADIAE2AhQgA0HQDzYCECADQSI2AgwMiwMLIAMoAgQhAEEAIQIgA0EANgIEIAMgACABEDMiAEUNKCADQQ02AhwgAyABNgIUIAMgADYCDAyKAwsgAEEVRg06QQAhAiADQQA2AhwgAyABNgIUIANB0A82AhAgA0EiNgIMDIkDCyADKAIEIQBBACECIANBADYCBCADIAAgARAzIgBFBEAgAUEBaiEBDCgLIANBDjYCHCADIAA2AgwgAyABQQFqNgIUDIgDCyAAQRVGDTdBACECIANBADYCHCADIAE2AhQgA0HQDzYCECADQSI2AgwMhwMLIAMoAgQhAEEAIQIgA0EANgIEIAMgACABEDMiAEUEQCABQQFqIQEMJwsgA0EPNgIcIAMgADYCDCADIAFBAWo2AhQMhgMLQQAhAiADQQA2AhwgAyABNgIUIANB4hc2AhAgA0EZNgIMDIUDCyAAQRVGDTNBACECIANBADYCHCADIAE2AhQgA0HWDDYCECADQSM2AgwMhAMLIAMoAgQhAEEAIQIgA0EANgIEIAMgACABEDQiAEUNJSADQRE2AhwgAyABNgIUIAMgADYCDAyDAwsgAEEVRg0wQQAhAiADQQA2AhwgAyABNgIUIANB1gw2AhAgA0EjNgIMDIIDCyADKAIEIQBBACECIANBADYCBCADIAAgARA0IgBFBEAgAUEBaiEBDCULIANBEjYCHCADIAA2AgwgAyABQQFqNgIUDIEDCyADQS9qLQAAQQFxRQ0BC0EXIQIM5gILQQAhAiADQQA2AhwgAyABNgIUIANB4hc2AhAgA0EZNgIMDP4CCyAAQTtHDQAgAUEBaiEBDAwLQQAhAiADQQA2AhwgAyABNgIUIANBkhg2AhAgA0ECNgIMDPwCCyAAQRVGDShBACECIANBADYCHCADIAE2AhQgA0HWDDYCECADQSM2AgwM+wILIANBFDYCHCADIAE2AhQgAyAANgIMDPoCCyADKAIEIQBBACECIANBADYCBCADIAAgARA0IgBFBEAgAUEBaiEBDPUCCyADQRU2AhwgAyAANgIMIAMgAUEBajYCFAz5AgsgAygCBCEAQQAhAiADQQA2AgQgAyAAIAEQNCIARQRAIAFBAWohAQzzAgsgA0EXNgIcIAMgADYCDCADIAFBAWo2AhQM+AILIABBFUYNI0EAIQIgA0EANgIcIAMgATYCFCADQdYMNgIQIANBIzYCDAz3AgsgAygCBCEAQQAhAiADQQA2AgQgAyAAIAEQNCIARQRAIAFBAWohAQwdCyADQRk2AhwgAyAANgIMIAMgAUEBajYCFAz2AgsgAygCBCEAQQAhAiADQQA2AgQgAyAAIAEQNCIARQRAIAFBAWohAQzvAgsgA0EaNgIcIAMgADYCDCADIAFBAWo2AhQM9QILIABBFUYNH0EAIQIgA0EANgIcIAMgATYCFCADQdAPNgIQIANBIjYCDAz0AgsgAygCBCEAIANBADYCBCADIAAgARAzIgBFBEAgAUEBaiEBDBsLIANBHDYCHCADIAA2AgwgAyABQQFqNgIUQQAhAgzzAgsgAygCBCEAIANBADYCBCADIAAgARAzIgBFBEAgAUEBaiEBDOsCCyADQR02AhwgAyAANgIMIAMgAUEBajYCFEEAIQIM8gILIABBO0cNASABQQFqIQELQSYhAgzXAgtBACECIANBADYCHCADIAE2AhQgA0GfFTYCECADQQw2AgwM7wILIAEgBEcEQANAIAEtAABBIEcNhAIgBCABQQFqIgFHDQALQSwhAgzvAgtBLCECDO4CCyABIARGBEBBNCECDO4CCwJAAkADQAJAIAEtAABBCmsOBAIAAAMACyAEIAFBAWoiAUcNAAtBNCECDO8CCyADKAIEIQAgA0EANgIEIAMgACABEDEiAEUNnwIgA0EyNgIcIAMgATYCFCADIAA2AgxBACECDO4CCyADKAIEIQAgA0EANgIEIAMgACABEDEiAEUEQCABQQFqIQEMnwILIANBMjYCHCADIAA2AgwgAyABQQFqNgIUQQAhAgztAgsgASAERwRAAkADQCABLQAAQTBrIgBB/wFxQQpPBEBBOiECDNcCCyADKQMgIgtCmbPmzJmz5swZVg0BIAMgC0IKfiIKNwMgIAogAK1C/wGDIgtCf4VWDQEgAyAKIAt8NwMgIAQgAUEBaiIBRw0AC0HAACECDO4CCyADKAIEIQAgA0EANgIEIAMgACABQQFqIgEQMSIADRcM4gILQcAAIQIM7AILIAEgBEYEQEHJACECDOwCCwJAA0ACQCABLQAAQQlrDhgAAqICogKpAqICogKiAqICogKiAqICogKiAqICogKiAqICogKiAqICogKiAgCiAgsgBCABQQFqIgFHDQALQckAIQIM7AILIAFBAWohASADQS9qLQAAQQFxDaUCIANBADYCHCADIAE2AhQgA0GXEDYCECADQQo2AgxBACECDOsCCyABIARHBEADQCABLQAAQSBHDRUgBCABQQFqIgFHDQALQfgAIQIM6wILQfgAIQIM6gILIANBAjoAKAw4C0EAIQIgA0EANgIcIANBvws2AhAgA0ECNgIMIAMgAUEBajYCFAzoAgtBACECDM4CC0ENIQIMzQILQRMhAgzMAgtBFSECDMsCC0EWIQIMygILQRghAgzJAgtBGSECDMgCC0EaIQIMxwILQRshAgzGAgtBHCECDMUCC0EdIQIMxAILQR4hAgzDAgtBHyECDMICC0EgIQIMwQILQSIhAgzAAgtBIyECDL8CC0ElIQIMvgILQeUAIQIMvQILIANBPTYCHCADIAE2AhQgAyAANgIMQQAhAgzVAgsgA0EbNgIcIAMgATYCFCADQaQcNgIQIANBFTYCDEEAIQIM1AILIANBIDYCHCADIAE2AhQgA0GYGjYCECADQRU2AgxBACECDNMCCyADQRM2AhwgAyABNgIUIANBmBo2AhAgA0EVNgIMQQAhAgzSAgsgA0ELNgIcIAMgATYCFCADQZgaNgIQIANBFTYCDEEAIQIM0QILIANBEDYCHCADIAE2AhQgA0GYGjYCECADQRU2AgxBACECDNACCyADQSA2AhwgAyABNgIUIANBpBw2AhAgA0EVNgIMQQAhAgzPAgsgA0ELNgIcIAMgATYCFCADQaQcNgIQIANBFTYCDEEAIQIMzgILIANBDDYCHCADIAE2AhQgA0GkHDYCECADQRU2AgxBACECDM0CC0EAIQIgA0EANgIcIAMgATYCFCADQd0ONgIQIANBEjYCDAzMAgsCQANAAkAgAS0AAEEKaw4EAAICAAILIAQgAUEBaiIBRw0AC0H9ASECDMwCCwJAAkAgAy0ANkEBRw0AQQAhAAJAIAMoAjgiAkUNACACKAJgIgJFDQAgAyACEQAAIQALIABFDQAgAEEVRw0BIANB/AE2AhwgAyABNgIUIANB3Bk2AhAgA0EVNgIMQQAhAgzNAgtB3AEhAgyzAgsgA0EANgIcIAMgATYCFCADQfkLNgIQIANBHzYCDEEAIQIMywILAkACQCADLQAoQQFrDgIEAQALQdsBIQIMsgILQdQBIQIMsQILIANBAjoAMUEAIQACQCADKAI4IgJFDQAgAigCACICRQ0AIAMgAhEAACEACyAARQRAQd0BIQIMsQILIABBFUcEQCADQQA2AhwgAyABNgIUIANBtAw2AhAgA0EQNgIMQQAhAgzKAgsgA0H7ATYCHCADIAE2AhQgA0GBGjYCECADQRU2AgxBACECDMkCCyABIARGBEBB+gEhAgzJAgsgAS0AAEHIAEYNASADQQE6ACgLQcABIQIMrgILQdoBIQIMrQILIAEgBEcEQCADQQw2AgggAyABNgIEQdkBIQIMrQILQfkBIQIMxQILIAEgBEYEQEH4ASECDMUCCyABLQAAQcgARw0EIAFBAWohAUHYASECDKsCCyABIARGBEBB9wEhAgzEAgsCQAJAIAEtAABBxQBrDhAABQUFBQUFBQUFBQUFBQUBBQsgAUEBaiEBQdYBIQIMqwILIAFBAWohAUHXASECDKoCC0H2ASECIAEgBEYNwgIgAygCACIAIAQgAWtqIQUgASAAa0ECaiEGAkADQCABLQAAIABButUAai0AAEcNAyAAQQJGDQEgAEEBaiEAIAQgAUEBaiIBRw0ACyADIAU2AgAMwwILIAMoAgQhACADQgA3AwAgAyAAIAZBAWoiARAuIgBFBEBB4wEhAgyqAgsgA0H1ATYCHCADIAE2AhQgAyAANgIMQQAhAgzCAgtB9AEhAiABIARGDcECIAMoAgAiACAEIAFraiEFIAEgAGtBAWohBgJAA0AgAS0AACAAQbjVAGotAABHDQIgAEEBRg0BIABBAWohACAEIAFBAWoiAUcNAAsgAyAFNgIADMICCyADQYEEOwEoIAMoAgQhACADQgA3AwAgAyAAIAZBAWoiARAuIgANAwwCCyADQQA2AgALQQAhAiADQQA2AhwgAyABNgIUIANB5R82AhAgA0EINgIMDL8CC0HVASECDKUCCyADQfMBNgIcIAMgATYCFCADIAA2AgxBACECDL0CC0EAIQACQCADKAI4IgJFDQAgAigCQCICRQ0AIAMgAhEAACEACyAARQ1uIABBFUcEQCADQQA2AhwgAyABNgIUIANBgg82AhAgA0EgNgIMQQAhAgy9AgsgA0GPATYCHCADIAE2AhQgA0HsGzYCECADQRU2AgxBACECDLwCCyABIARHBEAgA0ENNgIIIAMgATYCBEHTASECDKMCC0HyASECDLsCCyABIARGBEBB8QEhAgy7AgsCQAJAAkAgAS0AAEHIAGsOCwABCAgICAgICAgCCAsgAUEBaiEBQdABIQIMowILIAFBAWohAUHRASECDKICCyABQQFqIQFB0gEhAgyhAgtB8AEhAiABIARGDbkCIAMoAgAiACAEIAFraiEGIAEgAGtBAmohBQNAIAEtAAAgAEG11QBqLQAARw0EIABBAkYNAyAAQQFqIQAgBCABQQFqIgFHDQALIAMgBjYCAAy5AgtB7wEhAiABIARGDbgCIAMoAgAiACAEIAFraiEGIAEgAGtBAWohBQNAIAEtAAAgAEGz1QBqLQAARw0DIABBAUYNAiAAQQFqIQAgBCABQQFqIgFHDQALIAMgBjYCAAy4AgtB7gEhAiABIARGDbcCIAMoAgAiACAEIAFraiEGIAEgAGtBAmohBQNAIAEtAAAgAEGw1QBqLQAARw0CIABBAkYNASAAQQFqIQAgBCABQQFqIgFHDQALIAMgBjYCAAy3AgsgAygCBCEAIANCADcDACADIAAgBUEBaiIBECsiAEUNAiADQewBNgIcIAMgATYCFCADIAA2AgxBACECDLYCCyADQQA2AgALIAMoAgQhACADQQA2AgQgAyAAIAEQKyIARQ2cAiADQe0BNgIcIAMgATYCFCADIAA2AgxBACECDLQCC0HPASECDJoCC0EAIQACQCADKAI4IgJFDQAgAigCNCICRQ0AIAMgAhEAACEACwJAIAAEQCAAQRVGDQEgA0EANgIcIAMgATYCFCADQeoNNgIQIANBJjYCDEEAIQIMtAILQc4BIQIMmgILIANB6wE2AhwgAyABNgIUIANBgBs2AhAgA0EVNgIMQQAhAgyyAgsgASAERgRAQesBIQIMsgILIAEtAABBL0YEQCABQQFqIQEMAQsgA0EANgIcIAMgATYCFCADQbI4NgIQIANBCDYCDEEAIQIMsQILQc0BIQIMlwILIAEgBEcEQCADQQ42AgggAyABNgIEQcwBIQIMlwILQeoBIQIMrwILIAEgBEYEQEHpASECDK8CCyABLQAAQTBrIgBB/wFxQQpJBEAgAyAAOgAqIAFBAWohAUHLASECDJYCCyADKAIEIQAgA0EANgIEIAMgACABEC8iAEUNlwIgA0HoATYCHCADIAE2AhQgAyAANgIMQQAhAgyuAgsgASAERgRAQecBIQIMrgILAkAgAS0AAEEuRgRAIAFBAWohAQwBCyADKAIEIQAgA0EANgIEIAMgACABEC8iAEUNmAIgA0HmATYCHCADIAE2AhQgAyAANgIMQQAhAgyuAgtBygEhAgyUAgsgASAERgRAQeUBIQIMrQILQQAhAEEBIQVBASEHQQAhAgJAAkACQAJAAkACfwJAAkACQAJAAkACQAJAIAEtAABBMGsOCgoJAAECAwQFBggLC0ECDAYLQQMMBQtBBAwEC0EFDAMLQQYMAgtBBwwBC0EICyECQQAhBUEAIQcMAgtBCSECQQEhAEEAIQVBACEHDAELQQAhBUEBIQILIAMgAjoAKyABQQFqIQECQAJAIAMtAC5BEHENAAJAAkACQCADLQAqDgMBAAIECyAHRQ0DDAILIAANAQwCCyAFRQ0BCyADKAIEIQAgA0EANgIEIAMgACABEC8iAEUNAiADQeIBNgIcIAMgATYCFCADIAA2AgxBACECDK8CCyADKAIEIQAgA0EANgIEIAMgACABEC8iAEUNmgIgA0HjATYCHCADIAE2AhQgAyAANgIMQQAhAgyuAgsgAygCBCEAIANBADYCBCADIAAgARAvIgBFDZgCIANB5AE2AhwgAyABNgIUIAMgADYCDAytAgtByQEhAgyTAgtBACEAAkAgAygCOCICRQ0AIAIoAkQiAkUNACADIAIRAAAhAAsCQCAABEAgAEEVRg0BIANBADYCHCADIAE2AhQgA0GkDTYCECADQSE2AgxBACECDK0CC0HIASECDJMCCyADQeEBNgIcIAMgATYCFCADQdAaNgIQIANBFTYCDEEAIQIMqwILIAEgBEYEQEHhASECDKsCCwJAIAEtAABBIEYEQCADQQA7ATQgAUEBaiEBDAELIANBADYCHCADIAE2AhQgA0GZETYCECADQQk2AgxBACECDKsCC0HHASECDJECCyABIARGBEBB4AEhAgyqAgsCQCABLQAAQTBrQf8BcSICQQpJBEAgAUEBaiEBAkAgAy8BNCIAQZkzSw0AIAMgAEEKbCIAOwE0IABB/v8DcSACQf//A3NLDQAgAyAAIAJqOwE0DAILQQAhAiADQQA2AhwgAyABNgIUIANBlR42AhAgA0ENNgIMDKsCCyADQQA2AhwgAyABNgIUIANBlR42AhAgA0ENNgIMQQAhAgyqAgtBxgEhAgyQAgsgASAERgRAQd8BIQIMqQILAkAgAS0AAEEwa0H/AXEiAkEKSQRAIAFBAWohAQJAIAMvATQiAEGZM0sNACADIABBCmwiADsBNCAAQf7/A3EgAkH//wNzSw0AIAMgACACajsBNAwCC0EAIQIgA0EANgIcIAMgATYCFCADQZUeNgIQIANBDTYCDAyqAgsgA0EANgIcIAMgATYCFCADQZUeNgIQIANBDTYCDEEAIQIMqQILQcUBIQIMjwILIAEgBEYEQEHeASECDKgCCwJAIAEtAABBMGtB/wFxIgJBCkkEQCABQQFqIQECQCADLwE0IgBBmTNLDQAgAyAAQQpsIgA7ATQgAEH+/wNxIAJB//8Dc0sNACADIAAgAmo7ATQMAgtBACECIANBADYCHCADIAE2AhQgA0GVHjYCECADQQ02AgwMqQILIANBADYCHCADIAE2AhQgA0GVHjYCECADQQ02AgxBACECDKgCC0HEASECDI4CCyABIARGBEBB3QEhAgynAgsCQAJAAkACQCABLQAAQQprDhcCAwMAAwMDAwMDAwMDAwMDAwMDAwMDAQMLIAFBAWoMBQsgAUEBaiEBQcMBIQIMjwILIAFBAWohASADQS9qLQAAQQFxDQggA0EANgIcIAMgATYCFCADQY0LNgIQIANBDTYCDEEAIQIMpwILIANBADYCHCADIAE2AhQgA0GNCzYCECADQQ02AgxBACECDKYCCyABIARHBEAgA0EPNgIIIAMgATYCBEEBIQIMjQILQdwBIQIMpQILAkACQANAAkAgAS0AAEEKaw4EAgAAAwALIAQgAUEBaiIBRw0AC0HbASECDKYCCyADKAIEIQAgA0EANgIEIAMgACABEC0iAEUEQCABQQFqIQEMBAsgA0HaATYCHCADIAA2AgwgAyABQQFqNgIUQQAhAgylAgsgAygCBCEAIANBADYCBCADIAAgARAtIgANASABQQFqCyEBQcEBIQIMigILIANB2QE2AhwgAyAANgIMIAMgAUEBajYCFEEAIQIMogILQcIBIQIMiAILIANBL2otAABBAXENASADQQA2AhwgAyABNgIUIANB5Bw2AhAgA0EZNgIMQQAhAgygAgsgASAERgRAQdkBIQIMoAILAkACQAJAIAEtAABBCmsOBAECAgACCyABQQFqIQEMAgsgAUEBaiEBDAELIAMtAC5BwABxRQ0BC0EAIQACQCADKAI4IgJFDQAgAigCPCICRQ0AIAMgAhEAACEACyAARQ2gASAAQRVGBEAgA0HZADYCHCADIAE2AhQgA0G3GjYCECADQRU2AgxBACECDJ8CCyADQQA2AhwgAyABNgIUIANBgA02AhAgA0EbNgIMQQAhAgyeAgsgA0EANgIcIAMgATYCFCADQdwoNgIQIANBAjYCDEEAIQIMnQILIAEgBEcEQCADQQw2AgggAyABNgIEQb8BIQIMhAILQdgBIQIMnAILIAEgBEYEQEHXASECDJwCCwJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkAgAS0AAEHBAGsOFQABAgNaBAUGWlpaBwgJCgsMDQ4PEFoLIAFBAWohAUH7ACECDJICCyABQQFqIQFB/AAhAgyRAgsgAUEBaiEBQYEBIQIMkAILIAFBAWohAUGFASECDI8CCyABQQFqIQFBhgEhAgyOAgsgAUEBaiEBQYkBIQIMjQILIAFBAWohAUGKASECDIwCCyABQQFqIQFBjQEhAgyLAgsgAUEBaiEBQZYBIQIMigILIAFBAWohAUGXASECDIkCCyABQQFqIQFBmAEhAgyIAgsgAUEBaiEBQaUBIQIMhwILIAFBAWohAUGmASECDIYCCyABQQFqIQFBrAEhAgyFAgsgAUEBaiEBQbQBIQIMhAILIAFBAWohAUG3ASECDIMCCyABQQFqIQFBvgEhAgyCAgsgASAERgRAQdYBIQIMmwILIAEtAABBzgBHDUggAUEBaiEBQb0BIQIMgQILIAEgBEYEQEHVASECDJoCCwJAAkACQCABLQAAQcIAaw4SAEpKSkpKSkpKSgFKSkpKSkoCSgsgAUEBaiEBQbgBIQIMggILIAFBAWohAUG7ASECDIECCyABQQFqIQFBvAEhAgyAAgtB1AEhAiABIARGDZgCIAMoAgAiACAEIAFraiEFIAEgAGtBB2ohBgJAA0AgAS0AACAAQajVAGotAABHDUUgAEEHRg0BIABBAWohACAEIAFBAWoiAUcNAAsgAyAFNgIADJkCCyADQQA2AgAgBkEBaiEBQRsMRQsgASAERgRAQdMBIQIMmAILAkACQCABLQAAQckAaw4HAEdHR0dHAUcLIAFBAWohAUG5ASECDP8BCyABQQFqIQFBugEhAgz+AQtB0gEhAiABIARGDZYCIAMoAgAiACAEIAFraiEFIAEgAGtBAWohBgJAA0AgAS0AACAAQabVAGotAABHDUMgAEEBRg0BIABBAWohACAEIAFBAWoiAUcNAAsgAyAFNgIADJcCCyADQQA2AgAgBkEBaiEBQQ8MQwtB0QEhAiABIARGDZUCIAMoAgAiACAEIAFraiEFIAEgAGtBAWohBgJAA0AgAS0AACAAQaTVAGotAABHDUIgAEEBRg0BIABBAWohACAEIAFBAWoiAUcNAAsgAyAFNgIADJYCCyADQQA2AgAgBkEBaiEBQSAMQgtB0AEhAiABIARGDZQCIAMoAgAiACAEIAFraiEFIAEgAGtBAmohBgJAA0AgAS0AACAAQaHVAGotAABHDUEgAEECRg0BIABBAWohACAEIAFBAWoiAUcNAAsgAyAFNgIADJUCCyADQQA2AgAgBkEBaiEBQRIMQQsgASAERgRAQc8BIQIMlAILAkACQCABLQAAQcUAaw4OAENDQ0NDQ0NDQ0NDQwFDCyABQQFqIQFBtQEhAgz7AQsgAUEBaiEBQbYBIQIM+gELQc4BIQIgASAERg2SAiADKAIAIgAgBCABa2ohBSABIABrQQJqIQYCQANAIAEtAAAgAEGe1QBqLQAARw0/IABBAkYNASAAQQFqIQAgBCABQQFqIgFHDQALIAMgBTYCAAyTAgsgA0EANgIAIAZBAWohAUEHDD8LQc0BIQIgASAERg2RAiADKAIAIgAgBCABa2ohBSABIABrQQVqIQYCQANAIAEtAAAgAEGY1QBqLQAARw0+IABBBUYNASAAQQFqIQAgBCABQQFqIgFHDQALIAMgBTYCAAySAgsgA0EANgIAIAZBAWohAUEoDD4LIAEgBEYEQEHMASECDJECCwJAAkACQCABLQAAQcUAaw4RAEFBQUFBQUFBQQFBQUFBQQJBCyABQQFqIQFBsQEhAgz5AQsgAUEBaiEBQbIBIQIM+AELIAFBAWohAUGzASECDPcBC0HLASECIAEgBEYNjwIgAygCACIAIAQgAWtqIQUgASAAa0EGaiEGAkADQCABLQAAIABBkdUAai0AAEcNPCAAQQZGDQEgAEEBaiEAIAQgAUEBaiIBRw0ACyADIAU2AgAMkAILIANBADYCACAGQQFqIQFBGgw8C0HKASECIAEgBEYNjgIgAygCACIAIAQgAWtqIQUgASAAa0EDaiEGAkADQCABLQAAIABBjdUAai0AAEcNOyAAQQNGDQEgAEEBaiEAIAQgAUEBaiIBRw0ACyADIAU2AgAMjwILIANBADYCACAGQQFqIQFBIQw7CyABIARGBEBByQEhAgyOAgsCQAJAIAEtAABBwQBrDhQAPT09PT09PT09PT09PT09PT09AT0LIAFBAWohAUGtASECDPUBCyABQQFqIQFBsAEhAgz0AQsgASAERgRAQcgBIQIMjQILAkACQCABLQAAQdUAaw4LADw8PDw8PDw8PAE8CyABQQFqIQFBrgEhAgz0AQsgAUEBaiEBQa8BIQIM8wELQccBIQIgASAERg2LAiADKAIAIgAgBCABa2ohBSABIABrQQhqIQYCQANAIAEtAAAgAEGE1QBqLQAARw04IABBCEYNASAAQQFqIQAgBCABQQFqIgFHDQALIAMgBTYCAAyMAgsgA0EANgIAIAZBAWohAUEqDDgLIAEgBEYEQEHGASECDIsCCyABLQAAQdAARw04IAFBAWohAUElDDcLQcUBIQIgASAERg2JAiADKAIAIgAgBCABa2ohBSABIABrQQJqIQYCQANAIAEtAAAgAEGB1QBqLQAARw02IABBAkYNASAAQQFqIQAgBCABQQFqIgFHDQALIAMgBTYCAAyKAgsgA0EANgIAIAZBAWohAUEODDYLIAEgBEYEQEHEASECDIkCCyABLQAAQcUARw02IAFBAWohAUGrASECDO8BCyABIARGBEBBwwEhAgyIAgsCQAJAAkACQCABLQAAQcIAaw4PAAECOTk5OTk5OTk5OTkDOQsgAUEBaiEBQacBIQIM8QELIAFBAWohAUGoASECDPABCyABQQFqIQFBqQEhAgzvAQsgAUEBaiEBQaoBIQIM7gELQcIBIQIgASAERg2GAiADKAIAIgAgBCABa2ohBSABIABrQQJqIQYCQANAIAEtAAAgAEH+1ABqLQAARw0zIABBAkYNASAAQQFqIQAgBCABQQFqIgFHDQALIAMgBTYCAAyHAgsgA0EANgIAIAZBAWohAUEUDDMLQcEBIQIgASAERg2FAiADKAIAIgAgBCABa2ohBSABIABrQQRqIQYCQANAIAEtAAAgAEH51ABqLQAARw0yIABBBEYNASAAQQFqIQAgBCABQQFqIgFHDQALIAMgBTYCAAyGAgsgA0EANgIAIAZBAWohAUErDDILQcABIQIgASAERg2EAiADKAIAIgAgBCABa2ohBSABIABrQQJqIQYCQANAIAEtAAAgAEH21ABqLQAARw0xIABBAkYNASAAQQFqIQAgBCABQQFqIgFHDQALIAMgBTYCAAyFAgsgA0EANgIAIAZBAWohAUEsDDELQb8BIQIgASAERg2DAiADKAIAIgAgBCABa2ohBSABIABrQQJqIQYCQANAIAEtAAAgAEGh1QBqLQAARw0wIABBAkYNASAAQQFqIQAgBCABQQFqIgFHDQALIAMgBTYCAAyEAgsgA0EANgIAIAZBAWohAUERDDALQb4BIQIgASAERg2CAiADKAIAIgAgBCABa2ohBSABIABrQQNqIQYCQANAIAEtAAAgAEHy1ABqLQAARw0vIABBA0YNASAAQQFqIQAgBCABQQFqIgFHDQALIAMgBTYCAAyDAgsgA0EANgIAIAZBAWohAUEuDC8LIAEgBEYEQEG9ASECDIICCwJAAkACQAJAAkAgAS0AAEHBAGsOFQA0NDQ0NDQ0NDQ0ATQ0AjQ0AzQ0BDQLIAFBAWohAUGbASECDOwBCyABQQFqIQFBnAEhAgzrAQsgAUEBaiEBQZ0BIQIM6gELIAFBAWohAUGiASECDOkBCyABQQFqIQFBpAEhAgzoAQsgASAERgRAQbwBIQIMgQILAkACQCABLQAAQdIAaw4DADABMAsgAUEBaiEBQaMBIQIM6AELIAFBAWohAUEEDC0LQbsBIQIgASAERg3/ASADKAIAIgAgBCABa2ohBSABIABrQQFqIQYCQANAIAEtAAAgAEHw1ABqLQAARw0sIABBAUYNASAAQQFqIQAgBCABQQFqIgFHDQALIAMgBTYCAAyAAgsgA0EANgIAIAZBAWohAUEdDCwLIAEgBEYEQEG6ASECDP8BCwJAAkAgAS0AAEHJAGsOBwEuLi4uLgAuCyABQQFqIQFBoQEhAgzmAQsgAUEBaiEBQSIMKwsgASAERgRAQbkBIQIM/gELIAEtAABB0ABHDSsgAUEBaiEBQaABIQIM5AELIAEgBEYEQEG4ASECDP0BCwJAAkAgAS0AAEHGAGsOCwAsLCwsLCwsLCwBLAsgAUEBaiEBQZ4BIQIM5AELIAFBAWohAUGfASECDOMBC0G3ASECIAEgBEYN+wEgAygCACIAIAQgAWtqIQUgASAAa0EDaiEGAkADQCABLQAAIABB7NQAai0AAEcNKCAAQQNGDQEgAEEBaiEAIAQgAUEBaiIBRw0ACyADIAU2AgAM/AELIANBADYCACAGQQFqIQFBDQwoC0G2ASECIAEgBEYN+gEgAygCACIAIAQgAWtqIQUgASAAa0ECaiEGAkADQCABLQAAIABBodUAai0AAEcNJyAAQQJGDQEgAEEBaiEAIAQgAUEBaiIBRw0ACyADIAU2AgAM+wELIANBADYCACAGQQFqIQFBDAwnC0G1ASECIAEgBEYN+QEgAygCACIAIAQgAWtqIQUgASAAa0EBaiEGAkADQCABLQAAIABB6tQAai0AAEcNJiAAQQFGDQEgAEEBaiEAIAQgAUEBaiIBRw0ACyADIAU2AgAM+gELIANBADYCACAGQQFqIQFBAwwmC0G0ASECIAEgBEYN+AEgAygCACIAIAQgAWtqIQUgASAAa0EBaiEGAkADQCABLQAAIABB6NQAai0AAEcNJSAAQQFGDQEgAEEBaiEAIAQgAUEBaiIBRw0ACyADIAU2AgAM+QELIANBADYCACAGQQFqIQFBJgwlCyABIARGBEBBswEhAgz4AQsCQAJAIAEtAABB1ABrDgIAAScLIAFBAWohAUGZASECDN8BCyABQQFqIQFBmgEhAgzeAQtBsgEhAiABIARGDfYBIAMoAgAiACAEIAFraiEFIAEgAGtBAWohBgJAA0AgAS0AACAAQebUAGotAABHDSMgAEEBRg0BIABBAWohACAEIAFBAWoiAUcNAAsgAyAFNgIADPcBCyADQQA2AgAgBkEBaiEBQScMIwtBsQEhAiABIARGDfUBIAMoAgAiACAEIAFraiEFIAEgAGtBAWohBgJAA0AgAS0AACAAQeTUAGotAABHDSIgAEEBRg0BIABBAWohACAEIAFBAWoiAUcNAAsgAyAFNgIADPYBCyADQQA2AgAgBkEBaiEBQRwMIgtBsAEhAiABIARGDfQBIAMoAgAiACAEIAFraiEFIAEgAGtBBWohBgJAA0AgAS0AACAAQd7UAGotAABHDSEgAEEFRg0BIABBAWohACAEIAFBAWoiAUcNAAsgAyAFNgIADPUBCyADQQA2AgAgBkEBaiEBQQYMIQtBrwEhAiABIARGDfMBIAMoAgAiACAEIAFraiEFIAEgAGtBBGohBgJAA0AgAS0AACAAQdnUAGotAABHDSAgAEEERg0BIABBAWohACAEIAFBAWoiAUcNAAsgAyAFNgIADPQBCyADQQA2AgAgBkEBaiEBQRkMIAsgASAERgRAQa4BIQIM8wELAkACQAJAAkAgAS0AAEEtaw4jACQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkASQkJCQkAiQkJAMkCyABQQFqIQFBjgEhAgzcAQsgAUEBaiEBQY8BIQIM2wELIAFBAWohAUGUASECDNoBCyABQQFqIQFBlQEhAgzZAQtBrQEhAiABIARGDfEBIAMoAgAiACAEIAFraiEFIAEgAGtBAWohBgJAA0AgAS0AACAAQdfUAGotAABHDR4gAEEBRg0BIABBAWohACAEIAFBAWoiAUcNAAsgAyAFNgIADPIBCyADQQA2AgAgBkEBaiEBQQsMHgsgASAERgRAQawBIQIM8QELAkACQCABLQAAQcEAaw4DACABIAsgAUEBaiEBQZABIQIM2AELIAFBAWohAUGTASECDNcBCyABIARGBEBBqwEhAgzwAQsCQAJAIAEtAABBwQBrDg8AHx8fHx8fHx8fHx8fHwEfCyABQQFqIQFBkQEhAgzXAQsgAUEBaiEBQZIBIQIM1gELIAEgBEYEQEGqASECDO8BCyABLQAAQcwARw0cIAFBAWohAUEKDBsLQakBIQIgASAERg3tASADKAIAIgAgBCABa2ohBSABIABrQQVqIQYCQANAIAEtAAAgAEHR1ABqLQAARw0aIABBBUYNASAAQQFqIQAgBCABQQFqIgFHDQALIAMgBTYCAAzuAQsgA0EANgIAIAZBAWohAUEeDBoLQagBIQIgASAERg3sASADKAIAIgAgBCABa2ohBSABIABrQQZqIQYCQANAIAEtAAAgAEHK1ABqLQAARw0ZIABBBkYNASAAQQFqIQAgBCABQQFqIgFHDQALIAMgBTYCAAztAQsgA0EANgIAIAZBAWohAUEVDBkLQacBIQIgASAERg3rASADKAIAIgAgBCABa2ohBSABIABrQQJqIQYCQANAIAEtAAAgAEHH1ABqLQAARw0YIABBAkYNASAAQQFqIQAgBCABQQFqIgFHDQALIAMgBTYCAAzsAQsgA0EANgIAIAZBAWohAUEXDBgLQaYBIQIgASAERg3qASADKAIAIgAgBCABa2ohBSABIABrQQVqIQYCQANAIAEtAAAgAEHB1ABqLQAARw0XIABBBUYNASAAQQFqIQAgBCABQQFqIgFHDQALIAMgBTYCAAzrAQsgA0EANgIAIAZBAWohAUEYDBcLIAEgBEYEQEGlASECDOoBCwJAAkAgAS0AAEHJAGsOBwAZGRkZGQEZCyABQQFqIQFBiwEhAgzRAQsgAUEBaiEBQYwBIQIM0AELQaQBIQIgASAERg3oASADKAIAIgAgBCABa2ohBSABIABrQQFqIQYCQANAIAEtAAAgAEGm1QBqLQAARw0VIABBAUYNASAAQQFqIQAgBCABQQFqIgFHDQALIAMgBTYCAAzpAQsgA0EANgIAIAZBAWohAUEJDBULQaMBIQIgASAERg3nASADKAIAIgAgBCABa2ohBSABIABrQQFqIQYCQANAIAEtAAAgAEGk1QBqLQAARw0UIABBAUYNASAAQQFqIQAgBCABQQFqIgFHDQALIAMgBTYCAAzoAQsgA0EANgIAIAZBAWohAUEfDBQLQaIBIQIgASAERg3mASADKAIAIgAgBCABa2ohBSABIABrQQJqIQYCQANAIAEtAAAgAEG+1ABqLQAARw0TIABBAkYNASAAQQFqIQAgBCABQQFqIgFHDQALIAMgBTYCAAznAQsgA0EANgIAIAZBAWohAUECDBMLQaEBIQIgASAERg3lASADKAIAIgAgBCABa2ohBSABIABrQQFqIQYDQCABLQAAIABBvNQAai0AAEcNESAAQQFGDQIgAEEBaiEAIAQgAUEBaiIBRw0ACyADIAU2AgAM5QELIAEgBEYEQEGgASECDOUBC0EBIAEtAABB3wBHDREaIAFBAWohAUGHASECDMsBCyADQQA2AgAgBkEBaiEBQYgBIQIMygELQZ8BIQIgASAERg3iASADKAIAIgAgBCABa2ohBSABIABrQQhqIQYCQANAIAEtAAAgAEGE1QBqLQAARw0PIABBCEYNASAAQQFqIQAgBCABQQFqIgFHDQALIAMgBTYCAAzjAQsgA0EANgIAIAZBAWohAUEpDA8LQZ4BIQIgASAERg3hASADKAIAIgAgBCABa2ohBSABIABrQQNqIQYCQANAIAEtAAAgAEG41ABqLQAARw0OIABBA0YNASAAQQFqIQAgBCABQQFqIgFHDQALIAMgBTYCAAziAQsgA0EANgIAIAZBAWohAUEtDA4LIAEgBEYEQEGdASECDOEBCyABLQAAQcUARw0OIAFBAWohAUGEASECDMcBCyABIARGBEBBnAEhAgzgAQsCQAJAIAEtAABBzABrDggADw8PDw8PAQ8LIAFBAWohAUGCASECDMcBCyABQQFqIQFBgwEhAgzGAQtBmwEhAiABIARGDd4BIAMoAgAiACAEIAFraiEFIAEgAGtBBGohBgJAA0AgAS0AACAAQbPUAGotAABHDQsgAEEERg0BIABBAWohACAEIAFBAWoiAUcNAAsgAyAFNgIADN8BCyADQQA2AgAgBkEBaiEBQSMMCwtBmgEhAiABIARGDd0BIAMoAgAiACAEIAFraiEFIAEgAGtBAmohBgJAA0AgAS0AACAAQbDUAGotAABHDQogAEECRg0BIABBAWohACAEIAFBAWoiAUcNAAsgAyAFNgIADN4BCyADQQA2AgAgBkEBaiEBQQAMCgsgASAERgRAQZkBIQIM3QELAkACQCABLQAAQcgAaw4IAAwMDAwMDAEMCyABQQFqIQFB/QAhAgzEAQsgAUEBaiEBQYABIQIMwwELIAEgBEYEQEGYASECDNwBCwJAAkAgAS0AAEHOAGsOAwALAQsLIAFBAWohAUH+ACECDMMBCyABQQFqIQFB/wAhAgzCAQsgASAERgRAQZcBIQIM2wELIAEtAABB2QBHDQggAUEBaiEBQQgMBwtBlgEhAiABIARGDdkBIAMoAgAiACAEIAFraiEFIAEgAGtBA2ohBgJAA0AgAS0AACAAQazUAGotAABHDQYgAEEDRg0BIABBAWohACAEIAFBAWoiAUcNAAsgAyAFNgIADNoBCyADQQA2AgAgBkEBaiEBQQUMBgtBlQEhAiABIARGDdgBIAMoAgAiACAEIAFraiEFIAEgAGtBBWohBgJAA0AgAS0AACAAQabUAGotAABHDQUgAEEFRg0BIABBAWohACAEIAFBAWoiAUcNAAsgAyAFNgIADNkBCyADQQA2AgAgBkEBaiEBQRYMBQtBlAEhAiABIARGDdcBIAMoAgAiACAEIAFraiEFIAEgAGtBAmohBgJAA0AgAS0AACAAQaHVAGotAABHDQQgAEECRg0BIABBAWohACAEIAFBAWoiAUcNAAsgAyAFNgIADNgBCyADQQA2AgAgBkEBaiEBQRAMBAsgASAERgRAQZMBIQIM1wELAkACQCABLQAAQcMAaw4MAAYGBgYGBgYGBgYBBgsgAUEBaiEBQfkAIQIMvgELIAFBAWohAUH6ACECDL0BC0GSASECIAEgBEYN1QEgAygCACIAIAQgAWtqIQUgASAAa0EFaiEGAkADQCABLQAAIABBoNQAai0AAEcNAiAAQQVGDQEgAEEBaiEAIAQgAUEBaiIBRw0ACyADIAU2AgAM1gELIANBADYCACAGQQFqIQFBJAwCCyADQQA2AgAMAgsgASAERgRAQZEBIQIM1AELIAEtAABBzABHDQEgAUEBaiEBQRMLOgApIAMoAgQhACADQQA2AgQgAyAAIAEQLiIADQIMAQtBACECIANBADYCHCADIAE2AhQgA0H+HzYCECADQQY2AgwM0QELQfgAIQIMtwELIANBkAE2AhwgAyABNgIUIAMgADYCDEEAIQIMzwELQQAhAAJAIAMoAjgiAkUNACACKAJAIgJFDQAgAyACEQAAIQALIABFDQAgAEEVRg0BIANBADYCHCADIAE2AhQgA0GCDzYCECADQSA2AgxBACECDM4BC0H3ACECDLQBCyADQY8BNgIcIAMgATYCFCADQewbNgIQIANBFTYCDEEAIQIMzAELIAEgBEYEQEGPASECDMwBCwJAIAEtAABBIEYEQCABQQFqIQEMAQsgA0EANgIcIAMgATYCFCADQZsfNgIQIANBBjYCDEEAIQIMzAELQQIhAgyyAQsDQCABLQAAQSBHDQIgBCABQQFqIgFHDQALQY4BIQIMygELIAEgBEYEQEGNASECDMoBCwJAIAEtAABBCWsOBEoAAEoAC0H1ACECDLABCyADLQApQQVGBEBB9gAhAgywAQtB9AAhAgyvAQsgASAERgRAQYwBIQIMyAELIANBEDYCCCADIAE2AgQMCgsgASAERgRAQYsBIQIMxwELAkAgAS0AAEEJaw4ERwAARwALQfMAIQIMrQELIAEgBEcEQCADQRA2AgggAyABNgIEQfEAIQIMrQELQYoBIQIMxQELAkAgASAERwRAA0AgAS0AAEGg0ABqLQAAIgBBA0cEQAJAIABBAWsOAkkABAtB8AAhAgyvAQsgBCABQQFqIgFHDQALQYgBIQIMxgELQYgBIQIMxQELIANBADYCHCADIAE2AhQgA0HbIDYCECADQQc2AgxBACECDMQBCyABIARGBEBBiQEhAgzEAQsCQAJAAkAgAS0AAEGg0gBqLQAAQQFrDgNGAgABC0HyACECDKwBCyADQQA2AhwgAyABNgIUIANBtBI2AhAgA0EHNgIMQQAhAgzEAQtB6gAhAgyqAQsgASAERwRAIAFBAWohAUHvACECDKoBC0GHASECDMIBCyAEIAEiAEYEQEGGASECDMIBCyAALQAAIgFBL0YEQCAAQQFqIQFB7gAhAgypAQsgAUEJayICQRdLDQEgACEBQQEgAnRBm4CABHENQQwBCyAEIAEiAEYEQEGFASECDMEBCyAALQAAQS9HDQAgAEEBaiEBDAMLQQAhAiADQQA2AhwgAyAANgIUIANB2yA2AhAgA0EHNgIMDL8BCwJAAkACQAJAAkADQCABLQAAQaDOAGotAAAiAEEFRwRAAkACQCAAQQFrDghHBQYHCAAEAQgLQesAIQIMrQELIAFBAWohAUHtACECDKwBCyAEIAFBAWoiAUcNAAtBhAEhAgzDAQsgAUEBagwUCyADKAIEIQAgA0EANgIEIAMgACABECwiAEUNHiADQdsANgIcIAMgATYCFCADIAA2AgxBACECDMEBCyADKAIEIQAgA0EANgIEIAMgACABECwiAEUNHiADQd0ANgIcIAMgATYCFCADIAA2AgxBACECDMABCyADKAIEIQAgA0EANgIEIAMgACABECwiAEUNHiADQfoANgIcIAMgATYCFCADIAA2AgxBACECDL8BCyADQQA2AhwgAyABNgIUIANB+Q82AhAgA0EHNgIMQQAhAgy+AQsgASAERgRAQYMBIQIMvgELAkAgAS0AAEGgzgBqLQAAQQFrDgg+BAUGAAgCAwcLIAFBAWohAQtBAyECDKMBCyABQQFqDA0LQQAhAiADQQA2AhwgA0HREjYCECADQQc2AgwgAyABQQFqNgIUDLoBCyADKAIEIQAgA0EANgIEIAMgACABECwiAEUNFiADQdsANgIcIAMgATYCFCADIAA2AgxBACECDLkBCyADKAIEIQAgA0EANgIEIAMgACABECwiAEUNFiADQd0ANgIcIAMgATYCFCADIAA2AgxBACECDLgBCyADKAIEIQAgA0EANgIEIAMgACABECwiAEUNFiADQfoANgIcIAMgATYCFCADIAA2AgxBACECDLcBCyADQQA2AhwgAyABNgIUIANB+Q82AhAgA0EHNgIMQQAhAgy2AQtB7AAhAgycAQsgASAERgRAQYIBIQIMtQELIAFBAWoMAgsgASAERgRAQYEBIQIMtAELIAFBAWoMAQsgASAERg0BIAFBAWoLIQFBBCECDJgBC0GAASECDLABCwNAIAEtAABBoMwAai0AACIAQQJHBEAgAEEBRwRAQekAIQIMmQELDDELIAQgAUEBaiIBRw0AC0H/ACECDK8BCyABIARGBEBB/gAhAgyvAQsCQCABLQAAQQlrDjcvAwYvBAYGBgYGBgYGBgYGBgYGBgYGBgUGBgIGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYABgsgAUEBagshAUEFIQIMlAELIAFBAWoMBgsgAygCBCEAIANBADYCBCADIAAgARAsIgBFDQggA0HbADYCHCADIAE2AhQgAyAANgIMQQAhAgyrAQsgAygCBCEAIANBADYCBCADIAAgARAsIgBFDQggA0HdADYCHCADIAE2AhQgAyAANgIMQQAhAgyqAQsgAygCBCEAIANBADYCBCADIAAgARAsIgBFDQggA0H6ADYCHCADIAE2AhQgAyAANgIMQQAhAgypAQsgA0EANgIcIAMgATYCFCADQY0UNgIQIANBBzYCDEEAIQIMqAELAkACQAJAAkADQCABLQAAQaDKAGotAAAiAEEFRwRAAkAgAEEBaw4GLgMEBQYABgtB6AAhAgyUAQsgBCABQQFqIgFHDQALQf0AIQIMqwELIAMoAgQhACADQQA2AgQgAyAAIAEQLCIARQ0HIANB2wA2AhwgAyABNgIUIAMgADYCDEEAIQIMqgELIAMoAgQhACADQQA2AgQgAyAAIAEQLCIARQ0HIANB3QA2AhwgAyABNgIUIAMgADYCDEEAIQIMqQELIAMoAgQhACADQQA2AgQgAyAAIAEQLCIARQ0HIANB+gA2AhwgAyABNgIUIAMgADYCDEEAIQIMqAELIANBADYCHCADIAE2AhQgA0HkCDYCECADQQc2AgxBACECDKcBCyABIARGDQEgAUEBagshAUEGIQIMjAELQfwAIQIMpAELAkACQAJAAkADQCABLQAAQaDIAGotAAAiAEEFRwRAIABBAWsOBCkCAwQFCyAEIAFBAWoiAUcNAAtB+wAhAgynAQsgAygCBCEAIANBADYCBCADIAAgARAsIgBFDQMgA0HbADYCHCADIAE2AhQgAyAANgIMQQAhAgymAQsgAygCBCEAIANBADYCBCADIAAgARAsIgBFDQMgA0HdADYCHCADIAE2AhQgAyAANgIMQQAhAgylAQsgAygCBCEAIANBADYCBCADIAAgARAsIgBFDQMgA0H6ADYCHCADIAE2AhQgAyAANgIMQQAhAgykAQsgA0EANgIcIAMgATYCFCADQbwKNgIQIANBBzYCDEEAIQIMowELQc8AIQIMiQELQdEAIQIMiAELQecAIQIMhwELIAEgBEYEQEH6ACECDKABCwJAIAEtAABBCWsOBCAAACAACyABQQFqIQFB5gAhAgyGAQsgASAERgRAQfkAIQIMnwELAkAgAS0AAEEJaw4EHwAAHwALQQAhAAJAIAMoAjgiAkUNACACKAI4IgJFDQAgAyACEQAAIQALIABFBEBB4gEhAgyGAQsgAEEVRwRAIANBADYCHCADIAE2AhQgA0HJDTYCECADQRo2AgxBACECDJ8BCyADQfgANgIcIAMgATYCFCADQeoaNgIQIANBFTYCDEEAIQIMngELIAEgBEcEQCADQQ02AgggAyABNgIEQeQAIQIMhQELQfcAIQIMnQELIAEgBEYEQEH2ACECDJ0BCwJAAkACQCABLQAAQcgAaw4LAAELCwsLCwsLCwILCyABQQFqIQFB3QAhAgyFAQsgAUEBaiEBQeAAIQIMhAELIAFBAWohAUHjACECDIMBC0H1ACECIAEgBEYNmwEgAygCACIAIAQgAWtqIQUgASAAa0ECaiEGAkADQCABLQAAIABBtdUAai0AAEcNCCAAQQJGDQEgAEEBaiEAIAQgAUEBaiIBRw0ACyADIAU2AgAMnAELIAMoAgQhACADQgA3AwAgAyAAIAZBAWoiARArIgAEQCADQfQANgIcIAMgATYCFCADIAA2AgxBACECDJwBC0HiACECDIIBC0EAIQACQCADKAI4IgJFDQAgAigCNCICRQ0AIAMgAhEAACEACwJAIAAEQCAAQRVGDQEgA0EANgIcIAMgATYCFCADQeoNNgIQIANBJjYCDEEAIQIMnAELQeEAIQIMggELIANB8wA2AhwgAyABNgIUIANBgBs2AhAgA0EVNgIMQQAhAgyaAQsgAy0AKSIAQSNrQQtJDQkCQCAAQQZLDQBBASAAdEHKAHFFDQAMCgtBACECIANBADYCHCADIAE2AhQgA0HtCTYCECADQQg2AgwMmQELQfIAIQIgASAERg2YASADKAIAIgAgBCABa2ohBSABIABrQQFqIQYCQANAIAEtAAAgAEGz1QBqLQAARw0FIABBAUYNASAAQQFqIQAgBCABQQFqIgFHDQALIAMgBTYCAAyZAQsgAygCBCEAIANCADcDACADIAAgBkEBaiIBECsiAARAIANB8QA2AhwgAyABNgIUIAMgADYCDEEAIQIMmQELQd8AIQIMfwtBACEAAkAgAygCOCICRQ0AIAIoAjQiAkUNACADIAIRAAAhAAsCQCAABEAgAEEVRg0BIANBADYCHCADIAE2AhQgA0HqDTYCECADQSY2AgxBACECDJkBC0HeACECDH8LIANB8AA2AhwgAyABNgIUIANBgBs2AhAgA0EVNgIMQQAhAgyXAQsgAy0AKUEhRg0GIANBADYCHCADIAE2AhQgA0GRCjYCECADQQg2AgxBACECDJYBC0HvACECIAEgBEYNlQEgAygCACIAIAQgAWtqIQUgASAAa0ECaiEGAkADQCABLQAAIABBsNUAai0AAEcNAiAAQQJGDQEgAEEBaiEAIAQgAUEBaiIBRw0ACyADIAU2AgAMlgELIAMoAgQhACADQgA3AwAgAyAAIAZBAWoiARArIgBFDQIgA0HtADYCHCADIAE2AhQgAyAANgIMQQAhAgyVAQsgA0EANgIACyADKAIEIQAgA0EANgIEIAMgACABECsiAEUNgAEgA0HuADYCHCADIAE2AhQgAyAANgIMQQAhAgyTAQtB3AAhAgx5C0EAIQACQCADKAI4IgJFDQAgAigCNCICRQ0AIAMgAhEAACEACwJAIAAEQCAAQRVGDQEgA0EANgIcIAMgATYCFCADQeoNNgIQIANBJjYCDEEAIQIMkwELQdsAIQIMeQsgA0HsADYCHCADIAE2AhQgA0GAGzYCECADQRU2AgxBACECDJEBCyADLQApIgBBI0kNACAAQS5GDQAgA0EANgIcIAMgATYCFCADQckJNgIQIANBCDYCDEEAIQIMkAELQdoAIQIMdgsgASAERgRAQesAIQIMjwELAkAgAS0AAEEvRgRAIAFBAWohAQwBCyADQQA2AhwgAyABNgIUIANBsjg2AhAgA0EINgIMQQAhAgyPAQtB2QAhAgx1CyABIARHBEAgA0EONgIIIAMgATYCBEHYACECDHULQeoAIQIMjQELIAEgBEYEQEHpACECDI0BCyABLQAAQTBrIgBB/wFxQQpJBEAgAyAAOgAqIAFBAWohAUHXACECDHQLIAMoAgQhACADQQA2AgQgAyAAIAEQLyIARQ16IANB6AA2AhwgAyABNgIUIAMgADYCDEEAIQIMjAELIAEgBEYEQEHnACECDIwBCwJAIAEtAABBLkYEQCABQQFqIQEMAQsgAygCBCEAIANBADYCBCADIAAgARAvIgBFDXsgA0HmADYCHCADIAE2AhQgAyAANgIMQQAhAgyMAQtB1gAhAgxyCyABIARGBEBB5QAhAgyLAQtBACEAQQEhBUEBIQdBACECAkACQAJAAkACQAJ/AkACQAJAAkACQAJAAkAgAS0AAEEwaw4KCgkAAQIDBAUGCAsLQQIMBgtBAwwFC0EEDAQLQQUMAwtBBgwCC0EHDAELQQgLIQJBACEFQQAhBwwCC0EJIQJBASEAQQAhBUEAIQcMAQtBACEFQQEhAgsgAyACOgArIAFBAWohAQJAAkAgAy0ALkEQcQ0AAkACQAJAIAMtACoOAwEAAgQLIAdFDQMMAgsgAA0BDAILIAVFDQELIAMoAgQhACADQQA2AgQgAyAAIAEQLyIARQ0CIANB4gA2AhwgAyABNgIUIAMgADYCDEEAIQIMjQELIAMoAgQhACADQQA2AgQgAyAAIAEQLyIARQ19IANB4wA2AhwgAyABNgIUIAMgADYCDEEAIQIMjAELIAMoAgQhACADQQA2AgQgAyAAIAEQLyIARQ17IANB5AA2AhwgAyABNgIUIAMgADYCDAyLAQtB1AAhAgxxCyADLQApQSJGDYYBQdMAIQIMcAtBACEAAkAgAygCOCICRQ0AIAIoAkQiAkUNACADIAIRAAAhAAsgAEUEQEHVACECDHALIABBFUcEQCADQQA2AhwgAyABNgIUIANBpA02AhAgA0EhNgIMQQAhAgyJAQsgA0HhADYCHCADIAE2AhQgA0HQGjYCECADQRU2AgxBACECDIgBCyABIARGBEBB4AAhAgyIAQsCQAJAAkACQAJAIAEtAABBCmsOBAEEBAAECyABQQFqIQEMAQsgAUEBaiEBIANBL2otAABBAXFFDQELQdIAIQIMcAsgA0EANgIcIAMgATYCFCADQbYRNgIQIANBCTYCDEEAIQIMiAELIANBADYCHCADIAE2AhQgA0G2ETYCECADQQk2AgxBACECDIcBCyABIARGBEBB3wAhAgyHAQsgAS0AAEEKRgRAIAFBAWohAQwJCyADLQAuQcAAcQ0IIANBADYCHCADIAE2AhQgA0G2ETYCECADQQI2AgxBACECDIYBCyABIARGBEBB3QAhAgyGAQsgAS0AACICQQ1GBEAgAUEBaiEBQdAAIQIMbQsgASEAIAJBCWsOBAUBAQUBCyAEIAEiAEYEQEHcACECDIUBCyAALQAAQQpHDQAgAEEBagwCC0EAIQIgA0EANgIcIAMgADYCFCADQcotNgIQIANBBzYCDAyDAQsgASAERgRAQdsAIQIMgwELAkAgAS0AAEEJaw4EAwAAAwALIAFBAWoLIQFBzgAhAgxoCyABIARGBEBB2gAhAgyBAQsgAS0AAEEJaw4EAAEBAAELQQAhAiADQQA2AhwgA0GaEjYCECADQQc2AgwgAyABQQFqNgIUDH8LIANBgBI7ASpBACEAAkAgAygCOCICRQ0AIAIoAjgiAkUNACADIAIRAAAhAAsgAEUNACAAQRVHDQEgA0HZADYCHCADIAE2AhQgA0HqGjYCECADQRU2AgxBACECDH4LQc0AIQIMZAsgA0EANgIcIAMgATYCFCADQckNNgIQIANBGjYCDEEAIQIMfAsgASAERgRAQdkAIQIMfAsgAS0AAEEgRw09IAFBAWohASADLQAuQQFxDT0gA0EANgIcIAMgATYCFCADQcIcNgIQIANBHjYCDEEAIQIMewsgASAERgRAQdgAIQIMewsCQAJAAkACQAJAIAEtAAAiAEEKaw4EAgMDAAELIAFBAWohAUEsIQIMZQsgAEE6Rw0BIANBADYCHCADIAE2AhQgA0HnETYCECADQQo2AgxBACECDH0LIAFBAWohASADQS9qLQAAQQFxRQ1zIAMtADJBgAFxRQRAIANBMmohAiADEDVBACEAAkAgAygCOCIGRQ0AIAYoAigiBkUNACADIAYRAAAhAAsCQAJAIAAOFk1MSwEBAQEBAQEBAQEBAQEBAQEBAQABCyADQSk2AhwgAyABNgIUIANBrBk2AhAgA0EVNgIMQQAhAgx+CyADQQA2AhwgAyABNgIUIANB5Qs2AhAgA0ERNgIMQQAhAgx9C0EAIQACQCADKAI4IgJFDQAgAigCXCICRQ0AIAMgAhEAACEACyAARQ1ZIABBFUcNASADQQU2AhwgAyABNgIUIANBmxs2AhAgA0EVNgIMQQAhAgx8C0HLACECDGILQQAhAiADQQA2AhwgAyABNgIUIANBkA42AhAgA0EUNgIMDHoLIAMgAy8BMkGAAXI7ATIMOwsgASAERwRAIANBETYCCCADIAE2AgRBygAhAgxgC0HXACECDHgLIAEgBEYEQEHWACECDHgLAkACQAJAAkAgAS0AACIAQSByIAAgAEHBAGtB/wFxQRpJG0H/AXFB4wBrDhMAQEBAQEBAQEBAQEBAAUBAQAIDQAsgAUEBaiEBQcYAIQIMYQsgAUEBaiEBQccAIQIMYAsgAUEBaiEBQcgAIQIMXwsgAUEBaiEBQckAIQIMXgtB1QAhAiAEIAEiAEYNdiAEIAFrIAMoAgAiAWohBiAAIAFrQQVqIQcDQCABQZDIAGotAAAgAC0AACIFQSByIAUgBUHBAGtB/wFxQRpJG0H/AXFHDQhBBCABQQVGDQoaIAFBAWohASAEIABBAWoiAEcNAAsgAyAGNgIADHYLQdQAIQIgBCABIgBGDXUgBCABayADKAIAIgFqIQYgACABa0EPaiEHA0AgAUGAyABqLQAAIAAtAAAiBUEgciAFIAVBwQBrQf8BcUEaSRtB/wFxRw0HQQMgAUEPRg0JGiABQQFqIQEgBCAAQQFqIgBHDQALIAMgBjYCAAx1C0HTACECIAQgASIARg10IAQgAWsgAygCACIBaiEGIAAgAWtBDmohBwNAIAFB4scAai0AACAALQAAIgVBIHIgBSAFQcEAa0H/AXFBGkkbQf8BcUcNBiABQQ5GDQcgAUEBaiEBIAQgAEEBaiIARw0ACyADIAY2AgAMdAtB0gAhAiAEIAEiAEYNcyAEIAFrIAMoAgAiAWohBSAAIAFrQQFqIQYDQCABQeDHAGotAAAgAC0AACIHQSByIAcgB0HBAGtB/wFxQRpJG0H/AXFHDQUgAUEBRg0CIAFBAWohASAEIABBAWoiAEcNAAsgAyAFNgIADHMLIAEgBEYEQEHRACECDHMLAkACQCABLQAAIgBBIHIgACAAQcEAa0H/AXFBGkkbQf8BcUHuAGsOBwA5OTk5OQE5CyABQQFqIQFBwwAhAgxaCyABQQFqIQFBxAAhAgxZCyADQQA2AgAgBkEBaiEBQcUAIQIMWAtB0AAhAiAEIAEiAEYNcCAEIAFrIAMoAgAiAWohBiAAIAFrQQlqIQcDQCABQdbHAGotAAAgAC0AACIFQSByIAUgBUHBAGtB/wFxQRpJG0H/AXFHDQJBAiABQQlGDQQaIAFBAWohASAEIABBAWoiAEcNAAsgAyAGNgIADHALQc8AIQIgBCABIgBGDW8gBCABayADKAIAIgFqIQYgACABa0EFaiEHA0AgAUHQxwBqLQAAIAAtAAAiBUEgciAFIAVBwQBrQf8BcUEaSRtB/wFxRw0BIAFBBUYNAiABQQFqIQEgBCAAQQFqIgBHDQALIAMgBjYCAAxvCyAAIQEgA0EANgIADDMLQQELOgAsIANBADYCACAHQQFqIQELQS0hAgxSCwJAA0AgAS0AAEHQxQBqLQAAQQFHDQEgBCABQQFqIgFHDQALQc0AIQIMawtBwgAhAgxRCyABIARGBEBBzAAhAgxqCyABLQAAQTpGBEAgAygCBCEAIANBADYCBCADIAAgARAwIgBFDTMgA0HLADYCHCADIAA2AgwgAyABQQFqNgIUQQAhAgxqCyADQQA2AhwgAyABNgIUIANB5xE2AhAgA0EKNgIMQQAhAgxpCwJAAkAgAy0ALEECaw4CAAEnCyADQTNqLQAAQQJxRQ0mIAMtAC5BAnENJiADQQA2AhwgAyABNgIUIANBphQ2AhAgA0ELNgIMQQAhAgxpCyADLQAyQSBxRQ0lIAMtAC5BAnENJSADQQA2AhwgAyABNgIUIANBvRM2AhAgA0EPNgIMQQAhAgxoC0EAIQACQCADKAI4IgJFDQAgAigCSCICRQ0AIAMgAhEAACEACyAARQRAQcEAIQIMTwsgAEEVRwRAIANBADYCHCADIAE2AhQgA0GmDzYCECADQRw2AgxBACECDGgLIANBygA2AhwgAyABNgIUIANBhRw2AhAgA0EVNgIMQQAhAgxnCyABIARHBEAgASECA0AgBCACIgFrQRBOBEAgAUEQaiEC/Qz/////////////////////IAH9AAAAIg1BB/1sIA39DODg4ODg4ODg4ODg4ODg4OD9bv0MX19fX19fX19fX19fX19fX/0mIA39DAkJCQkJCQkJCQkJCQkJCQn9I/1Q/VL9ZEF/c2giAEEQRg0BIAAgAWohAQwYCyABIARGBEBBxAAhAgxpCyABLQAAQcDBAGotAABBAUcNFyAEIAFBAWoiAkcNAAtBxAAhAgxnC0HEACECDGYLIAEgBEcEQANAAkAgAS0AACIAQSByIAAgAEHBAGtB/wFxQRpJG0H/AXEiAEEJRg0AIABBIEYNAAJAAkACQAJAIABB4wBrDhMAAwMDAwMDAwEDAwMDAwMDAwMCAwsgAUEBaiEBQTYhAgxSCyABQQFqIQFBNyECDFELIAFBAWohAUE4IQIMUAsMFQsgBCABQQFqIgFHDQALQTwhAgxmC0E8IQIMZQsgASAERgRAQcgAIQIMZQsgA0ESNgIIIAMgATYCBAJAAkACQAJAAkAgAy0ALEEBaw4EFAABAgkLIAMtADJBIHENA0HgASECDE8LAkAgAy8BMiIAQQhxRQ0AIAMtAChBAUcNACADLQAuQQhxRQ0CCyADIABB9/sDcUGABHI7ATIMCwsgAyADLwEyQRByOwEyDAQLIANBADYCBCADIAEgARAxIgAEQCADQcEANgIcIAMgADYCDCADIAFBAWo2AhRBACECDGYLIAFBAWohAQxYCyADQQA2AhwgAyABNgIUIANB9BM2AhAgA0EENgIMQQAhAgxkC0HHACECIAEgBEYNYyADKAIAIgAgBCABa2ohBSABIABrQQZqIQYCQANAIABBwMUAai0AACABLQAAQSByRw0BIABBBkYNSiAAQQFqIQAgBCABQQFqIgFHDQALIAMgBTYCAAxkCyADQQA2AgAMBQsCQCABIARHBEADQCABLQAAQcDDAGotAAAiAEEBRwRAIABBAkcNAyABQQFqIQEMBQsgBCABQQFqIgFHDQALQcUAIQIMZAtBxQAhAgxjCwsgA0EAOgAsDAELQQshAgxHC0E/IQIMRgsCQAJAA0AgAS0AACIAQSBHBEACQCAAQQprDgQDBQUDAAsgAEEsRg0DDAQLIAQgAUEBaiIBRw0AC0HGACECDGALIANBCDoALAwOCyADLQAoQQFHDQIgAy0ALkEIcQ0CIAMoAgQhACADQQA2AgQgAyAAIAEQMSIABEAgA0HCADYCHCADIAA2AgwgAyABQQFqNgIUQQAhAgxfCyABQQFqIQEMUAtBOyECDEQLAkADQCABLQAAIgBBIEcgAEEJR3ENASAEIAFBAWoiAUcNAAtBwwAhAgxdCwtBPCECDEILAkACQCABIARHBEADQCABLQAAIgBBIEcEQCAAQQprDgQDBAQDBAsgBCABQQFqIgFHDQALQT8hAgxdC0E/IQIMXAsgAyADLwEyQSByOwEyDAoLIAMoAgQhACADQQA2AgQgAyAAIAEQMSIARQ1OIANBPjYCHCADIAE2AhQgAyAANgIMQQAhAgxaCwJAIAEgBEcEQANAIAEtAABBwMMAai0AACIAQQFHBEAgAEECRg0DDAwLIAQgAUEBaiIBRw0AC0E3IQIMWwtBNyECDFoLIAFBAWohAQwEC0E7IQIgBCABIgBGDVggBCABayADKAIAIgFqIQYgACABa0EFaiEHAkADQCABQZDIAGotAAAgAC0AACIFQSByIAUgBUHBAGtB/wFxQRpJG0H/AXFHDQEgAUEFRgRAQQchAQw/CyABQQFqIQEgBCAAQQFqIgBHDQALIAMgBjYCAAxZCyADQQA2AgAgACEBDAULQTohAiAEIAEiAEYNVyAEIAFrIAMoAgAiAWohBiAAIAFrQQhqIQcCQANAIAFBtMEAai0AACAALQAAIgVBIHIgBSAFQcEAa0H/AXFBGkkbQf8BcUcNASABQQhGBEBBBSEBDD4LIAFBAWohASAEIABBAWoiAEcNAAsgAyAGNgIADFgLIANBADYCACAAIQEMBAtBOSECIAQgASIARg1WIAQgAWsgAygCACIBaiEGIAAgAWtBA2ohBwJAA0AgAUGwwQBqLQAAIAAtAAAiBUEgciAFIAVBwQBrQf8BcUEaSRtB/wFxRw0BIAFBA0YEQEEGIQEMPQsgAUEBaiEBIAQgAEEBaiIARw0ACyADIAY2AgAMVwsgA0EANgIAIAAhAQwDCwJAA0AgAS0AACIAQSBHBEAgAEEKaw4EBwQEBwILIAQgAUEBaiIBRw0AC0E4IQIMVgsgAEEsRw0BIAFBAWohAEEBIQECQAJAAkACQAJAIAMtACxBBWsOBAMBAgQACyAAIQEMBAtBAiEBDAELQQQhAQsgA0EBOgAsIAMgAy8BMiABcjsBMiAAIQEMAQsgAyADLwEyQQhyOwEyIAAhAQtBPiECDDsLIANBADoALAtBOSECDDkLIAEgBEYEQEE2IQIMUgsCQAJAAkACQAJAIAEtAABBCmsOBAACAgECCyADKAIEIQAgA0EANgIEIAMgACABEDEiAEUNAiADQTM2AhwgAyABNgIUIAMgADYCDEEAIQIMVQsgAygCBCEAIANBADYCBCADIAAgARAxIgBFBEAgAUEBaiEBDAYLIANBMjYCHCADIAA2AgwgAyABQQFqNgIUQQAhAgxUCyADLQAuQQFxBEBB3wEhAgw7CyADKAIEIQAgA0EANgIEIAMgACABEDEiAA0BDEkLQTQhAgw5CyADQTU2AhwgAyABNgIUIAMgADYCDEEAIQIMUQtBNSECDDcLIANBL2otAABBAXENACADQQA2AhwgAyABNgIUIANB6xY2AhAgA0EZNgIMQQAhAgxPC0EzIQIMNQsgASAERgRAQTIhAgxOCwJAIAEtAABBCkYEQCABQQFqIQEMAQsgA0EANgIcIAMgATYCFCADQZIXNgIQIANBAzYCDEEAIQIMTgtBMiECDDQLIAEgBEYEQEExIQIMTQsCQCABLQAAIgBBCUYNACAAQSBGDQBBASECAkAgAy0ALEEFaw4EBgQFAA0LIAMgAy8BMkEIcjsBMgwMCyADLQAuQQFxRQ0BIAMtACxBCEcNACADQQA6ACwLQT0hAgwyCyADQQA2AhwgAyABNgIUIANBwhY2AhAgA0EKNgIMQQAhAgxKC0ECIQIMAQtBBCECCyADQQE6ACwgAyADLwEyIAJyOwEyDAYLIAEgBEYEQEEwIQIMRwsgAS0AAEEKRgRAIAFBAWohAQwBCyADLQAuQQFxDQAgA0EANgIcIAMgATYCFCADQdwoNgIQIANBAjYCDEEAIQIMRgtBMCECDCwLIAFBAWohAUExIQIMKwsgASAERgRAQS8hAgxECyABLQAAIgBBCUcgAEEgR3FFBEAgAUEBaiEBIAMtAC5BAXENASADQQA2AhwgAyABNgIUIANBlxA2AhAgA0EKNgIMQQAhAgxEC0EBIQICQAJAAkACQAJAAkAgAy0ALEECaw4HBQQEAwECAAQLIAMgAy8BMkEIcjsBMgwDC0ECIQIMAQtBBCECCyADQQE6ACwgAyADLwEyIAJyOwEyC0EvIQIMKwsgA0EANgIcIAMgATYCFCADQYQTNgIQIANBCzYCDEEAIQIMQwtB4QEhAgwpCyABIARGBEBBLiECDEILIANBADYCBCADQRI2AgggAyABIAEQMSIADQELQS4hAgwnCyADQS02AhwgAyABNgIUIAMgADYCDEEAIQIMPwtBACEAAkAgAygCOCICRQ0AIAIoAkwiAkUNACADIAIRAAAhAAsgAEUNACAAQRVHDQEgA0HYADYCHCADIAE2AhQgA0GzGzYCECADQRU2AgxBACECDD4LQcwAIQIMJAsgA0EANgIcIAMgATYCFCADQbMONgIQIANBHTYCDEEAIQIMPAsgASAERgRAQc4AIQIMPAsgAS0AACIAQSBGDQIgAEE6Rg0BCyADQQA6ACxBCSECDCELIAMoAgQhACADQQA2AgQgAyAAIAEQMCIADQEMAgsgAy0ALkEBcQRAQd4BIQIMIAsgAygCBCEAIANBADYCBCADIAAgARAwIgBFDQIgA0EqNgIcIAMgADYCDCADIAFBAWo2AhRBACECDDgLIANBywA2AhwgAyAANgIMIAMgAUEBajYCFEEAIQIMNwsgAUEBaiEBQcAAIQIMHQsgAUEBaiEBDCwLIAEgBEYEQEErIQIMNQsCQCABLQAAQQpGBEAgAUEBaiEBDAELIAMtAC5BwABxRQ0GCyADLQAyQYABcQRAQQAhAAJAIAMoAjgiAkUNACACKAJcIgJFDQAgAyACEQAAIQALIABFDRIgAEEVRgRAIANBBTYCHCADIAE2AhQgA0GbGzYCECADQRU2AgxBACECDDYLIANBADYCHCADIAE2AhQgA0GQDjYCECADQRQ2AgxBACECDDULIANBMmohAiADEDVBACEAAkAgAygCOCIGRQ0AIAYoAigiBkUNACADIAYRAAAhAAsgAA4WAgEABAQEBAQEBAQEBAQEBAQEBAQEAwQLIANBAToAMAsgAiACLwEAQcAAcjsBAAtBKyECDBgLIANBKTYCHCADIAE2AhQgA0GsGTYCECADQRU2AgxBACECDDALIANBADYCHCADIAE2AhQgA0HlCzYCECADQRE2AgxBACECDC8LIANBADYCHCADIAE2AhQgA0GlCzYCECADQQI2AgxBACECDC4LQQEhByADLwEyIgVBCHFFBEAgAykDIEIAUiEHCwJAIAMtADAEQEEBIQAgAy0AKUEFRg0BIAVBwABxRSAHcUUNAQsCQCADLQAoIgJBAkYEQEEBIQAgAy8BNCIGQeUARg0CQQAhACAFQcAAcQ0CIAZB5ABGDQIgBkHmAGtBAkkNAiAGQcwBRg0CIAZBsAJGDQIMAQtBACEAIAVBwABxDQELQQIhACAFQQhxDQAgBUGABHEEQAJAIAJBAUcNACADLQAuQQpxDQBBBSEADAILQQQhAAwBCyAFQSBxRQRAIAMQNkEAR0ECdCEADAELQQBBAyADKQMgUBshAAsgAEEBaw4FAgAHAQMEC0ERIQIMEwsgA0EBOgAxDCkLQQAhAgJAIAMoAjgiAEUNACAAKAIwIgBFDQAgAyAAEQAAIQILIAJFDSYgAkEVRgRAIANBAzYCHCADIAE2AhQgA0HSGzYCECADQRU2AgxBACECDCsLQQAhAiADQQA2AhwgAyABNgIUIANB3Q42AhAgA0ESNgIMDCoLIANBADYCHCADIAE2AhQgA0H5IDYCECADQQ82AgxBACECDCkLQQAhAAJAIAMoAjgiAkUNACACKAIwIgJFDQAgAyACEQAAIQALIAANAQtBDiECDA4LIABBFUYEQCADQQI2AhwgAyABNgIUIANB0hs2AhAgA0EVNgIMQQAhAgwnCyADQQA2AhwgAyABNgIUIANB3Q42AhAgA0ESNgIMQQAhAgwmC0EqIQIMDAsgASAERwRAIANBCTYCCCADIAE2AgRBKSECDAwLQSYhAgwkCyADIAMpAyAiDCAEIAFrrSIKfSILQgAgCyAMWBs3AyAgCiAMVARAQSUhAgwkCyADKAIEIQAgA0EANgIEIAMgACABIAynaiIBEDIiAEUNACADQQU2AhwgAyABNgIUIAMgADYCDEEAIQIMIwtBDyECDAkLQgAhCgJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQCABLQAAQTBrDjcXFgABAgMEBQYHFBQUFBQUFAgJCgsMDRQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUDg8QERITFAtCAiEKDBYLQgMhCgwVC0IEIQoMFAtCBSEKDBMLQgYhCgwSC0IHIQoMEQtCCCEKDBALQgkhCgwPC0IKIQoMDgtCCyEKDA0LQgwhCgwMC0INIQoMCwtCDiEKDAoLQg8hCgwJC0IKIQoMCAtCCyEKDAcLQgwhCgwGC0INIQoMBQtCDiEKDAQLQg8hCgwDCyADQQA2AhwgAyABNgIUIANBnxU2AhAgA0EMNgIMQQAhAgwhCyABIARGBEBBIiECDCELQgAhCgJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkAgAS0AAEEwaw43FRQAAQIDBAUGBxYWFhYWFhYICQoLDA0WFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFg4PEBESExYLQgIhCgwUC0IDIQoMEwtCBCEKDBILQgUhCgwRC0IGIQoMEAtCByEKDA8LQgghCgwOC0IJIQoMDQtCCiEKDAwLQgshCgwLC0IMIQoMCgtCDSEKDAkLQg4hCgwIC0IPIQoMBwtCCiEKDAYLQgshCgwFC0IMIQoMBAtCDSEKDAMLQg4hCgwCC0IPIQoMAQtCASEKCyABQQFqIQEgAykDICILQv//////////D1gEQCADIAtCBIYgCoQ3AyAMAgsgA0EANgIcIAMgATYCFCADQbUJNgIQIANBDDYCDEEAIQIMHgtBJyECDAQLQSghAgwDCyADIAE6ACwgA0EANgIAIAdBAWohAUEMIQIMAgsgA0EANgIAIAZBAWohAUEKIQIMAQsgAUEBaiEBQQghAgwACwALQQAhAiADQQA2AhwgAyABNgIUIANBsjg2AhAgA0EINgIMDBcLQQAhAiADQQA2AhwgAyABNgIUIANBgxE2AhAgA0EJNgIMDBYLQQAhAiADQQA2AhwgAyABNgIUIANB3wo2AhAgA0EJNgIMDBULQQAhAiADQQA2AhwgAyABNgIUIANB7RA2AhAgA0EJNgIMDBQLQQAhAiADQQA2AhwgAyABNgIUIANB0hE2AhAgA0EJNgIMDBMLQQAhAiADQQA2AhwgAyABNgIUIANBsjg2AhAgA0EINgIMDBILQQAhAiADQQA2AhwgAyABNgIUIANBgxE2AhAgA0EJNgIMDBELQQAhAiADQQA2AhwgAyABNgIUIANB3wo2AhAgA0EJNgIMDBALQQAhAiADQQA2AhwgAyABNgIUIANB7RA2AhAgA0EJNgIMDA8LQQAhAiADQQA2AhwgAyABNgIUIANB0hE2AhAgA0EJNgIMDA4LQQAhAiADQQA2AhwgAyABNgIUIANBuRc2AhAgA0EPNgIMDA0LQQAhAiADQQA2AhwgAyABNgIUIANBuRc2AhAgA0EPNgIMDAwLQQAhAiADQQA2AhwgAyABNgIUIANBmRM2AhAgA0ELNgIMDAsLQQAhAiADQQA2AhwgAyABNgIUIANBnQk2AhAgA0ELNgIMDAoLQQAhAiADQQA2AhwgAyABNgIUIANBlxA2AhAgA0EKNgIMDAkLQQAhAiADQQA2AhwgAyABNgIUIANBsRA2AhAgA0EKNgIMDAgLQQAhAiADQQA2AhwgAyABNgIUIANBux02AhAgA0ECNgIMDAcLQQAhAiADQQA2AhwgAyABNgIUIANBlhY2AhAgA0ECNgIMDAYLQQAhAiADQQA2AhwgAyABNgIUIANB+Rg2AhAgA0ECNgIMDAULQQAhAiADQQA2AhwgAyABNgIUIANBxBg2AhAgA0ECNgIMDAQLIANBAjYCHCADIAE2AhQgA0GpHjYCECADQRY2AgxBACECDAMLQd4AIQIgASAERg0CIAlBCGohByADKAIAIQUCQAJAIAEgBEcEQCAFQZbIAGohCCAEIAVqIAFrIQYgBUF/c0EKaiIFIAFqIQADQCABLQAAIAgtAABHBEBBAiEIDAMLIAVFBEBBACEIIAAhAQwDCyAFQQFrIQUgCEEBaiEIIAQgAUEBaiIBRw0ACyAGIQUgBCEBCyAHQQE2AgAgAyAFNgIADAELIANBADYCACAHIAg2AgALIAcgATYCBCAJKAIMIQACQAJAIAkoAghBAWsOAgQBAAsgA0EANgIcIANBwh42AhAgA0EXNgIMIAMgAEEBajYCFEEAIQIMAwsgA0EANgIcIAMgADYCFCADQdceNgIQIANBCTYCDEEAIQIMAgsgASAERgRAQSghAgwCCyADQQk2AgggAyABNgIEQSchAgwBCyABIARGBEBBASECDAELA0ACQAJAAkAgAS0AAEEKaw4EAAEBAAELIAFBAWohAQwBCyABQQFqIQEgAy0ALkEgcQ0AQQAhAiADQQA2AhwgAyABNgIUIANBoSE2AhAgA0EFNgIMDAILQQEhAiABIARHDQALCyAJQRBqJAAgAkUEQCADKAIMIQAMAQsgAyACNgIcQQAhACADKAIEIgFFDQAgAyABIAQgAygCCBEBACIBRQ0AIAMgBDYCFCADIAE2AgwgASEACyAAC74CAQJ/IABBADoAACAAQeQAaiIBQQFrQQA6AAAgAEEAOgACIABBADoAASABQQNrQQA6AAAgAUECa0EAOgAAIABBADoAAyABQQRrQQA6AABBACAAa0EDcSIBIABqIgBBADYCAEHkACABa0F8cSICIABqIgFBBGtBADYCAAJAIAJBCUkNACAAQQA2AgggAEEANgIEIAFBCGtBADYCACABQQxrQQA2AgAgAkEZSQ0AIABBADYCGCAAQQA2AhQgAEEANgIQIABBADYCDCABQRBrQQA2AgAgAUEUa0EANgIAIAFBGGtBADYCACABQRxrQQA2AgAgAiAAQQRxQRhyIgJrIgFBIEkNACAAIAJqIQADQCAAQgA3AxggAEIANwMQIABCADcDCCAAQgA3AwAgAEEgaiEAIAFBIGsiAUEfSw0ACwsLVgEBfwJAIAAoAgwNAAJAAkACQAJAIAAtADEOAwEAAwILIAAoAjgiAUUNACABKAIwIgFFDQAgACABEQAAIgENAwtBAA8LAAsgAEHKGTYCEEEOIQELIAELGgAgACgCDEUEQCAAQd4fNgIQIABBFTYCDAsLFAAgACgCDEEVRgRAIABBADYCDAsLFAAgACgCDEEWRgRAIABBADYCDAsLBwAgACgCDAsHACAAKAIQCwkAIAAgATYCEAsHACAAKAIUCysAAkAgAEEnTw0AQv//////CSAArYhCAYNQDQAgAEECdEHQOGooAgAPCwALFwAgAEEvTwRAAAsgAEECdEHsOWooAgALvwkBAX9B9C0hAQJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAIABB5ABrDvQDY2IAAWFhYWFhYQIDBAVhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhBgcICQoLDA0OD2FhYWFhEGFhYWFhYWFhYWFhEWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYRITFBUWFxgZGhthYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhHB0eHyAhIiMkJSYnKCkqKywtLi8wMTIzNDU2YTc4OTphYWFhYWFhYTthYWE8YWFhYT0+P2FhYWFhYWFhQGFhQWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYUJDREVGR0hJSktMTU5PUFFSU2FhYWFhYWFhVFVWV1hZWlthXF1hYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFeYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhX2BhC0HqLA8LQZgmDwtB7TEPC0GgNw8LQckpDwtBtCkPC0GWLQ8LQesrDwtBojUPC0HbNA8LQeApDwtB4yQPC0HVJA8LQe4kDwtB5iUPC0HKNA8LQdA3DwtBqjUPC0H1LA8LQfYmDwtBgiIPC0HyMw8LQb4oDwtB5zcPC0HNIQ8LQcAhDwtBuCUPC0HLJQ8LQZYkDwtBjzQPC0HNNQ8LQd0qDwtB7jMPC0GcNA8LQZ4xDwtB9DUPC0HlIg8LQa8lDwtBmTEPC0GyNg8LQfk2DwtBxDIPC0HdLA8LQYIxDwtBwTEPC0GNNw8LQckkDwtB7DYPC0HnKg8LQcgjDwtB4iEPC0HJNw8LQaUiDwtBlCIPC0HbNg8LQd41DwtBhiYPC0G8Kw8LQYsyDwtBoCMPC0H2MA8LQYAsDwtBiSsPC0GkJg8LQfIjDwtBgSgPC0GrMg8LQesnDwtBwjYPC0GiJA8LQc8qDwtB3CMPC0GHJw8LQeQ0DwtBtyIPC0GtMQ8LQdUiDwtBrzQPC0HeJg8LQdYyDwtB9DQPC0GBOA8LQfQ3DwtBkjYPC0GdJw8LQYIpDwtBjSMPC0HXMQ8LQb01DwtBtDcPC0HYMA8LQbYnDwtBmjgPC0GnKg8LQcQnDwtBriMPC0H1Ig8LAAtByiYhAQsgAQsXACAAIAAvAS5B/v8DcSABQQBHcjsBLgsaACAAIAAvAS5B/f8DcSABQQBHQQF0cjsBLgsaACAAIAAvAS5B+/8DcSABQQBHQQJ0cjsBLgsaACAAIAAvAS5B9/8DcSABQQBHQQN0cjsBLgsaACAAIAAvAS5B7/8DcSABQQBHQQR0cjsBLgsaACAAIAAvAS5B3/8DcSABQQBHQQV0cjsBLgsaACAAIAAvAS5Bv/8DcSABQQBHQQZ0cjsBLgsaACAAIAAvAS5B//4DcSABQQBHQQd0cjsBLgsaACAAIAAvAS5B//0DcSABQQBHQQh0cjsBLgsaACAAIAAvAS5B//sDcSABQQBHQQl0cjsBLgs+AQJ/AkAgACgCOCIDRQ0AIAMoAgQiA0UNACAAIAEgAiABayADEQEAIgRBf0cNACAAQeESNgIQQRghBAsgBAs+AQJ/AkAgACgCOCIDRQ0AIAMoAggiA0UNACAAIAEgAiABayADEQEAIgRBf0cNACAAQfwRNgIQQRghBAsgBAs+AQJ/AkAgACgCOCIDRQ0AIAMoAgwiA0UNACAAIAEgAiABayADEQEAIgRBf0cNACAAQewKNgIQQRghBAsgBAs+AQJ/AkAgACgCOCIDRQ0AIAMoAhAiA0UNACAAIAEgAiABayADEQEAIgRBf0cNACAAQfoeNgIQQRghBAsgBAs+AQJ/AkAgACgCOCIDRQ0AIAMoAhQiA0UNACAAIAEgAiABayADEQEAIgRBf0cNACAAQcsQNgIQQRghBAsgBAs+AQJ/AkAgACgCOCIDRQ0AIAMoAhgiA0UNACAAIAEgAiABayADEQEAIgRBf0cNACAAQbcfNgIQQRghBAsgBAs+AQJ/AkAgACgCOCIDRQ0AIAMoAhwiA0UNACAAIAEgAiABayADEQEAIgRBf0cNACAAQb8VNgIQQRghBAsgBAs+AQJ/AkAgACgCOCIDRQ0AIAMoAiwiA0UNACAAIAEgAiABayADEQEAIgRBf0cNACAAQf4INgIQQRghBAsgBAs+AQJ/AkAgACgCOCIDRQ0AIAMoAiAiA0UNACAAIAEgAiABayADEQEAIgRBf0cNACAAQYwdNgIQQRghBAsgBAs+AQJ/AkAgACgCOCIDRQ0AIAMoAiQiA0UNACAAIAEgAiABayADEQEAIgRBf0cNACAAQeYVNgIQQRghBAsgBAs4ACAAAn8gAC8BMkEUcUEURgRAQQEgAC0AKEEBRg0BGiAALwE0QeUARgwBCyAALQApQQVGCzoAMAtZAQJ/AkAgAC0AKEEBRg0AIAAvATQiAUHkAGtB5ABJDQAgAUHMAUYNACABQbACRg0AIAAvATIiAEHAAHENAEEBIQIgAEGIBHFBgARGDQAgAEEocUUhAgsgAguMAQECfwJAAkACQCAALQAqRQ0AIAAtACtFDQAgAC8BMiIBQQJxRQ0BDAILIAAvATIiAUEBcUUNAQtBASECIAAtAChBAUYNACAALwE0IgBB5ABrQeQASQ0AIABBzAFGDQAgAEGwAkYNACABQcAAcQ0AQQAhAiABQYgEcUGABEYNACABQShxQQBHIQILIAILcwAgAEEQav0MAAAAAAAAAAAAAAAAAAAAAP0LAwAgAP0MAAAAAAAAAAAAAAAAAAAAAP0LAwAgAEEwav0MAAAAAAAAAAAAAAAAAAAAAP0LAwAgAEEgav0MAAAAAAAAAAAAAAAAAAAAAP0LAwAgAEH9ATYCHAsGACAAEDoLmi0BC38jAEEQayIKJABB3NUAKAIAIglFBEBBnNkAKAIAIgVFBEBBqNkAQn83AgBBoNkAQoCAhICAgMAANwIAQZzZACAKQQhqQXBxQdiq1aoFcyIFNgIAQbDZAEEANgIAQYDZAEEANgIAC0GE2QBBwNkENgIAQdTVAEHA2QQ2AgBB6NUAIAU2AgBB5NUAQX82AgBBiNkAQcCmAzYCAANAIAFBgNYAaiABQfTVAGoiAjYCACACIAFB7NUAaiIDNgIAIAFB+NUAaiADNgIAIAFBiNYAaiABQfzVAGoiAzYCACADIAI2AgAgAUGQ1gBqIAFBhNYAaiICNgIAIAIgAzYCACABQYzWAGogAjYCACABQSBqIgFBgAJHDQALQczZBEGBpgM2AgBB4NUAQazZACgCADYCAEHQ1QBBgKYDNgIAQdzVAEHI2QQ2AgBBzP8HQTg2AgBByNkEIQkLAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkAgAEHsAU0EQEHE1QAoAgAiBkEQIABBE2pBcHEgAEELSRsiBEEDdiIAdiIBQQNxBEACQCABQQFxIAByQQFzIgJBA3QiAEHs1QBqIgEgAEH01QBqKAIAIgAoAggiA0YEQEHE1QAgBkF+IAJ3cTYCAAwBCyABIAM2AgggAyABNgIMCyAAQQhqIQEgACACQQN0IgJBA3I2AgQgACACaiIAIAAoAgRBAXI2AgQMEQtBzNUAKAIAIgggBE8NASABBEACQEECIAB0IgJBACACa3IgASAAdHFoIgBBA3QiAkHs1QBqIgEgAkH01QBqKAIAIgIoAggiA0YEQEHE1QAgBkF+IAB3cSIGNgIADAELIAEgAzYCCCADIAE2AgwLIAIgBEEDcjYCBCAAQQN0IgAgBGshBSAAIAJqIAU2AgAgAiAEaiIEIAVBAXI2AgQgCARAIAhBeHFB7NUAaiEAQdjVACgCACEDAn9BASAIQQN2dCIBIAZxRQRAQcTVACABIAZyNgIAIAAMAQsgACgCCAsiASADNgIMIAAgAzYCCCADIAA2AgwgAyABNgIICyACQQhqIQFB2NUAIAQ2AgBBzNUAIAU2AgAMEQtByNUAKAIAIgtFDQEgC2hBAnRB9NcAaigCACIAKAIEQXhxIARrIQUgACECA0ACQCACKAIQIgFFBEAgAkEUaigCACIBRQ0BCyABKAIEQXhxIARrIgMgBUkhAiADIAUgAhshBSABIAAgAhshACABIQIMAQsLIAAoAhghCSAAKAIMIgMgAEcEQEHU1QAoAgAaIAMgACgCCCIBNgIIIAEgAzYCDAwQCyAAQRRqIgIoAgAiAUUEQCAAKAIQIgFFDQMgAEEQaiECCwNAIAIhByABIgNBFGoiAigCACIBDQAgA0EQaiECIAMoAhAiAQ0ACyAHQQA2AgAMDwtBfyEEIABBv39LDQAgAEETaiIBQXBxIQRByNUAKAIAIghFDQBBACAEayEFAkACQAJAAn9BACAEQYACSQ0AGkEfIARB////B0sNABogBEEmIAFBCHZnIgBrdkEBcSAAQQF0a0E+agsiBkECdEH01wBqKAIAIgJFBEBBACEBQQAhAwwBC0EAIQEgBEEZIAZBAXZrQQAgBkEfRxt0IQBBACEDA0ACQCACKAIEQXhxIARrIgcgBU8NACACIQMgByIFDQBBACEFIAIhAQwDCyABIAJBFGooAgAiByAHIAIgAEEddkEEcWpBEGooAgAiAkYbIAEgBxshASAAQQF0IQAgAg0ACwsgASADckUEQEEAIQNBAiAGdCIAQQAgAGtyIAhxIgBFDQMgAGhBAnRB9NcAaigCACEBCyABRQ0BCwNAIAEoAgRBeHEgBGsiAiAFSSEAIAIgBSAAGyEFIAEgAyAAGyEDIAEoAhAiAAR/IAAFIAFBFGooAgALIgENAAsLIANFDQAgBUHM1QAoAgAgBGtPDQAgAygCGCEHIAMgAygCDCIARwRAQdTVACgCABogACADKAIIIgE2AgggASAANgIMDA4LIANBFGoiAigCACIBRQRAIAMoAhAiAUUNAyADQRBqIQILA0AgAiEGIAEiAEEUaiICKAIAIgENACAAQRBqIQIgACgCECIBDQALIAZBADYCAAwNC0HM1QAoAgAiAyAETwRAQdjVACgCACEBAkAgAyAEayICQRBPBEAgASAEaiIAIAJBAXI2AgQgASADaiACNgIAIAEgBEEDcjYCBAwBCyABIANBA3I2AgQgASADaiIAIAAoAgRBAXI2AgRBACEAQQAhAgtBzNUAIAI2AgBB2NUAIAA2AgAgAUEIaiEBDA8LQdDVACgCACIDIARLBEAgBCAJaiIAIAMgBGsiAUEBcjYCBEHc1QAgADYCAEHQ1QAgATYCACAJIARBA3I2AgQgCUEIaiEBDA8LQQAhASAEAn9BnNkAKAIABEBBpNkAKAIADAELQajZAEJ/NwIAQaDZAEKAgISAgIDAADcCAEGc2QAgCkEMakFwcUHYqtWqBXM2AgBBsNkAQQA2AgBBgNkAQQA2AgBBgIAECyIAIARBxwBqIgVqIgZBACAAayIHcSICTwRAQbTZAEEwNgIADA8LAkBB/NgAKAIAIgFFDQBB9NgAKAIAIgggAmohACAAIAFNIAAgCEtxDQBBACEBQbTZAEEwNgIADA8LQYDZAC0AAEEEcQ0EAkACQCAJBEBBhNkAIQEDQCABKAIAIgAgCU0EQCAAIAEoAgRqIAlLDQMLIAEoAggiAQ0ACwtBABA7IgBBf0YNBSACIQZBoNkAKAIAIgFBAWsiAyAAcQRAIAIgAGsgACADakEAIAFrcWohBgsgBCAGTw0FIAZB/v///wdLDQVB/NgAKAIAIgMEQEH02AAoAgAiByAGaiEBIAEgB00NBiABIANLDQYLIAYQOyIBIABHDQEMBwsgBiADayAHcSIGQf7///8HSw0EIAYQOyEAIAAgASgCACABKAIEakYNAyAAIQELAkAgBiAEQcgAak8NACABQX9GDQBBpNkAKAIAIgAgBSAGa2pBACAAa3EiAEH+////B0sEQCABIQAMBwsgABA7QX9HBEAgACAGaiEGIAEhAAwHC0EAIAZrEDsaDAQLIAEiAEF/Rw0FDAMLQQAhAwwMC0EAIQAMCgsgAEF/Rw0CC0GA2QBBgNkAKAIAQQRyNgIACyACQf7///8HSw0BIAIQOyEAQQAQOyEBIABBf0YNASABQX9GDQEgACABTw0BIAEgAGsiBiAEQThqTQ0BC0H02ABB9NgAKAIAIAZqIgE2AgBB+NgAKAIAIAFJBEBB+NgAIAE2AgALAkACQAJAQdzVACgCACICBEBBhNkAIQEDQCAAIAEoAgAiAyABKAIEIgVqRg0CIAEoAggiAQ0ACwwCC0HU1QAoAgAiAUEARyAAIAFPcUUEQEHU1QAgADYCAAtBACEBQYjZACAGNgIAQYTZACAANgIAQeTVAEF/NgIAQejVAEGc2QAoAgA2AgBBkNkAQQA2AgADQCABQYDWAGogAUH01QBqIgI2AgAgAiABQezVAGoiAzYCACABQfjVAGogAzYCACABQYjWAGogAUH81QBqIgM2AgAgAyACNgIAIAFBkNYAaiABQYTWAGoiAjYCACACIAM2AgAgAUGM1gBqIAI2AgAgAUEgaiIBQYACRw0AC0F4IABrQQ9xIgEgAGoiAiAGQThrIgMgAWsiAUEBcjYCBEHg1QBBrNkAKAIANgIAQdDVACABNgIAQdzVACACNgIAIAAgA2pBODYCBAwCCyAAIAJNDQAgAiADSQ0AIAEoAgxBCHENAEF4IAJrQQ9xIgAgAmoiA0HQ1QAoAgAgBmoiByAAayIAQQFyNgIEIAEgBSAGajYCBEHg1QBBrNkAKAIANgIAQdDVACAANgIAQdzVACADNgIAIAIgB2pBODYCBAwBCyAAQdTVACgCAEkEQEHU1QAgADYCAAsgACAGaiEDQYTZACEBAkACQAJAA0AgAyABKAIARwRAIAEoAggiAQ0BDAILCyABLQAMQQhxRQ0BC0GE2QAhAQNAIAEoAgAiAyACTQRAIAMgASgCBGoiBSACSw0DCyABKAIIIQEMAAsACyABIAA2AgAgASABKAIEIAZqNgIEIABBeCAAa0EPcWoiCSAEQQNyNgIEIANBeCADa0EPcWoiBiAEIAlqIgRrIQEgAiAGRgRAQdzVACAENgIAQdDVAEHQ1QAoAgAgAWoiADYCACAEIABBAXI2AgQMCAtB2NUAKAIAIAZGBEBB2NUAIAQ2AgBBzNUAQczVACgCACABaiIANgIAIAQgAEEBcjYCBCAAIARqIAA2AgAMCAsgBigCBCIFQQNxQQFHDQYgBUF4cSEIIAVB/wFNBEAgBUEDdiEDIAYoAggiACAGKAIMIgJGBEBBxNUAQcTVACgCAEF+IAN3cTYCAAwHCyACIAA2AgggACACNgIMDAYLIAYoAhghByAGIAYoAgwiAEcEQCAAIAYoAggiAjYCCCACIAA2AgwMBQsgBkEUaiICKAIAIgVFBEAgBigCECIFRQ0EIAZBEGohAgsDQCACIQMgBSIAQRRqIgIoAgAiBQ0AIABBEGohAiAAKAIQIgUNAAsgA0EANgIADAQLQXggAGtBD3EiASAAaiIHIAZBOGsiAyABayIBQQFyNgIEIAAgA2pBODYCBCACIAVBNyAFa0EPcWpBP2siAyADIAJBEGpJGyIDQSM2AgRB4NUAQazZACgCADYCAEHQ1QAgATYCAEHc1QAgBzYCACADQRBqQYzZACkCADcCACADQYTZACkCADcCCEGM2QAgA0EIajYCAEGI2QAgBjYCAEGE2QAgADYCAEGQ2QBBADYCACADQSRqIQEDQCABQQc2AgAgBSABQQRqIgFLDQALIAIgA0YNACADIAMoAgRBfnE2AgQgAyADIAJrIgU2AgAgAiAFQQFyNgIEIAVB/wFNBEAgBUF4cUHs1QBqIQACf0HE1QAoAgAiAUEBIAVBA3Z0IgNxRQRAQcTVACABIANyNgIAIAAMAQsgACgCCAsiASACNgIMIAAgAjYCCCACIAA2AgwgAiABNgIIDAELQR8hASAFQf///wdNBEAgBUEmIAVBCHZnIgBrdkEBcSAAQQF0a0E+aiEBCyACIAE2AhwgAkIANwIQIAFBAnRB9NcAaiEAQcjVACgCACIDQQEgAXQiBnFFBEAgACACNgIAQcjVACADIAZyNgIAIAIgADYCGCACIAI2AgggAiACNgIMDAELIAVBGSABQQF2a0EAIAFBH0cbdCEBIAAoAgAhAwJAA0AgAyIAKAIEQXhxIAVGDQEgAUEddiEDIAFBAXQhASAAIANBBHFqQRBqIgYoAgAiAw0ACyAGIAI2AgAgAiAANgIYIAIgAjYCDCACIAI2AggMAQsgACgCCCIBIAI2AgwgACACNgIIIAJBADYCGCACIAA2AgwgAiABNgIIC0HQ1QAoAgAiASAETQ0AQdzVACgCACIAIARqIgIgASAEayIBQQFyNgIEQdDVACABNgIAQdzVACACNgIAIAAgBEEDcjYCBCAAQQhqIQEMCAtBACEBQbTZAEEwNgIADAcLQQAhAAsgB0UNAAJAIAYoAhwiAkECdEH01wBqIgMoAgAgBkYEQCADIAA2AgAgAA0BQcjVAEHI1QAoAgBBfiACd3E2AgAMAgsgB0EQQRQgBygCECAGRhtqIAA2AgAgAEUNAQsgACAHNgIYIAYoAhAiAgRAIAAgAjYCECACIAA2AhgLIAZBFGooAgAiAkUNACAAQRRqIAI2AgAgAiAANgIYCyABIAhqIQEgBiAIaiIGKAIEIQULIAYgBUF+cTYCBCABIARqIAE2AgAgBCABQQFyNgIEIAFB/wFNBEAgAUF4cUHs1QBqIQACf0HE1QAoAgAiAkEBIAFBA3Z0IgFxRQRAQcTVACABIAJyNgIAIAAMAQsgACgCCAsiASAENgIMIAAgBDYCCCAEIAA2AgwgBCABNgIIDAELQR8hBSABQf///wdNBEAgAUEmIAFBCHZnIgBrdkEBcSAAQQF0a0E+aiEFCyAEIAU2AhwgBEIANwIQIAVBAnRB9NcAaiEAQcjVACgCACICQQEgBXQiA3FFBEAgACAENgIAQcjVACACIANyNgIAIAQgADYCGCAEIAQ2AgggBCAENgIMDAELIAFBGSAFQQF2a0EAIAVBH0cbdCEFIAAoAgAhAAJAA0AgACICKAIEQXhxIAFGDQEgBUEddiEAIAVBAXQhBSACIABBBHFqQRBqIgMoAgAiAA0ACyADIAQ2AgAgBCACNgIYIAQgBDYCDCAEIAQ2AggMAQsgAigCCCIAIAQ2AgwgAiAENgIIIARBADYCGCAEIAI2AgwgBCAANgIICyAJQQhqIQEMAgsCQCAHRQ0AAkAgAygCHCIBQQJ0QfTXAGoiAigCACADRgRAIAIgADYCACAADQFByNUAIAhBfiABd3EiCDYCAAwCCyAHQRBBFCAHKAIQIANGG2ogADYCACAARQ0BCyAAIAc2AhggAygCECIBBEAgACABNgIQIAEgADYCGAsgA0EUaigCACIBRQ0AIABBFGogATYCACABIAA2AhgLAkAgBUEPTQRAIAMgBCAFaiIAQQNyNgIEIAAgA2oiACAAKAIEQQFyNgIEDAELIAMgBGoiAiAFQQFyNgIEIAMgBEEDcjYCBCACIAVqIAU2AgAgBUH/AU0EQCAFQXhxQezVAGohAAJ/QcTVACgCACIBQQEgBUEDdnQiBXFFBEBBxNUAIAEgBXI2AgAgAAwBCyAAKAIICyIBIAI2AgwgACACNgIIIAIgADYCDCACIAE2AggMAQtBHyEBIAVB////B00EQCAFQSYgBUEIdmciAGt2QQFxIABBAXRrQT5qIQELIAIgATYCHCACQgA3AhAgAUECdEH01wBqIQBBASABdCIEIAhxRQRAIAAgAjYCAEHI1QAgBCAIcjYCACACIAA2AhggAiACNgIIIAIgAjYCDAwBCyAFQRkgAUEBdmtBACABQR9HG3QhASAAKAIAIQQCQANAIAQiACgCBEF4cSAFRg0BIAFBHXYhBCABQQF0IQEgACAEQQRxakEQaiIGKAIAIgQNAAsgBiACNgIAIAIgADYCGCACIAI2AgwgAiACNgIIDAELIAAoAggiASACNgIMIAAgAjYCCCACQQA2AhggAiAANgIMIAIgATYCCAsgA0EIaiEBDAELAkAgCUUNAAJAIAAoAhwiAUECdEH01wBqIgIoAgAgAEYEQCACIAM2AgAgAw0BQcjVACALQX4gAXdxNgIADAILIAlBEEEUIAkoAhAgAEYbaiADNgIAIANFDQELIAMgCTYCGCAAKAIQIgEEQCADIAE2AhAgASADNgIYCyAAQRRqKAIAIgFFDQAgA0EUaiABNgIAIAEgAzYCGAsCQCAFQQ9NBEAgACAEIAVqIgFBA3I2AgQgACABaiIBIAEoAgRBAXI2AgQMAQsgACAEaiIHIAVBAXI2AgQgACAEQQNyNgIEIAUgB2ogBTYCACAIBEAgCEF4cUHs1QBqIQFB2NUAKAIAIQMCf0EBIAhBA3Z0IgIgBnFFBEBBxNUAIAIgBnI2AgAgAQwBCyABKAIICyICIAM2AgwgASADNgIIIAMgATYCDCADIAI2AggLQdjVACAHNgIAQczVACAFNgIACyAAQQhqIQELIApBEGokACABC0MAIABFBEA/AEEQdA8LAkAgAEH//wNxDQAgAEEASA0AIABBEHZAACIAQX9GBEBBtNkAQTA2AgBBfw8LIABBEHQPCwALC5lCIgBBgAgLDQEAAAAAAAAAAgAAAAMAQZgICwUEAAAABQBBqAgLCQYAAAAHAAAACABB5AgLwjJJbnZhbGlkIGNoYXIgaW4gdXJsIHF1ZXJ5AFNwYW4gY2FsbGJhY2sgZXJyb3IgaW4gb25fYm9keQBDb250ZW50LUxlbmd0aCBvdmVyZmxvdwBDaHVuayBzaXplIG92ZXJmbG93AEludmFsaWQgbWV0aG9kIGZvciBIVFRQL3gueCByZXF1ZXN0AEludmFsaWQgbWV0aG9kIGZvciBSVFNQL3gueCByZXF1ZXN0AEV4cGVjdGVkIFNPVVJDRSBtZXRob2QgZm9yIElDRS94LnggcmVxdWVzdABJbnZhbGlkIGNoYXIgaW4gdXJsIGZyYWdtZW50IHN0YXJ0AEV4cGVjdGVkIGRvdABTcGFuIGNhbGxiYWNrIGVycm9yIGluIG9uX3N0YXR1cwBJbnZhbGlkIHJlc3BvbnNlIHN0YXR1cwBFeHBlY3RlZCBMRiBhZnRlciBoZWFkZXJzAEludmFsaWQgY2hhcmFjdGVyIGluIGNodW5rIGV4dGVuc2lvbnMAVXNlciBjYWxsYmFjayBlcnJvcgBgb25fcmVzZXRgIGNhbGxiYWNrIGVycm9yAGBvbl9jaHVua19oZWFkZXJgIGNhbGxiYWNrIGVycm9yAGBvbl9tZXNzYWdlX2JlZ2luYCBjYWxsYmFjayBlcnJvcgBgb25fY2h1bmtfZXh0ZW5zaW9uX3ZhbHVlYCBjYWxsYmFjayBlcnJvcgBgb25fc3RhdHVzX2NvbXBsZXRlYCBjYWxsYmFjayBlcnJvcgBgb25fdmVyc2lvbl9jb21wbGV0ZWAgY2FsbGJhY2sgZXJyb3IAYG9uX3VybF9jb21wbGV0ZWAgY2FsbGJhY2sgZXJyb3IAYG9uX3Byb3RvY29sX2NvbXBsZXRlYCBjYWxsYmFjayBlcnJvcgBgb25fY2h1bmtfY29tcGxldGVgIGNhbGxiYWNrIGVycm9yAGBvbl9oZWFkZXJfdmFsdWVfY29tcGxldGVgIGNhbGxiYWNrIGVycm9yAGBvbl9tZXNzYWdlX2NvbXBsZXRlYCBjYWxsYmFjayBlcnJvcgBgb25fbWV0aG9kX2NvbXBsZXRlYCBjYWxsYmFjayBlcnJvcgBgb25faGVhZGVyX2ZpZWxkX2NvbXBsZXRlYCBjYWxsYmFjayBlcnJvcgBgb25fY2h1bmtfZXh0ZW5zaW9uX25hbWVgIGNhbGxiYWNrIGVycm9yAFVuZXhwZWN0ZWQgY2hhciBpbiB1cmwgc2VydmVyAEludmFsaWQgaGVhZGVyIHZhbHVlIGNoYXIASW52YWxpZCBoZWFkZXIgZmllbGQgY2hhcgBTcGFuIGNhbGxiYWNrIGVycm9yIGluIG9uX3ZlcnNpb24ASW52YWxpZCBtaW5vciB2ZXJzaW9uAEludmFsaWQgbWFqb3IgdmVyc2lvbgBFeHBlY3RlZCBzcGFjZSBhZnRlciB2ZXJzaW9uAEV4cGVjdGVkIENSTEYgYWZ0ZXIgdmVyc2lvbgBJbnZhbGlkIEhUVFAgdmVyc2lvbgBJbnZhbGlkIGhlYWRlciB0b2tlbgBTcGFuIGNhbGxiYWNrIGVycm9yIGluIG9uX3VybABJbnZhbGlkIGNoYXJhY3RlcnMgaW4gdXJsAFVuZXhwZWN0ZWQgc3RhcnQgY2hhciBpbiB1cmwARG91YmxlIEAgaW4gdXJsAFNwYW4gY2FsbGJhY2sgZXJyb3IgaW4gb25fcHJvdG9jb2wARW1wdHkgQ29udGVudC1MZW5ndGgASW52YWxpZCBjaGFyYWN0ZXIgaW4gQ29udGVudC1MZW5ndGgAVHJhbnNmZXItRW5jb2RpbmcgY2FuJ3QgYmUgcHJlc2VudCB3aXRoIENvbnRlbnQtTGVuZ3RoAER1cGxpY2F0ZSBDb250ZW50LUxlbmd0aABJbnZhbGlkIGNoYXIgaW4gdXJsIHBhdGgAQ29udGVudC1MZW5ndGggY2FuJ3QgYmUgcHJlc2VudCB3aXRoIFRyYW5zZmVyLUVuY29kaW5nAE1pc3NpbmcgZXhwZWN0ZWQgQ1IgYWZ0ZXIgY2h1bmsgc2l6ZQBFeHBlY3RlZCBMRiBhZnRlciBjaHVuayBzaXplAEludmFsaWQgY2hhcmFjdGVyIGluIGNodW5rIHNpemUAU3BhbiBjYWxsYmFjayBlcnJvciBpbiBvbl9oZWFkZXJfdmFsdWUAU3BhbiBjYWxsYmFjayBlcnJvciBpbiBvbl9jaHVua19leHRlbnNpb25fdmFsdWUASW52YWxpZCBjaGFyYWN0ZXIgaW4gY2h1bmsgZXh0ZW5zaW9ucyB2YWx1ZQBVbmV4cGVjdGVkIHdoaXRlc3BhY2UgYWZ0ZXIgaGVhZGVyIHZhbHVlAE1pc3NpbmcgZXhwZWN0ZWQgQ1IgYWZ0ZXIgaGVhZGVyIHZhbHVlAE1pc3NpbmcgZXhwZWN0ZWQgTEYgYWZ0ZXIgaGVhZGVyIHZhbHVlAEludmFsaWQgYFRyYW5zZmVyLUVuY29kaW5nYCBoZWFkZXIgdmFsdWUATWlzc2luZyBleHBlY3RlZCBDUiBhZnRlciBjaHVuayBleHRlbnNpb24gdmFsdWUASW52YWxpZCBjaGFyYWN0ZXIgaW4gY2h1bmsgZXh0ZW5zaW9ucyBxdW90ZSB2YWx1ZQBJbnZhbGlkIHF1b3RlZC1wYWlyIGluIGNodW5rIGV4dGVuc2lvbnMgcXVvdGVkIHZhbHVlAEludmFsaWQgY2hhcmFjdGVyIGluIGNodW5rIGV4dGVuc2lvbnMgcXVvdGVkIHZhbHVlAFBhdXNlZCBieSBvbl9oZWFkZXJzX2NvbXBsZXRlAEludmFsaWQgRU9GIHN0YXRlAG9uX3Jlc2V0IHBhdXNlAG9uX2NodW5rX2hlYWRlciBwYXVzZQBvbl9tZXNzYWdlX2JlZ2luIHBhdXNlAG9uX2NodW5rX2V4dGVuc2lvbl92YWx1ZSBwYXVzZQBvbl9zdGF0dXNfY29tcGxldGUgcGF1c2UAb25fdmVyc2lvbl9jb21wbGV0ZSBwYXVzZQBvbl91cmxfY29tcGxldGUgcGF1c2UAb25fcHJvdG9jb2xfY29tcGxldGUgcGF1c2UAb25fY2h1bmtfY29tcGxldGUgcGF1c2UAb25faGVhZGVyX3ZhbHVlX2NvbXBsZXRlIHBhdXNlAG9uX21lc3NhZ2VfY29tcGxldGUgcGF1c2UAb25fbWV0aG9kX2NvbXBsZXRlIHBhdXNlAG9uX2hlYWRlcl9maWVsZF9jb21wbGV0ZSBwYXVzZQBvbl9jaHVua19leHRlbnNpb25fbmFtZSBwYXVzZQBVbmV4cGVjdGVkIHNwYWNlIGFmdGVyIHN0YXJ0IGxpbmUATWlzc2luZyBleHBlY3RlZCBDUiBhZnRlciByZXNwb25zZSBsaW5lAFNwYW4gY2FsbGJhY2sgZXJyb3IgaW4gb25fY2h1bmtfZXh0ZW5zaW9uX25hbWUASW52YWxpZCBjaGFyYWN0ZXIgaW4gY2h1bmsgZXh0ZW5zaW9ucyBuYW1lAE1pc3NpbmcgZXhwZWN0ZWQgQ1IgYWZ0ZXIgY2h1bmsgZXh0ZW5zaW9uIG5hbWUASW52YWxpZCBzdGF0dXMgY29kZQBQYXVzZSBvbiBDT05ORUNUL1VwZ3JhZGUAUGF1c2Ugb24gUFJJL1VwZ3JhZGUARXhwZWN0ZWQgSFRUUC8yIENvbm5lY3Rpb24gUHJlZmFjZQBTcGFuIGNhbGxiYWNrIGVycm9yIGluIG9uX21ldGhvZABFeHBlY3RlZCBzcGFjZSBhZnRlciBtZXRob2QAU3BhbiBjYWxsYmFjayBlcnJvciBpbiBvbl9oZWFkZXJfZmllbGQAUGF1c2VkAEludmFsaWQgd29yZCBlbmNvdW50ZXJlZABJbnZhbGlkIG1ldGhvZCBlbmNvdW50ZXJlZABNaXNzaW5nIGV4cGVjdGVkIENSIGFmdGVyIGNodW5rIGRhdGEARXhwZWN0ZWQgTEYgYWZ0ZXIgY2h1bmsgZGF0YQBVbmV4cGVjdGVkIGNoYXIgaW4gdXJsIHNjaGVtYQBSZXF1ZXN0IGhhcyBpbnZhbGlkIGBUcmFuc2Zlci1FbmNvZGluZ2AARGF0YSBhZnRlciBgQ29ubmVjdGlvbjogY2xvc2VgAFNXSVRDSF9QUk9YWQBVU0VfUFJPWFkATUtBQ1RJVklUWQBVTlBST0NFU1NBQkxFX0VOVElUWQBRVUVSWQBDT1BZAE1PVkVEX1BFUk1BTkVOVExZAFRPT19FQVJMWQBOT1RJRlkARkFJTEVEX0RFUEVOREVOQ1kAQkFEX0dBVEVXQVkAUExBWQBQVVQAQ0hFQ0tPVVQAR0FURVdBWV9USU1FT1VUAFJFUVVFU1RfVElNRU9VVABORVRXT1JLX0NPTk5FQ1RfVElNRU9VVABDT05ORUNUSU9OX1RJTUVPVVQATE9HSU5fVElNRU9VVABORVRXT1JLX1JFQURfVElNRU9VVABQT1NUAE1JU0RJUkVDVEVEX1JFUVVFU1QAQ0xJRU5UX0NMT1NFRF9SRVFVRVNUAENMSUVOVF9DTE9TRURfTE9BRF9CQUxBTkNFRF9SRVFVRVNUAEJBRF9SRVFVRVNUAEhUVFBfUkVRVUVTVF9TRU5UX1RPX0hUVFBTX1BPUlQAUkVQT1JUAElNX0FfVEVBUE9UAFJFU0VUX0NPTlRFTlQATk9fQ09OVEVOVABQQVJUSUFMX0NPTlRFTlQASFBFX0lOVkFMSURfQ09OU1RBTlQASFBFX0NCX1JFU0VUAEdFVABIUEVfU1RSSUNUAENPTkZMSUNUAFRFTVBPUkFSWV9SRURJUkVDVABQRVJNQU5FTlRfUkVESVJFQ1QAQ09OTkVDVABNVUxUSV9TVEFUVVMASFBFX0lOVkFMSURfU1RBVFVTAFRPT19NQU5ZX1JFUVVFU1RTAEVBUkxZX0hJTlRTAFVOQVZBSUxBQkxFX0ZPUl9MRUdBTF9SRUFTT05TAE9QVElPTlMAU1dJVENISU5HX1BST1RPQ09MUwBWQVJJQU5UX0FMU09fTkVHT1RJQVRFUwBNVUxUSVBMRV9DSE9JQ0VTAElOVEVSTkFMX1NFUlZFUl9FUlJPUgBXRUJfU0VSVkVSX1VOS05PV05fRVJST1IAUkFJTEdVTl9FUlJPUgBJREVOVElUWV9QUk9WSURFUl9BVVRIRU5USUNBVElPTl9FUlJPUgBTU0xfQ0VSVElGSUNBVEVfRVJST1IASU5WQUxJRF9YX0ZPUldBUkRFRF9GT1IAU0VUX1BBUkFNRVRFUgBHRVRfUEFSQU1FVEVSAEhQRV9VU0VSAFNFRV9PVEhFUgBIUEVfQ0JfQ0hVTktfSEVBREVSAEV4cGVjdGVkIExGIGFmdGVyIENSAE1LQ0FMRU5EQVIAU0VUVVAAV0VCX1NFUlZFUl9JU19ET1dOAFRFQVJET1dOAEhQRV9DTE9TRURfQ09OTkVDVElPTgBIRVVSSVNUSUNfRVhQSVJBVElPTgBESVNDT05ORUNURURfT1BFUkFUSU9OAE5PTl9BVVRIT1JJVEFUSVZFX0lORk9STUFUSU9OAEhQRV9JTlZBTElEX1ZFUlNJT04ASFBFX0NCX01FU1NBR0VfQkVHSU4AU0lURV9JU19GUk9aRU4ASFBFX0lOVkFMSURfSEVBREVSX1RPS0VOAElOVkFMSURfVE9LRU4ARk9SQklEREVOAEVOSEFOQ0VfWU9VUl9DQUxNAEhQRV9JTlZBTElEX1VSTABCTE9DS0VEX0JZX1BBUkVOVEFMX0NPTlRST0wATUtDT0wAQUNMAEhQRV9JTlRFUk5BTABSRVFVRVNUX0hFQURFUl9GSUVMRFNfVE9PX0xBUkdFX1VOT0ZGSUNJQUwASFBFX09LAFVOTElOSwBVTkxPQ0sAUFJJAFJFVFJZX1dJVEgASFBFX0lOVkFMSURfQ09OVEVOVF9MRU5HVEgASFBFX1VORVhQRUNURURfQ09OVEVOVF9MRU5HVEgARkxVU0gAUFJPUFBBVENIAE0tU0VBUkNIAFVSSV9UT09fTE9ORwBQUk9DRVNTSU5HAE1JU0NFTExBTkVPVVNfUEVSU0lTVEVOVF9XQVJOSU5HAE1JU0NFTExBTkVPVVNfV0FSTklORwBIUEVfSU5WQUxJRF9UUkFOU0ZFUl9FTkNPRElORwBFeHBlY3RlZCBDUkxGAEhQRV9JTlZBTElEX0NIVU5LX1NJWkUATU9WRQBDT05USU5VRQBIUEVfQ0JfU1RBVFVTX0NPTVBMRVRFAEhQRV9DQl9IRUFERVJTX0NPTVBMRVRFAEhQRV9DQl9WRVJTSU9OX0NPTVBMRVRFAEhQRV9DQl9VUkxfQ09NUExFVEUASFBFX0NCX1BST1RPQ09MX0NPTVBMRVRFAEhQRV9DQl9DSFVOS19DT01QTEVURQBIUEVfQ0JfSEVBREVSX1ZBTFVFX0NPTVBMRVRFAEhQRV9DQl9DSFVOS19FWFRFTlNJT05fVkFMVUVfQ09NUExFVEUASFBFX0NCX0NIVU5LX0VYVEVOU0lPTl9OQU1FX0NPTVBMRVRFAEhQRV9DQl9NRVNTQUdFX0NPTVBMRVRFAEhQRV9DQl9NRVRIT0RfQ09NUExFVEUASFBFX0NCX0hFQURFUl9GSUVMRF9DT01QTEVURQBERUxFVEUASFBFX0lOVkFMSURfRU9GX1NUQVRFAElOVkFMSURfU1NMX0NFUlRJRklDQVRFAFBBVVNFAE5PX1JFU1BPTlNFAFVOU1VQUE9SVEVEX01FRElBX1RZUEUAR09ORQBOT1RfQUNDRVBUQUJMRQBTRVJWSUNFX1VOQVZBSUxBQkxFAFJBTkdFX05PVF9TQVRJU0ZJQUJMRQBPUklHSU5fSVNfVU5SRUFDSEFCTEUAUkVTUE9OU0VfSVNfU1RBTEUAUFVSR0UATUVSR0UAUkVRVUVTVF9IRUFERVJfRklFTERTX1RPT19MQVJHRQBSRVFVRVNUX0hFQURFUl9UT09fTEFSR0UAUEFZTE9BRF9UT09fTEFSR0UASU5TVUZGSUNJRU5UX1NUT1JBR0UASFBFX1BBVVNFRF9VUEdSQURFAEhQRV9QQVVTRURfSDJfVVBHUkFERQBTT1VSQ0UAQU5OT1VOQ0UAVFJBQ0UASFBFX1VORVhQRUNURURfU1BBQ0UAREVTQ1JJQkUAVU5TVUJTQ1JJQkUAUkVDT1JEAEhQRV9JTlZBTElEX01FVEhPRABOT1RfRk9VTkQAUFJPUEZJTkQAVU5CSU5EAFJFQklORABVTkFVVEhPUklaRUQATUVUSE9EX05PVF9BTExPV0VEAEhUVFBfVkVSU0lPTl9OT1RfU1VQUE9SVEVEAEFMUkVBRFlfUkVQT1JURUQAQUNDRVBURUQATk9UX0lNUExFTUVOVEVEAExPT1BfREVURUNURUQASFBFX0NSX0VYUEVDVEVEAEhQRV9MRl9FWFBFQ1RFRABDUkVBVEVEAElNX1VTRUQASFBFX1BBVVNFRABUSU1FT1VUX09DQ1VSRUQAUEFZTUVOVF9SRVFVSVJFRABQUkVDT05ESVRJT05fUkVRVUlSRUQAUFJPWFlfQVVUSEVOVElDQVRJT05fUkVRVUlSRUQATkVUV09SS19BVVRIRU5USUNBVElPTl9SRVFVSVJFRABMRU5HVEhfUkVRVUlSRUQAU1NMX0NFUlRJRklDQVRFX1JFUVVJUkVEAFVQR1JBREVfUkVRVUlSRUQAUEFHRV9FWFBJUkVEAFBSRUNPTkRJVElPTl9GQUlMRUQARVhQRUNUQVRJT05fRkFJTEVEAFJFVkFMSURBVElPTl9GQUlMRUQAU1NMX0hBTkRTSEFLRV9GQUlMRUQATE9DS0VEAFRSQU5TRk9STUFUSU9OX0FQUExJRUQATk9UX01PRElGSUVEAE5PVF9FWFRFTkRFRABCQU5EV0lEVEhfTElNSVRfRVhDRUVERUQAU0lURV9JU19PVkVSTE9BREVEAEhFQUQARXhwZWN0ZWQgSFRUUC8sIFJUU1AvIG9yIElDRS8A5xUAAK8VAACkEgAAkhoAACYWAACeFAAA2xkAAHkVAAB+EgAA/hQAADYVAAALFgAA2BYAAPMSAABCGAAArBYAABIVAAAUFwAA7xcAAEgUAABxFwAAshoAAGsZAAB+GQAANRQAAIIaAABEFwAA/RYAAB4YAACHFwAAqhkAAJMSAAAHGAAALBcAAMoXAACkFwAA5xUAAOcVAABYFwAAOxgAAKASAAAtHAAAwxEAAEgRAADeEgAAQhMAAKQZAAD9EAAA9xUAAKUVAADvFgAA+BkAAEoWAABWFgAA9RUAAAoaAAAIGgAAARoAAKsVAABCEgAA1xAAAEwRAAAFGQAAVBYAAB4RAADKGQAAyBkAAE4WAAD/GAAAcRQAAPAVAADuFQAAlBkAAPwVAAC/GQAAmxkAAHwUAABDEQAAcBgAAJUUAAAnFAAAGRQAANUSAADUGQAARBYAAPcQAEG5OwsBAQBB0DsL4AEBAQIBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEDAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQBBuj0LBAEAAAIAQdE9C14DBAMDAwMDAAADAwADAwADAwMDAwMDAwMDAAUAAAAAAAMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAAAAAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMAAwADAEG6PwsEAQAAAgBB0T8LXgMAAwMDAwMAAAMDAAMDAAMDAwMDAwMDAwMABAAFAAAAAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMAAAADAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwADAAMAQbDBAAsNbG9zZWVlcC1hbGl2ZQBBycEACwEBAEHgwQAL4AEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQBBycMACwEBAEHgwwAL5wEBAQEBAQEBAQEBAQECAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAWNodW5rZWQAQfHFAAteAQABAQEBAQAAAQEAAQEAAQEBAQEBAQEBAQAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEAAQBB0McACyFlY3Rpb25lbnQtbGVuZ3Rob25yb3h5LWNvbm5lY3Rpb24AQYDIAAsgcmFuc2Zlci1lbmNvZGluZ3BncmFkZQ0KDQpTTQ0KDQoAQanIAAsFAQIAAQMAQcDIAAtfBAUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUAQanKAAsFAQIAAQMAQcDKAAtfBAUFBgUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUAQanMAAsEAQAAAQBBwcwAC14CAgACAgICAgICAgICAgICAgICAgICAgICAgICAgIAAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAEGpzgALBQECAAEDAEHAzgALXwQFAAAFBQUFBQUFBQUFBQYFBQUFBQUFBQUFBQUABQAHCAUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQAFAAUABQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUAAAAFAEGp0AALBQEBAAEBAEHA0AALAQEAQdrQAAtBAgAAAAAAAAMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAAAAAAAAAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMAQanSAAsFAQEAAQEAQcDSAAsBAQBBytIACwYCAAAAAAIAQeHSAAs6AwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMAAAAAAAADAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwBBoNQAC50BTk9VTkNFRUNLT1VUTkVDVEVURUNSSUJFTFVTSEVURUFEU0VBUkNIUkdFQ1RJVklUWUxFTkRBUlZFT1RJRllQVElPTlNDSFNFQVlTVEFUQ0hHRVVFUllPUkRJUkVDVE9SVFJDSFBBUkFNRVRFUlVSQ0VCU0NSSUJFQVJET1dOQUNFSU5ETktDS1VCU0NSSUJFVFRQQ0VUU1BBRFRQLw=='
let wasmBuffer
Object.defineProperty(module, 'exports', {
get: () => {
return wasmBuffer
? wasmBuffer
: (wasmBuffer = Buffer.from(wasmBase64, 'base64'))
}
})
================================================
FILE: lib/llhttp/utils.d.ts
================================================
import type { IntDict } from './constants';
export declare function enumToMap(obj: IntDict, filter?: readonly number[], exceptions?: readonly number[]): IntDict;
================================================
FILE: lib/llhttp/utils.js
================================================
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.enumToMap = enumToMap;
function enumToMap(obj, filter = [], exceptions = []) {
const emptyFilter = (filter?.length ?? 0) === 0;
const emptyExceptions = (exceptions?.length ?? 0) === 0;
return Object.fromEntries(Object.entries(obj).filter(([, value]) => {
return (typeof value === 'number' &&
(emptyFilter || filter.includes(value)) &&
(emptyExceptions || !exceptions.includes(value)));
}));
}
================================================
FILE: lib/mock/mock-agent.js
================================================
'use strict'
const { kClients } = require('../core/symbols')
const Agent = require('../dispatcher/agent')
const {
kAgent,
kMockAgentSet,
kMockAgentGet,
kDispatches,
kIsMockActive,
kNetConnect,
kGetNetConnect,
kOptions,
kFactory,
kMockAgentRegisterCallHistory,
kMockAgentIsCallHistoryEnabled,
kMockAgentAddCallHistoryLog,
kMockAgentMockCallHistoryInstance,
kMockAgentAcceptsNonStandardSearchParameters,
kMockCallHistoryAddLog,
kIgnoreTrailingSlash
} = require('./mock-symbols')
const MockClient = require('./mock-client')
const MockPool = require('./mock-pool')
const { matchValue, normalizeSearchParams, buildAndValidateMockOptions, normalizeOrigin } = require('./mock-utils')
const { InvalidArgumentError, UndiciError } = require('../core/errors')
const Dispatcher = require('../dispatcher/dispatcher')
const PendingInterceptorsFormatter = require('./pending-interceptors-formatter')
const { MockCallHistory } = require('./mock-call-history')
class MockAgent extends Dispatcher {
constructor (opts = {}) {
super(opts)
const mockOptions = buildAndValidateMockOptions(opts)
this[kNetConnect] = true
this[kIsMockActive] = true
this[kMockAgentIsCallHistoryEnabled] = mockOptions.enableCallHistory ?? false
this[kMockAgentAcceptsNonStandardSearchParameters] = mockOptions.acceptNonStandardSearchParameters ?? false
this[kIgnoreTrailingSlash] = mockOptions.ignoreTrailingSlash ?? false
// Instantiate Agent and encapsulate
if (opts?.agent && typeof opts.agent.dispatch !== 'function') {
throw new InvalidArgumentError('Argument opts.agent must implement Agent')
}
const agent = opts?.agent ? opts.agent : new Agent(opts)
this[kAgent] = agent
this[kClients] = agent[kClients]
this[kOptions] = mockOptions
if (this[kMockAgentIsCallHistoryEnabled]) {
this[kMockAgentRegisterCallHistory]()
}
}
get (origin) {
// Normalize origin to handle URL objects and case-insensitive hostnames
const normalizedOrigin = normalizeOrigin(origin)
const originKey = this[kIgnoreTrailingSlash] ? normalizedOrigin.replace(/\/$/, '') : normalizedOrigin
let dispatcher = this[kMockAgentGet](originKey)
if (!dispatcher) {
dispatcher = this[kFactory](originKey)
this[kMockAgentSet](originKey, dispatcher)
}
return dispatcher
}
dispatch (opts, handler) {
opts.origin = normalizeOrigin(opts.origin)
// Call MockAgent.get to perform additional setup before dispatching as normal
this.get(opts.origin)
this[kMockAgentAddCallHistoryLog](opts)
const acceptNonStandardSearchParameters = this[kMockAgentAcceptsNonStandardSearchParameters]
const dispatchOpts = { ...opts }
if (acceptNonStandardSearchParameters && dispatchOpts.path) {
const [path, searchParams] = dispatchOpts.path.split('?')
const normalizedSearchParams = normalizeSearchParams(searchParams, acceptNonStandardSearchParameters)
dispatchOpts.path = `${path}?${normalizedSearchParams}`
}
return this[kAgent].dispatch(dispatchOpts, handler)
}
async close () {
this.clearCallHistory()
await this[kAgent].close()
this[kClients].clear()
}
deactivate () {
this[kIsMockActive] = false
}
activate () {
this[kIsMockActive] = true
}
enableNetConnect (matcher) {
if (typeof matcher === 'string' || typeof matcher === 'function' || matcher instanceof RegExp) {
if (Array.isArray(this[kNetConnect])) {
this[kNetConnect].push(matcher)
} else {
this[kNetConnect] = [matcher]
}
} else if (typeof matcher === 'undefined') {
this[kNetConnect] = true
} else {
throw new InvalidArgumentError('Unsupported matcher. Must be one of String|Function|RegExp.')
}
}
disableNetConnect () {
this[kNetConnect] = false
}
enableCallHistory () {
this[kMockAgentIsCallHistoryEnabled] = true
return this
}
disableCallHistory () {
this[kMockAgentIsCallHistoryEnabled] = false
return this
}
getCallHistory () {
return this[kMockAgentMockCallHistoryInstance]
}
clearCallHistory () {
if (this[kMockAgentMockCallHistoryInstance] !== undefined) {
this[kMockAgentMockCallHistoryInstance].clear()
}
}
// This is required to bypass issues caused by using global symbols - see:
// https://github.com/nodejs/undici/issues/1447
get isMockActive () {
return this[kIsMockActive]
}
[kMockAgentRegisterCallHistory] () {
if (this[kMockAgentMockCallHistoryInstance] === undefined) {
this[kMockAgentMockCallHistoryInstance] = new MockCallHistory()
}
}
[kMockAgentAddCallHistoryLog] (opts) {
if (this[kMockAgentIsCallHistoryEnabled]) {
// additional setup when enableCallHistory class method is used after mockAgent instantiation
this[kMockAgentRegisterCallHistory]()
// add call history log on every call (intercepted or not)
this[kMockAgentMockCallHistoryInstance][kMockCallHistoryAddLog](opts)
}
}
[kMockAgentSet] (origin, dispatcher) {
this[kClients].set(origin, { count: 0, dispatcher })
}
[kFactory] (origin) {
const mockOptions = Object.assign({ agent: this }, this[kOptions])
return this[kOptions] && this[kOptions].connections === 1
? new MockClient(origin, mockOptions)
: new MockPool(origin, mockOptions)
}
[kMockAgentGet] (origin) {
// First check if we can immediately find it
const result = this[kClients].get(origin)
if (result?.dispatcher) {
return result.dispatcher
}
// If the origin is not a string create a dummy parent pool and return to user
if (typeof origin !== 'string') {
const dispatcher = this[kFactory]('http://localhost:9999')
this[kMockAgentSet](origin, dispatcher)
return dispatcher
}
// If we match, create a pool and assign the same dispatches
for (const [keyMatcher, result] of Array.from(this[kClients])) {
if (result && typeof keyMatcher !== 'string' && matchValue(keyMatcher, origin)) {
const dispatcher = this[kFactory](origin)
this[kMockAgentSet](origin, dispatcher)
dispatcher[kDispatches] = result.dispatcher[kDispatches]
return dispatcher
}
}
}
[kGetNetConnect] () {
return this[kNetConnect]
}
pendingInterceptors () {
const mockAgentClients = this[kClients]
return Array.from(mockAgentClients.entries())
.flatMap(([origin, result]) => result.dispatcher[kDispatches].map(dispatch => ({ ...dispatch, origin })))
.filter(({ pending }) => pending)
}
assertNoPendingInterceptors ({ pendingInterceptorsFormatter = new PendingInterceptorsFormatter() } = {}) {
const pending = this.pendingInterceptors()
if (pending.length === 0) {
return
}
throw new UndiciError(
pending.length === 1
? `1 interceptor is pending:\n\n${pendingInterceptorsFormatter.format(pending)}`.trim()
: `${pending.length} interceptors are pending:\n\n${pendingInterceptorsFormatter.format(pending)}`.trim()
)
}
}
module.exports = MockAgent
================================================
FILE: lib/mock/mock-call-history.js
================================================
'use strict'
const { kMockCallHistoryAddLog } = require('./mock-symbols')
const { InvalidArgumentError } = require('../core/errors')
function handleFilterCallsWithOptions (criteria, options, handler, store) {
switch (options.operator) {
case 'OR':
store.push(...handler(criteria))
return store
case 'AND':
return handler.call({ logs: store }, criteria)
default:
// guard -- should never happens because buildAndValidateFilterCallsOptions is called before
throw new InvalidArgumentError('options.operator must to be a case insensitive string equal to \'OR\' or \'AND\'')
}
}
function buildAndValidateFilterCallsOptions (options = {}) {
const finalOptions = {}
if ('operator' in options) {
if (typeof options.operator !== 'string' || (options.operator.toUpperCase() !== 'OR' && options.operator.toUpperCase() !== 'AND')) {
throw new InvalidArgumentError('options.operator must to be a case insensitive string equal to \'OR\' or \'AND\'')
}
return {
...finalOptions,
operator: options.operator.toUpperCase()
}
}
return finalOptions
}
function makeFilterCalls (parameterName) {
return (parameterValue) => {
if (typeof parameterValue === 'string' || parameterValue == null) {
return this.logs.filter((log) => {
return log[parameterName] === parameterValue
})
}
if (parameterValue instanceof RegExp) {
return this.logs.filter((log) => {
return parameterValue.test(log[parameterName])
})
}
throw new InvalidArgumentError(`${parameterName} parameter should be one of string, regexp, undefined or null`)
}
}
function computeUrlWithMaybeSearchParameters (requestInit) {
// path can contains query url parameters
// or query can contains query url parameters
try {
const url = new URL(requestInit.path, requestInit.origin)
// requestInit.path contains query url parameters
// requestInit.query is then undefined
if (url.search.length !== 0) {
return url
}
// requestInit.query can be populated here
url.search = new URLSearchParams(requestInit.query).toString()
return url
} catch (error) {
throw new InvalidArgumentError('An error occurred when computing MockCallHistoryLog.url', { cause: error })
}
}
class MockCallHistoryLog {
constructor (requestInit = {}) {
this.body = requestInit.body
this.headers = requestInit.headers
this.method = requestInit.method
const url = computeUrlWithMaybeSearchParameters(requestInit)
this.fullUrl = url.toString()
this.origin = url.origin
this.path = url.pathname
this.searchParams = Object.fromEntries(url.searchParams)
this.protocol = url.protocol
this.host = url.host
this.port = url.port
this.hash = url.hash
}
toMap () {
return new Map([
['protocol', this.protocol],
['host', this.host],
['port', this.port],
['origin', this.origin],
['path', this.path],
['hash', this.hash],
['searchParams', this.searchParams],
['fullUrl', this.fullUrl],
['method', this.method],
['body', this.body],
['headers', this.headers]]
)
}
toString () {
const options = { betweenKeyValueSeparator: '->', betweenPairSeparator: '|' }
let result = ''
this.toMap().forEach((value, key) => {
if (typeof value === 'string' || value === undefined || value === null) {
result = `${result}${key}${options.betweenKeyValueSeparator}${value}${options.betweenPairSeparator}`
}
if ((typeof value === 'object' && value !== null) || Array.isArray(value)) {
result = `${result}${key}${options.betweenKeyValueSeparator}${JSON.stringify(value)}${options.betweenPairSeparator}`
}
// maybe miss something for non Record / Array headers and searchParams here
})
// delete last betweenPairSeparator
return result.slice(0, -1)
}
}
class MockCallHistory {
logs = []
calls () {
return this.logs
}
firstCall () {
return this.logs.at(0)
}
lastCall () {
return this.logs.at(-1)
}
nthCall (number) {
if (typeof number !== 'number') {
throw new InvalidArgumentError('nthCall must be called with a number')
}
if (!Number.isInteger(number)) {
throw new InvalidArgumentError('nthCall must be called with an integer')
}
if (Math.sign(number) !== 1) {
throw new InvalidArgumentError('nthCall must be called with a positive value. use firstCall or lastCall instead')
}
// non zero based index. this is more human readable
return this.logs.at(number - 1)
}
filterCalls (criteria, options) {
// perf
if (this.logs.length === 0) {
return this.logs
}
if (typeof criteria === 'function') {
return this.logs.filter(criteria)
}
if (criteria instanceof RegExp) {
return this.logs.filter((log) => {
return criteria.test(log.toString())
})
}
if (typeof criteria === 'object' && criteria !== null) {
// no criteria - returning all logs
if (Object.keys(criteria).length === 0) {
return this.logs
}
const finalOptions = { operator: 'OR', ...buildAndValidateFilterCallsOptions(options) }
let maybeDuplicatedLogsFiltered = []
if ('protocol' in criteria) {
maybeDuplicatedLogsFiltered = handleFilterCallsWithOptions(criteria.protocol, finalOptions, this.filterCallsByProtocol, maybeDuplicatedLogsFiltered)
}
if ('host' in criteria) {
maybeDuplicatedLogsFiltered = handleFilterCallsWithOptions(criteria.host, finalOptions, this.filterCallsByHost, maybeDuplicatedLogsFiltered)
}
if ('port' in criteria) {
maybeDuplicatedLogsFiltered = handleFilterCallsWithOptions(criteria.port, finalOptions, this.filterCallsByPort, maybeDuplicatedLogsFiltered)
}
if ('origin' in criteria) {
maybeDuplicatedLogsFiltered = handleFilterCallsWithOptions(criteria.origin, finalOptions, this.filterCallsByOrigin, maybeDuplicatedLogsFiltered)
}
if ('path' in criteria) {
maybeDuplicatedLogsFiltered = handleFilterCallsWithOptions(criteria.path, finalOptions, this.filterCallsByPath, maybeDuplicatedLogsFiltered)
}
if ('hash' in criteria) {
maybeDuplicatedLogsFiltered = handleFilterCallsWithOptions(criteria.hash, finalOptions, this.filterCallsByHash, maybeDuplicatedLogsFiltered)
}
if ('fullUrl' in criteria) {
maybeDuplicatedLogsFiltered = handleFilterCallsWithOptions(criteria.fullUrl, finalOptions, this.filterCallsByFullUrl, maybeDuplicatedLogsFiltered)
}
if ('method' in criteria) {
maybeDuplicatedLogsFiltered = handleFilterCallsWithOptions(criteria.method, finalOptions, this.filterCallsByMethod, maybeDuplicatedLogsFiltered)
}
const uniqLogsFiltered = [...new Set(maybeDuplicatedLogsFiltered)]
return uniqLogsFiltered
}
throw new InvalidArgumentError('criteria parameter should be one of function, regexp, or object')
}
filterCallsByProtocol = makeFilterCalls.call(this, 'protocol')
filterCallsByHost = makeFilterCalls.call(this, 'host')
filterCallsByPort = makeFilterCalls.call(this, 'port')
filterCallsByOrigin = makeFilterCalls.call(this, 'origin')
filterCallsByPath = makeFilterCalls.call(this, 'path')
filterCallsByHash = makeFilterCalls.call(this, 'hash')
filterCallsByFullUrl = makeFilterCalls.call(this, 'fullUrl')
filterCallsByMethod = makeFilterCalls.call(this, 'method')
clear () {
this.logs = []
}
[kMockCallHistoryAddLog] (requestInit) {
const log = new MockCallHistoryLog(requestInit)
this.logs.push(log)
return log
}
* [Symbol.iterator] () {
for (const log of this.calls()) {
yield log
}
}
}
module.exports.MockCallHistory = MockCallHistory
module.exports.MockCallHistoryLog = MockCallHistoryLog
================================================
FILE: lib/mock/mock-client.js
================================================
'use strict'
const { promisify } = require('node:util')
const Client = require('../dispatcher/client')
const { buildMockDispatch } = require('./mock-utils')
const {
kDispatches,
kMockAgent,
kClose,
kOriginalClose,
kOrigin,
kOriginalDispatch,
kConnected,
kIgnoreTrailingSlash
} = require('./mock-symbols')
const { MockInterceptor } = require('./mock-interceptor')
const Symbols = require('../core/symbols')
const { InvalidArgumentError } = require('../core/errors')
/**
* MockClient provides an API that extends the Client to influence the mockDispatches.
*/
class MockClient extends Client {
constructor (origin, opts) {
if (!opts || !opts.agent || typeof opts.agent.dispatch !== 'function') {
throw new InvalidArgumentError('Argument opts.agent must implement Agent')
}
super(origin, opts)
this[kMockAgent] = opts.agent
this[kOrigin] = origin
this[kIgnoreTrailingSlash] = opts.ignoreTrailingSlash ?? false
this[kDispatches] = []
this[kConnected] = 1
this[kOriginalDispatch] = this.dispatch
this[kOriginalClose] = this.close.bind(this)
this.dispatch = buildMockDispatch.call(this)
this.close = this[kClose]
}
get [Symbols.kConnected] () {
return this[kConnected]
}
/**
* Sets up the base interceptor for mocking replies from undici.
*/
intercept (opts) {
return new MockInterceptor(
opts && { ignoreTrailingSlash: this[kIgnoreTrailingSlash], ...opts },
this[kDispatches]
)
}
cleanMocks () {
this[kDispatches] = []
}
async [kClose] () {
await promisify(this[kOriginalClose])()
this[kConnected] = 0
this[kMockAgent][Symbols.kClients].delete(this[kOrigin])
}
}
module.exports = MockClient
================================================
FILE: lib/mock/mock-errors.js
================================================
'use strict'
const { UndiciError } = require('../core/errors')
const kMockNotMatchedError = Symbol.for('undici.error.UND_MOCK_ERR_MOCK_NOT_MATCHED')
/**
* The request does not match any registered mock dispatches.
*/
class MockNotMatchedError extends UndiciError {
constructor (message) {
super(message)
this.name = 'MockNotMatchedError'
this.message = message || 'The request does not match any registered mock dispatches'
this.code = 'UND_MOCK_ERR_MOCK_NOT_MATCHED'
}
static [Symbol.hasInstance] (instance) {
return instance && instance[kMockNotMatchedError] === true
}
get [kMockNotMatchedError] () {
return true
}
}
module.exports = {
MockNotMatchedError
}
================================================
FILE: lib/mock/mock-interceptor.js
================================================
'use strict'
const { getResponseData, buildKey, addMockDispatch } = require('./mock-utils')
const {
kDispatches,
kDispatchKey,
kDefaultHeaders,
kDefaultTrailers,
kContentLength,
kMockDispatch,
kIgnoreTrailingSlash
} = require('./mock-symbols')
const { InvalidArgumentError } = require('../core/errors')
const { serializePathWithQuery } = require('../core/util')
/**
* Defines the scope API for an interceptor reply
*/
class MockScope {
constructor (mockDispatch) {
this[kMockDispatch] = mockDispatch
}
/**
* Delay a reply by a set amount in ms.
*/
delay (waitInMs) {
if (typeof waitInMs !== 'number' || !Number.isInteger(waitInMs) || waitInMs <= 0) {
throw new InvalidArgumentError('waitInMs must be a valid integer > 0')
}
this[kMockDispatch].delay = waitInMs
return this
}
/**
* For a defined reply, never mark as consumed.
*/
persist () {
this[kMockDispatch].persist = true
return this
}
/**
* Allow one to define a reply for a set amount of matching requests.
*/
times (repeatTimes) {
if (typeof repeatTimes !== 'number' || !Number.isInteger(repeatTimes) || repeatTimes <= 0) {
throw new InvalidArgumentError('repeatTimes must be a valid integer > 0')
}
this[kMockDispatch].times = repeatTimes
return this
}
}
/**
* Defines an interceptor for a Mock
*/
class MockInterceptor {
constructor (opts, mockDispatches) {
if (typeof opts !== 'object') {
throw new InvalidArgumentError('opts must be an object')
}
if (typeof opts.path === 'undefined') {
throw new InvalidArgumentError('opts.path must be defined')
}
if (typeof opts.method === 'undefined') {
opts.method = 'GET'
}
// See https://github.com/nodejs/undici/issues/1245
// As per RFC 3986, clients are not supposed to send URI
// fragments to servers when they retrieve a document,
if (typeof opts.path === 'string') {
if (opts.query) {
opts.path = serializePathWithQuery(opts.path, opts.query)
} else {
// Matches https://github.com/nodejs/undici/blob/main/lib/web/fetch/index.js#L1811
const parsedURL = new URL(opts.path, 'data://')
opts.path = parsedURL.pathname + parsedURL.search
}
}
if (typeof opts.method === 'string') {
opts.method = opts.method.toUpperCase()
}
this[kDispatchKey] = buildKey(opts)
this[kDispatches] = mockDispatches
this[kIgnoreTrailingSlash] = opts.ignoreTrailingSlash ?? false
this[kDefaultHeaders] = {}
this[kDefaultTrailers] = {}
this[kContentLength] = false
}
createMockScopeDispatchData ({ statusCode, data, responseOptions }) {
const responseData = getResponseData(data)
const contentLength = this[kContentLength] ? { 'content-length': responseData.length } : {}
const headers = { ...this[kDefaultHeaders], ...contentLength, ...responseOptions.headers }
const trailers = { ...this[kDefaultTrailers], ...responseOptions.trailers }
return { statusCode, data, headers, trailers }
}
validateReplyParameters (replyParameters) {
if (typeof replyParameters.statusCode === 'undefined') {
throw new InvalidArgumentError('statusCode must be defined')
}
if (typeof replyParameters.responseOptions !== 'object' || replyParameters.responseOptions === null) {
throw new InvalidArgumentError('responseOptions must be an object')
}
}
/**
* Mock an undici request with a defined reply.
*/
reply (replyOptionsCallbackOrStatusCode) {
// Values of reply aren't available right now as they
// can only be available when the reply callback is invoked.
if (typeof replyOptionsCallbackOrStatusCode === 'function') {
// We'll first wrap the provided callback in another function,
// this function will properly resolve the data from the callback
// when invoked.
const wrappedDefaultsCallback = (opts) => {
// Our reply options callback contains the parameter for statusCode, data and options.
const resolvedData = replyOptionsCallbackOrStatusCode(opts)
// Check if it is in the right format
if (typeof resolvedData !== 'object' || resolvedData === null) {
throw new InvalidArgumentError('reply options callback must return an object')
}
const replyParameters = { data: '', responseOptions: {}, ...resolvedData }
this.validateReplyParameters(replyParameters)
// Since the values can be obtained immediately we return them
// from this higher order function that will be resolved later.
return {
...this.createMockScopeDispatchData(replyParameters)
}
}
// Add usual dispatch data, but this time set the data parameter to function that will eventually provide data.
const newMockDispatch = addMockDispatch(this[kDispatches], this[kDispatchKey], wrappedDefaultsCallback, { ignoreTrailingSlash: this[kIgnoreTrailingSlash] })
return new MockScope(newMockDispatch)
}
// We can have either one or three parameters, if we get here,
// we should have 1-3 parameters. So we spread the arguments of
// this function to obtain the parameters, since replyData will always
// just be the statusCode.
const replyParameters = {
statusCode: replyOptionsCallbackOrStatusCode,
data: arguments[1] === undefined ? '' : arguments[1],
responseOptions: arguments[2] === undefined ? {} : arguments[2]
}
this.validateReplyParameters(replyParameters)
// Send in-already provided data like usual
const dispatchData = this.createMockScopeDispatchData(replyParameters)
const newMockDispatch = addMockDispatch(this[kDispatches], this[kDispatchKey], dispatchData, { ignoreTrailingSlash: this[kIgnoreTrailingSlash] })
return new MockScope(newMockDispatch)
}
/**
* Mock an undici request with a defined error.
*/
replyWithError (error) {
if (typeof error === 'undefined') {
throw new InvalidArgumentError('error must be defined')
}
const newMockDispatch = addMockDispatch(this[kDispatches], this[kDispatchKey], { error }, { ignoreTrailingSlash: this[kIgnoreTrailingSlash] })
return new MockScope(newMockDispatch)
}
/**
* Set default reply headers on the interceptor for subsequent replies
*/
defaultReplyHeaders (headers) {
if (typeof headers === 'undefined') {
throw new InvalidArgumentError('headers must be defined')
}
this[kDefaultHeaders] = headers
return this
}
/**
* Set default reply trailers on the interceptor for subsequent replies
*/
defaultReplyTrailers (trailers) {
if (typeof trailers === 'undefined') {
throw new InvalidArgumentError('trailers must be defined')
}
this[kDefaultTrailers] = trailers
return this
}
/**
* Set reply content length header for replies on the interceptor
*/
replyContentLength () {
this[kContentLength] = true
return this
}
}
module.exports.MockInterceptor = MockInterceptor
module.exports.MockScope = MockScope
================================================
FILE: lib/mock/mock-pool.js
================================================
'use strict'
const { promisify } = require('node:util')
const Pool = require('../dispatcher/pool')
const { buildMockDispatch } = require('./mock-utils')
const {
kDispatches,
kMockAgent,
kClose,
kOriginalClose,
kOrigin,
kOriginalDispatch,
kConnected,
kIgnoreTrailingSlash
} = require('./mock-symbols')
const { MockInterceptor } = require('./mock-interceptor')
const Symbols = require('../core/symbols')
const { InvalidArgumentError } = require('../core/errors')
/**
* MockPool provides an API that extends the Pool to influence the mockDispatches.
*/
class MockPool extends Pool {
constructor (origin, opts) {
if (!opts || !opts.agent || typeof opts.agent.dispatch !== 'function') {
throw new InvalidArgumentError('Argument opts.agent must implement Agent')
}
super(origin, opts)
this[kMockAgent] = opts.agent
this[kOrigin] = origin
this[kIgnoreTrailingSlash] = opts.ignoreTrailingSlash ?? false
this[kDispatches] = []
this[kConnected] = 1
this[kOriginalDispatch] = this.dispatch
this[kOriginalClose] = this.close.bind(this)
this.dispatch = buildMockDispatch.call(this)
this.close = this[kClose]
}
get [Symbols.kConnected] () {
return this[kConnected]
}
/**
* Sets up the base interceptor for mocking replies from undici.
*/
intercept (opts) {
return new MockInterceptor(
opts && { ignoreTrailingSlash: this[kIgnoreTrailingSlash], ...opts },
this[kDispatches]
)
}
cleanMocks () {
this[kDispatches] = []
}
async [kClose] () {
await promisify(this[kOriginalClose])()
this[kConnected] = 0
this[kMockAgent][Symbols.kClients].delete(this[kOrigin])
}
}
module.exports = MockPool
================================================
FILE: lib/mock/mock-symbols.js
================================================
'use strict'
module.exports = {
kAgent: Symbol('agent'),
kOptions: Symbol('options'),
kFactory: Symbol('factory'),
kDispatches: Symbol('dispatches'),
kDispatchKey: Symbol('dispatch key'),
kDefaultHeaders: Symbol('default headers'),
kDefaultTrailers: Symbol('default trailers'),
kContentLength: Symbol('content length'),
kMockAgent: Symbol('mock agent'),
kMockAgentSet: Symbol('mock agent set'),
kMockAgentGet: Symbol('mock agent get'),
kMockDispatch: Symbol('mock dispatch'),
kClose: Symbol('close'),
kOriginalClose: Symbol('original agent close'),
kOriginalDispatch: Symbol('original dispatch'),
kOrigin: Symbol('origin'),
kIsMockActive: Symbol('is mock active'),
kNetConnect: Symbol('net connect'),
kGetNetConnect: Symbol('get net connect'),
kConnected: Symbol('connected'),
kIgnoreTrailingSlash: Symbol('ignore trailing slash'),
kMockAgentMockCallHistoryInstance: Symbol('mock agent mock call history name'),
kMockAgentRegisterCallHistory: Symbol('mock agent register mock call history'),
kMockAgentAddCallHistoryLog: Symbol('mock agent add call history log'),
kMockAgentIsCallHistoryEnabled: Symbol('mock agent is call history enabled'),
kMockAgentAcceptsNonStandardSearchParameters: Symbol('mock agent accepts non standard search parameters'),
kMockCallHistoryAddLog: Symbol('mock call history add log')
}
================================================
FILE: lib/mock/mock-utils.js
================================================
'use strict'
const { MockNotMatchedError } = require('./mock-errors')
const {
kDispatches,
kMockAgent,
kOriginalDispatch,
kOrigin,
kGetNetConnect
} = require('./mock-symbols')
const { serializePathWithQuery } = require('../core/util')
const { STATUS_CODES } = require('node:http')
const {
types: {
isPromise
}
} = require('node:util')
const { InvalidArgumentError } = require('../core/errors')
function matchValue (match, value) {
if (typeof match === 'string') {
return match === value
}
if (match instanceof RegExp) {
return match.test(value)
}
if (typeof match === 'function') {
return match(value) === true
}
return false
}
function lowerCaseEntries (headers) {
return Object.fromEntries(
Object.entries(headers).map(([headerName, headerValue]) => {
return [headerName.toLocaleLowerCase(), headerValue]
})
)
}
/**
* @param {import('../../index').Headers|string[]|Record} headers
* @param {string} key
*/
function getHeaderByName (headers, key) {
if (Array.isArray(headers)) {
for (let i = 0; i < headers.length; i += 2) {
if (headers[i].toLocaleLowerCase() === key.toLocaleLowerCase()) {
return headers[i + 1]
}
}
return undefined
} else if (typeof headers.get === 'function') {
return headers.get(key)
} else {
return lowerCaseEntries(headers)[key.toLocaleLowerCase()]
}
}
/** @param {string[]} headers */
function buildHeadersFromArray (headers) { // fetch HeadersList
const clone = headers.slice()
const entries = []
for (let index = 0; index < clone.length; index += 2) {
entries.push([clone[index], clone[index + 1]])
}
return Object.fromEntries(entries)
}
function matchHeaders (mockDispatch, headers) {
if (typeof mockDispatch.headers === 'function') {
if (Array.isArray(headers)) { // fetch HeadersList
headers = buildHeadersFromArray(headers)
}
return mockDispatch.headers(headers ? lowerCaseEntries(headers) : {})
}
if (typeof mockDispatch.headers === 'undefined') {
return true
}
if (typeof headers !== 'object' || typeof mockDispatch.headers !== 'object') {
return false
}
for (const [matchHeaderName, matchHeaderValue] of Object.entries(mockDispatch.headers)) {
const headerValue = getHeaderByName(headers, matchHeaderName)
if (!matchValue(matchHeaderValue, headerValue)) {
return false
}
}
return true
}
function normalizeSearchParams (query) {
if (typeof query !== 'string') {
return query
}
const originalQp = new URLSearchParams(query)
const normalizedQp = new URLSearchParams()
for (let [key, value] of originalQp.entries()) {
key = key.replace('[]', '')
const valueRepresentsString = /^(['"]).*\1$/.test(value)
if (valueRepresentsString) {
normalizedQp.append(key, value)
continue
}
if (value.includes(',')) {
const values = value.split(',')
for (const v of values) {
normalizedQp.append(key, v)
}
continue
}
normalizedQp.append(key, value)
}
return normalizedQp
}
function safeUrl (path) {
if (typeof path !== 'string') {
return path
}
const pathSegments = path.split('?', 3)
if (pathSegments.length !== 2) {
return path
}
const qp = new URLSearchParams(pathSegments.pop())
qp.sort()
return [...pathSegments, qp.toString()].join('?')
}
function matchKey (mockDispatch, { path, method, body, headers }) {
const pathMatch = matchValue(mockDispatch.path, path)
const methodMatch = matchValue(mockDispatch.method, method)
const bodyMatch = typeof mockDispatch.body !== 'undefined' ? matchValue(mockDispatch.body, body) : true
const headersMatch = matchHeaders(mockDispatch, headers)
return pathMatch && methodMatch && bodyMatch && headersMatch
}
function getResponseData (data) {
if (Buffer.isBuffer(data)) {
return data
} else if (data instanceof Uint8Array) {
return data
} else if (data instanceof ArrayBuffer) {
return data
} else if (typeof data === 'object') {
return JSON.stringify(data)
} else if (data) {
return data.toString()
} else {
return ''
}
}
function getMockDispatch (mockDispatches, key) {
const basePath = key.query ? serializePathWithQuery(key.path, key.query) : key.path
const resolvedPath = typeof basePath === 'string' ? safeUrl(basePath) : basePath
const resolvedPathWithoutTrailingSlash = removeTrailingSlash(resolvedPath)
// Match path
let matchedMockDispatches = mockDispatches
.filter(({ consumed }) => !consumed)
.filter(({ path, ignoreTrailingSlash }) => {
return ignoreTrailingSlash
? matchValue(removeTrailingSlash(safeUrl(path)), resolvedPathWithoutTrailingSlash)
: matchValue(safeUrl(path), resolvedPath)
})
if (matchedMockDispatches.length === 0) {
throw new MockNotMatchedError(`Mock dispatch not matched for path '${resolvedPath}'`)
}
// Match method
matchedMockDispatches = matchedMockDispatches.filter(({ method }) => matchValue(method, key.method))
if (matchedMockDispatches.length === 0) {
throw new MockNotMatchedError(`Mock dispatch not matched for method '${key.method}' on path '${resolvedPath}'`)
}
// Match body
matchedMockDispatches = matchedMockDispatches.filter(({ body }) => typeof body !== 'undefined' ? matchValue(body, key.body) : true)
if (matchedMockDispatches.length === 0) {
throw new MockNotMatchedError(`Mock dispatch not matched for body '${key.body}' on path '${resolvedPath}'`)
}
// Match headers
matchedMockDispatches = matchedMockDispatches.filter((mockDispatch) => matchHeaders(mockDispatch, key.headers))
if (matchedMockDispatches.length === 0) {
const headers = typeof key.headers === 'object' ? JSON.stringify(key.headers) : key.headers
throw new MockNotMatchedError(`Mock dispatch not matched for headers '${headers}' on path '${resolvedPath}'`)
}
return matchedMockDispatches[0]
}
function addMockDispatch (mockDispatches, key, data, opts) {
const baseData = { timesInvoked: 0, times: 1, persist: false, consumed: false, ...opts }
const replyData = typeof data === 'function' ? { callback: data } : { ...data }
const newMockDispatch = { ...baseData, ...key, pending: true, data: { error: null, ...replyData } }
mockDispatches.push(newMockDispatch)
return newMockDispatch
}
function deleteMockDispatch (mockDispatches, key) {
const index = mockDispatches.findIndex(dispatch => {
if (!dispatch.consumed) {
return false
}
return matchKey(dispatch, key)
})
if (index !== -1) {
mockDispatches.splice(index, 1)
}
}
/**
* @param {string} path Path to remove trailing slash from
*/
function removeTrailingSlash (path) {
while (path.endsWith('/')) {
path = path.slice(0, -1)
}
if (path.length === 0) {
path = '/'
}
return path
}
function buildKey (opts) {
const { path, method, body, headers, query } = opts
return {
path,
method,
body,
headers,
query
}
}
function generateKeyValues (data) {
const keys = Object.keys(data)
const result = []
for (let i = 0; i < keys.length; ++i) {
const key = keys[i]
const value = data[key]
const name = Buffer.from(`${key}`)
if (Array.isArray(value)) {
for (let j = 0; j < value.length; ++j) {
result.push(name, Buffer.from(`${value[j]}`))
}
} else {
result.push(name, Buffer.from(`${value}`))
}
}
return result
}
/**
* @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Status
* @param {number} statusCode
*/
function getStatusText (statusCode) {
return STATUS_CODES[statusCode] || 'unknown'
}
async function getResponse (body) {
const buffers = []
for await (const data of body) {
buffers.push(data)
}
return Buffer.concat(buffers).toString('utf8')
}
/**
* Mock dispatch function used to simulate undici dispatches
*/
function mockDispatch (opts, handler) {
// Get mock dispatch from built key
const key = buildKey(opts)
const mockDispatch = getMockDispatch(this[kDispatches], key)
mockDispatch.timesInvoked++
// Here's where we resolve a callback if a callback is present for the dispatch data.
if (mockDispatch.data.callback) {
mockDispatch.data = { ...mockDispatch.data, ...mockDispatch.data.callback(opts) }
}
// Parse mockDispatch data
const { data: { statusCode, data, headers, trailers, error }, delay, persist } = mockDispatch
const { timesInvoked, times } = mockDispatch
// If it's used up and not persistent, mark as consumed
mockDispatch.consumed = !persist && timesInvoked >= times
mockDispatch.pending = timesInvoked < times
// If specified, trigger dispatch error
if (error !== null) {
deleteMockDispatch(this[kDispatches], key)
handler.onError(error)
return true
}
// Track whether the request has been aborted
let aborted = false
let timer = null
function abort (err) {
if (aborted) {
return
}
aborted = true
// Clear the pending delayed response if any
if (timer !== null) {
clearTimeout(timer)
timer = null
}
// Notify the handler of the abort
handler.onError(err)
}
// Call onConnect to allow the handler to register the abort callback
handler.onConnect?.(abort, null)
// Handle the request with a delay if necessary
if (typeof delay === 'number' && delay > 0) {
timer = setTimeout(() => {
timer = null
handleReply(this[kDispatches])
}, delay)
} else {
handleReply(this[kDispatches])
}
function handleReply (mockDispatches, _data = data) {
// Don't send response if the request was aborted
if (aborted) {
return
}
// fetch's HeadersList is a 1D string array
const optsHeaders = Array.isArray(opts.headers)
? buildHeadersFromArray(opts.headers)
: opts.headers
const body = typeof _data === 'function'
? _data({ ...opts, headers: optsHeaders })
: _data
// util.types.isPromise is likely needed for jest.
if (isPromise(body)) {
// If handleReply is asynchronous, throwing an error
// in the callback will reject the promise, rather than
// synchronously throw the error, which breaks some tests.
// Rather, we wait for the callback to resolve if it is a
// promise, and then re-run handleReply with the new body.
return body.then((newData) => handleReply(mockDispatches, newData))
}
// Check again if aborted after async body resolution
if (aborted) {
return
}
const responseData = getResponseData(body)
const responseHeaders = generateKeyValues(headers)
const responseTrailers = generateKeyValues(trailers)
handler.onHeaders?.(statusCode, responseHeaders, resume, getStatusText(statusCode))
handler.onData?.(Buffer.from(responseData))
handler.onComplete?.(responseTrailers)
deleteMockDispatch(mockDispatches, key)
}
function resume () {}
return true
}
function buildMockDispatch () {
const agent = this[kMockAgent]
const origin = this[kOrigin]
const originalDispatch = this[kOriginalDispatch]
return function dispatch (opts, handler) {
if (agent.isMockActive) {
try {
mockDispatch.call(this, opts, handler)
} catch (error) {
if (error.code === 'UND_MOCK_ERR_MOCK_NOT_MATCHED') {
const netConnect = agent[kGetNetConnect]()
if (netConnect === false) {
throw new MockNotMatchedError(`${error.message}: subsequent request to origin ${origin} was not allowed (net.connect disabled)`)
}
if (checkNetConnect(netConnect, origin)) {
originalDispatch.call(this, opts, handler)
} else {
throw new MockNotMatchedError(`${error.message}: subsequent request to origin ${origin} was not allowed (net.connect is not enabled for this origin)`)
}
} else {
throw error
}
}
} else {
originalDispatch.call(this, opts, handler)
}
}
}
function checkNetConnect (netConnect, origin) {
const url = new URL(origin)
if (netConnect === true) {
return true
} else if (Array.isArray(netConnect) && netConnect.some((matcher) => matchValue(matcher, url.host))) {
return true
}
return false
}
function normalizeOrigin (origin) {
if (typeof origin !== 'string' && !(origin instanceof URL)) {
return origin
}
if (origin instanceof URL) {
return origin.origin
}
return origin.toLowerCase()
}
function buildAndValidateMockOptions (opts) {
const { agent, ...mockOptions } = opts
if ('enableCallHistory' in mockOptions && typeof mockOptions.enableCallHistory !== 'boolean') {
throw new InvalidArgumentError('options.enableCallHistory must to be a boolean')
}
if ('acceptNonStandardSearchParameters' in mockOptions && typeof mockOptions.acceptNonStandardSearchParameters !== 'boolean') {
throw new InvalidArgumentError('options.acceptNonStandardSearchParameters must to be a boolean')
}
if ('ignoreTrailingSlash' in mockOptions && typeof mockOptions.ignoreTrailingSlash !== 'boolean') {
throw new InvalidArgumentError('options.ignoreTrailingSlash must to be a boolean')
}
return mockOptions
}
module.exports = {
getResponseData,
getMockDispatch,
addMockDispatch,
deleteMockDispatch,
buildKey,
generateKeyValues,
matchValue,
getResponse,
getStatusText,
mockDispatch,
buildMockDispatch,
checkNetConnect,
buildAndValidateMockOptions,
getHeaderByName,
buildHeadersFromArray,
normalizeSearchParams,
normalizeOrigin
}
================================================
FILE: lib/mock/pending-interceptors-formatter.js
================================================
'use strict'
const { Transform } = require('node:stream')
const { Console } = require('node:console')
const PERSISTENT = process.versions.icu ? '✅' : 'Y '
const NOT_PERSISTENT = process.versions.icu ? '❌' : 'N '
/**
* Gets the output of `console.table(…)` as a string.
*/
module.exports = class PendingInterceptorsFormatter {
constructor ({ disableColors } = {}) {
this.transform = new Transform({
transform (chunk, _enc, cb) {
cb(null, chunk)
}
})
this.logger = new Console({
stdout: this.transform,
inspectOptions: {
colors: !disableColors && !process.env.CI
}
})
}
format (pendingInterceptors) {
const withPrettyHeaders = pendingInterceptors.map(
({ method, path, data: { statusCode }, persist, times, timesInvoked, origin }) => ({
Method: method,
Origin: origin,
Path: path,
'Status code': statusCode,
Persistent: persist ? PERSISTENT : NOT_PERSISTENT,
Invocations: timesInvoked,
Remaining: persist ? Infinity : times - timesInvoked
}))
this.logger.table(withPrettyHeaders)
return this.transform.read().toString()
}
}
================================================
FILE: lib/mock/snapshot-agent.js
================================================
'use strict'
const Agent = require('../dispatcher/agent')
const MockAgent = require('./mock-agent')
const { SnapshotRecorder } = require('./snapshot-recorder')
const WrapHandler = require('../handler/wrap-handler')
const { InvalidArgumentError, UndiciError } = require('../core/errors')
const { validateSnapshotMode } = require('./snapshot-utils')
const kSnapshotRecorder = Symbol('kSnapshotRecorder')
const kSnapshotMode = Symbol('kSnapshotMode')
const kSnapshotPath = Symbol('kSnapshotPath')
const kSnapshotLoaded = Symbol('kSnapshotLoaded')
const kRealAgent = Symbol('kRealAgent')
// Static flag to ensure warning is only emitted once per process
let warningEmitted = false
class SnapshotAgent extends MockAgent {
constructor (opts = {}) {
// Emit experimental warning only once
if (!warningEmitted) {
process.emitWarning(
'SnapshotAgent is experimental and subject to change',
'ExperimentalWarning'
)
warningEmitted = true
}
const {
mode = 'record',
snapshotPath = null,
...mockAgentOpts
} = opts
super(mockAgentOpts)
validateSnapshotMode(mode)
// Validate snapshotPath is provided when required
if ((mode === 'playback' || mode === 'update') && !snapshotPath) {
throw new InvalidArgumentError(`snapshotPath is required when mode is '${mode}'`)
}
this[kSnapshotMode] = mode
this[kSnapshotPath] = snapshotPath
this[kSnapshotRecorder] = new SnapshotRecorder({
snapshotPath: this[kSnapshotPath],
mode: this[kSnapshotMode],
maxSnapshots: opts.maxSnapshots,
autoFlush: opts.autoFlush,
flushInterval: opts.flushInterval,
matchHeaders: opts.matchHeaders,
ignoreHeaders: opts.ignoreHeaders,
excludeHeaders: opts.excludeHeaders,
matchBody: opts.matchBody,
matchQuery: opts.matchQuery,
caseSensitive: opts.caseSensitive,
shouldRecord: opts.shouldRecord,
shouldPlayback: opts.shouldPlayback,
excludeUrls: opts.excludeUrls
})
this[kSnapshotLoaded] = false
// For recording/update mode, we need a real agent to make actual requests
// For playback mode, we need a real agent if there are excluded URLs
if (this[kSnapshotMode] === 'record' || this[kSnapshotMode] === 'update' ||
(this[kSnapshotMode] === 'playback' && opts.excludeUrls && opts.excludeUrls.length > 0)) {
this[kRealAgent] = new Agent(opts)
}
// Auto-load snapshots in playback/update mode
if ((this[kSnapshotMode] === 'playback' || this[kSnapshotMode] === 'update') && this[kSnapshotPath]) {
this.loadSnapshots().catch(() => {
// Ignore load errors - file might not exist yet
})
}
}
dispatch (opts, handler) {
handler = WrapHandler.wrap(handler)
const mode = this[kSnapshotMode]
// Check if URL should be excluded (pass through without mocking/recording)
if (this[kSnapshotRecorder].isUrlExcluded(opts)) {
// Real agent is guaranteed by constructor when excludeUrls is configured
return this[kRealAgent].dispatch(opts, handler)
}
if (mode === 'playback' || mode === 'update') {
// Ensure snapshots are loaded
if (!this[kSnapshotLoaded]) {
// Need to load asynchronously, delegate to async version
return this.#asyncDispatch(opts, handler)
}
// Try to find existing snapshot (synchronous)
const snapshot = this[kSnapshotRecorder].findSnapshot(opts)
if (snapshot) {
// Use recorded response (synchronous)
return this.#replaySnapshot(snapshot, handler)
} else if (mode === 'update') {
// Make real request and record it (async required)
return this.#recordAndReplay(opts, handler)
} else {
// Playback mode but no snapshot found
const error = new UndiciError(`No snapshot found for ${opts.method || 'GET'} ${opts.path}`)
if (handler.onError) {
handler.onError(error)
return
}
throw error
}
} else if (mode === 'record') {
// Record mode - make real request and save response (async required)
return this.#recordAndReplay(opts, handler)
}
}
/**
* Async version of dispatch for when we need to load snapshots first
*/
async #asyncDispatch (opts, handler) {
await this.loadSnapshots()
return this.dispatch(opts, handler)
}
/**
* Records a real request and replays the response
*/
#recordAndReplay (opts, handler) {
const responseData = {
statusCode: null,
headers: {},
trailers: {},
body: []
}
const self = this // Capture 'this' context for use within nested handler callbacks
const recordingHandler = {
onRequestStart (controller, context) {
return handler.onRequestStart(controller, { ...context, history: this.history })
},
onRequestUpgrade (controller, statusCode, headers, socket) {
return handler.onRequestUpgrade(controller, statusCode, headers, socket)
},
onResponseStart (controller, statusCode, headers, statusMessage) {
responseData.statusCode = statusCode
responseData.headers = headers
return handler.onResponseStart(controller, statusCode, headers, statusMessage)
},
onResponseData (controller, chunk) {
responseData.body.push(chunk)
return handler.onResponseData(controller, chunk)
},
onResponseEnd (controller, trailers) {
responseData.trailers = trailers
// Record the interaction using captured 'self' context (fire and forget)
const responseBody = Buffer.concat(responseData.body)
self[kSnapshotRecorder].record(opts, {
statusCode: responseData.statusCode,
headers: responseData.headers,
body: responseBody,
trailers: responseData.trailers
})
.then(() => handler.onResponseEnd(controller, trailers))
.catch((error) => handler.onResponseError(controller, error))
}
}
// Use composed agent if available (includes interceptors), otherwise use real agent
const agent = this[kRealAgent]
return agent.dispatch(opts, recordingHandler)
}
/**
* Replays a recorded response
*
* @param {Object} snapshot - The recorded snapshot to replay.
* @param {Object} handler - The handler to call with the response data.
* @returns {void}
*/
#replaySnapshot (snapshot, handler) {
try {
const { response } = snapshot
const controller = {
pause () { },
resume () { },
abort (reason) {
this.aborted = true
this.reason = reason
},
aborted: false,
paused: false
}
handler.onRequestStart(controller)
handler.onResponseStart(controller, response.statusCode, response.headers)
// Body is always stored as base64 string
const body = Buffer.from(response.body, 'base64')
handler.onResponseData(controller, body)
handler.onResponseEnd(controller, response.trailers)
} catch (error) {
handler.onError?.(error)
}
}
/**
* Loads snapshots from file
*
* @param {string} [filePath] - Optional file path to load snapshots from.
* @returns {Promise} - Resolves when snapshots are loaded.
*/
async loadSnapshots (filePath) {
await this[kSnapshotRecorder].loadSnapshots(filePath || this[kSnapshotPath])
this[kSnapshotLoaded] = true
// In playback mode, set up MockAgent interceptors for all snapshots
if (this[kSnapshotMode] === 'playback') {
this.#setupMockInterceptors()
}
}
/**
* Saves snapshots to file
*
* @param {string} [filePath] - Optional file path to save snapshots to.
* @returns {Promise} - Resolves when snapshots are saved.
*/
async saveSnapshots (filePath) {
return this[kSnapshotRecorder].saveSnapshots(filePath || this[kSnapshotPath])
}
/**
* Sets up MockAgent interceptors based on recorded snapshots.
*
* This method creates MockAgent interceptors for each recorded snapshot,
* allowing the SnapshotAgent to fall back to MockAgent's standard intercept
* mechanism in playback mode. Each interceptor is configured to persist
* (remain active for multiple requests) and responds with the recorded
* response data.
*
* Called automatically when loading snapshots in playback mode.
*
* @returns {void}
*/
#setupMockInterceptors () {
for (const snapshot of this[kSnapshotRecorder].getSnapshots()) {
const { request, responses, response } = snapshot
const url = new URL(request.url)
const mockPool = this.get(url.origin)
// Handle both new format (responses array) and legacy format (response object)
const responseData = responses ? responses[0] : response
if (!responseData) continue
mockPool.intercept({
path: url.pathname + url.search,
method: request.method,
headers: request.headers,
body: request.body
}).reply(responseData.statusCode, responseData.body, {
headers: responseData.headers,
trailers: responseData.trailers
}).persist()
}
}
/**
* Gets the snapshot recorder
* @return {SnapshotRecorder} - The snapshot recorder instance
*/
getRecorder () {
return this[kSnapshotRecorder]
}
/**
* Gets the current mode
* @return {import('./snapshot-utils').SnapshotMode} - The current snapshot mode
*/
getMode () {
return this[kSnapshotMode]
}
/**
* Clears all snapshots
* @returns {void}
*/
clearSnapshots () {
this[kSnapshotRecorder].clear()
}
/**
* Resets call counts for all snapshots (useful for test cleanup)
* @returns {void}
*/
resetCallCounts () {
this[kSnapshotRecorder].resetCallCounts()
}
/**
* Deletes a specific snapshot by request options
* @param {import('./snapshot-recorder').SnapshotRequestOptions} requestOpts - Request options to identify the snapshot
* @return {Promise} - Returns true if the snapshot was deleted, false if not found
*/
deleteSnapshot (requestOpts) {
return this[kSnapshotRecorder].deleteSnapshot(requestOpts)
}
/**
* Gets information about a specific snapshot
* @returns {import('./snapshot-recorder').SnapshotInfo|null} - Snapshot information or null if not found
*/
getSnapshotInfo (requestOpts) {
return this[kSnapshotRecorder].getSnapshotInfo(requestOpts)
}
/**
* Replaces all snapshots with new data (full replacement)
* @param {Array<{hash: string; snapshot: import('./snapshot-recorder').SnapshotEntryshotEntry}>|Record} snapshotData - New snapshot data to replace existing snapshots
* @returns {void}
*/
replaceSnapshots (snapshotData) {
this[kSnapshotRecorder].replaceSnapshots(snapshotData)
}
/**
* Closes the agent, saving snapshots and cleaning up resources.
*
* @returns {Promise}
*/
async close () {
await this[kSnapshotRecorder].close()
await this[kRealAgent]?.close()
await super.close()
}
}
module.exports = SnapshotAgent
================================================
FILE: lib/mock/snapshot-recorder.js
================================================
'use strict'
const { writeFile, readFile, mkdir } = require('node:fs/promises')
const { dirname, resolve } = require('node:path')
const { setTimeout, clearTimeout } = require('node:timers')
const { InvalidArgumentError, UndiciError } = require('../core/errors')
const { hashId, isUrlExcludedFactory, normalizeHeaders, createHeaderFilters } = require('./snapshot-utils')
/**
* @typedef {Object} SnapshotRequestOptions
* @property {string} method - HTTP method (e.g. 'GET', 'POST', etc.)
* @property {string} path - Request path
* @property {string} origin - Request origin (base URL)
* @property {import('./snapshot-utils').Headers|import('./snapshot-utils').UndiciHeaders} headers - Request headers
* @property {import('./snapshot-utils').NormalizedHeaders} _normalizedHeaders - Request headers as a lowercase object
* @property {string|Buffer} [body] - Request body (optional)
*/
/**
* @typedef {Object} SnapshotEntryRequest
* @property {string} method - HTTP method (e.g. 'GET', 'POST', etc.)
* @property {string} url - Full URL of the request
* @property {import('./snapshot-utils').NormalizedHeaders} headers - Normalized headers as a lowercase object
* @property {string|Buffer} [body] - Request body (optional)
*/
/**
* @typedef {Object} SnapshotEntryResponse
* @property {number} statusCode - HTTP status code of the response
* @property {import('./snapshot-utils').NormalizedHeaders} headers - Normalized response headers as a lowercase object
* @property {string} body - Response body as a base64url encoded string
* @property {Object} [trailers] - Optional response trailers
*/
/**
* @typedef {Object} SnapshotEntry
* @property {SnapshotEntryRequest} request - The request object
* @property {Array} responses - Array of response objects
* @property {number} callCount - Number of times this snapshot has been called
* @property {string} timestamp - ISO timestamp of when the snapshot was created
*/
/**
* @typedef {Object} SnapshotRecorderMatchOptions
* @property {Array} [matchHeaders=[]] - Headers to match (empty array means match all headers)
* @property {Array} [ignoreHeaders=[]] - Headers to ignore for matching
* @property {Array} [excludeHeaders=[]] - Headers to exclude from matching
* @property {boolean} [matchBody=true] - Whether to match request body
* @property {boolean} [matchQuery=true] - Whether to match query properties
* @property {boolean} [caseSensitive=false] - Whether header matching is case-sensitive
*/
/**
* @typedef {Object} SnapshotRecorderOptions
* @property {string} [snapshotPath] - Path to save/load snapshots
* @property {import('./snapshot-utils').SnapshotMode} [mode='record'] - Mode: 'record' or 'playback'
* @property {number} [maxSnapshots=Infinity] - Maximum number of snapshots to keep
* @property {boolean} [autoFlush=false] - Whether to automatically flush snapshots to disk
* @property {number} [flushInterval=30000] - Auto-flush interval in milliseconds (default: 30 seconds)
* @property {Array} [excludeUrls=[]] - URLs to exclude from recording
* @property {function} [shouldRecord=null] - Function to filter requests for recording
* @property {function} [shouldPlayback=null] - Function to filter requests
*/
/**
* @typedef {Object} SnapshotFormattedRequest
* @property {string} method - HTTP method (e.g. 'GET', 'POST', etc.)
* @property {string} url - Full URL of the request (with query parameters if matchQuery is true)
* @property {import('./snapshot-utils').NormalizedHeaders} headers - Normalized headers as a lowercase object
* @property {string} body - Request body (optional, only if matchBody is true)
*/
/**
* @typedef {Object} SnapshotInfo
* @property {string} hash - Hash key for the snapshot
* @property {SnapshotEntryRequest} request - The request object
* @property {number} responseCount - Number of responses recorded for this request
* @property {number} callCount - Number of times this snapshot has been called
* @property {string} timestamp - ISO timestamp of when the snapshot was created
*/
/**
* Formats a request for consistent snapshot storage
* Caches normalized headers to avoid repeated processing
*
* @param {SnapshotRequestOptions} opts - Request options
* @param {import('./snapshot-utils').HeaderFilters} headerFilters - Cached header sets for performance
* @param {SnapshotRecorderMatchOptions} [matchOptions] - Matching options for headers and body
* @returns {SnapshotFormattedRequest} - Formatted request object
*/
function formatRequestKey (opts, headerFilters, matchOptions = {}) {
const url = new URL(opts.path, opts.origin)
// Cache normalized headers if not already done
const normalized = opts._normalizedHeaders || normalizeHeaders(opts.headers)
if (!opts._normalizedHeaders) {
opts._normalizedHeaders = normalized
}
return {
method: opts.method || 'GET',
url: matchOptions.matchQuery !== false ? url.toString() : `${url.origin}${url.pathname}`,
headers: filterHeadersForMatching(normalized, headerFilters, matchOptions),
body: matchOptions.matchBody !== false && opts.body ? String(opts.body) : ''
}
}
/**
* Filters headers based on matching configuration
*
* @param {import('./snapshot-utils').Headers} headers - Headers to filter
* @param {import('./snapshot-utils').HeaderFilters} headerFilters - Cached sets for ignore, exclude, and match headers
* @param {SnapshotRecorderMatchOptions} [matchOptions] - Matching options for headers
*/
function filterHeadersForMatching (headers, headerFilters, matchOptions = {}) {
if (!headers || typeof headers !== 'object') return {}
const {
caseSensitive = false
} = matchOptions
const filtered = {}
const { ignore, exclude, match } = headerFilters
for (const [key, value] of Object.entries(headers)) {
const headerKey = caseSensitive ? key : key.toLowerCase()
// Skip if in exclude list (for security)
if (exclude.has(headerKey)) continue
// Skip if in ignore list (for matching)
if (ignore.has(headerKey)) continue
// If matchHeaders is specified, only include those headers
if (match.size !== 0) {
if (!match.has(headerKey)) continue
}
filtered[headerKey] = value
}
return filtered
}
/**
* Filters headers for storage (only excludes sensitive headers)
*
* @param {import('./snapshot-utils').Headers} headers - Headers to filter
* @param {import('./snapshot-utils').HeaderFilters} headerFilters - Cached sets for ignore, exclude, and match headers
* @param {SnapshotRecorderMatchOptions} [matchOptions] - Matching options for headers
*/
function filterHeadersForStorage (headers, headerFilters, matchOptions = {}) {
if (!headers || typeof headers !== 'object') return {}
const {
caseSensitive = false
} = matchOptions
const filtered = {}
const { exclude: excludeSet } = headerFilters
for (const [key, value] of Object.entries(headers)) {
const headerKey = caseSensitive ? key : key.toLowerCase()
// Skip if in exclude list (for security)
if (excludeSet.has(headerKey)) continue
filtered[headerKey] = value
}
return filtered
}
/**
* Creates a hash key for request matching
* Properly orders headers to avoid conflicts and uses crypto hashing when available
*
* @param {SnapshotFormattedRequest} formattedRequest - Request object
* @returns {string} - Base64url encoded hash of the request
*/
function createRequestHash (formattedRequest) {
const parts = [
formattedRequest.method,
formattedRequest.url
]
// Process headers in a deterministic way to avoid conflicts
if (formattedRequest.headers && typeof formattedRequest.headers === 'object') {
const headerKeys = Object.keys(formattedRequest.headers).sort()
for (const key of headerKeys) {
const values = Array.isArray(formattedRequest.headers[key])
? formattedRequest.headers[key]
: [formattedRequest.headers[key]]
// Add header name
parts.push(key)
// Add all values for this header, sorted for consistency
for (const value of values.sort()) {
parts.push(String(value))
}
}
}
// Add body
parts.push(formattedRequest.body)
const content = parts.join('|')
return hashId(content)
}
class SnapshotRecorder {
/** @type {NodeJS.Timeout | null} */
#flushTimeout
/** @type {import('./snapshot-utils').IsUrlExcluded} */
#isUrlExcluded
/** @type {Map} */
#snapshots = new Map()
/** @type {string|undefined} */
#snapshotPath
/** @type {number} */
#maxSnapshots = Infinity
/** @type {boolean} */
#autoFlush = false
/** @type {import('./snapshot-utils').HeaderFilters} */
#headerFilters
/**
* Creates a new SnapshotRecorder instance
* @param {SnapshotRecorderOptions&SnapshotRecorderMatchOptions} [options={}] - Configuration options for the recorder
*/
constructor (options = {}) {
this.#snapshotPath = options.snapshotPath
this.#maxSnapshots = options.maxSnapshots || Infinity
this.#autoFlush = options.autoFlush || false
this.flushInterval = options.flushInterval || 30000 // 30 seconds default
this._flushTimer = null
// Matching configuration
/** @type {Required} */
this.matchOptions = {
matchHeaders: options.matchHeaders || [], // empty means match all headers
ignoreHeaders: options.ignoreHeaders || [],
excludeHeaders: options.excludeHeaders || [],
matchBody: options.matchBody !== false, // default: true
matchQuery: options.matchQuery !== false, // default: true
caseSensitive: options.caseSensitive || false
}
// Cache processed header sets to avoid recreating them on every request
this.#headerFilters = createHeaderFilters(this.matchOptions)
// Request filtering callbacks
this.shouldRecord = options.shouldRecord || (() => true) // function(requestOpts) -> boolean
this.shouldPlayback = options.shouldPlayback || (() => true) // function(requestOpts) -> boolean
// URL pattern filtering
this.#isUrlExcluded = isUrlExcludedFactory(options.excludeUrls) // Array of regex patterns or strings
// Start auto-flush timer if enabled
if (this.#autoFlush && this.#snapshotPath) {
this.#startAutoFlush()
}
}
/**
* Records a request-response interaction
* @param {SnapshotRequestOptions} requestOpts - Request options
* @param {SnapshotEntryResponse} response - Response data to record
* @return {Promise} - Resolves when the recording is complete
*/
async record (requestOpts, response) {
// Check if recording should be filtered out
if (!this.shouldRecord(requestOpts)) {
return // Skip recording
}
// Check URL exclusion patterns
if (this.isUrlExcluded(requestOpts)) {
return // Skip recording
}
const request = formatRequestKey(requestOpts, this.#headerFilters, this.matchOptions)
const hash = createRequestHash(request)
// Extract response data - always store body as base64
const normalizedHeaders = normalizeHeaders(response.headers)
/** @type {SnapshotEntryResponse} */
const responseData = {
statusCode: response.statusCode,
headers: filterHeadersForStorage(normalizedHeaders, this.#headerFilters, this.matchOptions),
body: Buffer.isBuffer(response.body)
? response.body.toString('base64')
: Buffer.from(String(response.body || '')).toString('base64'),
trailers: response.trailers
}
// Remove oldest snapshot if we exceed maxSnapshots limit
if (this.#snapshots.size >= this.#maxSnapshots && !this.#snapshots.has(hash)) {
const oldestKey = this.#snapshots.keys().next().value
this.#snapshots.delete(oldestKey)
}
// Support sequential responses - if snapshot exists, add to responses array
const existingSnapshot = this.#snapshots.get(hash)
if (existingSnapshot && existingSnapshot.responses) {
existingSnapshot.responses.push(responseData)
existingSnapshot.timestamp = new Date().toISOString()
} else {
this.#snapshots.set(hash, {
request,
responses: [responseData], // Always store as array for consistency
callCount: 0,
timestamp: new Date().toISOString()
})
}
// Auto-flush if enabled
if (this.#autoFlush && this.#snapshotPath) {
this.#scheduleFlush()
}
}
/**
* Checks if a URL should be excluded from recording/playback
* @param {SnapshotRequestOptions} requestOpts - Request options to check
* @returns {boolean} - True if URL is excluded
*/
isUrlExcluded (requestOpts) {
const url = new URL(requestOpts.path, requestOpts.origin).toString()
return this.#isUrlExcluded(url)
}
/**
* Finds a matching snapshot for the given request
* Returns the appropriate response based on call count for sequential responses
*
* @param {SnapshotRequestOptions} requestOpts - Request options to match
* @returns {SnapshotEntry&Record<'response', SnapshotEntryResponse>|undefined} - Matching snapshot response or undefined if not found
*/
findSnapshot (requestOpts) {
// Check if playback should be filtered out
if (!this.shouldPlayback(requestOpts)) {
return undefined // Skip playback
}
// Check URL exclusion patterns
if (this.isUrlExcluded(requestOpts)) {
return undefined // Skip playback
}
const request = formatRequestKey(requestOpts, this.#headerFilters, this.matchOptions)
const hash = createRequestHash(request)
const snapshot = this.#snapshots.get(hash)
if (!snapshot) return undefined
// Handle sequential responses
const currentCallCount = snapshot.callCount || 0
const responseIndex = Math.min(currentCallCount, snapshot.responses.length - 1)
snapshot.callCount = currentCallCount + 1
return {
...snapshot,
response: snapshot.responses[responseIndex]
}
}
/**
* Loads snapshots from file
* @param {string} [filePath] - Optional file path to load snapshots from
* @return {Promise} - Resolves when snapshots are loaded
*/
async loadSnapshots (filePath) {
const path = filePath || this.#snapshotPath
if (!path) {
throw new InvalidArgumentError('Snapshot path is required')
}
try {
const data = await readFile(resolve(path), 'utf8')
const parsed = JSON.parse(data)
// Convert array format back to Map
if (Array.isArray(parsed)) {
this.#snapshots.clear()
for (const { hash, snapshot } of parsed) {
this.#snapshots.set(hash, snapshot)
}
} else {
// Legacy object format
this.#snapshots = new Map(Object.entries(parsed))
}
} catch (error) {
if (error.code === 'ENOENT') {
// File doesn't exist yet - that's ok for recording mode
this.#snapshots.clear()
} else {
throw new UndiciError(`Failed to load snapshots from ${path}`, { cause: error })
}
}
}
/**
* Saves snapshots to file
*
* @param {string} [filePath] - Optional file path to save snapshots
* @returns {Promise} - Resolves when snapshots are saved
*/
async saveSnapshots (filePath) {
const path = filePath || this.#snapshotPath
if (!path) {
throw new InvalidArgumentError('Snapshot path is required')
}
const resolvedPath = resolve(path)
// Ensure directory exists
await mkdir(dirname(resolvedPath), { recursive: true })
// Convert Map to serializable format
const data = Array.from(this.#snapshots.entries()).map(([hash, snapshot]) => ({
hash,
snapshot
}))
await writeFile(resolvedPath, JSON.stringify(data, null, 2), { flush: true })
}
/**
* Clears all recorded snapshots
* @returns {void}
*/
clear () {
this.#snapshots.clear()
}
/**
* Gets all recorded snapshots
* @return {Array} - Array of all recorded snapshots
*/
getSnapshots () {
return Array.from(this.#snapshots.values())
}
/**
* Gets snapshot count
* @return {number} - Number of recorded snapshots
*/
size () {
return this.#snapshots.size
}
/**
* Resets call counts for all snapshots (useful for test cleanup)
* @returns {void}
*/
resetCallCounts () {
for (const snapshot of this.#snapshots.values()) {
snapshot.callCount = 0
}
}
/**
* Deletes a specific snapshot by request options
* @param {SnapshotRequestOptions} requestOpts - Request options to match
* @returns {boolean} - True if snapshot was deleted, false if not found
*/
deleteSnapshot (requestOpts) {
const request = formatRequestKey(requestOpts, this.#headerFilters, this.matchOptions)
const hash = createRequestHash(request)
return this.#snapshots.delete(hash)
}
/**
* Gets information about a specific snapshot
* @param {SnapshotRequestOptions} requestOpts - Request options to match
* @returns {SnapshotInfo|null} - Snapshot information or null if not found
*/
getSnapshotInfo (requestOpts) {
const request = formatRequestKey(requestOpts, this.#headerFilters, this.matchOptions)
const hash = createRequestHash(request)
const snapshot = this.#snapshots.get(hash)
if (!snapshot) return null
return {
hash,
request: snapshot.request,
responseCount: snapshot.responses ? snapshot.responses.length : (snapshot.response ? 1 : 0), // .response for legacy snapshots
callCount: snapshot.callCount || 0,
timestamp: snapshot.timestamp
}
}
/**
* Replaces all snapshots with new data (full replacement)
* @param {Array<{hash: string; snapshot: SnapshotEntry}>|Record} snapshotData - New snapshot data to replace existing ones
* @returns {void}
*/
replaceSnapshots (snapshotData) {
this.#snapshots.clear()
if (Array.isArray(snapshotData)) {
for (const { hash, snapshot } of snapshotData) {
this.#snapshots.set(hash, snapshot)
}
} else if (snapshotData && typeof snapshotData === 'object') {
// Legacy object format
this.#snapshots = new Map(Object.entries(snapshotData))
}
}
/**
* Starts the auto-flush timer
* @returns {void}
*/
#startAutoFlush () {
return this.#scheduleFlush()
}
/**
* Stops the auto-flush timer
* @returns {void}
*/
#stopAutoFlush () {
if (this.#flushTimeout) {
clearTimeout(this.#flushTimeout)
// Ensure any pending flush is completed
this.saveSnapshots().catch(() => {
// Ignore flush errors
})
this.#flushTimeout = null
}
}
/**
* Schedules a flush (debounced to avoid excessive writes)
*/
#scheduleFlush () {
this.#flushTimeout = setTimeout(() => {
this.saveSnapshots().catch(() => {
// Ignore flush errors
})
if (this.#autoFlush) {
this.#flushTimeout?.refresh()
} else {
this.#flushTimeout = null
}
}, 1000) // 1 second debounce
}
/**
* Cleanup method to stop timers
* @returns {void}
*/
destroy () {
this.#stopAutoFlush()
if (this.#flushTimeout) {
clearTimeout(this.#flushTimeout)
this.#flushTimeout = null
}
}
/**
* Async close method that saves all recordings and performs cleanup
* @returns {Promise}
*/
async close () {
// Save any pending recordings if we have a snapshot path
if (this.#snapshotPath && this.#snapshots.size !== 0) {
await this.saveSnapshots()
}
// Perform cleanup
this.destroy()
}
}
module.exports = { SnapshotRecorder, formatRequestKey, createRequestHash, filterHeadersForMatching, filterHeadersForStorage, createHeaderFilters }
================================================
FILE: lib/mock/snapshot-utils.js
================================================
'use strict'
const { InvalidArgumentError } = require('../core/errors')
const { runtimeFeatures } = require('../util/runtime-features.js')
/**
* @typedef {Object} HeaderFilters
* @property {Set} ignore - Set of headers to ignore for matching
* @property {Set} exclude - Set of headers to exclude from matching
* @property {Set} match - Set of headers to match (empty means match
*/
/**
* Creates cached header sets for performance
*
* @param {import('./snapshot-recorder').SnapshotRecorderMatchOptions} matchOptions - Matching options for headers
* @returns {HeaderFilters} - Cached sets for ignore, exclude, and match headers
*/
function createHeaderFilters (matchOptions = {}) {
const { ignoreHeaders = [], excludeHeaders = [], matchHeaders = [], caseSensitive = false } = matchOptions
return {
ignore: new Set(ignoreHeaders.map(header => caseSensitive ? header : header.toLowerCase())),
exclude: new Set(excludeHeaders.map(header => caseSensitive ? header : header.toLowerCase())),
match: new Set(matchHeaders.map(header => caseSensitive ? header : header.toLowerCase()))
}
}
const crypto = runtimeFeatures.has('crypto')
? require('node:crypto')
: null
/**
* @callback HashIdFunction
* @param {string} value - The value to hash
* @returns {string} - The base64url encoded hash of the value
*/
/**
* Generates a hash for a given value
* @type {HashIdFunction}
*/
const hashId = crypto?.hash
? (value) => crypto.hash('sha256', value, 'base64url')
: (value) => Buffer.from(value).toString('base64url')
/**
* @typedef {(url: string) => boolean} IsUrlExcluded Checks if a URL matches any of the exclude patterns
*/
/** @typedef {{[key: Lowercase]: string}} NormalizedHeaders */
/** @typedef {Array} UndiciHeaders */
/** @typedef {Record} Headers */
/**
* @param {*} headers
* @returns {headers is UndiciHeaders}
*/
function isUndiciHeaders (headers) {
return Array.isArray(headers) && (headers.length & 1) === 0
}
/**
* Factory function to create a URL exclusion checker
* @param {Array} [excludePatterns=[]] - Array of patterns to exclude
* @returns {IsUrlExcluded} - A function that checks if a URL matches any of the exclude patterns
*/
function isUrlExcludedFactory (excludePatterns = []) {
if (excludePatterns.length === 0) {
return () => false
}
return function isUrlExcluded (url) {
let urlLowerCased
for (const pattern of excludePatterns) {
if (typeof pattern === 'string') {
if (!urlLowerCased) {
// Convert URL to lowercase only once
urlLowerCased = url.toLowerCase()
}
// Simple string match (case-insensitive)
if (urlLowerCased.includes(pattern.toLowerCase())) {
return true
}
} else if (pattern instanceof RegExp) {
// Regex pattern match
if (pattern.test(url)) {
return true
}
}
}
return false
}
}
/**
* Normalizes headers for consistent comparison
*
* @param {Object|UndiciHeaders} headers - Headers to normalize
* @returns {NormalizedHeaders} - Normalized headers as a lowercase object
*/
function normalizeHeaders (headers) {
/** @type {NormalizedHeaders} */
const normalizedHeaders = {}
if (!headers) return normalizedHeaders
// Handle array format (undici internal format: [name, value, name, value, ...])
if (isUndiciHeaders(headers)) {
for (let i = 0; i < headers.length; i += 2) {
const key = headers[i]
const value = headers[i + 1]
if (key && value !== undefined) {
// Convert Buffers to strings if needed
const keyStr = Buffer.isBuffer(key) ? key.toString() : key
const valueStr = Buffer.isBuffer(value) ? value.toString() : value
normalizedHeaders[keyStr.toLowerCase()] = valueStr
}
}
return normalizedHeaders
}
// Handle object format
if (headers && typeof headers === 'object') {
for (const [key, value] of Object.entries(headers)) {
if (key && typeof key === 'string') {
normalizedHeaders[key.toLowerCase()] = Array.isArray(value) ? value.join(', ') : String(value)
}
}
}
return normalizedHeaders
}
const validSnapshotModes = /** @type {const} */ (['record', 'playback', 'update'])
/** @typedef {typeof validSnapshotModes[number]} SnapshotMode */
/**
* @param {*} mode - The snapshot mode to validate
* @returns {asserts mode is SnapshotMode}
*/
function validateSnapshotMode (mode) {
if (!validSnapshotModes.includes(mode)) {
throw new InvalidArgumentError(`Invalid snapshot mode: ${mode}. Must be one of: ${validSnapshotModes.join(', ')}`)
}
}
module.exports = {
createHeaderFilters,
hashId,
isUndiciHeaders,
normalizeHeaders,
isUrlExcludedFactory,
validateSnapshotMode
}
================================================
FILE: lib/util/cache.js
================================================
'use strict'
const {
safeHTTPMethods,
pathHasQueryOrFragment,
hasSafeIterator
} = require('../core/util')
const { serializePathWithQuery } = require('../core/util')
/**
* @param {import('../../types/dispatcher.d.ts').default.DispatchOptions} opts
*/
function makeCacheKey (opts) {
if (!opts.origin) {
throw new Error('opts.origin is undefined')
}
let fullPath = opts.path || '/'
if (opts.query && !pathHasQueryOrFragment(opts.path)) {
fullPath = serializePathWithQuery(fullPath, opts.query)
}
return {
origin: opts.origin.toString(),
method: opts.method,
path: fullPath,
headers: opts.headers
}
}
/**
* @param {Record}
* @returns {Record}
*/
function normalizeHeaders (opts) {
let headers
if (opts.headers == null) {
headers = {}
} else if (typeof opts.headers === 'object') {
headers = {}
if (hasSafeIterator(opts.headers)) {
for (const x of opts.headers) {
if (!Array.isArray(x)) {
throw new Error('opts.headers is not a valid header map')
}
const [key, val] = x
if (typeof key !== 'string' || typeof val !== 'string') {
throw new Error('opts.headers is not a valid header map')
}
headers[key.toLowerCase()] = val
}
} else {
for (const key of Object.keys(opts.headers)) {
headers[key.toLowerCase()] = opts.headers[key]
}
}
} else {
throw new Error('opts.headers is not an object')
}
return headers
}
/**
* @param {any} key
*/
function assertCacheKey (key) {
if (typeof key !== 'object') {
throw new TypeError(`expected key to be object, got ${typeof key}`)
}
for (const property of ['origin', 'method', 'path']) {
if (typeof key[property] !== 'string') {
throw new TypeError(`expected key.${property} to be string, got ${typeof key[property]}`)
}
}
if (key.headers !== undefined && typeof key.headers !== 'object') {
throw new TypeError(`expected headers to be object, got ${typeof key}`)
}
}
/**
* @param {any} value
*/
function assertCacheValue (value) {
if (typeof value !== 'object') {
throw new TypeError(`expected value to be object, got ${typeof value}`)
}
for (const property of ['statusCode', 'cachedAt', 'staleAt', 'deleteAt']) {
if (typeof value[property] !== 'number') {
throw new TypeError(`expected value.${property} to be number, got ${typeof value[property]}`)
}
}
if (typeof value.statusMessage !== 'string') {
throw new TypeError(`expected value.statusMessage to be string, got ${typeof value.statusMessage}`)
}
if (value.headers != null && typeof value.headers !== 'object') {
throw new TypeError(`expected value.rawHeaders to be object, got ${typeof value.headers}`)
}
if (value.vary !== undefined && typeof value.vary !== 'object') {
throw new TypeError(`expected value.vary to be object, got ${typeof value.vary}`)
}
if (value.etag !== undefined && typeof value.etag !== 'string') {
throw new TypeError(`expected value.etag to be string, got ${typeof value.etag}`)
}
}
/**
* @see https://www.rfc-editor.org/rfc/rfc9111.html#name-cache-control
* @see https://www.iana.org/assignments/http-cache-directives/http-cache-directives.xhtml
* @param {string | string[]} header
* @returns {import('../../types/cache-interceptor.d.ts').default.CacheControlDirectives}
*/
function parseCacheControlHeader (header) {
/**
* @type {import('../../types/cache-interceptor.d.ts').default.CacheControlDirectives}
*/
const output = {}
let directives
if (Array.isArray(header)) {
directives = []
for (const directive of header) {
directives.push(...directive.split(','))
}
} else {
directives = header.split(',')
}
for (let i = 0; i < directives.length; i++) {
const directive = directives[i].toLowerCase()
const keyValueDelimiter = directive.indexOf('=')
let key
let value
if (keyValueDelimiter !== -1) {
key = directive.substring(0, keyValueDelimiter).trimStart()
value = directive.substring(keyValueDelimiter + 1)
} else {
key = directive.trim()
}
switch (key) {
case 'min-fresh':
case 'max-stale':
case 'max-age':
case 's-maxage':
case 'stale-while-revalidate':
case 'stale-if-error': {
if (value === undefined || value[0] === ' ') {
continue
}
if (
value.length >= 2 &&
value[0] === '"' &&
value[value.length - 1] === '"'
) {
value = value.substring(1, value.length - 1)
}
const parsedValue = parseInt(value, 10)
// eslint-disable-next-line no-self-compare
if (parsedValue !== parsedValue) {
continue
}
if (key === 'max-age' && key in output && output[key] >= parsedValue) {
continue
}
output[key] = parsedValue
break
}
case 'private':
case 'no-cache': {
if (value) {
// The private and no-cache directives can be unqualified (aka just
// `private` or `no-cache`) or qualified (w/ a value). When they're
// qualified, it's a list of headers like `no-cache=header1`,
// `no-cache="header1"`, or `no-cache="header1, header2"`
// If we're given multiple headers, the comma messes us up since
// we split the full header by commas. So, let's loop through the
// remaining parts in front of us until we find one that ends in a
// quote. We can then just splice all of the parts in between the
// starting quote and the ending quote out of the directives array
// and continue parsing like normal.
// https://www.rfc-editor.org/rfc/rfc9111.html#name-no-cache-2
if (value[0] === '"') {
// Something like `no-cache="some-header"` OR `no-cache="some-header, another-header"`.
// Add the first header on and cut off the leading quote
const headers = [value.substring(1)]
let foundEndingQuote = value[value.length - 1] === '"'
if (!foundEndingQuote) {
// Something like `no-cache="some-header, another-header"`
// This can still be something invalid, e.g. `no-cache="some-header, ...`
for (let j = i + 1; j < directives.length; j++) {
const nextPart = directives[j]
const nextPartLength = nextPart.length
headers.push(nextPart.trim())
if (nextPartLength !== 0 && nextPart[nextPartLength - 1] === '"') {
foundEndingQuote = true
break
}
}
}
if (foundEndingQuote) {
let lastHeader = headers[headers.length - 1]
if (lastHeader[lastHeader.length - 1] === '"') {
lastHeader = lastHeader.substring(0, lastHeader.length - 1)
headers[headers.length - 1] = lastHeader
}
if (key in output) {
output[key] = output[key].concat(headers)
} else {
output[key] = headers
}
}
} else {
// Something like `no-cache="some-header"`
if (key in output) {
output[key] = output[key].concat(value)
} else {
output[key] = [value]
}
}
break
}
}
// eslint-disable-next-line no-fallthrough
case 'public':
case 'no-store':
case 'must-revalidate':
case 'proxy-revalidate':
case 'immutable':
case 'no-transform':
case 'must-understand':
case 'only-if-cached':
if (value) {
// These are qualified (something like `public=...`) when they aren't
// allowed to be, skip
continue
}
output[key] = true
break
default:
// Ignore unknown directives as per https://www.rfc-editor.org/rfc/rfc9111.html#section-5.2.3-1
continue
}
}
return output
}
/**
* @param {string | string[]} varyHeader Vary header from the server
* @param {Record} headers Request headers
* @returns {Record}
*/
function parseVaryHeader (varyHeader, headers) {
if (typeof varyHeader === 'string' && varyHeader.includes('*')) {
return headers
}
const output = /** @type {Record} */ ({})
const varyingHeaders = typeof varyHeader === 'string'
? varyHeader.split(',')
: varyHeader
for (const header of varyingHeaders) {
const trimmedHeader = header.trim().toLowerCase()
output[trimmedHeader] = headers[trimmedHeader] ?? null
}
return output
}
/**
* Note: this deviates from the spec a little. Empty etags ("", W/"") are valid,
* however, including them in cached resposnes serves little to no purpose.
*
* @see https://www.rfc-editor.org/rfc/rfc9110.html#name-etag
*
* @param {string} etag
* @returns {boolean}
*/
function isEtagUsable (etag) {
if (etag.length <= 2) {
// Shortest an etag can be is two chars (just ""). This is where we deviate
// from the spec requiring a min of 3 chars however
return false
}
if (etag[0] === '"' && etag[etag.length - 1] === '"') {
// ETag: ""asd123"" or ETag: "W/"asd123"", kinda undefined behavior in the
// spec. Some servers will accept these while others don't.
// ETag: "asd123"
return !(etag[1] === '"' || etag.startsWith('"W/'))
}
if (etag.startsWith('W/"') && etag[etag.length - 1] === '"') {
// ETag: W/"", also where we deviate from the spec & require a min of 3
// chars
// ETag: for W/"", W/"asd123"
return etag.length !== 4
}
// Anything else
return false
}
/**
* @param {unknown} store
* @returns {asserts store is import('../../types/cache-interceptor.d.ts').default.CacheStore}
*/
function assertCacheStore (store, name = 'CacheStore') {
if (typeof store !== 'object' || store === null) {
throw new TypeError(`expected type of ${name} to be a CacheStore, got ${store === null ? 'null' : typeof store}`)
}
for (const fn of ['get', 'createWriteStream', 'delete']) {
if (typeof store[fn] !== 'function') {
throw new TypeError(`${name} needs to have a \`${fn}()\` function`)
}
}
}
/**
* @param {unknown} methods
* @returns {asserts methods is import('../../types/cache-interceptor.d.ts').default.CacheMethods[]}
*/
function assertCacheMethods (methods, name = 'CacheMethods') {
if (!Array.isArray(methods)) {
throw new TypeError(`expected type of ${name} needs to be an array, got ${methods === null ? 'null' : typeof methods}`)
}
if (methods.length === 0) {
throw new TypeError(`${name} needs to have at least one method`)
}
for (const method of methods) {
if (!safeHTTPMethods.includes(method)) {
throw new TypeError(`element of ${name}-array needs to be one of following values: ${safeHTTPMethods.join(', ')}, got ${method}`)
}
}
}
/**
* Creates a string key for request deduplication purposes.
* This key is used to identify in-flight requests that can be shared.
* @param {import('../../types/cache-interceptor.d.ts').default.CacheKey} cacheKey
* @param {Set} [excludeHeaders] Set of lowercase header names to exclude from the key
* @returns {string}
*/
function makeDeduplicationKey (cacheKey, excludeHeaders) {
// Create a deterministic string key from the cache key
// Include origin, method, path, and sorted headers
let key = `${cacheKey.origin}:${cacheKey.method}:${cacheKey.path}`
if (cacheKey.headers) {
const sortedHeaders = Object.keys(cacheKey.headers).sort()
for (const header of sortedHeaders) {
// Skip excluded headers
if (excludeHeaders?.has(header.toLowerCase())) {
continue
}
const value = cacheKey.headers[header]
key += `:${header}=${Array.isArray(value) ? value.join(',') : value}`
}
}
return key
}
module.exports = {
makeCacheKey,
normalizeHeaders,
assertCacheKey,
assertCacheValue,
parseCacheControlHeader,
parseVaryHeader,
isEtagUsable,
assertCacheMethods,
assertCacheStore,
makeDeduplicationKey
}
================================================
FILE: lib/util/date.js
================================================
'use strict'
/**
* @see https://www.rfc-editor.org/rfc/rfc9110.html#name-date-time-formats
*
* @param {string} date
* @returns {Date | undefined}
*/
function parseHttpDate (date) {
// Sun, 06 Nov 1994 08:49:37 GMT ; IMF-fixdate
// Sun Nov 6 08:49:37 1994 ; ANSI C's asctime() format
// Sunday, 06-Nov-94 08:49:37 GMT ; obsolete RFC 850 format
switch (date[3]) {
case ',': return parseImfDate(date)
case ' ': return parseAscTimeDate(date)
default: return parseRfc850Date(date)
}
}
/**
* @see https://httpwg.org/specs/rfc9110.html#preferred.date.format
*
* @param {string} date
* @returns {Date | undefined}
*/
function parseImfDate (date) {
if (
date.length !== 29 ||
date[4] !== ' ' ||
date[7] !== ' ' ||
date[11] !== ' ' ||
date[16] !== ' ' ||
date[19] !== ':' ||
date[22] !== ':' ||
date[25] !== ' ' ||
date[26] !== 'G' ||
date[27] !== 'M' ||
date[28] !== 'T'
) {
return undefined
}
let weekday = -1
if (date[0] === 'S' && date[1] === 'u' && date[2] === 'n') { // Sunday
weekday = 0
} else if (date[0] === 'M' && date[1] === 'o' && date[2] === 'n') { // Monday
weekday = 1
} else if (date[0] === 'T' && date[1] === 'u' && date[2] === 'e') { // Tuesday
weekday = 2
} else if (date[0] === 'W' && date[1] === 'e' && date[2] === 'd') { // Wednesday
weekday = 3
} else if (date[0] === 'T' && date[1] === 'h' && date[2] === 'u') { // Thursday
weekday = 4
} else if (date[0] === 'F' && date[1] === 'r' && date[2] === 'i') { // Friday
weekday = 5
} else if (date[0] === 'S' && date[1] === 'a' && date[2] === 't') { // Saturday
weekday = 6
} else {
return undefined // Not a valid day of the week
}
let day = 0
if (date[5] === '0') {
// Single digit day, e.g. "Sun Nov 6 08:49:37 1994"
const code = date.charCodeAt(6)
if (code < 49 || code > 57) {
return undefined // Not a digit
}
day = code - 48 // Convert ASCII code to number
} else {
const code1 = date.charCodeAt(5)
if (code1 < 49 || code1 > 51) {
return undefined // Not a digit between 1 and 3
}
const code2 = date.charCodeAt(6)
if (code2 < 48 || code2 > 57) {
return undefined // Not a digit
}
day = (code1 - 48) * 10 + (code2 - 48) // Convert ASCII codes to number
}
let monthIdx = -1
if (
(date[8] === 'J' && date[9] === 'a' && date[10] === 'n')
) {
monthIdx = 0 // Jan
} else if (
(date[8] === 'F' && date[9] === 'e' && date[10] === 'b')
) {
monthIdx = 1 // Feb
} else if (
(date[8] === 'M' && date[9] === 'a')
) {
if (date[10] === 'r') {
monthIdx = 2 // Mar
} else if (date[10] === 'y') {
monthIdx = 4 // May
} else {
return undefined // Invalid month
}
} else if (
(date[8] === 'J')
) {
if (date[9] === 'a' && date[10] === 'n') {
monthIdx = 0 // Jan
} else if (date[9] === 'u') {
if (date[10] === 'n') {
monthIdx = 5 // Jun
} else if (date[10] === 'l') {
monthIdx = 6 // Jul
} else {
return undefined // Invalid month
}
} else {
return undefined // Invalid month
}
} else if (
(date[8] === 'A')
) {
if (date[9] === 'p' && date[10] === 'r') {
monthIdx = 3 // Apr
} else if (date[9] === 'u' && date[10] === 'g') {
monthIdx = 7 // Aug
} else {
return undefined // Invalid month
}
} else if (
(date[8] === 'S' && date[9] === 'e' && date[10] === 'p')
) {
monthIdx = 8 // Sep
} else if (
(date[8] === 'O' && date[9] === 'c' && date[10] === 't')
) {
monthIdx = 9 // Oct
} else if (
(date[8] === 'N' && date[9] === 'o' && date[10] === 'v')
) {
monthIdx = 10 // Nov
} else if (
(date[8] === 'D' && date[9] === 'e' && date[10] === 'c')
) {
monthIdx = 11 // Dec
} else {
// Not a valid month
return undefined
}
const yearDigit1 = date.charCodeAt(12)
if (yearDigit1 < 48 || yearDigit1 > 57) {
return undefined // Not a digit
}
const yearDigit2 = date.charCodeAt(13)
if (yearDigit2 < 48 || yearDigit2 > 57) {
return undefined // Not a digit
}
const yearDigit3 = date.charCodeAt(14)
if (yearDigit3 < 48 || yearDigit3 > 57) {
return undefined // Not a digit
}
const yearDigit4 = date.charCodeAt(15)
if (yearDigit4 < 48 || yearDigit4 > 57) {
return undefined // Not a digit
}
const year = (yearDigit1 - 48) * 1000 + (yearDigit2 - 48) * 100 + (yearDigit3 - 48) * 10 + (yearDigit4 - 48)
let hour = 0
if (date[17] === '0') {
const code = date.charCodeAt(18)
if (code < 48 || code > 57) {
return undefined // Not a digit
}
hour = code - 48 // Convert ASCII code to number
} else {
const code1 = date.charCodeAt(17)
if (code1 < 48 || code1 > 50) {
return undefined // Not a digit between 0 and 2
}
const code2 = date.charCodeAt(18)
if (code2 < 48 || code2 > 57) {
return undefined // Not a digit
}
if (code1 === 50 && code2 > 51) {
return undefined // Hour cannot be greater than 23
}
hour = (code1 - 48) * 10 + (code2 - 48) // Convert ASCII codes to number
}
let minute = 0
if (date[20] === '0') {
const code = date.charCodeAt(21)
if (code < 48 || code > 57) {
return undefined // Not a digit
}
minute = code - 48 // Convert ASCII code to number
} else {
const code1 = date.charCodeAt(20)
if (code1 < 48 || code1 > 53) {
return undefined // Not a digit between 0 and 5
}
const code2 = date.charCodeAt(21)
if (code2 < 48 || code2 > 57) {
return undefined // Not a digit
}
minute = (code1 - 48) * 10 + (code2 - 48) // Convert ASCII codes to number
}
let second = 0
if (date[23] === '0') {
const code = date.charCodeAt(24)
if (code < 48 || code > 57) {
return undefined // Not a digit
}
second = code - 48 // Convert ASCII code to number
} else {
const code1 = date.charCodeAt(23)
if (code1 < 48 || code1 > 53) {
return undefined // Not a digit between 0 and 5
}
const code2 = date.charCodeAt(24)
if (code2 < 48 || code2 > 57) {
return undefined // Not a digit
}
second = (code1 - 48) * 10 + (code2 - 48) // Convert ASCII codes to number
}
const result = new Date(Date.UTC(year, monthIdx, day, hour, minute, second))
return result.getUTCDay() === weekday ? result : undefined
}
/**
* @see https://httpwg.org/specs/rfc9110.html#obsolete.date.formats
*
* @param {string} date
* @returns {Date | undefined}
*/
function parseAscTimeDate (date) {
// This is assumed to be in UTC
if (
date.length !== 24 ||
date[7] !== ' ' ||
date[10] !== ' ' ||
date[19] !== ' '
) {
return undefined
}
let weekday = -1
if (date[0] === 'S' && date[1] === 'u' && date[2] === 'n') { // Sunday
weekday = 0
} else if (date[0] === 'M' && date[1] === 'o' && date[2] === 'n') { // Monday
weekday = 1
} else if (date[0] === 'T' && date[1] === 'u' && date[2] === 'e') { // Tuesday
weekday = 2
} else if (date[0] === 'W' && date[1] === 'e' && date[2] === 'd') { // Wednesday
weekday = 3
} else if (date[0] === 'T' && date[1] === 'h' && date[2] === 'u') { // Thursday
weekday = 4
} else if (date[0] === 'F' && date[1] === 'r' && date[2] === 'i') { // Friday
weekday = 5
} else if (date[0] === 'S' && date[1] === 'a' && date[2] === 't') { // Saturday
weekday = 6
} else {
return undefined // Not a valid day of the week
}
let monthIdx = -1
if (
(date[4] === 'J' && date[5] === 'a' && date[6] === 'n')
) {
monthIdx = 0 // Jan
} else if (
(date[4] === 'F' && date[5] === 'e' && date[6] === 'b')
) {
monthIdx = 1 // Feb
} else if (
(date[4] === 'M' && date[5] === 'a')
) {
if (date[6] === 'r') {
monthIdx = 2 // Mar
} else if (date[6] === 'y') {
monthIdx = 4 // May
} else {
return undefined // Invalid month
}
} else if (
(date[4] === 'J')
) {
if (date[5] === 'a' && date[6] === 'n') {
monthIdx = 0 // Jan
} else if (date[5] === 'u') {
if (date[6] === 'n') {
monthIdx = 5 // Jun
} else if (date[6] === 'l') {
monthIdx = 6 // Jul
} else {
return undefined // Invalid month
}
} else {
return undefined // Invalid month
}
} else if (
(date[4] === 'A')
) {
if (date[5] === 'p' && date[6] === 'r') {
monthIdx = 3 // Apr
} else if (date[5] === 'u' && date[6] === 'g') {
monthIdx = 7 // Aug
} else {
return undefined // Invalid month
}
} else if (
(date[4] === 'S' && date[5] === 'e' && date[6] === 'p')
) {
monthIdx = 8 // Sep
} else if (
(date[4] === 'O' && date[5] === 'c' && date[6] === 't')
) {
monthIdx = 9 // Oct
} else if (
(date[4] === 'N' && date[5] === 'o' && date[6] === 'v')
) {
monthIdx = 10 // Nov
} else if (
(date[4] === 'D' && date[5] === 'e' && date[6] === 'c')
) {
monthIdx = 11 // Dec
} else {
// Not a valid month
return undefined
}
let day = 0
if (date[8] === ' ') {
// Single digit day, e.g. "Sun Nov 6 08:49:37 1994"
const code = date.charCodeAt(9)
if (code < 49 || code > 57) {
return undefined // Not a digit
}
day = code - 48 // Convert ASCII code to number
} else {
const code1 = date.charCodeAt(8)
if (code1 < 49 || code1 > 51) {
return undefined // Not a digit between 1 and 3
}
const code2 = date.charCodeAt(9)
if (code2 < 48 || code2 > 57) {
return undefined // Not a digit
}
day = (code1 - 48) * 10 + (code2 - 48) // Convert ASCII codes to number
}
let hour = 0
if (date[11] === '0') {
const code = date.charCodeAt(12)
if (code < 48 || code > 57) {
return undefined // Not a digit
}
hour = code - 48 // Convert ASCII code to number
} else {
const code1 = date.charCodeAt(11)
if (code1 < 48 || code1 > 50) {
return undefined // Not a digit between 0 and 2
}
const code2 = date.charCodeAt(12)
if (code2 < 48 || code2 > 57) {
return undefined // Not a digit
}
if (code1 === 50 && code2 > 51) {
return undefined // Hour cannot be greater than 23
}
hour = (code1 - 48) * 10 + (code2 - 48) // Convert ASCII codes to number
}
let minute = 0
if (date[14] === '0') {
const code = date.charCodeAt(15)
if (code < 48 || code > 57) {
return undefined // Not a digit
}
minute = code - 48 // Convert ASCII code to number
} else {
const code1 = date.charCodeAt(14)
if (code1 < 48 || code1 > 53) {
return undefined // Not a digit between 0 and 5
}
const code2 = date.charCodeAt(15)
if (code2 < 48 || code2 > 57) {
return undefined // Not a digit
}
minute = (code1 - 48) * 10 + (code2 - 48) // Convert ASCII codes to number
}
let second = 0
if (date[17] === '0') {
const code = date.charCodeAt(18)
if (code < 48 || code > 57) {
return undefined // Not a digit
}
second = code - 48 // Convert ASCII code to number
} else {
const code1 = date.charCodeAt(17)
if (code1 < 48 || code1 > 53) {
return undefined // Not a digit between 0 and 5
}
const code2 = date.charCodeAt(18)
if (code2 < 48 || code2 > 57) {
return undefined // Not a digit
}
second = (code1 - 48) * 10 + (code2 - 48) // Convert ASCII codes to number
}
const yearDigit1 = date.charCodeAt(20)
if (yearDigit1 < 48 || yearDigit1 > 57) {
return undefined // Not a digit
}
const yearDigit2 = date.charCodeAt(21)
if (yearDigit2 < 48 || yearDigit2 > 57) {
return undefined // Not a digit
}
const yearDigit3 = date.charCodeAt(22)
if (yearDigit3 < 48 || yearDigit3 > 57) {
return undefined // Not a digit
}
const yearDigit4 = date.charCodeAt(23)
if (yearDigit4 < 48 || yearDigit4 > 57) {
return undefined // Not a digit
}
const year = (yearDigit1 - 48) * 1000 + (yearDigit2 - 48) * 100 + (yearDigit3 - 48) * 10 + (yearDigit4 - 48)
const result = new Date(Date.UTC(year, monthIdx, day, hour, minute, second))
return result.getUTCDay() === weekday ? result : undefined
}
/**
* @see https://httpwg.org/specs/rfc9110.html#obsolete.date.formats
*
* @param {string} date
* @returns {Date | undefined}
*/
function parseRfc850Date (date) {
let commaIndex = -1
let weekday = -1
if (date[0] === 'S') {
if (date[1] === 'u' && date[2] === 'n' && date[3] === 'd' && date[4] === 'a' && date[5] === 'y') {
weekday = 0 // Sunday
commaIndex = 6
} else if (date[1] === 'a' && date[2] === 't' && date[3] === 'u' && date[4] === 'r' && date[5] === 'd' && date[6] === 'a' && date[7] === 'y') {
weekday = 6 // Saturday
commaIndex = 8
}
} else if (date[0] === 'M' && date[1] === 'o' && date[2] === 'n' && date[3] === 'd' && date[4] === 'a' && date[5] === 'y') {
weekday = 1 // Monday
commaIndex = 6
} else if (date[0] === 'T') {
if (date[1] === 'u' && date[2] === 'e' && date[3] === 's' && date[4] === 'd' && date[5] === 'a' && date[6] === 'y') {
weekday = 2 // Tuesday
commaIndex = 7
} else if (date[1] === 'h' && date[2] === 'u' && date[3] === 'r' && date[4] === 's' && date[5] === 'd' && date[6] === 'a' && date[7] === 'y') {
weekday = 4 // Thursday
commaIndex = 8
}
} else if (date[0] === 'W' && date[1] === 'e' && date[2] === 'd' && date[3] === 'n' && date[4] === 'e' && date[5] === 's' && date[6] === 'd' && date[7] === 'a' && date[8] === 'y') {
weekday = 3 // Wednesday
commaIndex = 9
} else if (date[0] === 'F' && date[1] === 'r' && date[2] === 'i' && date[3] === 'd' && date[4] === 'a' && date[5] === 'y') {
weekday = 5 // Friday
commaIndex = 6
} else {
// Not a valid day name
return undefined
}
if (
date[commaIndex] !== ',' ||
(date.length - commaIndex - 1) !== 23 ||
date[commaIndex + 1] !== ' ' ||
date[commaIndex + 4] !== '-' ||
date[commaIndex + 8] !== '-' ||
date[commaIndex + 11] !== ' ' ||
date[commaIndex + 14] !== ':' ||
date[commaIndex + 17] !== ':' ||
date[commaIndex + 20] !== ' ' ||
date[commaIndex + 21] !== 'G' ||
date[commaIndex + 22] !== 'M' ||
date[commaIndex + 23] !== 'T'
) {
return undefined
}
let day = 0
if (date[commaIndex + 2] === '0') {
// Single digit day, e.g. "Sun Nov 6 08:49:37 1994"
const code = date.charCodeAt(commaIndex + 3)
if (code < 49 || code > 57) {
return undefined // Not a digit
}
day = code - 48 // Convert ASCII code to number
} else {
const code1 = date.charCodeAt(commaIndex + 2)
if (code1 < 49 || code1 > 51) {
return undefined // Not a digit between 1 and 3
}
const code2 = date.charCodeAt(commaIndex + 3)
if (code2 < 48 || code2 > 57) {
return undefined // Not a digit
}
day = (code1 - 48) * 10 + (code2 - 48) // Convert ASCII codes to number
}
let monthIdx = -1
if (
(date[commaIndex + 5] === 'J' && date[commaIndex + 6] === 'a' && date[commaIndex + 7] === 'n')
) {
monthIdx = 0 // Jan
} else if (
(date[commaIndex + 5] === 'F' && date[commaIndex + 6] === 'e' && date[commaIndex + 7] === 'b')
) {
monthIdx = 1 // Feb
} else if (
(date[commaIndex + 5] === 'M' && date[commaIndex + 6] === 'a' && date[commaIndex + 7] === 'r')
) {
monthIdx = 2 // Mar
} else if (
(date[commaIndex + 5] === 'A' && date[commaIndex + 6] === 'p' && date[commaIndex + 7] === 'r')
) {
monthIdx = 3 // Apr
} else if (
(date[commaIndex + 5] === 'M' && date[commaIndex + 6] === 'a' && date[commaIndex + 7] === 'y')
) {
monthIdx = 4 // May
} else if (
(date[commaIndex + 5] === 'J' && date[commaIndex + 6] === 'u' && date[commaIndex + 7] === 'n')
) {
monthIdx = 5 // Jun
} else if (
(date[commaIndex + 5] === 'J' && date[commaIndex + 6] === 'u' && date[commaIndex + 7] === 'l')
) {
monthIdx = 6 // Jul
} else if (
(date[commaIndex + 5] === 'A' && date[commaIndex + 6] === 'u' && date[commaIndex + 7] === 'g')
) {
monthIdx = 7 // Aug
} else if (
(date[commaIndex + 5] === 'S' && date[commaIndex + 6] === 'e' && date[commaIndex + 7] === 'p')
) {
monthIdx = 8 // Sep
} else if (
(date[commaIndex + 5] === 'O' && date[commaIndex + 6] === 'c' && date[commaIndex + 7] === 't')
) {
monthIdx = 9 // Oct
} else if (
(date[commaIndex + 5] === 'N' && date[commaIndex + 6] === 'o' && date[commaIndex + 7] === 'v')
) {
monthIdx = 10 // Nov
} else if (
(date[commaIndex + 5] === 'D' && date[commaIndex + 6] === 'e' && date[commaIndex + 7] === 'c')
) {
monthIdx = 11 // Dec
} else {
// Not a valid month
return undefined
}
const yearDigit1 = date.charCodeAt(commaIndex + 9)
if (yearDigit1 < 48 || yearDigit1 > 57) {
return undefined // Not a digit
}
const yearDigit2 = date.charCodeAt(commaIndex + 10)
if (yearDigit2 < 48 || yearDigit2 > 57) {
return undefined // Not a digit
}
let year = (yearDigit1 - 48) * 10 + (yearDigit2 - 48) // Convert ASCII codes to number
// RFC 6265 states that the year is in the range 1970-2069.
// @see https://datatracker.ietf.org/doc/html/rfc6265#section-5.1.1
//
// 3. If the year-value is greater than or equal to 70 and less than or
// equal to 99, increment the year-value by 1900.
// 4. If the year-value is greater than or equal to 0 and less than or
// equal to 69, increment the year-value by 2000.
year += year < 70 ? 2000 : 1900
let hour = 0
if (date[commaIndex + 12] === '0') {
const code = date.charCodeAt(commaIndex + 13)
if (code < 48 || code > 57) {
return undefined // Not a digit
}
hour = code - 48 // Convert ASCII code to number
} else {
const code1 = date.charCodeAt(commaIndex + 12)
if (code1 < 48 || code1 > 50) {
return undefined // Not a digit between 0 and 2
}
const code2 = date.charCodeAt(commaIndex + 13)
if (code2 < 48 || code2 > 57) {
return undefined // Not a digit
}
if (code1 === 50 && code2 > 51) {
return undefined // Hour cannot be greater than 23
}
hour = (code1 - 48) * 10 + (code2 - 48) // Convert ASCII codes to number
}
let minute = 0
if (date[commaIndex + 15] === '0') {
const code = date.charCodeAt(commaIndex + 16)
if (code < 48 || code > 57) {
return undefined // Not a digit
}
minute = code - 48 // Convert ASCII code to number
} else {
const code1 = date.charCodeAt(commaIndex + 15)
if (code1 < 48 || code1 > 53) {
return undefined // Not a digit between 0 and 5
}
const code2 = date.charCodeAt(commaIndex + 16)
if (code2 < 48 || code2 > 57) {
return undefined // Not a digit
}
minute = (code1 - 48) * 10 + (code2 - 48) // Convert ASCII codes to number
}
let second = 0
if (date[commaIndex + 18] === '0') {
const code = date.charCodeAt(commaIndex + 19)
if (code < 48 || code > 57) {
return undefined // Not a digit
}
second = code - 48 // Convert ASCII code to number
} else {
const code1 = date.charCodeAt(commaIndex + 18)
if (code1 < 48 || code1 > 53) {
return undefined // Not a digit between 0 and 5
}
const code2 = date.charCodeAt(commaIndex + 19)
if (code2 < 48 || code2 > 57) {
return undefined // Not a digit
}
second = (code1 - 48) * 10 + (code2 - 48) // Convert ASCII codes to number
}
const result = new Date(Date.UTC(year, monthIdx, day, hour, minute, second))
return result.getUTCDay() === weekday ? result : undefined
}
module.exports = {
parseHttpDate
}
================================================
FILE: lib/util/promise.js
================================================
'use strict'
/**
* @template {*} T
* @typedef {Object} DeferredPromise
* @property {Promise} promise
* @property {(value?: T) => void} resolve
* @property {(reason?: any) => void} reject
*/
/**
* @template {*} T
* @returns {DeferredPromise} An object containing a promise and its resolve/reject methods.
*/
function createDeferredPromise () {
let res
let rej
const promise = new Promise((resolve, reject) => {
res = resolve
rej = reject
})
return { promise, resolve: res, reject: rej }
}
module.exports = {
createDeferredPromise
}
================================================
FILE: lib/util/runtime-features.js
================================================
'use strict'
/** @typedef {`node:${string}`} NodeModuleName */
/** @type {Record any>} */
const lazyLoaders = {
__proto__: null,
'node:crypto': () => require('node:crypto'),
'node:sqlite': () => require('node:sqlite'),
'node:worker_threads': () => require('node:worker_threads'),
'node:zlib': () => require('node:zlib')
}
/**
* @param {NodeModuleName} moduleName
* @returns {boolean}
*/
function detectRuntimeFeatureByNodeModule (moduleName) {
try {
lazyLoaders[moduleName]()
return true
} catch (err) {
if (err.code !== 'ERR_UNKNOWN_BUILTIN_MODULE' && err.code !== 'ERR_NO_CRYPTO') {
throw err
}
return false
}
}
/**
* @param {NodeModuleName} moduleName
* @param {string} property
* @returns {boolean}
*/
function detectRuntimeFeatureByExportedProperty (moduleName, property) {
const module = lazyLoaders[moduleName]()
return typeof module[property] !== 'undefined'
}
const runtimeFeaturesByExportedProperty = /** @type {const} */ (['markAsUncloneable', 'zstd'])
/** @type {Record} */
const exportedPropertyLookup = {
markAsUncloneable: ['node:worker_threads', 'markAsUncloneable'],
zstd: ['node:zlib', 'createZstdDecompress']
}
/** @typedef {typeof runtimeFeaturesByExportedProperty[number]} RuntimeFeatureByExportedProperty */
const runtimeFeaturesAsNodeModule = /** @type {const} */ (['crypto', 'sqlite'])
/** @typedef {typeof runtimeFeaturesAsNodeModule[number]} RuntimeFeatureByNodeModule */
const features = /** @type {const} */ ([
...runtimeFeaturesAsNodeModule,
...runtimeFeaturesByExportedProperty
])
/** @typedef {typeof features[number]} Feature */
/**
* @param {Feature} feature
* @returns {boolean}
*/
function detectRuntimeFeature (feature) {
if (runtimeFeaturesAsNodeModule.includes(/** @type {RuntimeFeatureByNodeModule} */ (feature))) {
return detectRuntimeFeatureByNodeModule(`node:${feature}`)
} else if (runtimeFeaturesByExportedProperty.includes(/** @type {RuntimeFeatureByExportedProperty} */ (feature))) {
const [moduleName, property] = exportedPropertyLookup[feature]
return detectRuntimeFeatureByExportedProperty(moduleName, property)
}
throw new TypeError(`unknown feature: ${feature}`)
}
/**
* @class
* @name RuntimeFeatures
*/
class RuntimeFeatures {
/** @type {Map} */
#map = new Map()
/**
* Clears all cached feature detections.
*/
clear () {
this.#map.clear()
}
/**
* @param {Feature} feature
* @returns {boolean}
*/
has (feature) {
return (
this.#map.get(feature) ?? this.#detectRuntimeFeature(feature)
)
}
/**
* @param {Feature} feature
* @param {boolean} value
*/
set (feature, value) {
if (features.includes(feature) === false) {
throw new TypeError(`unknown feature: ${feature}`)
}
this.#map.set(feature, value)
}
/**
* @param {Feature} feature
* @returns {boolean}
*/
#detectRuntimeFeature (feature) {
const result = detectRuntimeFeature(feature)
this.#map.set(feature, result)
return result
}
}
const instance = new RuntimeFeatures()
module.exports.runtimeFeatures = instance
module.exports.default = instance
================================================
FILE: lib/util/stats.js
================================================
'use strict'
const {
kConnected,
kPending,
kRunning,
kSize,
kFree,
kQueued
} = require('../core/symbols')
class ClientStats {
constructor (client) {
this.connected = client[kConnected]
this.pending = client[kPending]
this.running = client[kRunning]
this.size = client[kSize]
}
}
class PoolStats {
constructor (pool) {
this.connected = pool[kConnected]
this.free = pool[kFree]
this.pending = pool[kPending]
this.queued = pool[kQueued]
this.running = pool[kRunning]
this.size = pool[kSize]
}
}
module.exports = { ClientStats, PoolStats }
================================================
FILE: lib/util/timers.js
================================================
'use strict'
/**
* This module offers an optimized timer implementation designed for scenarios
* where high precision is not critical.
*
* The timer achieves faster performance by using a low-resolution approach,
* with an accuracy target of within 500ms. This makes it particularly useful
* for timers with delays of 1 second or more, where exact timing is less
* crucial.
*
* It's important to note that Node.js timers are inherently imprecise, as
* delays can occur due to the event loop being blocked by other operations.
* Consequently, timers may trigger later than their scheduled time.
*/
/**
* The fastNow variable contains the internal fast timer clock value.
*
* @type {number}
*/
let fastNow = 0
/**
* RESOLUTION_MS represents the target resolution time in milliseconds.
*
* @type {number}
* @default 1000
*/
const RESOLUTION_MS = 1e3
/**
* TICK_MS defines the desired interval in milliseconds between each tick.
* The target value is set to half the resolution time, minus 1 ms, to account
* for potential event loop overhead.
*
* @type {number}
* @default 499
*/
const TICK_MS = (RESOLUTION_MS >> 1) - 1
/**
* fastNowTimeout is a Node.js timer used to manage and process
* the FastTimers stored in the `fastTimers` array.
*
* @type {NodeJS.Timeout}
*/
let fastNowTimeout
/**
* The kFastTimer symbol is used to identify FastTimer instances.
*
* @type {Symbol}
*/
const kFastTimer = Symbol('kFastTimer')
/**
* The fastTimers array contains all active FastTimers.
*
* @type {FastTimer[]}
*/
const fastTimers = []
/**
* These constants represent the various states of a FastTimer.
*/
/**
* The `NOT_IN_LIST` constant indicates that the FastTimer is not included
* in the `fastTimers` array. Timers with this status will not be processed
* during the next tick by the `onTick` function.
*
* A FastTimer can be re-added to the `fastTimers` array by invoking the
* `refresh` method on the FastTimer instance.
*
* @type {-2}
*/
const NOT_IN_LIST = -2
/**
* The `TO_BE_CLEARED` constant indicates that the FastTimer is scheduled
* for removal from the `fastTimers` array. A FastTimer in this state will
* be removed in the next tick by the `onTick` function and will no longer
* be processed.
*
* This status is also set when the `clear` method is called on the FastTimer instance.
*
* @type {-1}
*/
const TO_BE_CLEARED = -1
/**
* The `PENDING` constant signifies that the FastTimer is awaiting processing
* in the next tick by the `onTick` function. Timers with this status will have
* their `_idleStart` value set and their status updated to `ACTIVE` in the next tick.
*
* @type {0}
*/
const PENDING = 0
/**
* The `ACTIVE` constant indicates that the FastTimer is active and waiting
* for its timer to expire. During the next tick, the `onTick` function will
* check if the timer has expired, and if so, it will execute the associated callback.
*
* @type {1}
*/
const ACTIVE = 1
/**
* The onTick function processes the fastTimers array.
*
* @returns {void}
*/
function onTick () {
/**
* Increment the fastNow value by the TICK_MS value, despite the actual time
* that has passed since the last tick. This approach ensures independence
* from the system clock and delays caused by a blocked event loop.
*
* @type {number}
*/
fastNow += TICK_MS
/**
* The `idx` variable is used to iterate over the `fastTimers` array.
* Expired timers are removed by replacing them with the last element in the array.
* Consequently, `idx` is only incremented when the current element is not removed.
*
* @type {number}
*/
let idx = 0
/**
* The len variable will contain the length of the fastTimers array
* and will be decremented when a FastTimer should be removed from the
* fastTimers array.
*
* @type {number}
*/
let len = fastTimers.length
while (idx < len) {
/**
* @type {FastTimer}
*/
const timer = fastTimers[idx]
// If the timer is in the ACTIVE state and the timer has expired, it will
// be processed in the next tick.
if (timer._state === PENDING) {
// Set the _idleStart value to the fastNow value minus the TICK_MS value
// to account for the time the timer was in the PENDING state.
timer._idleStart = fastNow - TICK_MS
timer._state = ACTIVE
} else if (
timer._state === ACTIVE &&
fastNow >= timer._idleStart + timer._idleTimeout
) {
timer._state = TO_BE_CLEARED
timer._idleStart = -1
timer._onTimeout(timer._timerArg)
}
if (timer._state === TO_BE_CLEARED) {
timer._state = NOT_IN_LIST
// Move the last element to the current index and decrement len if it is
// not the only element in the array.
if (--len !== 0) {
fastTimers[idx] = fastTimers[len]
}
} else {
++idx
}
}
// Set the length of the fastTimers array to the new length and thus
// removing the excess FastTimers elements from the array.
fastTimers.length = len
// If there are still active FastTimers in the array, refresh the Timer.
// If there are no active FastTimers, the timer will be refreshed again
// when a new FastTimer is instantiated.
if (fastTimers.length !== 0) {
refreshTimeout()
}
}
function refreshTimeout () {
// If the fastNowTimeout is already set and the Timer has the refresh()-
// method available, call it to refresh the timer.
// Some timer objects returned by setTimeout may not have a .refresh()
// method (e.g. mocked timers in tests).
if (fastNowTimeout?.refresh) {
fastNowTimeout.refresh()
// fastNowTimeout is not instantiated yet or refresh is not availabe,
// create a new Timer.
} else {
clearTimeout(fastNowTimeout)
fastNowTimeout = setTimeout(onTick, TICK_MS)
// If the Timer has an unref method, call it to allow the process to exit,
// if there are no other active handles. When using fake timers or mocked
// environments (like Jest), .unref() may not be defined,
fastNowTimeout?.unref()
}
}
/**
* The `FastTimer` class is a data structure designed to store and manage
* timer information.
*/
class FastTimer {
[kFastTimer] = true
/**
* The state of the timer, which can be one of the following:
* - NOT_IN_LIST (-2)
* - TO_BE_CLEARED (-1)
* - PENDING (0)
* - ACTIVE (1)
*
* @type {-2|-1|0|1}
* @private
*/
_state = NOT_IN_LIST
/**
* The number of milliseconds to wait before calling the callback.
*
* @type {number}
* @private
*/
_idleTimeout = -1
/**
* The time in milliseconds when the timer was started. This value is used to
* calculate when the timer should expire.
*
* @type {number}
* @default -1
* @private
*/
_idleStart = -1
/**
* The function to be executed when the timer expires.
* @type {Function}
* @private
*/
_onTimeout
/**
* The argument to be passed to the callback when the timer expires.
*
* @type {*}
* @private
*/
_timerArg
/**
* @constructor
* @param {Function} callback A function to be executed after the timer
* expires.
* @param {number} delay The time, in milliseconds that the timer should wait
* before the specified function or code is executed.
* @param {*} arg
*/
constructor (callback, delay, arg) {
this._onTimeout = callback
this._idleTimeout = delay
this._timerArg = arg
this.refresh()
}
/**
* Sets the timer's start time to the current time, and reschedules the timer
* to call its callback at the previously specified duration adjusted to the
* current time.
* Using this on a timer that has already called its callback will reactivate
* the timer.
*
* @returns {void}
*/
refresh () {
// In the special case that the timer is not in the list of active timers,
// add it back to the array to be processed in the next tick by the onTick
// function.
if (this._state === NOT_IN_LIST) {
fastTimers.push(this)
}
// If the timer is the only active timer, refresh the fastNowTimeout for
// better resolution.
if (!fastNowTimeout || fastTimers.length === 1) {
refreshTimeout()
}
// Setting the state to PENDING will cause the timer to be reset in the
// next tick by the onTick function.
this._state = PENDING
}
/**
* The `clear` method cancels the timer, preventing it from executing.
*
* @returns {void}
* @private
*/
clear () {
// Set the state to TO_BE_CLEARED to mark the timer for removal in the next
// tick by the onTick function.
this._state = TO_BE_CLEARED
// Reset the _idleStart value to -1 to indicate that the timer is no longer
// active.
this._idleStart = -1
}
}
/**
* This module exports a setTimeout and clearTimeout function that can be
* used as a drop-in replacement for the native functions.
*/
module.exports = {
/**
* The setTimeout() method sets a timer which executes a function once the
* timer expires.
* @param {Function} callback A function to be executed after the timer
* expires.
* @param {number} delay The time, in milliseconds that the timer should
* wait before the specified function or code is executed.
* @param {*} [arg] An optional argument to be passed to the callback function
* when the timer expires.
* @returns {NodeJS.Timeout|FastTimer}
*/
setTimeout (callback, delay, arg) {
// If the delay is less than or equal to the RESOLUTION_MS value return a
// native Node.js Timer instance.
return delay <= RESOLUTION_MS
? setTimeout(callback, delay, arg)
: new FastTimer(callback, delay, arg)
},
/**
* The clearTimeout method cancels an instantiated Timer previously created
* by calling setTimeout.
*
* @param {NodeJS.Timeout|FastTimer} timeout
*/
clearTimeout (timeout) {
// If the timeout is a FastTimer, call its own clear method.
if (timeout[kFastTimer]) {
/**
* @type {FastTimer}
*/
timeout.clear()
// Otherwise it is an instance of a native NodeJS.Timeout, so call the
// Node.js native clearTimeout function.
} else {
clearTimeout(timeout)
}
},
/**
* The setFastTimeout() method sets a fastTimer which executes a function once
* the timer expires.
* @param {Function} callback A function to be executed after the timer
* expires.
* @param {number} delay The time, in milliseconds that the timer should
* wait before the specified function or code is executed.
* @param {*} [arg] An optional argument to be passed to the callback function
* when the timer expires.
* @returns {FastTimer}
*/
setFastTimeout (callback, delay, arg) {
return new FastTimer(callback, delay, arg)
},
/**
* The clearTimeout method cancels an instantiated FastTimer previously
* created by calling setFastTimeout.
*
* @param {FastTimer} timeout
*/
clearFastTimeout (timeout) {
timeout.clear()
},
/**
* The now method returns the value of the internal fast timer clock.
*
* @returns {number}
*/
now () {
return fastNow
},
/**
* Trigger the onTick function to process the fastTimers array.
* Exported for testing purposes only.
* Marking as deprecated to discourage any use outside of testing.
* @deprecated
* @param {number} [delay=0] The delay in milliseconds to add to the now value.
*/
tick (delay = 0) {
fastNow += delay - RESOLUTION_MS + 1
onTick()
onTick()
},
/**
* Reset FastTimers.
* Exported for testing purposes only.
* Marking as deprecated to discourage any use outside of testing.
* @deprecated
*/
reset () {
fastNow = 0
fastTimers.length = 0
clearTimeout(fastNowTimeout)
fastNowTimeout = null
},
/**
* Exporting for testing purposes only.
* Marking as deprecated to discourage any use outside of testing.
* @deprecated
*/
kFastTimer
}
================================================
FILE: lib/web/cache/cache.js
================================================
'use strict'
const assert = require('node:assert')
const { kConstruct } = require('../../core/symbols')
const { urlEquals, getFieldValues } = require('./util')
const { kEnumerableProperty, isDisturbed } = require('../../core/util')
const { webidl } = require('../webidl')
const { cloneResponse, fromInnerResponse, getResponseState } = require('../fetch/response')
const { Request, fromInnerRequest, getRequestState } = require('../fetch/request')
const { fetching } = require('../fetch/index')
const { urlIsHttpHttpsScheme, readAllBytes } = require('../fetch/util')
const { createDeferredPromise } = require('../../util/promise')
/**
* @see https://w3c.github.io/ServiceWorker/#dfn-cache-batch-operation
* @typedef {Object} CacheBatchOperation
* @property {'delete' | 'put'} type
* @property {any} request
* @property {any} response
* @property {import('../../../types/cache').CacheQueryOptions} options
*/
/**
* @see https://w3c.github.io/ServiceWorker/#dfn-request-response-list
* @typedef {[any, any][]} requestResponseList
*/
class Cache {
/**
* @see https://w3c.github.io/ServiceWorker/#dfn-relevant-request-response-list
* @type {requestResponseList}
*/
#relevantRequestResponseList
constructor () {
if (arguments[0] !== kConstruct) {
webidl.illegalConstructor()
}
webidl.util.markAsUncloneable(this)
this.#relevantRequestResponseList = arguments[1]
}
async match (request, options = {}) {
webidl.brandCheck(this, Cache)
const prefix = 'Cache.match'
webidl.argumentLengthCheck(arguments, 1, prefix)
request = webidl.converters.RequestInfo(request)
options = webidl.converters.CacheQueryOptions(options, prefix, 'options')
const p = this.#internalMatchAll(request, options, 1)
if (p.length === 0) {
return
}
return p[0]
}
async matchAll (request = undefined, options = {}) {
webidl.brandCheck(this, Cache)
const prefix = 'Cache.matchAll'
if (request !== undefined) request = webidl.converters.RequestInfo(request)
options = webidl.converters.CacheQueryOptions(options, prefix, 'options')
return this.#internalMatchAll(request, options)
}
async add (request) {
webidl.brandCheck(this, Cache)
const prefix = 'Cache.add'
webidl.argumentLengthCheck(arguments, 1, prefix)
request = webidl.converters.RequestInfo(request)
// 1.
const requests = [request]
// 2.
const responseArrayPromise = this.addAll(requests)
// 3.
return await responseArrayPromise
}
async addAll (requests) {
webidl.brandCheck(this, Cache)
const prefix = 'Cache.addAll'
webidl.argumentLengthCheck(arguments, 1, prefix)
// 1.
const responsePromises = []
// 2.
const requestList = []
// 3.
for (let request of requests) {
if (request === undefined) {
throw webidl.errors.conversionFailed({
prefix,
argument: 'Argument 1',
types: ['undefined is not allowed']
})
}
request = webidl.converters.RequestInfo(request)
if (typeof request === 'string') {
continue
}
// 3.1
const r = getRequestState(request)
// 3.2
if (!urlIsHttpHttpsScheme(r.url) || r.method !== 'GET') {
throw webidl.errors.exception({
header: prefix,
message: 'Expected http/s scheme when method is not GET.'
})
}
}
// 4.
/** @type {ReturnType[]} */
const fetchControllers = []
// 5.
for (const request of requests) {
// 5.1
const r = getRequestState(new Request(request))
// 5.2
if (!urlIsHttpHttpsScheme(r.url)) {
throw webidl.errors.exception({
header: prefix,
message: 'Expected http/s scheme.'
})
}
// 5.4
r.initiator = 'fetch'
r.destination = 'subresource'
// 5.5
requestList.push(r)
// 5.6
const responsePromise = createDeferredPromise()
// 5.7
fetchControllers.push(fetching({
request: r,
processResponse (response) {
// 1.
if (response.type === 'error' || response.status === 206 || response.status < 200 || response.status > 299) {
responsePromise.reject(webidl.errors.exception({
header: 'Cache.addAll',
message: 'Received an invalid status code or the request failed.'
}))
} else if (response.headersList.contains('vary')) { // 2.
// 2.1
const fieldValues = getFieldValues(response.headersList.get('vary'))
// 2.2
for (const fieldValue of fieldValues) {
// 2.2.1
if (fieldValue === '*') {
responsePromise.reject(webidl.errors.exception({
header: 'Cache.addAll',
message: 'invalid vary field value'
}))
for (const controller of fetchControllers) {
controller.abort()
}
return
}
}
}
},
processResponseEndOfBody (response) {
// 1.
if (response.aborted) {
responsePromise.reject(new DOMException('aborted', 'AbortError'))
return
}
// 2.
responsePromise.resolve(response)
}
}))
// 5.8
responsePromises.push(responsePromise.promise)
}
// 6.
const p = Promise.all(responsePromises)
// 7.
const responses = await p
// 7.1
const operations = []
// 7.2
let index = 0
// 7.3
for (const response of responses) {
// 7.3.1
/** @type {CacheBatchOperation} */
const operation = {
type: 'put', // 7.3.2
request: requestList[index], // 7.3.3
response // 7.3.4
}
operations.push(operation) // 7.3.5
index++ // 7.3.6
}
// 7.5
const cacheJobPromise = createDeferredPromise()
// 7.6.1
let errorData = null
// 7.6.2
try {
this.#batchCacheOperations(operations)
} catch (e) {
errorData = e
}
// 7.6.3
queueMicrotask(() => {
// 7.6.3.1
if (errorData === null) {
cacheJobPromise.resolve(undefined)
} else {
// 7.6.3.2
cacheJobPromise.reject(errorData)
}
})
// 7.7
return cacheJobPromise.promise
}
async put (request, response) {
webidl.brandCheck(this, Cache)
const prefix = 'Cache.put'
webidl.argumentLengthCheck(arguments, 2, prefix)
request = webidl.converters.RequestInfo(request)
response = webidl.converters.Response(response, prefix, 'response')
// 1.
let innerRequest = null
// 2.
if (webidl.is.Request(request)) {
innerRequest = getRequestState(request)
} else { // 3.
innerRequest = getRequestState(new Request(request))
}
// 4.
if (!urlIsHttpHttpsScheme(innerRequest.url) || innerRequest.method !== 'GET') {
throw webidl.errors.exception({
header: prefix,
message: 'Expected an http/s scheme when method is not GET'
})
}
// 5.
const innerResponse = getResponseState(response)
// 6.
if (innerResponse.status === 206) {
throw webidl.errors.exception({
header: prefix,
message: 'Got 206 status'
})
}
// 7.
if (innerResponse.headersList.contains('vary')) {
// 7.1.
const fieldValues = getFieldValues(innerResponse.headersList.get('vary'))
// 7.2.
for (const fieldValue of fieldValues) {
// 7.2.1
if (fieldValue === '*') {
throw webidl.errors.exception({
header: prefix,
message: 'Got * vary field value'
})
}
}
}
// 8.
if (innerResponse.body && (isDisturbed(innerResponse.body.stream) || innerResponse.body.stream.locked)) {
throw webidl.errors.exception({
header: prefix,
message: 'Response body is locked or disturbed'
})
}
// 9.
const clonedResponse = cloneResponse(innerResponse)
// 10.
const bodyReadPromise = createDeferredPromise()
// 11.
if (innerResponse.body != null) {
// 11.1
const stream = innerResponse.body.stream
// 11.2
const reader = stream.getReader()
// 11.3
readAllBytes(reader, bodyReadPromise.resolve, bodyReadPromise.reject)
} else {
bodyReadPromise.resolve(undefined)
}
// 12.
/** @type {CacheBatchOperation[]} */
const operations = []
// 13.
/** @type {CacheBatchOperation} */
const operation = {
type: 'put', // 14.
request: innerRequest, // 15.
response: clonedResponse // 16.
}
// 17.
operations.push(operation)
// 19.
const bytes = await bodyReadPromise.promise
if (clonedResponse.body != null) {
clonedResponse.body.source = bytes
}
// 19.1
const cacheJobPromise = createDeferredPromise()
// 19.2.1
let errorData = null
// 19.2.2
try {
this.#batchCacheOperations(operations)
} catch (e) {
errorData = e
}
// 19.2.3
queueMicrotask(() => {
// 19.2.3.1
if (errorData === null) {
cacheJobPromise.resolve()
} else { // 19.2.3.2
cacheJobPromise.reject(errorData)
}
})
return cacheJobPromise.promise
}
async delete (request, options = {}) {
webidl.brandCheck(this, Cache)
const prefix = 'Cache.delete'
webidl.argumentLengthCheck(arguments, 1, prefix)
request = webidl.converters.RequestInfo(request)
options = webidl.converters.CacheQueryOptions(options, prefix, 'options')
/**
* @type {Request}
*/
let r = null
if (webidl.is.Request(request)) {
r = getRequestState(request)
if (r.method !== 'GET' && !options.ignoreMethod) {
return false
}
} else {
assert(typeof request === 'string')
r = getRequestState(new Request(request))
}
/** @type {CacheBatchOperation[]} */
const operations = []
/** @type {CacheBatchOperation} */
const operation = {
type: 'delete',
request: r,
options
}
operations.push(operation)
const cacheJobPromise = createDeferredPromise()
let errorData = null
let requestResponses
try {
requestResponses = this.#batchCacheOperations(operations)
} catch (e) {
errorData = e
}
queueMicrotask(() => {
if (errorData === null) {
cacheJobPromise.resolve(!!requestResponses?.length)
} else {
cacheJobPromise.reject(errorData)
}
})
return cacheJobPromise.promise
}
/**
* @see https://w3c.github.io/ServiceWorker/#dom-cache-keys
* @param {any} request
* @param {import('../../../types/cache').CacheQueryOptions} options
* @returns {Promise}
*/
async keys (request = undefined, options = {}) {
webidl.brandCheck(this, Cache)
const prefix = 'Cache.keys'
if (request !== undefined) request = webidl.converters.RequestInfo(request)
options = webidl.converters.CacheQueryOptions(options, prefix, 'options')
// 1.
let r = null
// 2.
if (request !== undefined) {
// 2.1
if (webidl.is.Request(request)) {
// 2.1.1
r = getRequestState(request)
// 2.1.2
if (r.method !== 'GET' && !options.ignoreMethod) {
return []
}
} else if (typeof request === 'string') { // 2.2
r = getRequestState(new Request(request))
}
}
// 4.
const promise = createDeferredPromise()
// 5.
// 5.1
const requests = []
// 5.2
if (request === undefined) {
// 5.2.1
for (const requestResponse of this.#relevantRequestResponseList) {
// 5.2.1.1
requests.push(requestResponse[0])
}
} else { // 5.3
// 5.3.1
const requestResponses = this.#queryCache(r, options)
// 5.3.2
for (const requestResponse of requestResponses) {
// 5.3.2.1
requests.push(requestResponse[0])
}
}
// 5.4
queueMicrotask(() => {
// 5.4.1
const requestList = []
// 5.4.2
for (const request of requests) {
const requestObject = fromInnerRequest(
request,
undefined,
new AbortController().signal,
'immutable'
)
// 5.4.2.1
requestList.push(requestObject)
}
// 5.4.3
promise.resolve(Object.freeze(requestList))
})
return promise.promise
}
/**
* @see https://w3c.github.io/ServiceWorker/#batch-cache-operations-algorithm
* @param {CacheBatchOperation[]} operations
* @returns {requestResponseList}
*/
#batchCacheOperations (operations) {
// 1.
const cache = this.#relevantRequestResponseList
// 2.
const backupCache = [...cache]
// 3.
const addedItems = []
// 4.1
const resultList = []
try {
// 4.2
for (const operation of operations) {
// 4.2.1
if (operation.type !== 'delete' && operation.type !== 'put') {
throw webidl.errors.exception({
header: 'Cache.#batchCacheOperations',
message: 'operation type does not match "delete" or "put"'
})
}
// 4.2.2
if (operation.type === 'delete' && operation.response != null) {
throw webidl.errors.exception({
header: 'Cache.#batchCacheOperations',
message: 'delete operation should not have an associated response'
})
}
// 4.2.3
if (this.#queryCache(operation.request, operation.options, addedItems).length) {
throw new DOMException('???', 'InvalidStateError')
}
// 4.2.4
let requestResponses
// 4.2.5
if (operation.type === 'delete') {
// 4.2.5.1
requestResponses = this.#queryCache(operation.request, operation.options)
// TODO: the spec is wrong, this is needed to pass WPTs
if (requestResponses.length === 0) {
return []
}
// 4.2.5.2
for (const requestResponse of requestResponses) {
const idx = cache.indexOf(requestResponse)
assert(idx !== -1)
// 4.2.5.2.1
cache.splice(idx, 1)
}
} else if (operation.type === 'put') { // 4.2.6
// 4.2.6.1
if (operation.response == null) {
throw webidl.errors.exception({
header: 'Cache.#batchCacheOperations',
message: 'put operation should have an associated response'
})
}
// 4.2.6.2
const r = operation.request
// 4.2.6.3
if (!urlIsHttpHttpsScheme(r.url)) {
throw webidl.errors.exception({
header: 'Cache.#batchCacheOperations',
message: 'expected http or https scheme'
})
}
// 4.2.6.4
if (r.method !== 'GET') {
throw webidl.errors.exception({
header: 'Cache.#batchCacheOperations',
message: 'not get method'
})
}
// 4.2.6.5
if (operation.options != null) {
throw webidl.errors.exception({
header: 'Cache.#batchCacheOperations',
message: 'options must not be defined'
})
}
// 4.2.6.6
requestResponses = this.#queryCache(operation.request)
// 4.2.6.7
for (const requestResponse of requestResponses) {
const idx = cache.indexOf(requestResponse)
assert(idx !== -1)
// 4.2.6.7.1
cache.splice(idx, 1)
}
// 4.2.6.8
cache.push([operation.request, operation.response])
// 4.2.6.10
addedItems.push([operation.request, operation.response])
}
// 4.2.7
resultList.push([operation.request, operation.response])
}
// 4.3
return resultList
} catch (e) { // 5.
// 5.1
this.#relevantRequestResponseList.length = 0
// 5.2
this.#relevantRequestResponseList = backupCache
// 5.3
throw e
}
}
/**
* @see https://w3c.github.io/ServiceWorker/#query-cache
* @param {any} requestQuery
* @param {import('../../../types/cache').CacheQueryOptions} options
* @param {requestResponseList} targetStorage
* @returns {requestResponseList}
*/
#queryCache (requestQuery, options, targetStorage) {
/** @type {requestResponseList} */
const resultList = []
const storage = targetStorage ?? this.#relevantRequestResponseList
for (const requestResponse of storage) {
const [cachedRequest, cachedResponse] = requestResponse
if (this.#requestMatchesCachedItem(requestQuery, cachedRequest, cachedResponse, options)) {
resultList.push(requestResponse)
}
}
return resultList
}
/**
* @see https://w3c.github.io/ServiceWorker/#request-matches-cached-item-algorithm
* @param {any} requestQuery
* @param {any} request
* @param {any | null} response
* @param {import('../../../types/cache').CacheQueryOptions | undefined} options
* @returns {boolean}
*/
#requestMatchesCachedItem (requestQuery, request, response = null, options) {
// if (options?.ignoreMethod === false && request.method === 'GET') {
// return false
// }
const queryURL = new URL(requestQuery.url)
const cachedURL = new URL(request.url)
if (options?.ignoreSearch) {
cachedURL.search = ''
queryURL.search = ''
}
if (!urlEquals(queryURL, cachedURL, true)) {
return false
}
if (
response == null ||
options?.ignoreVary ||
!response.headersList.contains('vary')
) {
return true
}
const fieldValues = getFieldValues(response.headersList.get('vary'))
for (const fieldValue of fieldValues) {
if (fieldValue === '*') {
return false
}
const requestValue = request.headersList.get(fieldValue)
const queryValue = requestQuery.headersList.get(fieldValue)
// If one has the header and the other doesn't, or one has
// a different value than the other, return false
if (requestValue !== queryValue) {
return false
}
}
return true
}
#internalMatchAll (request, options, maxResponses = Infinity) {
// 1.
let r = null
// 2.
if (request !== undefined) {
if (webidl.is.Request(request)) {
// 2.1.1
r = getRequestState(request)
// 2.1.2
if (r.method !== 'GET' && !options.ignoreMethod) {
return []
}
} else if (typeof request === 'string') {
// 2.2.1
r = getRequestState(new Request(request))
}
}
// 5.
// 5.1
const responses = []
// 5.2
if (request === undefined) {
// 5.2.1
for (const requestResponse of this.#relevantRequestResponseList) {
responses.push(requestResponse[1])
}
} else { // 5.3
// 5.3.1
const requestResponses = this.#queryCache(r, options)
// 5.3.2
for (const requestResponse of requestResponses) {
responses.push(requestResponse[1])
}
}
// 5.4
// We don't implement CORs so we don't need to loop over the responses, yay!
// 5.5.1
const responseList = []
// 5.5.2
for (const response of responses) {
// 5.5.2.1
const responseObject = fromInnerResponse(cloneResponse(response), 'immutable')
responseList.push(responseObject)
if (responseList.length >= maxResponses) {
break
}
}
// 6.
return Object.freeze(responseList)
}
}
Object.defineProperties(Cache.prototype, {
[Symbol.toStringTag]: {
value: 'Cache',
configurable: true
},
match: kEnumerableProperty,
matchAll: kEnumerableProperty,
add: kEnumerableProperty,
addAll: kEnumerableProperty,
put: kEnumerableProperty,
delete: kEnumerableProperty,
keys: kEnumerableProperty
})
const cacheQueryOptionConverters = [
{
key: 'ignoreSearch',
converter: webidl.converters.boolean,
defaultValue: () => false
},
{
key: 'ignoreMethod',
converter: webidl.converters.boolean,
defaultValue: () => false
},
{
key: 'ignoreVary',
converter: webidl.converters.boolean,
defaultValue: () => false
}
]
webidl.converters.CacheQueryOptions = webidl.dictionaryConverter(cacheQueryOptionConverters)
webidl.converters.MultiCacheQueryOptions = webidl.dictionaryConverter([
...cacheQueryOptionConverters,
{
key: 'cacheName',
converter: webidl.converters.DOMString
}
])
webidl.converters.Response = webidl.interfaceConverter(
webidl.is.Response,
'Response'
)
webidl.converters['sequence'] = webidl.sequenceConverter(
webidl.converters.RequestInfo
)
module.exports = {
Cache
}
================================================
FILE: lib/web/cache/cachestorage.js
================================================
'use strict'
const { Cache } = require('./cache')
const { webidl } = require('../webidl')
const { kEnumerableProperty } = require('../../core/util')
const { kConstruct } = require('../../core/symbols')
class CacheStorage {
/**
* @see https://w3c.github.io/ServiceWorker/#dfn-relevant-name-to-cache-map
* @type {Map}
*/
async has (cacheName) {
webidl.brandCheck(this, CacheStorage)
const prefix = 'CacheStorage.has'
webidl.argumentLengthCheck(arguments, 1, prefix)
cacheName = webidl.converters.DOMString(cacheName, prefix, 'cacheName')
// 2.1.1
// 2.2
return this.#caches.has(cacheName)
}
/**
* @see https://w3c.github.io/ServiceWorker/#dom-cachestorage-open
* @param {string} cacheName
* @returns {Promise}
*/
async open (cacheName) {
webidl.brandCheck(this, CacheStorage)
const prefix = 'CacheStorage.open'
webidl.argumentLengthCheck(arguments, 1, prefix)
cacheName = webidl.converters.DOMString(cacheName, prefix, 'cacheName')
// 2.1
if (this.#caches.has(cacheName)) {
// await caches.open('v1') !== await caches.open('v1')
// 2.1.1
const cache = this.#caches.get(cacheName)
// 2.1.1.1
return new Cache(kConstruct, cache)
}
// 2.2
const cache = []
// 2.3
this.#caches.set(cacheName, cache)
// 2.4
return new Cache(kConstruct, cache)
}
/**
* @see https://w3c.github.io/ServiceWorker/#cache-storage-delete
* @param {string} cacheName
* @returns {Promise}
*/
async delete (cacheName) {
webidl.brandCheck(this, CacheStorage)
const prefix = 'CacheStorage.delete'
webidl.argumentLengthCheck(arguments, 1, prefix)
cacheName = webidl.converters.DOMString(cacheName, prefix, 'cacheName')
return this.#caches.delete(cacheName)
}
/**
* @see https://w3c.github.io/ServiceWorker/#cache-storage-keys
* @returns {Promise}
*/
async keys () {
webidl.brandCheck(this, CacheStorage)
// 2.1
const keys = this.#caches.keys()
// 2.2
return [...keys]
}
}
Object.defineProperties(CacheStorage.prototype, {
[Symbol.toStringTag]: {
value: 'CacheStorage',
configurable: true
},
match: kEnumerableProperty,
has: kEnumerableProperty,
open: kEnumerableProperty,
delete: kEnumerableProperty,
keys: kEnumerableProperty
})
module.exports = {
CacheStorage
}
================================================
FILE: lib/web/cache/util.js
================================================
'use strict'
const assert = require('node:assert')
const { URLSerializer } = require('../fetch/data-url')
const { isValidHeaderName } = require('../fetch/util')
/**
* @see https://url.spec.whatwg.org/#concept-url-equals
* @param {URL} A
* @param {URL} B
* @param {boolean | undefined} excludeFragment
* @returns {boolean}
*/
function urlEquals (A, B, excludeFragment = false) {
const serializedA = URLSerializer(A, excludeFragment)
const serializedB = URLSerializer(B, excludeFragment)
return serializedA === serializedB
}
/**
* @see https://github.com/chromium/chromium/blob/694d20d134cb553d8d89e5500b9148012b1ba299/content/browser/cache_storage/cache_storage_cache.cc#L260-L262
* @param {string} header
*/
function getFieldValues (header) {
assert(header !== null)
const values = []
for (let value of header.split(',')) {
value = value.trim()
if (isValidHeaderName(value)) {
values.push(value)
}
}
return values
}
module.exports = {
urlEquals,
getFieldValues
}
================================================
FILE: lib/web/cookies/constants.js
================================================
'use strict'
// https://wicg.github.io/cookie-store/#cookie-maximum-attribute-value-size
const maxAttributeValueSize = 1024
// https://wicg.github.io/cookie-store/#cookie-maximum-name-value-pair-size
const maxNameValuePairSize = 4096
module.exports = {
maxAttributeValueSize,
maxNameValuePairSize
}
================================================
FILE: lib/web/cookies/index.js
================================================
'use strict'
const { parseSetCookie } = require('./parse')
const { stringify } = require('./util')
const { webidl } = require('../webidl')
const { Headers } = require('../fetch/headers')
const brandChecks = webidl.brandCheckMultiple([Headers, globalThis.Headers].filter(Boolean))
/**
* @typedef {Object} Cookie
* @property {string} name
* @property {string} value
* @property {Date|number} [expires]
* @property {number} [maxAge]
* @property {string} [domain]
* @property {string} [path]
* @property {boolean} [secure]
* @property {boolean} [httpOnly]
* @property {'Strict'|'Lax'|'None'} [sameSite]
* @property {string[]} [unparsed]
*/
/**
* @param {Headers} headers
* @returns {Record}
*/
function getCookies (headers) {
webidl.argumentLengthCheck(arguments, 1, 'getCookies')
brandChecks(headers)
const cookie = headers.get('cookie')
/** @type {Record} */
const out = {}
if (!cookie) {
return out
}
for (const piece of cookie.split(';')) {
const [name, ...value] = piece.split('=')
out[name.trim()] = value.join('=')
}
return out
}
/**
* @param {Headers} headers
* @param {string} name
* @param {{ path?: string, domain?: string }|undefined} attributes
* @returns {void}
*/
function deleteCookie (headers, name, attributes) {
brandChecks(headers)
const prefix = 'deleteCookie'
webidl.argumentLengthCheck(arguments, 2, prefix)
name = webidl.converters.DOMString(name, prefix, 'name')
attributes = webidl.converters.DeleteCookieAttributes(attributes)
// Matches behavior of
// https://github.com/denoland/deno_std/blob/63827b16330b82489a04614027c33b7904e08be5/http/cookie.ts#L278
setCookie(headers, {
name,
value: '',
expires: new Date(0),
...attributes
})
}
/**
* @param {Headers} headers
* @returns {Cookie[]}
*/
function getSetCookies (headers) {
webidl.argumentLengthCheck(arguments, 1, 'getSetCookies')
brandChecks(headers)
const cookies = headers.getSetCookie()
if (!cookies) {
return []
}
return cookies.map((pair) => parseSetCookie(pair))
}
/**
* Parses a cookie string
* @param {string} cookie
*/
function parseCookie (cookie) {
cookie = webidl.converters.DOMString(cookie)
return parseSetCookie(cookie)
}
/**
* @param {Headers} headers
* @param {Cookie} cookie
* @returns {void}
*/
function setCookie (headers, cookie) {
webidl.argumentLengthCheck(arguments, 2, 'setCookie')
brandChecks(headers)
cookie = webidl.converters.Cookie(cookie)
const str = stringify(cookie)
if (str) {
headers.append('set-cookie', str, true)
}
}
webidl.converters.DeleteCookieAttributes = webidl.dictionaryConverter([
{
converter: webidl.nullableConverter(webidl.converters.DOMString),
key: 'path',
defaultValue: () => null
},
{
converter: webidl.nullableConverter(webidl.converters.DOMString),
key: 'domain',
defaultValue: () => null
}
])
webidl.converters.Cookie = webidl.dictionaryConverter([
{
converter: webidl.converters.DOMString,
key: 'name'
},
{
converter: webidl.converters.DOMString,
key: 'value'
},
{
converter: webidl.nullableConverter((value) => {
if (typeof value === 'number') {
return webidl.converters['unsigned long long'](value)
}
return new Date(value)
}),
key: 'expires',
defaultValue: () => null
},
{
converter: webidl.nullableConverter(webidl.converters['long long']),
key: 'maxAge',
defaultValue: () => null
},
{
converter: webidl.nullableConverter(webidl.converters.DOMString),
key: 'domain',
defaultValue: () => null
},
{
converter: webidl.nullableConverter(webidl.converters.DOMString),
key: 'path',
defaultValue: () => null
},
{
converter: webidl.nullableConverter(webidl.converters.boolean),
key: 'secure',
defaultValue: () => null
},
{
converter: webidl.nullableConverter(webidl.converters.boolean),
key: 'httpOnly',
defaultValue: () => null
},
{
converter: webidl.converters.USVString,
key: 'sameSite',
allowedValues: ['Strict', 'Lax', 'None']
},
{
converter: webidl.sequenceConverter(webidl.converters.DOMString),
key: 'unparsed',
defaultValue: () => []
}
])
module.exports = {
getCookies,
deleteCookie,
getSetCookies,
setCookie,
parseCookie
}
================================================
FILE: lib/web/cookies/parse.js
================================================
'use strict'
const { collectASequenceOfCodePointsFast } = require('../infra')
const { maxNameValuePairSize, maxAttributeValueSize } = require('./constants')
const { isCTLExcludingHtab } = require('./util')
const assert = require('node:assert')
const { unescape: qsUnescape } = require('node:querystring')
/**
* @description Parses the field-value attributes of a set-cookie header string.
* @see https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis#section-5.4
* @param {string} header
* @returns {import('./index').Cookie|null} if the header is invalid, null will be returned
*/
function parseSetCookie (header) {
// 1. If the set-cookie-string contains a %x00-08 / %x0A-1F / %x7F
// character (CTL characters excluding HTAB): Abort these steps and
// ignore the set-cookie-string entirely.
if (isCTLExcludingHtab(header)) {
return null
}
let nameValuePair = ''
let unparsedAttributes = ''
let name = ''
let value = ''
// 2. If the set-cookie-string contains a %x3B (";") character:
if (header.includes(';')) {
// 1. The name-value-pair string consists of the characters up to,
// but not including, the first %x3B (";"), and the unparsed-
// attributes consist of the remainder of the set-cookie-string
// (including the %x3B (";") in question).
const position = { position: 0 }
nameValuePair = collectASequenceOfCodePointsFast(';', header, position)
unparsedAttributes = header.slice(position.position)
} else {
// Otherwise:
// 1. The name-value-pair string consists of all the characters
// contained in the set-cookie-string, and the unparsed-
// attributes is the empty string.
nameValuePair = header
}
// 3. If the name-value-pair string lacks a %x3D ("=") character, then
// the name string is empty, and the value string is the value of
// name-value-pair.
if (!nameValuePair.includes('=')) {
value = nameValuePair
} else {
// Otherwise, the name string consists of the characters up to, but
// not including, the first %x3D ("=") character, and the (possibly
// empty) value string consists of the characters after the first
// %x3D ("=") character.
const position = { position: 0 }
name = collectASequenceOfCodePointsFast(
'=',
nameValuePair,
position
)
value = nameValuePair.slice(position.position + 1)
}
// 4. Remove any leading or trailing WSP characters from the name
// string and the value string.
name = name.trim()
value = value.trim()
// 5. If the sum of the lengths of the name string and the value string
// is more than 4096 octets, abort these steps and ignore the set-
// cookie-string entirely.
if (name.length + value.length > maxNameValuePairSize) {
return null
}
// 6. The cookie-name is the name string, and the cookie-value is the
// value string.
// https://datatracker.ietf.org/doc/html/rfc6265
// To maximize compatibility with user agents, servers that wish to
// store arbitrary data in a cookie-value SHOULD encode that data, for
// example, using Base64 [RFC4648].
return {
name, value: qsUnescape(value), ...parseUnparsedAttributes(unparsedAttributes)
}
}
/**
* Parses the remaining attributes of a set-cookie header
* @see https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis#section-5.4
* @param {string} unparsedAttributes
* @param {Object.} [cookieAttributeList={}]
*/
function parseUnparsedAttributes (unparsedAttributes, cookieAttributeList = {}) {
// 1. If the unparsed-attributes string is empty, skip the rest of
// these steps.
if (unparsedAttributes.length === 0) {
return cookieAttributeList
}
// 2. Discard the first character of the unparsed-attributes (which
// will be a %x3B (";") character).
assert(unparsedAttributes[0] === ';')
unparsedAttributes = unparsedAttributes.slice(1)
let cookieAv = ''
// 3. If the remaining unparsed-attributes contains a %x3B (";")
// character:
if (unparsedAttributes.includes(';')) {
// 1. Consume the characters of the unparsed-attributes up to, but
// not including, the first %x3B (";") character.
cookieAv = collectASequenceOfCodePointsFast(
';',
unparsedAttributes,
{ position: 0 }
)
unparsedAttributes = unparsedAttributes.slice(cookieAv.length)
} else {
// Otherwise:
// 1. Consume the remainder of the unparsed-attributes.
cookieAv = unparsedAttributes
unparsedAttributes = ''
}
// Let the cookie-av string be the characters consumed in this step.
let attributeName = ''
let attributeValue = ''
// 4. If the cookie-av string contains a %x3D ("=") character:
if (cookieAv.includes('=')) {
// 1. The (possibly empty) attribute-name string consists of the
// characters up to, but not including, the first %x3D ("=")
// character, and the (possibly empty) attribute-value string
// consists of the characters after the first %x3D ("=")
// character.
const position = { position: 0 }
attributeName = collectASequenceOfCodePointsFast(
'=',
cookieAv,
position
)
attributeValue = cookieAv.slice(position.position + 1)
} else {
// Otherwise:
// 1. The attribute-name string consists of the entire cookie-av
// string, and the attribute-value string is empty.
attributeName = cookieAv
}
// 5. Remove any leading or trailing WSP characters from the attribute-
// name string and the attribute-value string.
attributeName = attributeName.trim()
attributeValue = attributeValue.trim()
// 6. If the attribute-value is longer than 1024 octets, ignore the
// cookie-av string and return to Step 1 of this algorithm.
if (attributeValue.length > maxAttributeValueSize) {
return parseUnparsedAttributes(unparsedAttributes, cookieAttributeList)
}
// 7. Process the attribute-name and attribute-value according to the
// requirements in the following subsections. (Notice that
// attributes with unrecognized attribute-names are ignored.)
const attributeNameLowercase = attributeName.toLowerCase()
// https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis#section-5.4.1
// If the attribute-name case-insensitively matches the string
// "Expires", the user agent MUST process the cookie-av as follows.
if (attributeNameLowercase === 'expires') {
// 1. Let the expiry-time be the result of parsing the attribute-value
// as cookie-date (see Section 5.1.1).
const expiryTime = new Date(attributeValue)
// 2. If the attribute-value failed to parse as a cookie date, ignore
// the cookie-av.
cookieAttributeList.expires = expiryTime
} else if (attributeNameLowercase === 'max-age') {
// https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis#section-5.4.2
// If the attribute-name case-insensitively matches the string "Max-
// Age", the user agent MUST process the cookie-av as follows.
// 1. If the first character of the attribute-value is not a DIGIT or a
// "-" character, ignore the cookie-av.
const charCode = attributeValue.charCodeAt(0)
if ((charCode < 48 || charCode > 57) && attributeValue[0] !== '-') {
return parseUnparsedAttributes(unparsedAttributes, cookieAttributeList)
}
// 2. If the remainder of attribute-value contains a non-DIGIT
// character, ignore the cookie-av.
if (!/^\d+$/.test(attributeValue)) {
return parseUnparsedAttributes(unparsedAttributes, cookieAttributeList)
}
// 3. Let delta-seconds be the attribute-value converted to an integer.
const deltaSeconds = Number(attributeValue)
// 4. Let cookie-age-limit be the maximum age of the cookie (which
// SHOULD be 400 days or less, see Section 4.1.2.2).
// 5. Set delta-seconds to the smaller of its present value and cookie-
// age-limit.
// deltaSeconds = Math.min(deltaSeconds * 1000, maxExpiresMs)
// 6. If delta-seconds is less than or equal to zero (0), let expiry-
// time be the earliest representable date and time. Otherwise, let
// the expiry-time be the current date and time plus delta-seconds
// seconds.
// const expiryTime = deltaSeconds <= 0 ? Date.now() : Date.now() + deltaSeconds
// 7. Append an attribute to the cookie-attribute-list with an
// attribute-name of Max-Age and an attribute-value of expiry-time.
cookieAttributeList.maxAge = deltaSeconds
} else if (attributeNameLowercase === 'domain') {
// https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis#section-5.4.3
// If the attribute-name case-insensitively matches the string "Domain",
// the user agent MUST process the cookie-av as follows.
// 1. Let cookie-domain be the attribute-value.
let cookieDomain = attributeValue
// 2. If cookie-domain starts with %x2E ("."), let cookie-domain be
// cookie-domain without its leading %x2E (".").
if (cookieDomain[0] === '.') {
cookieDomain = cookieDomain.slice(1)
}
// 3. Convert the cookie-domain to lower case.
cookieDomain = cookieDomain.toLowerCase()
// 4. Append an attribute to the cookie-attribute-list with an
// attribute-name of Domain and an attribute-value of cookie-domain.
cookieAttributeList.domain = cookieDomain
} else if (attributeNameLowercase === 'path') {
// https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis#section-5.4.4
// If the attribute-name case-insensitively matches the string "Path",
// the user agent MUST process the cookie-av as follows.
// 1. If the attribute-value is empty or if the first character of the
// attribute-value is not %x2F ("/"):
let cookiePath = ''
if (attributeValue.length === 0 || attributeValue[0] !== '/') {
// 1. Let cookie-path be the default-path.
cookiePath = '/'
} else {
// Otherwise:
// 1. Let cookie-path be the attribute-value.
cookiePath = attributeValue
}
// 2. Append an attribute to the cookie-attribute-list with an
// attribute-name of Path and an attribute-value of cookie-path.
cookieAttributeList.path = cookiePath
} else if (attributeNameLowercase === 'secure') {
// https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis#section-5.4.5
// If the attribute-name case-insensitively matches the string "Secure",
// the user agent MUST append an attribute to the cookie-attribute-list
// with an attribute-name of Secure and an empty attribute-value.
cookieAttributeList.secure = true
} else if (attributeNameLowercase === 'httponly') {
// https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis#section-5.4.6
// If the attribute-name case-insensitively matches the string
// "HttpOnly", the user agent MUST append an attribute to the cookie-
// attribute-list with an attribute-name of HttpOnly and an empty
// attribute-value.
cookieAttributeList.httpOnly = true
} else if (attributeNameLowercase === 'samesite') {
// https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis#section-5.4.7
// If the attribute-name case-insensitively matches the string
// "SameSite", the user agent MUST process the cookie-av as follows:
// 1. Let enforcement be "Default".
let enforcement = 'Default'
const attributeValueLowercase = attributeValue.toLowerCase()
// 2. If cookie-av's attribute-value is a case-insensitive match for
// "None", set enforcement to "None".
if (attributeValueLowercase.includes('none')) {
enforcement = 'None'
}
// 3. If cookie-av's attribute-value is a case-insensitive match for
// "Strict", set enforcement to "Strict".
if (attributeValueLowercase.includes('strict')) {
enforcement = 'Strict'
}
// 4. If cookie-av's attribute-value is a case-insensitive match for
// "Lax", set enforcement to "Lax".
if (attributeValueLowercase.includes('lax')) {
enforcement = 'Lax'
}
// 5. Append an attribute to the cookie-attribute-list with an
// attribute-name of "SameSite" and an attribute-value of
// enforcement.
cookieAttributeList.sameSite = enforcement
} else {
cookieAttributeList.unparsed ??= []
cookieAttributeList.unparsed.push(`${attributeName}=${attributeValue}`)
}
// 8. Return to Step 1 of this algorithm.
return parseUnparsedAttributes(unparsedAttributes, cookieAttributeList)
}
module.exports = {
parseSetCookie,
parseUnparsedAttributes
}
================================================
FILE: lib/web/cookies/util.js
================================================
'use strict'
/**
* @param {string} value
* @returns {boolean}
*/
function isCTLExcludingHtab (value) {
for (let i = 0; i < value.length; ++i) {
const code = value.charCodeAt(i)
if (
(code >= 0x00 && code <= 0x08) ||
(code >= 0x0A && code <= 0x1F) ||
code === 0x7F
) {
return true
}
}
return false
}
/**
CHAR =
token = 1*
separators = "(" | ")" | "<" | ">" | "@"
| "," | ";" | ":" | "\" | <">
| "/" | "[" | "]" | "?" | "="
| "{" | "}" | SP | HT
* @param {string} name
*/
function validateCookieName (name) {
for (let i = 0; i < name.length; ++i) {
const code = name.charCodeAt(i)
if (
code < 0x21 || // exclude CTLs (0-31), SP and HT
code > 0x7E || // exclude non-ascii and DEL
code === 0x22 || // "
code === 0x28 || // (
code === 0x29 || // )
code === 0x3C || // <
code === 0x3E || // >
code === 0x40 || // @
code === 0x2C || // ,
code === 0x3B || // ;
code === 0x3A || // :
code === 0x5C || // \
code === 0x2F || // /
code === 0x5B || // [
code === 0x5D || // ]
code === 0x3F || // ?
code === 0x3D || // =
code === 0x7B || // {
code === 0x7D // }
) {
throw new Error('Invalid cookie name')
}
}
}
/**
cookie-value = *cookie-octet / ( DQUOTE *cookie-octet DQUOTE )
cookie-octet = %x21 / %x23-2B / %x2D-3A / %x3C-5B / %x5D-7E
; US-ASCII characters excluding CTLs,
; whitespace DQUOTE, comma, semicolon,
; and backslash
* @param {string} value
*/
function validateCookieValue (value) {
let len = value.length
let i = 0
// if the value is wrapped in DQUOTE
if (value[0] === '"') {
if (len === 1 || value[len - 1] !== '"') {
throw new Error('Invalid cookie value')
}
--len
++i
}
while (i < len) {
const code = value.charCodeAt(i++)
if (
code < 0x21 || // exclude CTLs (0-31)
code > 0x7E || // non-ascii and DEL (127)
code === 0x22 || // "
code === 0x2C || // ,
code === 0x3B || // ;
code === 0x5C // \
) {
throw new Error('Invalid cookie value')
}
}
}
/**
* path-value =
* @param {string} path
*/
function validateCookiePath (path) {
for (let i = 0; i < path.length; ++i) {
const code = path.charCodeAt(i)
if (
code < 0x20 || // exclude CTLs (0-31)
code === 0x7F || // DEL
code === 0x3B // ;
) {
throw new Error('Invalid cookie path')
}
}
}
/**
* I have no idea why these values aren't allowed to be honest,
* but Deno tests these. - Khafra
* @param {string} domain
*/
function validateCookieDomain (domain) {
if (
domain.startsWith('-') ||
domain.endsWith('.') ||
domain.endsWith('-')
) {
throw new Error('Invalid cookie domain')
}
}
const IMFDays = [
'Sun', 'Mon', 'Tue', 'Wed',
'Thu', 'Fri', 'Sat'
]
const IMFMonths = [
'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'
]
const IMFPaddedNumbers = Array(61).fill(0).map((_, i) => i.toString().padStart(2, '0'))
/**
* @see https://www.rfc-editor.org/rfc/rfc7231#section-7.1.1.1
* @param {number|Date} date
IMF-fixdate = day-name "," SP date1 SP time-of-day SP GMT
; fixed length/zone/capitalization subset of the format
; see Section 3.3 of [RFC5322]
day-name = %x4D.6F.6E ; "Mon", case-sensitive
/ %x54.75.65 ; "Tue", case-sensitive
/ %x57.65.64 ; "Wed", case-sensitive
/ %x54.68.75 ; "Thu", case-sensitive
/ %x46.72.69 ; "Fri", case-sensitive
/ %x53.61.74 ; "Sat", case-sensitive
/ %x53.75.6E ; "Sun", case-sensitive
date1 = day SP month SP year
; e.g., 02 Jun 1982
day = 2DIGIT
month = %x4A.61.6E ; "Jan", case-sensitive
/ %x46.65.62 ; "Feb", case-sensitive
/ %x4D.61.72 ; "Mar", case-sensitive
/ %x41.70.72 ; "Apr", case-sensitive
/ %x4D.61.79 ; "May", case-sensitive
/ %x4A.75.6E ; "Jun", case-sensitive
/ %x4A.75.6C ; "Jul", case-sensitive
/ %x41.75.67 ; "Aug", case-sensitive
/ %x53.65.70 ; "Sep", case-sensitive
/ %x4F.63.74 ; "Oct", case-sensitive
/ %x4E.6F.76 ; "Nov", case-sensitive
/ %x44.65.63 ; "Dec", case-sensitive
year = 4DIGIT
GMT = %x47.4D.54 ; "GMT", case-sensitive
time-of-day = hour ":" minute ":" second
; 00:00:00 - 23:59:60 (leap second)
hour = 2DIGIT
minute = 2DIGIT
second = 2DIGIT
*/
function toIMFDate (date) {
if (typeof date === 'number') {
date = new Date(date)
}
return `${IMFDays[date.getUTCDay()]}, ${IMFPaddedNumbers[date.getUTCDate()]} ${IMFMonths[date.getUTCMonth()]} ${date.getUTCFullYear()} ${IMFPaddedNumbers[date.getUTCHours()]}:${IMFPaddedNumbers[date.getUTCMinutes()]}:${IMFPaddedNumbers[date.getUTCSeconds()]} GMT`
}
/**
max-age-av = "Max-Age=" non-zero-digit *DIGIT
; In practice, both expires-av and max-age-av
; are limited to dates representable by the
; user agent.
* @param {number} maxAge
*/
function validateCookieMaxAge (maxAge) {
if (maxAge < 0) {
throw new Error('Invalid cookie max-age')
}
}
/**
* @see https://www.rfc-editor.org/rfc/rfc6265#section-4.1.1
* @param {import('./index').Cookie} cookie
*/
function stringify (cookie) {
if (cookie.name.length === 0) {
return null
}
validateCookieName(cookie.name)
validateCookieValue(cookie.value)
const out = [`${cookie.name}=${cookie.value}`]
// https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-cookie-prefixes-00#section-3.1
// https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-cookie-prefixes-00#section-3.2
if (cookie.name.startsWith('__Secure-')) {
cookie.secure = true
}
if (cookie.name.startsWith('__Host-')) {
cookie.secure = true
cookie.domain = null
cookie.path = '/'
}
if (cookie.secure) {
out.push('Secure')
}
if (cookie.httpOnly) {
out.push('HttpOnly')
}
if (typeof cookie.maxAge === 'number') {
validateCookieMaxAge(cookie.maxAge)
out.push(`Max-Age=${cookie.maxAge}`)
}
if (cookie.domain) {
validateCookieDomain(cookie.domain)
out.push(`Domain=${cookie.domain}`)
}
if (cookie.path) {
validateCookiePath(cookie.path)
out.push(`Path=${cookie.path}`)
}
if (cookie.expires && cookie.expires.toString() !== 'Invalid Date') {
out.push(`Expires=${toIMFDate(cookie.expires)}`)
}
if (cookie.sameSite) {
out.push(`SameSite=${cookie.sameSite}`)
}
for (const part of cookie.unparsed) {
if (!part.includes('=')) {
throw new Error('Invalid unparsed')
}
const [key, ...value] = part.split('=')
out.push(`${key.trim()}=${value.join('=')}`)
}
return out.join('; ')
}
module.exports = {
isCTLExcludingHtab,
validateCookieName,
validateCookiePath,
validateCookieValue,
toIMFDate,
stringify
}
================================================
FILE: lib/web/eventsource/eventsource-stream.js
================================================
'use strict'
const { Transform } = require('node:stream')
const { isASCIINumber, isValidLastEventId } = require('./util')
/**
* @type {number[]} BOM
*/
const BOM = [0xEF, 0xBB, 0xBF]
/**
* @type {10} LF
*/
const LF = 0x0A
/**
* @type {13} CR
*/
const CR = 0x0D
/**
* @type {58} COLON
*/
const COLON = 0x3A
/**
* @type {32} SPACE
*/
const SPACE = 0x20
/**
* @typedef {object} EventSourceStreamEvent
* @type {object}
* @property {string} [event] The event type.
* @property {string} [data] The data of the message.
* @property {string} [id] A unique ID for the event.
* @property {string} [retry] The reconnection time, in milliseconds.
*/
/**
* @typedef eventSourceSettings
* @type {object}
* @property {string} [lastEventId] The last event ID received from the server.
* @property {string} [origin] The origin of the event source.
* @property {number} [reconnectionTime] The reconnection time, in milliseconds.
*/
class EventSourceStream extends Transform {
/**
* @type {eventSourceSettings}
*/
state
/**
* Leading byte-order-mark check.
* @type {boolean}
*/
checkBOM = true
/**
* @type {boolean}
*/
crlfCheck = false
/**
* @type {boolean}
*/
eventEndCheck = false
/**
* @type {Buffer|null}
*/
buffer = null
pos = 0
event = {
data: undefined,
event: undefined,
id: undefined,
retry: undefined
}
/**
* @param {object} options
* @param {boolean} [options.readableObjectMode]
* @param {eventSourceSettings} [options.eventSourceSettings]
* @param {(chunk: any, encoding?: BufferEncoding | undefined) => boolean} [options.push]
*/
constructor (options = {}) {
// Enable object mode as EventSourceStream emits objects of shape
// EventSourceStreamEvent
options.readableObjectMode = true
super(options)
this.state = options.eventSourceSettings || {}
if (options.push) {
this.push = options.push
}
}
/**
* @param {Buffer} chunk
* @param {string} _encoding
* @param {Function} callback
* @returns {void}
*/
_transform (chunk, _encoding, callback) {
if (chunk.length === 0) {
callback()
return
}
// Cache the chunk in the buffer, as the data might not be complete while
// processing it
// TODO: Investigate if there is a more performant way to handle
// incoming chunks
// see: https://github.com/nodejs/undici/issues/2630
if (this.buffer) {
this.buffer = Buffer.concat([this.buffer, chunk])
} else {
this.buffer = chunk
}
// Strip leading byte-order-mark if we opened the stream and started
// the processing of the incoming data
if (this.checkBOM) {
switch (this.buffer.length) {
case 1:
// Check if the first byte is the same as the first byte of the BOM
if (this.buffer[0] === BOM[0]) {
// If it is, we need to wait for more data
callback()
return
}
// Set the checkBOM flag to false as we don't need to check for the
// BOM anymore
this.checkBOM = false
// The buffer only contains one byte so we need to wait for more data
callback()
return
case 2:
// Check if the first two bytes are the same as the first two bytes
// of the BOM
if (
this.buffer[0] === BOM[0] &&
this.buffer[1] === BOM[1]
) {
// If it is, we need to wait for more data, because the third byte
// is needed to determine if it is the BOM or not
callback()
return
}
// Set the checkBOM flag to false as we don't need to check for the
// BOM anymore
this.checkBOM = false
break
case 3:
// Check if the first three bytes are the same as the first three
// bytes of the BOM
if (
this.buffer[0] === BOM[0] &&
this.buffer[1] === BOM[1] &&
this.buffer[2] === BOM[2]
) {
// If it is, we can drop the buffered data, as it is only the BOM
this.buffer = Buffer.alloc(0)
// Set the checkBOM flag to false as we don't need to check for the
// BOM anymore
this.checkBOM = false
// Await more data
callback()
return
}
// If it is not the BOM, we can start processing the data
this.checkBOM = false
break
default:
// The buffer is longer than 3 bytes, so we can drop the BOM if it is
// present
if (
this.buffer[0] === BOM[0] &&
this.buffer[1] === BOM[1] &&
this.buffer[2] === BOM[2]
) {
// Remove the BOM from the buffer
this.buffer = this.buffer.subarray(3)
}
// Set the checkBOM flag to false as we don't need to check for the
this.checkBOM = false
break
}
}
while (this.pos < this.buffer.length) {
// If the previous line ended with an end-of-line, we need to check
// if the next character is also an end-of-line.
if (this.eventEndCheck) {
// If the the current character is an end-of-line, then the event
// is finished and we can process it
// If the previous line ended with a carriage return, we need to
// check if the current character is a line feed and remove it
// from the buffer.
if (this.crlfCheck) {
// If the current character is a line feed, we can remove it
// from the buffer and reset the crlfCheck flag
if (this.buffer[this.pos] === LF) {
this.buffer = this.buffer.subarray(this.pos + 1)
this.pos = 0
this.crlfCheck = false
// It is possible that the line feed is not the end of the
// event. We need to check if the next character is an
// end-of-line character to determine if the event is
// finished. We simply continue the loop to check the next
// character.
// As we removed the line feed from the buffer and set the
// crlfCheck flag to false, we basically don't make any
// distinction between a line feed and a carriage return.
continue
}
this.crlfCheck = false
}
if (this.buffer[this.pos] === LF || this.buffer[this.pos] === CR) {
// If the current character is a carriage return, we need to
// set the crlfCheck flag to true, as we need to check if the
// next character is a line feed so we can remove it from the
// buffer
if (this.buffer[this.pos] === CR) {
this.crlfCheck = true
}
this.buffer = this.buffer.subarray(this.pos + 1)
this.pos = 0
if (
this.event.data !== undefined || this.event.event || this.event.id !== undefined || this.event.retry) {
this.processEvent(this.event)
}
this.clearEvent()
continue
}
// If the current character is not an end-of-line, then the event
// is not finished and we have to reset the eventEndCheck flag
this.eventEndCheck = false
continue
}
// If the current character is an end-of-line, we can process the
// line
if (this.buffer[this.pos] === LF || this.buffer[this.pos] === CR) {
// If the current character is a carriage return, we need to
// set the crlfCheck flag to true, as we need to check if the
// next character is a line feed
if (this.buffer[this.pos] === CR) {
this.crlfCheck = true
}
// In any case, we can process the line as we reached an
// end-of-line character
this.parseLine(this.buffer.subarray(0, this.pos), this.event)
// Remove the processed line from the buffer
this.buffer = this.buffer.subarray(this.pos + 1)
// Reset the position as we removed the processed line from the buffer
this.pos = 0
// A line was processed and this could be the end of the event. We need
// to check if the next line is empty to determine if the event is
// finished.
this.eventEndCheck = true
continue
}
this.pos++
}
callback()
}
/**
* @param {Buffer} line
* @param {EventSourceStreamEvent} event
*/
parseLine (line, event) {
// If the line is empty (a blank line)
// Dispatch the event, as defined below.
// This will be handled in the _transform method
if (line.length === 0) {
return
}
// If the line starts with a U+003A COLON character (:)
// Ignore the line.
const colonPosition = line.indexOf(COLON)
if (colonPosition === 0) {
return
}
let field = ''
let value = ''
// If the line contains a U+003A COLON character (:)
if (colonPosition !== -1) {
// Collect the characters on the line before the first U+003A COLON
// character (:), and let field be that string.
// TODO: Investigate if there is a more performant way to extract the
// field
// see: https://github.com/nodejs/undici/issues/2630
field = line.subarray(0, colonPosition).toString('utf8')
// Collect the characters on the line after the first U+003A COLON
// character (:), and let value be that string.
// If value starts with a U+0020 SPACE character, remove it from value.
let valueStart = colonPosition + 1
if (line[valueStart] === SPACE) {
++valueStart
}
// TODO: Investigate if there is a more performant way to extract the
// value
// see: https://github.com/nodejs/undici/issues/2630
value = line.subarray(valueStart).toString('utf8')
// Otherwise, the string is not empty but does not contain a U+003A COLON
// character (:)
} else {
// Process the field using the steps described below, using the whole
// line as the field name, and the empty string as the field value.
field = line.toString('utf8')
value = ''
}
// Modify the event with the field name and value. The value is also
// decoded as UTF-8
switch (field) {
case 'data':
if (event[field] === undefined) {
event[field] = value
} else {
event[field] += `\n${value}`
}
break
case 'retry':
if (isASCIINumber(value)) {
event[field] = value
}
break
case 'id':
if (isValidLastEventId(value)) {
event[field] = value
}
break
case 'event':
if (value.length > 0) {
event[field] = value
}
break
}
}
/**
* @param {EventSourceStreamEvent} event
*/
processEvent (event) {
if (event.retry && isASCIINumber(event.retry)) {
this.state.reconnectionTime = parseInt(event.retry, 10)
}
if (event.id !== undefined && isValidLastEventId(event.id)) {
this.state.lastEventId = event.id
}
// only dispatch event, when data is provided
if (event.data !== undefined) {
this.push({
type: event.event || 'message',
options: {
data: event.data,
lastEventId: this.state.lastEventId,
origin: this.state.origin
}
})
}
}
clearEvent () {
this.event = {
data: undefined,
event: undefined,
id: undefined,
retry: undefined
}
}
}
module.exports = {
EventSourceStream
}
================================================
FILE: lib/web/eventsource/eventsource.js
================================================
'use strict'
const { pipeline } = require('node:stream')
const { fetching } = require('../fetch')
const { makeRequest } = require('../fetch/request')
const { webidl } = require('../webidl')
const { EventSourceStream } = require('./eventsource-stream')
const { parseMIMEType } = require('../fetch/data-url')
const { createFastMessageEvent } = require('../websocket/events')
const { isNetworkError } = require('../fetch/response')
const { kEnumerableProperty } = require('../../core/util')
const { environmentSettingsObject } = require('../fetch/util')
let experimentalWarned = false
/**
* A reconnection time, in milliseconds. This must initially be an implementation-defined value,
* probably in the region of a few seconds.
*
* In Comparison:
* - Chrome uses 3000ms.
* - Deno uses 5000ms.
*
* @type {3000}
*/
const defaultReconnectionTime = 3000
/**
* The readyState attribute represents the state of the connection.
* @typedef ReadyState
* @type {0|1|2}
* @readonly
* @see https://html.spec.whatwg.org/multipage/server-sent-events.html#dom-eventsource-readystate-dev
*/
/**
* The connection has not yet been established, or it was closed and the user
* agent is reconnecting.
* @type {0}
*/
const CONNECTING = 0
/**
* The user agent has an open connection and is dispatching events as it
* receives them.
* @type {1}
*/
const OPEN = 1
/**
* The connection is not open, and the user agent is not trying to reconnect.
* @type {2}
*/
const CLOSED = 2
/**
* Requests for the element will have their mode set to "cors" and their credentials mode set to "same-origin".
* @type {'anonymous'}
*/
const ANONYMOUS = 'anonymous'
/**
* Requests for the element will have their mode set to "cors" and their credentials mode set to "include".
* @type {'use-credentials'}
*/
const USE_CREDENTIALS = 'use-credentials'
/**
* The EventSource interface is used to receive server-sent events. It
* connects to a server over HTTP and receives events in text/event-stream
* format without closing the connection.
* @extends {EventTarget}
* @see https://html.spec.whatwg.org/multipage/server-sent-events.html#server-sent-events
* @api public
*/
class EventSource extends EventTarget {
#events = {
open: null,
error: null,
message: null
}
#url
#withCredentials = false
/**
* @type {ReadyState}
*/
#readyState = CONNECTING
#request = null
#controller = null
#dispatcher
/**
* @type {import('./eventsource-stream').eventSourceSettings}
*/
#state
/**
* Creates a new EventSource object.
* @param {string} url
* @param {EventSourceInit} [eventSourceInitDict={}]
* @see https://html.spec.whatwg.org/multipage/server-sent-events.html#the-eventsource-interface
*/
constructor (url, eventSourceInitDict = {}) {
// 1. Let ev be a new EventSource object.
super()
webidl.util.markAsUncloneable(this)
const prefix = 'EventSource constructor'
webidl.argumentLengthCheck(arguments, 1, prefix)
if (!experimentalWarned) {
experimentalWarned = true
process.emitWarning('EventSource is experimental, expect them to change at any time.', {
code: 'UNDICI-ES'
})
}
url = webidl.converters.USVString(url)
eventSourceInitDict = webidl.converters.EventSourceInitDict(eventSourceInitDict, prefix, 'eventSourceInitDict')
this.#dispatcher = eventSourceInitDict.node.dispatcher || eventSourceInitDict.dispatcher
this.#state = {
lastEventId: '',
reconnectionTime: eventSourceInitDict.node.reconnectionTime
}
// 2. Let settings be ev's relevant settings object.
// https://html.spec.whatwg.org/multipage/webappapis.html#environment-settings-object
const settings = environmentSettingsObject
let urlRecord
try {
// 3. Let urlRecord be the result of encoding-parsing a URL given url, relative to settings.
urlRecord = new URL(url, settings.settingsObject.baseUrl)
this.#state.origin = urlRecord.origin
} catch (e) {
// 4. If urlRecord is failure, then throw a "SyntaxError" DOMException.
throw new DOMException(e, 'SyntaxError')
}
// 5. Set ev's url to urlRecord.
this.#url = urlRecord.href
// 6. Let corsAttributeState be Anonymous.
let corsAttributeState = ANONYMOUS
// 7. If the value of eventSourceInitDict's withCredentials member is true,
// then set corsAttributeState to Use Credentials and set ev's
// withCredentials attribute to true.
if (eventSourceInitDict.withCredentials === true) {
corsAttributeState = USE_CREDENTIALS
this.#withCredentials = true
}
// 8. Let request be the result of creating a potential-CORS request given
// urlRecord, the empty string, and corsAttributeState.
const initRequest = {
redirect: 'follow',
keepalive: true,
// @see https://html.spec.whatwg.org/multipage/urls-and-fetching.html#cors-settings-attributes
mode: 'cors',
credentials: corsAttributeState === 'anonymous'
? 'same-origin'
: 'omit',
referrer: 'no-referrer'
}
// 9. Set request's client to settings.
initRequest.client = environmentSettingsObject.settingsObject
// 10. User agents may set (`Accept`, `text/event-stream`) in request's header list.
initRequest.headersList = [['accept', { name: 'accept', value: 'text/event-stream' }]]
// 11. Set request's cache mode to "no-store".
initRequest.cache = 'no-store'
// 12. Set request's initiator type to "other".
initRequest.initiator = 'other'
initRequest.urlList = [new URL(this.#url)]
// 13. Set ev's request to request.
this.#request = makeRequest(initRequest)
this.#connect()
}
/**
* Returns the state of this EventSource object's connection. It can have the
* values described below.
* @returns {ReadyState}
* @readonly
*/
get readyState () {
return this.#readyState
}
/**
* Returns the URL providing the event stream.
* @readonly
* @returns {string}
*/
get url () {
return this.#url
}
/**
* Returns a boolean indicating whether the EventSource object was
* instantiated with CORS credentials set (true), or not (false, the default).
*/
get withCredentials () {
return this.#withCredentials
}
#connect () {
if (this.#readyState === CLOSED) return
this.#readyState = CONNECTING
const fetchParams = {
request: this.#request,
dispatcher: this.#dispatcher
}
// 14. Let processEventSourceEndOfBody given response res be the following step: if res is not a network error, then reestablish the connection.
const processEventSourceEndOfBody = (response) => {
if (!isNetworkError(response)) {
return this.#reconnect()
}
}
// 15. Fetch request, with processResponseEndOfBody set to processEventSourceEndOfBody...
fetchParams.processResponseEndOfBody = processEventSourceEndOfBody
// and processResponse set to the following steps given response res:
fetchParams.processResponse = (response) => {
// 1. If res is an aborted network error, then fail the connection.
if (isNetworkError(response)) {
// 1. When a user agent is to fail the connection, the user agent
// must queue a task which, if the readyState attribute is set to a
// value other than CLOSED, sets the readyState attribute to CLOSED
// and fires an event named error at the EventSource object. Once the
// user agent has failed the connection, it does not attempt to
// reconnect.
if (response.aborted) {
this.close()
this.dispatchEvent(new Event('error'))
return
// 2. Otherwise, if res is a network error, then reestablish the
// connection, unless the user agent knows that to be futile, in
// which case the user agent may fail the connection.
} else {
this.#reconnect()
return
}
}
// 3. Otherwise, if res's status is not 200, or if res's `Content-Type`
// is not `text/event-stream`, then fail the connection.
const contentType = response.headersList.get('content-type', true)
const mimeType = contentType !== null ? parseMIMEType(contentType) : 'failure'
const contentTypeValid = mimeType !== 'failure' && mimeType.essence === 'text/event-stream'
if (
response.status !== 200 ||
contentTypeValid === false
) {
this.close()
this.dispatchEvent(new Event('error'))
return
}
// 4. Otherwise, announce the connection and interpret res's body
// line by line.
// When a user agent is to announce the connection, the user agent
// must queue a task which, if the readyState attribute is set to a
// value other than CLOSED, sets the readyState attribute to OPEN
// and fires an event named open at the EventSource object.
// @see https://html.spec.whatwg.org/multipage/server-sent-events.html#sse-processing-model
this.#readyState = OPEN
this.dispatchEvent(new Event('open'))
// If redirected to a different origin, set the origin to the new origin.
this.#state.origin = response.urlList[response.urlList.length - 1].origin
const eventSourceStream = new EventSourceStream({
eventSourceSettings: this.#state,
push: (event) => {
this.dispatchEvent(createFastMessageEvent(
event.type,
event.options
))
}
})
pipeline(response.body.stream,
eventSourceStream,
(error) => {
if (
error?.aborted === false
) {
this.close()
this.dispatchEvent(new Event('error'))
}
})
}
this.#controller = fetching(fetchParams)
}
/**
* @see https://html.spec.whatwg.org/multipage/server-sent-events.html#sse-processing-model
* @returns {void}
*/
#reconnect () {
// When a user agent is to reestablish the connection, the user agent must
// run the following steps. These steps are run in parallel, not as part of
// a task. (The tasks that it queues, of course, are run like normal tasks
// and not themselves in parallel.)
// 1. Queue a task to run the following steps:
// 1. If the readyState attribute is set to CLOSED, abort the task.
if (this.#readyState === CLOSED) return
// 2. Set the readyState attribute to CONNECTING.
this.#readyState = CONNECTING
// 3. Fire an event named error at the EventSource object.
this.dispatchEvent(new Event('error'))
// 2. Wait a delay equal to the reconnection time of the event source.
setTimeout(() => {
// 5. Queue a task to run the following steps:
// 1. If the EventSource object's readyState attribute is not set to
// CONNECTING, then return.
if (this.#readyState !== CONNECTING) return
// 2. Let request be the EventSource object's request.
// 3. If the EventSource object's last event ID string is not the empty
// string, then:
// 1. Let lastEventIDValue be the EventSource object's last event ID
// string, encoded as UTF-8.
// 2. Set (`Last-Event-ID`, lastEventIDValue) in request's header
// list.
if (this.#state.lastEventId.length) {
this.#request.headersList.set('last-event-id', this.#state.lastEventId, true)
}
// 4. Fetch request and process the response obtained in this fashion, if any, as described earlier in this section.
this.#connect()
}, this.#state.reconnectionTime)?.unref()
}
/**
* Closes the connection, if any, and sets the readyState attribute to
* CLOSED.
*/
close () {
webidl.brandCheck(this, EventSource)
if (this.#readyState === CLOSED) return
this.#readyState = CLOSED
this.#controller.abort()
this.#request = null
}
get onopen () {
return this.#events.open
}
set onopen (fn) {
if (this.#events.open) {
this.removeEventListener('open', this.#events.open)
}
const listener = webidl.converters.EventHandlerNonNull(fn)
if (listener !== null) {
this.addEventListener('open', listener)
this.#events.open = fn
} else {
this.#events.open = null
}
}
get onmessage () {
return this.#events.message
}
set onmessage (fn) {
if (this.#events.message) {
this.removeEventListener('message', this.#events.message)
}
const listener = webidl.converters.EventHandlerNonNull(fn)
if (listener !== null) {
this.addEventListener('message', listener)
this.#events.message = fn
} else {
this.#events.message = null
}
}
get onerror () {
return this.#events.error
}
set onerror (fn) {
if (this.#events.error) {
this.removeEventListener('error', this.#events.error)
}
const listener = webidl.converters.EventHandlerNonNull(fn)
if (listener !== null) {
this.addEventListener('error', listener)
this.#events.error = fn
} else {
this.#events.error = null
}
}
}
const constantsPropertyDescriptors = {
CONNECTING: {
__proto__: null,
configurable: false,
enumerable: true,
value: CONNECTING,
writable: false
},
OPEN: {
__proto__: null,
configurable: false,
enumerable: true,
value: OPEN,
writable: false
},
CLOSED: {
__proto__: null,
configurable: false,
enumerable: true,
value: CLOSED,
writable: false
}
}
Object.defineProperties(EventSource, constantsPropertyDescriptors)
Object.defineProperties(EventSource.prototype, constantsPropertyDescriptors)
Object.defineProperties(EventSource.prototype, {
close: kEnumerableProperty,
onerror: kEnumerableProperty,
onmessage: kEnumerableProperty,
onopen: kEnumerableProperty,
readyState: kEnumerableProperty,
url: kEnumerableProperty,
withCredentials: kEnumerableProperty
})
webidl.converters.EventSourceInitDict = webidl.dictionaryConverter([
{
key: 'withCredentials',
converter: webidl.converters.boolean,
defaultValue: () => false
},
{
key: 'dispatcher', // undici only
converter: webidl.converters.any
},
{
key: 'node', // undici only
converter: webidl.dictionaryConverter([
{
key: 'reconnectionTime',
converter: webidl.converters['unsigned long'],
defaultValue: () => defaultReconnectionTime
},
{
key: 'dispatcher',
converter: webidl.converters.any
}
]),
defaultValue: () => ({})
}
])
module.exports = {
EventSource,
defaultReconnectionTime
}
================================================
FILE: lib/web/eventsource/util.js
================================================
'use strict'
/**
* Checks if the given value is a valid LastEventId.
* @param {string} value
* @returns {boolean}
*/
function isValidLastEventId (value) {
// LastEventId should not contain U+0000 NULL
return value.indexOf('\u0000') === -1
}
/**
* Checks if the given value is a base 10 digit.
* @param {string} value
* @returns {boolean}
*/
function isASCIINumber (value) {
if (value.length === 0) return false
for (let i = 0; i < value.length; i++) {
if (value.charCodeAt(i) < 0x30 || value.charCodeAt(i) > 0x39) return false
}
return true
}
module.exports = {
isValidLastEventId,
isASCIINumber
}
================================================
FILE: lib/web/fetch/LICENSE
================================================
MIT License
Copyright (c) 2020 Ethan Arrowood
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: lib/web/fetch/body.js
================================================
'use strict'
const util = require('../../core/util')
const {
ReadableStreamFrom,
readableStreamClose,
fullyReadBody,
extractMimeType
} = require('./util')
const { FormData, setFormDataState } = require('./formdata')
const { webidl } = require('../webidl')
const assert = require('node:assert')
const { isErrored, isDisturbed } = require('node:stream')
const { isUint8Array } = require('node:util/types')
const { serializeAMimeType } = require('./data-url')
const { multipartFormDataParser } = require('./formdata-parser')
const { createDeferredPromise } = require('../../util/promise')
const { parseJSONFromBytes } = require('../infra')
const { utf8DecodeBytes } = require('../../encoding')
const { runtimeFeatures } = require('../../util/runtime-features.js')
const random = runtimeFeatures.has('crypto')
? require('node:crypto').randomInt
: (max) => Math.floor(Math.random() * max)
const textEncoder = new TextEncoder()
function noop () {}
const streamRegistry = new FinalizationRegistry((weakRef) => {
const stream = weakRef.deref()
if (stream && !stream.locked && !isDisturbed(stream) && !isErrored(stream)) {
stream.cancel('Response object has been garbage collected').catch(noop)
}
})
/**
* Extract a body with type from a byte sequence or BodyInit object
*
* @param {import('../../../types').BodyInit} object - The BodyInit object to extract from
* @param {boolean} [keepalive=false] - If true, indicates that the body
* @returns {[{stream: ReadableStream, source: any, length: number | null}, string | null]} - Returns a tuple containing the body and its type
*
* @see https://fetch.spec.whatwg.org/#concept-bodyinit-extract
*/
function extractBody (object, keepalive = false) {
// 1. Let stream be null.
let stream = null
let controller = null
// 2. If object is a ReadableStream object, then set stream to object.
if (webidl.is.ReadableStream(object)) {
stream = object
} else if (webidl.is.Blob(object)) {
// 3. Otherwise, if object is a Blob object, set stream to the
// result of running object’s get stream.
stream = object.stream()
} else {
// 4. Otherwise, set stream to a new ReadableStream object, and set
// up stream with byte reading support.
stream = new ReadableStream({
pull () {},
start (c) {
controller = c
},
cancel () {},
type: 'bytes'
})
}
// 5. Assert: stream is a ReadableStream object.
assert(webidl.is.ReadableStream(stream))
// 6. Let action be null.
let action = null
// 7. Let source be null.
let source = null
// 8. Let length be null.
let length = null
// 9. Let type be null.
let type = null
// 10. Switch on object:
if (typeof object === 'string') {
// Set source to the UTF-8 encoding of object.
// Note: setting source to a Uint8Array here breaks some mocking assumptions.
source = object
// Set type to `text/plain;charset=UTF-8`.
type = 'text/plain;charset=UTF-8'
} else if (webidl.is.URLSearchParams(object)) {
// URLSearchParams
// spec says to run application/x-www-form-urlencoded on body.list
// this is implemented in Node.js as apart of an URLSearchParams instance toString method
// See: https://github.com/nodejs/node/blob/e46c680bf2b211bbd52cf959ca17ee98c7f657f5/lib/internal/url.js#L490
// and https://github.com/nodejs/node/blob/e46c680bf2b211bbd52cf959ca17ee98c7f657f5/lib/internal/url.js#L1100
// Set source to the result of running the application/x-www-form-urlencoded serializer with object’s list.
source = object.toString()
// Set type to `application/x-www-form-urlencoded;charset=UTF-8`.
type = 'application/x-www-form-urlencoded;charset=UTF-8'
} else if (webidl.is.BufferSource(object)) {
// Set source to a copy of the bytes held by object.
source = webidl.util.getCopyOfBytesHeldByBufferSource(object)
} else if (webidl.is.FormData(object)) {
const boundary = `----formdata-undici-0${`${random(1e11)}`.padStart(11, '0')}`
const prefix = `--${boundary}\r\nContent-Disposition: form-data`
/*! formdata-polyfill. MIT License. Jimmy Wärting */
const formdataEscape = (str) =>
str.replace(/\n/g, '%0A').replace(/\r/g, '%0D').replace(/"/g, '%22')
const normalizeLinefeeds = (value) => value.replace(/\r?\n|\r/g, '\r\n')
// Set action to this step: run the multipart/form-data
// encoding algorithm, with object’s entry list and UTF-8.
// - This ensures that the body is immutable and can't be changed afterwords
// - That the content-length is calculated in advance.
// - And that all parts are pre-encoded and ready to be sent.
const blobParts = []
const rn = new Uint8Array([13, 10]) // '\r\n'
length = 0
let hasUnknownSizeValue = false
for (const [name, value] of object) {
if (typeof value === 'string') {
const chunk = textEncoder.encode(prefix +
`; name="${formdataEscape(normalizeLinefeeds(name))}"` +
`\r\n\r\n${normalizeLinefeeds(value)}\r\n`)
blobParts.push(chunk)
length += chunk.byteLength
} else {
const chunk = textEncoder.encode(`${prefix}; name="${formdataEscape(normalizeLinefeeds(name))}"` +
(value.name ? `; filename="${formdataEscape(value.name)}"` : '') + '\r\n' +
`Content-Type: ${
value.type || 'application/octet-stream'
}\r\n\r\n`)
blobParts.push(chunk, value, rn)
if (typeof value.size === 'number') {
length += chunk.byteLength + value.size + rn.byteLength
} else {
hasUnknownSizeValue = true
}
}
}
// CRLF is appended to the body to function with legacy servers and match other implementations.
// https://github.com/curl/curl/blob/3434c6b46e682452973972e8313613dfa58cd690/lib/mime.c#L1029-L1030
// https://github.com/form-data/form-data/issues/63
const chunk = textEncoder.encode(`--${boundary}--\r\n`)
blobParts.push(chunk)
length += chunk.byteLength
if (hasUnknownSizeValue) {
length = null
}
// Set source to object.
source = object
action = async function * () {
for (const part of blobParts) {
if (part.stream) {
yield * part.stream()
} else {
yield part
}
}
}
// Set type to `multipart/form-data; boundary=`,
// followed by the multipart/form-data boundary string generated
// by the multipart/form-data encoding algorithm.
type = `multipart/form-data; boundary=${boundary}`
} else if (webidl.is.Blob(object)) {
// Blob
// Set source to object.
source = object
// Set length to object’s size.
length = object.size
// If object’s type attribute is not the empty byte sequence, set
// type to its value.
if (object.type) {
type = object.type
}
} else if (typeof object[Symbol.asyncIterator] === 'function') {
// If keepalive is true, then throw a TypeError.
if (keepalive) {
throw new TypeError('keepalive')
}
// If object is disturbed or locked, then throw a TypeError.
if (util.isDisturbed(object) || object.locked) {
throw new TypeError(
'Response body object should not be disturbed or locked'
)
}
stream =
webidl.is.ReadableStream(object) ? object : ReadableStreamFrom(object)
}
// 11. If source is a byte sequence, then set action to a
// step that returns source and length to source’s length.
if (typeof source === 'string' || isUint8Array(source)) {
action = () => {
length = typeof source === 'string' ? Buffer.byteLength(source) : source.length
return source
}
}
// 12. If action is non-null, then run these steps in parallel:
if (action != null) {
;(async () => {
// 1. Run action.
const result = action()
// 2. Whenever one or more bytes are available and stream is not errored,
// enqueue the result of creating a Uint8Array from the available bytes into stream.
const iterator = result?.[Symbol.asyncIterator]?.()
if (iterator) {
for await (const bytes of iterator) {
if (isErrored(stream)) break
if (bytes.length) {
controller.enqueue(new Uint8Array(bytes))
}
}
} else if (result?.length && !isErrored(stream)) {
controller.enqueue(typeof result === 'string' ? textEncoder.encode(result) : new Uint8Array(result))
}
// 3. When running action is done, close stream.
queueMicrotask(() => readableStreamClose(controller))
})()
}
// 13. Let body be a body whose stream is stream, source is source,
// and length is length.
const body = { stream, source, length }
// 14. Return (body, type).
return [body, type]
}
/**
* @typedef {object} ExtractBodyResult
* @property {ReadableStream>} stream - The ReadableStream containing the body data
* @property {any} source - The original source of the body data
* @property {number | null} length - The length of the body data, or null
*/
/**
* Safely extract a body with type from a byte sequence or BodyInit object.
*
* @param {import('../../../types').BodyInit} object - The BodyInit object to extract from
* @param {boolean} [keepalive=false] - If true, indicates that the body
* @returns {[ExtractBodyResult, string | null]} - Returns a tuple containing the body and its type
*
* @see https://fetch.spec.whatwg.org/#bodyinit-safely-extract
*/
function safelyExtractBody (object, keepalive = false) {
// To safely extract a body and a `Content-Type` value from
// a byte sequence or BodyInit object object, run these steps:
// 1. If object is a ReadableStream object, then:
if (webidl.is.ReadableStream(object)) {
// Assert: object is neither disturbed nor locked.
assert(!util.isDisturbed(object), 'The body has already been consumed.')
assert(!object.locked, 'The stream is locked.')
}
// 2. Return the results of extracting object.
return extractBody(object, keepalive)
}
function cloneBody (body) {
// To clone a body body, run these steps:
// https://fetch.spec.whatwg.org/#concept-body-clone
// 1. Let « out1, out2 » be the result of teeing body’s stream.
const { 0: out1, 1: out2 } = body.stream.tee()
// 2. Set body’s stream to out1.
body.stream = out1
// 3. Return a body whose stream is out2 and other members are copied from body.
return {
stream: out2,
length: body.length,
source: body.source
}
}
function bodyMixinMethods (instance, getInternalState) {
const methods = {
blob () {
// The blob() method steps are to return the result of
// running consume body with this and the following step
// given a byte sequence bytes: return a Blob whose
// contents are bytes and whose type attribute is this’s
// MIME type.
return consumeBody(this, (bytes) => {
let mimeType = bodyMimeType(getInternalState(this))
if (mimeType === null) {
mimeType = ''
} else if (mimeType) {
mimeType = serializeAMimeType(mimeType)
}
// Return a Blob whose contents are bytes and type attribute
// is mimeType.
return new Blob([bytes], { type: mimeType })
}, instance, getInternalState)
},
arrayBuffer () {
// The arrayBuffer() method steps are to return the result
// of running consume body with this and the following step
// given a byte sequence bytes: return a new ArrayBuffer
// whose contents are bytes.
return consumeBody(this, (bytes) => {
return new Uint8Array(bytes).buffer
}, instance, getInternalState)
},
text () {
// The text() method steps are to return the result of running
// consume body with this and UTF-8 decode.
return consumeBody(this, utf8DecodeBytes, instance, getInternalState)
},
json () {
// The json() method steps are to return the result of running
// consume body with this and parse JSON from bytes.
return consumeBody(this, parseJSONFromBytes, instance, getInternalState)
},
formData () {
// The formData() method steps are to return the result of running
// consume body with this and the following step given a byte sequence bytes:
return consumeBody(this, (value) => {
// 1. Let mimeType be the result of get the MIME type with this.
const mimeType = bodyMimeType(getInternalState(this))
// 2. If mimeType is non-null, then switch on mimeType’s essence and run
// the corresponding steps:
if (mimeType !== null) {
switch (mimeType.essence) {
case 'multipart/form-data': {
// 1. ... [long step]
// 2. If that fails for some reason, then throw a TypeError.
const parsed = multipartFormDataParser(value, mimeType)
// 3. Return a new FormData object, appending each entry,
// resulting from the parsing operation, to its entry list.
const fd = new FormData()
setFormDataState(fd, parsed)
return fd
}
case 'application/x-www-form-urlencoded': {
// 1. Let entries be the result of parsing bytes.
const entries = new URLSearchParams(value.toString())
// 2. If entries is failure, then throw a TypeError.
// 3. Return a new FormData object whose entry list is entries.
const fd = new FormData()
for (const [name, value] of entries) {
fd.append(name, value)
}
return fd
}
}
}
// 3. Throw a TypeError.
throw new TypeError(
'Content-Type was not one of "multipart/form-data" or "application/x-www-form-urlencoded".'
)
}, instance, getInternalState)
},
bytes () {
// The bytes() method steps are to return the result of running consume body
// with this and the following step given a byte sequence bytes: return the
// result of creating a Uint8Array from bytes in this’s relevant realm.
return consumeBody(this, (bytes) => {
return new Uint8Array(bytes)
}, instance, getInternalState)
}
}
return methods
}
function mixinBody (prototype, getInternalState) {
Object.assign(prototype.prototype, bodyMixinMethods(prototype, getInternalState))
}
/**
* @see https://fetch.spec.whatwg.org/#concept-body-consume-body
* @param {any} object internal state
* @param {(value: unknown) => unknown} convertBytesToJSValue
* @param {any} instance
* @param {(target: any) => any} getInternalState
*/
function consumeBody (object, convertBytesToJSValue, instance, getInternalState) {
try {
webidl.brandCheck(object, instance)
} catch (e) {
return Promise.reject(e)
}
object = getInternalState(object)
// 1. If object is unusable, then return a promise rejected
// with a TypeError.
if (bodyUnusable(object)) {
return Promise.reject(new TypeError('Body is unusable: Body has already been read'))
}
// 2. Let promise be a new promise.
const promise = createDeferredPromise()
// 3. Let errorSteps given error be to reject promise with error.
const errorSteps = promise.reject
// 4. Let successSteps given a byte sequence data be to resolve
// promise with the result of running convertBytesToJSValue
// with data. If that threw an exception, then run errorSteps
// with that exception.
const successSteps = (data) => {
try {
promise.resolve(convertBytesToJSValue(data))
} catch (e) {
errorSteps(e)
}
}
// 5. If object’s body is null, then run successSteps with an
// empty byte sequence.
if (object.body == null) {
successSteps(Buffer.allocUnsafe(0))
return promise.promise
}
// 6. Otherwise, fully read object’s body given successSteps,
// errorSteps, and object’s relevant global object.
fullyReadBody(object.body, successSteps, errorSteps)
// 7. Return promise.
return promise.promise
}
/**
* @see https://fetch.spec.whatwg.org/#body-unusable
* @param {any} object internal state
*/
function bodyUnusable (object) {
const body = object.body
// An object including the Body interface mixin is
// said to be unusable if its body is non-null and
// its body’s stream is disturbed or locked.
return body != null && (body.stream.locked || util.isDisturbed(body.stream))
}
/**
* @see https://fetch.spec.whatwg.org/#concept-body-mime-type
* @param {any} requestOrResponse internal state
*/
function bodyMimeType (requestOrResponse) {
// 1. Let headers be null.
// 2. If requestOrResponse is a Request object, then set headers to requestOrResponse’s request’s header list.
// 3. Otherwise, set headers to requestOrResponse’s response’s header list.
/** @type {import('./headers').HeadersList} */
const headers = requestOrResponse.headersList
// 4. Let mimeType be the result of extracting a MIME type from headers.
const mimeType = extractMimeType(headers)
// 5. If mimeType is failure, then return null.
if (mimeType === 'failure') {
return null
}
// 6. Return mimeType.
return mimeType
}
module.exports = {
extractBody,
safelyExtractBody,
cloneBody,
mixinBody,
streamRegistry,
bodyUnusable
}
================================================
FILE: lib/web/fetch/constants.js
================================================
'use strict'
const corsSafeListedMethods = /** @type {const} */ (['GET', 'HEAD', 'POST'])
const corsSafeListedMethodsSet = new Set(corsSafeListedMethods)
const nullBodyStatus = /** @type {const} */ ([101, 204, 205, 304])
const redirectStatus = /** @type {const} */ ([301, 302, 303, 307, 308])
const redirectStatusSet = new Set(redirectStatus)
/**
* @see https://fetch.spec.whatwg.org/#block-bad-port
*/
const badPorts = /** @type {const} */ ([
'1', '7', '9', '11', '13', '15', '17', '19', '20', '21', '22', '23', '25', '37', '42', '43', '53', '69', '77', '79',
'87', '95', '101', '102', '103', '104', '109', '110', '111', '113', '115', '117', '119', '123', '135', '137',
'139', '143', '161', '179', '389', '427', '465', '512', '513', '514', '515', '526', '530', '531', '532',
'540', '548', '554', '556', '563', '587', '601', '636', '989', '990', '993', '995', '1719', '1720', '1723',
'2049', '3659', '4045', '4190', '5060', '5061', '6000', '6566', '6665', '6666', '6667', '6668', '6669', '6679',
'6697', '10080'
])
const badPortsSet = new Set(badPorts)
/**
* @see https://w3c.github.io/webappsec-referrer-policy/#referrer-policy-header
*/
const referrerPolicyTokens = /** @type {const} */ ([
'no-referrer',
'no-referrer-when-downgrade',
'same-origin',
'origin',
'strict-origin',
'origin-when-cross-origin',
'strict-origin-when-cross-origin',
'unsafe-url'
])
/**
* @see https://w3c.github.io/webappsec-referrer-policy/#referrer-policies
*/
const referrerPolicy = /** @type {const} */ ([
'',
...referrerPolicyTokens
])
const referrerPolicyTokensSet = new Set(referrerPolicyTokens)
const requestRedirect = /** @type {const} */ (['follow', 'manual', 'error'])
const safeMethods = /** @type {const} */ (['GET', 'HEAD', 'OPTIONS', 'TRACE'])
const safeMethodsSet = new Set(safeMethods)
const requestMode = /** @type {const} */ (['navigate', 'same-origin', 'no-cors', 'cors'])
const requestCredentials = /** @type {const} */ (['omit', 'same-origin', 'include'])
const requestCache = /** @type {const} */ ([
'default',
'no-store',
'reload',
'no-cache',
'force-cache',
'only-if-cached'
])
/**
* @see https://fetch.spec.whatwg.org/#request-body-header-name
*/
const requestBodyHeader = /** @type {const} */ ([
'content-encoding',
'content-language',
'content-location',
'content-type',
// See https://github.com/nodejs/undici/issues/2021
// 'Content-Length' is a forbidden header name, which is typically
// removed in the Headers implementation. However, undici doesn't
// filter out headers, so we add it here.
'content-length'
])
/**
* @see https://fetch.spec.whatwg.org/#enumdef-requestduplex
*/
const requestDuplex = /** @type {const} */ ([
'half'
])
/**
* @see http://fetch.spec.whatwg.org/#forbidden-method
*/
const forbiddenMethods = /** @type {const} */ (['CONNECT', 'TRACE', 'TRACK'])
const forbiddenMethodsSet = new Set(forbiddenMethods)
const subresource = /** @type {const} */ ([
'audio',
'audioworklet',
'font',
'image',
'manifest',
'paintworklet',
'script',
'style',
'track',
'video',
'xslt',
''
])
const subresourceSet = new Set(subresource)
module.exports = {
subresource,
forbiddenMethods,
requestBodyHeader,
referrerPolicy,
requestRedirect,
requestMode,
requestCredentials,
requestCache,
redirectStatus,
corsSafeListedMethods,
nullBodyStatus,
safeMethods,
badPorts,
requestDuplex,
subresourceSet,
badPortsSet,
redirectStatusSet,
corsSafeListedMethodsSet,
safeMethodsSet,
forbiddenMethodsSet,
referrerPolicyTokens: referrerPolicyTokensSet
}
================================================
FILE: lib/web/fetch/data-url.js
================================================
'use strict'
const assert = require('node:assert')
const { forgivingBase64, collectASequenceOfCodePoints, collectASequenceOfCodePointsFast, isomorphicDecode, removeASCIIWhitespace, removeChars } = require('../infra')
const encoder = new TextEncoder()
/**
* @see https://mimesniff.spec.whatwg.org/#http-token-code-point
*/
const HTTP_TOKEN_CODEPOINTS = /^[-!#$%&'*+.^_|~A-Za-z0-9]+$/u
const HTTP_WHITESPACE_REGEX = /[\u000A\u000D\u0009\u0020]/u // eslint-disable-line
/**
* @see https://mimesniff.spec.whatwg.org/#http-quoted-string-token-code-point
*/
const HTTP_QUOTED_STRING_TOKENS = /^[\u0009\u0020-\u007E\u0080-\u00FF]+$/u // eslint-disable-line
// https://fetch.spec.whatwg.org/#data-url-processor
/** @param {URL} dataURL */
function dataURLProcessor (dataURL) {
// 1. Assert: dataURL’s scheme is "data".
assert(dataURL.protocol === 'data:')
// 2. Let input be the result of running the URL
// serializer on dataURL with exclude fragment
// set to true.
let input = URLSerializer(dataURL, true)
// 3. Remove the leading "data:" string from input.
input = input.slice(5)
// 4. Let position point at the start of input.
const position = { position: 0 }
// 5. Let mimeType be the result of collecting a
// sequence of code points that are not equal
// to U+002C (,), given position.
let mimeType = collectASequenceOfCodePointsFast(
',',
input,
position
)
// 6. Strip leading and trailing ASCII whitespace
// from mimeType.
// Undici implementation note: we need to store the
// length because if the mimetype has spaces removed,
// the wrong amount will be sliced from the input in
// step #9
const mimeTypeLength = mimeType.length
mimeType = removeASCIIWhitespace(mimeType, true, true)
// 7. If position is past the end of input, then
// return failure
if (position.position >= input.length) {
return 'failure'
}
// 8. Advance position by 1.
position.position++
// 9. Let encodedBody be the remainder of input.
const encodedBody = input.slice(mimeTypeLength + 1)
// 10. Let body be the percent-decoding of encodedBody.
let body = stringPercentDecode(encodedBody)
// 11. If mimeType ends with U+003B (;), followed by
// zero or more U+0020 SPACE, followed by an ASCII
// case-insensitive match for "base64", then:
if (/;(?:\u0020*)base64$/ui.test(mimeType)) {
// 1. Let stringBody be the isomorphic decode of body.
const stringBody = isomorphicDecode(body)
// 2. Set body to the forgiving-base64 decode of
// stringBody.
body = forgivingBase64(stringBody)
// 3. If body is failure, then return failure.
if (body === 'failure') {
return 'failure'
}
// 4. Remove the last 6 code points from mimeType.
mimeType = mimeType.slice(0, -6)
// 5. Remove trailing U+0020 SPACE code points from mimeType,
// if any.
mimeType = mimeType.replace(/(\u0020+)$/u, '')
// 6. Remove the last U+003B (;) code point from mimeType.
mimeType = mimeType.slice(0, -1)
}
// 12. If mimeType starts with U+003B (;), then prepend
// "text/plain" to mimeType.
if (mimeType.startsWith(';')) {
mimeType = 'text/plain' + mimeType
}
// 13. Let mimeTypeRecord be the result of parsing
// mimeType.
let mimeTypeRecord = parseMIMEType(mimeType)
// 14. If mimeTypeRecord is failure, then set
// mimeTypeRecord to text/plain;charset=US-ASCII.
if (mimeTypeRecord === 'failure') {
mimeTypeRecord = parseMIMEType('text/plain;charset=US-ASCII')
}
// 15. Return a new data: URL struct whose MIME
// type is mimeTypeRecord and body is body.
// https://fetch.spec.whatwg.org/#data-url-struct
return { mimeType: mimeTypeRecord, body }
}
// https://url.spec.whatwg.org/#concept-url-serializer
/**
* @param {URL} url
* @param {boolean} excludeFragment
*/
function URLSerializer (url, excludeFragment = false) {
if (!excludeFragment) {
return url.href
}
const href = url.href
const hashLength = url.hash.length
const serialized = hashLength === 0 ? href : href.substring(0, href.length - hashLength)
if (!hashLength && href.endsWith('#')) {
return serialized.slice(0, -1)
}
return serialized
}
// https://url.spec.whatwg.org/#string-percent-decode
/** @param {string} input */
function stringPercentDecode (input) {
// 1. Let bytes be the UTF-8 encoding of input.
const bytes = encoder.encode(input)
// 2. Return the percent-decoding of bytes.
return percentDecode(bytes)
}
/**
* @param {number} byte
*/
function isHexCharByte (byte) {
// 0-9 A-F a-f
return (byte >= 0x30 && byte <= 0x39) || (byte >= 0x41 && byte <= 0x46) || (byte >= 0x61 && byte <= 0x66)
}
/**
* @param {number} byte
*/
function hexByteToNumber (byte) {
return (
// 0-9
byte >= 0x30 && byte <= 0x39
? (byte - 48)
// Convert to uppercase
// ((byte & 0xDF) - 65) + 10
: ((byte & 0xDF) - 55)
)
}
// https://url.spec.whatwg.org/#percent-decode
/** @param {Uint8Array} input */
function percentDecode (input) {
const length = input.length
// 1. Let output be an empty byte sequence.
/** @type {Uint8Array} */
const output = new Uint8Array(length)
let j = 0
let i = 0
// 2. For each byte byte in input:
while (i < length) {
const byte = input[i]
// 1. If byte is not 0x25 (%), then append byte to output.
if (byte !== 0x25) {
output[j++] = byte
// 2. Otherwise, if byte is 0x25 (%) and the next two bytes
// after byte in input are not in the ranges
// 0x30 (0) to 0x39 (9), 0x41 (A) to 0x46 (F),
// and 0x61 (a) to 0x66 (f), all inclusive, append byte
// to output.
} else if (
byte === 0x25 &&
!(isHexCharByte(input[i + 1]) && isHexCharByte(input[i + 2]))
) {
output[j++] = 0x25
// 3. Otherwise:
} else {
// 1. Let bytePoint be the two bytes after byte in input,
// decoded, and then interpreted as hexadecimal number.
// 2. Append a byte whose value is bytePoint to output.
output[j++] = (hexByteToNumber(input[i + 1]) << 4) | hexByteToNumber(input[i + 2])
// 3. Skip the next two bytes in input.
i += 2
}
++i
}
// 3. Return output.
return length === j ? output : output.subarray(0, j)
}
// https://mimesniff.spec.whatwg.org/#parse-a-mime-type
/** @param {string} input */
function parseMIMEType (input) {
// 1. Remove any leading and trailing HTTP whitespace
// from input.
input = removeHTTPWhitespace(input, true, true)
// 2. Let position be a position variable for input,
// initially pointing at the start of input.
const position = { position: 0 }
// 3. Let type be the result of collecting a sequence
// of code points that are not U+002F (/) from
// input, given position.
const type = collectASequenceOfCodePointsFast(
'/',
input,
position
)
// 4. If type is the empty string or does not solely
// contain HTTP token code points, then return failure.
// https://mimesniff.spec.whatwg.org/#http-token-code-point
if (type.length === 0 || !HTTP_TOKEN_CODEPOINTS.test(type)) {
return 'failure'
}
// 5. If position is past the end of input, then return
// failure
if (position.position >= input.length) {
return 'failure'
}
// 6. Advance position by 1. (This skips past U+002F (/).)
position.position++
// 7. Let subtype be the result of collecting a sequence of
// code points that are not U+003B (;) from input, given
// position.
let subtype = collectASequenceOfCodePointsFast(
';',
input,
position
)
// 8. Remove any trailing HTTP whitespace from subtype.
subtype = removeHTTPWhitespace(subtype, false, true)
// 9. If subtype is the empty string or does not solely
// contain HTTP token code points, then return failure.
if (subtype.length === 0 || !HTTP_TOKEN_CODEPOINTS.test(subtype)) {
return 'failure'
}
const typeLowercase = type.toLowerCase()
const subtypeLowercase = subtype.toLowerCase()
// 10. Let mimeType be a new MIME type record whose type
// is type, in ASCII lowercase, and subtype is subtype,
// in ASCII lowercase.
// https://mimesniff.spec.whatwg.org/#mime-type
const mimeType = {
type: typeLowercase,
subtype: subtypeLowercase,
/** @type {Map} */
parameters: new Map(),
// https://mimesniff.spec.whatwg.org/#mime-type-essence
essence: `${typeLowercase}/${subtypeLowercase}`
}
// 11. While position is not past the end of input:
while (position.position < input.length) {
// 1. Advance position by 1. (This skips past U+003B (;).)
position.position++
// 2. Collect a sequence of code points that are HTTP
// whitespace from input given position.
collectASequenceOfCodePoints(
// https://fetch.spec.whatwg.org/#http-whitespace
char => HTTP_WHITESPACE_REGEX.test(char),
input,
position
)
// 3. Let parameterName be the result of collecting a
// sequence of code points that are not U+003B (;)
// or U+003D (=) from input, given position.
let parameterName = collectASequenceOfCodePoints(
(char) => char !== ';' && char !== '=',
input,
position
)
// 4. Set parameterName to parameterName, in ASCII
// lowercase.
parameterName = parameterName.toLowerCase()
// 5. If position is not past the end of input, then:
if (position.position < input.length) {
// 1. If the code point at position within input is
// U+003B (;), then continue.
if (input[position.position] === ';') {
continue
}
// 2. Advance position by 1. (This skips past U+003D (=).)
position.position++
}
// 6. If position is past the end of input, then break.
if (position.position >= input.length) {
break
}
// 7. Let parameterValue be null.
let parameterValue = null
// 8. If the code point at position within input is
// U+0022 ("), then:
if (input[position.position] === '"') {
// 1. Set parameterValue to the result of collecting
// an HTTP quoted string from input, given position
// and the extract-value flag.
parameterValue = collectAnHTTPQuotedString(input, position, true)
// 2. Collect a sequence of code points that are not
// U+003B (;) from input, given position.
collectASequenceOfCodePointsFast(
';',
input,
position
)
// 9. Otherwise:
} else {
// 1. Set parameterValue to the result of collecting
// a sequence of code points that are not U+003B (;)
// from input, given position.
parameterValue = collectASequenceOfCodePointsFast(
';',
input,
position
)
// 2. Remove any trailing HTTP whitespace from parameterValue.
parameterValue = removeHTTPWhitespace(parameterValue, false, true)
// 3. If parameterValue is the empty string, then continue.
if (parameterValue.length === 0) {
continue
}
}
// 10. If all of the following are true
// - parameterName is not the empty string
// - parameterName solely contains HTTP token code points
// - parameterValue solely contains HTTP quoted-string token code points
// - mimeType’s parameters[parameterName] does not exist
// then set mimeType’s parameters[parameterName] to parameterValue.
if (
parameterName.length !== 0 &&
HTTP_TOKEN_CODEPOINTS.test(parameterName) &&
(parameterValue.length === 0 || HTTP_QUOTED_STRING_TOKENS.test(parameterValue)) &&
!mimeType.parameters.has(parameterName)
) {
mimeType.parameters.set(parameterName, parameterValue)
}
}
// 12. Return mimeType.
return mimeType
}
// https://fetch.spec.whatwg.org/#collect-an-http-quoted-string
// tests: https://fetch.spec.whatwg.org/#example-http-quoted-string
/**
* @param {string} input
* @param {{ position: number }} position
* @param {boolean} [extractValue=false]
*/
function collectAnHTTPQuotedString (input, position, extractValue = false) {
// 1. Let positionStart be position.
const positionStart = position.position
// 2. Let value be the empty string.
let value = ''
// 3. Assert: the code point at position within input
// is U+0022 (").
assert(input[position.position] === '"')
// 4. Advance position by 1.
position.position++
// 5. While true:
while (true) {
// 1. Append the result of collecting a sequence of code points
// that are not U+0022 (") or U+005C (\) from input, given
// position, to value.
value += collectASequenceOfCodePoints(
(char) => char !== '"' && char !== '\\',
input,
position
)
// 2. If position is past the end of input, then break.
if (position.position >= input.length) {
break
}
// 3. Let quoteOrBackslash be the code point at position within
// input.
const quoteOrBackslash = input[position.position]
// 4. Advance position by 1.
position.position++
// 5. If quoteOrBackslash is U+005C (\), then:
if (quoteOrBackslash === '\\') {
// 1. If position is past the end of input, then append
// U+005C (\) to value and break.
if (position.position >= input.length) {
value += '\\'
break
}
// 2. Append the code point at position within input to value.
value += input[position.position]
// 3. Advance position by 1.
position.position++
// 6. Otherwise:
} else {
// 1. Assert: quoteOrBackslash is U+0022 (").
assert(quoteOrBackslash === '"')
// 2. Break.
break
}
}
// 6. If the extract-value flag is set, then return value.
if (extractValue) {
return value
}
// 7. Return the code points from positionStart to position,
// inclusive, within input.
return input.slice(positionStart, position.position)
}
/**
* @see https://mimesniff.spec.whatwg.org/#serialize-a-mime-type
*/
function serializeAMimeType (mimeType) {
assert(mimeType !== 'failure')
const { parameters, essence } = mimeType
// 1. Let serialization be the concatenation of mimeType’s
// type, U+002F (/), and mimeType’s subtype.
let serialization = essence
// 2. For each name → value of mimeType’s parameters:
for (let [name, value] of parameters.entries()) {
// 1. Append U+003B (;) to serialization.
serialization += ';'
// 2. Append name to serialization.
serialization += name
// 3. Append U+003D (=) to serialization.
serialization += '='
// 4. If value does not solely contain HTTP token code
// points or value is the empty string, then:
if (!HTTP_TOKEN_CODEPOINTS.test(value)) {
// 1. Precede each occurrence of U+0022 (") or
// U+005C (\) in value with U+005C (\).
value = value.replace(/[\\"]/ug, '\\$&')
// 2. Prepend U+0022 (") to value.
value = '"' + value
// 3. Append U+0022 (") to value.
value += '"'
}
// 5. Append value to serialization.
serialization += value
}
// 3. Return serialization.
return serialization
}
/**
* @see https://fetch.spec.whatwg.org/#http-whitespace
* @param {number} char
*/
function isHTTPWhiteSpace (char) {
// "\r\n\t "
return char === 0x00d || char === 0x00a || char === 0x009 || char === 0x020
}
/**
* @see https://fetch.spec.whatwg.org/#http-whitespace
* @param {string} str
* @param {boolean} [leading=true]
* @param {boolean} [trailing=true]
*/
function removeHTTPWhitespace (str, leading = true, trailing = true) {
return removeChars(str, leading, trailing, isHTTPWhiteSpace)
}
/**
* @see https://mimesniff.spec.whatwg.org/#minimize-a-supported-mime-type
* @param {Exclude, 'failure'>} mimeType
*/
function minimizeSupportedMimeType (mimeType) {
switch (mimeType.essence) {
case 'application/ecmascript':
case 'application/javascript':
case 'application/x-ecmascript':
case 'application/x-javascript':
case 'text/ecmascript':
case 'text/javascript':
case 'text/javascript1.0':
case 'text/javascript1.1':
case 'text/javascript1.2':
case 'text/javascript1.3':
case 'text/javascript1.4':
case 'text/javascript1.5':
case 'text/jscript':
case 'text/livescript':
case 'text/x-ecmascript':
case 'text/x-javascript':
// 1. If mimeType is a JavaScript MIME type, then return "text/javascript".
return 'text/javascript'
case 'application/json':
case 'text/json':
// 2. If mimeType is a JSON MIME type, then return "application/json".
return 'application/json'
case 'image/svg+xml':
// 3. If mimeType’s essence is "image/svg+xml", then return "image/svg+xml".
return 'image/svg+xml'
case 'text/xml':
case 'application/xml':
// 4. If mimeType is an XML MIME type, then return "application/xml".
return 'application/xml'
}
// 2. If mimeType is a JSON MIME type, then return "application/json".
if (mimeType.subtype.endsWith('+json')) {
return 'application/json'
}
// 4. If mimeType is an XML MIME type, then return "application/xml".
if (mimeType.subtype.endsWith('+xml')) {
return 'application/xml'
}
// 5. If mimeType is supported by the user agent, then return mimeType’s essence.
// Technically, node doesn't support any mimetypes.
// 6. Return the empty string.
return ''
}
module.exports = {
dataURLProcessor,
URLSerializer,
stringPercentDecode,
parseMIMEType,
collectAnHTTPQuotedString,
serializeAMimeType,
removeHTTPWhitespace,
minimizeSupportedMimeType,
HTTP_TOKEN_CODEPOINTS
}
================================================
FILE: lib/web/fetch/formdata-parser.js
================================================
'use strict'
const { bufferToLowerCasedHeaderName } = require('../../core/util')
const { HTTP_TOKEN_CODEPOINTS } = require('./data-url')
const { makeEntry } = require('./formdata')
const { webidl } = require('../webidl')
const assert = require('node:assert')
const { isomorphicDecode } = require('../infra')
const dd = Buffer.from('--')
const decoder = new TextDecoder()
const decoderIgnoreBOM = new TextDecoder('utf-8', { ignoreBOM: true })
/**
* @param {string} chars
*/
function isAsciiString (chars) {
for (let i = 0; i < chars.length; ++i) {
if ((chars.charCodeAt(i) & ~0x7F) !== 0) {
return false
}
}
return true
}
/**
* @see https://andreubotella.github.io/multipart-form-data/#multipart-form-data-boundary
* @param {string} boundary
*/
function validateBoundary (boundary) {
const length = boundary.length
// - its length is greater or equal to 27 and lesser or equal to 70, and
if (length < 27 || length > 70) {
return false
}
// - it is composed by bytes in the ranges 0x30 to 0x39, 0x41 to 0x5A, or
// 0x61 to 0x7A, inclusive (ASCII alphanumeric), or which are 0x27 ('),
// 0x2D (-) or 0x5F (_).
for (let i = 0; i < length; ++i) {
const cp = boundary.charCodeAt(i)
if (!(
(cp >= 0x30 && cp <= 0x39) ||
(cp >= 0x41 && cp <= 0x5a) ||
(cp >= 0x61 && cp <= 0x7a) ||
cp === 0x27 ||
cp === 0x2d ||
cp === 0x5f
)) {
return false
}
}
return true
}
/**
* @see https://andreubotella.github.io/multipart-form-data/#multipart-form-data-parser
* @param {Buffer} input
* @param {ReturnType} mimeType
*/
function multipartFormDataParser (input, mimeType) {
// 1. Assert: mimeType’s essence is "multipart/form-data".
assert(mimeType !== 'failure' && mimeType.essence === 'multipart/form-data')
const boundaryString = mimeType.parameters.get('boundary')
// 2. If mimeType’s parameters["boundary"] does not exist, return failure.
// Otherwise, let boundary be the result of UTF-8 decoding mimeType’s
// parameters["boundary"].
if (boundaryString === undefined) {
throw parsingError('missing boundary in content-type header')
}
const boundary = Buffer.from(`--${boundaryString}`, 'utf8')
// 3. Let entry list be an empty entry list.
const entryList = []
// 4. Let position be a pointer to a byte in input, initially pointing at
// the first byte.
const position = { position: 0 }
// Note: Per RFC 2046 Section 5.1.1, we must ignore anything before the
// first boundary delimiter line (preamble). Search for the first boundary.
const firstBoundaryIndex = input.indexOf(boundary)
if (firstBoundaryIndex === -1) {
throw parsingError('no boundary found in multipart body')
}
// Start parsing from the first boundary, ignoring any preamble
position.position = firstBoundaryIndex
// 5. While true:
while (true) {
// 5.1. If position points to a sequence of bytes starting with 0x2D 0x2D
// (`--`) followed by boundary, advance position by 2 + the length of
// boundary. Otherwise, return failure.
// Note: boundary is padded with 2 dashes already, no need to add 2.
if (input.subarray(position.position, position.position + boundary.length).equals(boundary)) {
position.position += boundary.length
} else {
throw parsingError('expected a value starting with -- and the boundary')
}
// 5.2. If position points to the sequence of bytes 0x2D 0x2D 0x0D 0x0A
// (`--` followed by CR LF) followed by the end of input, return entry list.
// Note: Per RFC 2046 Section 5.1.1, we must ignore anything after the
// final boundary delimiter (epilogue). Check for -- or --CRLF and return
// regardless of what follows.
if (bufferStartsWith(input, dd, position)) {
// Found closing boundary delimiter (--), ignore any epilogue
return entryList
}
// 5.3. If position does not point to a sequence of bytes starting with 0x0D
// 0x0A (CR LF), return failure.
if (input[position.position] !== 0x0d || input[position.position + 1] !== 0x0a) {
throw parsingError('expected CRLF')
}
// 5.4. Advance position by 2. (This skips past the newline.)
position.position += 2
// 5.5. Let name, filename and contentType be the result of parsing
// multipart/form-data headers on input and position, if the result
// is not failure. Otherwise, return failure.
const result = parseMultipartFormDataHeaders(input, position)
let { name, filename, contentType, encoding } = result
// 5.6. Advance position by 2. (This skips past the empty line that marks
// the end of the headers.)
position.position += 2
// 5.7. Let body be the empty byte sequence.
let body
// 5.8. Body loop: While position is not past the end of input:
// TODO: the steps here are completely wrong
{
const boundaryIndex = input.indexOf(boundary.subarray(2), position.position)
if (boundaryIndex === -1) {
throw parsingError('expected boundary after body')
}
body = input.subarray(position.position, boundaryIndex - 4)
position.position += body.length
// Note: position must be advanced by the body's length before being
// decoded, otherwise the parsing will fail.
if (encoding === 'base64') {
body = Buffer.from(body.toString(), 'base64')
}
}
// 5.9. If position does not point to a sequence of bytes starting with
// 0x0D 0x0A (CR LF), return failure. Otherwise, advance position by 2.
if (input[position.position] !== 0x0d || input[position.position + 1] !== 0x0a) {
throw parsingError('expected CRLF')
} else {
position.position += 2
}
// 5.10. If filename is not null:
let value
if (filename !== null) {
// 5.10.1. If contentType is null, set contentType to "text/plain".
contentType ??= 'text/plain'
// 5.10.2. If contentType is not an ASCII string, set contentType to the empty string.
// Note: `buffer.isAscii` can be used at zero-cost, but converting a string to a buffer is a high overhead.
// Content-Type is a relatively small string, so it is faster to use `String#charCodeAt`.
if (!isAsciiString(contentType)) {
contentType = ''
}
// 5.10.3. Let value be a new File object with name filename, type contentType, and body body.
value = new File([body], filename, { type: contentType })
} else {
// 5.11. Otherwise:
// 5.11.1. Let value be the UTF-8 decoding without BOM of body.
value = decoderIgnoreBOM.decode(Buffer.from(body))
}
// 5.12. Assert: name is a scalar value string and value is either a scalar value string or a File object.
assert(webidl.is.USVString(name))
assert((typeof value === 'string' && webidl.is.USVString(value)) || webidl.is.File(value))
// 5.13. Create an entry with name and value, and append it to entry list.
entryList.push(makeEntry(name, value, filename))
}
}
/**
* Parses content-disposition attributes (e.g., name="value" or filename*=utf-8''encoded)
* @param {Buffer} input
* @param {{ position: number }} position
* @returns {{ name: string, value: string }}
*/
function parseContentDispositionAttribute (input, position) {
// Skip leading semicolon and whitespace
if (input[position.position] === 0x3b /* ; */) {
position.position++
}
// Skip whitespace
collectASequenceOfBytes(
(char) => char === 0x20 || char === 0x09,
input,
position
)
// Collect attribute name (token characters)
const attributeName = collectASequenceOfBytes(
(char) => isToken(char) && char !== 0x3d && char !== 0x2a, // not = or *
input,
position
)
if (attributeName.length === 0) {
return null
}
const attrNameStr = attributeName.toString('ascii').toLowerCase()
// Check for extended notation (attribute*)
const isExtended = input[position.position] === 0x2a /* * */
if (isExtended) {
position.position++ // skip *
}
// Expect = sign
if (input[position.position] !== 0x3d /* = */) {
return null
}
position.position++ // skip =
// Skip whitespace
collectASequenceOfBytes(
(char) => char === 0x20 || char === 0x09,
input,
position
)
let value
if (isExtended) {
// Extended attribute format: charset'language'encoded-value
const headerValue = collectASequenceOfBytes(
(char) => char !== 0x20 && char !== 0x0d && char !== 0x0a && char !== 0x3b, // not space, CRLF, or ;
input,
position
)
// Check for utf-8'' prefix (case insensitive)
if (
(headerValue[0] !== 0x75 && headerValue[0] !== 0x55) || // u or U
(headerValue[1] !== 0x74 && headerValue[1] !== 0x54) || // t or T
(headerValue[2] !== 0x66 && headerValue[2] !== 0x46) || // f or F
headerValue[3] !== 0x2d || // -
headerValue[4] !== 0x38 // 8
) {
throw parsingError('unknown encoding, expected utf-8\'\'')
}
// Skip utf-8'' and decode the rest
value = decodeURIComponent(decoder.decode(headerValue.subarray(7)))
} else if (input[position.position] === 0x22 /* " */) {
// Quoted string
position.position++ // skip opening quote
const quotedValue = collectASequenceOfBytes(
(char) => char !== 0x0a && char !== 0x0d && char !== 0x22, // not LF, CR, or "
input,
position
)
if (input[position.position] !== 0x22) {
throw parsingError('Closing quote not found')
}
position.position++ // skip closing quote
value = decoder.decode(quotedValue)
.replace(/%0A/ig, '\n')
.replace(/%0D/ig, '\r')
.replace(/%22/g, '"')
} else {
// Token value (no quotes)
const tokenValue = collectASequenceOfBytes(
(char) => isToken(char) && char !== 0x3b, // not ;
input,
position
)
value = decoder.decode(tokenValue)
}
return { name: attrNameStr, value }
}
/**
* @see https://andreubotella.github.io/multipart-form-data/#parse-multipart-form-data-headers
* @param {Buffer} input
* @param {{ position: number }} position
*/
function parseMultipartFormDataHeaders (input, position) {
// 1. Let name, filename and contentType be null.
let name = null
let filename = null
let contentType = null
let encoding = null
// 2. While true:
while (true) {
// 2.1. If position points to a sequence of bytes starting with 0x0D 0x0A (CR LF):
if (input[position.position] === 0x0d && input[position.position + 1] === 0x0a) {
// 2.1.1. If name is null, return failure.
if (name === null) {
throw parsingError('header name is null')
}
// 2.1.2. Return name, filename and contentType.
return { name, filename, contentType, encoding }
}
// 2.2. Let header name be the result of collecting a sequence of bytes that are
// not 0x0A (LF), 0x0D (CR) or 0x3A (:), given position.
let headerName = collectASequenceOfBytes(
(char) => char !== 0x0a && char !== 0x0d && char !== 0x3a,
input,
position
)
// 2.3. Remove any HTTP tab or space bytes from the start or end of header name.
headerName = removeChars(headerName, true, true, (char) => char === 0x9 || char === 0x20)
// 2.4. If header name does not match the field-name token production, return failure.
if (!HTTP_TOKEN_CODEPOINTS.test(headerName.toString())) {
throw parsingError('header name does not match the field-name token production')
}
// 2.5. If the byte at position is not 0x3A (:), return failure.
if (input[position.position] !== 0x3a) {
throw parsingError('expected :')
}
// 2.6. Advance position by 1.
position.position++
// 2.7. Collect a sequence of bytes that are HTTP tab or space bytes given position.
// (Do nothing with those bytes.)
collectASequenceOfBytes(
(char) => char === 0x20 || char === 0x09,
input,
position
)
// 2.8. Byte-lowercase header name and switch on the result:
switch (bufferToLowerCasedHeaderName(headerName)) {
case 'content-disposition': {
name = filename = null
// Collect the disposition type (should be "form-data")
const dispositionType = collectASequenceOfBytes(
(char) => isToken(char),
input,
position
)
if (dispositionType.toString('ascii').toLowerCase() !== 'form-data') {
throw parsingError('expected form-data for content-disposition header')
}
// Parse attributes recursively until CRLF
while (
position.position < input.length &&
input[position.position] !== 0x0d &&
input[position.position + 1] !== 0x0a
) {
const attribute = parseContentDispositionAttribute(input, position)
if (!attribute) {
break
}
if (attribute.name === 'name') {
name = attribute.value
} else if (attribute.name === 'filename') {
filename = attribute.value
}
}
if (name === null) {
throw parsingError('name attribute is required in content-disposition header')
}
break
}
case 'content-type': {
// 1. Let header value be the result of collecting a sequence of bytes that are
// not 0x0A (LF) or 0x0D (CR), given position.
let headerValue = collectASequenceOfBytes(
(char) => char !== 0x0a && char !== 0x0d,
input,
position
)
// 2. Remove any HTTP tab or space bytes from the end of header value.
headerValue = removeChars(headerValue, false, true, (char) => char === 0x9 || char === 0x20)
// 3. Set contentType to the isomorphic decoding of header value.
contentType = isomorphicDecode(headerValue)
break
}
case 'content-transfer-encoding': {
let headerValue = collectASequenceOfBytes(
(char) => char !== 0x0a && char !== 0x0d,
input,
position
)
headerValue = removeChars(headerValue, false, true, (char) => char === 0x9 || char === 0x20)
encoding = isomorphicDecode(headerValue)
break
}
default: {
// Collect a sequence of bytes that are not 0x0A (LF) or 0x0D (CR), given position.
// (Do nothing with those bytes.)
collectASequenceOfBytes(
(char) => char !== 0x0a && char !== 0x0d,
input,
position
)
}
}
// 2.9. If position does not point to a sequence of bytes starting with 0x0D 0x0A
// (CR LF), return failure. Otherwise, advance position by 2 (past the newline).
if (input[position.position] !== 0x0d && input[position.position + 1] !== 0x0a) {
throw parsingError('expected CRLF')
} else {
position.position += 2
}
}
}
/**
* @param {(char: number) => boolean} condition
* @param {Buffer} input
* @param {{ position: number }} position
*/
function collectASequenceOfBytes (condition, input, position) {
let start = position.position
while (start < input.length && condition(input[start])) {
++start
}
return input.subarray(position.position, (position.position = start))
}
/**
* @param {Buffer} buf
* @param {boolean} leading
* @param {boolean} trailing
* @param {(charCode: number) => boolean} predicate
* @returns {Buffer}
*/
function removeChars (buf, leading, trailing, predicate) {
let lead = 0
let trail = buf.length - 1
if (leading) {
while (lead < buf.length && predicate(buf[lead])) lead++
}
if (trailing) {
while (trail > 0 && predicate(buf[trail])) trail--
}
return lead === 0 && trail === buf.length - 1 ? buf : buf.subarray(lead, trail + 1)
}
/**
* Checks if {@param buffer} starts with {@param start}
* @param {Buffer} buffer
* @param {Buffer} start
* @param {{ position: number }} position
*/
function bufferStartsWith (buffer, start, position) {
if (buffer.length < start.length) {
return false
}
for (let i = 0; i < start.length; i++) {
if (start[i] !== buffer[position.position + i]) {
return false
}
}
return true
}
function parsingError (cause) {
return new TypeError('Failed to parse body as FormData.', { cause: new TypeError(cause) })
}
/**
* CTL =
* @param {number} char
*/
function isCTL (char) {
return char <= 0x1f || char === 0x7f
}
/**
* tspecials := "(" / ")" / "<" / ">" / "@" /
* "," / ";" / ":" / "\" / <">
* "/" / "[" / "]" / "?" / "="
* ; Must be in quoted-string,
* ; to use within parameter values
* @param {number} char
*/
function isTSpecial (char) {
return (
char === 0x28 || // (
char === 0x29 || // )
char === 0x3c || // <
char === 0x3e || // >
char === 0x40 || // @
char === 0x2c || // ,
char === 0x3b || // ;
char === 0x3a || // :
char === 0x5c || // \
char === 0x22 || // "
char === 0x2f || // /
char === 0x5b || // [
char === 0x5d || // ]
char === 0x3f || // ?
char === 0x3d // +
)
}
/**
* token := 1*
* @param {number} char
*/
function isToken (char) {
return (
char <= 0x7f && // ascii
char !== 0x20 && // space
char !== 0x09 &&
!isCTL(char) &&
!isTSpecial(char)
)
}
module.exports = {
multipartFormDataParser,
validateBoundary
}
================================================
FILE: lib/web/fetch/formdata.js
================================================
'use strict'
const { iteratorMixin } = require('./util')
const { kEnumerableProperty } = require('../../core/util')
const { webidl } = require('../webidl')
const nodeUtil = require('node:util')
// https://xhr.spec.whatwg.org/#formdata
class FormData {
#state = []
constructor (form = undefined) {
webidl.util.markAsUncloneable(this)
if (form !== undefined) {
throw webidl.errors.conversionFailed({
prefix: 'FormData constructor',
argument: 'Argument 1',
types: ['undefined']
})
}
}
append (name, value, filename = undefined) {
webidl.brandCheck(this, FormData)
const prefix = 'FormData.append'
webidl.argumentLengthCheck(arguments, 2, prefix)
name = webidl.converters.USVString(name)
if (arguments.length === 3 || webidl.is.Blob(value)) {
value = webidl.converters.Blob(value, prefix, 'value')
if (filename !== undefined) {
filename = webidl.converters.USVString(filename)
}
} else {
value = webidl.converters.USVString(value)
}
// 1. Let value be value if given; otherwise blobValue.
// 2. Let entry be the result of creating an entry with
// name, value, and filename if given.
const entry = makeEntry(name, value, filename)
// 3. Append entry to this’s entry list.
this.#state.push(entry)
}
delete (name) {
webidl.brandCheck(this, FormData)
const prefix = 'FormData.delete'
webidl.argumentLengthCheck(arguments, 1, prefix)
name = webidl.converters.USVString(name)
// The delete(name) method steps are to remove all entries whose name
// is name from this’s entry list.
this.#state = this.#state.filter(entry => entry.name !== name)
}
get (name) {
webidl.brandCheck(this, FormData)
const prefix = 'FormData.get'
webidl.argumentLengthCheck(arguments, 1, prefix)
name = webidl.converters.USVString(name)
// 1. If there is no entry whose name is name in this’s entry list,
// then return null.
const idx = this.#state.findIndex((entry) => entry.name === name)
if (idx === -1) {
return null
}
// 2. Return the value of the first entry whose name is name from
// this’s entry list.
return this.#state[idx].value
}
getAll (name) {
webidl.brandCheck(this, FormData)
const prefix = 'FormData.getAll'
webidl.argumentLengthCheck(arguments, 1, prefix)
name = webidl.converters.USVString(name)
// 1. If there is no entry whose name is name in this’s entry list,
// then return the empty list.
// 2. Return the values of all entries whose name is name, in order,
// from this’s entry list.
return this.#state
.filter((entry) => entry.name === name)
.map((entry) => entry.value)
}
has (name) {
webidl.brandCheck(this, FormData)
const prefix = 'FormData.has'
webidl.argumentLengthCheck(arguments, 1, prefix)
name = webidl.converters.USVString(name)
// The has(name) method steps are to return true if there is an entry
// whose name is name in this’s entry list; otherwise false.
return this.#state.findIndex((entry) => entry.name === name) !== -1
}
set (name, value, filename = undefined) {
webidl.brandCheck(this, FormData)
const prefix = 'FormData.set'
webidl.argumentLengthCheck(arguments, 2, prefix)
name = webidl.converters.USVString(name)
if (arguments.length === 3 || webidl.is.Blob(value)) {
value = webidl.converters.Blob(value, prefix, 'value')
if (filename !== undefined) {
filename = webidl.converters.USVString(filename)
}
} else {
value = webidl.converters.USVString(value)
}
// The set(name, value) and set(name, blobValue, filename) method steps
// are:
// 1. Let value be value if given; otherwise blobValue.
// 2. Let entry be the result of creating an entry with name, value, and
// filename if given.
const entry = makeEntry(name, value, filename)
// 3. If there are entries in this’s entry list whose name is name, then
// replace the first such entry with entry and remove the others.
const idx = this.#state.findIndex((entry) => entry.name === name)
if (idx !== -1) {
this.#state = [
...this.#state.slice(0, idx),
entry,
...this.#state.slice(idx + 1).filter((entry) => entry.name !== name)
]
} else {
// 4. Otherwise, append entry to this’s entry list.
this.#state.push(entry)
}
}
[nodeUtil.inspect.custom] (depth, options) {
const state = this.#state.reduce((a, b) => {
if (a[b.name]) {
if (Array.isArray(a[b.name])) {
a[b.name].push(b.value)
} else {
a[b.name] = [a[b.name], b.value]
}
} else {
a[b.name] = b.value
}
return a
}, { __proto__: null })
options.depth ??= depth
options.colors ??= true
const output = nodeUtil.formatWithOptions(options, state)
// remove [Object null prototype]
return `FormData ${output.slice(output.indexOf(']') + 2)}`
}
/**
* @param {FormData} formData
*/
static getFormDataState (formData) {
return formData.#state
}
/**
* @param {FormData} formData
* @param {any[]} newState
*/
static setFormDataState (formData, newState) {
formData.#state = newState
}
}
const { getFormDataState, setFormDataState } = FormData
Reflect.deleteProperty(FormData, 'getFormDataState')
Reflect.deleteProperty(FormData, 'setFormDataState')
iteratorMixin('FormData', FormData, getFormDataState, 'name', 'value')
Object.defineProperties(FormData.prototype, {
append: kEnumerableProperty,
delete: kEnumerableProperty,
get: kEnumerableProperty,
getAll: kEnumerableProperty,
has: kEnumerableProperty,
set: kEnumerableProperty,
[Symbol.toStringTag]: {
value: 'FormData',
configurable: true
}
})
/**
* @see https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#create-an-entry
* @param {string} name
* @param {string|Blob} value
* @param {?string} filename
* @returns
*/
function makeEntry (name, value, filename) {
// 1. Set name to the result of converting name into a scalar value string.
// Note: This operation was done by the webidl converter USVString.
// 2. If value is a string, then set value to the result of converting
// value into a scalar value string.
if (typeof value === 'string') {
// Note: This operation was done by the webidl converter USVString.
} else {
// 3. Otherwise:
// 1. If value is not a File object, then set value to a new File object,
// representing the same bytes, whose name attribute value is "blob"
if (!webidl.is.File(value)) {
value = new File([value], 'blob', { type: value.type })
}
// 2. If filename is given, then set value to a new File object,
// representing the same bytes, whose name attribute is filename.
if (filename !== undefined) {
/** @type {FilePropertyBag} */
const options = {
type: value.type,
lastModified: value.lastModified
}
value = new File([value], filename, options)
}
}
// 4. Return an entry whose name is name and whose value is value.
return { name, value }
}
webidl.is.FormData = webidl.util.MakeTypeAssertion(FormData)
module.exports = { FormData, makeEntry, setFormDataState }
================================================
FILE: lib/web/fetch/global.js
================================================
'use strict'
// In case of breaking changes, increase the version
// number to avoid conflicts.
const globalOrigin = Symbol.for('undici.globalOrigin.1')
function getGlobalOrigin () {
return globalThis[globalOrigin]
}
function setGlobalOrigin (newOrigin) {
if (newOrigin === undefined) {
Object.defineProperty(globalThis, globalOrigin, {
value: undefined,
writable: true,
enumerable: false,
configurable: false
})
return
}
const parsedURL = new URL(newOrigin)
if (parsedURL.protocol !== 'http:' && parsedURL.protocol !== 'https:') {
throw new TypeError(`Only http & https urls are allowed, received ${parsedURL.protocol}`)
}
Object.defineProperty(globalThis, globalOrigin, {
value: parsedURL,
writable: true,
enumerable: false,
configurable: false
})
}
module.exports = {
getGlobalOrigin,
setGlobalOrigin
}
================================================
FILE: lib/web/fetch/headers.js
================================================
// https://github.com/Ethan-Arrowood/undici-fetch
'use strict'
const { kConstruct } = require('../../core/symbols')
const { kEnumerableProperty } = require('../../core/util')
const {
iteratorMixin,
isValidHeaderName,
isValidHeaderValue
} = require('./util')
const { webidl } = require('../webidl')
const assert = require('node:assert')
const util = require('node:util')
/**
* @param {number} code
* @returns {code is (0x0a | 0x0d | 0x09 | 0x20)}
*/
function isHTTPWhiteSpaceCharCode (code) {
return code === 0x0a || code === 0x0d || code === 0x09 || code === 0x20
}
/**
* @see https://fetch.spec.whatwg.org/#concept-header-value-normalize
* @param {string} potentialValue
* @returns {string}
*/
function headerValueNormalize (potentialValue) {
// To normalize a byte sequence potentialValue, remove
// any leading and trailing HTTP whitespace bytes from
// potentialValue.
let i = 0; let j = potentialValue.length
while (j > i && isHTTPWhiteSpaceCharCode(potentialValue.charCodeAt(j - 1))) --j
while (j > i && isHTTPWhiteSpaceCharCode(potentialValue.charCodeAt(i))) ++i
return i === 0 && j === potentialValue.length ? potentialValue : potentialValue.substring(i, j)
}
/**
* @param {Headers} headers
* @param {Array|Object} object
*/
function fill (headers, object) {
// To fill a Headers object headers with a given object object, run these steps:
// 1. If object is a sequence, then for each header in object:
// Note: webidl conversion to array has already been done.
if (Array.isArray(object)) {
for (let i = 0; i < object.length; ++i) {
const header = object[i]
// 1. If header does not contain exactly two items, then throw a TypeError.
if (header.length !== 2) {
throw webidl.errors.exception({
header: 'Headers constructor',
message: `expected name/value pair to be length 2, found ${header.length}.`
})
}
// 2. Append (header’s first item, header’s second item) to headers.
appendHeader(headers, header[0], header[1])
}
} else if (typeof object === 'object' && object !== null) {
// Note: null should throw
// 2. Otherwise, object is a record, then for each key → value in object,
// append (key, value) to headers
const keys = Object.keys(object)
for (let i = 0; i < keys.length; ++i) {
appendHeader(headers, keys[i], object[keys[i]])
}
} else {
throw webidl.errors.conversionFailed({
prefix: 'Headers constructor',
argument: 'Argument 1',
types: ['sequence>', 'record']
})
}
}
/**
* @see https://fetch.spec.whatwg.org/#concept-headers-append
* @param {Headers} headers
* @param {string} name
* @param {string} value
*/
function appendHeader (headers, name, value) {
// 1. Normalize value.
value = headerValueNormalize(value)
// 2. If name is not a header name or value is not a
// header value, then throw a TypeError.
if (!isValidHeaderName(name)) {
throw webidl.errors.invalidArgument({
prefix: 'Headers.append',
value: name,
type: 'header name'
})
} else if (!isValidHeaderValue(value)) {
throw webidl.errors.invalidArgument({
prefix: 'Headers.append',
value,
type: 'header value'
})
}
// 3. If headers’s guard is "immutable", then throw a TypeError.
// 4. Otherwise, if headers’s guard is "request" and name is a
// forbidden header name, return.
// 5. Otherwise, if headers’s guard is "request-no-cors":
// TODO
// Note: undici does not implement forbidden header names
if (getHeadersGuard(headers) === 'immutable') {
throw new TypeError('immutable')
}
// 6. Otherwise, if headers’s guard is "response" and name is a
// forbidden response-header name, return.
// 7. Append (name, value) to headers’s header list.
return getHeadersList(headers).append(name, value, false)
// 8. If headers’s guard is "request-no-cors", then remove
// privileged no-CORS request headers from headers
}
// https://fetch.spec.whatwg.org/#concept-header-list-sort-and-combine
/**
* @param {Headers} target
*/
function headersListSortAndCombine (target) {
const headersList = getHeadersList(target)
if (!headersList) {
return []
}
if (headersList.sortedMap) {
return headersList.sortedMap
}
// 1. Let headers be an empty list of headers with the key being the name
// and value the value.
const headers = []
// 2. Let names be the result of convert header names to a sorted-lowercase
// set with all the names of the headers in list.
const names = headersList.toSortedArray()
const cookies = headersList.cookies
// fast-path
if (cookies === null || cookies.length === 1) {
// Note: The non-null assertion of value has already been done by `HeadersList#toSortedArray`
return (headersList.sortedMap = names)
}
// 3. For each name of names:
for (let i = 0; i < names.length; ++i) {
const { 0: name, 1: value } = names[i]
// 1. If name is `set-cookie`, then:
if (name === 'set-cookie') {
// 1. Let values be a list of all values of headers in list whose name
// is a byte-case-insensitive match for name, in order.
// 2. For each value of values:
// 1. Append (name, value) to headers.
for (let j = 0; j < cookies.length; ++j) {
headers.push([name, cookies[j]])
}
} else {
// 2. Otherwise:
// 1. Let value be the result of getting name from list.
// 2. Assert: value is non-null.
// Note: This operation was done by `HeadersList#toSortedArray`.
// 3. Append (name, value) to headers.
headers.push([name, value])
}
}
// 4. Return headers.
return (headersList.sortedMap = headers)
}
function compareHeaderName (a, b) {
return a[0] < b[0] ? -1 : 1
}
class HeadersList {
/** @type {[string, string][]|null} */
cookies = null
sortedMap
headersMap
constructor (init) {
if (init instanceof HeadersList) {
this.headersMap = new Map(init.headersMap)
this.sortedMap = init.sortedMap
this.cookies = init.cookies === null ? null : [...init.cookies]
} else {
this.headersMap = new Map(init)
this.sortedMap = null
}
}
/**
* @see https://fetch.spec.whatwg.org/#header-list-contains
* @param {string} name
* @param {boolean} isLowerCase
*/
contains (name, isLowerCase) {
// A header list list contains a header name name if list
// contains a header whose name is a byte-case-insensitive
// match for name.
return this.headersMap.has(isLowerCase ? name : name.toLowerCase())
}
clear () {
this.headersMap.clear()
this.sortedMap = null
this.cookies = null
}
/**
* @see https://fetch.spec.whatwg.org/#concept-header-list-append
* @param {string} name
* @param {string} value
* @param {boolean} isLowerCase
*/
append (name, value, isLowerCase) {
this.sortedMap = null
// 1. If list contains name, then set name to the first such
// header’s name.
const lowercaseName = isLowerCase ? name : name.toLowerCase()
const exists = this.headersMap.get(lowercaseName)
// 2. Append (name, value) to list.
if (exists) {
const delimiter = lowercaseName === 'cookie' ? '; ' : ', '
this.headersMap.set(lowercaseName, {
name: exists.name,
value: `${exists.value}${delimiter}${value}`
})
} else {
this.headersMap.set(lowercaseName, { name, value })
}
if (lowercaseName === 'set-cookie') {
(this.cookies ??= []).push(value)
}
}
/**
* @see https://fetch.spec.whatwg.org/#concept-header-list-set
* @param {string} name
* @param {string} value
* @param {boolean} isLowerCase
*/
set (name, value, isLowerCase) {
this.sortedMap = null
const lowercaseName = isLowerCase ? name : name.toLowerCase()
if (lowercaseName === 'set-cookie') {
this.cookies = [value]
}
// 1. If list contains name, then set the value of
// the first such header to value and remove the
// others.
// 2. Otherwise, append header (name, value) to list.
this.headersMap.set(lowercaseName, { name, value })
}
/**
* @see https://fetch.spec.whatwg.org/#concept-header-list-delete
* @param {string} name
* @param {boolean} isLowerCase
*/
delete (name, isLowerCase) {
this.sortedMap = null
if (!isLowerCase) name = name.toLowerCase()
if (name === 'set-cookie') {
this.cookies = null
}
this.headersMap.delete(name)
}
/**
* @see https://fetch.spec.whatwg.org/#concept-header-list-get
* @param {string} name
* @param {boolean} isLowerCase
* @returns {string | null}
*/
get (name, isLowerCase) {
// 1. If list does not contain name, then return null.
// 2. Return the values of all headers in list whose name
// is a byte-case-insensitive match for name,
// separated from each other by 0x2C 0x20, in order.
return this.headersMap.get(isLowerCase ? name : name.toLowerCase())?.value ?? null
}
* [Symbol.iterator] () {
// use the lowercased name
for (const { 0: name, 1: { value } } of this.headersMap) {
yield [name, value]
}
}
get entries () {
const headers = {}
if (this.headersMap.size !== 0) {
for (const { name, value } of this.headersMap.values()) {
headers[name] = value
}
}
return headers
}
rawValues () {
return this.headersMap.values()
}
get entriesList () {
const headers = []
if (this.headersMap.size !== 0) {
for (const { 0: lowerName, 1: { name, value } } of this.headersMap) {
if (lowerName === 'set-cookie') {
for (const cookie of this.cookies) {
headers.push([name, cookie])
}
} else {
headers.push([name, value])
}
}
}
return headers
}
// https://fetch.spec.whatwg.org/#convert-header-names-to-a-sorted-lowercase-set
toSortedArray () {
const size = this.headersMap.size
const array = new Array(size)
// In most cases, you will use the fast-path.
// fast-path: Use binary insertion sort for small arrays.
if (size <= 32) {
if (size === 0) {
// If empty, it is an empty array. To avoid the first index assignment.
return array
}
// Improve performance by unrolling loop and avoiding double-loop.
// Double-loop-less version of the binary insertion sort.
const iterator = this.headersMap[Symbol.iterator]()
const firstValue = iterator.next().value
// set [name, value] to first index.
array[0] = [firstValue[0], firstValue[1].value]
// https://fetch.spec.whatwg.org/#concept-header-list-sort-and-combine
// 3.2.2. Assert: value is non-null.
assert(firstValue[1].value !== null)
for (
let i = 1, j = 0, right = 0, left = 0, pivot = 0, x, value;
i < size;
++i
) {
// get next value
value = iterator.next().value
// set [name, value] to current index.
x = array[i] = [value[0], value[1].value]
// https://fetch.spec.whatwg.org/#concept-header-list-sort-and-combine
// 3.2.2. Assert: value is non-null.
assert(x[1] !== null)
left = 0
right = i
// binary search
while (left < right) {
// middle index
pivot = left + ((right - left) >> 1)
// compare header name
if (array[pivot][0] <= x[0]) {
left = pivot + 1
} else {
right = pivot
}
}
if (i !== pivot) {
j = i
while (j > left) {
array[j] = array[--j]
}
array[left] = x
}
}
/* c8 ignore next 4 */
if (!iterator.next().done) {
// This is for debugging and will never be called.
throw new TypeError('Unreachable')
}
return array
} else {
// This case would be a rare occurrence.
// slow-path: fallback
let i = 0
for (const { 0: name, 1: { value } } of this.headersMap) {
array[i++] = [name, value]
// https://fetch.spec.whatwg.org/#concept-header-list-sort-and-combine
// 3.2.2. Assert: value is non-null.
assert(value !== null)
}
return array.sort(compareHeaderName)
}
}
}
// https://fetch.spec.whatwg.org/#headers-class
class Headers {
#guard
/**
* @type {HeadersList}
*/
#headersList
/**
* @param {HeadersInit|Symbol} [init]
* @returns
*/
constructor (init = undefined) {
webidl.util.markAsUncloneable(this)
if (init === kConstruct) {
return
}
this.#headersList = new HeadersList()
// The new Headers(init) constructor steps are:
// 1. Set this’s guard to "none".
this.#guard = 'none'
// 2. If init is given, then fill this with init.
if (init !== undefined) {
init = webidl.converters.HeadersInit(init, 'Headers constructor', 'init')
fill(this, init)
}
}
// https://fetch.spec.whatwg.org/#dom-headers-append
append (name, value) {
webidl.brandCheck(this, Headers)
webidl.argumentLengthCheck(arguments, 2, 'Headers.append')
const prefix = 'Headers.append'
name = webidl.converters.ByteString(name, prefix, 'name')
value = webidl.converters.ByteString(value, prefix, 'value')
return appendHeader(this, name, value)
}
// https://fetch.spec.whatwg.org/#dom-headers-delete
delete (name) {
webidl.brandCheck(this, Headers)
webidl.argumentLengthCheck(arguments, 1, 'Headers.delete')
const prefix = 'Headers.delete'
name = webidl.converters.ByteString(name, prefix, 'name')
// 1. If name is not a header name, then throw a TypeError.
if (!isValidHeaderName(name)) {
throw webidl.errors.invalidArgument({
prefix: 'Headers.delete',
value: name,
type: 'header name'
})
}
// 2. If this’s guard is "immutable", then throw a TypeError.
// 3. Otherwise, if this’s guard is "request" and name is a
// forbidden header name, return.
// 4. Otherwise, if this’s guard is "request-no-cors", name
// is not a no-CORS-safelisted request-header name, and
// name is not a privileged no-CORS request-header name,
// return.
// 5. Otherwise, if this’s guard is "response" and name is
// a forbidden response-header name, return.
// Note: undici does not implement forbidden header names
if (this.#guard === 'immutable') {
throw new TypeError('immutable')
}
// 6. If this’s header list does not contain name, then
// return.
if (!this.#headersList.contains(name, false)) {
return
}
// 7. Delete name from this’s header list.
// 8. If this’s guard is "request-no-cors", then remove
// privileged no-CORS request headers from this.
this.#headersList.delete(name, false)
}
// https://fetch.spec.whatwg.org/#dom-headers-get
get (name) {
webidl.brandCheck(this, Headers)
webidl.argumentLengthCheck(arguments, 1, 'Headers.get')
const prefix = 'Headers.get'
name = webidl.converters.ByteString(name, prefix, 'name')
// 1. If name is not a header name, then throw a TypeError.
if (!isValidHeaderName(name)) {
throw webidl.errors.invalidArgument({
prefix,
value: name,
type: 'header name'
})
}
// 2. Return the result of getting name from this’s header
// list.
return this.#headersList.get(name, false)
}
// https://fetch.spec.whatwg.org/#dom-headers-has
has (name) {
webidl.brandCheck(this, Headers)
webidl.argumentLengthCheck(arguments, 1, 'Headers.has')
const prefix = 'Headers.has'
name = webidl.converters.ByteString(name, prefix, 'name')
// 1. If name is not a header name, then throw a TypeError.
if (!isValidHeaderName(name)) {
throw webidl.errors.invalidArgument({
prefix,
value: name,
type: 'header name'
})
}
// 2. Return true if this’s header list contains name;
// otherwise false.
return this.#headersList.contains(name, false)
}
// https://fetch.spec.whatwg.org/#dom-headers-set
set (name, value) {
webidl.brandCheck(this, Headers)
webidl.argumentLengthCheck(arguments, 2, 'Headers.set')
const prefix = 'Headers.set'
name = webidl.converters.ByteString(name, prefix, 'name')
value = webidl.converters.ByteString(value, prefix, 'value')
// 1. Normalize value.
value = headerValueNormalize(value)
// 2. If name is not a header name or value is not a
// header value, then throw a TypeError.
if (!isValidHeaderName(name)) {
throw webidl.errors.invalidArgument({
prefix,
value: name,
type: 'header name'
})
} else if (!isValidHeaderValue(value)) {
throw webidl.errors.invalidArgument({
prefix,
value,
type: 'header value'
})
}
// 3. If this’s guard is "immutable", then throw a TypeError.
// 4. Otherwise, if this’s guard is "request" and name is a
// forbidden header name, return.
// 5. Otherwise, if this’s guard is "request-no-cors" and
// name/value is not a no-CORS-safelisted request-header,
// return.
// 6. Otherwise, if this’s guard is "response" and name is a
// forbidden response-header name, return.
// Note: undici does not implement forbidden header names
if (this.#guard === 'immutable') {
throw new TypeError('immutable')
}
// 7. Set (name, value) in this’s header list.
// 8. If this’s guard is "request-no-cors", then remove
// privileged no-CORS request headers from this
this.#headersList.set(name, value, false)
}
// https://fetch.spec.whatwg.org/#dom-headers-getsetcookie
getSetCookie () {
webidl.brandCheck(this, Headers)
// 1. If this’s header list does not contain `Set-Cookie`, then return « ».
// 2. Return the values of all headers in this’s header list whose name is
// a byte-case-insensitive match for `Set-Cookie`, in order.
const list = this.#headersList.cookies
if (list) {
return [...list]
}
return []
}
[util.inspect.custom] (depth, options) {
options.depth ??= depth
return `Headers ${util.formatWithOptions(options, this.#headersList.entries)}`
}
static getHeadersGuard (o) {
return o.#guard
}
static setHeadersGuard (o, guard) {
o.#guard = guard
}
/**
* @param {Headers} o
*/
static getHeadersList (o) {
return o.#headersList
}
/**
* @param {Headers} target
* @param {HeadersList} list
*/
static setHeadersList (target, list) {
target.#headersList = list
}
}
const { getHeadersGuard, setHeadersGuard, getHeadersList, setHeadersList } = Headers
Reflect.deleteProperty(Headers, 'getHeadersGuard')
Reflect.deleteProperty(Headers, 'setHeadersGuard')
Reflect.deleteProperty(Headers, 'getHeadersList')
Reflect.deleteProperty(Headers, 'setHeadersList')
iteratorMixin('Headers', Headers, headersListSortAndCombine, 0, 1)
Object.defineProperties(Headers.prototype, {
append: kEnumerableProperty,
delete: kEnumerableProperty,
get: kEnumerableProperty,
has: kEnumerableProperty,
set: kEnumerableProperty,
getSetCookie: kEnumerableProperty,
[Symbol.toStringTag]: {
value: 'Headers',
configurable: true
},
[util.inspect.custom]: {
enumerable: false
}
})
webidl.converters.HeadersInit = function (V, prefix, argument) {
if (webidl.util.Type(V) === webidl.util.Types.OBJECT) {
const iterator = Reflect.get(V, Symbol.iterator)
// A work-around to ensure we send the properly-cased Headers when V is a Headers object.
// Read https://github.com/nodejs/undici/pull/3159#issuecomment-2075537226 before touching, please.
if (!util.types.isProxy(V) && iterator === Headers.prototype.entries) { // Headers object
try {
return getHeadersList(V).entriesList
} catch {
// fall-through
}
}
if (typeof iterator === 'function') {
return webidl.converters['sequence>'](V, prefix, argument, iterator.bind(V))
}
return webidl.converters['record'](V, prefix, argument)
}
throw webidl.errors.conversionFailed({
prefix: 'Headers constructor',
argument: 'Argument 1',
types: ['sequence>', 'record']
})
}
module.exports = {
fill,
// for test.
compareHeaderName,
Headers,
HeadersList,
getHeadersGuard,
setHeadersGuard,
setHeadersList,
getHeadersList
}
================================================
FILE: lib/web/fetch/index.js
================================================
// https://github.com/Ethan-Arrowood/undici-fetch
'use strict'
const {
makeNetworkError,
makeAppropriateNetworkError,
filterResponse,
makeResponse,
fromInnerResponse,
getResponseState
} = require('./response')
const { HeadersList } = require('./headers')
const { Request, cloneRequest, getRequestDispatcher, getRequestState } = require('./request')
const zlib = require('node:zlib')
const {
makePolicyContainer,
clonePolicyContainer,
requestBadPort,
TAOCheck,
appendRequestOriginHeader,
responseLocationURL,
requestCurrentURL,
setRequestReferrerPolicyOnRedirect,
tryUpgradeRequestToAPotentiallyTrustworthyURL,
createOpaqueTimingInfo,
appendFetchMetadata,
corsCheck,
crossOriginResourcePolicyCheck,
determineRequestsReferrer,
coarsenedSharedCurrentTime,
sameOrigin,
isCancelled,
isAborted,
isErrorLike,
fullyReadBody,
readableStreamClose,
urlIsLocal,
urlIsHttpHttpsScheme,
urlHasHttpsScheme,
clampAndCoarsenConnectionTimingInfo,
simpleRangeHeaderValue,
buildContentRange,
createInflate,
extractMimeType,
hasAuthenticationEntry,
includesCredentials,
isTraversableNavigable
} = require('./util')
const assert = require('node:assert')
const { safelyExtractBody, extractBody } = require('./body')
const {
redirectStatusSet,
nullBodyStatus,
safeMethodsSet,
requestBodyHeader,
subresourceSet
} = require('./constants')
const EE = require('node:events')
const { Readable, pipeline, finished, isErrored, isReadable } = require('node:stream')
const { addAbortListener, bufferToLowerCasedHeaderName } = require('../../core/util')
const { dataURLProcessor, serializeAMimeType, minimizeSupportedMimeType } = require('./data-url')
const { getGlobalDispatcher } = require('../../global')
const { webidl } = require('../webidl')
const { STATUS_CODES } = require('node:http')
const { bytesMatch } = require('../subresource-integrity/subresource-integrity')
const { createDeferredPromise } = require('../../util/promise')
const { isomorphicEncode } = require('../infra')
const { runtimeFeatures } = require('../../util/runtime-features')
// Node.js v23.8.0+ and v22.15.0+ supports Zstandard
const hasZstd = runtimeFeatures.has('zstd')
const GET_OR_HEAD = ['GET', 'HEAD']
const defaultUserAgent = typeof __UNDICI_IS_NODE__ !== 'undefined' || typeof esbuildDetection !== 'undefined'
? 'node'
: 'undici'
/** @type {import('buffer').resolveObjectURL} */
let resolveObjectURL
class Fetch extends EE {
constructor (dispatcher) {
super()
this.dispatcher = dispatcher
this.connection = null
this.dump = false
this.state = 'ongoing'
}
terminate (reason) {
if (this.state !== 'ongoing') {
return
}
this.state = 'terminated'
this.connection?.destroy(reason)
this.emit('terminated', reason)
}
// https://fetch.spec.whatwg.org/#fetch-controller-abort
abort (error) {
if (this.state !== 'ongoing') {
return
}
// 1. Set controller’s state to "aborted".
this.state = 'aborted'
// 2. Let fallbackError be an "AbortError" DOMException.
// 3. Set error to fallbackError if it is not given.
if (!error) {
error = new DOMException('The operation was aborted.', 'AbortError')
}
// 4. Let serializedError be StructuredSerialize(error).
// If that threw an exception, catch it, and let
// serializedError be StructuredSerialize(fallbackError).
// 5. Set controller’s serialized abort reason to serializedError.
this.serializedAbortReason = error
this.connection?.destroy(error)
this.emit('terminated', error)
}
}
function handleFetchDone (response) {
finalizeAndReportTiming(response, 'fetch')
}
// https://fetch.spec.whatwg.org/#fetch-method
function fetch (input, init = undefined) {
webidl.argumentLengthCheck(arguments, 1, 'globalThis.fetch')
// 1. Let p be a new promise.
let p = createDeferredPromise()
// 2. Let requestObject be the result of invoking the initial value of
// Request as constructor with input and init as arguments. If this throws
// an exception, reject p with it and return p.
let requestObject
try {
requestObject = new Request(input, init)
} catch (e) {
p.reject(e)
return p.promise
}
// 3. Let request be requestObject’s request.
const request = getRequestState(requestObject)
// 4. If requestObject’s signal’s aborted flag is set, then:
if (requestObject.signal.aborted) {
// 1. Abort the fetch() call with p, request, null, and
// requestObject’s signal’s abort reason.
abortFetch(p, request, null, requestObject.signal.reason, null)
// 2. Return p.
return p.promise
}
// 5. Let globalObject be request’s client’s global object.
const globalObject = request.client.globalObject
// 6. If globalObject is a ServiceWorkerGlobalScope object, then set
// request’s service-workers mode to "none".
if (globalObject?.constructor?.name === 'ServiceWorkerGlobalScope') {
request.serviceWorkers = 'none'
}
// 7. Let responseObject be null.
let responseObject = null
// 8. Let relevantRealm be this’s relevant Realm.
// 9. Let locallyAborted be false.
let locallyAborted = false
// 10. Let controller be null.
let controller = null
// 11. Add the following abort steps to requestObject’s signal:
addAbortListener(
requestObject.signal,
() => {
// 1. Set locallyAborted to true.
locallyAborted = true
// 2. Assert: controller is non-null.
assert(controller != null)
// 3. Abort controller with requestObject’s signal’s abort reason.
controller.abort(requestObject.signal.reason)
const realResponse = responseObject?.deref()
// 4. Abort the fetch() call with p, request, responseObject,
// and requestObject’s signal’s abort reason.
abortFetch(p, request, realResponse, requestObject.signal.reason, controller.controller)
}
)
// 12. Let handleFetchDone given response response be to finalize and
// report timing with response, globalObject, and "fetch".
// see function handleFetchDone
// 13. Set controller to the result of calling fetch given request,
// with processResponseEndOfBody set to handleFetchDone, and processResponse
// given response being these substeps:
const processResponse = (response) => {
// 1. If locallyAborted is true, terminate these substeps.
if (locallyAborted) {
return
}
// 2. If response’s aborted flag is set, then:
if (response.aborted) {
// 1. Let deserializedError be the result of deserialize a serialized
// abort reason given controller’s serialized abort reason and
// relevantRealm.
// 2. Abort the fetch() call with p, request, responseObject, and
// deserializedError.
abortFetch(p, request, responseObject, controller.serializedAbortReason, controller.controller)
return
}
// 3. If response is a network error, then reject p with a TypeError
// and terminate these substeps.
if (response.type === 'error') {
p.reject(new TypeError('fetch failed', { cause: response.error }))
return
}
// 4. Set responseObject to the result of creating a Response object,
// given response, "immutable", and relevantRealm.
responseObject = new WeakRef(fromInnerResponse(response, 'immutable'))
// 5. Resolve p with responseObject.
p.resolve(responseObject.deref())
p = null
}
controller = fetching({
request,
processResponseEndOfBody: handleFetchDone,
processResponse,
dispatcher: getRequestDispatcher(requestObject), // undici
// Keep requestObject alive to prevent its AbortController from being GC'd
// See https://github.com/nodejs/undici/issues/4627
requestObject
})
// 14. Return p.
return p.promise
}
// https://fetch.spec.whatwg.org/#finalize-and-report-timing
function finalizeAndReportTiming (response, initiatorType = 'other') {
// 1. If response is an aborted network error, then return.
if (response.type === 'error' && response.aborted) {
return
}
// 2. If response’s URL list is null or empty, then return.
if (!response.urlList?.length) {
return
}
// 3. Let originalURL be response’s URL list[0].
const originalURL = response.urlList[0]
// 4. Let timingInfo be response’s timing info.
let timingInfo = response.timingInfo
// 5. Let cacheState be response’s cache state.
let cacheState = response.cacheState
// 6. If originalURL’s scheme is not an HTTP(S) scheme, then return.
if (!urlIsHttpHttpsScheme(originalURL)) {
return
}
// 7. If timingInfo is null, then return.
if (timingInfo === null) {
return
}
// 8. If response’s timing allow passed flag is not set, then:
if (!response.timingAllowPassed) {
// 1. Set timingInfo to a the result of creating an opaque timing info for timingInfo.
timingInfo = createOpaqueTimingInfo({
startTime: timingInfo.startTime
})
// 2. Set cacheState to the empty string.
cacheState = ''
}
// 9. Set timingInfo’s end time to the coarsened shared current time
// given global’s relevant settings object’s cross-origin isolated
// capability.
// TODO: given global’s relevant settings object’s cross-origin isolated
// capability?
timingInfo.endTime = coarsenedSharedCurrentTime()
// 10. Set response’s timing info to timingInfo.
response.timingInfo = timingInfo
// 11. Mark resource timing for timingInfo, originalURL, initiatorType,
// global, and cacheState.
markResourceTiming(
timingInfo,
originalURL.href,
initiatorType,
globalThis,
cacheState,
'', // bodyType
response.status
)
}
// https://w3c.github.io/resource-timing/#dfn-mark-resource-timing
const markResourceTiming = performance.markResourceTiming
// https://fetch.spec.whatwg.org/#abort-fetch
function abortFetch (p, request, responseObject, error, controller /* undici-specific */) {
// 1. Reject promise with error.
if (p) {
// We might have already resolved the promise at this stage
p.reject(error)
}
// 2. If request’s body is not null and is readable, then cancel request’s
// body with error.
if (request.body?.stream != null && isReadable(request.body.stream)) {
request.body.stream.cancel(error).catch((err) => {
if (err.code === 'ERR_INVALID_STATE') {
// Node bug?
return
}
throw err
})
}
// 3. If responseObject is null, then return.
if (responseObject == null) {
return
}
// 4. Let response be responseObject’s response.
const response = getResponseState(responseObject)
// 5. If response’s body is not null and is readable, then error response’s
// body with error.
if (response.body?.stream != null && isReadable(response.body.stream)) {
controller.error(error)
}
}
// https://fetch.spec.whatwg.org/#fetching
function fetching ({
request,
processRequestBodyChunkLength,
processRequestEndOfBody,
processResponse,
processResponseEndOfBody,
processResponseConsumeBody,
useParallelQueue = false,
dispatcher = getGlobalDispatcher(), // undici
requestObject = null // Keep alive to prevent AbortController GC, see #4627
}) {
// Ensure that the dispatcher is set accordingly
assert(dispatcher)
// 1. Let taskDestination be null.
let taskDestination = null
// 2. Let crossOriginIsolatedCapability be false.
let crossOriginIsolatedCapability = false
// 3. If request’s client is non-null, then:
if (request.client != null) {
// 1. Set taskDestination to request’s client’s global object.
taskDestination = request.client.globalObject
// 2. Set crossOriginIsolatedCapability to request’s client’s cross-origin
// isolated capability.
crossOriginIsolatedCapability =
request.client.crossOriginIsolatedCapability
}
// 4. If useParallelQueue is true, then set taskDestination to the result of
// starting a new parallel queue.
// TODO
// 5. Let timingInfo be a new fetch timing info whose start time and
// post-redirect start time are the coarsened shared current time given
// crossOriginIsolatedCapability.
const currentTime = coarsenedSharedCurrentTime(crossOriginIsolatedCapability)
const timingInfo = createOpaqueTimingInfo({
startTime: currentTime
})
// 6. Let fetchParams be a new fetch params whose
// request is request,
// timing info is timingInfo,
// process request body chunk length is processRequestBodyChunkLength,
// process request end-of-body is processRequestEndOfBody,
// process response is processResponse,
// process response consume body is processResponseConsumeBody,
// process response end-of-body is processResponseEndOfBody,
// task destination is taskDestination,
// and cross-origin isolated capability is crossOriginIsolatedCapability.
const fetchParams = {
controller: new Fetch(dispatcher),
request,
timingInfo,
processRequestBodyChunkLength,
processRequestEndOfBody,
processResponse,
processResponseConsumeBody,
processResponseEndOfBody,
taskDestination,
crossOriginIsolatedCapability,
// Keep requestObject alive to prevent its AbortController from being GC'd
requestObject
}
// 7. If request’s body is a byte sequence, then set request’s body to
// request’s body as a body.
// NOTE: Since fetching is only called from fetch, body should already be
// extracted.
assert(!request.body || request.body.stream)
// 8. If request’s window is "client", then set request’s window to request’s
// client, if request’s client’s global object is a Window object; otherwise
// "no-window".
if (request.window === 'client') {
// TODO: What if request.client is null?
request.window =
request.client?.globalObject?.constructor?.name === 'Window'
? request.client
: 'no-window'
}
// 9. If request’s origin is "client", then set request’s origin to request’s
// client’s origin.
if (request.origin === 'client') {
request.origin = request.client.origin
}
// 10. If all of the following conditions are true:
// TODO
// 11. If request’s policy container is "client", then:
if (request.policyContainer === 'client') {
// 1. If request’s client is non-null, then set request’s policy
// container to a clone of request’s client’s policy container. [HTML]
if (request.client != null) {
request.policyContainer = clonePolicyContainer(
request.client.policyContainer
)
} else {
// 2. Otherwise, set request’s policy container to a new policy
// container.
request.policyContainer = makePolicyContainer()
}
}
// 12. If request’s header list does not contain `Accept`, then:
if (!request.headersList.contains('accept', true)) {
// 1. Let value be `*/*`.
const value = '*/*'
// 2. A user agent should set value to the first matching statement, if
// any, switching on request’s destination:
// "document"
// "frame"
// "iframe"
// `text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8`
// "image"
// `image/png,image/svg+xml,image/*;q=0.8,*/*;q=0.5`
// "style"
// `text/css,*/*;q=0.1`
// TODO
// 3. Append `Accept`/value to request’s header list.
request.headersList.append('accept', value, true)
}
// 13. If request’s header list does not contain `Accept-Language`, then
// user agents should append `Accept-Language`/an appropriate value to
// request’s header list.
if (!request.headersList.contains('accept-language', true)) {
request.headersList.append('accept-language', '*', true)
}
// 14. If request’s priority is null, then use request’s initiator and
// destination appropriately in setting request’s priority to a
// user-agent-defined object.
if (request.priority === null) {
// TODO
}
// 15. If request is a subresource request, then:
if (subresourceSet.has(request.destination)) {
// TODO
}
// 16. Run main fetch given fetchParams.
mainFetch(fetchParams, false)
// 17. Return fetchParam's controller
return fetchParams.controller
}
// https://fetch.spec.whatwg.org/#concept-main-fetch
async function mainFetch (fetchParams, recursive) {
try {
// 1. Let request be fetchParams’s request.
const request = fetchParams.request
// 2. Let response be null.
let response = null
// 3. If request’s local-URLs-only flag is set and request’s current URL is
// not local, then set response to a network error.
if (request.localURLsOnly && !urlIsLocal(requestCurrentURL(request))) {
response = makeNetworkError('local URLs only')
}
// 4. Run report Content Security Policy violations for request.
// TODO
// 5. Upgrade request to a potentially trustworthy URL, if appropriate.
tryUpgradeRequestToAPotentiallyTrustworthyURL(request)
// 6. If should request be blocked due to a bad port, should fetching request
// be blocked as mixed content, or should request be blocked by Content
// Security Policy returns blocked, then set response to a network error.
if (requestBadPort(request) === 'blocked') {
response = makeNetworkError('bad port')
}
// TODO: should fetching request be blocked as mixed content?
// TODO: should request be blocked by Content Security Policy?
// 7. If request’s referrer policy is the empty string, then set request’s
// referrer policy to request’s policy container’s referrer policy.
if (request.referrerPolicy === '') {
request.referrerPolicy = request.policyContainer.referrerPolicy
}
// 8. If request’s referrer is not "no-referrer", then set request’s
// referrer to the result of invoking determine request’s referrer.
if (request.referrer !== 'no-referrer') {
request.referrer = determineRequestsReferrer(request)
}
// 9. Set request’s current URL’s scheme to "https" if all of the following
// conditions are true:
// - request’s current URL’s scheme is "http"
// - request’s current URL’s host is a domain
// - Matching request’s current URL’s host per Known HSTS Host Domain Name
// Matching results in either a superdomain match with an asserted
// includeSubDomains directive or a congruent match (with or without an
// asserted includeSubDomains directive). [HSTS]
// TODO
// 10. If recursive is false, then run the remaining steps in parallel.
// TODO
// 11. If response is null, then set response to the result of running
// the steps corresponding to the first matching statement:
if (response === null) {
const currentURL = requestCurrentURL(request)
if (
// - request’s current URL’s origin is same origin with request’s origin,
// and request’s response tainting is "basic"
(sameOrigin(currentURL, request.url) && request.responseTainting === 'basic') ||
// request’s current URL’s scheme is "data"
(currentURL.protocol === 'data:') ||
// - request’s mode is "navigate" or "websocket"
(request.mode === 'navigate' || request.mode === 'websocket')
) {
// 1. Set request’s response tainting to "basic".
request.responseTainting = 'basic'
// 2. Return the result of running scheme fetch given fetchParams.
response = await schemeFetch(fetchParams)
// request’s mode is "same-origin"
} else if (request.mode === 'same-origin') {
// 1. Return a network error.
response = makeNetworkError('request mode cannot be "same-origin"')
// request’s mode is "no-cors"
} else if (request.mode === 'no-cors') {
// 1. If request’s redirect mode is not "follow", then return a network
// error.
if (request.redirect !== 'follow') {
response = makeNetworkError(
'redirect mode cannot be "follow" for "no-cors" request'
)
} else {
// 2. Set request’s response tainting to "opaque".
request.responseTainting = 'opaque'
// 3. Return the result of running scheme fetch given fetchParams.
response = await schemeFetch(fetchParams)
}
// request’s current URL’s scheme is not an HTTP(S) scheme
} else if (!urlIsHttpHttpsScheme(requestCurrentURL(request))) {
// Return a network error.
response = makeNetworkError('URL scheme must be a HTTP(S) scheme')
// - request’s use-CORS-preflight flag is set
// - request’s unsafe-request flag is set and either request’s method is
// not a CORS-safelisted method or CORS-unsafe request-header names with
// request’s header list is not empty
// 1. Set request’s response tainting to "cors".
// 2. Let corsWithPreflightResponse be the result of running HTTP fetch
// given fetchParams and true.
// 3. If corsWithPreflightResponse is a network error, then clear cache
// entries using request.
// 4. Return corsWithPreflightResponse.
// TODO
// Otherwise
} else {
// 1. Set request’s response tainting to "cors".
request.responseTainting = 'cors'
// 2. Return the result of running HTTP fetch given fetchParams.
response = await httpFetch(fetchParams)
}
}
// 12. If recursive is true, then return response.
if (recursive) {
return response
}
// 13. If response is not a network error and response is not a filtered
// response, then:
if (response.status !== 0 && !response.internalResponse) {
// If request’s response tainting is "cors", then:
if (request.responseTainting === 'cors') {
// 1. Let headerNames be the result of extracting header list values
// given `Access-Control-Expose-Headers` and response’s header list.
// TODO
// 2. If request’s credentials mode is not "include" and headerNames
// contains `*`, then set response’s CORS-exposed header-name list to
// all unique header names in response’s header list.
// TODO
// 3. Otherwise, if headerNames is not null or failure, then set
// response’s CORS-exposed header-name list to headerNames.
// TODO
}
// Set response to the following filtered response with response as its
// internal response, depending on request’s response tainting:
if (request.responseTainting === 'basic') {
response = filterResponse(response, 'basic')
} else if (request.responseTainting === 'cors') {
response = filterResponse(response, 'cors')
} else if (request.responseTainting === 'opaque') {
response = filterResponse(response, 'opaque')
} else {
assert(false)
}
}
// 14. Let internalResponse be response, if response is a network error,
// and response’s internal response otherwise.
let internalResponse =
response.status === 0 ? response : response.internalResponse
// 15. If internalResponse’s URL list is empty, then set it to a clone of
// request’s URL list.
if (internalResponse.urlList.length === 0) {
internalResponse.urlList.push(...request.urlList)
}
// 16. If request’s timing allow failed flag is unset, then set
// internalResponse’s timing allow passed flag.
if (!request.timingAllowFailed) {
response.timingAllowPassed = true
}
// 17. If response is not a network error and any of the following returns
// blocked
// - should internalResponse to request be blocked as mixed content
// - should internalResponse to request be blocked by Content Security Policy
// - should internalResponse to request be blocked due to its MIME type
// - should internalResponse to request be blocked due to nosniff
// TODO
// 18. If response’s type is "opaque", internalResponse’s status is 206,
// internalResponse’s range-requested flag is set, and request’s header
// list does not contain `Range`, then set response and internalResponse
// to a network error.
if (
response.type === 'opaque' &&
internalResponse.status === 206 &&
internalResponse.rangeRequested &&
!request.headers.contains('range', true)
) {
response = internalResponse = makeNetworkError()
}
// 19. If response is not a network error and either request’s method is
// `HEAD` or `CONNECT`, or internalResponse’s status is a null body status,
// set internalResponse’s body to null and disregard any enqueuing toward
// it (if any).
if (
response.status !== 0 &&
(request.method === 'HEAD' ||
request.method === 'CONNECT' ||
nullBodyStatus.includes(internalResponse.status))
) {
internalResponse.body = null
fetchParams.controller.dump = true
}
// 20. If request’s integrity metadata is not the empty string, then:
if (request.integrity) {
// 1. Let processBodyError be this step: run fetch finale given fetchParams
// and a network error.
const processBodyError = (reason) =>
fetchFinale(fetchParams, makeNetworkError(reason))
// 2. If request’s response tainting is "opaque", or response’s body is null,
// then run processBodyError and abort these steps.
if (request.responseTainting === 'opaque' || response.body == null) {
processBodyError(response.error)
return
}
// 3. Let processBody given bytes be these steps:
const processBody = (bytes) => {
// 1. If bytes do not match request’s integrity metadata,
// then run processBodyError and abort these steps. [SRI]
if (!bytesMatch(bytes, request.integrity)) {
processBodyError('integrity mismatch')
return
}
// 2. Set response’s body to bytes as a body.
response.body = safelyExtractBody(bytes)[0]
// 3. Run fetch finale given fetchParams and response.
fetchFinale(fetchParams, response)
}
// 4. Fully read response’s body given processBody and processBodyError.
fullyReadBody(response.body, processBody, processBodyError)
} else {
// 21. Otherwise, run fetch finale given fetchParams and response.
fetchFinale(fetchParams, response)
}
} catch (err) {
fetchParams.controller.terminate(err)
}
}
// https://fetch.spec.whatwg.org/#concept-scheme-fetch
// given a fetch params fetchParams
function schemeFetch (fetchParams) {
// Note: since the connection is destroyed on redirect, which sets fetchParams to a
// cancelled state, we do not want this condition to trigger *unless* there have been
// no redirects. See https://github.com/nodejs/undici/issues/1776
// 1. If fetchParams is canceled, then return the appropriate network error for fetchParams.
if (isCancelled(fetchParams) && fetchParams.request.redirectCount === 0) {
return Promise.resolve(makeAppropriateNetworkError(fetchParams))
}
// 2. Let request be fetchParams’s request.
const { request } = fetchParams
const { protocol: scheme } = requestCurrentURL(request)
// 3. Switch on request’s current URL’s scheme and run the associated steps:
switch (scheme) {
case 'about:': {
// If request’s current URL’s path is the string "blank", then return a new response
// whose status message is `OK`, header list is « (`Content-Type`, `text/html;charset=utf-8`) »,
// and body is the empty byte sequence as a body.
// Otherwise, return a network error.
return Promise.resolve(makeNetworkError('about scheme is not supported'))
}
case 'blob:': {
if (!resolveObjectURL) {
resolveObjectURL = require('node:buffer').resolveObjectURL
}
// 1. Let blobURLEntry be request’s current URL’s blob URL entry.
const blobURLEntry = requestCurrentURL(request)
// https://github.com/web-platform-tests/wpt/blob/7b0ebaccc62b566a1965396e5be7bb2bc06f841f/FileAPI/url/resources/fetch-tests.js#L52-L56
// Buffer.resolveObjectURL does not ignore URL queries.
if (blobURLEntry.search.length !== 0) {
return Promise.resolve(makeNetworkError('NetworkError when attempting to fetch resource.'))
}
const blob = resolveObjectURL(blobURLEntry.toString())
// 2. If request’s method is not `GET`, blobURLEntry is null, or blobURLEntry’s
// object is not a Blob object, then return a network error.
if (request.method !== 'GET' || !webidl.is.Blob(blob)) {
return Promise.resolve(makeNetworkError('invalid method'))
}
// 3. Let blob be blobURLEntry’s object.
// Note: done above
// 4. Let response be a new response.
const response = makeResponse()
// 5. Let fullLength be blob’s size.
const fullLength = blob.size
// 6. Let serializedFullLength be fullLength, serialized and isomorphic encoded.
const serializedFullLength = isomorphicEncode(`${fullLength}`)
// 7. Let type be blob’s type.
const type = blob.type
// 8. If request’s header list does not contain `Range`:
// 9. Otherwise:
if (!request.headersList.contains('range', true)) {
// 1. Let bodyWithType be the result of safely extracting blob.
// Note: in the FileAPI a blob "object" is a Blob *or* a MediaSource.
// In node, this can only ever be a Blob. Therefore we can safely
// use extractBody directly.
const bodyWithType = extractBody(blob)
// 2. Set response’s status message to `OK`.
response.statusText = 'OK'
// 3. Set response’s body to bodyWithType’s body.
response.body = bodyWithType[0]
// 4. Set response’s header list to « (`Content-Length`, serializedFullLength), (`Content-Type`, type) ».
response.headersList.set('content-length', serializedFullLength, true)
response.headersList.set('content-type', type, true)
} else {
// 1. Set response’s range-requested flag.
response.rangeRequested = true
// 2. Let rangeHeader be the result of getting `Range` from request’s header list.
const rangeHeader = request.headersList.get('range', true)
// 3. Let rangeValue be the result of parsing a single range header value given rangeHeader and true.
const rangeValue = simpleRangeHeaderValue(rangeHeader, true)
// 4. If rangeValue is failure, then return a network error.
if (rangeValue === 'failure') {
return Promise.resolve(makeNetworkError('failed to fetch the data URL'))
}
// 5. Let (rangeStart, rangeEnd) be rangeValue.
let { rangeStartValue: rangeStart, rangeEndValue: rangeEnd } = rangeValue
// 6. If rangeStart is null:
// 7. Otherwise:
if (rangeStart === null) {
// 1. Set rangeStart to fullLength − rangeEnd.
rangeStart = fullLength - rangeEnd
// 2. Set rangeEnd to rangeStart + rangeEnd − 1.
rangeEnd = rangeStart + rangeEnd - 1
} else {
// 1. If rangeStart is greater than or equal to fullLength, then return a network error.
if (rangeStart >= fullLength) {
return Promise.resolve(makeNetworkError('Range start is greater than the blob\'s size.'))
}
// 2. If rangeEnd is null or rangeEnd is greater than or equal to fullLength, then set
// rangeEnd to fullLength − 1.
if (rangeEnd === null || rangeEnd >= fullLength) {
rangeEnd = fullLength - 1
}
}
// 8. Let slicedBlob be the result of invoking slice blob given blob, rangeStart,
// rangeEnd + 1, and type.
const slicedBlob = blob.slice(rangeStart, rangeEnd + 1, type)
// 9. Let slicedBodyWithType be the result of safely extracting slicedBlob.
// Note: same reason as mentioned above as to why we use extractBody
const slicedBodyWithType = extractBody(slicedBlob)
// 10. Set response’s body to slicedBodyWithType’s body.
response.body = slicedBodyWithType[0]
// 11. Let serializedSlicedLength be slicedBlob’s size, serialized and isomorphic encoded.
const serializedSlicedLength = isomorphicEncode(`${slicedBlob.size}`)
// 12. Let contentRange be the result of invoking build a content range given rangeStart,
// rangeEnd, and fullLength.
const contentRange = buildContentRange(rangeStart, rangeEnd, fullLength)
// 13. Set response’s status to 206.
response.status = 206
// 14. Set response’s status message to `Partial Content`.
response.statusText = 'Partial Content'
// 15. Set response’s header list to « (`Content-Length`, serializedSlicedLength),
// (`Content-Type`, type), (`Content-Range`, contentRange) ».
response.headersList.set('content-length', serializedSlicedLength, true)
response.headersList.set('content-type', type, true)
response.headersList.set('content-range', contentRange, true)
}
// 10. Return response.
return Promise.resolve(response)
}
case 'data:': {
// 1. Let dataURLStruct be the result of running the
// data: URL processor on request’s current URL.
const currentURL = requestCurrentURL(request)
const dataURLStruct = dataURLProcessor(currentURL)
// 2. If dataURLStruct is failure, then return a
// network error.
if (dataURLStruct === 'failure') {
return Promise.resolve(makeNetworkError('failed to fetch the data URL'))
}
// 3. Let mimeType be dataURLStruct’s MIME type, serialized.
const mimeType = serializeAMimeType(dataURLStruct.mimeType)
// 4. Return a response whose status message is `OK`,
// header list is « (`Content-Type`, mimeType) »,
// and body is dataURLStruct’s body as a body.
return Promise.resolve(makeResponse({
statusText: 'OK',
headersList: [
['content-type', { name: 'Content-Type', value: mimeType }]
],
body: safelyExtractBody(dataURLStruct.body)[0]
}))
}
case 'file:': {
// For now, unfortunate as it is, file URLs are left as an exercise for the reader.
// When in doubt, return a network error.
return Promise.resolve(makeNetworkError('not implemented... yet...'))
}
case 'http:':
case 'https:': {
// Return the result of running HTTP fetch given fetchParams.
return httpFetch(fetchParams)
.catch((err) => makeNetworkError(err))
}
default: {
return Promise.resolve(makeNetworkError('unknown scheme'))
}
}
}
// https://fetch.spec.whatwg.org/#finalize-response
function finalizeResponse (fetchParams, response) {
// 1. Set fetchParams’s request’s done flag.
fetchParams.request.done = true
// 2, If fetchParams’s process response done is not null, then queue a fetch
// task to run fetchParams’s process response done given response, with
// fetchParams’s task destination.
if (fetchParams.processResponseDone != null) {
queueMicrotask(() => fetchParams.processResponseDone(response))
}
}
// https://fetch.spec.whatwg.org/#fetch-finale
function fetchFinale (fetchParams, response) {
// 1. Let timingInfo be fetchParams’s timing info.
let timingInfo = fetchParams.timingInfo
// 2. If response is not a network error and fetchParams’s request’s client is a secure context,
// then set timingInfo’s server-timing headers to the result of getting, decoding, and splitting
// `Server-Timing` from response’s internal response’s header list.
// TODO
// 3. Let processResponseEndOfBody be the following steps:
const processResponseEndOfBody = () => {
// 1. Let unsafeEndTime be the unsafe shared current time.
const unsafeEndTime = Date.now() // ?
// 2. If fetchParams’s request’s destination is "document", then set fetchParams’s controller’s
// full timing info to fetchParams’s timing info.
if (fetchParams.request.destination === 'document') {
fetchParams.controller.fullTimingInfo = timingInfo
}
// 3. Set fetchParams’s controller’s report timing steps to the following steps given a global object global:
fetchParams.controller.reportTimingSteps = () => {
// 1. If fetchParams’s request’s URL’s scheme is not an HTTP(S) scheme, then return.
if (!urlIsHttpHttpsScheme(fetchParams.request.url)) {
return
}
// 2. Set timingInfo’s end time to the relative high resolution time given unsafeEndTime and global.
timingInfo.endTime = unsafeEndTime
// 3. Let cacheState be response’s cache state.
let cacheState = response.cacheState
// 4. Let bodyInfo be response’s body info.
const bodyInfo = response.bodyInfo
// 5. If response’s timing allow passed flag is not set, then set timingInfo to the result of creating an
// opaque timing info for timingInfo and set cacheState to the empty string.
if (!response.timingAllowPassed) {
timingInfo = createOpaqueTimingInfo(timingInfo)
cacheState = ''
}
// 6. Let responseStatus be 0.
let responseStatus = 0
// 7. If fetchParams’s request’s mode is not "navigate" or response’s has-cross-origin-redirects is false:
if (fetchParams.request.mode !== 'navigator' || !response.hasCrossOriginRedirects) {
// 1. Set responseStatus to response’s status.
responseStatus = response.status
// 2. Let mimeType be the result of extracting a MIME type from response’s header list.
const mimeType = extractMimeType(response.headersList)
// 3. If mimeType is not failure, then set bodyInfo’s content type to the result of minimizing a supported MIME type given mimeType.
if (mimeType !== 'failure') {
bodyInfo.contentType = minimizeSupportedMimeType(mimeType)
}
}
// 8. If fetchParams’s request’s initiator type is non-null, then mark resource timing given timingInfo,
// fetchParams’s request’s URL, fetchParams’s request’s initiator type, global, cacheState, bodyInfo,
// and responseStatus.
if (fetchParams.request.initiatorType != null) {
markResourceTiming(timingInfo, fetchParams.request.url.href, fetchParams.request.initiatorType, globalThis, cacheState, bodyInfo, responseStatus)
}
}
// 4. Let processResponseEndOfBodyTask be the following steps:
const processResponseEndOfBodyTask = () => {
// 1. Set fetchParams’s request’s done flag.
fetchParams.request.done = true
// 2. If fetchParams’s process response end-of-body is non-null, then run fetchParams’s process
// response end-of-body given response.
if (fetchParams.processResponseEndOfBody != null) {
queueMicrotask(() => fetchParams.processResponseEndOfBody(response))
}
// 3. If fetchParams’s request’s initiator type is non-null and fetchParams’s request’s client’s
// global object is fetchParams’s task destination, then run fetchParams’s controller’s report
// timing steps given fetchParams’s request’s client’s global object.
if (fetchParams.request.initiatorType != null) {
fetchParams.controller.reportTimingSteps()
}
}
// 5. Queue a fetch task to run processResponseEndOfBodyTask with fetchParams’s task destination
queueMicrotask(() => processResponseEndOfBodyTask())
}
// 4. If fetchParams’s process response is non-null, then queue a fetch task to run fetchParams’s
// process response given response, with fetchParams’s task destination.
if (fetchParams.processResponse != null) {
queueMicrotask(() => {
fetchParams.processResponse(response)
fetchParams.processResponse = null
})
}
// 5. Let internalResponse be response, if response is a network error; otherwise response’s internal response.
const internalResponse = response.type === 'error' ? response : (response.internalResponse ?? response)
// 6. If internalResponse’s body is null, then run processResponseEndOfBody.
// 7. Otherwise:
if (internalResponse.body == null) {
processResponseEndOfBody()
} else {
// mcollina: all the following steps of the specs are skipped.
// The internal transform stream is not needed.
// See https://github.com/nodejs/undici/pull/3093#issuecomment-2050198541
// 1. Let transformStream be a new TransformStream.
// 2. Let identityTransformAlgorithm be an algorithm which, given chunk, enqueues chunk in transformStream.
// 3. Set up transformStream with transformAlgorithm set to identityTransformAlgorithm and flushAlgorithm
// set to processResponseEndOfBody.
// 4. Set internalResponse’s body’s stream to the result of internalResponse’s body’s stream piped through transformStream.
finished(internalResponse.body.stream, () => {
processResponseEndOfBody()
})
}
}
// https://fetch.spec.whatwg.org/#http-fetch
async function httpFetch (fetchParams) {
// 1. Let request be fetchParams’s request.
const request = fetchParams.request
// 2. Let response be null.
let response = null
// 3. Let actualResponse be null.
let actualResponse = null
// 4. Let timingInfo be fetchParams’s timing info.
const timingInfo = fetchParams.timingInfo
// 5. If request’s service-workers mode is "all", then:
if (request.serviceWorkers === 'all') {
// TODO
}
// 6. If response is null, then:
if (response === null) {
// 1. If makeCORSPreflight is true and one of these conditions is true:
// TODO
// 2. If request’s redirect mode is "follow", then set request’s
// service-workers mode to "none".
if (request.redirect === 'follow') {
request.serviceWorkers = 'none'
}
// 3. Set response and actualResponse to the result of running
// HTTP-network-or-cache fetch given fetchParams.
actualResponse = response = await httpNetworkOrCacheFetch(fetchParams)
// 4. If request’s response tainting is "cors" and a CORS check
// for request and response returns failure, then return a network error.
if (
request.responseTainting === 'cors' &&
corsCheck(request, response) === 'failure'
) {
return makeNetworkError('cors failure')
}
// 5. If the TAO check for request and response returns failure, then set
// request’s timing allow failed flag.
if (TAOCheck(request, response) === 'failure') {
request.timingAllowFailed = true
}
}
// 7. If either request’s response tainting or response’s type
// is "opaque", and the cross-origin resource policy check with
// request’s origin, request’s client, request’s destination,
// and actualResponse returns blocked, then return a network error.
if (
(request.responseTainting === 'opaque' || response.type === 'opaque') &&
crossOriginResourcePolicyCheck(
request.origin,
request.client,
request.destination,
actualResponse
) === 'blocked'
) {
return makeNetworkError('blocked')
}
// 8. If actualResponse’s status is a redirect status, then:
if (redirectStatusSet.has(actualResponse.status)) {
// 1. If actualResponse’s status is not 303, request’s body is not null,
// and the connection uses HTTP/2, then user agents may, and are even
// encouraged to, transmit an RST_STREAM frame.
// See, https://github.com/whatwg/fetch/issues/1288
if (request.redirect !== 'manual') {
fetchParams.controller.connection.destroy(undefined, false)
}
// 2. Switch on request’s redirect mode:
if (request.redirect === 'error') {
// Set response to a network error.
response = makeNetworkError('unexpected redirect')
} else if (request.redirect === 'manual') {
// Set response to an opaque-redirect filtered response whose internal
// response is actualResponse.
// NOTE(spec): On the web this would return an `opaqueredirect` response,
// but that doesn't make sense server side.
// See https://github.com/nodejs/undici/issues/1193.
response = actualResponse
} else if (request.redirect === 'follow') {
// Set response to the result of running HTTP-redirect fetch given
// fetchParams and response.
response = await httpRedirectFetch(fetchParams, response)
} else {
assert(false)
}
}
// 9. Set response’s timing info to timingInfo.
response.timingInfo = timingInfo
// 10. Return response.
return response
}
// https://fetch.spec.whatwg.org/#http-redirect-fetch
function httpRedirectFetch (fetchParams, response) {
// 1. Let request be fetchParams’s request.
const request = fetchParams.request
// 2. Let actualResponse be response, if response is not a filtered response,
// and response’s internal response otherwise.
const actualResponse = response.internalResponse
? response.internalResponse
: response
// 3. Let locationURL be actualResponse’s location URL given request’s current
// URL’s fragment.
let locationURL
try {
locationURL = responseLocationURL(
actualResponse,
requestCurrentURL(request).hash
)
// 4. If locationURL is null, then return response.
if (locationURL == null) {
return response
}
} catch (err) {
// 5. If locationURL is failure, then return a network error.
return Promise.resolve(makeNetworkError(err))
}
// 6. If locationURL’s scheme is not an HTTP(S) scheme, then return a network
// error.
if (!urlIsHttpHttpsScheme(locationURL)) {
return Promise.resolve(makeNetworkError('URL scheme must be a HTTP(S) scheme'))
}
// 7. If request’s redirect count is 20, then return a network error.
if (request.redirectCount === 20) {
return Promise.resolve(makeNetworkError('redirect count exceeded'))
}
// 8. Increase request’s redirect count by 1.
request.redirectCount += 1
// 9. If request’s mode is "cors", locationURL includes credentials, and
// request’s origin is not same origin with locationURL’s origin, then return
// a network error.
if (
request.mode === 'cors' &&
(locationURL.username || locationURL.password) &&
!sameOrigin(request, locationURL)
) {
return Promise.resolve(makeNetworkError('cross origin not allowed for request mode "cors"'))
}
// 10. If request’s response tainting is "cors" and locationURL includes
// credentials, then return a network error.
if (
request.responseTainting === 'cors' &&
(locationURL.username || locationURL.password)
) {
return Promise.resolve(makeNetworkError(
'URL cannot contain credentials for request mode "cors"'
))
}
// 11. If actualResponse’s status is not 303, request’s body is non-null,
// and request’s body’s source is null, then return a network error.
if (
actualResponse.status !== 303 &&
request.body != null &&
request.body.source == null
) {
return Promise.resolve(makeNetworkError())
}
// 12. If one of the following is true
// - actualResponse’s status is 301 or 302 and request’s method is `POST`
// - actualResponse’s status is 303 and request’s method is not `GET` or `HEAD`
if (
([301, 302].includes(actualResponse.status) && request.method === 'POST') ||
(actualResponse.status === 303 &&
!GET_OR_HEAD.includes(request.method))
) {
// then:
// 1. Set request’s method to `GET` and request’s body to null.
request.method = 'GET'
request.body = null
// 2. For each headerName of request-body-header name, delete headerName from
// request’s header list.
for (const headerName of requestBodyHeader) {
request.headersList.delete(headerName)
}
}
// 13. If request’s current URL’s origin is not same origin with locationURL’s
// origin, then for each headerName of CORS non-wildcard request-header name,
// delete headerName from request’s header list.
if (!sameOrigin(requestCurrentURL(request), locationURL)) {
// https://fetch.spec.whatwg.org/#cors-non-wildcard-request-header-name
request.headersList.delete('authorization', true)
// https://fetch.spec.whatwg.org/#authentication-entries
request.headersList.delete('proxy-authorization', true)
// "Cookie" and "Host" are forbidden request-headers, which undici doesn't implement.
request.headersList.delete('cookie', true)
request.headersList.delete('host', true)
}
// 14. If request's body is non-null, then set request's body to the first return
// value of safely extracting request's body's source.
if (request.body != null) {
assert(request.body.source != null)
request.body = safelyExtractBody(request.body.source)[0]
}
// 15. Let timingInfo be fetchParams’s timing info.
const timingInfo = fetchParams.timingInfo
// 16. Set timingInfo’s redirect end time and post-redirect start time to the
// coarsened shared current time given fetchParams’s cross-origin isolated
// capability.
timingInfo.redirectEndTime = timingInfo.postRedirectStartTime =
coarsenedSharedCurrentTime(fetchParams.crossOriginIsolatedCapability)
// 17. If timingInfo’s redirect start time is 0, then set timingInfo’s
// redirect start time to timingInfo’s start time.
if (timingInfo.redirectStartTime === 0) {
timingInfo.redirectStartTime = timingInfo.startTime
}
// 18. Append locationURL to request’s URL list.
request.urlList.push(locationURL)
// 19. Invoke set request’s referrer policy on redirect on request and
// actualResponse.
setRequestReferrerPolicyOnRedirect(request, actualResponse)
// 20. Return the result of running main fetch given fetchParams and true.
return mainFetch(fetchParams, true)
}
// https://fetch.spec.whatwg.org/#http-network-or-cache-fetch
async function httpNetworkOrCacheFetch (
fetchParams,
isAuthenticationFetch = false,
isNewConnectionFetch = false
) {
// 1. Let request be fetchParams’s request.
const request = fetchParams.request
// 2. Let httpFetchParams be null.
let httpFetchParams = null
// 3. Let httpRequest be null.
let httpRequest = null
// 4. Let response be null.
let response = null
// 5. Let storedResponse be null.
// TODO: cache
// 6. Let httpCache be null.
const httpCache = null
// 7. Let the revalidatingFlag be unset.
const revalidatingFlag = false
// 8. Run these steps, but abort when the ongoing fetch is terminated:
// 1. If request’s window is "no-window" and request’s redirect mode is
// "error", then set httpFetchParams to fetchParams and httpRequest to
// request.
if (request.window === 'no-window' && request.redirect === 'error') {
httpFetchParams = fetchParams
httpRequest = request
} else {
// Otherwise:
// 1. Set httpRequest to a clone of request.
httpRequest = cloneRequest(request)
// 2. Set httpFetchParams to a copy of fetchParams.
httpFetchParams = { ...fetchParams }
// 3. Set httpFetchParams’s request to httpRequest.
httpFetchParams.request = httpRequest
}
// 3. Let includeCredentials be true if one of
const includeCredentials =
request.credentials === 'include' ||
(request.credentials === 'same-origin' &&
request.responseTainting === 'basic')
// 4. Let contentLength be httpRequest’s body’s length, if httpRequest’s
// body is non-null; otherwise null.
const contentLength = httpRequest.body ? httpRequest.body.length : null
// 5. Let contentLengthHeaderValue be null.
let contentLengthHeaderValue = null
// 6. If httpRequest’s body is null and httpRequest’s method is `POST` or
// `PUT`, then set contentLengthHeaderValue to `0`.
if (
httpRequest.body == null &&
['POST', 'PUT'].includes(httpRequest.method)
) {
contentLengthHeaderValue = '0'
}
// 7. If contentLength is non-null, then set contentLengthHeaderValue to
// contentLength, serialized and isomorphic encoded.
if (contentLength != null) {
contentLengthHeaderValue = isomorphicEncode(`${contentLength}`)
}
// 8. If contentLengthHeaderValue is non-null, then append
// `Content-Length`/contentLengthHeaderValue to httpRequest’s header
// list.
if (contentLengthHeaderValue != null) {
httpRequest.headersList.append('content-length', contentLengthHeaderValue, true)
}
// 9. If contentLengthHeaderValue is non-null, then append (`Content-Length`,
// contentLengthHeaderValue) to httpRequest’s header list.
// 10. If contentLength is non-null and httpRequest’s keepalive is true,
// then:
if (contentLength != null && httpRequest.keepalive) {
// NOTE: keepalive is a noop outside of browser context.
}
// 11. If httpRequest’s referrer is a URL, then append
// `Referer`/httpRequest’s referrer, serialized and isomorphic encoded,
// to httpRequest’s header list.
if (webidl.is.URL(httpRequest.referrer)) {
httpRequest.headersList.append('referer', isomorphicEncode(httpRequest.referrer.href), true)
}
// 12. Append a request `Origin` header for httpRequest.
appendRequestOriginHeader(httpRequest)
// 13. Append the Fetch metadata headers for httpRequest. [FETCH-METADATA]
appendFetchMetadata(httpRequest)
// 14. If httpRequest’s header list does not contain `User-Agent`, then
// user agents should append `User-Agent`/default `User-Agent` value to
// httpRequest’s header list.
if (!httpRequest.headersList.contains('user-agent', true)) {
httpRequest.headersList.append('user-agent', defaultUserAgent, true)
}
// 15. If httpRequest’s cache mode is "default" and httpRequest’s header
// list contains `If-Modified-Since`, `If-None-Match`,
// `If-Unmodified-Since`, `If-Match`, or `If-Range`, then set
// httpRequest’s cache mode to "no-store".
if (
httpRequest.cache === 'default' &&
(httpRequest.headersList.contains('if-modified-since', true) ||
httpRequest.headersList.contains('if-none-match', true) ||
httpRequest.headersList.contains('if-unmodified-since', true) ||
httpRequest.headersList.contains('if-match', true) ||
httpRequest.headersList.contains('if-range', true))
) {
httpRequest.cache = 'no-store'
}
// 16. If httpRequest’s cache mode is "no-cache", httpRequest’s prevent
// no-cache cache-control header modification flag is unset, and
// httpRequest’s header list does not contain `Cache-Control`, then append
// `Cache-Control`/`max-age=0` to httpRequest’s header list.
if (
httpRequest.cache === 'no-cache' &&
!httpRequest.preventNoCacheCacheControlHeaderModification &&
!httpRequest.headersList.contains('cache-control', true)
) {
httpRequest.headersList.append('cache-control', 'max-age=0', true)
}
// 17. If httpRequest’s cache mode is "no-store" or "reload", then:
if (httpRequest.cache === 'no-store' || httpRequest.cache === 'reload') {
// 1. If httpRequest’s header list does not contain `Pragma`, then append
// `Pragma`/`no-cache` to httpRequest’s header list.
if (!httpRequest.headersList.contains('pragma', true)) {
httpRequest.headersList.append('pragma', 'no-cache', true)
}
// 2. If httpRequest’s header list does not contain `Cache-Control`,
// then append `Cache-Control`/`no-cache` to httpRequest’s header list.
if (!httpRequest.headersList.contains('cache-control', true)) {
httpRequest.headersList.append('cache-control', 'no-cache', true)
}
}
// 18. If httpRequest’s header list contains `Range`, then append
// `Accept-Encoding`/`identity` to httpRequest’s header list.
if (httpRequest.headersList.contains('range', true)) {
httpRequest.headersList.append('accept-encoding', 'identity', true)
}
// 19. Modify httpRequest’s header list per HTTP. Do not append a given
// header if httpRequest’s header list contains that header’s name.
// TODO: https://github.com/whatwg/fetch/issues/1285#issuecomment-896560129
if (!httpRequest.headersList.contains('accept-encoding', true)) {
if (urlHasHttpsScheme(requestCurrentURL(httpRequest))) {
httpRequest.headersList.append('accept-encoding', 'br, gzip, deflate', true)
} else {
httpRequest.headersList.append('accept-encoding', 'gzip, deflate', true)
}
}
httpRequest.headersList.delete('host', true)
// 21. If includeCredentials is true, then:
if (includeCredentials) {
// 1. If the user agent is not configured to block cookies for httpRequest
// (see section 7 of [COOKIES]), then:
// TODO: credentials
// 2. If httpRequest’s header list does not contain `Authorization`, then:
if (!httpRequest.headersList.contains('authorization', true)) {
// 1. Let authorizationValue be null.
let authorizationValue = null
// 2. If there’s an authentication entry for httpRequest and either
// httpRequest’s use-URL-credentials flag is unset or httpRequest’s
// current URL does not include credentials, then set
// authorizationValue to authentication entry.
if (hasAuthenticationEntry(httpRequest) && (
httpRequest.useURLCredentials === undefined || !includesCredentials(requestCurrentURL(httpRequest))
)) {
// TODO
} else if (includesCredentials(requestCurrentURL(httpRequest)) && isAuthenticationFetch) {
// 3. Otherwise, if httpRequest’s current URL does include credentials
// and isAuthenticationFetch is true, set authorizationValue to
// httpRequest’s current URL, converted to an `Authorization` value
const { username, password } = requestCurrentURL(httpRequest)
authorizationValue = `Basic ${Buffer.from(`${username}:${password}`).toString('base64')}`
}
// 4. If authorizationValue is non-null, then append (`Authorization`,
// authorizationValue) to httpRequest’s header list.
if (authorizationValue !== null) {
httpRequest.headersList.append('Authorization', authorizationValue, false)
}
}
}
// 21. If there’s a proxy-authentication entry, use it as appropriate.
// TODO: proxy-authentication
// 22. Set httpCache to the result of determining the HTTP cache
// partition, given httpRequest.
// TODO: cache
// 23. If httpCache is null, then set httpRequest’s cache mode to
// "no-store".
if (httpCache == null) {
httpRequest.cache = 'no-store'
}
// 24. If httpRequest’s cache mode is neither "no-store" nor "reload",
// then:
if (httpRequest.cache !== 'no-store' && httpRequest.cache !== 'reload') {
// TODO: cache
}
// 9. If aborted, then return the appropriate network error for fetchParams.
// TODO
// 10. If response is null, then:
if (response == null) {
// 1. If httpRequest’s cache mode is "only-if-cached", then return a
// network error.
if (httpRequest.cache === 'only-if-cached') {
return makeNetworkError('only if cached')
}
// 2. Let forwardResponse be the result of running HTTP-network fetch
// given httpFetchParams, includeCredentials, and isNewConnectionFetch.
const forwardResponse = await httpNetworkFetch(
httpFetchParams,
includeCredentials,
isNewConnectionFetch
)
// 3. If httpRequest’s method is unsafe and forwardResponse’s status is
// in the range 200 to 399, inclusive, invalidate appropriate stored
// responses in httpCache, as per the "Invalidation" chapter of HTTP
// Caching, and set storedResponse to null. [HTTP-CACHING]
if (
!safeMethodsSet.has(httpRequest.method) &&
forwardResponse.status >= 200 &&
forwardResponse.status <= 399
) {
// TODO: cache
}
// 4. If the revalidatingFlag is set and forwardResponse’s status is 304,
// then:
if (revalidatingFlag && forwardResponse.status === 304) {
// TODO: cache
}
// 5. If response is null, then:
if (response == null) {
// 1. Set response to forwardResponse.
response = forwardResponse
// 2. Store httpRequest and forwardResponse in httpCache, as per the
// "Storing Responses in Caches" chapter of HTTP Caching. [HTTP-CACHING]
// TODO: cache
}
}
// 11. Set response’s URL list to a clone of httpRequest’s URL list.
response.urlList = [...httpRequest.urlList]
// 12. If httpRequest’s header list contains `Range`, then set response’s
// range-requested flag.
if (httpRequest.headersList.contains('range', true)) {
response.rangeRequested = true
}
// 13. Set response’s request-includes-credentials to includeCredentials.
response.requestIncludesCredentials = includeCredentials
// 14. If response’s status is 401, httpRequest’s response tainting is not "cors",
// includeCredentials is true, and request’s traversable for user prompts is
// a traversable navigable:
if (response.status === 401 && httpRequest.responseTainting !== 'cors' && includeCredentials && isTraversableNavigable(request.traversableForUserPrompts)) {
// 2. If request’s body is non-null, then:
if (request.body != null) {
// 1. If request’s body’s source is null, then return a network error.
if (request.body.source == null) {
return makeNetworkError('expected non-null body source')
}
// 2. Set request’s body to the body of the result of safely extracting
// request’s body’s source.
request.body = safelyExtractBody(request.body.source)[0]
}
// 3. If request’s use-URL-credentials flag is unset or isAuthenticationFetch is
// true, then:
if (request.useURLCredentials === undefined || isAuthenticationFetch) {
// 1. If fetchParams is canceled, then return the appropriate network error
// for fetchParams.
if (isCancelled(fetchParams)) {
return makeAppropriateNetworkError(fetchParams)
}
// 2. Let username and password be the result of prompting the end user for a
// username and password, respectively, in request’s traversable for user prompts.
// TODO
// 3. Set the username given request’s current URL and username.
// requestCurrentURL(request).username = TODO
// 4. Set the password given request’s current URL and password.
// requestCurrentURL(request).password = TODO
// In browsers, the user will be prompted to enter a username/password before the request
// is re-sent. To prevent an infinite 401 loop, return the response for now.
// https://github.com/nodejs/undici/pull/4756
return response
}
// 4. Set response to the result of running HTTP-network-or-cache fetch given
// fetchParams and true.
fetchParams.controller.connection.destroy()
response = await httpNetworkOrCacheFetch(fetchParams, true)
}
// 15. If response’s status is 407, then:
if (response.status === 407) {
// 1. If request’s window is "no-window", then return a network error.
if (request.window === 'no-window') {
return makeNetworkError()
}
// 2. ???
// 3. If fetchParams is canceled, then return the appropriate network error for fetchParams.
if (isCancelled(fetchParams)) {
return makeAppropriateNetworkError(fetchParams)
}
// 4. Prompt the end user as appropriate in request’s window and store
// the result as a proxy-authentication entry. [HTTP-AUTH]
// TODO: Invoke some kind of callback?
// 5. Set response to the result of running HTTP-network-or-cache fetch given
// fetchParams.
// TODO
return makeNetworkError('proxy authentication required')
}
// 16. If all of the following are true
if (
// response’s status is 421
response.status === 421 &&
// isNewConnectionFetch is false
!isNewConnectionFetch &&
// request’s body is null, or request’s body is non-null and request’s body’s source is non-null
(request.body == null || request.body.source != null)
) {
// then:
// 1. If fetchParams is canceled, then return the appropriate network error for fetchParams.
if (isCancelled(fetchParams)) {
return makeAppropriateNetworkError(fetchParams)
}
// 2. Set response to the result of running HTTP-network-or-cache
// fetch given fetchParams, isAuthenticationFetch, and true.
// TODO (spec): The spec doesn't specify this but we need to cancel
// the active response before we can start a new one.
// https://github.com/whatwg/fetch/issues/1293
fetchParams.controller.connection.destroy()
response = await httpNetworkOrCacheFetch(
fetchParams,
isAuthenticationFetch,
true
)
}
// 17. If isAuthenticationFetch is true, then create an authentication entry
if (isAuthenticationFetch) {
// TODO
}
// 18. Return response.
return response
}
// https://fetch.spec.whatwg.org/#http-network-fetch
async function httpNetworkFetch (
fetchParams,
includeCredentials = false,
forceNewConnection = false
) {
assert(!fetchParams.controller.connection || fetchParams.controller.connection.destroyed)
fetchParams.controller.connection = {
abort: null,
destroyed: false,
destroy (err, abort = true) {
if (!this.destroyed) {
this.destroyed = true
if (abort) {
this.abort?.(err ?? new DOMException('The operation was aborted.', 'AbortError'))
}
}
}
}
// 1. Let request be fetchParams’s request.
const request = fetchParams.request
// 2. Let response be null.
let response = null
// 3. Let timingInfo be fetchParams’s timing info.
const timingInfo = fetchParams.timingInfo
// 4. Let httpCache be the result of determining the HTTP cache partition,
// given request.
// TODO: cache
const httpCache = null
// 5. If httpCache is null, then set request’s cache mode to "no-store".
if (httpCache == null) {
request.cache = 'no-store'
}
// 6. Let networkPartitionKey be the result of determining the network
// partition key given request.
// TODO
// 7. Let newConnection be "yes" if forceNewConnection is true; otherwise
// "no".
const newConnection = forceNewConnection ? 'yes' : 'no' // eslint-disable-line no-unused-vars
// 8. Switch on request’s mode:
if (request.mode === 'websocket') {
// Let connection be the result of obtaining a WebSocket connection,
// given request’s current URL.
// TODO
} else {
// Let connection be the result of obtaining a connection, given
// networkPartitionKey, request’s current URL’s origin,
// includeCredentials, and forceNewConnection.
// TODO
}
// 9. Run these steps, but abort when the ongoing fetch is terminated:
// 1. If connection is failure, then return a network error.
// 2. Set timingInfo’s final connection timing info to the result of
// calling clamp and coarsen connection timing info with connection’s
// timing info, timingInfo’s post-redirect start time, and fetchParams’s
// cross-origin isolated capability.
// 3. If connection is not an HTTP/2 connection, request’s body is non-null,
// and request’s body’s source is null, then append (`Transfer-Encoding`,
// `chunked`) to request’s header list.
// 4. Set timingInfo’s final network-request start time to the coarsened
// shared current time given fetchParams’s cross-origin isolated
// capability.
// 5. Set response to the result of making an HTTP request over connection
// using request with the following caveats:
// - Follow the relevant requirements from HTTP. [HTTP] [HTTP-SEMANTICS]
// [HTTP-COND] [HTTP-CACHING] [HTTP-AUTH]
// - If request’s body is non-null, and request’s body’s source is null,
// then the user agent may have a buffer of up to 64 kibibytes and store
// a part of request’s body in that buffer. If the user agent reads from
// request’s body beyond that buffer’s size and the user agent needs to
// resend request, then instead return a network error.
// - Set timingInfo’s final network-response start time to the coarsened
// shared current time given fetchParams’s cross-origin isolated capability,
// immediately after the user agent’s HTTP parser receives the first byte
// of the response (e.g., frame header bytes for HTTP/2 or response status
// line for HTTP/1.x).
// - Wait until all the headers are transmitted.
// - Any responses whose status is in the range 100 to 199, inclusive,
// and is not 101, are to be ignored, except for the purposes of setting
// timingInfo’s final network-response start time above.
// - If request’s header list contains `Transfer-Encoding`/`chunked` and
// response is transferred via HTTP/1.0 or older, then return a network
// error.
// - If the HTTP request results in a TLS client certificate dialog, then:
// 1. If request’s window is an environment settings object, make the
// dialog available in request’s window.
// 2. Otherwise, return a network error.
// To transmit request’s body body, run these steps:
let requestBody = null
// 1. If body is null and fetchParams’s process request end-of-body is
// non-null, then queue a fetch task given fetchParams’s process request
// end-of-body and fetchParams’s task destination.
if (request.body == null && fetchParams.processRequestEndOfBody) {
queueMicrotask(() => fetchParams.processRequestEndOfBody())
} else if (request.body != null) {
// 2. Otherwise, if body is non-null:
// 1. Let processBodyChunk given bytes be these steps:
const processBodyChunk = async function * (bytes) {
// 1. If the ongoing fetch is terminated, then abort these steps.
if (isCancelled(fetchParams)) {
return
}
// 2. Run this step in parallel: transmit bytes.
yield bytes
// 3. If fetchParams’s process request body is non-null, then run
// fetchParams’s process request body given bytes’s length.
fetchParams.processRequestBodyChunkLength?.(bytes.byteLength)
}
// 2. Let processEndOfBody be these steps:
const processEndOfBody = () => {
// 1. If fetchParams is canceled, then abort these steps.
if (isCancelled(fetchParams)) {
return
}
// 2. If fetchParams’s process request end-of-body is non-null,
// then run fetchParams’s process request end-of-body.
if (fetchParams.processRequestEndOfBody) {
fetchParams.processRequestEndOfBody()
}
}
// 3. Let processBodyError given e be these steps:
const processBodyError = (e) => {
// 1. If fetchParams is canceled, then abort these steps.
if (isCancelled(fetchParams)) {
return
}
// 2. If e is an "AbortError" DOMException, then abort fetchParams’s controller.
if (e.name === 'AbortError') {
fetchParams.controller.abort()
} else {
fetchParams.controller.terminate(e)
}
}
// 4. Incrementally read request’s body given processBodyChunk, processEndOfBody,
// processBodyError, and fetchParams’s task destination.
requestBody = (async function * () {
try {
for await (const bytes of request.body.stream) {
yield * processBodyChunk(bytes)
}
processEndOfBody()
} catch (err) {
processBodyError(err)
}
})()
}
try {
// socket is only provided for websockets
const { body, status, statusText, headersList, socket } = await dispatch({ body: requestBody })
if (socket) {
response = makeResponse({ status, statusText, headersList, socket })
} else {
const iterator = body[Symbol.asyncIterator]()
fetchParams.controller.next = () => iterator.next()
response = makeResponse({ status, statusText, headersList })
}
} catch (err) {
// 10. If aborted, then:
if (err.name === 'AbortError') {
// 1. If connection uses HTTP/2, then transmit an RST_STREAM frame.
fetchParams.controller.connection.destroy()
// 2. Return the appropriate network error for fetchParams.
return makeAppropriateNetworkError(fetchParams, err)
}
return makeNetworkError(err)
}
// 11. Let pullAlgorithm be an action that resumes the ongoing fetch
// if it is suspended.
const pullAlgorithm = () => {
return fetchParams.controller.resume()
}
// 12. Let cancelAlgorithm be an algorithm that aborts fetchParams’s
// controller with reason, given reason.
const cancelAlgorithm = (reason) => {
// If the aborted fetch was already terminated, then we do not
// need to do anything.
if (!isCancelled(fetchParams)) {
fetchParams.controller.abort(reason)
}
}
// 13. Let highWaterMark be a non-negative, non-NaN number, chosen by
// the user agent.
// TODO
// 14. Let sizeAlgorithm be an algorithm that accepts a chunk object
// and returns a non-negative, non-NaN, non-infinite number, chosen by the user agent.
// TODO
// 15. Let stream be a new ReadableStream.
// 16. Set up stream with byte reading support with pullAlgorithm set to pullAlgorithm,
// cancelAlgorithm set to cancelAlgorithm.
const stream = new ReadableStream(
{
start (controller) {
fetchParams.controller.controller = controller
},
pull: pullAlgorithm,
cancel: cancelAlgorithm,
type: 'bytes'
}
)
// 17. Run these steps, but abort when the ongoing fetch is terminated:
// 1. Set response’s body to a new body whose stream is stream.
response.body = { stream, source: null, length: null }
// 2. If response is not a network error and request’s cache mode is
// not "no-store", then update response in httpCache for request.
// TODO
// 3. If includeCredentials is true and the user agent is not configured
// to block cookies for request (see section 7 of [COOKIES]), then run the
// "set-cookie-string" parsing algorithm (see section 5.2 of [COOKIES]) on
// the value of each header whose name is a byte-case-insensitive match for
// `Set-Cookie` in response’s header list, if any, and request’s current URL.
// TODO
// 18. If aborted, then:
// TODO
// 19. Run these steps in parallel:
// 1. Run these steps, but abort when fetchParams is canceled:
if (!fetchParams.controller.resume) {
fetchParams.controller.on('terminated', onAborted)
}
fetchParams.controller.resume = async () => {
// 1. While true
while (true) {
// 1-3. See onData...
// 4. Set bytes to the result of handling content codings given
// codings and bytes.
let bytes
let isFailure
try {
const { done, value } = await fetchParams.controller.next()
if (isAborted(fetchParams)) {
break
}
bytes = done ? undefined : value
} catch (err) {
if (fetchParams.controller.ended && !timingInfo.encodedBodySize) {
// zlib doesn't like empty streams.
bytes = undefined
} else {
bytes = err
// err may be propagated from the result of calling readablestream.cancel,
// which might not be an error. https://github.com/nodejs/undici/issues/2009
isFailure = true
}
}
if (bytes === undefined) {
// 2. Otherwise, if the bytes transmission for response’s message
// body is done normally and stream is readable, then close
// stream, finalize response for fetchParams and response, and
// abort these in-parallel steps.
readableStreamClose(fetchParams.controller.controller)
finalizeResponse(fetchParams, response)
return
}
// 5. Increase timingInfo’s decoded body size by bytes’s length.
timingInfo.decodedBodySize += bytes?.byteLength ?? 0
// 6. If bytes is failure, then terminate fetchParams’s controller.
if (isFailure) {
fetchParams.controller.terminate(bytes)
return
}
// 7. Enqueue a Uint8Array wrapping an ArrayBuffer containing bytes
// into stream.
const buffer = new Uint8Array(bytes)
if (buffer.byteLength) {
fetchParams.controller.controller.enqueue(buffer)
}
// 8. If stream is errored, then terminate the ongoing fetch.
if (isErrored(stream)) {
fetchParams.controller.terminate()
return
}
// 9. If stream doesn’t need more data ask the user agent to suspend
// the ongoing fetch.
if (fetchParams.controller.controller.desiredSize <= 0) {
return
}
}
}
// 2. If aborted, then:
function onAborted (reason) {
// 2. If fetchParams is aborted, then:
if (isAborted(fetchParams)) {
// 1. Set response’s aborted flag.
response.aborted = true
// 2. If stream is readable, then error stream with the result of
// deserialize a serialized abort reason given fetchParams’s
// controller’s serialized abort reason and an
// implementation-defined realm.
if (isReadable(stream)) {
fetchParams.controller.controller.error(
fetchParams.controller.serializedAbortReason
)
}
} else {
// 3. Otherwise, if stream is readable, error stream with a TypeError.
if (isReadable(stream)) {
fetchParams.controller.controller.error(new TypeError('terminated', {
cause: isErrorLike(reason) ? reason : undefined
}))
}
}
// 4. If connection uses HTTP/2, then transmit an RST_STREAM frame.
// 5. Otherwise, the user agent should close connection unless it would be bad for performance to do so.
fetchParams.controller.connection.destroy()
}
// 20. Return response.
return response
function dispatch ({ body }) {
const url = requestCurrentURL(request)
/** @type {import('../../..').Agent} */
const agent = fetchParams.controller.dispatcher
const path = url.pathname + url.search
const hasTrailingQuestionMark = url.search.length === 0 && url.href[url.href.length - url.hash.length - 1] === '?'
return new Promise((resolve, reject) => agent.dispatch(
{
path: hasTrailingQuestionMark ? `${path}?` : path,
origin: url.origin,
method: request.method,
body: agent.isMockActive ? request.body && (request.body.source || request.body.stream) : body,
headers: request.headersList.entries,
maxRedirections: 0,
upgrade: request.mode === 'websocket' ? 'websocket' : undefined
},
{
body: null,
abort: null,
onConnect (abort) {
// TODO (fix): Do we need connection here?
const { connection } = fetchParams.controller
// Set timingInfo’s final connection timing info to the result of calling clamp and coarsen
// connection timing info with connection’s timing info, timingInfo’s post-redirect start
// time, and fetchParams’s cross-origin isolated capability.
// TODO: implement connection timing
timingInfo.finalConnectionTimingInfo = clampAndCoarsenConnectionTimingInfo(undefined, timingInfo.postRedirectStartTime, fetchParams.crossOriginIsolatedCapability)
if (connection.destroyed) {
abort(new DOMException('The operation was aborted.', 'AbortError'))
} else {
fetchParams.controller.on('terminated', abort)
this.abort = connection.abort = abort
}
// Set timingInfo’s final network-request start time to the coarsened shared current time given
// fetchParams’s cross-origin isolated capability.
timingInfo.finalNetworkRequestStartTime = coarsenedSharedCurrentTime(fetchParams.crossOriginIsolatedCapability)
},
onResponseStarted () {
// Set timingInfo’s final network-response start time to the coarsened shared current
// time given fetchParams’s cross-origin isolated capability, immediately after the
// user agent’s HTTP parser receives the first byte of the response (e.g., frame header
// bytes for HTTP/2 or response status line for HTTP/1.x).
timingInfo.finalNetworkResponseStartTime = coarsenedSharedCurrentTime(fetchParams.crossOriginIsolatedCapability)
},
onHeaders (status, rawHeaders, resume, statusText) {
if (status < 200) {
return false
}
const headersList = new HeadersList()
for (let i = 0; i < rawHeaders.length; i += 2) {
headersList.append(bufferToLowerCasedHeaderName(rawHeaders[i]), rawHeaders[i + 1].toString('latin1'), true)
}
const location = headersList.get('location', true)
this.body = new Readable({ read: resume })
const willFollow = location && request.redirect === 'follow' &&
redirectStatusSet.has(status)
const decoders = []
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Encoding
if (request.method !== 'HEAD' && request.method !== 'CONNECT' && !nullBodyStatus.includes(status) && !willFollow) {
// https://www.rfc-editor.org/rfc/rfc7231#section-3.1.2.1
const contentEncoding = headersList.get('content-encoding', true)
// "All content-coding values are case-insensitive..."
/** @type {string[]} */
const codings = contentEncoding ? contentEncoding.toLowerCase().split(',') : []
// Limit the number of content-encodings to prevent resource exhaustion.
// CVE fix similar to urllib3 (GHSA-gm62-xv2j-4w53) and curl (CVE-2022-32206).
const maxContentEncodings = 5
if (codings.length > maxContentEncodings) {
reject(new Error(`too many content-encodings in response: ${codings.length}, maximum allowed is ${maxContentEncodings}`))
return true
}
for (let i = codings.length - 1; i >= 0; --i) {
const coding = codings[i].trim()
// https://www.rfc-editor.org/rfc/rfc9112.html#section-7.2
if (coding === 'x-gzip' || coding === 'gzip') {
decoders.push(zlib.createGunzip({
// Be less strict when decoding compressed responses, since sometimes
// servers send slightly invalid responses that are still accepted
// by common browsers.
// Always using Z_SYNC_FLUSH is what cURL does.
flush: zlib.constants.Z_SYNC_FLUSH,
finishFlush: zlib.constants.Z_SYNC_FLUSH
}))
} else if (coding === 'deflate') {
decoders.push(createInflate({
flush: zlib.constants.Z_SYNC_FLUSH,
finishFlush: zlib.constants.Z_SYNC_FLUSH
}))
} else if (coding === 'br') {
decoders.push(zlib.createBrotliDecompress({
flush: zlib.constants.BROTLI_OPERATION_FLUSH,
finishFlush: zlib.constants.BROTLI_OPERATION_FLUSH
}))
} else if (coding === 'zstd' && hasZstd) {
decoders.push(zlib.createZstdDecompress({
flush: zlib.constants.ZSTD_e_continue,
finishFlush: zlib.constants.ZSTD_e_end
}))
} else {
decoders.length = 0
break
}
}
}
const onError = this.onError.bind(this)
resolve({
status,
statusText,
headersList,
body: decoders.length
? pipeline(this.body, ...decoders, (err) => {
if (err) {
this.onError(err)
}
}).on('error', onError)
: this.body.on('error', onError)
})
return true
},
onData (chunk) {
if (fetchParams.controller.dump) {
return
}
// 1. If one or more bytes have been transmitted from response’s
// message body, then:
// 1. Let bytes be the transmitted bytes.
const bytes = chunk
// 2. Let codings be the result of extracting header list values
// given `Content-Encoding` and response’s header list.
// See pullAlgorithm.
// 3. Increase timingInfo’s encoded body size by bytes’s length.
timingInfo.encodedBodySize += bytes.byteLength
// 4. See pullAlgorithm...
return this.body.push(bytes)
},
onComplete () {
if (this.abort) {
fetchParams.controller.off('terminated', this.abort)
}
fetchParams.controller.ended = true
this.body.push(null)
},
onError (error) {
if (this.abort) {
fetchParams.controller.off('terminated', this.abort)
}
this.body?.destroy(error)
fetchParams.controller.terminate(error)
reject(error)
},
onRequestUpgrade (_controller, status, headers, socket) {
// We need to support 200 for websocket over h2 as per RFC-8441
// Absence of session means H1
if ((socket.session != null && status !== 200) || (socket.session == null && status !== 101)) {
return false
}
const headersList = new HeadersList()
for (const [name, value] of Object.entries(headers)) {
if (value == null) {
continue
}
const headerName = name.toLowerCase()
if (Array.isArray(value)) {
for (const entry of value) {
headersList.append(headerName, String(entry), true)
}
} else {
headersList.append(headerName, String(value), true)
}
}
resolve({
status,
statusText: STATUS_CODES[status],
headersList,
socket
})
return true
},
onUpgrade (status, rawHeaders, socket) {
// We need to support 200 for websocket over h2 as per RFC-8441
// Absence of session means H1
if ((socket.session != null && status !== 200) || (socket.session == null && status !== 101)) {
return false
}
const headersList = new HeadersList()
for (let i = 0; i < rawHeaders.length; i += 2) {
headersList.append(bufferToLowerCasedHeaderName(rawHeaders[i]), rawHeaders[i + 1].toString('latin1'), true)
}
resolve({
status,
statusText: STATUS_CODES[status],
headersList,
socket
})
return true
}
}
))
}
}
module.exports = {
fetch,
Fetch,
fetching,
finalizeAndReportTiming
}
================================================
FILE: lib/web/fetch/request.js
================================================
/* globals AbortController */
'use strict'
const { extractBody, mixinBody, cloneBody, bodyUnusable } = require('./body')
const { Headers, fill: fillHeaders, HeadersList, setHeadersGuard, getHeadersGuard, setHeadersList, getHeadersList } = require('./headers')
const util = require('../../core/util')
const nodeUtil = require('node:util')
const {
isValidHTTPToken,
sameOrigin,
environmentSettingsObject
} = require('./util')
const {
forbiddenMethodsSet,
corsSafeListedMethodsSet,
referrerPolicy,
requestRedirect,
requestMode,
requestCredentials,
requestCache,
requestDuplex
} = require('./constants')
const { kEnumerableProperty, normalizedMethodRecordsBase, normalizedMethodRecords } = util
const { webidl } = require('../webidl')
const { URLSerializer } = require('./data-url')
const { kConstruct } = require('../../core/symbols')
const assert = require('node:assert')
const { getMaxListeners, setMaxListeners, defaultMaxListeners } = require('node:events')
const kAbortController = Symbol('abortController')
const requestFinalizer = new FinalizationRegistry(({ signal, abort }) => {
signal.removeEventListener('abort', abort)
})
const dependentControllerMap = new WeakMap()
let abortSignalHasEventHandlerLeakWarning
try {
abortSignalHasEventHandlerLeakWarning = getMaxListeners(new AbortController().signal) > 0
} catch {
abortSignalHasEventHandlerLeakWarning = false
}
function buildAbort (acRef) {
return abort
function abort () {
const ac = acRef.deref()
if (ac !== undefined) {
// Currently, there is a problem with FinalizationRegistry.
// https://github.com/nodejs/node/issues/49344
// https://github.com/nodejs/node/issues/47748
// In the case of abort, the first step is to unregister from it.
// If the controller can refer to it, it is still registered.
// It will be removed in the future.
requestFinalizer.unregister(abort)
// Unsubscribe a listener.
// FinalizationRegistry will no longer be called, so this must be done.
this.removeEventListener('abort', abort)
ac.abort(this.reason)
const controllerList = dependentControllerMap.get(ac.signal)
if (controllerList !== undefined) {
if (controllerList.size !== 0) {
for (const ref of controllerList) {
const ctrl = ref.deref()
if (ctrl !== undefined) {
ctrl.abort(this.reason)
}
}
controllerList.clear()
}
dependentControllerMap.delete(ac.signal)
}
}
}
}
let patchMethodWarning = false
// https://fetch.spec.whatwg.org/#request-class
class Request {
/** @type {AbortSignal} */
#signal
/** @type {import('../../dispatcher/dispatcher')} */
#dispatcher
/** @type {Headers} */
#headers
#state
// https://fetch.spec.whatwg.org/#dom-request
constructor (input, init = undefined) {
webidl.util.markAsUncloneable(this)
if (input === kConstruct) {
return
}
const prefix = 'Request constructor'
webidl.argumentLengthCheck(arguments, 1, prefix)
input = webidl.converters.RequestInfo(input)
init = webidl.converters.RequestInit(init)
// 1. Let request be null.
let request = null
// 2. Let fallbackMode be null.
let fallbackMode = null
// 3. Let baseURL be this’s relevant settings object’s API base URL.
const baseUrl = environmentSettingsObject.settingsObject.baseUrl
// 4. Let signal be null.
let signal = null
// 5. If input is a string, then:
if (typeof input === 'string') {
this.#dispatcher = init.dispatcher
// 1. Let parsedURL be the result of parsing input with baseURL.
// 2. If parsedURL is failure, then throw a TypeError.
let parsedURL
try {
parsedURL = new URL(input, baseUrl)
} catch (err) {
throw new TypeError('Failed to parse URL from ' + input, { cause: err })
}
// 3. If parsedURL includes credentials, then throw a TypeError.
if (parsedURL.username || parsedURL.password) {
throw new TypeError(
'Request cannot be constructed from a URL that includes credentials: ' +
input
)
}
// 4. Set request to a new request whose URL is parsedURL.
request = makeRequest({ urlList: [parsedURL] })
// 5. Set fallbackMode to "cors".
fallbackMode = 'cors'
} else {
// 6. Otherwise:
// 7. Assert: input is a Request object.
assert(webidl.is.Request(input))
// 8. Set request to input’s request.
request = input.#state
// 9. Set signal to input’s signal.
signal = input.#signal
this.#dispatcher = init.dispatcher || input.#dispatcher
}
// 7. Let origin be this’s relevant settings object’s origin.
const origin = environmentSettingsObject.settingsObject.origin
// 8. Let window be "client".
let window = 'client'
// 9. If request’s window is an environment settings object and its origin
// is same origin with origin, then set window to request’s window.
if (
request.window?.constructor?.name === 'EnvironmentSettingsObject' &&
sameOrigin(request.window, origin)
) {
window = request.window
}
// 10. If init["window"] exists and is non-null, then throw a TypeError.
if (init.window != null) {
throw new TypeError(`'window' option '${window}' must be null`)
}
// 11. If init["window"] exists, then set window to "no-window".
if ('window' in init) {
window = 'no-window'
}
// 12. Set request to a new request with the following properties:
request = makeRequest({
// URL request’s URL.
// undici implementation note: this is set as the first item in request's urlList in makeRequest
// method request’s method.
method: request.method,
// header list A copy of request’s header list.
// undici implementation note: headersList is cloned in makeRequest
headersList: request.headersList,
// unsafe-request flag Set.
unsafeRequest: request.unsafeRequest,
// client This’s relevant settings object.
client: environmentSettingsObject.settingsObject,
// window window.
window,
// priority request’s priority.
priority: request.priority,
// origin request’s origin. The propagation of the origin is only significant for navigation requests
// being handled by a service worker. In this scenario a request can have an origin that is different
// from the current client.
origin: request.origin,
// referrer request’s referrer.
referrer: request.referrer,
// referrer policy request’s referrer policy.
referrerPolicy: request.referrerPolicy,
// mode request’s mode.
mode: request.mode,
// credentials mode request’s credentials mode.
credentials: request.credentials,
// cache mode request’s cache mode.
cache: request.cache,
// redirect mode request’s redirect mode.
redirect: request.redirect,
// integrity metadata request’s integrity metadata.
integrity: request.integrity,
// keepalive request’s keepalive.
keepalive: request.keepalive,
// reload-navigation flag request’s reload-navigation flag.
reloadNavigation: request.reloadNavigation,
// history-navigation flag request’s history-navigation flag.
historyNavigation: request.historyNavigation,
// URL list A clone of request’s URL list.
urlList: [...request.urlList]
})
const initHasKey = Object.keys(init).length !== 0
// 13. If init is not empty, then:
if (initHasKey) {
// 1. If request’s mode is "navigate", then set it to "same-origin".
if (request.mode === 'navigate') {
request.mode = 'same-origin'
}
// 2. Unset request’s reload-navigation flag.
request.reloadNavigation = false
// 3. Unset request’s history-navigation flag.
request.historyNavigation = false
// 4. Set request’s origin to "client".
request.origin = 'client'
// 5. Set request’s referrer to "client"
request.referrer = 'client'
// 6. Set request’s referrer policy to the empty string.
request.referrerPolicy = ''
// 7. Set request’s URL to request’s current URL.
request.url = request.urlList[request.urlList.length - 1]
// 8. Set request’s URL list to « request’s URL ».
request.urlList = [request.url]
}
// 14. If init["referrer"] exists, then:
if (init.referrer !== undefined) {
// 1. Let referrer be init["referrer"].
const referrer = init.referrer
// 2. If referrer is the empty string, then set request’s referrer to "no-referrer".
if (referrer === '') {
request.referrer = 'no-referrer'
} else {
// 1. Let parsedReferrer be the result of parsing referrer with
// baseURL.
// 2. If parsedReferrer is failure, then throw a TypeError.
let parsedReferrer
try {
parsedReferrer = new URL(referrer, baseUrl)
} catch (err) {
throw new TypeError(`Referrer "${referrer}" is not a valid URL.`, { cause: err })
}
// 3. If one of the following is true
// - parsedReferrer’s scheme is "about" and path is the string "client"
// - parsedReferrer’s origin is not same origin with origin
// then set request’s referrer to "client".
if (
(parsedReferrer.protocol === 'about:' && parsedReferrer.hostname === 'client') ||
(origin && !sameOrigin(parsedReferrer, environmentSettingsObject.settingsObject.baseUrl))
) {
request.referrer = 'client'
} else {
// 4. Otherwise, set request’s referrer to parsedReferrer.
request.referrer = parsedReferrer
}
}
}
// 15. If init["referrerPolicy"] exists, then set request’s referrer policy
// to it.
if (init.referrerPolicy !== undefined) {
request.referrerPolicy = init.referrerPolicy
}
// 16. Let mode be init["mode"] if it exists, and fallbackMode otherwise.
let mode
if (init.mode !== undefined) {
mode = init.mode
} else {
mode = fallbackMode
}
// 17. If mode is "navigate", then throw a TypeError.
if (mode === 'navigate') {
throw webidl.errors.exception({
header: 'Request constructor',
message: 'invalid request mode navigate.'
})
}
// 18. If mode is non-null, set request’s mode to mode.
if (mode != null) {
request.mode = mode
}
// 19. If init["credentials"] exists, then set request’s credentials mode
// to it.
if (init.credentials !== undefined) {
request.credentials = init.credentials
}
// 18. If init["cache"] exists, then set request’s cache mode to it.
if (init.cache !== undefined) {
request.cache = init.cache
}
// 21. If request’s cache mode is "only-if-cached" and request’s mode is
// not "same-origin", then throw a TypeError.
if (request.cache === 'only-if-cached' && request.mode !== 'same-origin') {
throw new TypeError(
"'only-if-cached' can be set only with 'same-origin' mode"
)
}
// 22. If init["redirect"] exists, then set request’s redirect mode to it.
if (init.redirect !== undefined) {
request.redirect = init.redirect
}
// 23. If init["integrity"] exists, then set request’s integrity metadata to it.
if (init.integrity != null) {
request.integrity = String(init.integrity)
}
// 24. If init["keepalive"] exists, then set request’s keepalive to it.
if (init.keepalive !== undefined) {
request.keepalive = Boolean(init.keepalive)
}
// 25. If init["method"] exists, then:
if (init.method !== undefined) {
// 1. Let method be init["method"].
let method = init.method
const mayBeNormalized = normalizedMethodRecords[method]
if (mayBeNormalized !== undefined) {
// Note: Bypass validation DELETE, GET, HEAD, OPTIONS, POST, PUT, PATCH and these lowercase ones
request.method = mayBeNormalized
} else {
// 2. If method is not a method or method is a forbidden method, then
// throw a TypeError.
if (!isValidHTTPToken(method)) {
throw new TypeError(`'${method}' is not a valid HTTP method.`)
}
const upperCase = method.toUpperCase()
if (forbiddenMethodsSet.has(upperCase)) {
throw new TypeError(`'${method}' HTTP method is unsupported.`)
}
// 3. Normalize method.
// https://fetch.spec.whatwg.org/#concept-method-normalize
// Note: must be in uppercase
method = normalizedMethodRecordsBase[upperCase] ?? method
// 4. Set request’s method to method.
request.method = method
}
if (!patchMethodWarning && request.method === 'patch') {
process.emitWarning('Using `patch` is highly likely to result in a `405 Method Not Allowed`. `PATCH` is much more likely to succeed.', {
code: 'UNDICI-FETCH-patch'
})
patchMethodWarning = true
}
}
// 26. If init["signal"] exists, then set signal to it.
if (init.signal !== undefined) {
signal = init.signal
}
// 27. Set this’s request to request.
this.#state = request
// 28. Set this’s signal to a new AbortSignal object with this’s relevant
// Realm.
// TODO: could this be simplified with AbortSignal.any
// (https://dom.spec.whatwg.org/#dom-abortsignal-any)
const ac = new AbortController()
this.#signal = ac.signal
// 29. If signal is not null, then make this’s signal follow signal.
if (signal != null) {
if (signal.aborted) {
ac.abort(signal.reason)
} else {
// Keep a strong ref to ac while request object
// is alive. This is needed to prevent AbortController
// from being prematurely garbage collected.
// See, https://github.com/nodejs/undici/issues/1926.
this[kAbortController] = ac
const acRef = new WeakRef(ac)
const abort = buildAbort(acRef)
// If the max amount of listeners is equal to the default, increase it
if (abortSignalHasEventHandlerLeakWarning && getMaxListeners(signal) === defaultMaxListeners) {
setMaxListeners(1500, signal)
}
util.addAbortListener(signal, abort)
// The third argument must be a registry key to be unregistered.
// Without it, you cannot unregister.
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/FinalizationRegistry
// abort is used as the unregister key. (because it is unique)
requestFinalizer.register(ac, { signal, abort }, abort)
}
}
// 30. Set this’s headers to a new Headers object with this’s relevant
// Realm, whose header list is request’s header list and guard is
// "request".
this.#headers = new Headers(kConstruct)
setHeadersList(this.#headers, request.headersList)
setHeadersGuard(this.#headers, 'request')
// 31. If this’s request’s mode is "no-cors", then:
if (mode === 'no-cors') {
// 1. If this’s request’s method is not a CORS-safelisted method,
// then throw a TypeError.
if (!corsSafeListedMethodsSet.has(request.method)) {
throw new TypeError(
`'${request.method} is unsupported in no-cors mode.`
)
}
// 2. Set this’s headers’s guard to "request-no-cors".
setHeadersGuard(this.#headers, 'request-no-cors')
}
// 32. If init is not empty, then:
if (initHasKey) {
/** @type {HeadersList} */
const headersList = getHeadersList(this.#headers)
// 1. Let headers be a copy of this’s headers and its associated header
// list.
// 2. If init["headers"] exists, then set headers to init["headers"].
const headers = init.headers !== undefined ? init.headers : new HeadersList(headersList)
// 3. Empty this’s headers’s header list.
headersList.clear()
// 4. If headers is a Headers object, then for each header in its header
// list, append header’s name/header’s value to this’s headers.
if (headers instanceof HeadersList) {
for (const { name, value } of headers.rawValues()) {
headersList.append(name, value, false)
}
// Note: Copy the `set-cookie` meta-data.
headersList.cookies = headers.cookies
} else {
// 5. Otherwise, fill this’s headers with headers.
fillHeaders(this.#headers, headers)
}
}
// 33. Let inputBody be input’s request’s body if input is a Request
// object; otherwise null.
const inputBody = webidl.is.Request(input) ? input.#state.body : null
// 34. If either init["body"] exists and is non-null or inputBody is
// non-null, and request’s method is `GET` or `HEAD`, then throw a
// TypeError.
if (
(init.body != null || inputBody != null) &&
(request.method === 'GET' || request.method === 'HEAD')
) {
throw new TypeError('Request with GET/HEAD method cannot have body.')
}
// 35. Let initBody be null.
let initBody = null
// 36. If init["body"] exists and is non-null, then:
if (init.body != null) {
// 1. Let Content-Type be null.
// 2. Set initBody and Content-Type to the result of extracting
// init["body"], with keepalive set to request’s keepalive.
const [extractedBody, contentType] = extractBody(
init.body,
request.keepalive
)
initBody = extractedBody
// 3, If Content-Type is non-null and this’s headers’s header list does
// not contain `Content-Type`, then append `Content-Type`/Content-Type to
// this’s headers.
if (contentType && !getHeadersList(this.#headers).contains('content-type', true)) {
this.#headers.append('content-type', contentType, true)
}
}
// 37. Let inputOrInitBody be initBody if it is non-null; otherwise
// inputBody.
const inputOrInitBody = initBody ?? inputBody
// 38. If inputOrInitBody is non-null and inputOrInitBody’s source is
// null, then:
if (inputOrInitBody != null && inputOrInitBody.source == null) {
// 1. If initBody is non-null and init["duplex"] does not exist,
// then throw a TypeError.
if (initBody != null && init.duplex == null) {
throw new TypeError('RequestInit: duplex option is required when sending a body.')
}
// 2. If this’s request’s mode is neither "same-origin" nor "cors",
// then throw a TypeError.
if (request.mode !== 'same-origin' && request.mode !== 'cors') {
throw new TypeError(
'If request is made from ReadableStream, mode should be "same-origin" or "cors"'
)
}
// 3. Set this’s request’s use-CORS-preflight flag.
request.useCORSPreflightFlag = true
}
// 39. Let finalBody be inputOrInitBody.
let finalBody = inputOrInitBody
// 40. If initBody is null and inputBody is non-null, then:
if (initBody == null && inputBody != null) {
// 1. If input is unusable, then throw a TypeError.
if (bodyUnusable(input.#state)) {
throw new TypeError(
'Cannot construct a Request with a Request object that has already been used.'
)
}
// 2. Set finalBody to the result of creating a proxy for inputBody.
// https://streams.spec.whatwg.org/#readablestream-create-a-proxy
const identityTransform = new TransformStream()
inputBody.stream.pipeThrough(identityTransform)
finalBody = {
source: inputBody.source,
length: inputBody.length,
stream: identityTransform.readable
}
}
// 41. Set this’s request’s body to finalBody.
this.#state.body = finalBody
}
// Returns request’s HTTP method, which is "GET" by default.
get method () {
webidl.brandCheck(this, Request)
// The method getter steps are to return this’s request’s method.
return this.#state.method
}
// Returns the URL of request as a string.
get url () {
webidl.brandCheck(this, Request)
// The url getter steps are to return this’s request’s URL, serialized.
return URLSerializer(this.#state.url)
}
// Returns a Headers object consisting of the headers associated with request.
// Note that headers added in the network layer by the user agent will not
// be accounted for in this object, e.g., the "Host" header.
get headers () {
webidl.brandCheck(this, Request)
// The headers getter steps are to return this’s headers.
return this.#headers
}
// Returns the kind of resource requested by request, e.g., "document"
// or "script".
get destination () {
webidl.brandCheck(this, Request)
// The destination getter are to return this’s request’s destination.
return this.#state.destination
}
// Returns the referrer of request. Its value can be a same-origin URL if
// explicitly set in init, the empty string to indicate no referrer, and
// "about:client" when defaulting to the global’s default. This is used
// during fetching to determine the value of the `Referer` header of the
// request being made.
get referrer () {
webidl.brandCheck(this, Request)
// 1. If this’s request’s referrer is "no-referrer", then return the
// empty string.
if (this.#state.referrer === 'no-referrer') {
return ''
}
// 2. If this’s request’s referrer is "client", then return
// "about:client".
if (this.#state.referrer === 'client') {
return 'about:client'
}
// Return this’s request’s referrer, serialized.
return this.#state.referrer.toString()
}
// Returns the referrer policy associated with request.
// This is used during fetching to compute the value of the request’s
// referrer.
get referrerPolicy () {
webidl.brandCheck(this, Request)
// The referrerPolicy getter steps are to return this’s request’s referrer policy.
return this.#state.referrerPolicy
}
// Returns the mode associated with request, which is a string indicating
// whether the request will use CORS, or will be restricted to same-origin
// URLs.
get mode () {
webidl.brandCheck(this, Request)
// The mode getter steps are to return this’s request’s mode.
return this.#state.mode
}
// Returns the credentials mode associated with request,
// which is a string indicating whether credentials will be sent with the
// request always, never, or only when sent to a same-origin URL.
get credentials () {
webidl.brandCheck(this, Request)
// The credentials getter steps are to return this’s request’s credentials mode.
return this.#state.credentials
}
// Returns the cache mode associated with request,
// which is a string indicating how the request will
// interact with the browser’s cache when fetching.
get cache () {
webidl.brandCheck(this, Request)
// The cache getter steps are to return this’s request’s cache mode.
return this.#state.cache
}
// Returns the redirect mode associated with request,
// which is a string indicating how redirects for the
// request will be handled during fetching. A request
// will follow redirects by default.
get redirect () {
webidl.brandCheck(this, Request)
// The redirect getter steps are to return this’s request’s redirect mode.
return this.#state.redirect
}
// Returns request’s subresource integrity metadata, which is a
// cryptographic hash of the resource being fetched. Its value
// consists of multiple hashes separated by whitespace. [SRI]
get integrity () {
webidl.brandCheck(this, Request)
// The integrity getter steps are to return this’s request’s integrity
// metadata.
return this.#state.integrity
}
// Returns a boolean indicating whether or not request can outlive the
// global in which it was created.
get keepalive () {
webidl.brandCheck(this, Request)
// The keepalive getter steps are to return this’s request’s keepalive.
return this.#state.keepalive
}
// Returns a boolean indicating whether or not request is for a reload
// navigation.
get isReloadNavigation () {
webidl.brandCheck(this, Request)
// The isReloadNavigation getter steps are to return true if this’s
// request’s reload-navigation flag is set; otherwise false.
return this.#state.reloadNavigation
}
// Returns a boolean indicating whether or not request is for a history
// navigation (a.k.a. back-forward navigation).
get isHistoryNavigation () {
webidl.brandCheck(this, Request)
// The isHistoryNavigation getter steps are to return true if this’s request’s
// history-navigation flag is set; otherwise false.
return this.#state.historyNavigation
}
// Returns the signal associated with request, which is an AbortSignal
// object indicating whether or not request has been aborted, and its
// abort event handler.
get signal () {
webidl.brandCheck(this, Request)
// The signal getter steps are to return this’s signal.
return this.#signal
}
get body () {
webidl.brandCheck(this, Request)
return this.#state.body ? this.#state.body.stream : null
}
get bodyUsed () {
webidl.brandCheck(this, Request)
return !!this.#state.body && util.isDisturbed(this.#state.body.stream)
}
get duplex () {
webidl.brandCheck(this, Request)
return 'half'
}
// Returns a clone of request.
clone () {
webidl.brandCheck(this, Request)
// 1. If this is unusable, then throw a TypeError.
if (bodyUnusable(this.#state)) {
throw new TypeError('unusable')
}
// 2. Let clonedRequest be the result of cloning this’s request.
const clonedRequest = cloneRequest(this.#state)
// 3. Let clonedRequestObject be the result of creating a Request object,
// given clonedRequest, this’s headers’s guard, and this’s relevant Realm.
// 4. Make clonedRequestObject’s signal follow this’s signal.
const ac = new AbortController()
if (this.signal.aborted) {
ac.abort(this.signal.reason)
} else {
let list = dependentControllerMap.get(this.signal)
if (list === undefined) {
list = new Set()
dependentControllerMap.set(this.signal, list)
}
const acRef = new WeakRef(ac)
list.add(acRef)
util.addAbortListener(
ac.signal,
buildAbort(acRef)
)
}
// 4. Return clonedRequestObject.
return fromInnerRequest(clonedRequest, this.#dispatcher, ac.signal, getHeadersGuard(this.#headers))
}
[nodeUtil.inspect.custom] (depth, options) {
if (options.depth === null) {
options.depth = 2
}
options.colors ??= true
const properties = {
method: this.method,
url: this.url,
headers: this.headers,
destination: this.destination,
referrer: this.referrer,
referrerPolicy: this.referrerPolicy,
mode: this.mode,
credentials: this.credentials,
cache: this.cache,
redirect: this.redirect,
integrity: this.integrity,
keepalive: this.keepalive,
isReloadNavigation: this.isReloadNavigation,
isHistoryNavigation: this.isHistoryNavigation,
signal: this.signal
}
return `Request ${nodeUtil.formatWithOptions(options, properties)}`
}
/**
* @param {Request} request
* @param {AbortSignal} newSignal
*/
static setRequestSignal (request, newSignal) {
request.#signal = newSignal
return request
}
/**
* @param {Request} request
*/
static getRequestDispatcher (request) {
return request.#dispatcher
}
/**
* @param {Request} request
* @param {import('../../dispatcher/dispatcher')} newDispatcher
*/
static setRequestDispatcher (request, newDispatcher) {
request.#dispatcher = newDispatcher
}
/**
* @param {Request} request
* @param {Headers} newHeaders
*/
static setRequestHeaders (request, newHeaders) {
request.#headers = newHeaders
}
/**
* @param {Request} request
*/
static getRequestState (request) {
return request.#state
}
/**
* @param {Request} request
* @param {any} newState
*/
static setRequestState (request, newState) {
request.#state = newState
}
}
const { setRequestSignal, getRequestDispatcher, setRequestDispatcher, setRequestHeaders, getRequestState, setRequestState } = Request
Reflect.deleteProperty(Request, 'setRequestSignal')
Reflect.deleteProperty(Request, 'getRequestDispatcher')
Reflect.deleteProperty(Request, 'setRequestDispatcher')
Reflect.deleteProperty(Request, 'setRequestHeaders')
Reflect.deleteProperty(Request, 'getRequestState')
Reflect.deleteProperty(Request, 'setRequestState')
mixinBody(Request, getRequestState)
// https://fetch.spec.whatwg.org/#requests
function makeRequest (init) {
return {
method: init.method ?? 'GET',
localURLsOnly: init.localURLsOnly ?? false,
unsafeRequest: init.unsafeRequest ?? false,
body: init.body ?? null,
client: init.client ?? null,
reservedClient: init.reservedClient ?? null,
replacesClientId: init.replacesClientId ?? '',
window: init.window ?? 'client',
keepalive: init.keepalive ?? false,
serviceWorkers: init.serviceWorkers ?? 'all',
initiator: init.initiator ?? '',
destination: init.destination ?? '',
priority: init.priority ?? null,
origin: init.origin ?? 'client',
policyContainer: init.policyContainer ?? 'client',
referrer: init.referrer ?? 'client',
referrerPolicy: init.referrerPolicy ?? '',
mode: init.mode ?? 'no-cors',
useCORSPreflightFlag: init.useCORSPreflightFlag ?? false,
credentials: init.credentials ?? 'same-origin',
useCredentials: init.useCredentials ?? false,
cache: init.cache ?? 'default',
redirect: init.redirect ?? 'follow',
integrity: init.integrity ?? '',
cryptoGraphicsNonceMetadata: init.cryptoGraphicsNonceMetadata ?? '',
parserMetadata: init.parserMetadata ?? '',
reloadNavigation: init.reloadNavigation ?? false,
historyNavigation: init.historyNavigation ?? false,
userActivation: init.userActivation ?? false,
taintedOrigin: init.taintedOrigin ?? false,
redirectCount: init.redirectCount ?? 0,
responseTainting: init.responseTainting ?? 'basic',
preventNoCacheCacheControlHeaderModification: init.preventNoCacheCacheControlHeaderModification ?? false,
done: init.done ?? false,
timingAllowFailed: init.timingAllowFailed ?? false,
useURLCredentials: init.useURLCredentials ?? undefined,
traversableForUserPrompts: init.traversableForUserPrompts ?? 'client',
urlList: init.urlList,
url: init.urlList[0],
headersList: init.headersList
? new HeadersList(init.headersList)
: new HeadersList()
}
}
// https://fetch.spec.whatwg.org/#concept-request-clone
function cloneRequest (request) {
// To clone a request request, run these steps:
// 1. Let newRequest be a copy of request, except for its body.
const newRequest = makeRequest({ ...request, body: null })
// 2. If request’s body is non-null, set newRequest’s body to the
// result of cloning request’s body.
if (request.body != null) {
newRequest.body = cloneBody(request.body)
}
// 3. Return newRequest.
return newRequest
}
/**
* @see https://fetch.spec.whatwg.org/#request-create
* @param {any} innerRequest
* @param {import('../../dispatcher/agent')} dispatcher
* @param {AbortSignal} signal
* @param {'request' | 'immutable' | 'request-no-cors' | 'response' | 'none'} guard
* @returns {Request}
*/
function fromInnerRequest (innerRequest, dispatcher, signal, guard) {
const request = new Request(kConstruct)
setRequestState(request, innerRequest)
setRequestDispatcher(request, dispatcher)
setRequestSignal(request, signal)
const headers = new Headers(kConstruct)
setRequestHeaders(request, headers)
setHeadersList(headers, innerRequest.headersList)
setHeadersGuard(headers, guard)
return request
}
Object.defineProperties(Request.prototype, {
method: kEnumerableProperty,
url: kEnumerableProperty,
headers: kEnumerableProperty,
redirect: kEnumerableProperty,
clone: kEnumerableProperty,
signal: kEnumerableProperty,
duplex: kEnumerableProperty,
destination: kEnumerableProperty,
body: kEnumerableProperty,
bodyUsed: kEnumerableProperty,
isHistoryNavigation: kEnumerableProperty,
isReloadNavigation: kEnumerableProperty,
keepalive: kEnumerableProperty,
integrity: kEnumerableProperty,
cache: kEnumerableProperty,
credentials: kEnumerableProperty,
attribute: kEnumerableProperty,
referrerPolicy: kEnumerableProperty,
referrer: kEnumerableProperty,
mode: kEnumerableProperty,
[Symbol.toStringTag]: {
value: 'Request',
configurable: true
}
})
webidl.is.Request = webidl.util.MakeTypeAssertion(Request)
/**
* @param {*} V
* @returns {import('../../../types/fetch').Request|string}
*
* @see https://fetch.spec.whatwg.org/#requestinfo
*/
webidl.converters.RequestInfo = function (V) {
if (typeof V === 'string') {
return webidl.converters.USVString(V)
}
if (webidl.is.Request(V)) {
return V
}
return webidl.converters.USVString(V)
}
/**
* @param {*} V
* @returns {import('../../../types/fetch').RequestInit}
* @see https://fetch.spec.whatwg.org/#requestinit
*/
webidl.converters.RequestInit = webidl.dictionaryConverter([
{
key: 'method',
converter: webidl.converters.ByteString
},
{
key: 'headers',
converter: webidl.converters.HeadersInit
},
{
key: 'body',
converter: webidl.nullableConverter(
webidl.converters.BodyInit
)
},
{
key: 'referrer',
converter: webidl.converters.USVString
},
{
key: 'referrerPolicy',
converter: webidl.converters.DOMString,
// https://w3c.github.io/webappsec-referrer-policy/#referrer-policy
allowedValues: referrerPolicy
},
{
key: 'mode',
converter: webidl.converters.DOMString,
// https://fetch.spec.whatwg.org/#concept-request-mode
allowedValues: requestMode
},
{
key: 'credentials',
converter: webidl.converters.DOMString,
// https://fetch.spec.whatwg.org/#requestcredentials
allowedValues: requestCredentials
},
{
key: 'cache',
converter: webidl.converters.DOMString,
// https://fetch.spec.whatwg.org/#requestcache
allowedValues: requestCache
},
{
key: 'redirect',
converter: webidl.converters.DOMString,
// https://fetch.spec.whatwg.org/#requestredirect
allowedValues: requestRedirect
},
{
key: 'integrity',
converter: webidl.converters.DOMString
},
{
key: 'keepalive',
converter: webidl.converters.boolean
},
{
key: 'signal',
converter: webidl.nullableConverter(
(signal) => webidl.converters.AbortSignal(
signal,
'RequestInit',
'signal'
)
)
},
{
key: 'window',
converter: webidl.converters.any
},
{
key: 'duplex',
converter: webidl.converters.DOMString,
allowedValues: requestDuplex
},
{
key: 'dispatcher', // undici specific option
converter: webidl.converters.any
},
{
key: 'priority',
converter: webidl.converters.DOMString,
allowedValues: ['high', 'low', 'auto'],
defaultValue: () => 'auto'
}
])
module.exports = {
Request,
makeRequest,
fromInnerRequest,
cloneRequest,
getRequestDispatcher,
getRequestState
}
================================================
FILE: lib/web/fetch/response.js
================================================
'use strict'
const { Headers, HeadersList, fill, getHeadersGuard, setHeadersGuard, setHeadersList } = require('./headers')
const { extractBody, cloneBody, mixinBody, streamRegistry, bodyUnusable } = require('./body')
const util = require('../../core/util')
const nodeUtil = require('node:util')
const { kEnumerableProperty } = util
const {
isValidReasonPhrase,
isCancelled,
isAborted,
isErrorLike,
environmentSettingsObject: relevantRealm
} = require('./util')
const {
redirectStatusSet,
nullBodyStatus
} = require('./constants')
const { webidl } = require('../webidl')
const { URLSerializer } = require('./data-url')
const { kConstruct } = require('../../core/symbols')
const assert = require('node:assert')
const { isomorphicEncode, serializeJavascriptValueToJSONString } = require('../infra')
const textEncoder = new TextEncoder('utf-8')
// https://fetch.spec.whatwg.org/#response-class
class Response {
/** @type {Headers} */
#headers
#state
// Creates network error Response.
static error () {
// The static error() method steps are to return the result of creating a
// Response object, given a new network error, "immutable", and this’s
// relevant Realm.
const responseObject = fromInnerResponse(makeNetworkError(), 'immutable')
return responseObject
}
// https://fetch.spec.whatwg.org/#dom-response-json
static json (data, init = undefined) {
webidl.argumentLengthCheck(arguments, 1, 'Response.json')
if (init !== null) {
init = webidl.converters.ResponseInit(init)
}
// 1. Let bytes the result of running serialize a JavaScript value to JSON bytes on data.
const bytes = textEncoder.encode(
serializeJavascriptValueToJSONString(data)
)
// 2. Let body be the result of extracting bytes.
const body = extractBody(bytes)
// 3. Let responseObject be the result of creating a Response object, given a new response,
// "response", and this’s relevant Realm.
const responseObject = fromInnerResponse(makeResponse({}), 'response')
// 4. Perform initialize a response given responseObject, init, and (body, "application/json").
initializeResponse(responseObject, init, { body: body[0], type: 'application/json' })
// 5. Return responseObject.
return responseObject
}
// Creates a redirect Response that redirects to url with status status.
static redirect (url, status = 302) {
webidl.argumentLengthCheck(arguments, 1, 'Response.redirect')
url = webidl.converters.USVString(url)
status = webidl.converters['unsigned short'](status)
// 1. Let parsedURL be the result of parsing url with current settings
// object’s API base URL.
// 2. If parsedURL is failure, then throw a TypeError.
// TODO: base-URL?
let parsedURL
try {
parsedURL = new URL(url, relevantRealm.settingsObject.baseUrl)
} catch (err) {
throw new TypeError(`Failed to parse URL from ${url}`, { cause: err })
}
// 3. If status is not a redirect status, then throw a RangeError.
if (!redirectStatusSet.has(status)) {
throw new RangeError(`Invalid status code ${status}`)
}
// 4. Let responseObject be the result of creating a Response object,
// given a new response, "immutable", and this’s relevant Realm.
const responseObject = fromInnerResponse(makeResponse({}), 'immutable')
// 5. Set responseObject’s response’s status to status.
responseObject.#state.status = status
// 6. Let value be parsedURL, serialized and isomorphic encoded.
const value = isomorphicEncode(URLSerializer(parsedURL))
// 7. Append `Location`/value to responseObject’s response’s header list.
responseObject.#state.headersList.append('location', value, true)
// 8. Return responseObject.
return responseObject
}
// https://fetch.spec.whatwg.org/#dom-response
constructor (body = null, init = undefined) {
webidl.util.markAsUncloneable(this)
if (body === kConstruct) {
return
}
if (body !== null) {
body = webidl.converters.BodyInit(body, 'Response', 'body')
}
init = webidl.converters.ResponseInit(init)
// 1. Set this’s response to a new response.
this.#state = makeResponse({})
// 2. Set this’s headers to a new Headers object with this’s relevant
// Realm, whose header list is this’s response’s header list and guard
// is "response".
this.#headers = new Headers(kConstruct)
setHeadersGuard(this.#headers, 'response')
setHeadersList(this.#headers, this.#state.headersList)
// 3. Let bodyWithType be null.
let bodyWithType = null
// 4. If body is non-null, then set bodyWithType to the result of extracting body.
if (body != null) {
const [extractedBody, type] = extractBody(body)
bodyWithType = { body: extractedBody, type }
}
// 5. Perform initialize a response given this, init, and bodyWithType.
initializeResponse(this, init, bodyWithType)
}
// Returns response’s type, e.g., "cors".
get type () {
webidl.brandCheck(this, Response)
// The type getter steps are to return this’s response’s type.
return this.#state.type
}
// Returns response’s URL, if it has one; otherwise the empty string.
get url () {
webidl.brandCheck(this, Response)
const urlList = this.#state.urlList
// The url getter steps are to return the empty string if this’s
// response’s URL is null; otherwise this’s response’s URL,
// serialized with exclude fragment set to true.
const url = urlList[urlList.length - 1] ?? null
if (url === null) {
return ''
}
return URLSerializer(url, true)
}
// Returns whether response was obtained through a redirect.
get redirected () {
webidl.brandCheck(this, Response)
// The redirected getter steps are to return true if this’s response’s URL
// list has more than one item; otherwise false.
return this.#state.urlList.length > 1
}
// Returns response’s status.
get status () {
webidl.brandCheck(this, Response)
// The status getter steps are to return this’s response’s status.
return this.#state.status
}
// Returns whether response’s status is an ok status.
get ok () {
webidl.brandCheck(this, Response)
// The ok getter steps are to return true if this’s response’s status is an
// ok status; otherwise false.
return this.#state.status >= 200 && this.#state.status <= 299
}
// Returns response’s status message.
get statusText () {
webidl.brandCheck(this, Response)
// The statusText getter steps are to return this’s response’s status
// message.
return this.#state.statusText
}
// Returns response’s headers as Headers.
get headers () {
webidl.brandCheck(this, Response)
// The headers getter steps are to return this’s headers.
return this.#headers
}
get body () {
webidl.brandCheck(this, Response)
return this.#state.body ? this.#state.body.stream : null
}
get bodyUsed () {
webidl.brandCheck(this, Response)
return !!this.#state.body && util.isDisturbed(this.#state.body.stream)
}
// Returns a clone of response.
clone () {
webidl.brandCheck(this, Response)
// 1. If this is unusable, then throw a TypeError.
if (bodyUnusable(this.#state)) {
throw webidl.errors.exception({
header: 'Response.clone',
message: 'Body has already been consumed.'
})
}
// 2. Let clonedResponse be the result of cloning this’s response.
const clonedResponse = cloneResponse(this.#state)
// Note: To re-register because of a new stream.
// Don't set finalizers other than for fetch responses.
if (this.#state.urlList.length !== 0 && this.#state.body?.stream) {
streamRegistry.register(this, new WeakRef(this.#state.body.stream))
}
// 3. Return the result of creating a Response object, given
// clonedResponse, this’s headers’s guard, and this’s relevant Realm.
return fromInnerResponse(clonedResponse, getHeadersGuard(this.#headers))
}
[nodeUtil.inspect.custom] (depth, options) {
if (options.depth === null) {
options.depth = 2
}
options.colors ??= true
const properties = {
status: this.status,
statusText: this.statusText,
headers: this.headers,
body: this.body,
bodyUsed: this.bodyUsed,
ok: this.ok,
redirected: this.redirected,
type: this.type,
url: this.url
}
return `Response ${nodeUtil.formatWithOptions(options, properties)}`
}
/**
* @param {Response} response
*/
static getResponseHeaders (response) {
return response.#headers
}
/**
* @param {Response} response
* @param {Headers} newHeaders
*/
static setResponseHeaders (response, newHeaders) {
response.#headers = newHeaders
}
/**
* @param {Response} response
*/
static getResponseState (response) {
return response.#state
}
/**
* @param {Response} response
* @param {any} newState
*/
static setResponseState (response, newState) {
response.#state = newState
}
}
const { getResponseHeaders, setResponseHeaders, getResponseState, setResponseState } = Response
Reflect.deleteProperty(Response, 'getResponseHeaders')
Reflect.deleteProperty(Response, 'setResponseHeaders')
Reflect.deleteProperty(Response, 'getResponseState')
Reflect.deleteProperty(Response, 'setResponseState')
mixinBody(Response, getResponseState)
Object.defineProperties(Response.prototype, {
type: kEnumerableProperty,
url: kEnumerableProperty,
status: kEnumerableProperty,
ok: kEnumerableProperty,
redirected: kEnumerableProperty,
statusText: kEnumerableProperty,
headers: kEnumerableProperty,
clone: kEnumerableProperty,
body: kEnumerableProperty,
bodyUsed: kEnumerableProperty,
[Symbol.toStringTag]: {
value: 'Response',
configurable: true
}
})
Object.defineProperties(Response, {
json: kEnumerableProperty,
redirect: kEnumerableProperty,
error: kEnumerableProperty
})
// https://fetch.spec.whatwg.org/#concept-response-clone
function cloneResponse (response) {
// To clone a response response, run these steps:
// 1. If response is a filtered response, then return a new identical
// filtered response whose internal response is a clone of response’s
// internal response.
if (response.internalResponse) {
return filterResponse(
cloneResponse(response.internalResponse),
response.type
)
}
// 2. Let newResponse be a copy of response, except for its body.
const newResponse = makeResponse({ ...response, body: null })
// 3. If response’s body is non-null, then set newResponse’s body to the
// result of cloning response’s body.
if (response.body != null) {
newResponse.body = cloneBody(response.body)
}
// 4. Return newResponse.
return newResponse
}
function makeResponse (init) {
return {
aborted: false,
rangeRequested: false,
timingAllowPassed: false,
requestIncludesCredentials: false,
type: 'default',
status: 200,
timingInfo: null,
cacheState: '',
statusText: '',
...init,
headersList: init?.headersList
? new HeadersList(init?.headersList)
: new HeadersList(),
urlList: init?.urlList ? [...init.urlList] : []
}
}
function makeNetworkError (reason) {
const isError = isErrorLike(reason)
return makeResponse({
type: 'error',
status: 0,
error: isError
? reason
: new Error(reason ? String(reason) : reason),
aborted: reason && reason.name === 'AbortError'
})
}
// @see https://fetch.spec.whatwg.org/#concept-network-error
function isNetworkError (response) {
return (
// A network error is a response whose type is "error",
response.type === 'error' &&
// status is 0
response.status === 0
)
}
function makeFilteredResponse (response, state) {
state = {
internalResponse: response,
...state
}
return new Proxy(response, {
get (target, p) {
return p in state ? state[p] : target[p]
},
set (target, p, value) {
assert(!(p in state))
target[p] = value
return true
}
})
}
// https://fetch.spec.whatwg.org/#concept-filtered-response
function filterResponse (response, type) {
// Set response to the following filtered response with response as its
// internal response, depending on request’s response tainting:
if (type === 'basic') {
// A basic filtered response is a filtered response whose type is "basic"
// and header list excludes any headers in internal response’s header list
// whose name is a forbidden response-header name.
// Note: undici does not implement forbidden response-header names
return makeFilteredResponse(response, {
type: 'basic',
headersList: response.headersList
})
} else if (type === 'cors') {
// A CORS filtered response is a filtered response whose type is "cors"
// and header list excludes any headers in internal response’s header
// list whose name is not a CORS-safelisted response-header name, given
// internal response’s CORS-exposed header-name list.
// Note: undici does not implement CORS-safelisted response-header names
return makeFilteredResponse(response, {
type: 'cors',
headersList: response.headersList
})
} else if (type === 'opaque') {
// An opaque filtered response is a filtered response whose type is
// "opaque", URL list is the empty list, status is 0, status message
// is the empty byte sequence, header list is empty, and body is null.
return makeFilteredResponse(response, {
type: 'opaque',
urlList: [],
status: 0,
statusText: '',
body: null
})
} else if (type === 'opaqueredirect') {
// An opaque-redirect filtered response is a filtered response whose type
// is "opaqueredirect", status is 0, status message is the empty byte
// sequence, header list is empty, and body is null.
return makeFilteredResponse(response, {
type: 'opaqueredirect',
status: 0,
statusText: '',
headersList: [],
body: null
})
} else {
assert(false)
}
}
// https://fetch.spec.whatwg.org/#appropriate-network-error
function makeAppropriateNetworkError (fetchParams, err = null) {
// 1. Assert: fetchParams is canceled.
assert(isCancelled(fetchParams))
// 2. Return an aborted network error if fetchParams is aborted;
// otherwise return a network error.
return isAborted(fetchParams)
? makeNetworkError(Object.assign(new DOMException('The operation was aborted.', 'AbortError'), { cause: err }))
: makeNetworkError(Object.assign(new DOMException('Request was cancelled.'), { cause: err }))
}
// https://whatpr.org/fetch/1392.html#initialize-a-response
function initializeResponse (response, init, body) {
// 1. If init["status"] is not in the range 200 to 599, inclusive, then
// throw a RangeError.
if (init.status !== null && (init.status < 200 || init.status > 599)) {
throw new RangeError('init["status"] must be in the range of 200 to 599, inclusive.')
}
// 2. If init["statusText"] does not match the reason-phrase token production,
// then throw a TypeError.
if ('statusText' in init && init.statusText != null) {
// See, https://datatracker.ietf.org/doc/html/rfc7230#section-3.1.2:
// reason-phrase = *( HTAB / SP / VCHAR / obs-text )
if (!isValidReasonPhrase(String(init.statusText))) {
throw new TypeError('Invalid statusText')
}
}
// 3. Set response’s response’s status to init["status"].
if ('status' in init && init.status != null) {
getResponseState(response).status = init.status
}
// 4. Set response’s response’s status message to init["statusText"].
if ('statusText' in init && init.statusText != null) {
getResponseState(response).statusText = init.statusText
}
// 5. If init["headers"] exists, then fill response’s headers with init["headers"].
if ('headers' in init && init.headers != null) {
fill(getResponseHeaders(response), init.headers)
}
// 6. If body was given, then:
if (body) {
// 1. If response's status is a null body status, then throw a TypeError.
if (nullBodyStatus.includes(response.status)) {
throw webidl.errors.exception({
header: 'Response constructor',
message: `Invalid response status code ${response.status}`
})
}
// 2. Set response's body to body's body.
getResponseState(response).body = body.body
// 3. If body's type is non-null and response's header list does not contain
// `Content-Type`, then append (`Content-Type`, body's type) to response's header list.
if (body.type != null && !getResponseState(response).headersList.contains('content-type', true)) {
getResponseState(response).headersList.append('content-type', body.type, true)
}
}
}
/**
* @see https://fetch.spec.whatwg.org/#response-create
* @param {any} innerResponse
* @param {'request' | 'immutable' | 'request-no-cors' | 'response' | 'none'} guard
* @returns {Response}
*/
function fromInnerResponse (innerResponse, guard) {
const response = new Response(kConstruct)
setResponseState(response, innerResponse)
const headers = new Headers(kConstruct)
setResponseHeaders(response, headers)
setHeadersList(headers, innerResponse.headersList)
setHeadersGuard(headers, guard)
// Note: If innerResponse's urlList contains a URL, it is a fetch response.
if (innerResponse.urlList.length !== 0 && innerResponse.body?.stream) {
// If the target (response) is reclaimed, the cleanup callback may be called at some point with
// the held value provided for it (innerResponse.body.stream). The held value can be any value:
// a primitive or an object, even undefined. If the held value is an object, the registry keeps
// a strong reference to it (so it can pass it to the cleanup callback later). Reworded from
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/FinalizationRegistry
streamRegistry.register(response, new WeakRef(innerResponse.body.stream))
}
return response
}
// https://fetch.spec.whatwg.org/#typedefdef-xmlhttprequestbodyinit
webidl.converters.XMLHttpRequestBodyInit = function (V, prefix, name) {
if (typeof V === 'string') {
return webidl.converters.USVString(V, prefix, name)
}
if (webidl.is.Blob(V)) {
return V
}
if (webidl.is.BufferSource(V)) {
return V
}
if (webidl.is.FormData(V)) {
return V
}
if (webidl.is.URLSearchParams(V)) {
return V
}
return webidl.converters.DOMString(V, prefix, name)
}
// https://fetch.spec.whatwg.org/#bodyinit
webidl.converters.BodyInit = function (V, prefix, argument) {
if (webidl.is.ReadableStream(V)) {
return V
}
// Note: the spec doesn't include async iterables,
// this is an undici extension.
if (V?.[Symbol.asyncIterator]) {
return V
}
return webidl.converters.XMLHttpRequestBodyInit(V, prefix, argument)
}
webidl.converters.ResponseInit = webidl.dictionaryConverter([
{
key: 'status',
converter: webidl.converters['unsigned short'],
defaultValue: () => 200
},
{
key: 'statusText',
converter: webidl.converters.ByteString,
defaultValue: () => ''
},
{
key: 'headers',
converter: webidl.converters.HeadersInit
}
])
webidl.is.Response = webidl.util.MakeTypeAssertion(Response)
module.exports = {
isNetworkError,
makeNetworkError,
makeResponse,
makeAppropriateNetworkError,
filterResponse,
Response,
cloneResponse,
fromInnerResponse,
getResponseState
}
================================================
FILE: lib/web/fetch/util.js
================================================
'use strict'
const { Transform } = require('node:stream')
const zlib = require('node:zlib')
const { redirectStatusSet, referrerPolicyTokens, badPortsSet } = require('./constants')
const { getGlobalOrigin } = require('./global')
const { collectAnHTTPQuotedString, parseMIMEType } = require('./data-url')
const { performance } = require('node:perf_hooks')
const { ReadableStreamFrom, isValidHTTPToken, normalizedMethodRecordsBase } = require('../../core/util')
const assert = require('node:assert')
const { isUint8Array } = require('node:util/types')
const { webidl } = require('../webidl')
const { isomorphicEncode, collectASequenceOfCodePoints, removeChars } = require('../infra')
function responseURL (response) {
// https://fetch.spec.whatwg.org/#responses
// A response has an associated URL. It is a pointer to the last URL
// in response’s URL list and null if response’s URL list is empty.
const urlList = response.urlList
const length = urlList.length
return length === 0 ? null : urlList[length - 1].toString()
}
// https://fetch.spec.whatwg.org/#concept-response-location-url
function responseLocationURL (response, requestFragment) {
// 1. If response’s status is not a redirect status, then return null.
if (!redirectStatusSet.has(response.status)) {
return null
}
// 2. Let location be the result of extracting header list values given
// `Location` and response’s header list.
let location = response.headersList.get('location', true)
// 3. If location is a header value, then set location to the result of
// parsing location with response’s URL.
if (location !== null && isValidHeaderValue(location)) {
if (!isValidEncodedURL(location)) {
// Some websites respond location header in UTF-8 form without encoding them as ASCII
// and major browsers redirect them to correctly UTF-8 encoded addresses.
// Here, we handle that behavior in the same way.
location = normalizeBinaryStringToUtf8(location)
}
location = new URL(location, responseURL(response))
}
// 4. If location is a URL whose fragment is null, then set location’s
// fragment to requestFragment.
if (location && !location.hash) {
location.hash = requestFragment
}
// 5. Return location.
return location
}
/**
* @see https://www.rfc-editor.org/rfc/rfc1738#section-2.2
* @param {string} url
* @returns {boolean}
*/
function isValidEncodedURL (url) {
for (let i = 0; i < url.length; ++i) {
const code = url.charCodeAt(i)
if (
code > 0x7E || // Non-US-ASCII + DEL
code < 0x20 // Control characters NUL - US
) {
return false
}
}
return true
}
/**
* If string contains non-ASCII characters, assumes it's UTF-8 encoded and decodes it.
* Since UTF-8 is a superset of ASCII, this will work for ASCII strings as well.
* @param {string} value
* @returns {string}
*/
function normalizeBinaryStringToUtf8 (value) {
return Buffer.from(value, 'binary').toString('utf8')
}
/** @returns {URL} */
function requestCurrentURL (request) {
return request.urlList[request.urlList.length - 1]
}
function requestBadPort (request) {
// 1. Let url be request’s current URL.
const url = requestCurrentURL(request)
// 2. If url’s scheme is an HTTP(S) scheme and url’s port is a bad port,
// then return blocked.
if (urlIsHttpHttpsScheme(url) && badPortsSet.has(url.port)) {
return 'blocked'
}
// 3. Return allowed.
return 'allowed'
}
function isErrorLike (object) {
return object instanceof Error || (
object?.constructor?.name === 'Error' ||
object?.constructor?.name === 'DOMException'
)
}
// Check whether |statusText| is a ByteString and
// matches the Reason-Phrase token production.
// RFC 2616: https://tools.ietf.org/html/rfc2616
// RFC 7230: https://tools.ietf.org/html/rfc7230
// "reason-phrase = *( HTAB / SP / VCHAR / obs-text )"
// https://github.com/chromium/chromium/blob/94.0.4604.1/third_party/blink/renderer/core/fetch/response.cc#L116
function isValidReasonPhrase (statusText) {
for (let i = 0; i < statusText.length; ++i) {
const c = statusText.charCodeAt(i)
if (
!(
(
c === 0x09 || // HTAB
(c >= 0x20 && c <= 0x7e) || // SP / VCHAR
(c >= 0x80 && c <= 0xff)
) // obs-text
)
) {
return false
}
}
return true
}
/**
* @see https://fetch.spec.whatwg.org/#header-name
* @param {string} potentialValue
*/
const isValidHeaderName = isValidHTTPToken
/**
* @see https://fetch.spec.whatwg.org/#header-value
* @param {string} potentialValue
*/
function isValidHeaderValue (potentialValue) {
// - Has no leading or trailing HTTP tab or space bytes.
// - Contains no 0x00 (NUL) or HTTP newline bytes.
return (
potentialValue[0] === '\t' ||
potentialValue[0] === ' ' ||
potentialValue[potentialValue.length - 1] === '\t' ||
potentialValue[potentialValue.length - 1] === ' ' ||
potentialValue.includes('\n') ||
potentialValue.includes('\r') ||
potentialValue.includes('\0')
) === false
}
/**
* Parse a referrer policy from a Referrer-Policy header
* @see https://w3c.github.io/webappsec-referrer-policy/#parse-referrer-policy-from-header
*/
function parseReferrerPolicy (actualResponse) {
// 1. Let policy-tokens be the result of extracting header list values given `Referrer-Policy` and response’s header list.
const policyHeader = (actualResponse.headersList.get('referrer-policy', true) ?? '').split(',')
// 2. Let policy be the empty string.
let policy = ''
// 3. For each token in policy-tokens, if token is a referrer policy and token is not the empty string, then set policy to token.
// Note: As the referrer-policy can contain multiple policies
// separated by comma, we need to loop through all of them
// and pick the first valid one.
// Ref: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referrer-Policy#specify_a_fallback_policy
if (policyHeader.length) {
// The right-most policy takes precedence.
// The left-most policy is the fallback.
for (let i = policyHeader.length; i !== 0; i--) {
const token = policyHeader[i - 1].trim()
if (referrerPolicyTokens.has(token)) {
policy = token
break
}
}
}
// 4. Return policy.
return policy
}
/**
* Given a request request and a response actualResponse, this algorithm
* updates request’s referrer policy according to the Referrer-Policy
* header (if any) in actualResponse.
* @see https://w3c.github.io/webappsec-referrer-policy/#set-requests-referrer-policy-on-redirect
* @param {import('./request').Request} request
* @param {import('./response').Response} actualResponse
*/
function setRequestReferrerPolicyOnRedirect (request, actualResponse) {
// 1. Let policy be the result of executing § 8.1 Parse a referrer policy
// from a Referrer-Policy header on actualResponse.
const policy = parseReferrerPolicy(actualResponse)
// 2. If policy is not the empty string, then set request’s referrer policy to policy.
if (policy !== '') {
request.referrerPolicy = policy
}
}
// https://fetch.spec.whatwg.org/#cross-origin-resource-policy-check
function crossOriginResourcePolicyCheck () {
// TODO
return 'allowed'
}
// https://fetch.spec.whatwg.org/#concept-cors-check
function corsCheck () {
// TODO
return 'success'
}
// https://fetch.spec.whatwg.org/#concept-tao-check
function TAOCheck () {
// TODO
return 'success'
}
function appendFetchMetadata (httpRequest) {
// https://w3c.github.io/webappsec-fetch-metadata/#sec-fetch-dest-header
// TODO
// https://w3c.github.io/webappsec-fetch-metadata/#sec-fetch-mode-header
// 1. Assert: r’s url is a potentially trustworthy URL.
// TODO
// 2. Let header be a Structured Header whose value is a token.
let header = null
// 3. Set header’s value to r’s mode.
header = httpRequest.mode
// 4. Set a structured field value `Sec-Fetch-Mode`/header in r’s header list.
httpRequest.headersList.set('sec-fetch-mode', header, true)
// https://w3c.github.io/webappsec-fetch-metadata/#sec-fetch-site-header
// TODO
// https://w3c.github.io/webappsec-fetch-metadata/#sec-fetch-user-header
// TODO
}
// https://fetch.spec.whatwg.org/#append-a-request-origin-header
function appendRequestOriginHeader (request) {
// 1. Let serializedOrigin be the result of byte-serializing a request origin
// with request.
// TODO: implement "byte-serializing a request origin"
let serializedOrigin = request.origin
// - "'client' is changed to an origin during fetching."
// This doesn't happen in undici (in most cases) because undici, by default,
// has no concept of origin.
// - request.origin can also be set to request.client.origin (client being
// an environment settings object), which is undefined without using
// setGlobalOrigin.
if (serializedOrigin === 'client' || serializedOrigin === undefined) {
return
}
// 2. If request’s response tainting is "cors" or request’s mode is "websocket",
// then append (`Origin`, serializedOrigin) to request’s header list.
// 3. Otherwise, if request’s method is neither `GET` nor `HEAD`, then:
if (request.responseTainting === 'cors' || request.mode === 'websocket') {
request.headersList.append('origin', serializedOrigin, true)
} else if (request.method !== 'GET' && request.method !== 'HEAD') {
// 1. Switch on request’s referrer policy:
switch (request.referrerPolicy) {
case 'no-referrer':
// Set serializedOrigin to `null`.
serializedOrigin = null
break
case 'no-referrer-when-downgrade':
case 'strict-origin':
case 'strict-origin-when-cross-origin':
// If request’s origin is a tuple origin, its scheme is "https", and
// request’s current URL’s scheme is not "https", then set
// serializedOrigin to `null`.
if (request.origin && urlHasHttpsScheme(request.origin) && !urlHasHttpsScheme(requestCurrentURL(request))) {
serializedOrigin = null
}
break
case 'same-origin':
// If request’s origin is not same origin with request’s current URL’s
// origin, then set serializedOrigin to `null`.
if (!sameOrigin(request, requestCurrentURL(request))) {
serializedOrigin = null
}
break
default:
// Do nothing.
}
// 2. Append (`Origin`, serializedOrigin) to request’s header list.
request.headersList.append('origin', serializedOrigin, true)
}
}
// https://w3c.github.io/hr-time/#dfn-coarsen-time
function coarsenTime (timestamp, crossOriginIsolatedCapability) {
// TODO
return timestamp
}
// https://fetch.spec.whatwg.org/#clamp-and-coarsen-connection-timing-info
function clampAndCoarsenConnectionTimingInfo (connectionTimingInfo, defaultStartTime, crossOriginIsolatedCapability) {
if (!connectionTimingInfo?.startTime || connectionTimingInfo.startTime < defaultStartTime) {
return {
domainLookupStartTime: defaultStartTime,
domainLookupEndTime: defaultStartTime,
connectionStartTime: defaultStartTime,
connectionEndTime: defaultStartTime,
secureConnectionStartTime: defaultStartTime,
ALPNNegotiatedProtocol: connectionTimingInfo?.ALPNNegotiatedProtocol
}
}
return {
domainLookupStartTime: coarsenTime(connectionTimingInfo.domainLookupStartTime, crossOriginIsolatedCapability),
domainLookupEndTime: coarsenTime(connectionTimingInfo.domainLookupEndTime, crossOriginIsolatedCapability),
connectionStartTime: coarsenTime(connectionTimingInfo.connectionStartTime, crossOriginIsolatedCapability),
connectionEndTime: coarsenTime(connectionTimingInfo.connectionEndTime, crossOriginIsolatedCapability),
secureConnectionStartTime: coarsenTime(connectionTimingInfo.secureConnectionStartTime, crossOriginIsolatedCapability),
ALPNNegotiatedProtocol: connectionTimingInfo.ALPNNegotiatedProtocol
}
}
// https://w3c.github.io/hr-time/#dfn-coarsened-shared-current-time
function coarsenedSharedCurrentTime (crossOriginIsolatedCapability) {
return coarsenTime(performance.now(), crossOriginIsolatedCapability)
}
// https://fetch.spec.whatwg.org/#create-an-opaque-timing-info
function createOpaqueTimingInfo (timingInfo) {
return {
startTime: timingInfo.startTime ?? 0,
redirectStartTime: 0,
redirectEndTime: 0,
postRedirectStartTime: timingInfo.startTime ?? 0,
finalServiceWorkerStartTime: 0,
finalNetworkResponseStartTime: 0,
finalNetworkRequestStartTime: 0,
endTime: 0,
encodedBodySize: 0,
decodedBodySize: 0,
finalConnectionTimingInfo: null
}
}
// https://html.spec.whatwg.org/multipage/origin.html#policy-container
function makePolicyContainer () {
// Note: the fetch spec doesn't make use of embedder policy or CSP list
return {
referrerPolicy: 'strict-origin-when-cross-origin'
}
}
// https://html.spec.whatwg.org/multipage/origin.html#clone-a-policy-container
function clonePolicyContainer (policyContainer) {
return {
referrerPolicy: policyContainer.referrerPolicy
}
}
/**
* Determine request’s Referrer
*
* @see https://w3c.github.io/webappsec-referrer-policy/#determine-requests-referrer
*/
function determineRequestsReferrer (request) {
// Given a request request, we can determine the correct referrer information
// to send by examining its referrer policy as detailed in the following
// steps, which return either no referrer or a URL:
// 1. Let policy be request's referrer policy.
const policy = request.referrerPolicy
// Note: policy cannot (shouldn't) be null or an empty string.
assert(policy)
// 2. Let environment be request’s client.
let referrerSource = null
// 3. Switch on request’s referrer:
// "client"
if (request.referrer === 'client') {
// Note: node isn't a browser and doesn't implement document/iframes,
// so we bypass this step and replace it with our own.
const globalOrigin = getGlobalOrigin()
if (!globalOrigin || globalOrigin.origin === 'null') {
return 'no-referrer'
}
// Note: we need to clone it as it's mutated
referrerSource = new URL(globalOrigin)
// a URL
} else if (webidl.is.URL(request.referrer)) {
// Let referrerSource be request’s referrer.
referrerSource = request.referrer
}
// 4. Let request’s referrerURL be the result of stripping referrerSource for
// use as a referrer.
let referrerURL = stripURLForReferrer(referrerSource)
// 5. Let referrerOrigin be the result of stripping referrerSource for use as
// a referrer, with the origin-only flag set to true.
const referrerOrigin = stripURLForReferrer(referrerSource, true)
// 6. If the result of serializing referrerURL is a string whose length is
// greater than 4096, set referrerURL to referrerOrigin.
if (referrerURL.toString().length > 4096) {
referrerURL = referrerOrigin
}
// 7. The user agent MAY alter referrerURL or referrerOrigin at this point
// to enforce arbitrary policy considerations in the interests of minimizing
// data leakage. For example, the user agent could strip the URL down to an
// origin, modify its host, replace it with an empty string, etc.
// 8. Execute the switch statements corresponding to the value of policy:
switch (policy) {
case 'no-referrer':
// Return no referrer
return 'no-referrer'
case 'origin':
// Return referrerOrigin
if (referrerOrigin != null) {
return referrerOrigin
}
return stripURLForReferrer(referrerSource, true)
case 'unsafe-url':
// Return referrerURL.
return referrerURL
case 'strict-origin': {
const currentURL = requestCurrentURL(request)
// 1. If referrerURL is a potentially trustworthy URL and request’s
// current URL is not a potentially trustworthy URL, then return no
// referrer.
if (isURLPotentiallyTrustworthy(referrerURL) && !isURLPotentiallyTrustworthy(currentURL)) {
return 'no-referrer'
}
// 2. Return referrerOrigin
return referrerOrigin
}
case 'strict-origin-when-cross-origin': {
const currentURL = requestCurrentURL(request)
// 1. If the origin of referrerURL and the origin of request’s current
// URL are the same, then return referrerURL.
if (sameOrigin(referrerURL, currentURL)) {
return referrerURL
}
// 2. If referrerURL is a potentially trustworthy URL and request’s
// current URL is not a potentially trustworthy URL, then return no
// referrer.
if (isURLPotentiallyTrustworthy(referrerURL) && !isURLPotentiallyTrustworthy(currentURL)) {
return 'no-referrer'
}
// 3. Return referrerOrigin.
return referrerOrigin
}
case 'same-origin':
// 1. If the origin of referrerURL and the origin of request’s current
// URL are the same, then return referrerURL.
if (sameOrigin(request, referrerURL)) {
return referrerURL
}
// 2. Return no referrer.
return 'no-referrer'
case 'origin-when-cross-origin':
// 1. If the origin of referrerURL and the origin of request’s current
// URL are the same, then return referrerURL.
if (sameOrigin(request, referrerURL)) {
return referrerURL
}
// 2. Return referrerOrigin.
return referrerOrigin
case 'no-referrer-when-downgrade': {
const currentURL = requestCurrentURL(request)
// 1. If referrerURL is a potentially trustworthy URL and request’s
// current URL is not a potentially trustworthy URL, then return no
// referrer.
if (isURLPotentiallyTrustworthy(referrerURL) && !isURLPotentiallyTrustworthy(currentURL)) {
return 'no-referrer'
}
// 2. Return referrerURL.
return referrerURL
}
}
}
/**
* Certain portions of URLs must not be included when sending a URL as the
* value of a `Referer` header: a URLs fragment, username, and password
* components must be stripped from the URL before it’s sent out. This
* algorithm accepts a origin-only flag, which defaults to false. If set to
* true, the algorithm will additionally remove the URL’s path and query
* components, leaving only the scheme, host, and port.
*
* @see https://w3c.github.io/webappsec-referrer-policy/#strip-url
* @param {URL} url
* @param {boolean} [originOnly=false]
*/
function stripURLForReferrer (url, originOnly = false) {
// 1. Assert: url is a URL.
assert(webidl.is.URL(url))
// Note: Create a new URL instance to avoid mutating the original URL.
url = new URL(url)
// 2. If url’s scheme is a local scheme, then return no referrer.
if (urlIsLocal(url)) {
return 'no-referrer'
}
// 3. Set url’s username to the empty string.
url.username = ''
// 4. Set url’s password to the empty string.
url.password = ''
// 5. Set url’s fragment to null.
url.hash = ''
// 6. If the origin-only flag is true, then:
if (originOnly === true) {
// 1. Set url’s path to « the empty string ».
url.pathname = ''
// 2. Set url’s query to null.
url.search = ''
}
// 7. Return url.
return url
}
const isPotentialleTrustworthyIPv4 = RegExp.prototype.test
.bind(/^127\.(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)\.){2}(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)$/)
const isPotentiallyTrustworthyIPv6 = RegExp.prototype.test
.bind(/^(?:(?:0{1,4}:){7}|(?:0{1,4}:){1,6}:|::)0{0,3}1$/)
/**
* Check if host matches one of the CIDR notations 127.0.0.0/8 or ::1/128.
*
* @param {string} origin
* @returns {boolean}
*/
function isOriginIPPotentiallyTrustworthy (origin) {
// IPv6
if (origin.includes(':')) {
// Remove brackets from IPv6 addresses
if (origin[0] === '[' && origin[origin.length - 1] === ']') {
origin = origin.slice(1, -1)
}
return isPotentiallyTrustworthyIPv6(origin)
}
// IPv4
return isPotentialleTrustworthyIPv4(origin)
}
/**
* A potentially trustworthy origin is one which a user agent can generally
* trust as delivering data securely.
*
* Return value `true` means `Potentially Trustworthy`.
* Return value `false` means `Not Trustworthy`.
*
* @see https://w3c.github.io/webappsec-secure-contexts/#is-origin-trustworthy
* @param {string} origin
* @returns {boolean}
*/
function isOriginPotentiallyTrustworthy (origin) {
// 1. If origin is an opaque origin, return "Not Trustworthy".
if (origin == null || origin === 'null') {
return false
}
// 2. Assert: origin is a tuple origin.
origin = new URL(origin)
// 3. If origin’s scheme is either "https" or "wss",
// return "Potentially Trustworthy".
if (origin.protocol === 'https:' || origin.protocol === 'wss:') {
return true
}
// 4. If origin’s host matches one of the CIDR notations 127.0.0.0/8 or
// ::1/128 [RFC4632], return "Potentially Trustworthy".
if (isOriginIPPotentiallyTrustworthy(origin.hostname)) {
return true
}
// 5. If the user agent conforms to the name resolution rules in
// [let-localhost-be-localhost] and one of the following is true:
// origin’s host is "localhost" or "localhost."
if (origin.hostname === 'localhost' || origin.hostname === 'localhost.') {
return true
}
// origin’s host ends with ".localhost" or ".localhost."
if (origin.hostname.endsWith('.localhost') || origin.hostname.endsWith('.localhost.')) {
return true
}
// 6. If origin’s scheme is "file", return "Potentially Trustworthy".
if (origin.protocol === 'file:') {
return true
}
// 7. If origin’s scheme component is one which the user agent considers to
// be authenticated, return "Potentially Trustworthy".
// 8. If origin has been configured as a trustworthy origin, return
// "Potentially Trustworthy".
// 9. Return "Not Trustworthy".
return false
}
/**
* A potentially trustworthy URL is one which either inherits context from its
* creator (about:blank, about:srcdoc, data) or one whose origin is a
* potentially trustworthy origin.
*
* Return value `true` means `Potentially Trustworthy`.
* Return value `false` means `Not Trustworthy`.
*
* @see https://www.w3.org/TR/secure-contexts/#is-url-trustworthy
* @param {URL} url
* @returns {boolean}
*/
function isURLPotentiallyTrustworthy (url) {
// Given a URL record (url), the following algorithm returns "Potentially
// Trustworthy" or "Not Trustworthy" as appropriate:
if (!webidl.is.URL(url)) {
return false
}
// 1. If url is "about:blank" or "about:srcdoc",
// return "Potentially Trustworthy".
if (url.href === 'about:blank' || url.href === 'about:srcdoc') {
return true
}
// 2. If url’s scheme is "data", return "Potentially Trustworthy".
if (url.protocol === 'data:') return true
// Note: The origin of blob: URLs is the origin of the context in which they
// were created. Therefore, blobs created in a trustworthy origin will
// themselves be potentially trustworthy.
if (url.protocol === 'blob:') return true
// 3. Return the result of executing § 3.1 Is origin potentially trustworthy?
// on url’s origin.
return isOriginPotentiallyTrustworthy(url.origin)
}
// https://w3c.github.io/webappsec-upgrade-insecure-requests/#upgrade-request
function tryUpgradeRequestToAPotentiallyTrustworthyURL (request) {
// TODO
}
/**
* @link {https://html.spec.whatwg.org/multipage/origin.html#same-origin}
* @param {URL} A
* @param {URL} B
*/
function sameOrigin (A, B) {
// 1. If A and B are the same opaque origin, then return true.
if (A.origin === B.origin && A.origin === 'null') {
return true
}
// 2. If A and B are both tuple origins and their schemes,
// hosts, and port are identical, then return true.
if (A.protocol === B.protocol && A.hostname === B.hostname && A.port === B.port) {
return true
}
// 3. Return false.
return false
}
function isAborted (fetchParams) {
return fetchParams.controller.state === 'aborted'
}
function isCancelled (fetchParams) {
return fetchParams.controller.state === 'aborted' ||
fetchParams.controller.state === 'terminated'
}
/**
* @see https://fetch.spec.whatwg.org/#concept-method-normalize
* @param {string} method
*/
function normalizeMethod (method) {
return normalizedMethodRecordsBase[method.toLowerCase()] ?? method
}
// https://tc39.es/ecma262/#sec-%25iteratorprototype%25-object
const esIteratorPrototype = Object.getPrototypeOf(Object.getPrototypeOf([][Symbol.iterator]()))
/**
* @see https://webidl.spec.whatwg.org/#dfn-iterator-prototype-object
* @param {string} name name of the instance
* @param {((target: any) => any)} kInternalIterator
* @param {string | number} [keyIndex]
* @param {string | number} [valueIndex]
*/
function createIterator (name, kInternalIterator, keyIndex = 0, valueIndex = 1) {
class FastIterableIterator {
/** @type {any} */
#target
/** @type {'key' | 'value' | 'key+value'} */
#kind
/** @type {number} */
#index
/**
* @see https://webidl.spec.whatwg.org/#dfn-default-iterator-object
* @param {unknown} target
* @param {'key' | 'value' | 'key+value'} kind
*/
constructor (target, kind) {
this.#target = target
this.#kind = kind
this.#index = 0
}
next () {
// 1. Let interface be the interface for which the iterator prototype object exists.
// 2. Let thisValue be the this value.
// 3. Let object be ? ToObject(thisValue).
// 4. If object is a platform object, then perform a security
// check, passing:
// 5. If object is not a default iterator object for interface,
// then throw a TypeError.
if (typeof this !== 'object' || this === null || !(#target in this)) {
throw new TypeError(
`'next' called on an object that does not implement interface ${name} Iterator.`
)
}
// 6. Let index be object’s index.
// 7. Let kind be object’s kind.
// 8. Let values be object’s target's value pairs to iterate over.
const index = this.#index
const values = kInternalIterator(this.#target)
// 9. Let len be the length of values.
const len = values.length
// 10. If index is greater than or equal to len, then return
// CreateIterResultObject(undefined, true).
if (index >= len) {
return {
value: undefined,
done: true
}
}
// 11. Let pair be the entry in values at index index.
const { [keyIndex]: key, [valueIndex]: value } = values[index]
// 12. Set object’s index to index + 1.
this.#index = index + 1
// 13. Return the iterator result for pair and kind.
// https://webidl.spec.whatwg.org/#iterator-result
// 1. Let result be a value determined by the value of kind:
let result
switch (this.#kind) {
case 'key':
// 1. Let idlKey be pair’s key.
// 2. Let key be the result of converting idlKey to an
// ECMAScript value.
// 3. result is key.
result = key
break
case 'value':
// 1. Let idlValue be pair’s value.
// 2. Let value be the result of converting idlValue to
// an ECMAScript value.
// 3. result is value.
result = value
break
case 'key+value':
// 1. Let idlKey be pair’s key.
// 2. Let idlValue be pair’s value.
// 3. Let key be the result of converting idlKey to an
// ECMAScript value.
// 4. Let value be the result of converting idlValue to
// an ECMAScript value.
// 5. Let array be ! ArrayCreate(2).
// 6. Call ! CreateDataProperty(array, "0", key).
// 7. Call ! CreateDataProperty(array, "1", value).
// 8. result is array.
result = [key, value]
break
}
// 2. Return CreateIterResultObject(result, false).
return {
value: result,
done: false
}
}
}
// https://webidl.spec.whatwg.org/#dfn-iterator-prototype-object
// @ts-ignore
delete FastIterableIterator.prototype.constructor
Object.setPrototypeOf(FastIterableIterator.prototype, esIteratorPrototype)
Object.defineProperties(FastIterableIterator.prototype, {
[Symbol.toStringTag]: {
writable: false,
enumerable: false,
configurable: true,
value: `${name} Iterator`
},
next: { writable: true, enumerable: true, configurable: true }
})
/**
* @param {unknown} target
* @param {'key' | 'value' | 'key+value'} kind
* @returns {IterableIterator}
*/
return function (target, kind) {
return new FastIterableIterator(target, kind)
}
}
/**
* @see https://webidl.spec.whatwg.org/#dfn-iterator-prototype-object
* @param {string} name name of the instance
* @param {any} object class
* @param {(target: any) => any} kInternalIterator
* @param {string | number} [keyIndex]
* @param {string | number} [valueIndex]
*/
function iteratorMixin (name, object, kInternalIterator, keyIndex = 0, valueIndex = 1) {
const makeIterator = createIterator(name, kInternalIterator, keyIndex, valueIndex)
const properties = {
keys: {
writable: true,
enumerable: true,
configurable: true,
value: function keys () {
webidl.brandCheck(this, object)
return makeIterator(this, 'key')
}
},
values: {
writable: true,
enumerable: true,
configurable: true,
value: function values () {
webidl.brandCheck(this, object)
return makeIterator(this, 'value')
}
},
entries: {
writable: true,
enumerable: true,
configurable: true,
value: function entries () {
webidl.brandCheck(this, object)
return makeIterator(this, 'key+value')
}
},
forEach: {
writable: true,
enumerable: true,
configurable: true,
value: function forEach (callbackfn, thisArg = globalThis) {
webidl.brandCheck(this, object)
webidl.argumentLengthCheck(arguments, 1, `${name}.forEach`)
if (typeof callbackfn !== 'function') {
throw new TypeError(
`Failed to execute 'forEach' on '${name}': parameter 1 is not of type 'Function'.`
)
}
for (const { 0: key, 1: value } of makeIterator(this, 'key+value')) {
callbackfn.call(thisArg, value, key, this)
}
}
}
}
return Object.defineProperties(object.prototype, {
...properties,
[Symbol.iterator]: {
writable: true,
enumerable: false,
configurable: true,
value: properties.entries.value
}
})
}
/**
* @param {import('./body').ExtractBodyResult} body
* @param {(bytes: Uint8Array) => void} processBody
* @param {(error: Error) => void} processBodyError
* @returns {void}
*
* @see https://fetch.spec.whatwg.org/#body-fully-read
*/
function fullyReadBody (body, processBody, processBodyError) {
// 1. If taskDestination is null, then set taskDestination to
// the result of starting a new parallel queue.
// 2. Let successSteps given a byte sequence bytes be to queue a
// fetch task to run processBody given bytes, with taskDestination.
const successSteps = processBody
// 3. Let errorSteps be to queue a fetch task to run processBodyError,
// with taskDestination.
const errorSteps = processBodyError
try {
// 4. Let reader be the result of getting a reader for body’s stream.
// If that threw an exception, then run errorSteps with that
// exception and return.
const reader = body.stream.getReader()
// 5. Read all bytes from reader, given successSteps and errorSteps.
readAllBytes(reader, successSteps, errorSteps)
} catch (e) {
errorSteps(e)
}
}
/**
* @param {ReadableStreamController} controller
*/
function readableStreamClose (controller) {
try {
controller.close()
controller.byobRequest?.respond(0)
} catch (err) {
// TODO: add comment explaining why this error occurs.
if (!err.message.includes('Controller is already closed') && !err.message.includes('ReadableStream is already closed')) {
throw err
}
}
}
/**
* @see https://streams.spec.whatwg.org/#readablestreamdefaultreader-read-all-bytes
* @see https://streams.spec.whatwg.org/#read-loop
* @param {ReadableStream>} reader
* @param {(bytes: Uint8Array) => void} successSteps
* @param {(error: Error) => void} failureSteps
* @returns {Promise}
*/
async function readAllBytes (reader, successSteps, failureSteps) {
try {
const bytes = []
let byteLength = 0
do {
const { done, value: chunk } = await reader.read()
if (done) {
// 1. Call successSteps with bytes.
successSteps(Buffer.concat(bytes, byteLength))
return
}
// 1. If chunk is not a Uint8Array object, call failureSteps
// with a TypeError and abort these steps.
if (!isUint8Array(chunk)) {
failureSteps(new TypeError('Received non-Uint8Array chunk'))
return
}
// 2. Append the bytes represented by chunk to bytes.
bytes.push(chunk)
byteLength += chunk.length
// 3. Read-loop given reader, bytes, successSteps, and failureSteps.
} while (true)
} catch (e) {
// 1. Call failureSteps with e.
failureSteps(e)
}
}
/**
* @see https://fetch.spec.whatwg.org/#is-local
* @param {URL} url
* @returns {boolean}
*/
function urlIsLocal (url) {
assert('protocol' in url) // ensure it's a url object
const protocol = url.protocol
// A URL is local if its scheme is a local scheme.
// A local scheme is "about", "blob", or "data".
return protocol === 'about:' || protocol === 'blob:' || protocol === 'data:'
}
/**
* @param {string|URL} url
* @returns {boolean}
*/
function urlHasHttpsScheme (url) {
return (
(
typeof url === 'string' &&
url[5] === ':' &&
url[0] === 'h' &&
url[1] === 't' &&
url[2] === 't' &&
url[3] === 'p' &&
url[4] === 's'
) ||
url.protocol === 'https:'
)
}
/**
* @see https://fetch.spec.whatwg.org/#http-scheme
* @param {URL} url
*/
function urlIsHttpHttpsScheme (url) {
assert('protocol' in url) // ensure it's a url object
const protocol = url.protocol
return protocol === 'http:' || protocol === 'https:'
}
/**
* @typedef {Object} RangeHeaderValue
* @property {number|null} rangeStartValue
* @property {number|null} rangeEndValue
*/
/**
* @see https://fetch.spec.whatwg.org/#simple-range-header-value
* @param {string} value
* @param {boolean} allowWhitespace
* @return {RangeHeaderValue|'failure'}
*/
function simpleRangeHeaderValue (value, allowWhitespace) {
// 1. Let data be the isomorphic decoding of value.
// Note: isomorphic decoding takes a sequence of bytes (ie. a Uint8Array) and turns it into a string,
// nothing more. We obviously don't need to do that if value is a string already.
const data = value
// 2. If data does not start with "bytes", then return failure.
if (!data.startsWith('bytes')) {
return 'failure'
}
// 3. Let position be a position variable for data, initially pointing at the 5th code point of data.
const position = { position: 5 }
// 4. If allowWhitespace is true, collect a sequence of code points that are HTTP tab or space,
// from data given position.
if (allowWhitespace) {
collectASequenceOfCodePoints(
(char) => char === '\t' || char === ' ',
data,
position
)
}
// 5. If the code point at position within data is not U+003D (=), then return failure.
if (data.charCodeAt(position.position) !== 0x3D) {
return 'failure'
}
// 6. Advance position by 1.
position.position++
// 7. If allowWhitespace is true, collect a sequence of code points that are HTTP tab or space, from
// data given position.
if (allowWhitespace) {
collectASequenceOfCodePoints(
(char) => char === '\t' || char === ' ',
data,
position
)
}
// 8. Let rangeStart be the result of collecting a sequence of code points that are ASCII digits,
// from data given position.
const rangeStart = collectASequenceOfCodePoints(
(char) => {
const code = char.charCodeAt(0)
return code >= 0x30 && code <= 0x39
},
data,
position
)
// 9. Let rangeStartValue be rangeStart, interpreted as decimal number, if rangeStart is not the
// empty string; otherwise null.
const rangeStartValue = rangeStart.length ? Number(rangeStart) : null
// 10. If allowWhitespace is true, collect a sequence of code points that are HTTP tab or space,
// from data given position.
if (allowWhitespace) {
collectASequenceOfCodePoints(
(char) => char === '\t' || char === ' ',
data,
position
)
}
// 11. If the code point at position within data is not U+002D (-), then return failure.
if (data.charCodeAt(position.position) !== 0x2D) {
return 'failure'
}
// 12. Advance position by 1.
position.position++
// 13. If allowWhitespace is true, collect a sequence of code points that are HTTP tab
// or space, from data given position.
// Note from Khafra: its the same step as in #8 again lol
if (allowWhitespace) {
collectASequenceOfCodePoints(
(char) => char === '\t' || char === ' ',
data,
position
)
}
// 14. Let rangeEnd be the result of collecting a sequence of code points that are
// ASCII digits, from data given position.
// Note from Khafra: you wouldn't guess it, but this is also the same step as #8
const rangeEnd = collectASequenceOfCodePoints(
(char) => {
const code = char.charCodeAt(0)
return code >= 0x30 && code <= 0x39
},
data,
position
)
// 15. Let rangeEndValue be rangeEnd, interpreted as decimal number, if rangeEnd
// is not the empty string; otherwise null.
// Note from Khafra: THE SAME STEP, AGAIN!!!
// Note: why interpret as a decimal if we only collect ascii digits?
const rangeEndValue = rangeEnd.length ? Number(rangeEnd) : null
// 16. If position is not past the end of data, then return failure.
if (position.position < data.length) {
return 'failure'
}
// 17. If rangeEndValue and rangeStartValue are null, then return failure.
if (rangeEndValue === null && rangeStartValue === null) {
return 'failure'
}
// 18. If rangeStartValue and rangeEndValue are numbers, and rangeStartValue is
// greater than rangeEndValue, then return failure.
// Note: ... when can they not be numbers?
if (rangeStartValue > rangeEndValue) {
return 'failure'
}
// 19. Return (rangeStartValue, rangeEndValue).
return { rangeStartValue, rangeEndValue }
}
/**
* @see https://fetch.spec.whatwg.org/#build-a-content-range
* @param {number} rangeStart
* @param {number} rangeEnd
* @param {number} fullLength
*/
function buildContentRange (rangeStart, rangeEnd, fullLength) {
// 1. Let contentRange be `bytes `.
let contentRange = 'bytes '
// 2. Append rangeStart, serialized and isomorphic encoded, to contentRange.
contentRange += isomorphicEncode(`${rangeStart}`)
// 3. Append 0x2D (-) to contentRange.
contentRange += '-'
// 4. Append rangeEnd, serialized and isomorphic encoded to contentRange.
contentRange += isomorphicEncode(`${rangeEnd}`)
// 5. Append 0x2F (/) to contentRange.
contentRange += '/'
// 6. Append fullLength, serialized and isomorphic encoded to contentRange.
contentRange += isomorphicEncode(`${fullLength}`)
// 7. Return contentRange.
return contentRange
}
// A Stream, which pipes the response to zlib.createInflate() or
// zlib.createInflateRaw() depending on the first byte of the Buffer.
// If the lower byte of the first byte is 0x08, then the stream is
// interpreted as a zlib stream, otherwise it's interpreted as a
// raw deflate stream.
class InflateStream extends Transform {
#zlibOptions
/** @param {zlib.ZlibOptions} [zlibOptions] */
constructor (zlibOptions) {
super()
this.#zlibOptions = zlibOptions
}
_transform (chunk, encoding, callback) {
if (!this._inflateStream) {
if (chunk.length === 0) {
callback()
return
}
this._inflateStream = (chunk[0] & 0x0F) === 0x08
? zlib.createInflate(this.#zlibOptions)
: zlib.createInflateRaw(this.#zlibOptions)
this._inflateStream.on('data', this.push.bind(this))
this._inflateStream.on('end', () => this.push(null))
this._inflateStream.on('error', (err) => this.destroy(err))
}
this._inflateStream.write(chunk, encoding, callback)
}
_final (callback) {
if (this._inflateStream) {
this._inflateStream.end()
this._inflateStream = null
}
callback()
}
}
/**
* @param {zlib.ZlibOptions} [zlibOptions]
* @returns {InflateStream}
*/
function createInflate (zlibOptions) {
return new InflateStream(zlibOptions)
}
/**
* @see https://fetch.spec.whatwg.org/#concept-header-extract-mime-type
* @param {import('./headers').HeadersList} headers
*/
function extractMimeType (headers) {
// 1. Let charset be null.
let charset = null
// 2. Let essence be null.
let essence = null
// 3. Let mimeType be null.
let mimeType = null
// 4. Let values be the result of getting, decoding, and splitting `Content-Type` from headers.
const values = getDecodeSplit('content-type', headers)
// 5. If values is null, then return failure.
if (values === null) {
return 'failure'
}
// 6. For each value of values:
for (const value of values) {
// 6.1. Let temporaryMimeType be the result of parsing value.
const temporaryMimeType = parseMIMEType(value)
// 6.2. If temporaryMimeType is failure or its essence is "*/*", then continue.
if (temporaryMimeType === 'failure' || temporaryMimeType.essence === '*/*') {
continue
}
// 6.3. Set mimeType to temporaryMimeType.
mimeType = temporaryMimeType
// 6.4. If mimeType’s essence is not essence, then:
if (mimeType.essence !== essence) {
// 6.4.1. Set charset to null.
charset = null
// 6.4.2. If mimeType’s parameters["charset"] exists, then set charset to
// mimeType’s parameters["charset"].
if (mimeType.parameters.has('charset')) {
charset = mimeType.parameters.get('charset')
}
// 6.4.3. Set essence to mimeType’s essence.
essence = mimeType.essence
} else if (!mimeType.parameters.has('charset') && charset !== null) {
// 6.5. Otherwise, if mimeType’s parameters["charset"] does not exist, and
// charset is non-null, set mimeType’s parameters["charset"] to charset.
mimeType.parameters.set('charset', charset)
}
}
// 7. If mimeType is null, then return failure.
if (mimeType == null) {
return 'failure'
}
// 8. Return mimeType.
return mimeType
}
/**
* @see https://fetch.spec.whatwg.org/#header-value-get-decode-and-split
* @param {string|null} value
*/
function gettingDecodingSplitting (value) {
// 1. Let input be the result of isomorphic decoding value.
const input = value
// 2. Let position be a position variable for input, initially pointing at the start of input.
const position = { position: 0 }
// 3. Let values be a list of strings, initially empty.
const values = []
// 4. Let temporaryValue be the empty string.
let temporaryValue = ''
// 5. While position is not past the end of input:
while (position.position < input.length) {
// 5.1. Append the result of collecting a sequence of code points that are not U+0022 (")
// or U+002C (,) from input, given position, to temporaryValue.
temporaryValue += collectASequenceOfCodePoints(
(char) => char !== '"' && char !== ',',
input,
position
)
// 5.2. If position is not past the end of input, then:
if (position.position < input.length) {
// 5.2.1. If the code point at position within input is U+0022 ("), then:
if (input.charCodeAt(position.position) === 0x22) {
// 5.2.1.1. Append the result of collecting an HTTP quoted string from input, given position, to temporaryValue.
temporaryValue += collectAnHTTPQuotedString(
input,
position
)
// 5.2.1.2. If position is not past the end of input, then continue.
if (position.position < input.length) {
continue
}
} else {
// 5.2.2. Otherwise:
// 5.2.2.1. Assert: the code point at position within input is U+002C (,).
assert(input.charCodeAt(position.position) === 0x2C)
// 5.2.2.2. Advance position by 1.
position.position++
}
}
// 5.3. Remove all HTTP tab or space from the start and end of temporaryValue.
temporaryValue = removeChars(temporaryValue, true, true, (char) => char === 0x9 || char === 0x20)
// 5.4. Append temporaryValue to values.
values.push(temporaryValue)
// 5.6. Set temporaryValue to the empty string.
temporaryValue = ''
}
// 6. Return values.
return values
}
/**
* @see https://fetch.spec.whatwg.org/#concept-header-list-get-decode-split
* @param {string} name lowercase header name
* @param {import('./headers').HeadersList} list
*/
function getDecodeSplit (name, list) {
// 1. Let value be the result of getting name from list.
const value = list.get(name, true)
// 2. If value is null, then return null.
if (value === null) {
return null
}
// 3. Return the result of getting, decoding, and splitting value.
return gettingDecodingSplitting(value)
}
function hasAuthenticationEntry (request) {
return false
}
/**
* @see https://url.spec.whatwg.org/#include-credentials
* @param {URL} url
*/
function includesCredentials (url) {
// A URL includes credentials if its username or password is not the empty string.
return !!(url.username || url.password)
}
/**
* @see https://html.spec.whatwg.org/multipage/document-sequences.html#traversable-navigable
* @param {object|string} navigable
*/
function isTraversableNavigable (navigable) {
// TODO
return true
}
class EnvironmentSettingsObjectBase {
get baseUrl () {
return getGlobalOrigin()
}
get origin () {
return this.baseUrl?.origin
}
policyContainer = makePolicyContainer()
}
class EnvironmentSettingsObject {
settingsObject = new EnvironmentSettingsObjectBase()
}
const environmentSettingsObject = new EnvironmentSettingsObject()
module.exports = {
isAborted,
isCancelled,
isValidEncodedURL,
ReadableStreamFrom,
tryUpgradeRequestToAPotentiallyTrustworthyURL,
clampAndCoarsenConnectionTimingInfo,
coarsenedSharedCurrentTime,
determineRequestsReferrer,
makePolicyContainer,
clonePolicyContainer,
appendFetchMetadata,
appendRequestOriginHeader,
TAOCheck,
corsCheck,
crossOriginResourcePolicyCheck,
createOpaqueTimingInfo,
setRequestReferrerPolicyOnRedirect,
isValidHTTPToken,
requestBadPort,
requestCurrentURL,
responseURL,
responseLocationURL,
isURLPotentiallyTrustworthy,
isValidReasonPhrase,
sameOrigin,
normalizeMethod,
iteratorMixin,
createIterator,
isValidHeaderName,
isValidHeaderValue,
isErrorLike,
fullyReadBody,
readableStreamClose,
urlIsLocal,
urlHasHttpsScheme,
urlIsHttpHttpsScheme,
readAllBytes,
simpleRangeHeaderValue,
buildContentRange,
createInflate,
extractMimeType,
getDecodeSplit,
environmentSettingsObject,
isOriginIPPotentiallyTrustworthy,
hasAuthenticationEntry,
includesCredentials,
isTraversableNavigable
}
================================================
FILE: lib/web/infra/index.js
================================================
'use strict'
const assert = require('node:assert')
const { utf8DecodeBytes } = require('../../encoding')
/**
* @param {(char: string) => boolean} condition
* @param {string} input
* @param {{ position: number }} position
* @returns {string}
*
* @see https://infra.spec.whatwg.org/#collect-a-sequence-of-code-points
*/
function collectASequenceOfCodePoints (condition, input, position) {
// 1. Let result be the empty string.
let result = ''
// 2. While position doesn’t point past the end of input and the
// code point at position within input meets the condition condition:
while (position.position < input.length && condition(input[position.position])) {
// 1. Append that code point to the end of result.
result += input[position.position]
// 2. Advance position by 1.
position.position++
}
// 3. Return result.
return result
}
/**
* A faster collectASequenceOfCodePoints that only works when comparing a single character.
* @param {string} char
* @param {string} input
* @param {{ position: number }} position
* @returns {string}
*
* @see https://infra.spec.whatwg.org/#collect-a-sequence-of-code-points
*/
function collectASequenceOfCodePointsFast (char, input, position) {
const idx = input.indexOf(char, position.position)
const start = position.position
if (idx === -1) {
position.position = input.length
return input.slice(start)
}
position.position = idx
return input.slice(start, position.position)
}
const ASCII_WHITESPACE_REPLACE_REGEX = /[\u0009\u000A\u000C\u000D\u0020]/g // eslint-disable-line no-control-regex
/**
* @param {string} data
* @returns {Uint8Array | 'failure'}
*
* @see https://infra.spec.whatwg.org/#forgiving-base64-decode
*/
function forgivingBase64 (data) {
// 1. Remove all ASCII whitespace from data.
data = data.replace(ASCII_WHITESPACE_REPLACE_REGEX, '')
let dataLength = data.length
// 2. If data’s code point length divides by 4 leaving
// no remainder, then:
if (dataLength % 4 === 0) {
// 1. If data ends with one or two U+003D (=) code points,
// then remove them from data.
if (data.charCodeAt(dataLength - 1) === 0x003D) {
--dataLength
if (data.charCodeAt(dataLength - 1) === 0x003D) {
--dataLength
}
}
}
// 3. If data’s code point length divides by 4 leaving
// a remainder of 1, then return failure.
if (dataLength % 4 === 1) {
return 'failure'
}
// 4. If data contains a code point that is not one of
// U+002B (+)
// U+002F (/)
// ASCII alphanumeric
// then return failure.
if (/[^+/0-9A-Za-z]/.test(data.length === dataLength ? data : data.substring(0, dataLength))) {
return 'failure'
}
const buffer = Buffer.from(data, 'base64')
return new Uint8Array(buffer.buffer, buffer.byteOffset, buffer.byteLength)
}
/**
* @param {number} char
* @returns {boolean}
*
* @see https://infra.spec.whatwg.org/#ascii-whitespace
*/
function isASCIIWhitespace (char) {
return (
char === 0x09 || // \t
char === 0x0a || // \n
char === 0x0c || // \f
char === 0x0d || // \r
char === 0x20 // space
)
}
/**
* @param {Uint8Array} input
* @returns {string}
*
* @see https://infra.spec.whatwg.org/#isomorphic-decode
*/
function isomorphicDecode (input) {
// 1. To isomorphic decode a byte sequence input, return a string whose code point
// length is equal to input’s length and whose code points have the same values
// as the values of input’s bytes, in the same order.
const length = input.length
if ((2 << 15) - 1 > length) {
return String.fromCharCode.apply(null, input)
}
let result = ''
let i = 0
let addition = (2 << 15) - 1
while (i < length) {
if (i + addition > length) {
addition = length - i
}
result += String.fromCharCode.apply(null, input.subarray(i, i += addition))
}
return result
}
const invalidIsomorphicEncodeValueRegex = /[^\x00-\xFF]/ // eslint-disable-line no-control-regex
/**
* @param {string} input
* @returns {string}
*
* @see https://infra.spec.whatwg.org/#isomorphic-encode
*/
function isomorphicEncode (input) {
// 1. Assert: input contains no code points greater than U+00FF.
assert(!invalidIsomorphicEncodeValueRegex.test(input))
// 2. Return a byte sequence whose length is equal to input’s code
// point length and whose bytes have the same values as the
// values of input’s code points, in the same order
return input
}
/**
* @see https://infra.spec.whatwg.org/#parse-json-bytes-to-a-javascript-value
* @param {Uint8Array} bytes
*/
function parseJSONFromBytes (bytes) {
return JSON.parse(utf8DecodeBytes(bytes))
}
/**
* @param {string} str
* @param {boolean} [leading=true]
* @param {boolean} [trailing=true]
* @returns {string}
*
* @see https://infra.spec.whatwg.org/#strip-leading-and-trailing-ascii-whitespace
*/
function removeASCIIWhitespace (str, leading = true, trailing = true) {
return removeChars(str, leading, trailing, isASCIIWhitespace)
}
/**
* @param {string} str
* @param {boolean} leading
* @param {boolean} trailing
* @param {(charCode: number) => boolean} predicate
* @returns {string}
*/
function removeChars (str, leading, trailing, predicate) {
let lead = 0
let trail = str.length - 1
if (leading) {
while (lead < str.length && predicate(str.charCodeAt(lead))) lead++
}
if (trailing) {
while (trail > 0 && predicate(str.charCodeAt(trail))) trail--
}
return lead === 0 && trail === str.length - 1 ? str : str.slice(lead, trail + 1)
}
// https://infra.spec.whatwg.org/#serialize-a-javascript-value-to-a-json-string
function serializeJavascriptValueToJSONString (value) {
// 1. Let result be ? Call(%JSON.stringify%, undefined, « value »).
const result = JSON.stringify(value)
// 2. If result is undefined, then throw a TypeError.
if (result === undefined) {
throw new TypeError('Value is not JSON serializable')
}
// 3. Assert: result is a string.
assert(typeof result === 'string')
// 4. Return result.
return result
}
module.exports = {
collectASequenceOfCodePoints,
collectASequenceOfCodePointsFast,
forgivingBase64,
isASCIIWhitespace,
isomorphicDecode,
isomorphicEncode,
parseJSONFromBytes,
removeASCIIWhitespace,
removeChars,
serializeJavascriptValueToJSONString
}
================================================
FILE: lib/web/subresource-integrity/Readme.md
================================================
# Subresource Integrity
based on Editor’s Draft, 12 June 2025
This module provides support for Subresource Integrity (SRI) in the context of web fetch operations. SRI is a security feature that allows clients to verify that fetched resources are delivered without unexpected manipulation.
## Links
- [Subresource Integrity](https://w3c.github.io/webappsec-subresource-integrity/)
================================================
FILE: lib/web/subresource-integrity/subresource-integrity.js
================================================
'use strict'
const assert = require('node:assert')
const { runtimeFeatures } = require('../../util/runtime-features.js')
/**
* @typedef {object} Metadata
* @property {SRIHashAlgorithm} alg - The algorithm used for the hash.
* @property {string} val - The base64-encoded hash value.
*/
/**
* @typedef {Metadata[]} MetadataList
*/
/**
* @typedef {('sha256' | 'sha384' | 'sha512')} SRIHashAlgorithm
*/
/**
* @type {Map}
*
* The valid SRI hash algorithm token set is the ordered set « "sha256",
* "sha384", "sha512" » (corresponding to SHA-256, SHA-384, and SHA-512
* respectively). The ordering of this set is meaningful, with stronger
* algorithms appearing later in the set.
*
* @see https://w3c.github.io/webappsec-subresource-integrity/#valid-sri-hash-algorithm-token-set
*/
const validSRIHashAlgorithmTokenSet = new Map([['sha256', 0], ['sha384', 1], ['sha512', 2]])
// https://nodejs.org/api/crypto.html#determining-if-crypto-support-is-unavailable
/** @type {import('node:crypto')} */
let crypto
if (runtimeFeatures.has('crypto')) {
crypto = require('node:crypto')
const cryptoHashes = crypto.getHashes()
// If no hashes are available, we cannot support SRI.
if (cryptoHashes.length === 0) {
validSRIHashAlgorithmTokenSet.clear()
}
for (const algorithm of validSRIHashAlgorithmTokenSet.keys()) {
// If the algorithm is not supported, remove it from the list.
if (cryptoHashes.includes(algorithm) === false) {
validSRIHashAlgorithmTokenSet.delete(algorithm)
}
}
} else {
// If crypto is not available, we cannot support SRI.
validSRIHashAlgorithmTokenSet.clear()
}
/**
* @typedef GetSRIHashAlgorithmIndex
* @type {(algorithm: SRIHashAlgorithm) => number}
* @param {SRIHashAlgorithm} algorithm
* @returns {number} The index of the algorithm in the valid SRI hash algorithm
* token set.
*/
const getSRIHashAlgorithmIndex = /** @type {GetSRIHashAlgorithmIndex} */ (Map.prototype.get.bind(
validSRIHashAlgorithmTokenSet))
/**
* @typedef IsValidSRIHashAlgorithm
* @type {(algorithm: string) => algorithm is SRIHashAlgorithm}
* @param {*} algorithm
* @returns {algorithm is SRIHashAlgorithm}
*/
const isValidSRIHashAlgorithm = /** @type {IsValidSRIHashAlgorithm} */ (
Map.prototype.has.bind(validSRIHashAlgorithmTokenSet)
)
/**
* @param {Uint8Array} bytes
* @param {string} metadataList
* @returns {boolean}
*
* @see https://w3c.github.io/webappsec-subresource-integrity/#does-response-match-metadatalist
*/
const bytesMatch = runtimeFeatures.has('crypto') === false || validSRIHashAlgorithmTokenSet.size === 0
// If node is not built with OpenSSL support, we cannot check
// a request's integrity, so allow it by default (the spec will
// allow requests if an invalid hash is given, as precedence).
? () => true
: (bytes, metadataList) => {
// 1. Let parsedMetadata be the result of parsing metadataList.
const parsedMetadata = parseMetadata(metadataList)
// 2. If parsedMetadata is empty set, return true.
if (parsedMetadata.length === 0) {
return true
}
// 3. Let metadata be the result of getting the strongest
// metadata from parsedMetadata.
const metadata = getStrongestMetadata(parsedMetadata)
// 4. For each item in metadata:
for (const item of metadata) {
// 1. Let algorithm be the item["alg"].
const algorithm = item.alg
// 2. Let expectedValue be the item["val"].
const expectedValue = item.val
// See https://github.com/web-platform-tests/wpt/commit/e4c5cc7a5e48093220528dfdd1c4012dc3837a0e
// "be liberal with padding". This is annoying, and it's not even in the spec.
// 3. Let actualValue be the result of applying algorithm to bytes .
const actualValue = applyAlgorithmToBytes(algorithm, bytes)
// 4. If actualValue is a case-sensitive match for expectedValue,
// return true.
if (caseSensitiveMatch(actualValue, expectedValue)) {
return true
}
}
// 5. Return false.
return false
}
/**
* @param {MetadataList} metadataList
* @returns {MetadataList} The strongest hash algorithm from the metadata list.
*/
function getStrongestMetadata (metadataList) {
// 1. Let result be the empty set and strongest be the empty string.
const result = []
/** @type {Metadata|null} */
let strongest = null
// 2. For each item in set:
for (const item of metadataList) {
// 1. Assert: item["alg"] is a valid SRI hash algorithm token.
assert(isValidSRIHashAlgorithm(item.alg), 'Invalid SRI hash algorithm token')
// 2. If result is the empty set, then:
if (result.length === 0) {
// 1. Append item to result.
result.push(item)
// 2. Set strongest to item.
strongest = item
// 3. Continue.
continue
}
// 3. Let currentAlgorithm be strongest["alg"], and currentAlgorithmIndex be
// the index of currentAlgorithm in the valid SRI hash algorithm token set.
const currentAlgorithm = /** @type {Metadata} */ (strongest).alg
const currentAlgorithmIndex = getSRIHashAlgorithmIndex(currentAlgorithm)
// 4. Let newAlgorithm be the item["alg"], and newAlgorithmIndex be the
// index of newAlgorithm in the valid SRI hash algorithm token set.
const newAlgorithm = item.alg
const newAlgorithmIndex = getSRIHashAlgorithmIndex(newAlgorithm)
// 5. If newAlgorithmIndex is less than currentAlgorithmIndex, then continue.
if (newAlgorithmIndex < currentAlgorithmIndex) {
continue
// 6. Otherwise, if newAlgorithmIndex is greater than
// currentAlgorithmIndex:
} else if (newAlgorithmIndex > currentAlgorithmIndex) {
// 1. Set strongest to item.
strongest = item
// 2. Set result to « item ».
result[0] = item
result.length = 1
// 7. Otherwise, newAlgorithmIndex and currentAlgorithmIndex are the same
// value. Append item to result.
} else {
result.push(item)
}
}
// 3. Return result.
return result
}
/**
* @param {string} metadata
* @returns {MetadataList}
*
* @see https://w3c.github.io/webappsec-subresource-integrity/#parse-metadata
*/
function parseMetadata (metadata) {
// 1. Let result be the empty set.
/** @type {MetadataList} */
const result = []
// 2. For each item returned by splitting metadata on spaces:
for (const item of metadata.split(' ')) {
// 1. Let expression-and-options be the result of splitting item on U+003F (?).
const expressionAndOptions = item.split('?', 1)
// 2. Let algorithm-expression be expression-and-options[0].
const algorithmExpression = expressionAndOptions[0]
// 3. Let base64-value be the empty string.
let base64Value = ''
// 4. Let algorithm-and-value be the result of splitting algorithm-expression on U+002D (-).
const algorithmAndValue = [algorithmExpression.slice(0, 6), algorithmExpression.slice(7)]
// 5. Let algorithm be algorithm-and-value[0].
const algorithm = algorithmAndValue[0]
// 6. If algorithm is not a valid SRI hash algorithm token, then continue.
if (!isValidSRIHashAlgorithm(algorithm)) {
continue
}
// 7. If algorithm-and-value[1] exists, set base64-value to
// algorithm-and-value[1].
if (algorithmAndValue[1]) {
base64Value = algorithmAndValue[1]
}
// 8. Let metadata be the ordered map
// «["alg" → algorithm, "val" → base64-value]».
const metadata = {
alg: algorithm,
val: base64Value
}
// 9. Append metadata to result.
result.push(metadata)
}
// 3. Return result.
return result
}
/**
* Applies the specified hash algorithm to the given bytes
*
* @typedef {(algorithm: SRIHashAlgorithm, bytes: Uint8Array) => string} ApplyAlgorithmToBytes
* @param {SRIHashAlgorithm} algorithm
* @param {Uint8Array} bytes
* @returns {string}
*/
const applyAlgorithmToBytes = (algorithm, bytes) => {
return crypto.hash(algorithm, bytes, 'base64')
}
/**
* Compares two base64 strings, allowing for base64url
* in the second string.
*
* @param {string} actualValue base64 encoded string
* @param {string} expectedValue base64 or base64url encoded string
* @returns {boolean}
*/
function caseSensitiveMatch (actualValue, expectedValue) {
// Ignore padding characters from the end of the strings by
// decreasing the length by 1 or 2 if the last characters are `=`.
let actualValueLength = actualValue.length
if (actualValueLength !== 0 && actualValue[actualValueLength - 1] === '=') {
actualValueLength -= 1
}
if (actualValueLength !== 0 && actualValue[actualValueLength - 1] === '=') {
actualValueLength -= 1
}
let expectedValueLength = expectedValue.length
if (expectedValueLength !== 0 && expectedValue[expectedValueLength - 1] === '=') {
expectedValueLength -= 1
}
if (expectedValueLength !== 0 && expectedValue[expectedValueLength - 1] === '=') {
expectedValueLength -= 1
}
if (actualValueLength !== expectedValueLength) {
return false
}
for (let i = 0; i < actualValueLength; ++i) {
if (
actualValue[i] === expectedValue[i] ||
(actualValue[i] === '+' && expectedValue[i] === '-') ||
(actualValue[i] === '/' && expectedValue[i] === '_')
) {
continue
}
return false
}
return true
}
module.exports = {
applyAlgorithmToBytes,
bytesMatch,
caseSensitiveMatch,
isValidSRIHashAlgorithm,
getStrongestMetadata,
parseMetadata
}
================================================
FILE: lib/web/webidl/index.js
================================================
'use strict'
const assert = require('node:assert')
const { types, inspect } = require('node:util')
const { runtimeFeatures } = require('../../util/runtime-features')
const UNDEFINED = 1
const BOOLEAN = 2
const STRING = 3
const SYMBOL = 4
const NUMBER = 5
const BIGINT = 6
const NULL = 7
const OBJECT = 8 // function and object
const FunctionPrototypeSymbolHasInstance = Function.call.bind(Function.prototype[Symbol.hasInstance])
/** @type {import('../../../types/webidl').Webidl} */
const webidl = {
converters: {},
util: {},
errors: {},
is: {}
}
/**
* @description Instantiate an error.
*
* @param {Object} opts
* @param {string} opts.header
* @param {string} opts.message
* @returns {TypeError}
*/
webidl.errors.exception = function (message) {
return new TypeError(`${message.header}: ${message.message}`)
}
/**
* @description Instantiate an error when conversion from one type to another has failed.
*
* @param {Object} opts
* @param {string} opts.prefix
* @param {string} opts.argument
* @param {string[]} opts.types
* @returns {TypeError}
*/
webidl.errors.conversionFailed = function (opts) {
const plural = opts.types.length === 1 ? '' : ' one of'
const message =
`${opts.argument} could not be converted to` +
`${plural}: ${opts.types.join(', ')}.`
return webidl.errors.exception({
header: opts.prefix,
message
})
}
/**
* @description Instantiate an error when an invalid argument is provided
*
* @param {Object} context
* @param {string} context.prefix
* @param {string} context.value
* @param {string} context.type
* @returns {TypeError}
*/
webidl.errors.invalidArgument = function (context) {
return webidl.errors.exception({
header: context.prefix,
message: `"${context.value}" is an invalid ${context.type}.`
})
}
// https://webidl.spec.whatwg.org/#implements
webidl.brandCheck = function (V, I) {
if (!FunctionPrototypeSymbolHasInstance(I, V)) {
const err = new TypeError('Illegal invocation')
err.code = 'ERR_INVALID_THIS' // node compat.
throw err
}
}
webidl.brandCheckMultiple = function (List) {
const prototypes = List.map((c) => webidl.util.MakeTypeAssertion(c))
return (V) => {
if (prototypes.every(typeCheck => !typeCheck(V))) {
const err = new TypeError('Illegal invocation')
err.code = 'ERR_INVALID_THIS' // node compat.
throw err
}
}
}
webidl.argumentLengthCheck = function ({ length }, min, ctx) {
if (length < min) {
throw webidl.errors.exception({
message: `${min} argument${min !== 1 ? 's' : ''} required, ` +
`but${length ? ' only' : ''} ${length} found.`,
header: ctx
})
}
}
webidl.illegalConstructor = function () {
throw webidl.errors.exception({
header: 'TypeError',
message: 'Illegal constructor'
})
}
webidl.util.MakeTypeAssertion = function (I) {
return (O) => FunctionPrototypeSymbolHasInstance(I, O)
}
// https://tc39.es/ecma262/#sec-ecmascript-data-types-and-values
webidl.util.Type = function (V) {
switch (typeof V) {
case 'undefined': return UNDEFINED
case 'boolean': return BOOLEAN
case 'string': return STRING
case 'symbol': return SYMBOL
case 'number': return NUMBER
case 'bigint': return BIGINT
case 'function':
case 'object': {
if (V === null) {
return NULL
}
return OBJECT
}
}
}
webidl.util.Types = {
UNDEFINED,
BOOLEAN,
STRING,
SYMBOL,
NUMBER,
BIGINT,
NULL,
OBJECT
}
webidl.util.TypeValueToString = function (o) {
switch (webidl.util.Type(o)) {
case UNDEFINED: return 'Undefined'
case BOOLEAN: return 'Boolean'
case STRING: return 'String'
case SYMBOL: return 'Symbol'
case NUMBER: return 'Number'
case BIGINT: return 'BigInt'
case NULL: return 'Null'
case OBJECT: return 'Object'
}
}
webidl.util.markAsUncloneable = runtimeFeatures.has('markAsUncloneable')
? require('node:worker_threads').markAsUncloneable
: () => {}
// https://webidl.spec.whatwg.org/#abstract-opdef-converttoint
webidl.util.ConvertToInt = function (V, bitLength, signedness, flags) {
let upperBound
let lowerBound
// 1. If bitLength is 64, then:
if (bitLength === 64) {
// 1. Let upperBound be 2^53 − 1.
upperBound = Math.pow(2, 53) - 1
// 2. If signedness is "unsigned", then let lowerBound be 0.
if (signedness === 'unsigned') {
lowerBound = 0
} else {
// 3. Otherwise let lowerBound be −2^53 + 1.
lowerBound = Math.pow(-2, 53) + 1
}
} else if (signedness === 'unsigned') {
// 2. Otherwise, if signedness is "unsigned", then:
// 1. Let lowerBound be 0.
lowerBound = 0
// 2. Let upperBound be 2^bitLength − 1.
upperBound = Math.pow(2, bitLength) - 1
} else {
// 3. Otherwise:
// 1. Let lowerBound be -2^bitLength − 1.
lowerBound = Math.pow(-2, bitLength) - 1
// 2. Let upperBound be 2^bitLength − 1 − 1.
upperBound = Math.pow(2, bitLength - 1) - 1
}
// 4. Let x be ? ToNumber(V).
let x = Number(V)
// 5. If x is −0, then set x to +0.
if (x === 0) {
x = 0
}
// 6. If the conversion is to an IDL type associated
// with the [EnforceRange] extended attribute, then:
if (webidl.util.HasFlag(flags, webidl.attributes.EnforceRange)) {
// 1. If x is NaN, +∞, or −∞, then throw a TypeError.
if (
Number.isNaN(x) ||
x === Number.POSITIVE_INFINITY ||
x === Number.NEGATIVE_INFINITY
) {
throw webidl.errors.exception({
header: 'Integer conversion',
message: `Could not convert ${webidl.util.Stringify(V)} to an integer.`
})
}
// 2. Set x to IntegerPart(x).
x = webidl.util.IntegerPart(x)
// 3. If x < lowerBound or x > upperBound, then
// throw a TypeError.
if (x < lowerBound || x > upperBound) {
throw webidl.errors.exception({
header: 'Integer conversion',
message: `Value must be between ${lowerBound}-${upperBound}, got ${x}.`
})
}
// 4. Return x.
return x
}
// 7. If x is not NaN and the conversion is to an IDL
// type associated with the [Clamp] extended
// attribute, then:
if (!Number.isNaN(x) && webidl.util.HasFlag(flags, webidl.attributes.Clamp)) {
// 1. Set x to min(max(x, lowerBound), upperBound).
x = Math.min(Math.max(x, lowerBound), upperBound)
// 2. Round x to the nearest integer, choosing the
// even integer if it lies halfway between two,
// and choosing +0 rather than −0.
if (Math.floor(x) % 2 === 0) {
x = Math.floor(x)
} else {
x = Math.ceil(x)
}
// 3. Return x.
return x
}
// 8. If x is NaN, +0, +∞, or −∞, then return +0.
if (
Number.isNaN(x) ||
(x === 0 && Object.is(0, x)) ||
x === Number.POSITIVE_INFINITY ||
x === Number.NEGATIVE_INFINITY
) {
return 0
}
// 9. Set x to IntegerPart(x).
x = webidl.util.IntegerPart(x)
// 10. Set x to x modulo 2^bitLength.
x = x % Math.pow(2, bitLength)
// 11. If signedness is "signed" and x ≥ 2^bitLength − 1,
// then return x − 2^bitLength.
if (signedness === 'signed' && x >= Math.pow(2, bitLength) - 1) {
return x - Math.pow(2, bitLength)
}
// 12. Otherwise, return x.
return x
}
// https://webidl.spec.whatwg.org/#abstract-opdef-integerpart
webidl.util.IntegerPart = function (n) {
// 1. Let r be floor(abs(n)).
const r = Math.floor(Math.abs(n))
// 2. If n < 0, then return -1 × r.
if (n < 0) {
return -1 * r
}
// 3. Otherwise, return r.
return r
}
webidl.util.Stringify = function (V) {
const type = webidl.util.Type(V)
switch (type) {
case SYMBOL:
return `Symbol(${V.description})`
case OBJECT:
return inspect(V)
case STRING:
return `"${V}"`
case BIGINT:
return `${V}n`
default:
return `${V}`
}
}
webidl.util.IsResizableArrayBuffer = function (V) {
if (types.isArrayBuffer(V)) {
return V.resizable
}
if (types.isSharedArrayBuffer(V)) {
return V.growable
}
throw webidl.errors.exception({
header: 'IsResizableArrayBuffer',
message: `"${webidl.util.Stringify(V)}" is not an array buffer.`
})
}
webidl.util.HasFlag = function (flags, attributes) {
return typeof flags === 'number' && (flags & attributes) === attributes
}
// https://webidl.spec.whatwg.org/#es-sequence
webidl.sequenceConverter = function (converter) {
return (V, prefix, argument, Iterable) => {
// 1. If Type(V) is not Object, throw a TypeError.
if (webidl.util.Type(V) !== OBJECT) {
throw webidl.errors.exception({
header: prefix,
message: `${argument} (${webidl.util.Stringify(V)}) is not iterable.`
})
}
// 2. Let method be ? GetMethod(V, @@iterator).
/** @type {Generator} */
const method = typeof Iterable === 'function' ? Iterable() : V?.[Symbol.iterator]?.()
const seq = []
let index = 0
// 3. If method is undefined, throw a TypeError.
if (
method === undefined ||
typeof method.next !== 'function'
) {
throw webidl.errors.exception({
header: prefix,
message: `${argument} is not iterable.`
})
}
// https://webidl.spec.whatwg.org/#create-sequence-from-iterable
while (true) {
const { done, value } = method.next()
if (done) {
break
}
seq.push(converter(value, prefix, `${argument}[${index++}]`))
}
return seq
}
}
// https://webidl.spec.whatwg.org/#es-to-record
webidl.recordConverter = function (keyConverter, valueConverter) {
return (O, prefix, argument) => {
// 1. If Type(O) is not Object, throw a TypeError.
if (webidl.util.Type(O) !== OBJECT) {
throw webidl.errors.exception({
header: prefix,
message: `${argument} ("${webidl.util.TypeValueToString(O)}") is not an Object.`
})
}
// 2. Let result be a new empty instance of record.
const result = {}
if (!types.isProxy(O)) {
// 1. Let desc be ? O.[[GetOwnProperty]](key).
const keys = [...Object.getOwnPropertyNames(O), ...Object.getOwnPropertySymbols(O)]
for (const key of keys) {
const keyName = webidl.util.Stringify(key)
// 1. Let typedKey be key converted to an IDL value of type K.
const typedKey = keyConverter(key, prefix, `Key ${keyName} in ${argument}`)
// 2. Let value be ? Get(O, key).
// 3. Let typedValue be value converted to an IDL value of type V.
const typedValue = valueConverter(O[key], prefix, `${argument}[${keyName}]`)
// 4. Set result[typedKey] to typedValue.
result[typedKey] = typedValue
}
// 5. Return result.
return result
}
// 3. Let keys be ? O.[[OwnPropertyKeys]]().
const keys = Reflect.ownKeys(O)
// 4. For each key of keys.
for (const key of keys) {
// 1. Let desc be ? O.[[GetOwnProperty]](key).
const desc = Reflect.getOwnPropertyDescriptor(O, key)
// 2. If desc is not undefined and desc.[[Enumerable]] is true:
if (desc?.enumerable) {
// 1. Let typedKey be key converted to an IDL value of type K.
const typedKey = keyConverter(key, prefix, argument)
// 2. Let value be ? Get(O, key).
// 3. Let typedValue be value converted to an IDL value of type V.
const typedValue = valueConverter(O[key], prefix, argument)
// 4. Set result[typedKey] to typedValue.
result[typedKey] = typedValue
}
}
// 5. Return result.
return result
}
}
webidl.interfaceConverter = function (TypeCheck, name) {
return (V, prefix, argument) => {
if (!TypeCheck(V)) {
throw webidl.errors.exception({
header: prefix,
message: `Expected ${argument} ("${webidl.util.Stringify(V)}") to be an instance of ${name}.`
})
}
return V
}
}
webidl.dictionaryConverter = function (converters) {
// "For each dictionary member member declared on dictionary, in lexicographical order:"
converters.sort((a, b) => (a.key > b.key) - (a.key < b.key))
return (dictionary, prefix, argument) => {
const dict = {}
if (dictionary != null && webidl.util.Type(dictionary) !== OBJECT) {
throw webidl.errors.exception({
header: prefix,
message: `Expected ${dictionary} to be one of: Null, Undefined, Object.`
})
}
for (const options of converters) {
const { key, defaultValue, required, converter } = options
if (required === true) {
if (dictionary == null || !Object.hasOwn(dictionary, key)) {
throw webidl.errors.exception({
header: prefix,
message: `Missing required key "${key}".`
})
}
}
let value = dictionary?.[key]
const hasDefault = defaultValue !== undefined
// Only use defaultValue if value is undefined and
// a defaultValue options was provided.
if (hasDefault && value === undefined) {
value = defaultValue()
}
// A key can be optional and have no default value.
// When this happens, do not perform a conversion,
// and do not assign the key a value.
if (required || hasDefault || value !== undefined) {
value = converter(value, prefix, `${argument}.${key}`)
if (
options.allowedValues &&
!options.allowedValues.includes(value)
) {
throw webidl.errors.exception({
header: prefix,
message: `${value} is not an accepted type. Expected one of ${options.allowedValues.join(', ')}.`
})
}
dict[key] = value
}
}
return dict
}
}
webidl.nullableConverter = function (converter) {
return (V, prefix, argument) => {
if (V === null) {
return V
}
return converter(V, prefix, argument)
}
}
/**
* @param {*} value
* @returns {boolean}
*/
webidl.is.USVString = function (value) {
return (
typeof value === 'string' &&
value.isWellFormed()
)
}
webidl.is.ReadableStream = webidl.util.MakeTypeAssertion(ReadableStream)
webidl.is.Blob = webidl.util.MakeTypeAssertion(Blob)
webidl.is.URLSearchParams = webidl.util.MakeTypeAssertion(URLSearchParams)
webidl.is.File = webidl.util.MakeTypeAssertion(File)
webidl.is.URL = webidl.util.MakeTypeAssertion(URL)
webidl.is.AbortSignal = webidl.util.MakeTypeAssertion(AbortSignal)
webidl.is.MessagePort = webidl.util.MakeTypeAssertion(MessagePort)
webidl.is.BufferSource = function (V) {
return types.isArrayBuffer(V) || (
ArrayBuffer.isView(V) &&
types.isArrayBuffer(V.buffer)
)
}
// https://webidl.spec.whatwg.org/#dfn-get-buffer-source-copy
webidl.util.getCopyOfBytesHeldByBufferSource = function (bufferSource) {
// 1. Let jsBufferSource be the result of converting bufferSource to a JavaScript value.
const jsBufferSource = bufferSource
// 2. Let jsArrayBuffer be jsBufferSource.
let jsArrayBuffer = jsBufferSource
// 3. Let offset be 0.
let offset = 0
// 4. Let length be 0.
let length = 0
// 5. If jsBufferSource has a [[ViewedArrayBuffer]] internal slot, then:
if (types.isTypedArray(jsBufferSource) || types.isDataView(jsBufferSource)) {
// 5.1. Set jsArrayBuffer to jsBufferSource.[[ViewedArrayBuffer]].
jsArrayBuffer = jsBufferSource.buffer
// 5.2. Set offset to jsBufferSource.[[ByteOffset]].
offset = jsBufferSource.byteOffset
// 5.3. Set length to jsBufferSource.[[ByteLength]].
length = jsBufferSource.byteLength
} else {
// 6. Otherwise:
// 6.1. Assert: jsBufferSource is an ArrayBuffer or SharedArrayBuffer object.
assert(types.isAnyArrayBuffer(jsBufferSource))
// 6.2. Set length to jsBufferSource.[[ArrayBufferByteLength]].
length = jsBufferSource.byteLength
}
// 7. If IsDetachedBuffer(jsArrayBuffer) is true, then return the empty byte sequence.
if (jsArrayBuffer.detached) {
return new Uint8Array(0)
}
// 8. Let bytes be a new byte sequence of length equal to length.
const bytes = new Uint8Array(length)
// 9. For i in the range offset to offset + length − 1, inclusive,
// set bytes[i − offset] to GetValueFromBuffer(jsArrayBuffer, i, Uint8, true, Unordered).
const view = new Uint8Array(jsArrayBuffer, offset, length)
bytes.set(view)
// 10. Return bytes.
return bytes
}
// https://webidl.spec.whatwg.org/#es-DOMString
webidl.converters.DOMString = function (V, prefix, argument, flags) {
// 1. If V is null and the conversion is to an IDL type
// associated with the [LegacyNullToEmptyString]
// extended attribute, then return the DOMString value
// that represents the empty string.
if (V === null && webidl.util.HasFlag(flags, webidl.attributes.LegacyNullToEmptyString)) {
return ''
}
// 2. Let x be ? ToString(V).
if (typeof V === 'symbol') {
throw webidl.errors.exception({
header: prefix,
message: `${argument} is a symbol, which cannot be converted to a DOMString.`
})
}
// 3. Return the IDL DOMString value that represents the
// same sequence of code units as the one the
// ECMAScript String value x represents.
return String(V)
}
// https://webidl.spec.whatwg.org/#es-ByteString
webidl.converters.ByteString = function (V, prefix, argument) {
// 1. Let x be ? ToString(V).
if (typeof V === 'symbol') {
throw webidl.errors.exception({
header: prefix,
message: `${argument} is a symbol, which cannot be converted to a ByteString.`
})
}
const x = String(V)
// 2. If the value of any element of x is greater than
// 255, then throw a TypeError.
for (let index = 0; index < x.length; index++) {
if (x.charCodeAt(index) > 255) {
throw new TypeError(
'Cannot convert argument to a ByteString because the character at ' +
`index ${index} has a value of ${x.charCodeAt(index)} which is greater than 255.`
)
}
}
// 3. Return an IDL ByteString value whose length is the
// length of x, and where the value of each element is
// the value of the corresponding element of x.
return x
}
/**
* @param {unknown} value
* @returns {string}
* @see https://webidl.spec.whatwg.org/#es-USVString
*/
webidl.converters.USVString = function (value) {
// TODO: rewrite this so we can control the errors thrown
if (typeof value === 'string') {
return value.toWellFormed()
}
return `${value}`.toWellFormed()
}
// https://webidl.spec.whatwg.org/#es-boolean
webidl.converters.boolean = function (V) {
// 1. Let x be the result of computing ToBoolean(V).
// https://262.ecma-international.org/10.0/index.html#table-10
const x = Boolean(V)
// 2. Return the IDL boolean value that is the one that represents
// the same truth value as the ECMAScript Boolean value x.
return x
}
// https://webidl.spec.whatwg.org/#es-any
webidl.converters.any = function (V) {
return V
}
// https://webidl.spec.whatwg.org/#es-long-long
webidl.converters['long long'] = function (V, prefix, argument) {
// 1. Let x be ? ConvertToInt(V, 64, "signed").
const x = webidl.util.ConvertToInt(V, 64, 'signed', 0, prefix, argument)
// 2. Return the IDL long long value that represents
// the same numeric value as x.
return x
}
// https://webidl.spec.whatwg.org/#es-unsigned-long-long
webidl.converters['unsigned long long'] = function (V, prefix, argument) {
// 1. Let x be ? ConvertToInt(V, 64, "unsigned").
const x = webidl.util.ConvertToInt(V, 64, 'unsigned', 0, prefix, argument)
// 2. Return the IDL unsigned long long value that
// represents the same numeric value as x.
return x
}
// https://webidl.spec.whatwg.org/#es-unsigned-long
webidl.converters['unsigned long'] = function (V, prefix, argument) {
// 1. Let x be ? ConvertToInt(V, 32, "unsigned").
const x = webidl.util.ConvertToInt(V, 32, 'unsigned', 0, prefix, argument)
// 2. Return the IDL unsigned long value that
// represents the same numeric value as x.
return x
}
// https://webidl.spec.whatwg.org/#es-unsigned-short
webidl.converters['unsigned short'] = function (V, prefix, argument, flags) {
// 1. Let x be ? ConvertToInt(V, 16, "unsigned").
const x = webidl.util.ConvertToInt(V, 16, 'unsigned', flags, prefix, argument)
// 2. Return the IDL unsigned short value that represents
// the same numeric value as x.
return x
}
// https://webidl.spec.whatwg.org/#idl-ArrayBuffer
webidl.converters.ArrayBuffer = function (V, prefix, argument, flags) {
// 1. If V is not an Object, or V does not have an
// [[ArrayBufferData]] internal slot, then throw a
// TypeError.
// 2. If IsSharedArrayBuffer(V) is true, then throw a
// TypeError.
// see: https://tc39.es/ecma262/#sec-properties-of-the-arraybuffer-instances
if (
webidl.util.Type(V) !== OBJECT ||
!types.isArrayBuffer(V)
) {
throw webidl.errors.conversionFailed({
prefix,
argument: `${argument} ("${webidl.util.Stringify(V)}")`,
types: ['ArrayBuffer']
})
}
// 3. If the conversion is not to an IDL type associated
// with the [AllowResizable] extended attribute, and
// IsResizableArrayBuffer(V) is true, then throw a
// TypeError.
if (!webidl.util.HasFlag(flags, webidl.attributes.AllowResizable) && webidl.util.IsResizableArrayBuffer(V)) {
throw webidl.errors.exception({
header: prefix,
message: `${argument} cannot be a resizable ArrayBuffer.`
})
}
// 4. Return the IDL ArrayBuffer value that is a
// reference to the same object as V.
return V
}
// https://webidl.spec.whatwg.org/#idl-SharedArrayBuffer
webidl.converters.SharedArrayBuffer = function (V, prefix, argument, flags) {
// 1. If V is not an Object, or V does not have an
// [[ArrayBufferData]] internal slot, then throw a
// TypeError.
// 2. If IsSharedArrayBuffer(V) is false, then throw a
// TypeError.
// see: https://tc39.es/ecma262/#sec-properties-of-the-sharedarraybuffer-instances
if (
webidl.util.Type(V) !== OBJECT ||
!types.isSharedArrayBuffer(V)
) {
throw webidl.errors.conversionFailed({
prefix,
argument: `${argument} ("${webidl.util.Stringify(V)}")`,
types: ['SharedArrayBuffer']
})
}
// 3. If the conversion is not to an IDL type associated
// with the [AllowResizable] extended attribute, and
// IsResizableArrayBuffer(V) is true, then throw a
// TypeError.
if (!webidl.util.HasFlag(flags, webidl.attributes.AllowResizable) && webidl.util.IsResizableArrayBuffer(V)) {
throw webidl.errors.exception({
header: prefix,
message: `${argument} cannot be a resizable SharedArrayBuffer.`
})
}
// 4. Return the IDL SharedArrayBuffer value that is a
// reference to the same object as V.
return V
}
// https://webidl.spec.whatwg.org/#dfn-typed-array-type
webidl.converters.TypedArray = function (V, T, prefix, argument, flags) {
// 1. Let T be the IDL type V is being converted to.
// 2. If Type(V) is not Object, or V does not have a
// [[TypedArrayName]] internal slot with a value
// equal to T’s name, then throw a TypeError.
if (
webidl.util.Type(V) !== OBJECT ||
!types.isTypedArray(V) ||
V.constructor.name !== T.name
) {
throw webidl.errors.conversionFailed({
prefix,
argument: `${argument} ("${webidl.util.Stringify(V)}")`,
types: [T.name]
})
}
// 3. If the conversion is not to an IDL type associated
// with the [AllowShared] extended attribute, and
// IsSharedArrayBuffer(V.[[ViewedArrayBuffer]]) is
// true, then throw a TypeError.
if (!webidl.util.HasFlag(flags, webidl.attributes.AllowShared) && types.isSharedArrayBuffer(V.buffer)) {
throw webidl.errors.exception({
header: prefix,
message: `${argument} cannot be a view on a shared array buffer.`
})
}
// 4. If the conversion is not to an IDL type associated
// with the [AllowResizable] extended attribute, and
// IsResizableArrayBuffer(V.[[ViewedArrayBuffer]]) is
// true, then throw a TypeError.
if (!webidl.util.HasFlag(flags, webidl.attributes.AllowResizable) && webidl.util.IsResizableArrayBuffer(V.buffer)) {
throw webidl.errors.exception({
header: prefix,
message: `${argument} cannot be a view on a resizable array buffer.`
})
}
// 5. Return the IDL value of type T that is a reference
// to the same object as V.
return V
}
// https://webidl.spec.whatwg.org/#idl-DataView
webidl.converters.DataView = function (V, prefix, argument, flags) {
// 1. If Type(V) is not Object, or V does not have a
// [[DataView]] internal slot, then throw a TypeError.
if (webidl.util.Type(V) !== OBJECT || !types.isDataView(V)) {
throw webidl.errors.conversionFailed({
prefix,
argument: `${argument} ("${webidl.util.Stringify(V)}")`,
types: ['DataView']
})
}
// 2. If the conversion is not to an IDL type associated
// with the [AllowShared] extended attribute, and
// IsSharedArrayBuffer(V.[[ViewedArrayBuffer]]) is true,
// then throw a TypeError.
if (!webidl.util.HasFlag(flags, webidl.attributes.AllowShared) && types.isSharedArrayBuffer(V.buffer)) {
throw webidl.errors.exception({
header: prefix,
message: `${argument} cannot be a view on a shared array buffer.`
})
}
// 3. If the conversion is not to an IDL type associated
// with the [AllowResizable] extended attribute, and
// IsResizableArrayBuffer(V.[[ViewedArrayBuffer]]) is
// true, then throw a TypeError.
if (!webidl.util.HasFlag(flags, webidl.attributes.AllowResizable) && webidl.util.IsResizableArrayBuffer(V.buffer)) {
throw webidl.errors.exception({
header: prefix,
message: `${argument} cannot be a view on a resizable array buffer.`
})
}
// 4. Return the IDL DataView value that is a reference
// to the same object as V.
return V
}
// https://webidl.spec.whatwg.org/#ArrayBufferView
webidl.converters.ArrayBufferView = function (V, prefix, argument, flags) {
if (
webidl.util.Type(V) !== OBJECT ||
!types.isArrayBufferView(V)
) {
throw webidl.errors.conversionFailed({
prefix,
argument: `${argument} ("${webidl.util.Stringify(V)}")`,
types: ['ArrayBufferView']
})
}
if (!webidl.util.HasFlag(flags, webidl.attributes.AllowShared) && types.isSharedArrayBuffer(V.buffer)) {
throw webidl.errors.exception({
header: prefix,
message: `${argument} cannot be a view on a shared array buffer.`
})
}
if (!webidl.util.HasFlag(flags, webidl.attributes.AllowResizable) && webidl.util.IsResizableArrayBuffer(V.buffer)) {
throw webidl.errors.exception({
header: prefix,
message: `${argument} cannot be a view on a resizable array buffer.`
})
}
return V
}
// https://webidl.spec.whatwg.org/#BufferSource
webidl.converters.BufferSource = function (V, prefix, argument, flags) {
if (types.isArrayBuffer(V)) {
return webidl.converters.ArrayBuffer(V, prefix, argument, flags)
}
if (types.isArrayBufferView(V)) {
flags &= ~webidl.attributes.AllowShared
return webidl.converters.ArrayBufferView(V, prefix, argument, flags)
}
// Make this explicit for easier debugging
if (types.isSharedArrayBuffer(V)) {
throw webidl.errors.exception({
header: prefix,
message: `${argument} cannot be a SharedArrayBuffer.`
})
}
throw webidl.errors.conversionFailed({
prefix,
argument: `${argument} ("${webidl.util.Stringify(V)}")`,
types: ['ArrayBuffer', 'ArrayBufferView']
})
}
// https://webidl.spec.whatwg.org/#AllowSharedBufferSource
webidl.converters.AllowSharedBufferSource = function (V, prefix, argument, flags) {
if (types.isArrayBuffer(V)) {
return webidl.converters.ArrayBuffer(V, prefix, argument, flags)
}
if (types.isSharedArrayBuffer(V)) {
return webidl.converters.SharedArrayBuffer(V, prefix, argument, flags)
}
if (types.isArrayBufferView(V)) {
flags |= webidl.attributes.AllowShared
return webidl.converters.ArrayBufferView(V, prefix, argument, flags)
}
throw webidl.errors.conversionFailed({
prefix,
argument: `${argument} ("${webidl.util.Stringify(V)}")`,
types: ['ArrayBuffer', 'SharedArrayBuffer', 'ArrayBufferView']
})
}
webidl.converters['sequence'] = webidl.sequenceConverter(
webidl.converters.ByteString
)
webidl.converters['sequence>'] = webidl.sequenceConverter(
webidl.converters['sequence']
)
webidl.converters['record'] = webidl.recordConverter(
webidl.converters.ByteString,
webidl.converters.ByteString
)
webidl.converters.Blob = webidl.interfaceConverter(webidl.is.Blob, 'Blob')
webidl.converters.AbortSignal = webidl.interfaceConverter(
webidl.is.AbortSignal,
'AbortSignal'
)
/**
* [LegacyTreatNonObjectAsNull]
* callback EventHandlerNonNull = any (Event event);
* typedef EventHandlerNonNull? EventHandler;
* @param {*} V
*/
webidl.converters.EventHandlerNonNull = function (V) {
if (webidl.util.Type(V) !== OBJECT) {
return null
}
// [I]f the value is not an object, it will be converted to null, and if the value is not callable,
// it will be converted to a callback function value that does nothing when called.
if (typeof V === 'function') {
return V
}
return () => {}
}
webidl.attributes = {
Clamp: 1 << 0,
EnforceRange: 1 << 1,
AllowShared: 1 << 2,
AllowResizable: 1 << 3,
LegacyNullToEmptyString: 1 << 4
}
module.exports = {
webidl
}
================================================
FILE: lib/web/websocket/connection.js
================================================
'use strict'
const { uid, states, sentCloseFrameState, emptyBuffer, opcodes } = require('./constants')
const { parseExtensions, isClosed, isClosing, isEstablished, isConnecting, validateCloseCodeAndReason } = require('./util')
const { makeRequest } = require('../fetch/request')
const { fetching } = require('../fetch/index')
const { Headers, getHeadersList } = require('../fetch/headers')
const { getDecodeSplit } = require('../fetch/util')
const { WebsocketFrameSend } = require('./frame')
const assert = require('node:assert')
const { runtimeFeatures } = require('../../util/runtime-features')
const crypto = runtimeFeatures.has('crypto')
? require('node:crypto')
: null
let warningEmitted = false
/**
* @see https://websockets.spec.whatwg.org/#concept-websocket-establish
* @param {URL} url
* @param {string|string[]} protocols
* @param {import('./websocket').Handler} handler
* @param {Partial} options
*/
function establishWebSocketConnection (url, protocols, client, handler, options) {
// 1. Let requestURL be a copy of url, with its scheme set to "http", if url’s
// scheme is "ws", and to "https" otherwise.
const requestURL = url
requestURL.protocol = url.protocol === 'ws:' ? 'http:' : 'https:'
// 2. Let request be a new request, whose URL is requestURL, client is client,
// service-workers mode is "none", referrer is "no-referrer", mode is
// "websocket", credentials mode is "include", cache mode is "no-store" ,
// redirect mode is "error", and use-URL-credentials flag is set.
const request = makeRequest({
urlList: [requestURL],
client,
serviceWorkers: 'none',
referrer: 'no-referrer',
mode: 'websocket',
credentials: 'include',
cache: 'no-store',
redirect: 'error',
useURLCredentials: true
})
// Note: undici extension, allow setting custom headers.
if (options.headers) {
const headersList = getHeadersList(new Headers(options.headers))
request.headersList = headersList
}
// 3. Append (`Upgrade`, `websocket`) to request’s header list.
// 4. Append (`Connection`, `Upgrade`) to request’s header list.
// Note: both of these are handled by undici currently.
// https://github.com/nodejs/undici/blob/68c269c4144c446f3f1220951338daef4a6b5ec4/lib/client.js#L1397
// 5. Let keyValue be a nonce consisting of a randomly selected
// 16-byte value that has been forgiving-base64-encoded and
// isomorphic encoded.
const keyValue = crypto.randomBytes(16).toString('base64')
// 6. Append (`Sec-WebSocket-Key`, keyValue) to request’s
// header list.
request.headersList.append('sec-websocket-key', keyValue, true)
// 7. Append (`Sec-WebSocket-Version`, `13`) to request’s
// header list.
request.headersList.append('sec-websocket-version', '13', true)
// 8. For each protocol in protocols, combine
// (`Sec-WebSocket-Protocol`, protocol) in request’s header
// list.
for (const protocol of protocols) {
request.headersList.append('sec-websocket-protocol', protocol, true)
}
// 9. Let permessageDeflate be a user-agent defined
// "permessage-deflate" extension header value.
// https://github.com/mozilla/gecko-dev/blob/ce78234f5e653a5d3916813ff990f053510227bc/netwerk/protocol/websocket/WebSocketChannel.cpp#L2673
const permessageDeflate = 'permessage-deflate; client_max_window_bits'
// 10. Append (`Sec-WebSocket-Extensions`, permessageDeflate) to
// request’s header list.
request.headersList.append('sec-websocket-extensions', permessageDeflate, true)
// 11. Fetch request with useParallelQueue set to true, and
// processResponse given response being these steps:
const controller = fetching({
request,
useParallelQueue: true,
dispatcher: options.dispatcher,
processResponse (response) {
// 1. If response is a network error or its status is not 101,
// fail the WebSocket connection.
// if (response.type === 'error' || ((response.socket?.session != null && response.status !== 200) && response.status !== 101)) {
if (response.type === 'error' || response.status !== 101) {
// The presence of a session property on the socket indicates HTTP2
// HTTP1
if (response.socket?.session == null) {
failWebsocketConnection(handler, 1002, 'Received network error or non-101 status code.', response.error)
return
}
// HTTP2
if (response.status !== 200) {
failWebsocketConnection(handler, 1002, 'Received network error or non-200 status code.', response.error)
return
}
}
if (warningEmitted === false && response.socket?.session != null) {
process.emitWarning('WebSocket over HTTP2 is experimental, and subject to change.', 'ExperimentalWarning')
warningEmitted = true
}
// 2. If protocols is not the empty list and extracting header
// list values given `Sec-WebSocket-Protocol` and response’s
// header list results in null, failure, or the empty byte
// sequence, then fail the WebSocket connection.
if (protocols.length !== 0 && !response.headersList.get('Sec-WebSocket-Protocol')) {
failWebsocketConnection(handler, 1002, 'Server did not respond with sent protocols.')
return
}
// 3. Follow the requirements stated step 2 to step 6, inclusive,
// of the last set of steps in section 4.1 of The WebSocket
// Protocol to validate response. This either results in fail
// the WebSocket connection or the WebSocket connection is
// established.
// 2. If the response lacks an |Upgrade| header field or the |Upgrade|
// header field contains a value that is not an ASCII case-
// insensitive match for the value "websocket", the client MUST
// _Fail the WebSocket Connection_.
// For H2, no upgrade header is expected.
if (response.socket.session == null && response.headersList.get('Upgrade')?.toLowerCase() !== 'websocket') {
failWebsocketConnection(handler, 1002, 'Server did not set Upgrade header to "websocket".')
return
}
// 3. If the response lacks a |Connection| header field or the
// |Connection| header field doesn't contain a token that is an
// ASCII case-insensitive match for the value "Upgrade", the client
// MUST _Fail the WebSocket Connection_.
// For H2, no connection header is expected.
if (response.socket.session == null && response.headersList.get('Connection')?.toLowerCase() !== 'upgrade') {
failWebsocketConnection(handler, 1002, 'Server did not set Connection header to "upgrade".')
return
}
// 4. If the response lacks a |Sec-WebSocket-Accept| header field or
// the |Sec-WebSocket-Accept| contains a value other than the
// base64-encoded SHA-1 of the concatenation of the |Sec-WebSocket-
// Key| (as a string, not base64-decoded) with the string "258EAFA5-
// E914-47DA-95CA-C5AB0DC85B11" but ignoring any leading and
// trailing whitespace, the client MUST _Fail the WebSocket
// Connection_.
const secWSAccept = response.headersList.get('Sec-WebSocket-Accept')
const digest = crypto.hash('sha1', keyValue + uid, 'base64')
if (secWSAccept !== digest) {
failWebsocketConnection(handler, 1002, 'Incorrect hash received in Sec-WebSocket-Accept header.')
return
}
// 5. If the response includes a |Sec-WebSocket-Extensions| header
// field and this header field indicates the use of an extension
// that was not present in the client's handshake (the server has
// indicated an extension not requested by the client), the client
// MUST _Fail the WebSocket Connection_. (The parsing of this
// header field to determine which extensions are requested is
// discussed in Section 9.1.)
const secExtension = response.headersList.get('Sec-WebSocket-Extensions')
let extensions
if (secExtension !== null) {
extensions = parseExtensions(secExtension)
if (!extensions.has('permessage-deflate')) {
failWebsocketConnection(handler, 1002, 'Sec-WebSocket-Extensions header does not match.')
return
}
}
// 6. If the response includes a |Sec-WebSocket-Protocol| header field
// and this header field indicates the use of a subprotocol that was
// not present in the client's handshake (the server has indicated a
// subprotocol not requested by the client), the client MUST _Fail
// the WebSocket Connection_.
const secProtocol = response.headersList.get('Sec-WebSocket-Protocol')
if (secProtocol !== null) {
const requestProtocols = getDecodeSplit('sec-websocket-protocol', request.headersList)
// The client can request that the server use a specific subprotocol by
// including the |Sec-WebSocket-Protocol| field in its handshake. If it
// is specified, the server needs to include the same field and one of
// the selected subprotocol values in its response for the connection to
// be established.
if (!requestProtocols.includes(secProtocol)) {
failWebsocketConnection(handler, 1002, 'Protocol was not set in the opening handshake.')
return
}
}
response.socket.on('data', handler.onSocketData)
response.socket.on('close', handler.onSocketClose)
response.socket.on('error', handler.onSocketError)
handler.wasEverConnected = true
handler.onConnectionEstablished(response, extensions)
}
})
return controller
}
/**
* @see https://whatpr.org/websockets/48.html#close-the-websocket
* @param {import('./websocket').Handler} object
* @param {number} [code=null]
* @param {string} [reason='']
*/
function closeWebSocketConnection (object, code, reason, validate = false) {
// 1. If code was not supplied, let code be null.
code ??= null
// 2. If reason was not supplied, let reason be the empty string.
reason ??= ''
// 3. Validate close code and reason with code and reason.
if (validate) validateCloseCodeAndReason(code, reason)
// 4. Run the first matching steps from the following list:
// - If object’s ready state is CLOSING (2) or CLOSED (3)
// - If the WebSocket connection is not yet established [WSP]
// - If the WebSocket closing handshake has not yet been started [WSP]
// - Otherwise
if (isClosed(object.readyState) || isClosing(object.readyState)) {
// Do nothing.
} else if (!isEstablished(object.readyState)) {
// Fail the WebSocket connection and set object’s ready state to CLOSING (2). [WSP]
failWebsocketConnection(object)
object.readyState = states.CLOSING
} else if (!object.closeState.has(sentCloseFrameState.SENT) && !object.closeState.has(sentCloseFrameState.RECEIVED)) {
// Upon either sending or receiving a Close control frame, it is said
// that _The WebSocket Closing Handshake is Started_ and that the
// WebSocket connection is in the CLOSING state.
const frame = new WebsocketFrameSend()
// If neither code nor reason is present, the WebSocket Close
// message must not have a body.
// If code is present, then the status code to use in the
// WebSocket Close message must be the integer given by code.
// If code is null and reason is the empty string, the WebSocket Close frame must not have a body.
// If reason is non-empty but code is null, then set code to 1000 ("Normal Closure").
if (reason.length !== 0 && code === null) {
code = 1000
}
// If code is set, then the status code to use in the WebSocket Close frame must be the integer given by code.
assert(code === null || Number.isInteger(code))
if (code === null && reason.length === 0) {
frame.frameData = emptyBuffer
} else if (code !== null && reason === null) {
frame.frameData = Buffer.allocUnsafe(2)
frame.frameData.writeUInt16BE(code, 0)
} else if (code !== null && reason !== null) {
// If reason is also present, then reasonBytes must be
// provided in the Close message after the status code.
frame.frameData = Buffer.allocUnsafe(2 + Buffer.byteLength(reason))
frame.frameData.writeUInt16BE(code, 0)
// the body MAY contain UTF-8-encoded data with value /reason/
frame.frameData.write(reason, 2, 'utf-8')
} else {
frame.frameData = emptyBuffer
}
object.socket.write(frame.createFrame(opcodes.CLOSE))
object.closeState.add(sentCloseFrameState.SENT)
// Upon either sending or receiving a Close control frame, it is said
// that _The WebSocket Closing Handshake is Started_ and that the
// WebSocket connection is in the CLOSING state.
object.readyState = states.CLOSING
} else {
// Set object’s ready state to CLOSING (2).
object.readyState = states.CLOSING
}
}
/**
* @param {import('./websocket').Handler} handler
* @param {number} code
* @param {string|undefined} reason
* @param {unknown} cause
* @returns {void}
*/
function failWebsocketConnection (handler, code, reason, cause) {
// If _The WebSocket Connection is Established_ prior to the point where
// the endpoint is required to _Fail the WebSocket Connection_, the
// endpoint SHOULD send a Close frame with an appropriate status code
// (Section 7.4) before proceeding to _Close the WebSocket Connection_.
if (isEstablished(handler.readyState)) {
closeWebSocketConnection(handler, code, reason, false)
}
handler.controller.abort()
if (isConnecting(handler.readyState)) {
// If the connection was not established, we must still emit an 'error' and 'close' events
handler.onSocketClose()
} else if (handler.socket?.destroyed === false) {
handler.socket.destroy()
}
}
module.exports = {
establishWebSocketConnection,
failWebsocketConnection,
closeWebSocketConnection
}
================================================
FILE: lib/web/websocket/constants.js
================================================
'use strict'
/**
* This is a Globally Unique Identifier unique used to validate that the
* endpoint accepts websocket connections.
* @see https://www.rfc-editor.org/rfc/rfc6455.html#section-1.3
* @type {'258EAFA5-E914-47DA-95CA-C5AB0DC85B11'}
*/
const uid = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'
/**
* @type {PropertyDescriptor}
*/
const staticPropertyDescriptors = {
enumerable: true,
writable: false,
configurable: false
}
/**
* The states of the WebSocket connection.
*
* @readonly
* @enum
* @property {0} CONNECTING
* @property {1} OPEN
* @property {2} CLOSING
* @property {3} CLOSED
*/
const states = {
CONNECTING: 0,
OPEN: 1,
CLOSING: 2,
CLOSED: 3
}
/**
* @readonly
* @enum
* @property {0} NOT_SENT
* @property {1} PROCESSING
* @property {2} SENT
*/
const sentCloseFrameState = {
SENT: 1,
RECEIVED: 2
}
/**
* The WebSocket opcodes.
*
* @readonly
* @enum
* @property {0x0} CONTINUATION
* @property {0x1} TEXT
* @property {0x2} BINARY
* @property {0x8} CLOSE
* @property {0x9} PING
* @property {0xA} PONG
* @see https://datatracker.ietf.org/doc/html/rfc6455#section-5.2
*/
const opcodes = {
CONTINUATION: 0x0,
TEXT: 0x1,
BINARY: 0x2,
CLOSE: 0x8,
PING: 0x9,
PONG: 0xA
}
/**
* The maximum value for an unsigned 16-bit integer.
*
* @type {65535} 2 ** 16 - 1
*/
const maxUnsigned16Bit = 65535
/**
* The states of the parser.
*
* @readonly
* @enum
* @property {0} INFO
* @property {2} PAYLOADLENGTH_16
* @property {3} PAYLOADLENGTH_64
* @property {4} READ_DATA
*/
const parserStates = {
INFO: 0,
PAYLOADLENGTH_16: 2,
PAYLOADLENGTH_64: 3,
READ_DATA: 4
}
/**
* An empty buffer.
*
* @type {Buffer}
*/
const emptyBuffer = Buffer.allocUnsafe(0)
/**
* @readonly
* @property {1} text
* @property {2} typedArray
* @property {3} arrayBuffer
* @property {4} blob
*/
const sendHints = {
text: 1,
typedArray: 2,
arrayBuffer: 3,
blob: 4
}
module.exports = {
uid,
sentCloseFrameState,
staticPropertyDescriptors,
states,
opcodes,
maxUnsigned16Bit,
parserStates,
emptyBuffer,
sendHints
}
================================================
FILE: lib/web/websocket/events.js
================================================
'use strict'
const { webidl } = require('../webidl')
const { kEnumerableProperty } = require('../../core/util')
const { kConstruct } = require('../../core/symbols')
/**
* @see https://html.spec.whatwg.org/multipage/comms.html#messageevent
*/
class MessageEvent extends Event {
#eventInit
constructor (type, eventInitDict = {}) {
if (type === kConstruct) {
super(arguments[1], arguments[2])
webidl.util.markAsUncloneable(this)
return
}
const prefix = 'MessageEvent constructor'
webidl.argumentLengthCheck(arguments, 1, prefix)
type = webidl.converters.DOMString(type, prefix, 'type')
eventInitDict = webidl.converters.MessageEventInit(eventInitDict, prefix, 'eventInitDict')
super(type, eventInitDict)
this.#eventInit = eventInitDict
webidl.util.markAsUncloneable(this)
}
get data () {
webidl.brandCheck(this, MessageEvent)
return this.#eventInit.data
}
get origin () {
webidl.brandCheck(this, MessageEvent)
return this.#eventInit.origin
}
get lastEventId () {
webidl.brandCheck(this, MessageEvent)
return this.#eventInit.lastEventId
}
get source () {
webidl.brandCheck(this, MessageEvent)
return this.#eventInit.source
}
get ports () {
webidl.brandCheck(this, MessageEvent)
if (!Object.isFrozen(this.#eventInit.ports)) {
Object.freeze(this.#eventInit.ports)
}
return this.#eventInit.ports
}
initMessageEvent (
type,
bubbles = false,
cancelable = false,
data = null,
origin = '',
lastEventId = '',
source = null,
ports = []
) {
webidl.brandCheck(this, MessageEvent)
webidl.argumentLengthCheck(arguments, 1, 'MessageEvent.initMessageEvent')
return new MessageEvent(type, {
bubbles, cancelable, data, origin, lastEventId, source, ports
})
}
static createFastMessageEvent (type, init) {
const messageEvent = new MessageEvent(kConstruct, type, init)
messageEvent.#eventInit = init
messageEvent.#eventInit.data ??= null
messageEvent.#eventInit.origin ??= ''
messageEvent.#eventInit.lastEventId ??= ''
messageEvent.#eventInit.source ??= null
messageEvent.#eventInit.ports ??= []
return messageEvent
}
}
const { createFastMessageEvent } = MessageEvent
delete MessageEvent.createFastMessageEvent
/**
* @see https://websockets.spec.whatwg.org/#the-closeevent-interface
*/
class CloseEvent extends Event {
#eventInit
constructor (type, eventInitDict = {}) {
const prefix = 'CloseEvent constructor'
webidl.argumentLengthCheck(arguments, 1, prefix)
type = webidl.converters.DOMString(type, prefix, 'type')
eventInitDict = webidl.converters.CloseEventInit(eventInitDict)
super(type, eventInitDict)
this.#eventInit = eventInitDict
webidl.util.markAsUncloneable(this)
}
get wasClean () {
webidl.brandCheck(this, CloseEvent)
return this.#eventInit.wasClean
}
get code () {
webidl.brandCheck(this, CloseEvent)
return this.#eventInit.code
}
get reason () {
webidl.brandCheck(this, CloseEvent)
return this.#eventInit.reason
}
}
// https://html.spec.whatwg.org/multipage/webappapis.html#the-errorevent-interface
class ErrorEvent extends Event {
#eventInit
constructor (type, eventInitDict) {
const prefix = 'ErrorEvent constructor'
webidl.argumentLengthCheck(arguments, 1, prefix)
super(type, eventInitDict)
webidl.util.markAsUncloneable(this)
type = webidl.converters.DOMString(type, prefix, 'type')
eventInitDict = webidl.converters.ErrorEventInit(eventInitDict ?? {})
this.#eventInit = eventInitDict
}
get message () {
webidl.brandCheck(this, ErrorEvent)
return this.#eventInit.message
}
get filename () {
webidl.brandCheck(this, ErrorEvent)
return this.#eventInit.filename
}
get lineno () {
webidl.brandCheck(this, ErrorEvent)
return this.#eventInit.lineno
}
get colno () {
webidl.brandCheck(this, ErrorEvent)
return this.#eventInit.colno
}
get error () {
webidl.brandCheck(this, ErrorEvent)
return this.#eventInit.error
}
}
Object.defineProperties(MessageEvent.prototype, {
[Symbol.toStringTag]: {
value: 'MessageEvent',
configurable: true
},
data: kEnumerableProperty,
origin: kEnumerableProperty,
lastEventId: kEnumerableProperty,
source: kEnumerableProperty,
ports: kEnumerableProperty,
initMessageEvent: kEnumerableProperty
})
Object.defineProperties(CloseEvent.prototype, {
[Symbol.toStringTag]: {
value: 'CloseEvent',
configurable: true
},
reason: kEnumerableProperty,
code: kEnumerableProperty,
wasClean: kEnumerableProperty
})
Object.defineProperties(ErrorEvent.prototype, {
[Symbol.toStringTag]: {
value: 'ErrorEvent',
configurable: true
},
message: kEnumerableProperty,
filename: kEnumerableProperty,
lineno: kEnumerableProperty,
colno: kEnumerableProperty,
error: kEnumerableProperty
})
webidl.converters.MessagePort = webidl.interfaceConverter(
webidl.is.MessagePort,
'MessagePort'
)
webidl.converters['sequence'] = webidl.sequenceConverter(
webidl.converters.MessagePort
)
const eventInit = [
{
key: 'bubbles',
converter: webidl.converters.boolean,
defaultValue: () => false
},
{
key: 'cancelable',
converter: webidl.converters.boolean,
defaultValue: () => false
},
{
key: 'composed',
converter: webidl.converters.boolean,
defaultValue: () => false
}
]
webidl.converters.MessageEventInit = webidl.dictionaryConverter([
...eventInit,
{
key: 'data',
converter: webidl.converters.any,
defaultValue: () => null
},
{
key: 'origin',
converter: webidl.converters.USVString,
defaultValue: () => ''
},
{
key: 'lastEventId',
converter: webidl.converters.DOMString,
defaultValue: () => ''
},
{
key: 'source',
// Node doesn't implement WindowProxy or ServiceWorker, so the only
// valid value for source is a MessagePort.
converter: webidl.nullableConverter(webidl.converters.MessagePort),
defaultValue: () => null
},
{
key: 'ports',
converter: webidl.converters['sequence'],
defaultValue: () => []
}
])
webidl.converters.CloseEventInit = webidl.dictionaryConverter([
...eventInit,
{
key: 'wasClean',
converter: webidl.converters.boolean,
defaultValue: () => false
},
{
key: 'code',
converter: webidl.converters['unsigned short'],
defaultValue: () => 0
},
{
key: 'reason',
converter: webidl.converters.USVString,
defaultValue: () => ''
}
])
webidl.converters.ErrorEventInit = webidl.dictionaryConverter([
...eventInit,
{
key: 'message',
converter: webidl.converters.DOMString,
defaultValue: () => ''
},
{
key: 'filename',
converter: webidl.converters.USVString,
defaultValue: () => ''
},
{
key: 'lineno',
converter: webidl.converters['unsigned long'],
defaultValue: () => 0
},
{
key: 'colno',
converter: webidl.converters['unsigned long'],
defaultValue: () => 0
},
{
key: 'error',
converter: webidl.converters.any
}
])
module.exports = {
MessageEvent,
CloseEvent,
ErrorEvent,
createFastMessageEvent
}
================================================
FILE: lib/web/websocket/frame.js
================================================
'use strict'
const { runtimeFeatures } = require('../../util/runtime-features')
const { maxUnsigned16Bit, opcodes } = require('./constants')
const BUFFER_SIZE = 8 * 1024
let buffer = null
let bufIdx = BUFFER_SIZE
const randomFillSync = runtimeFeatures.has('crypto')
? require('node:crypto').randomFillSync
// not full compatibility, but minimum.
: function randomFillSync (buffer, _offset, _size) {
for (let i = 0; i < buffer.length; ++i) {
buffer[i] = Math.random() * 255 | 0
}
return buffer
}
function generateMask () {
if (bufIdx === BUFFER_SIZE) {
bufIdx = 0
randomFillSync((buffer ??= Buffer.allocUnsafeSlow(BUFFER_SIZE)), 0, BUFFER_SIZE)
}
return [buffer[bufIdx++], buffer[bufIdx++], buffer[bufIdx++], buffer[bufIdx++]]
}
class WebsocketFrameSend {
/**
* @param {Buffer|undefined} data
*/
constructor (data) {
this.frameData = data
}
createFrame (opcode) {
const frameData = this.frameData
const maskKey = generateMask()
const bodyLength = frameData?.byteLength ?? 0
/** @type {number} */
let payloadLength = bodyLength // 0-125
let offset = 6
if (bodyLength > maxUnsigned16Bit) {
offset += 8 // payload length is next 8 bytes
payloadLength = 127
} else if (bodyLength > 125) {
offset += 2 // payload length is next 2 bytes
payloadLength = 126
}
const buffer = Buffer.allocUnsafe(bodyLength + offset)
// Clear first 2 bytes, everything else is overwritten
buffer[0] = buffer[1] = 0
buffer[0] |= 0x80 // FIN
buffer[0] = (buffer[0] & 0xF0) + opcode // opcode
/*! ws. MIT License. Einar Otto Stangvik */
buffer[offset - 4] = maskKey[0]
buffer[offset - 3] = maskKey[1]
buffer[offset - 2] = maskKey[2]
buffer[offset - 1] = maskKey[3]
buffer[1] = payloadLength
if (payloadLength === 126) {
buffer.writeUInt16BE(bodyLength, 2)
} else if (payloadLength === 127) {
// Clear extended payload length
buffer[2] = buffer[3] = 0
buffer.writeUIntBE(bodyLength, 4, 6)
}
buffer[1] |= 0x80 // MASK
// mask body
for (let i = 0; i < bodyLength; ++i) {
buffer[offset + i] = frameData[i] ^ maskKey[i & 3]
}
return buffer
}
/**
* @param {Uint8Array} buffer
*/
static createFastTextFrame (buffer) {
const maskKey = generateMask()
const bodyLength = buffer.length
// mask body
for (let i = 0; i < bodyLength; ++i) {
buffer[i] ^= maskKey[i & 3]
}
let payloadLength = bodyLength
let offset = 6
if (bodyLength > maxUnsigned16Bit) {
offset += 8 // payload length is next 8 bytes
payloadLength = 127
} else if (bodyLength > 125) {
offset += 2 // payload length is next 2 bytes
payloadLength = 126
}
const head = Buffer.allocUnsafeSlow(offset)
head[0] = 0x80 /* FIN */ | opcodes.TEXT /* opcode TEXT */
head[1] = payloadLength | 0x80 /* MASK */
head[offset - 4] = maskKey[0]
head[offset - 3] = maskKey[1]
head[offset - 2] = maskKey[2]
head[offset - 1] = maskKey[3]
if (payloadLength === 126) {
head.writeUInt16BE(bodyLength, 2)
} else if (payloadLength === 127) {
head[2] = head[3] = 0
head.writeUIntBE(bodyLength, 4, 6)
}
return [head, buffer]
}
}
module.exports = {
WebsocketFrameSend,
generateMask // for benchmark
}
================================================
FILE: lib/web/websocket/permessage-deflate.js
================================================
'use strict'
const { createInflateRaw, Z_DEFAULT_WINDOWBITS } = require('node:zlib')
const { isValidClientWindowBits } = require('./util')
const { MessageSizeExceededError } = require('../../core/errors')
const tail = Buffer.from([0x00, 0x00, 0xff, 0xff])
const kBuffer = Symbol('kBuffer')
const kLength = Symbol('kLength')
// Default maximum decompressed message size: 4 MB
const kDefaultMaxDecompressedSize = 4 * 1024 * 1024
class PerMessageDeflate {
/** @type {import('node:zlib').InflateRaw} */
#inflate
#options = {}
/** @type {boolean} */
#aborted = false
/** @type {Function|null} */
#currentCallback = null
/**
* @param {Map} extensions
*/
constructor (extensions) {
this.#options.serverNoContextTakeover = extensions.has('server_no_context_takeover')
this.#options.serverMaxWindowBits = extensions.get('server_max_window_bits')
}
decompress (chunk, fin, callback) {
// An endpoint uses the following algorithm to decompress a message.
// 1. Append 4 octets of 0x00 0x00 0xff 0xff to the tail end of the
// payload of the message.
// 2. Decompress the resulting data using DEFLATE.
if (this.#aborted) {
callback(new MessageSizeExceededError())
return
}
if (!this.#inflate) {
let windowBits = Z_DEFAULT_WINDOWBITS
if (this.#options.serverMaxWindowBits) { // empty values default to Z_DEFAULT_WINDOWBITS
if (!isValidClientWindowBits(this.#options.serverMaxWindowBits)) {
callback(new Error('Invalid server_max_window_bits'))
return
}
windowBits = Number.parseInt(this.#options.serverMaxWindowBits)
}
try {
this.#inflate = createInflateRaw({ windowBits })
} catch (err) {
callback(err)
return
}
this.#inflate[kBuffer] = []
this.#inflate[kLength] = 0
this.#inflate.on('data', (data) => {
if (this.#aborted) {
return
}
this.#inflate[kLength] += data.length
if (this.#inflate[kLength] > kDefaultMaxDecompressedSize) {
this.#aborted = true
this.#inflate.removeAllListeners()
this.#inflate.destroy()
this.#inflate = null
if (this.#currentCallback) {
const cb = this.#currentCallback
this.#currentCallback = null
cb(new MessageSizeExceededError())
}
return
}
this.#inflate[kBuffer].push(data)
})
this.#inflate.on('error', (err) => {
this.#inflate = null
callback(err)
})
}
this.#currentCallback = callback
this.#inflate.write(chunk)
if (fin) {
this.#inflate.write(tail)
}
this.#inflate.flush(() => {
if (this.#aborted || !this.#inflate) {
return
}
const full = Buffer.concat(this.#inflate[kBuffer], this.#inflate[kLength])
this.#inflate[kBuffer].length = 0
this.#inflate[kLength] = 0
this.#currentCallback = null
callback(null, full)
})
}
}
module.exports = { PerMessageDeflate }
================================================
FILE: lib/web/websocket/receiver.js
================================================
'use strict'
const { Writable } = require('node:stream')
const assert = require('node:assert')
const { parserStates, opcodes, states, emptyBuffer, sentCloseFrameState } = require('./constants')
const {
isValidStatusCode,
isValidOpcode,
websocketMessageReceived,
utf8Decode,
isControlFrame,
isTextBinaryFrame,
isContinuationFrame
} = require('./util')
const { failWebsocketConnection } = require('./connection')
const { WebsocketFrameSend } = require('./frame')
const { PerMessageDeflate } = require('./permessage-deflate')
const { MessageSizeExceededError } = require('../../core/errors')
// This code was influenced by ws released under the MIT license.
// Copyright (c) 2011 Einar Otto Stangvik
// Copyright (c) 2013 Arnout Kazemier and contributors
// Copyright (c) 2016 Luigi Pinca and contributors
class ByteParser extends Writable {
#buffers = []
#fragmentsBytes = 0
#byteOffset = 0
#loop = false
#state = parserStates.INFO
#info = {}
#fragments = []
/** @type {Map} */
#extensions
/** @type {import('./websocket').Handler} */
#handler
/**
* @param {import('./websocket').Handler} handler
* @param {Map|null} extensions
*/
constructor (handler, extensions) {
super()
this.#handler = handler
this.#extensions = extensions == null ? new Map() : extensions
if (this.#extensions.has('permessage-deflate')) {
this.#extensions.set('permessage-deflate', new PerMessageDeflate(extensions))
}
}
/**
* @param {Buffer} chunk
* @param {() => void} callback
*/
_write (chunk, _, callback) {
this.#buffers.push(chunk)
this.#byteOffset += chunk.length
this.#loop = true
this.run(callback)
}
/**
* Runs whenever a new chunk is received.
* Callback is called whenever there are no more chunks buffering,
* or not enough bytes are buffered to parse.
*/
run (callback) {
while (this.#loop) {
if (this.#state === parserStates.INFO) {
// If there aren't enough bytes to parse the payload length, etc.
if (this.#byteOffset < 2) {
return callback()
}
const buffer = this.consume(2)
const fin = (buffer[0] & 0x80) !== 0
const opcode = buffer[0] & 0x0F
const masked = (buffer[1] & 0x80) === 0x80
const fragmented = !fin && opcode !== opcodes.CONTINUATION
const payloadLength = buffer[1] & 0x7F
const rsv1 = buffer[0] & 0x40
const rsv2 = buffer[0] & 0x20
const rsv3 = buffer[0] & 0x10
if (!isValidOpcode(opcode)) {
failWebsocketConnection(this.#handler, 1002, 'Invalid opcode received')
return callback()
}
if (masked) {
failWebsocketConnection(this.#handler, 1002, 'Frame cannot be masked')
return callback()
}
// MUST be 0 unless an extension is negotiated that defines meanings
// for non-zero values. If a nonzero value is received and none of
// the negotiated extensions defines the meaning of such a nonzero
// value, the receiving endpoint MUST _Fail the WebSocket
// Connection_.
// This document allocates the RSV1 bit of the WebSocket header for
// PMCEs and calls the bit the "Per-Message Compressed" bit. On a
// WebSocket connection where a PMCE is in use, this bit indicates
// whether a message is compressed or not.
if (rsv1 !== 0 && !this.#extensions.has('permessage-deflate')) {
failWebsocketConnection(this.#handler, 1002, 'Expected RSV1 to be clear.')
return
}
if (rsv2 !== 0 || rsv3 !== 0) {
failWebsocketConnection(this.#handler, 1002, 'RSV1, RSV2, RSV3 must be clear')
return
}
if (fragmented && !isTextBinaryFrame(opcode)) {
// Only text and binary frames can be fragmented
failWebsocketConnection(this.#handler, 1002, 'Invalid frame type was fragmented.')
return
}
// If we are already parsing a text/binary frame and do not receive either
// a continuation frame or close frame, fail the connection.
if (isTextBinaryFrame(opcode) && this.#fragments.length > 0) {
failWebsocketConnection(this.#handler, 1002, 'Expected continuation frame')
return
}
if (this.#info.fragmented && fragmented) {
// A fragmented frame can't be fragmented itself
failWebsocketConnection(this.#handler, 1002, 'Fragmented frame exceeded 125 bytes.')
return
}
// "All control frames MUST have a payload length of 125 bytes or less
// and MUST NOT be fragmented."
if ((payloadLength > 125 || fragmented) && isControlFrame(opcode)) {
failWebsocketConnection(this.#handler, 1002, 'Control frame either too large or fragmented')
return
}
if (isContinuationFrame(opcode) && this.#fragments.length === 0 && !this.#info.compressed) {
failWebsocketConnection(this.#handler, 1002, 'Unexpected continuation frame')
return
}
if (payloadLength <= 125) {
this.#info.payloadLength = payloadLength
this.#state = parserStates.READ_DATA
} else if (payloadLength === 126) {
this.#state = parserStates.PAYLOADLENGTH_16
} else if (payloadLength === 127) {
this.#state = parserStates.PAYLOADLENGTH_64
}
if (isTextBinaryFrame(opcode)) {
this.#info.binaryType = opcode
this.#info.compressed = rsv1 !== 0
}
this.#info.opcode = opcode
this.#info.masked = masked
this.#info.fin = fin
this.#info.fragmented = fragmented
} else if (this.#state === parserStates.PAYLOADLENGTH_16) {
if (this.#byteOffset < 2) {
return callback()
}
const buffer = this.consume(2)
this.#info.payloadLength = buffer.readUInt16BE(0)
this.#state = parserStates.READ_DATA
} else if (this.#state === parserStates.PAYLOADLENGTH_64) {
if (this.#byteOffset < 8) {
return callback()
}
const buffer = this.consume(8)
const upper = buffer.readUInt32BE(0)
const lower = buffer.readUInt32BE(4)
// 2^31 is the maximum bytes an arraybuffer can contain
// on 32-bit systems. Although, on 64-bit systems, this is
// 2^53-1 bytes.
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Errors/Invalid_array_length
// https://source.chromium.org/chromium/chromium/src/+/main:v8/src/common/globals.h;drc=1946212ac0100668f14eb9e2843bdd846e510a1e;bpv=1;bpt=1;l=1275
// https://source.chromium.org/chromium/chromium/src/+/main:v8/src/objects/js-array-buffer.h;l=34;drc=1946212ac0100668f14eb9e2843bdd846e510a1e
if (upper !== 0 || lower > 2 ** 31 - 1) {
failWebsocketConnection(this.#handler, 1009, 'Received payload length > 2^31 bytes.')
return
}
this.#info.payloadLength = lower
this.#state = parserStates.READ_DATA
} else if (this.#state === parserStates.READ_DATA) {
if (this.#byteOffset < this.#info.payloadLength) {
return callback()
}
const body = this.consume(this.#info.payloadLength)
if (isControlFrame(this.#info.opcode)) {
this.#loop = this.parseControlFrame(body)
this.#state = parserStates.INFO
} else {
if (!this.#info.compressed) {
this.writeFragments(body)
// If the frame is not fragmented, a message has been received.
// If the frame is fragmented, it will terminate with a fin bit set
// and an opcode of 0 (continuation), therefore we handle that when
// parsing continuation frames, not here.
if (!this.#info.fragmented && this.#info.fin) {
websocketMessageReceived(this.#handler, this.#info.binaryType, this.consumeFragments())
}
this.#state = parserStates.INFO
} else {
this.#extensions.get('permessage-deflate').decompress(body, this.#info.fin, (error, data) => {
if (error) {
// Use 1009 (Message Too Big) for decompression size limit errors
const code = error instanceof MessageSizeExceededError ? 1009 : 1007
failWebsocketConnection(this.#handler, code, error.message)
return
}
this.writeFragments(data)
if (!this.#info.fin) {
this.#state = parserStates.INFO
this.#loop = true
this.run(callback)
return
}
websocketMessageReceived(this.#handler, this.#info.binaryType, this.consumeFragments())
this.#loop = true
this.#state = parserStates.INFO
this.run(callback)
})
this.#loop = false
break
}
}
}
}
}
/**
* Take n bytes from the buffered Buffers
* @param {number} n
* @returns {Buffer}
*/
consume (n) {
if (n > this.#byteOffset) {
throw new Error('Called consume() before buffers satiated.')
} else if (n === 0) {
return emptyBuffer
}
this.#byteOffset -= n
const first = this.#buffers[0]
if (first.length > n) {
// replace with remaining buffer
this.#buffers[0] = first.subarray(n, first.length)
return first.subarray(0, n)
} else if (first.length === n) {
// prefect match
return this.#buffers.shift()
} else {
let offset = 0
// If Buffer.allocUnsafe is used, extra copies will be made because the offset is non-zero.
const buffer = Buffer.allocUnsafeSlow(n)
while (offset !== n) {
const next = this.#buffers[0]
const length = next.length
if (length + offset === n) {
buffer.set(this.#buffers.shift(), offset)
break
} else if (length + offset > n) {
buffer.set(next.subarray(0, n - offset), offset)
this.#buffers[0] = next.subarray(n - offset)
break
} else {
buffer.set(this.#buffers.shift(), offset)
offset += length
}
}
return buffer
}
}
writeFragments (fragment) {
this.#fragmentsBytes += fragment.length
this.#fragments.push(fragment)
}
consumeFragments () {
const fragments = this.#fragments
if (fragments.length === 1) {
// single fragment
this.#fragmentsBytes = 0
return fragments.shift()
}
let offset = 0
// If Buffer.allocUnsafe is used, extra copies will be made because the offset is non-zero.
const output = Buffer.allocUnsafeSlow(this.#fragmentsBytes)
for (let i = 0; i < fragments.length; ++i) {
const buffer = fragments[i]
output.set(buffer, offset)
offset += buffer.length
}
this.#fragments = []
this.#fragmentsBytes = 0
return output
}
parseCloseBody (data) {
assert(data.length !== 1)
// https://datatracker.ietf.org/doc/html/rfc6455#section-7.1.5
/** @type {number|undefined} */
let code
if (data.length >= 2) {
// _The WebSocket Connection Close Code_ is
// defined as the status code (Section 7.4) contained in the first Close
// control frame received by the application
code = data.readUInt16BE(0)
}
if (code !== undefined && !isValidStatusCode(code)) {
return { code: 1002, reason: 'Invalid status code', error: true }
}
// https://datatracker.ietf.org/doc/html/rfc6455#section-7.1.6
/** @type {Buffer} */
let reason = data.subarray(2)
// Remove BOM
if (reason[0] === 0xEF && reason[1] === 0xBB && reason[2] === 0xBF) {
reason = reason.subarray(3)
}
try {
reason = utf8Decode(reason)
} catch {
return { code: 1007, reason: 'Invalid UTF-8', error: true }
}
return { code, reason, error: false }
}
/**
* Parses control frames.
* @param {Buffer} body
*/
parseControlFrame (body) {
const { opcode, payloadLength } = this.#info
if (opcode === opcodes.CLOSE) {
if (payloadLength === 1) {
failWebsocketConnection(this.#handler, 1002, 'Received close frame with a 1-byte body.')
return false
}
this.#info.closeInfo = this.parseCloseBody(body)
if (this.#info.closeInfo.error) {
const { code, reason } = this.#info.closeInfo
failWebsocketConnection(this.#handler, code, reason)
return false
}
// Upon receiving such a frame, the other peer sends a
// Close frame in response, if it hasn't already sent one.
if (!this.#handler.closeState.has(sentCloseFrameState.SENT) && !this.#handler.closeState.has(sentCloseFrameState.RECEIVED)) {
// If an endpoint receives a Close frame and did not previously send a
// Close frame, the endpoint MUST send a Close frame in response. (When
// sending a Close frame in response, the endpoint typically echos the
// status code it received.)
let body = emptyBuffer
if (this.#info.closeInfo.code) {
body = Buffer.allocUnsafe(2)
body.writeUInt16BE(this.#info.closeInfo.code, 0)
}
const closeFrame = new WebsocketFrameSend(body)
this.#handler.socket.write(closeFrame.createFrame(opcodes.CLOSE))
this.#handler.closeState.add(sentCloseFrameState.SENT)
}
// Upon either sending or receiving a Close control frame, it is said
// that _The WebSocket Closing Handshake is Started_ and that the
// WebSocket connection is in the CLOSING state.
this.#handler.readyState = states.CLOSING
this.#handler.closeState.add(sentCloseFrameState.RECEIVED)
return false
} else if (opcode === opcodes.PING) {
// Upon receipt of a Ping frame, an endpoint MUST send a Pong frame in
// response, unless it already received a Close frame.
// A Pong frame sent in response to a Ping frame must have identical
// "Application data"
if (!this.#handler.closeState.has(sentCloseFrameState.RECEIVED)) {
const frame = new WebsocketFrameSend(body)
this.#handler.socket.write(frame.createFrame(opcodes.PONG))
this.#handler.onPing(body)
}
} else if (opcode === opcodes.PONG) {
// A Pong frame MAY be sent unsolicited. This serves as a
// unidirectional heartbeat. A response to an unsolicited Pong frame is
// not expected.
this.#handler.onPong(body)
}
return true
}
get closingInfo () {
return this.#info.closeInfo
}
}
module.exports = {
ByteParser
}
================================================
FILE: lib/web/websocket/sender.js
================================================
'use strict'
const { WebsocketFrameSend } = require('./frame')
const { opcodes, sendHints } = require('./constants')
const FixedQueue = require('../../dispatcher/fixed-queue')
/**
* @typedef {object} SendQueueNode
* @property {Promise | null} promise
* @property {((...args: any[]) => any)} callback
* @property {Buffer | null} frame
*/
class SendQueue {
/**
* @type {FixedQueue}
*/
#queue = new FixedQueue()
/**
* @type {boolean}
*/
#running = false
/** @type {import('node:net').Socket} */
#socket
constructor (socket) {
this.#socket = socket
}
add (item, cb, hint) {
if (hint !== sendHints.blob) {
if (!this.#running) {
// TODO(@tsctx): support fast-path for string on running
if (hint === sendHints.text) {
// special fast-path for string
const { 0: head, 1: body } = WebsocketFrameSend.createFastTextFrame(item)
this.#socket.cork()
this.#socket.write(head)
this.#socket.write(body, cb)
this.#socket.uncork()
} else {
// direct writing
this.#socket.write(createFrame(item, hint), cb)
}
} else {
/** @type {SendQueueNode} */
const node = {
promise: null,
callback: cb,
frame: createFrame(item, hint)
}
this.#queue.push(node)
}
return
}
/** @type {SendQueueNode} */
const node = {
promise: item.arrayBuffer().then((ab) => {
node.promise = null
node.frame = createFrame(ab, hint)
}),
callback: cb,
frame: null
}
this.#queue.push(node)
if (!this.#running) {
this.#run()
}
}
async #run () {
this.#running = true
const queue = this.#queue
while (!queue.isEmpty()) {
const node = queue.shift()
// wait pending promise
if (node.promise !== null) {
await node.promise
}
// write
this.#socket.write(node.frame, node.callback)
// cleanup
node.callback = node.frame = null
}
this.#running = false
}
}
function createFrame (data, hint) {
return new WebsocketFrameSend(toBuffer(data, hint)).createFrame(hint === sendHints.text ? opcodes.TEXT : opcodes.BINARY)
}
function toBuffer (data, hint) {
switch (hint) {
case sendHints.text:
case sendHints.typedArray:
return new Uint8Array(data.buffer, data.byteOffset, data.byteLength)
case sendHints.arrayBuffer:
case sendHints.blob:
return new Uint8Array(data)
}
}
module.exports = { SendQueue }
================================================
FILE: lib/web/websocket/stream/websocketerror.js
================================================
'use strict'
const { webidl } = require('../../webidl')
const { validateCloseCodeAndReason } = require('../util')
const { kConstruct } = require('../../../core/symbols')
const { kEnumerableProperty } = require('../../../core/util')
function createInheritableDOMException () {
// https://github.com/nodejs/node/issues/59677
class Test extends DOMException {
get reason () {
return ''
}
}
if (new Test().reason !== undefined) {
return DOMException
}
return new Proxy(DOMException, {
construct (target, args, newTarget) {
const instance = Reflect.construct(target, args, target)
Object.setPrototypeOf(instance, newTarget.prototype)
return instance
}
})
}
class WebSocketError extends createInheritableDOMException() {
#closeCode
#reason
constructor (message = '', init = undefined) {
message = webidl.converters.DOMString(message, 'WebSocketError', 'message')
// 1. Set this 's name to " WebSocketError ".
// 2. Set this 's message to message .
super(message, 'WebSocketError')
if (init === kConstruct) {
return
} else if (init !== null) {
init = webidl.converters.WebSocketCloseInfo(init)
}
// 3. Let code be init [" closeCode "] if it exists , or null otherwise.
let code = init.closeCode ?? null
// 4. Let reason be init [" reason "] if it exists , or the empty string otherwise.
const reason = init.reason ?? ''
// 5. Validate close code and reason with code and reason .
validateCloseCodeAndReason(code, reason)
// 6. If reason is non-empty, but code is not set, then set code to 1000 ("Normal Closure").
if (reason.length !== 0 && code === null) {
code = 1000
}
// 7. Set this 's closeCode to code .
this.#closeCode = code
// 8. Set this 's reason to reason .
this.#reason = reason
}
get closeCode () {
return this.#closeCode
}
get reason () {
return this.#reason
}
/**
* @param {string} message
* @param {number|null} code
* @param {string} reason
*/
static createUnvalidatedWebSocketError (message, code, reason) {
const error = new WebSocketError(message, kConstruct)
error.#closeCode = code
error.#reason = reason
return error
}
}
const { createUnvalidatedWebSocketError } = WebSocketError
delete WebSocketError.createUnvalidatedWebSocketError
Object.defineProperties(WebSocketError.prototype, {
closeCode: kEnumerableProperty,
reason: kEnumerableProperty,
[Symbol.toStringTag]: {
value: 'WebSocketError',
writable: false,
enumerable: false,
configurable: true
}
})
webidl.is.WebSocketError = webidl.util.MakeTypeAssertion(WebSocketError)
module.exports = { WebSocketError, createUnvalidatedWebSocketError }
================================================
FILE: lib/web/websocket/stream/websocketstream.js
================================================
'use strict'
const { createDeferredPromise } = require('../../../util/promise')
const { environmentSettingsObject } = require('../../fetch/util')
const { states, opcodes, sentCloseFrameState } = require('../constants')
const { webidl } = require('../../webidl')
const { getURLRecord, isValidSubprotocol, isEstablished, utf8Decode } = require('../util')
const { establishWebSocketConnection, failWebsocketConnection, closeWebSocketConnection } = require('../connection')
const { channels } = require('../../../core/diagnostics')
const { WebsocketFrameSend } = require('../frame')
const { ByteParser } = require('../receiver')
const { WebSocketError, createUnvalidatedWebSocketError } = require('./websocketerror')
const { kEnumerableProperty } = require('../../../core/util')
const { utf8DecodeBytes } = require('../../../encoding')
let emittedExperimentalWarning = false
class WebSocketStream {
// Each WebSocketStream object has an associated url , which is a URL record .
/** @type {URL} */
#url
// Each WebSocketStream object has an associated opened promise , which is a promise.
/** @type {import('../../../util/promise').DeferredPromise} */
#openedPromise
// Each WebSocketStream object has an associated closed promise , which is a promise.
/** @type {import('../../../util/promise').DeferredPromise} */
#closedPromise
// Each WebSocketStream object has an associated readable stream , which is a ReadableStream .
/** @type {ReadableStream} */
#readableStream
/** @type {ReadableStreamDefaultController} */
#readableStreamController
// Each WebSocketStream object has an associated writable stream , which is a WritableStream .
/** @type {WritableStream} */
#writableStream
// Each WebSocketStream object has an associated boolean handshake aborted , which is initially false.
#handshakeAborted = false
/** @type {import('../websocket').Handler} */
#handler = {
// https://whatpr.org/websockets/48/7b748d3...d5570f3.html#feedback-to-websocket-stream-from-the-protocol
onConnectionEstablished: (response, extensions) => this.#onConnectionEstablished(response, extensions),
onMessage: (opcode, data) => this.#onMessage(opcode, data),
onParserError: (err) => failWebsocketConnection(this.#handler, null, err.message),
onParserDrain: () => this.#handler.socket.resume(),
onSocketData: (chunk) => {
if (!this.#parser.write(chunk)) {
this.#handler.socket.pause()
}
},
onSocketError: (err) => {
this.#handler.readyState = states.CLOSING
if (channels.socketError.hasSubscribers) {
channels.socketError.publish(err)
}
this.#handler.socket.destroy()
},
onSocketClose: () => this.#onSocketClose(),
onPing: () => {},
onPong: () => {},
readyState: states.CONNECTING,
socket: null,
closeState: new Set(),
controller: null,
wasEverConnected: false
}
/** @type {import('../receiver').ByteParser} */
#parser
constructor (url, options = undefined) {
if (!emittedExperimentalWarning) {
process.emitWarning('WebSocketStream is experimental! Expect it to change at any time.', {
code: 'UNDICI-WSS'
})
emittedExperimentalWarning = true
}
webidl.argumentLengthCheck(arguments, 1, 'WebSocket')
url = webidl.converters.USVString(url)
if (options !== null) {
options = webidl.converters.WebSocketStreamOptions(options)
}
// 1. Let baseURL be this 's relevant settings object 's API base URL .
const baseURL = environmentSettingsObject.settingsObject.baseUrl
// 2. Let urlRecord be the result of getting a URL record given url and baseURL .
const urlRecord = getURLRecord(url, baseURL)
// 3. Let protocols be options [" protocols "] if it exists , otherwise an empty sequence.
const protocols = options.protocols
// 4. If any of the values in protocols occur more than once or otherwise fail to match the requirements for elements that comprise the value of ` Sec-WebSocket-Protocol ` fields as defined by The WebSocket Protocol , then throw a " SyntaxError " DOMException . [WSP]
if (protocols.length !== new Set(protocols.map(p => p.toLowerCase())).size) {
throw new DOMException('Invalid Sec-WebSocket-Protocol value', 'SyntaxError')
}
if (protocols.length > 0 && !protocols.every(p => isValidSubprotocol(p))) {
throw new DOMException('Invalid Sec-WebSocket-Protocol value', 'SyntaxError')
}
// 5. Set this 's url to urlRecord .
this.#url = urlRecord.toString()
// 6. Set this 's opened promise and closed promise to new promises.
this.#openedPromise = createDeferredPromise()
this.#closedPromise = createDeferredPromise()
// 7. Apply backpressure to the WebSocket.
// TODO
// 8. If options [" signal "] exists ,
if (options.signal != null) {
// 8.1. Let signal be options [" signal "].
const signal = options.signal
// 8.2. If signal is aborted , then reject this 's opened promise and closed promise with signal ’s abort reason
// and return.
if (signal.aborted) {
this.#openedPromise.reject(signal.reason)
this.#closedPromise.reject(signal.reason)
return
}
// 8.3. Add the following abort steps to signal :
signal.addEventListener('abort', () => {
// 8.3.1. If the WebSocket connection is not yet established : [WSP]
if (!isEstablished(this.#handler.readyState)) {
// 8.3.1.1. Fail the WebSocket connection .
failWebsocketConnection(this.#handler)
// Set this 's ready state to CLOSING .
this.#handler.readyState = states.CLOSING
// Reject this 's opened promise and closed promise with signal ’s abort reason .
this.#openedPromise.reject(signal.reason)
this.#closedPromise.reject(signal.reason)
// Set this 's handshake aborted to true.
this.#handshakeAborted = true
}
}, { once: true })
}
// 9. Let client be this 's relevant settings object .
const client = environmentSettingsObject.settingsObject
// 10. Run this step in parallel :
// 10.1. Establish a WebSocket connection given urlRecord , protocols , and client . [FETCH]
this.#handler.controller = establishWebSocketConnection(
urlRecord,
protocols,
client,
this.#handler,
options
)
}
// The url getter steps are to return this 's url , serialized .
get url () {
return this.#url.toString()
}
// The opened getter steps are to return this 's opened promise .
get opened () {
return this.#openedPromise.promise
}
// The closed getter steps are to return this 's closed promise .
get closed () {
return this.#closedPromise.promise
}
// The close( closeInfo ) method steps are:
close (closeInfo = undefined) {
if (closeInfo !== null) {
closeInfo = webidl.converters.WebSocketCloseInfo(closeInfo)
}
// 1. Let code be closeInfo [" closeCode "] if present, or null otherwise.
const code = closeInfo.closeCode ?? null
// 2. Let reason be closeInfo [" reason "].
const reason = closeInfo.reason
// 3. Close the WebSocket with this , code , and reason .
closeWebSocketConnection(this.#handler, code, reason, true)
}
#write (chunk) {
// See /websockets/stream/tentative/write.any.html
chunk = webidl.converters.WebSocketStreamWrite(chunk)
// 1. Let promise be a new promise created in stream ’s relevant realm .
const promise = createDeferredPromise()
// 2. Let data be null.
let data = null
// 3. Let opcode be null.
let opcode = null
// 4. If chunk is a BufferSource ,
if (webidl.is.BufferSource(chunk)) {
// 4.1. Set data to a copy of the bytes given chunk .
data = new Uint8Array(ArrayBuffer.isView(chunk) ? new Uint8Array(chunk.buffer, chunk.byteOffset, chunk.byteLength) : chunk.slice())
// 4.2. Set opcode to a binary frame opcode.
opcode = opcodes.BINARY
} else {
// 5. Otherwise,
// 5.1. Let string be the result of converting chunk to an IDL USVString .
// If this throws an exception, return a promise rejected with the exception.
let string
try {
string = webidl.converters.DOMString(chunk)
} catch (e) {
promise.reject(e)
return promise.promise
}
// 5.2. Set data to the result of UTF-8 encoding string .
data = new TextEncoder().encode(string)
// 5.3. Set opcode to a text frame opcode.
opcode = opcodes.TEXT
}
// 6. In parallel,
// 6.1. Wait until there is sufficient buffer space in stream to send the message.
// 6.2. If the closing handshake has not yet started , Send a WebSocket Message to stream comprised of data using opcode .
if (!this.#handler.closeState.has(sentCloseFrameState.SENT) && !this.#handler.closeState.has(sentCloseFrameState.RECEIVED)) {
const frame = new WebsocketFrameSend(data)
this.#handler.socket.write(frame.createFrame(opcode), () => {
promise.resolve(undefined)
})
}
// 6.3. Queue a global task on the WebSocket task source given stream ’s relevant global object to resolve promise with undefined.
return promise.promise
}
/** @type {import('../websocket').Handler['onConnectionEstablished']} */
#onConnectionEstablished (response, parsedExtensions) {
this.#handler.socket = response.socket
const parser = new ByteParser(this.#handler, parsedExtensions)
parser.on('drain', () => this.#handler.onParserDrain())
parser.on('error', (err) => this.#handler.onParserError(err))
this.#parser = parser
// 1. Change stream ’s ready state to OPEN (1).
this.#handler.readyState = states.OPEN
// 2. Set stream ’s was ever connected to true.
// This is done in the opening handshake.
// 3. Let extensions be the extensions in use .
const extensions = parsedExtensions ?? ''
// 4. Let protocol be the subprotocol in use .
const protocol = response.headersList.get('sec-websocket-protocol') ?? ''
// 5. Let pullAlgorithm be an action that pulls bytes from stream .
// 6. Let cancelAlgorithm be an action that cancels stream with reason , given reason .
// 7. Let readable be a new ReadableStream .
// 8. Set up readable with pullAlgorithm and cancelAlgorithm .
const readable = new ReadableStream({
start: (controller) => {
this.#readableStreamController = controller
},
pull (controller) {
let chunk
while (controller.desiredSize > 0 && (chunk = response.socket.read()) !== null) {
controller.enqueue(chunk)
}
},
cancel: (reason) => this.#cancel(reason)
})
// 9. Let writeAlgorithm be an action that writes chunk to stream , given chunk .
// 10. Let closeAlgorithm be an action that closes stream .
// 11. Let abortAlgorithm be an action that aborts stream with reason , given reason .
// 12. Let writable be a new WritableStream .
// 13. Set up writable with writeAlgorithm , closeAlgorithm , and abortAlgorithm .
const writable = new WritableStream({
write: (chunk) => this.#write(chunk),
close: () => closeWebSocketConnection(this.#handler, null, null),
abort: (reason) => this.#closeUsingReason(reason)
})
// Set stream ’s readable stream to readable .
this.#readableStream = readable
// Set stream ’s writable stream to writable .
this.#writableStream = writable
// Resolve stream ’s opened promise with WebSocketOpenInfo «[ " extensions " → extensions , " protocol " → protocol , " readable " → readable , " writable " → writable ]».
this.#openedPromise.resolve({
extensions,
protocol,
readable,
writable
})
}
/** @type {import('../websocket').Handler['onMessage']} */
#onMessage (type, data) {
// 1. If stream’s ready state is not OPEN (1), then return.
if (this.#handler.readyState !== states.OPEN) {
return
}
// 2. Let chunk be determined by switching on type:
// - type indicates that the data is Text
// a new DOMString containing data
// - type indicates that the data is Binary
// a new Uint8Array object, created in the relevant Realm of the
// WebSocketStream object, whose contents are data
let chunk
if (type === opcodes.TEXT) {
try {
chunk = utf8Decode(data)
} catch {
failWebsocketConnection(this.#handler, 'Received invalid UTF-8 in text frame.')
return
}
} else if (type === opcodes.BINARY) {
chunk = new Uint8Array(data.buffer, data.byteOffset, data.byteLength)
}
// 3. Enqueue chunk into stream’s readable stream.
this.#readableStreamController.enqueue(chunk)
// 4. Apply backpressure to the WebSocket.
}
/** @type {import('../websocket').Handler['onSocketClose']} */
#onSocketClose () {
const wasClean =
this.#handler.closeState.has(sentCloseFrameState.SENT) &&
this.#handler.closeState.has(sentCloseFrameState.RECEIVED)
// 1. Change the ready state to CLOSED (3).
this.#handler.readyState = states.CLOSED
// 2. If stream ’s handshake aborted is true, then return.
if (this.#handshakeAborted) {
return
}
// 3. If stream ’s was ever connected is false, then reject stream ’s opened promise with a new WebSocketError.
if (!this.#handler.wasEverConnected) {
this.#openedPromise.reject(new WebSocketError('Socket never opened'))
}
const result = this.#parser?.closingInfo
// 4. Let code be the WebSocket connection close code .
// https://datatracker.ietf.org/doc/html/rfc6455#section-7.1.5
// If this Close control frame contains no status code, _The WebSocket
// Connection Close Code_ is considered to be 1005. If _The WebSocket
// Connection is Closed_ and no Close control frame was received by the
// endpoint (such as could occur if the underlying transport connection
// is lost), _The WebSocket Connection Close Code_ is considered to be
// 1006.
let code = result?.code ?? 1005
if (!this.#handler.closeState.has(sentCloseFrameState.SENT) && !this.#handler.closeState.has(sentCloseFrameState.RECEIVED)) {
code = 1006
}
// 5. Let reason be the result of applying UTF-8 decode without BOM to the WebSocket connection close reason .
const reason = result?.reason == null ? '' : utf8DecodeBytes(Buffer.from(result.reason))
// 6. If the connection was closed cleanly ,
if (wasClean) {
// 6.1. Close stream ’s readable stream .
this.#readableStreamController.close()
// 6.2. Error stream ’s writable stream with an " InvalidStateError " DOMException indicating that a closed WebSocketStream cannot be written to.
if (!this.#writableStream.locked) {
this.#writableStream.abort(new DOMException('A closed WebSocketStream cannot be written to', 'InvalidStateError'))
}
// 6.3. Resolve stream ’s closed promise with WebSocketCloseInfo «[ " closeCode " → code , " reason " → reason ]».
this.#closedPromise.resolve({
closeCode: code,
reason
})
} else {
// 7. Otherwise,
// 7.1. Let error be a new WebSocketError whose closeCode is code and reason is reason .
const error = createUnvalidatedWebSocketError('unclean close', code, reason)
// 7.2. Error stream ’s readable stream with error .
this.#readableStreamController?.error(error)
// 7.3. Error stream ’s writable stream with error .
this.#writableStream?.abort(error)
// 7.4. Reject stream ’s closed promise with error .
this.#closedPromise.reject(error)
}
}
#closeUsingReason (reason) {
// 1. Let code be null.
let code = null
// 2. Let reasonString be the empty string.
let reasonString = ''
// 3. If reason implements WebSocketError ,
if (webidl.is.WebSocketError(reason)) {
// 3.1. Set code to reason ’s closeCode .
code = reason.closeCode
// 3.2. Set reasonString to reason ’s reason .
reasonString = reason.reason
}
// 4. Close the WebSocket with stream , code , and reasonString . If this throws an exception,
// discard code and reasonString and close the WebSocket with stream .
closeWebSocketConnection(this.#handler, code, reasonString)
}
// To cancel a WebSocketStream stream given reason , close using reason giving stream and reason .
#cancel (reason) {
this.#closeUsingReason(reason)
}
}
Object.defineProperties(WebSocketStream.prototype, {
url: kEnumerableProperty,
opened: kEnumerableProperty,
closed: kEnumerableProperty,
close: kEnumerableProperty,
[Symbol.toStringTag]: {
value: 'WebSocketStream',
writable: false,
enumerable: false,
configurable: true
}
})
webidl.converters.WebSocketStreamOptions = webidl.dictionaryConverter([
{
key: 'protocols',
converter: webidl.sequenceConverter(webidl.converters.USVString),
defaultValue: () => []
},
{
key: 'signal',
converter: webidl.nullableConverter(webidl.converters.AbortSignal),
defaultValue: () => null
}
])
webidl.converters.WebSocketCloseInfo = webidl.dictionaryConverter([
{
key: 'closeCode',
converter: (V) => webidl.converters['unsigned short'](V, webidl.attributes.EnforceRange)
},
{
key: 'reason',
converter: webidl.converters.USVString,
defaultValue: () => ''
}
])
webidl.converters.WebSocketStreamWrite = function (V) {
if (typeof V === 'string') {
return webidl.converters.USVString(V)
}
return webidl.converters.BufferSource(V)
}
module.exports = { WebSocketStream }
================================================
FILE: lib/web/websocket/util.js
================================================
'use strict'
const { states, opcodes } = require('./constants')
const { isUtf8 } = require('node:buffer')
const { removeHTTPWhitespace } = require('../fetch/data-url')
const { collectASequenceOfCodePointsFast } = require('../infra')
/**
* @param {number} readyState
* @returns {boolean}
*/
function isConnecting (readyState) {
// If the WebSocket connection is not yet established, and the connection
// is not yet closed, then the WebSocket connection is in the CONNECTING state.
return readyState === states.CONNECTING
}
/**
* @param {number} readyState
* @returns {boolean}
*/
function isEstablished (readyState) {
// If the server's response is validated as provided for above, it is
// said that _The WebSocket Connection is Established_ and that the
// WebSocket Connection is in the OPEN state.
return readyState === states.OPEN
}
/**
* @param {number} readyState
* @returns {boolean}
*/
function isClosing (readyState) {
// Upon either sending or receiving a Close control frame, it is said
// that _The WebSocket Closing Handshake is Started_ and that the
// WebSocket connection is in the CLOSING state.
return readyState === states.CLOSING
}
/**
* @param {number} readyState
* @returns {boolean}
*/
function isClosed (readyState) {
return readyState === states.CLOSED
}
/**
* @see https://dom.spec.whatwg.org/#concept-event-fire
* @param {string} e
* @param {EventTarget} target
* @param {(...args: ConstructorParameters) => Event} eventFactory
* @param {EventInit | undefined} eventInitDict
* @returns {void}
*/
function fireEvent (e, target, eventFactory = (type, init) => new Event(type, init), eventInitDict = {}) {
// 1. If eventConstructor is not given, then let eventConstructor be Event.
// 2. Let event be the result of creating an event given eventConstructor,
// in the relevant realm of target.
// 3. Initialize event’s type attribute to e.
const event = eventFactory(e, eventInitDict)
// 4. Initialize any other IDL attributes of event as described in the
// invocation of this algorithm.
// 5. Return the result of dispatching event at target, with legacy target
// override flag set if set.
target.dispatchEvent(event)
}
/**
* @see https://websockets.spec.whatwg.org/#feedback-from-the-protocol
* @param {import('./websocket').Handler} handler
* @param {number} type Opcode
* @param {Buffer} data application data
* @returns {void}
*/
function websocketMessageReceived (handler, type, data) {
handler.onMessage(type, data)
}
/**
* @param {Buffer} buffer
* @returns {ArrayBuffer}
*/
function toArrayBuffer (buffer) {
if (buffer.byteLength === buffer.buffer.byteLength) {
return buffer.buffer
}
return new Uint8Array(buffer).buffer
}
/**
* @see https://datatracker.ietf.org/doc/html/rfc6455
* @see https://datatracker.ietf.org/doc/html/rfc2616
* @see https://bugs.chromium.org/p/chromium/issues/detail?id=398407
* @param {string} protocol
* @returns {boolean}
*/
function isValidSubprotocol (protocol) {
// If present, this value indicates one
// or more comma-separated subprotocol the client wishes to speak,
// ordered by preference. The elements that comprise this value
// MUST be non-empty strings with characters in the range U+0021 to
// U+007E not including separator characters as defined in
// [RFC2616] and MUST all be unique strings.
if (protocol.length === 0) {
return false
}
for (let i = 0; i < protocol.length; ++i) {
const code = protocol.charCodeAt(i)
if (
code < 0x21 || // CTL, contains SP (0x20) and HT (0x09)
code > 0x7E ||
code === 0x22 || // "
code === 0x28 || // (
code === 0x29 || // )
code === 0x2C || // ,
code === 0x2F || // /
code === 0x3A || // :
code === 0x3B || // ;
code === 0x3C || // <
code === 0x3D || // =
code === 0x3E || // >
code === 0x3F || // ?
code === 0x40 || // @
code === 0x5B || // [
code === 0x5C || // \
code === 0x5D || // ]
code === 0x7B || // {
code === 0x7D // }
) {
return false
}
}
return true
}
/**
* @see https://datatracker.ietf.org/doc/html/rfc6455#section-7-4
* @param {number} code
* @returns {boolean}
*/
function isValidStatusCode (code) {
if (code >= 1000 && code < 1015) {
return (
code !== 1004 && // reserved
code !== 1005 && // "MUST NOT be set as a status code"
code !== 1006 // "MUST NOT be set as a status code"
)
}
return code >= 3000 && code <= 4999
}
/**
* @see https://datatracker.ietf.org/doc/html/rfc6455#section-5.5
* @param {number} opcode
* @returns {boolean}
*/
function isControlFrame (opcode) {
return (
opcode === opcodes.CLOSE ||
opcode === opcodes.PING ||
opcode === opcodes.PONG
)
}
/**
* @param {number} opcode
* @returns {boolean}
*/
function isContinuationFrame (opcode) {
return opcode === opcodes.CONTINUATION
}
/**
* @param {number} opcode
* @returns {boolean}
*/
function isTextBinaryFrame (opcode) {
return opcode === opcodes.TEXT || opcode === opcodes.BINARY
}
/**
*
* @param {number} opcode
* @returns {boolean}
*/
function isValidOpcode (opcode) {
return isTextBinaryFrame(opcode) || isContinuationFrame(opcode) || isControlFrame(opcode)
}
/**
* Parses a Sec-WebSocket-Extensions header value.
* @param {string} extensions
* @returns {Map}
*/
// TODO(@Uzlopak, @KhafraDev): make compliant https://datatracker.ietf.org/doc/html/rfc6455#section-9.1
function parseExtensions (extensions) {
const position = { position: 0 }
const extensionList = new Map()
while (position.position < extensions.length) {
const pair = collectASequenceOfCodePointsFast(';', extensions, position)
const [name, value = ''] = pair.split('=', 2)
extensionList.set(
removeHTTPWhitespace(name, true, false),
removeHTTPWhitespace(value, false, true)
)
position.position++
}
return extensionList
}
/**
* @see https://www.rfc-editor.org/rfc/rfc7692#section-7.1.2.2
* @description "client-max-window-bits = 1*DIGIT"
* @param {string} value
* @returns {boolean}
*/
function isValidClientWindowBits (value) {
// Must have at least one character
if (value.length === 0) {
return false
}
// Check all characters are ASCII digits
for (let i = 0; i < value.length; i++) {
const byte = value.charCodeAt(i)
if (byte < 0x30 || byte > 0x39) {
return false
}
}
// Check numeric range: zlib requires windowBits in range 8-15
const num = Number.parseInt(value, 10)
return num >= 8 && num <= 15
}
/**
* @see https://whatpr.org/websockets/48/7b748d3...d5570f3.html#get-a-url-record
* @param {string} url
* @param {string} [baseURL]
*/
function getURLRecord (url, baseURL) {
// 1. Let urlRecord be the result of applying the URL parser to url with baseURL .
// 2. If urlRecord is failure, then throw a " SyntaxError " DOMException .
let urlRecord
try {
urlRecord = new URL(url, baseURL)
} catch (e) {
throw new DOMException(e, 'SyntaxError')
}
// 3. If urlRecord ’s scheme is " http ", then set urlRecord ’s scheme to " ws ".
// 4. Otherwise, if urlRecord ’s scheme is " https ", set urlRecord ’s scheme to " wss ".
if (urlRecord.protocol === 'http:') {
urlRecord.protocol = 'ws:'
} else if (urlRecord.protocol === 'https:') {
urlRecord.protocol = 'wss:'
}
// 5. If urlRecord ’s scheme is not " ws " or " wss ", then throw a " SyntaxError " DOMException .
if (urlRecord.protocol !== 'ws:' && urlRecord.protocol !== 'wss:') {
throw new DOMException('expected a ws: or wss: url', 'SyntaxError')
}
// If urlRecord ’s fragment is non-null, then throw a " SyntaxError " DOMException .
if (urlRecord.hash.length || urlRecord.href.endsWith('#')) {
throw new DOMException('hash', 'SyntaxError')
}
// Return urlRecord .
return urlRecord
}
// https://whatpr.org/websockets/48.html#validate-close-code-and-reason
function validateCloseCodeAndReason (code, reason) {
// 1. If code is not null, but is neither an integer equal to
// 1000 nor an integer in the range 3000 to 4999, inclusive,
// throw an "InvalidAccessError" DOMException.
if (code !== null) {
if (code !== 1000 && (code < 3000 || code > 4999)) {
throw new DOMException('invalid code', 'InvalidAccessError')
}
}
// 2. If reason is not null, then:
if (reason !== null) {
// 2.1. Let reasonBytes be the result of UTF-8 encoding reason.
// 2.2. If reasonBytes is longer than 123 bytes, then throw a
// "SyntaxError" DOMException.
const reasonBytesLength = Buffer.byteLength(reason)
if (reasonBytesLength > 123) {
throw new DOMException(`Reason must be less than 123 bytes; received ${reasonBytesLength}`, 'SyntaxError')
}
}
}
/**
* Converts a Buffer to utf-8, even on platforms without icu.
* @type {(buffer: Buffer) => string}
*/
const utf8Decode = (() => {
if (typeof process.versions.icu === 'string') {
const fatalDecoder = new TextDecoder('utf-8', { fatal: true })
return fatalDecoder.decode.bind(fatalDecoder)
}
return function (buffer) {
if (isUtf8(buffer)) {
return buffer.toString('utf-8')
}
throw new TypeError('Invalid utf-8 received.')
}
})()
module.exports = {
isConnecting,
isEstablished,
isClosing,
isClosed,
fireEvent,
isValidSubprotocol,
isValidStatusCode,
websocketMessageReceived,
utf8Decode,
isControlFrame,
isContinuationFrame,
isTextBinaryFrame,
isValidOpcode,
parseExtensions,
isValidClientWindowBits,
toArrayBuffer,
getURLRecord,
validateCloseCodeAndReason
}
================================================
FILE: lib/web/websocket/websocket.js
================================================
'use strict'
const { isArrayBuffer } = require('node:util/types')
const { webidl } = require('../webidl')
const { URLSerializer } = require('../fetch/data-url')
const { environmentSettingsObject } = require('../fetch/util')
const { staticPropertyDescriptors, states, sentCloseFrameState, sendHints, opcodes } = require('./constants')
const {
isConnecting,
isEstablished,
isClosing,
isClosed,
isValidSubprotocol,
fireEvent,
utf8Decode,
toArrayBuffer,
getURLRecord
} = require('./util')
const { establishWebSocketConnection, closeWebSocketConnection, failWebsocketConnection } = require('./connection')
const { ByteParser } = require('./receiver')
const { kEnumerableProperty } = require('../../core/util')
const { getGlobalDispatcher } = require('../../global')
const { ErrorEvent, CloseEvent, createFastMessageEvent } = require('./events')
const { SendQueue } = require('./sender')
const { WebsocketFrameSend } = require('./frame')
const { channels } = require('../../core/diagnostics')
/**
* @typedef {object} Handler
* @property {(response: any, extensions?: string[]) => void} onConnectionEstablished
* @property {(opcode: number, data: Buffer) => void} onMessage
* @property {(error: Error) => void} onParserError
* @property {() => void} onParserDrain
* @property {(chunk: Buffer) => void} onSocketData
* @property {(err: Error) => void} onSocketError
* @property {() => void} onSocketClose
* @property {(body: Buffer) => void} onPing
* @property {(body: Buffer) => void} onPong
*
* @property {number} readyState
* @property {import('stream').Duplex} socket
* @property {Set} closeState
* @property {import('../fetch/index').Fetch} controller
* @property {boolean} [wasEverConnected=false]
*/
// https://websockets.spec.whatwg.org/#interface-definition
class WebSocket extends EventTarget {
#events = {
open: null,
error: null,
close: null,
message: null
}
#bufferedAmount = 0
#protocol = ''
#extensions = ''
/** @type {SendQueue} */
#sendQueue
/** @type {Handler} */
#handler = {
onConnectionEstablished: (response, extensions) => this.#onConnectionEstablished(response, extensions),
onMessage: (opcode, data) => this.#onMessage(opcode, data),
onParserError: (err) => failWebsocketConnection(this.#handler, null, err.message),
onParserDrain: () => this.#onParserDrain(),
onSocketData: (chunk) => {
if (!this.#parser.write(chunk)) {
this.#handler.socket.pause()
}
},
onSocketError: (err) => {
this.#handler.readyState = states.CLOSING
if (channels.socketError.hasSubscribers) {
channels.socketError.publish(err)
}
this.#handler.socket.destroy()
},
onSocketClose: () => this.#onSocketClose(),
onPing: (body) => {
if (channels.ping.hasSubscribers) {
channels.ping.publish({
payload: body,
websocket: this
})
}
},
onPong: (body) => {
if (channels.pong.hasSubscribers) {
channels.pong.publish({
payload: body,
websocket: this
})
}
},
readyState: states.CONNECTING,
socket: null,
closeState: new Set(),
controller: null,
wasEverConnected: false
}
#url
#binaryType
/** @type {import('./receiver').ByteParser} */
#parser
/**
* @param {string} url
* @param {string|string[]} protocols
*/
constructor (url, protocols = []) {
super()
webidl.util.markAsUncloneable(this)
const prefix = 'WebSocket constructor'
webidl.argumentLengthCheck(arguments, 1, prefix)
const options = webidl.converters['DOMString or sequence or WebSocketInit'](protocols, prefix, 'options')
url = webidl.converters.USVString(url)
protocols = options.protocols
// 1. Let baseURL be this's relevant settings object's API base URL.
const baseURL = environmentSettingsObject.settingsObject.baseUrl
// 2. Let urlRecord be the result of getting a URL record given url and baseURL.
const urlRecord = getURLRecord(url, baseURL)
// 3. If protocols is a string, set protocols to a sequence consisting
// of just that string.
if (typeof protocols === 'string') {
protocols = [protocols]
}
// 4. If any of the values in protocols occur more than once or otherwise
// fail to match the requirements for elements that comprise the value
// of `Sec-WebSocket-Protocol` fields as defined by The WebSocket
// protocol, then throw a "SyntaxError" DOMException.
if (protocols.length !== new Set(protocols.map(p => p.toLowerCase())).size) {
throw new DOMException('Invalid Sec-WebSocket-Protocol value', 'SyntaxError')
}
if (protocols.length > 0 && !protocols.every(p => isValidSubprotocol(p))) {
throw new DOMException('Invalid Sec-WebSocket-Protocol value', 'SyntaxError')
}
// 5. Set this's url to urlRecord.
this.#url = new URL(urlRecord.href)
// 6. Let client be this's relevant settings object.
const client = environmentSettingsObject.settingsObject
// 7. Run this step in parallel:
// 7.1. Establish a WebSocket connection given urlRecord, protocols,
// and client.
this.#handler.controller = establishWebSocketConnection(
urlRecord,
protocols,
client,
this.#handler,
options
)
// Each WebSocket object has an associated ready state, which is a
// number representing the state of the connection. Initially it must
// be CONNECTING (0).
this.#handler.readyState = WebSocket.CONNECTING
// The extensions attribute must initially return the empty string.
// The protocol attribute must initially return the empty string.
// Each WebSocket object has an associated binary type, which is a
// BinaryType. Initially it must be "blob".
this.#binaryType = 'blob'
}
/**
* @see https://websockets.spec.whatwg.org/#dom-websocket-close
* @param {number|undefined} code
* @param {string|undefined} reason
*/
close (code = undefined, reason = undefined) {
webidl.brandCheck(this, WebSocket)
const prefix = 'WebSocket.close'
if (code !== undefined) {
code = webidl.converters['unsigned short'](code, prefix, 'code', webidl.attributes.Clamp)
}
if (reason !== undefined) {
reason = webidl.converters.USVString(reason)
}
// 1. If code is the special value "missing", then set code to null.
code ??= null
// 2. If reason is the special value "missing", then set reason to the empty string.
reason ??= ''
// 3. Close the WebSocket with this, code, and reason.
closeWebSocketConnection(this.#handler, code, reason, true)
}
/**
* @see https://websockets.spec.whatwg.org/#dom-websocket-send
* @param {NodeJS.TypedArray|ArrayBuffer|Blob|string} data
*/
send (data) {
webidl.brandCheck(this, WebSocket)
const prefix = 'WebSocket.send'
webidl.argumentLengthCheck(arguments, 1, prefix)
data = webidl.converters.WebSocketSendData(data, prefix, 'data')
// 1. If this's ready state is CONNECTING, then throw an
// "InvalidStateError" DOMException.
if (isConnecting(this.#handler.readyState)) {
throw new DOMException('Sent before connected.', 'InvalidStateError')
}
// 2. Run the appropriate set of steps from the following list:
// https://datatracker.ietf.org/doc/html/rfc6455#section-6.1
// https://datatracker.ietf.org/doc/html/rfc6455#section-5.2
if (!isEstablished(this.#handler.readyState) || isClosing(this.#handler.readyState)) {
return
}
// If data is a string
if (typeof data === 'string') {
// If the WebSocket connection is established and the WebSocket
// closing handshake has not yet started, then the user agent
// must send a WebSocket Message comprised of the data argument
// using a text frame opcode; if the data cannot be sent, e.g.
// because it would need to be buffered but the buffer is full,
// the user agent must flag the WebSocket as full and then close
// the WebSocket connection. Any invocation of this method with a
// string argument that does not throw an exception must increase
// the bufferedAmount attribute by the number of bytes needed to
// express the argument as UTF-8.
const buffer = Buffer.from(data)
this.#bufferedAmount += buffer.byteLength
this.#sendQueue.add(buffer, () => {
this.#bufferedAmount -= buffer.byteLength
}, sendHints.text)
} else if (isArrayBuffer(data)) {
// If the WebSocket connection is established, and the WebSocket
// closing handshake has not yet started, then the user agent must
// send a WebSocket Message comprised of data using a binary frame
// opcode; if the data cannot be sent, e.g. because it would need
// to be buffered but the buffer is full, the user agent must flag
// the WebSocket as full and then close the WebSocket connection.
// The data to be sent is the data stored in the buffer described
// by the ArrayBuffer object. Any invocation of this method with an
// ArrayBuffer argument that does not throw an exception must
// increase the bufferedAmount attribute by the length of the
// ArrayBuffer in bytes.
this.#bufferedAmount += data.byteLength
this.#sendQueue.add(data, () => {
this.#bufferedAmount -= data.byteLength
}, sendHints.arrayBuffer)
} else if (ArrayBuffer.isView(data)) {
// If the WebSocket connection is established, and the WebSocket
// closing handshake has not yet started, then the user agent must
// send a WebSocket Message comprised of data using a binary frame
// opcode; if the data cannot be sent, e.g. because it would need to
// be buffered but the buffer is full, the user agent must flag the
// WebSocket as full and then close the WebSocket connection. The
// data to be sent is the data stored in the section of the buffer
// described by the ArrayBuffer object that data references. Any
// invocation of this method with this kind of argument that does
// not throw an exception must increase the bufferedAmount attribute
// by the length of data’s buffer in bytes.
this.#bufferedAmount += data.byteLength
this.#sendQueue.add(data, () => {
this.#bufferedAmount -= data.byteLength
}, sendHints.typedArray)
} else if (webidl.is.Blob(data)) {
// If the WebSocket connection is established, and the WebSocket
// closing handshake has not yet started, then the user agent must
// send a WebSocket Message comprised of data using a binary frame
// opcode; if the data cannot be sent, e.g. because it would need to
// be buffered but the buffer is full, the user agent must flag the
// WebSocket as full and then close the WebSocket connection. The data
// to be sent is the raw data represented by the Blob object. Any
// invocation of this method with a Blob argument that does not throw
// an exception must increase the bufferedAmount attribute by the size
// of the Blob object’s raw data, in bytes.
this.#bufferedAmount += data.size
this.#sendQueue.add(data, () => {
this.#bufferedAmount -= data.size
}, sendHints.blob)
}
}
get readyState () {
webidl.brandCheck(this, WebSocket)
// The readyState getter steps are to return this's ready state.
return this.#handler.readyState
}
get bufferedAmount () {
webidl.brandCheck(this, WebSocket)
return this.#bufferedAmount
}
get url () {
webidl.brandCheck(this, WebSocket)
// The url getter steps are to return this's url, serialized.
return URLSerializer(this.#url)
}
get extensions () {
webidl.brandCheck(this, WebSocket)
return this.#extensions
}
get protocol () {
webidl.brandCheck(this, WebSocket)
return this.#protocol
}
get onopen () {
webidl.brandCheck(this, WebSocket)
return this.#events.open
}
set onopen (fn) {
webidl.brandCheck(this, WebSocket)
if (this.#events.open) {
this.removeEventListener('open', this.#events.open)
}
const listener = webidl.converters.EventHandlerNonNull(fn)
if (listener !== null) {
this.addEventListener('open', listener)
this.#events.open = fn
} else {
this.#events.open = null
}
}
get onerror () {
webidl.brandCheck(this, WebSocket)
return this.#events.error
}
set onerror (fn) {
webidl.brandCheck(this, WebSocket)
if (this.#events.error) {
this.removeEventListener('error', this.#events.error)
}
const listener = webidl.converters.EventHandlerNonNull(fn)
if (listener !== null) {
this.addEventListener('error', listener)
this.#events.error = fn
} else {
this.#events.error = null
}
}
get onclose () {
webidl.brandCheck(this, WebSocket)
return this.#events.close
}
set onclose (fn) {
webidl.brandCheck(this, WebSocket)
if (this.#events.close) {
this.removeEventListener('close', this.#events.close)
}
const listener = webidl.converters.EventHandlerNonNull(fn)
if (listener !== null) {
this.addEventListener('close', listener)
this.#events.close = fn
} else {
this.#events.close = null
}
}
get onmessage () {
webidl.brandCheck(this, WebSocket)
return this.#events.message
}
set onmessage (fn) {
webidl.brandCheck(this, WebSocket)
if (this.#events.message) {
this.removeEventListener('message', this.#events.message)
}
const listener = webidl.converters.EventHandlerNonNull(fn)
if (listener !== null) {
this.addEventListener('message', listener)
this.#events.message = fn
} else {
this.#events.message = null
}
}
get binaryType () {
webidl.brandCheck(this, WebSocket)
return this.#binaryType
}
set binaryType (type) {
webidl.brandCheck(this, WebSocket)
if (type !== 'blob' && type !== 'arraybuffer') {
this.#binaryType = 'blob'
} else {
this.#binaryType = type
}
}
/**
* @see https://websockets.spec.whatwg.org/#feedback-from-the-protocol
*/
#onConnectionEstablished (response, parsedExtensions) {
// processResponse is called when the "response's header list has been received and initialized."
// once this happens, the connection is open
this.#handler.socket = response.socket
const parser = new ByteParser(this.#handler, parsedExtensions)
parser.on('drain', () => this.#handler.onParserDrain())
parser.on('error', (err) => this.#handler.onParserError(err))
this.#parser = parser
this.#sendQueue = new SendQueue(response.socket)
// 1. Change the ready state to OPEN (1).
this.#handler.readyState = states.OPEN
// 2. Change the extensions attribute’s value to the extensions in use, if
// it is not the null value.
// https://datatracker.ietf.org/doc/html/rfc6455#section-9.1
const extensions = response.headersList.get('sec-websocket-extensions')
if (extensions !== null) {
this.#extensions = extensions
}
// 3. Change the protocol attribute’s value to the subprotocol in use, if
// it is not the null value.
// https://datatracker.ietf.org/doc/html/rfc6455#section-1.9
const protocol = response.headersList.get('sec-websocket-protocol')
if (protocol !== null) {
this.#protocol = protocol
}
// 4. Fire an event named open at the WebSocket object.
fireEvent('open', this)
if (channels.open.hasSubscribers) {
// Convert headers to a plain object for the event
const headers = response.headersList.entries
channels.open.publish({
address: response.socket.address(),
protocol: this.#protocol,
extensions: this.#extensions,
websocket: this,
handshakeResponse: {
status: response.status,
statusText: response.statusText,
headers
}
})
}
}
#onMessage (type, data) {
// 1. If ready state is not OPEN (1), then return.
if (this.#handler.readyState !== states.OPEN) {
return
}
// 2. Let dataForEvent be determined by switching on type and binary type:
let dataForEvent
if (type === opcodes.TEXT) {
// -> type indicates that the data is Text
// a new DOMString containing data
try {
dataForEvent = utf8Decode(data)
} catch {
failWebsocketConnection(this.#handler, 1007, 'Received invalid UTF-8 in text frame.')
return
}
} else if (type === opcodes.BINARY) {
if (this.#binaryType === 'blob') {
// -> type indicates that the data is Binary and binary type is "blob"
// a new Blob object, created in the relevant Realm of the WebSocket
// object, that represents data as its raw data
dataForEvent = new Blob([data])
} else {
// -> type indicates that the data is Binary and binary type is "arraybuffer"
// a new ArrayBuffer object, created in the relevant Realm of the
// WebSocket object, whose contents are data
dataForEvent = toArrayBuffer(data)
}
}
// 3. Fire an event named message at the WebSocket object, using MessageEvent,
// with the origin attribute initialized to the serialization of the WebSocket
// object’s url's origin, and the data attribute initialized to dataForEvent.
fireEvent('message', this, createFastMessageEvent, {
origin: this.#url.origin,
data: dataForEvent
})
}
#onParserDrain () {
this.#handler.socket.resume()
}
/**
* @see https://websockets.spec.whatwg.org/#feedback-from-the-protocol
* @see https://datatracker.ietf.org/doc/html/rfc6455#section-7.1.4
*/
#onSocketClose () {
// If the TCP connection was closed after the
// WebSocket closing handshake was completed, the WebSocket connection
// is said to have been closed _cleanly_.
const wasClean =
this.#handler.closeState.has(sentCloseFrameState.SENT) &&
this.#handler.closeState.has(sentCloseFrameState.RECEIVED)
let code = 1005
let reason = ''
const result = this.#parser?.closingInfo
if (result && !result.error) {
code = result.code ?? 1005
reason = result.reason
}
// 1. Change the ready state to CLOSED (3).
this.#handler.readyState = states.CLOSED
// 2. If the user agent was required to fail the WebSocket
// connection, or if the WebSocket connection was closed
// after being flagged as full, fire an event named error
// at the WebSocket object.
if (!this.#handler.closeState.has(sentCloseFrameState.RECEIVED)) {
// If _The WebSocket
// Connection is Closed_ and no Close control frame was received by the
// endpoint (such as could occur if the underlying transport connection
// is lost), _The WebSocket Connection Close Code_ is considered to be
// 1006.
code = 1006
fireEvent('error', this, (type, init) => new ErrorEvent(type, init), {
error: new TypeError(reason)
})
}
// 3. Fire an event named close at the WebSocket object,
// using CloseEvent, with the wasClean attribute
// initialized to true if the connection closed cleanly
// and false otherwise, the code attribute initialized to
// the WebSocket connection close code, and the reason
// attribute initialized to the result of applying UTF-8
// decode without BOM to the WebSocket connection close
// reason.
// TODO: process.nextTick
fireEvent('close', this, (type, init) => new CloseEvent(type, init), {
wasClean, code, reason
})
if (channels.close.hasSubscribers) {
channels.close.publish({
websocket: this,
code,
reason
})
}
}
/**
* @param {WebSocket} ws
* @param {Buffer|undefined} buffer
*/
static ping (ws, buffer) {
if (Buffer.isBuffer(buffer)) {
if (buffer.length > 125) {
throw new TypeError('A PING frame cannot have a body larger than 125 bytes.')
}
} else if (buffer !== undefined) {
throw new TypeError('Expected buffer payload')
}
// An endpoint MAY send a Ping frame any time after the connection is
// established and before the connection is closed.
const readyState = ws.#handler.readyState
if (isEstablished(readyState) && !isClosing(readyState) && !isClosed(readyState)) {
const frame = new WebsocketFrameSend(buffer)
ws.#handler.socket.write(frame.createFrame(opcodes.PING))
}
}
}
const { ping } = WebSocket
Reflect.deleteProperty(WebSocket, 'ping')
// https://websockets.spec.whatwg.org/#dom-websocket-connecting
WebSocket.CONNECTING = WebSocket.prototype.CONNECTING = states.CONNECTING
// https://websockets.spec.whatwg.org/#dom-websocket-open
WebSocket.OPEN = WebSocket.prototype.OPEN = states.OPEN
// https://websockets.spec.whatwg.org/#dom-websocket-closing
WebSocket.CLOSING = WebSocket.prototype.CLOSING = states.CLOSING
// https://websockets.spec.whatwg.org/#dom-websocket-closed
WebSocket.CLOSED = WebSocket.prototype.CLOSED = states.CLOSED
Object.defineProperties(WebSocket.prototype, {
CONNECTING: staticPropertyDescriptors,
OPEN: staticPropertyDescriptors,
CLOSING: staticPropertyDescriptors,
CLOSED: staticPropertyDescriptors,
url: kEnumerableProperty,
readyState: kEnumerableProperty,
bufferedAmount: kEnumerableProperty,
onopen: kEnumerableProperty,
onerror: kEnumerableProperty,
onclose: kEnumerableProperty,
close: kEnumerableProperty,
onmessage: kEnumerableProperty,
binaryType: kEnumerableProperty,
send: kEnumerableProperty,
extensions: kEnumerableProperty,
protocol: kEnumerableProperty,
[Symbol.toStringTag]: {
value: 'WebSocket',
writable: false,
enumerable: false,
configurable: true
}
})
Object.defineProperties(WebSocket, {
CONNECTING: staticPropertyDescriptors,
OPEN: staticPropertyDescriptors,
CLOSING: staticPropertyDescriptors,
CLOSED: staticPropertyDescriptors
})
webidl.converters['sequence'] = webidl.sequenceConverter(
webidl.converters.DOMString
)
webidl.converters['DOMString or sequence'] = function (V, prefix, argument) {
if (webidl.util.Type(V) === webidl.util.Types.OBJECT && Symbol.iterator in V) {
return webidl.converters['sequence'](V)
}
return webidl.converters.DOMString(V, prefix, argument)
}
// This implements the proposal made in https://github.com/whatwg/websockets/issues/42
webidl.converters.WebSocketInit = webidl.dictionaryConverter([
{
key: 'protocols',
converter: webidl.converters['DOMString or sequence'],
defaultValue: () => []
},
{
key: 'dispatcher',
converter: webidl.converters.any,
defaultValue: () => getGlobalDispatcher()
},
{
key: 'headers',
converter: webidl.nullableConverter(webidl.converters.HeadersInit)
}
])
webidl.converters['DOMString or sequence or WebSocketInit'] = function (V) {
if (webidl.util.Type(V) === webidl.util.Types.OBJECT && !(Symbol.iterator in V)) {
return webidl.converters.WebSocketInit(V)
}
return { protocols: webidl.converters['DOMString or sequence'](V) }
}
webidl.converters.WebSocketSendData = function (V) {
if (webidl.util.Type(V) === webidl.util.Types.OBJECT) {
if (webidl.is.Blob(V)) {
return V
}
if (webidl.is.BufferSource(V)) {
return V
}
}
return webidl.converters.USVString(V)
}
module.exports = {
WebSocket,
ping
}
================================================
FILE: package.json
================================================
{
"name": "undici",
"version": "7.24.5",
"description": "An HTTP/1.1 client, written from scratch for Node.js",
"homepage": "https://undici.nodejs.org",
"bugs": {
"url": "https://github.com/nodejs/undici/issues"
},
"repository": {
"type": "git",
"url": "git+https://github.com/nodejs/undici.git"
},
"license": "MIT",
"contributors": [
{
"name": "Daniele Belardi",
"url": "https://github.com/dnlup",
"author": true
},
{
"name": "Ethan Arrowood",
"url": "https://github.com/ethan-arrowood",
"author": true
},
{
"name": "Matteo Collina",
"url": "https://github.com/mcollina",
"author": true
},
{
"name": "Matthew Aitken",
"url": "https://github.com/KhafraDev",
"author": true
},
{
"name": "Robert Nagy",
"url": "https://github.com/ronag",
"author": true
},
{
"name": "Szymon Marczak",
"url": "https://github.com/szmarczak",
"author": true
},
{
"name": "Tomas Della Vedova",
"url": "https://github.com/delvedor",
"author": true
}
],
"keywords": [
"fetch",
"http",
"https",
"promise",
"request",
"curl",
"wget",
"xhr",
"whatwg"
],
"main": "index.js",
"types": "index.d.ts",
"scripts": {
"build:node": "esbuild index-fetch.js --bundle --platform=node --outfile=undici-fetch.js --define:esbuildDetection=1 --keep-names && node scripts/strip-comments.js",
"build:wasm": "node build/wasm.js --docker",
"generate-pem": "node scripts/generate-pem.js",
"lint": "eslint --cache",
"lint:fix": "eslint --fix --cache",
"test": "npm run test:javascript && cross-env NODE_V8_COVERAGE= npm run test:typescript",
"test:javascript": "npm run test:javascript:no-jest && npm run test:jest",
"test:javascript:no-jest": "npm run generate-pem && npm run test:unit && npm run test:fetch && npm run test:node-fetch && npm run test:infra && npm run test:cache && npm run test:cache-interceptor && npm run test:interceptors && npm run test:cookies && npm run test:eventsource && npm run test:subresource-integrity && npm run test:wpt && npm run test:websocket && npm run test:node-test && npm run test:cache-tests",
"test:javascript:without-intl": "npm run test:javascript:no-jest",
"test:busboy": "borp --timeout 180000 -p \"test/busboy/*.js\"",
"test:cache": "borp --timeout 180000 -p \"test/cache/*.js\"",
"test:cache-interceptor": "borp --timeout 180000 -p \"test/cache-interceptor/*.js\"",
"test:cache-interceptor:sqlite": "cross-env NODE_OPTIONS=--experimental-sqlite npm run test:cache-interceptor",
"test:cookies": "borp --timeout 180000 -p \"test/cookie/*.js\"",
"test:eventsource": "npm run build:node && borp --timeout 180000 --expose-gc -p \"test/eventsource/*.js\"",
"test:fuzzing": "node test/fuzzing/fuzzing.test.js",
"test:fetch": "npm run build:node && borp --timeout 180000 --expose-gc --concurrency 1 -p \"test/fetch/*.js\" && npm run test:webidl && npm run test:busboy",
"test:subresource-integrity": "borp --timeout 180000 -p \"test/subresource-integrity/*.js\"",
"test:h2": "npm run test:h2:core && npm run test:h2:fetch",
"test:h2:core": "borp --timeout 180000 -p \"test/+(http2|h2)*.js\"",
"test:h2:fetch": "npm run build:node && borp --timeout 180000 -p \"test/fetch/http2*.js\"",
"test:infra": "borp --timeout 180000 -p \"test/infra/*.js\"",
"test:interceptors": "borp --timeout 180000 -p \"test/interceptors/*.js\"",
"test:jest": "cross-env NODE_V8_COVERAGE= jest",
"test:unit": "borp --timeout 180000 --expose-gc -p \"test/*.js\"",
"test:node-fetch": "borp --timeout 180000 -p \"test/node-fetch/**/*.js\"",
"test:node-test": "borp --timeout 180000 -p \"test/node-test/**/*.js\"",
"test:tdd": "borp --timeout 180000 --expose-gc -p \"test/*.js\"",
"test:tdd:node-test": "borp --timeout 180000 -p \"test/node-test/**/*.js\" -w",
"test:typescript": "tsd && tsc test/imports/undici-import.ts --typeRoots ./types --noEmit && tsc ./types/*.d.ts --noEmit --typeRoots ./types",
"test:webidl": "borp --timeout 180000 -p \"test/webidl/*.js\"",
"test:websocket": "borp --timeout 180000 -p \"test/websocket/**/*.js\"",
"test:websocket:autobahn": "node test/autobahn/client.js",
"test:websocket:autobahn:report": "node test/autobahn/report.js",
"test:wpt:setup": "node test/web-platform-tests/wpt-runner.mjs setup",
"test:wpt": "npm run test:wpt:setup && node test/web-platform-tests/wpt-runner.mjs run /fetch /mimesniff /xhr /websockets /serviceWorkers /eventsource",
"test:cache-tests": "node test/cache-interceptor/cache-tests.mjs --ci",
"coverage": "npm run coverage:clean && cross-env NODE_V8_COVERAGE=./coverage/tmp npm run test:javascript && npm run coverage:report",
"coverage:ci": "npm run coverage:clean && cross-env NODE_V8_COVERAGE=./coverage/tmp npm run test:javascript && npm run coverage:report:ci",
"coverage:clean": "node ./scripts/clean-coverage.js",
"coverage:report": "cross-env NODE_V8_COVERAGE= c8 report",
"coverage:report:ci": "c8 report",
"bench": "echo \"Error: Benchmarks have been moved to '/benchmarks'\" && exit 1",
"serve:website": "echo \"Error: Documentation has been moved to '/docs'\" && exit 1",
"prepare": "husky && node ./scripts/platform-shell.js"
},
"devDependencies": {
"@fastify/busboy": "3.2.0",
"@matteo.collina/tspl": "^0.2.0",
"@metcoder95/https-pem": "^1.0.0",
"@sinonjs/fake-timers": "^12.0.0",
"@types/node": "^20.19.22",
"abort-controller": "^3.0.0",
"borp": "^0.20.0",
"c8": "^10.0.0",
"cross-env": "^10.0.0",
"dns-packet": "^5.4.0",
"esbuild": "^0.27.3",
"eslint": "^9.9.0",
"fast-check": "^4.1.1",
"husky": "^9.0.7",
"jest": "^30.0.5",
"jsondiffpatch": "^0.7.3",
"neostandard": "^0.12.0",
"node-forge": "^1.3.1",
"proxy": "^2.1.1",
"tsd": "^0.33.0",
"typescript": "^5.6.2",
"ws": "^8.11.0"
},
"engines": {
"node": ">=20.18.1"
},
"tsd": {
"directory": "test/types",
"compilerOptions": {
"esModuleInterop": true,
"lib": [
"esnext"
]
}
},
"jest": {
"testMatch": [
"/test/jest/**"
]
}
}
================================================
FILE: scripts/clean-coverage.js
================================================
'use strict'
const { rmSync } = require('node:fs')
const { resolve } = require('node:path')
if (process.env.NODE_V8_COVERAGE) {
if (process.env.NODE_V8_COVERAGE.endsWith('/tmp')) {
rmSync(resolve(__dirname, process.env.NODE_V8_COVERAGE, '..'), { recursive: true, force: true })
} else {
rmSync(resolve(__dirname, process.env.NODE_V8_COVERAGE), { recursive: true, force: true })
}
} else {
console.log(resolve(__dirname, 'coverage'))
rmSync(resolve(__dirname, '../coverage'), { recursive: true, force: true })
}
================================================
FILE: scripts/generate-pem.js
================================================
'use strict'
require('@metcoder95/https-pem/install')
================================================
FILE: scripts/generate-undici-types-package-json.js
================================================
'use strict'
const fs = require('node:fs')
const path = require('node:path')
const packageJSONPath = path.join(__dirname, '..', 'package.json')
const packageJSONRaw = fs.readFileSync(packageJSONPath, 'utf-8')
const packageJSON = JSON.parse(packageJSONRaw)
const licensePath = path.join(__dirname, '..', 'LICENSE')
const licenseRaw = fs.readFileSync(licensePath, 'utf-8')
const packageTypesJSON = {
name: 'undici-types',
version: packageJSON.version,
description: 'A stand-alone types package for Undici',
homepage: packageJSON.homepage,
bugs: packageJSON.bugs,
repository: packageJSON.repository,
license: packageJSON.license,
types: 'index.d.ts',
files: ['*.d.ts'],
contributors: packageJSON.contributors
}
const packageTypesPath = path.join(__dirname, '..', 'types', 'package.json')
const licenseTypesPath = path.join(__dirname, '..', 'types', 'LICENSE')
fs.writeFileSync(packageTypesPath, JSON.stringify(packageTypesJSON, null, 2))
fs.writeFileSync(licenseTypesPath, licenseRaw)
================================================
FILE: scripts/platform-shell.js
================================================
'use strict'
const { platform } = require('node:os')
const { writeFileSync } = require('node:fs')
const { resolve } = require('node:path')
if (platform() === 'win32') {
writeFileSync(
resolve(__dirname, '.npmrc'),
'script-shell = "C:\\windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe"\n'
)
}
================================================
FILE: scripts/release.js
================================================
'use strict'
// Called from .github/workflows
const generateReleaseNotes = async ({ github, owner, repo, versionTag, commitHash }) => {
const { data: releases } = await github.rest.repos.listReleases({
owner,
repo
})
const previousRelease = releases.find((r) => r.tag_name.startsWith('v7'))
const { data: { body } } = await github.rest.repos.generateReleaseNotes({
owner,
repo,
tag_name: versionTag,
target_commitish: commitHash,
previous_tag_name: previousRelease?.tag_name
})
const bodyWithoutReleasePr = body.split('\n')
.filter((line) => !line.includes('[Release] v'))
.join('\n')
return bodyWithoutReleasePr
}
const generatePr = async ({ github, context, defaultBranch, versionTag, commitHash }) => {
const { owner, repo } = context.repo
const releaseNotes = await generateReleaseNotes({ github, owner, repo, versionTag, commitHash })
await github.rest.pulls.create({
owner,
repo,
head: `release/${versionTag}`,
base: defaultBranch,
title: `[Release] ${versionTag}`,
body: releaseNotes
})
}
const release = async ({ github, context, versionTag, commitHash }) => {
const { owner, repo } = context.repo
const releaseNotes = await generateReleaseNotes({ github, owner, repo, versionTag, commitHash })
await github.rest.repos.createRelease({
owner,
repo,
tag_name: versionTag,
target_commitish: commitHash,
name: versionTag,
body: releaseNotes,
draft: false,
prerelease: false,
generate_release_notes: false
})
try {
await github.rest.git.deleteRef({
owner,
repo,
ref: `heads/release/${versionTag}`
})
} catch (err) {
console.log("Couldn't delete release PR ref")
console.log(err)
}
}
module.exports = {
generatePr,
release
}
================================================
FILE: scripts/strip-comments.js
================================================
'use strict'
const { readFileSync, writeFileSync } = require('node:fs')
const { transcode } = require('node:buffer')
const buffer = transcode
? transcode(readFileSync('./undici-fetch.js'), 'utf8', 'latin1')
: readFileSync('./undici-fetch.js')
writeFileSync('./undici-fetch.js', buffer.toString('latin1'))
================================================
FILE: test/autobahn/.gitignore
================================================
reports/clients
================================================
FILE: test/autobahn/client.js
================================================
'use strict'
const { WebSocket } = require('../..')
const logOnError = process.env.LOG_ON_ERROR === 'true'
let currentTest = 1
let testCount
const autobahnFuzzingserverUrl = process.env.FUZZING_SERVER_URL || 'ws://localhost:9001'
function nextTest () {
let ws
if (currentTest > testCount) {
ws = new WebSocket(`${autobahnFuzzingserverUrl}/updateReports?agent=undici`)
ws.addEventListener('close', () => require('./report'))
return
}
console.log(`Running test case ${currentTest}/${testCount}`)
ws = new WebSocket(
`${autobahnFuzzingserverUrl}/runCase?case=${currentTest}&agent=undici`
)
ws.addEventListener('message', (data) => {
ws.send(data.data)
})
ws.addEventListener('close', () => {
currentTest++
process.nextTick(nextTest)
})
if (logOnError) {
ws.addEventListener('error', (e) => {
console.error(e.error)
})
}
}
const ws = new WebSocket(`${autobahnFuzzingserverUrl}/getCaseCount`)
ws.addEventListener('message', (data) => {
testCount = parseInt(data.data)
})
ws.addEventListener('close', () => {
if (testCount > 0) {
nextTest()
}
})
ws.addEventListener('error', (e) => {
console.error(e.error)
process.exitCode = 1
})
================================================
FILE: test/autobahn/config/fuzzingserver.json
================================================
{
"url": "ws://127.0.0.1:9001",
"outdir": "./reports/clients",
"cases": ["*"],
"exclude-cases": [],
"exclude-agent-cases": {}
}
================================================
FILE: test/autobahn/report.js
================================================
'use strict'
const result = require('./reports/clients/index.json').undici
const failOnError = process.env.FAIL_ON_ERROR === 'true'
let runFailed = false
let okTests = 0
let failedTests = 0
let nonStrictTests = 0
let wrongCodeTests = 0
let uncleanTests = 0
let failedByClientTests = 0
let informationalTests = 0
let unimplementedTests = 0
let totalTests = 0
function testCaseIdToWeight (testCaseId) {
const [major, minor, sub] = testCaseId.split('.')
return sub
? parseInt(major, 10) * 10000 + parseInt(minor, 10) * 100 + parseInt(sub, 10)
: parseInt(major, 10) * 10000 + parseInt(minor, 10) * 100
}
function isFailedTestCase (testCase) {
return (
testCase.behavior === 'FAILED' ||
testCase.behavior === 'WRONG CODE' ||
testCase.behavior === 'UNCLEAN' ||
testCase.behavior === 'FAILED BY CLIENT' ||
testCase.behaviorClose === 'FAILED' ||
testCase.behaviorClose === 'WRONG CODE' ||
testCase.behaviorClose === 'UNCLEAN' ||
testCase.behaviorClose === 'FAILED BY CLIENT'
)
}
const keys = Object.keys(result).sort((a, b) => {
a = testCaseIdToWeight(a)
b = testCaseIdToWeight(b)
return a - b
})
const reorderedResult = {}
for (const key of keys) {
reorderedResult[key] = result[key]
delete reorderedResult[key].reportfile
totalTests++
if (
failOnError &&
!runFailed &&
isFailedTestCase(result[key])
) {
runFailed = true
}
switch (result[key].behavior) {
case 'OK':
okTests++
break
case 'FAILED':
failedTests++
break
case 'NON-STRICT':
nonStrictTests++
break
case 'WRONG CODE':
wrongCodeTests++
break
case 'UNCLEAN':
uncleanTests++
break
case 'FAILED BY CLIENT':
failedByClientTests++
break
case 'INFORMATIONAL':
informationalTests++
break
case 'UNIMPLEMENTED':
unimplementedTests++
break
}
}
console.log('Autobahn Test Report\n\nSummary:')
console.table({
OK: okTests,
Failed: failedTests,
'Non-Strict': nonStrictTests,
'Wrong Code': wrongCodeTests,
Unclean: uncleanTests,
'Failed By Client': failedByClientTests,
Informational: informationalTests,
Unimplemented: unimplementedTests,
Total: totalTests
})
console.log('Details:')
console.table(reorderedResult)
process.exitCode = runFailed ? 1 : 0
================================================
FILE: test/autobahn/run.sh
================================================
docker run -it --rm \
-v "${PWD}/config:/config" \
-v "${PWD}/reports:/reports" \
-p 9001:9001 \
--name fuzzingserver \
crossbario/autobahn-testsuite
================================================
FILE: test/busboy/LICENSE
================================================
Copyright Brian White. All rights reserved.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to
deal in the Software without restriction, including without limitation the
rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
sell copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
IN THE SOFTWARE.
================================================
FILE: test/busboy/formdata-test.js
================================================
'use strict'
// Copyright 2009 The Go Authors.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are
// met:
//
// * Redistributions of source code must retain the above copyright
// notice, this list of conditions and the following disclaimer.
// * Redistributions in binary form must reproduce the above
// copyright notice, this list of conditions and the following disclaimer
// in the documentation and/or other materials provided with the
// distribution.
// * Neither the name of Google LLC nor the names of its
// contributors may be used to endorse or promote products derived from
// this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
// https://github.com/golang/go/blob/16e5d24480dca7ddcbdffb78a8ed5de3e5155dec/src/mime/multipart/formdata_test.go
const { test } = require('node:test')
const { Response } = require('../..')
const fileaContents = 'This is a test file.'
const filebContents = 'Another test file.'
const textaValue = 'foo'
const textbValue = 'bar'
const boundary = 'MyBoundary'
const message = [
'--MyBoundary',
'Content-Disposition: form-data; name="filea"; filename="filea.txt"',
'Content-Type: text/plain',
'',
fileaContents,
'--MyBoundary',
'Content-Disposition: form-data; name="fileb"; filename="fileb.txt"',
'Content-Type: text/plain',
'',
filebContents,
'--MyBoundary',
'Content-Disposition: form-data; name="texta"',
'',
textaValue,
'--MyBoundary',
'Content-Disposition: form-data; name="textb"',
'',
textbValue,
'--MyBoundary--'
].join('\r\n')
const messageWithFileWithoutName = [
'--MyBoundary',
'Content-Disposition: form-data; name="hiddenfile"; filename=""',
'Content-Type: text/plain',
'',
filebContents,
'--MyBoundary--'
].join('\r\n')
const messageWithFileName = [
'--MyBoundary',
'Content-Disposition: form-data; name="filea"; filename="filea.txt"',
'Content-Type: text/plain',
'',
fileaContents,
'--MyBoundary--'
].join('\r\n')
const messageWithTextContentType = [
'--MyBoundary',
'Content-Disposition: form-data; name="texta"',
'Content-Type: text/plain',
'',
textaValue,
'--MyBoundary--'
].join('\r\n')
function makeResponse (body, bnd) {
return new Response(body, {
headers: {
'content-type': `multipart/form-data; boundary=${bnd}`
}
})
}
test('ReadForm - fields and files', async (t) => {
const response = makeResponse(message, boundary)
const fd = await response.formData()
t.assert.strictEqual(fd.get('texta'), textaValue, 'texta value mismatch')
t.assert.strictEqual(fd.get('textb'), textbValue, 'textb value mismatch')
const filea = fd.get('filea')
t.assert.strictEqual(filea.name, 'filea.txt', 'filea filename mismatch')
t.assert.strictEqual(filea.size, fileaContents.length, 'filea size mismatch')
t.assert.strictEqual(await filea.text(), fileaContents, 'filea contents mismatch')
const fileb = fd.get('fileb')
t.assert.strictEqual(fileb.name, 'fileb.txt', 'fileb filename mismatch')
t.assert.strictEqual(fileb.size, filebContents.length, 'fileb size mismatch')
t.assert.strictEqual(await fileb.text(), filebContents, 'fileb contents mismatch')
})
test('ReadForm - file without name', async (t) => {
const response = makeResponse(messageWithFileWithoutName, boundary)
const fd = await response.formData()
// A file with an empty filename is treated as a field in the web platform FormData
const value = fd.get('hiddenfile')
if (typeof value === 'string') {
t.assert.strictEqual(value, filebContents, 'hiddenfile value mismatch')
} else {
t.assert.strictEqual(await value.text(), filebContents, 'hiddenfile contents mismatch')
}
})
test('ReadForm - file with filename', async (t) => {
const response = makeResponse(messageWithFileName, boundary)
const fd = await response.formData()
const filea = fd.get('filea')
t.assert.strictEqual(filea.name, 'filea.txt', 'filea filename mismatch')
t.assert.strictEqual(await filea.text(), fileaContents, 'filea contents mismatch')
})
test('ReadForm - text content type', async (t) => {
const response = makeResponse(messageWithTextContentType, boundary)
const fd = await response.formData()
t.assert.strictEqual(fd.get('texta'), textaValue, 'texta value mismatch')
})
test('ReadForm - no read after EOF', async (t) => {
const eofBoundary = '---------------------------8d345eef0d38dc9'
const body = [
'-----------------------------8d345eef0d38dc9',
'Content-Disposition: form-data; name="version"',
'',
'171',
'-----------------------------8d345eef0d38dc9--'
].join('\r\n')
const response = makeResponse(body, eofBoundary)
const fd = await response.formData()
t.assert.strictEqual(fd.get('version'), '171', 'version value mismatch')
})
test('ReadForm - non-file max memory (large text value)', async (t) => {
const n = 10 << 20 // 10 MB
const largeTextValue = '1'.repeat(n)
const body = [
'--MyBoundary',
'Content-Disposition: form-data; name="largetext"',
'',
largeTextValue,
'--MyBoundary--'
].join('\r\n')
const response = makeResponse(body, boundary)
const fd = await response.formData()
t.assert.strictEqual(fd.get('largetext'), largeTextValue, 'largetext value mismatch')
})
test('ReadForm - many files', async (t) => {
const numFiles = 10
const parts = []
const fileBoundary = 'TestBoundary'
for (let i = 0; i < numFiles; i++) {
parts.push(
`--${fileBoundary}`,
`Content-Disposition: form-data; name="${i}"; filename="${i}"`,
'Content-Type: application/octet-stream',
'',
`${i}`
)
}
parts.push(`--${fileBoundary}--`)
const body = parts.join('\r\n')
const response = makeResponse(body, fileBoundary)
const fd = await response.formData()
for (let i = 0; i < numFiles; i++) {
const file = fd.get(`${i}`)
t.assert.ok(file, `file "${i}" should exist`)
t.assert.strictEqual(file.name, `${i}`, `file "${i}" filename mismatch`)
const text = await file.text()
t.assert.strictEqual(text, `${i}`, `file "${i}" contents mismatch`)
}
})
test('ReadForm - limits with many values', async (t) => {
const numValues = 100
const parts = []
const limitBoundary = 'LimitBoundary'
for (let i = 0; i < numValues; i++) {
parts.push(
`--${limitBoundary}`,
`Content-Disposition: form-data; name="field${i}"`,
'',
`value ${i}`
)
}
parts.push(`--${limitBoundary}--`)
const body = parts.join('\r\n')
const response = makeResponse(body, limitBoundary)
const fd = await response.formData()
for (let i = 0; i < numValues; i++) {
t.assert.strictEqual(
fd.get(`field${i}`),
`value ${i}`,
`field${i} value mismatch`
)
}
})
test('ReadForm - limits with values and files', async (t) => {
const numValues = 50
const numFiles = 50
const parts = []
const limitBoundary = 'LimitBoundary'
for (let i = 0; i < numValues; i++) {
parts.push(
`--${limitBoundary}`,
`Content-Disposition: form-data; name="field${i}"`,
'',
`value ${i}`
)
}
for (let i = 0; i < numFiles; i++) {
parts.push(
`--${limitBoundary}`,
`Content-Disposition: form-data; name="file${i}"; filename="file${i}"`,
'Content-Type: application/octet-stream',
'',
`value ${i}`
)
}
parts.push(`--${limitBoundary}--`)
const body = parts.join('\r\n')
const response = makeResponse(body, limitBoundary)
const fd = await response.formData()
for (let i = 0; i < numValues; i++) {
t.assert.strictEqual(
fd.get(`field${i}`),
`value ${i}`,
`field${i} value mismatch`
)
}
for (let i = 0; i < numFiles; i++) {
const file = fd.get(`file${i}`)
t.assert.ok(file, `file${i} should exist`)
const text = await file.text()
t.assert.strictEqual(text, `value ${i}`, `file${i} contents mismatch`)
}
})
test('ReadForm - metadata too large (large field name)', async (t) => {
const largeName = 'a'.repeat(10 << 20)
const body = [
'--MyBoundary',
`Content-Disposition: form-data; name="${largeName}"`,
'',
'value',
'--MyBoundary--'
].join('\r\n')
const response = makeResponse(body, boundary)
// Expect parsing to either succeed (implementation dependent) or throw
try {
const fd = await response.formData()
// If it succeeds, the value should be correct
t.assert.strictEqual(fd.get(largeName), 'value')
} catch (err) {
// Implementation may reject overly large metadata
t.assert.ok(err, 'error thrown for large metadata')
}
})
test('ReadForm - metadata too large (large MIME header)', async (t) => {
const largeHeaderValue = 'a'.repeat(10 << 20)
const body = [
'--MyBoundary',
'Content-Disposition: form-data; name="a"',
`X-Foo: ${largeHeaderValue}`,
'',
'value',
'--MyBoundary--'
].join('\r\n')
const response = makeResponse(body, boundary)
try {
const fd = await response.formData()
t.assert.strictEqual(fd.get('a'), 'value')
} catch (err) {
t.assert.ok(err, 'error thrown for large MIME header')
}
})
test('ReadForm - metadata too large (many parts)', async (t) => {
const parts = []
const numParts = 110000
for (let i = 0; i < numParts; i++) {
parts.push(
'--MyBoundary',
'Content-Disposition: form-data; name="f"',
'',
'v'
)
}
parts.push('--MyBoundary--')
const body = parts.join('\r\n')
const response = makeResponse(body, boundary)
try {
const fd = await response.formData()
// If it succeeds, check that values are present
t.assert.ok(fd.getAll('f').length > 0)
} catch (err) {
// Implementation may reject too many parts
t.assert.ok(err, 'error thrown for too many parts')
}
})
test('ReadForm - endless header line (name)', async (t) => {
// Create a body with an extremely long header name that never ends
const longPrefix = 'X-' + 'X'.repeat(1 << 20)
const body = [
'--MyBoundary',
'Content-Disposition: form-data; name="a"',
'Content-Type: text/plain',
longPrefix
].join('\r\n')
const response = makeResponse(body, boundary)
try {
await response.formData()
// If parsing succeeds (truncated or ignored), that's acceptable
} catch (err) {
t.assert.ok(err, 'error thrown for endless header line')
}
})
test('ReadForm - endless header line (value)', async (t) => {
const longValue = 'X-Header: ' + 'X'.repeat(1 << 20)
const body = [
'--MyBoundary',
'Content-Disposition: form-data; name="a"',
'Content-Type: text/plain',
longValue
].join('\r\n')
const response = makeResponse(body, boundary)
try {
await response.formData()
} catch (err) {
t.assert.ok(err, 'error thrown for endless header value')
}
})
================================================
FILE: test/busboy/issue-3676.js
================================================
'use strict'
const { test } = require('node:test')
const { Response } = require('../..')
// https://github.com/nodejs/undici/issues/3676
test('Leading and trailing CRLFs are ignored', async (t) => {
const response = new Response([
'--axios-1.7.7-boundary-bPgZ9x77LfApGVUN839vui4V7\r\n' +
'Content-Disposition: form-data; name="file"; filename="doc.txt"\r\n' +
'Content-Type: text/plain\r\n' +
'\r\n' +
'Helloworld\r\n' +
'--axios-1.7.7-boundary-bPgZ9x77LfApGVUN839vui4V7--\r\n' +
'\r\n'
].join(''), {
headers: {
'content-type': 'multipart/form-data; boundary=axios-1.7.7-boundary-bPgZ9x77LfApGVUN839vui4V7'
}
})
await t.assert.doesNotReject(response.formData())
})
================================================
FILE: test/busboy/issue-3760.js
================================================
'use strict'
const { test } = require('node:test')
const { Response } = require('../..')
// https://github.com/nodejs/undici/issues/3760
test('filename* parameter is parsed properly', async (t) => {
const response = new Response([
'--83d82e0d-9ced-44c0-ac79-4e66a827415b\r\n' +
'Content-Type: text/plain\r\n' +
'Content-Disposition: form-data; name="file"; filename*=UTF-8\'\'%e2%82%ac%20rates\r\n' +
'\r\n' +
'testabc\r\n' +
'--83d82e0d-9ced-44c0-ac79-4e66a827415b--\r\n' +
'\r\n'
].join(''), {
headers: {
'content-type': 'multipart/form-data; boundary="83d82e0d-9ced-44c0-ac79-4e66a827415b"'
}
})
const fd = await response.formData()
t.assert.deepEqual(fd.get('file').name, '€ rates')
})
test('whitespace after filename[*]= is ignored', async (t) => {
for (const response of [
new Response([
'--83d82e0d-9ced-44c0-ac79-4e66a827415b\r\n' +
'Content-Type: text/plain\r\n' +
'Content-Disposition: form-data; name="file"; filename*= utf-8\'\'hello\r\n' +
'\r\n' +
'testabc\r\n' +
'--83d82e0d-9ced-44c0-ac79-4e66a827415b--\r\n' +
'\r\n'
].join(''), {
headers: {
'content-type': 'multipart/form-data; boundary="83d82e0d-9ced-44c0-ac79-4e66a827415b"'
}
}),
new Response([
'--83d82e0d-9ced-44c0-ac79-4e66a827415b\r\n' +
'Content-Type: text/plain\r\n' +
'Content-Disposition: form-data; name="file"; filename= "hello"\r\n' +
'\r\n' +
'testabc\r\n' +
'--83d82e0d-9ced-44c0-ac79-4e66a827415b--\r\n' +
'\r\n'
].join(''), {
headers: {
'content-type': 'multipart/form-data; boundary="83d82e0d-9ced-44c0-ac79-4e66a827415b"'
}
})
]) {
const fd = await response.formData()
t.assert.deepEqual(fd.get('file').name, 'hello')
}
})
================================================
FILE: test/busboy/issue-4660.js
================================================
'use strict'
const { test } = require('node:test')
const { Request } = require('../..')
// https://github.com/nodejs/undici/issues/4660
test('unquoted attributes are parsed correctly', async (t) => {
const request = new Request('http://localhost', {
method: 'POST',
headers: {
'Content-Type': 'multipart/form-data; boundary=7427efb8-6ce7-4740-9198-d90399842641'
},
body:
'--7427efb8-6ce7-4740-9198-d90399842641\r\n' +
'Content-Type: text/plain; charset=utf-8\r\n' +
'Content-Disposition: form-data; name=test\r\n' +
'\r\n' +
'abc\r\n' +
'--7427efb8-6ce7-4740-9198-d90399842641--'
})
const fd = await request.formData()
t.assert.deepEqual(fd.get('test'), 'abc')
})
test('leading spaces are allowed with quoted strings', async (t) => {
const request = new Request('http://localhost', {
method: 'POST',
headers: {
'Content-Type': 'multipart/form-data; boundary=7427efb8-6ce7-4740-9198-d90399842641'
},
body:
'--7427efb8-6ce7-4740-9198-d90399842641\r\n' +
'Content-Type: text/plain; charset=utf-8\r\n' +
'Content-Disposition: form-data; name= "test"\r\n' + // <-- space between attribute name and value
'\r\n' +
'abc\r\n' +
'--7427efb8-6ce7-4740-9198-d90399842641--'
})
const fd = await request.formData()
t.assert.deepEqual(fd.get('test'), 'abc')
})
test('leading spaces are allowed & ignored in unquoted strings', async (t) => {
const request = new Request('http://localhost', {
method: 'POST',
headers: {
'Content-Type': 'multipart/form-data; boundary=7427efb8-6ce7-4740-9198-d90399842641'
},
body:
'--7427efb8-6ce7-4740-9198-d90399842641\r\n' +
'Content-Type: text/plain; charset=utf-8\r\n' +
'Content-Disposition: form-data; name= test\r\n' + // <-- space between attribute name and value
'\r\n' +
'abc\r\n' +
'--7427efb8-6ce7-4740-9198-d90399842641--'
})
const fd = await request.formData()
t.assert.deepEqual(fd.get('test'), 'abc')
})
================================================
FILE: test/busboy/issue-4671.js
================================================
'use strict'
const { test } = require('node:test')
const { Request, Response, FormData } = require('../..')
// https://github.com/nodejs/undici/issues/4671
test('preamble and epilogue is ignored', async (t) => {
const request = new Request('https://example.com', {
method: 'POST',
body: (function () {
const formData = new FormData()
formData.append('a', 'b')
return formData
})()
})
const contentType = request.headers.get('Content-Type')
let bytes = await request.bytes()
bytes = new Uint8Array([...bytes, ...Array(10).fill(0)])
await t.test('epilogue', async () => {
await new Response(bytes, {
headers: {
'Content-Type': contentType
}
}).formData()
})
await t.test('preamble', async () => {
// preamble
bytes.set(bytes.subarray(0, -10), 10)
bytes.fill(0, 0, 8)
bytes[8] = 13
bytes[9] = 10
await new Response(bytes, {
headers: {
'Content-Type': contentType
}
}).formData()
})
})
================================================
FILE: test/busboy/test-parser.js
================================================
'use strict'
// Copyright (c) 2010-2025, Marcel Hellkamp
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
// https://github.com/defnull/multipart/blob/d390c3a98a619f4445d7f340d6022b0ccad2ca8e/test/test_push_parser.py
const { test } = require('node:test')
const { Response } = require('../..')
function b64decode (input) {
return Buffer.from(input, 'base64').toString()
}
function makeResponse (body, bnd) {
return new Response(body, {
headers: {
'content-type': `multipart/form-data; boundary=${bnd}`
}
})
}
const legacyTests = {
'firefox3-2png1txt': [
'-----------------------------186454651713519341951581030105\r\n',
'Content-Disposition: form-data; name="file1"; filename="anchor.png"\r\n',
'Content-Type: image/png\r\n',
'\r\n',
'[file1-data]\r\n',
'-----------------------------186454651713519341951581030105\r\n',
'Content-Disposition: form-data; name="file2"; filename="application_edit.png"\r\n',
'Content-Type: image/png\r\n',
'\r\n',
'[file2-data]\r\n',
'-----------------------------186454651713519341951581030105\r\n',
'Content-Disposition: form-data; name="text"\r\n',
'\r\n',
'[Text]\r\n',
'-----------------------------186454651713519341951581030105--\r\n'
],
'firefox3-2pnglongtext': [
'-----------------------------14904044739787191031754711748\r\n',
'Content-Disposition: form-data; name="file1"; filename="accept.png"\r\n',
'Content-Type: image/png\r\n',
'\r\n',
'[file1-data]\r\n',
'-----------------------------14904044739787191031754711748\r\n',
'Content-Disposition: form-data; name="file2"; filename="add.png"\r\n',
'Content-Type: image/png\r\n',
'\r\n',
'[file2-data]\r\n',
'-----------------------------14904044739787191031754711748\r\n',
'Content-Disposition: form-data; name="text"\r\n',
'\r\n',
'[Text]\r\n',
'-----------------------------14904044739787191031754711748--\r\n'
],
'opera8-2png1txt': [
'------------zEO9jQKmLc2Cq88c23Dx19\r\n',
'Content-Disposition: form-data; name="file1"; filename="arrow_branch.png"\r\n',
'Content-Type: image/png\r\n',
'\r\n',
'[file1-data]\r\n',
'------------zEO9jQKmLc2Cq88c23Dx19\r\n',
'Content-Disposition: form-data; name="file2"; filename="award_star_bronze_1.png"\r\n',
'Content-Type: image/png\r\n',
'\r\n',
'[file2-data]\r\n',
'------------zEO9jQKmLc2Cq88c23Dx19\r\n',
'Content-Disposition: form-data; name="text"\r\n',
'\r\n',
'[Text]\r\n',
'------------zEO9jQKmLc2Cq88c23Dx19--\r\n'
],
'webkit3-2png1txt': [
'------WebKitFormBoundaryjdSFhcARk8fyGNy6\r\n',
'Content-Disposition: form-data; name="file1"; filename="gtk-apply.png"\r\n',
'Content-Type: image/png\r\n',
'\r\n',
'[file1-data]\r\n',
'------WebKitFormBoundaryjdSFhcARk8fyGNy6\r\n',
'Content-Disposition: form-data; name="file2"; filename="gtk-no.png"\r\n',
'Content-Type: image/png\r\n',
'\r\n',
'[file2-data]\r\n',
'------WebKitFormBoundaryjdSFhcARk8fyGNy6\r\n',
'Content-Disposition: form-data; name="text"\r\n',
'\r\n',
'[Text]\r\n',
'------WebKitFormBoundaryjdSFhcARk8fyGNy6--\r\n'
],
'ie6-2png1txt': [
'-----------------------------7d91b03a20128\r\n',
'Content-Disposition: form-data; name="file1"; filename="C:\\Python25\\wztest\\werkzeug-main\\tests\\multipart\\firefox3-2png1txt\\file1.png"\r\n',
'Content-Type: image/x-png\r\n',
'\r\n',
'[file1-data]\r\n',
'-----------------------------7d91b03a20128\r\n',
'Content-Disposition: form-data; name="file2"; filename="C:\\Python25\\wztest\\werkzeug-main\\tests\\multipart\\firefox3-2png1txt\\file2.png"\r\n',
'Content-Type: image/x-png\r\n',
'\r\n',
'[file2-data]\r\n',
'-----------------------------7d91b03a20128\r\n',
'Content-Disposition: form-data; name="text"\r\n',
'\r\n',
'[Text]\r\n',
'-----------------------------7d91b03a20128--\r\n'
]
}
const browserTestCases = {
'firefox3-2png1txt': {
data: b64decode(
`
LS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0xODY0NTQ2NTE3MTM1MTkzNDE5NTE1ODEwMzAx
MDUNCkNvbnRlbnQtRGlzcG9zaXRpb246IGZvcm0tZGF0YTsgbmFtZT0iZmlsZTEiOyBmaWxlbmFt
ZT0iYW5jaG9yLnBuZyINCkNvbnRlbnQtVHlwZTogaW1hZ2UvcG5nDQoNColQTkcNChoKAAAADUlI
RFIAAAAQAAAAEAgGAAAAH/P/YQAAAARnQU1BAACvyDcFiukAAAAZdEVYdFNvZnR3YXJlAEFkb2Jl
IEltYWdlUmVhZHlxyWU8AAABnUlEQVQ4y6VTMWvCQBS+qwEFB10KGaS1P6FDpw7SrVvzAwRRx04V
Ck4K6iAoDhLXdhFcW9qhZCk4FQoW0gp2U4lQRDAUS4hJmn5Xgg2lsQ198PHu3b3vu5d3L9S2bfIf
47wOer1ewzTNtGEYBP48kUjkfsrb8BIAMb1cLovwRfi07wrYzcCr4/1/Am4FzzhzBGZeefR7E7vd
7j0Iu4wYjUYDBMfD0dBiMUQfstns3toKkHgF6EgmqqruW6bFiHcsxr70awVu63Q6NiOmUinquwfM
dF1f28CVgCRJx0jMAQ1BEFquRn7CbYVCYZVbr9dbnJMohoIh9kViu90WEW9nMpmxu4JyubyF/VEs
FiNcgCPyoyxiu7XhCPBzdU4s652VnUccbDabPLyN2C6VSmwdhFgel5DB84AJb64mEUlvmqadTKcv
40gkUkUsg1DjeZ7iRsrWgByP71T7/afxYrHIYry/eoBD9mxsaK4VRamFw2EBQknMAWGvRClNTpQJ
AfkCxFNgBmiez1ipVA4hdgQcOD/TLfylKIo3vubgL/YBnIw+ioOMLtwAAAAASUVORK5CYIINCi0t
LS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tMTg2NDU0NjUxNzEzNTE5MzQxOTUxNTgxMDMwMTA1
DQpDb250ZW50LURpc3Bvc2l0aW9uOiBmb3JtLWRhdGE7IG5hbWU9ImZpbGUyIjsgZmlsZW5hbWU9
ImFwcGxpY2F0aW9uX2VkaXQucG5nIg0KQ29udGVudC1UeXBlOiBpbWFnZS9wbmcNCg0KiVBORw0K
GgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdh
cmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAJRSURBVBgZpcHda81xHMDx9+d3fudYzuYw2RaZ5yTW
olEiuZpCSjGJFEktUUr8A6ZxQZGHmDtqdrGUXHgoeZqSp1F2bLFWjtkOB8PZzvmd7+djv5XaBRfL
6yVmxv+QjQeu7l25uuZYJmtxM0AVU8Wpw9RQU8w51AxzDqfKhFjwq6Mjdbj1RN0Zv2ZFzaloUdwr
L2Is4r+y7hRwxs8G5mUzPxmrwcA8hvnmjIZtcxmr3Y09hHwzJZQvOAwwNZyCYqgaThVXMFzBCD7f
Jfv8MpHiKvaV3ePV2f07fMwIiSeIGeYJJoao4HmCiIeIQzPXifY+paJqO4lZi/nWPZ/krabjvlNH
yANMBAQiBiqgakQMCunbxHJviM9bQeZdBzHJUzKhguLJlQnf1BghAmZ4gImAgAjk++8jP56QmL2G
XG8zsfFCz8skA1mQXKbaU3X8ISIgQsgDcun7FL7cJjFnLUMfLyLRr0SLS4hbhiup5Szd19rpFYKA
ESKICCERoS95neyHmyTmbmAodQ4vGpAfmEn6YTtTahv4ODiRkGdOCUUAAUSE/uQNfqTaKFu4jvyn
JiIxIzcwg/SjF1RsOk9R+QJMlZCvqvwhQFdbM4XvrynIVHpfn2ZSWYyhzHS+PUtSueUC0cQ0QmpG
yE9197TUnwzq1DnUKbXSxOb6S7xtPkjngzbGVVbzvS/FjaGt9DU8xlRRJdTCMDEzRjuyZ1FwaFe9
j+d4eecaPd1dPxNTSlfWHm1v5y/EzBitblXp4JLZ5f6yBbOwaK5tsD+9c33jq/f8w2+mRSjOllPh
kAAAAABJRU5ErkJggg0KLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0xODY0NTQ2NTE3MTM1
MTkzNDE5NTE1ODEwMzAxMDUNCkNvbnRlbnQtRGlzcG9zaXRpb246IGZvcm0tZGF0YTsgbmFtZT0i
dGV4dCINCg0KZXhhbXBsZSB0ZXh0DQotLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLTE4NjQ1
NDY1MTcxMzUxOTM0MTk1MTU4MTAzMDEwNS0tDQo=`
),
boundary: '---------------------------186454651713519341951581030105',
files: {
file1: [
'anchor.png',
'image/png',
b64decode(
`
iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0
U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAGdSURBVDjLpVMxa8JAFL6rAQUHXQoZpLU/
oUOnDtKtW/MDBFHHThUKTgrqICgOEtd2EVxb2qFkKTgVChbSCnZTiVBEMBRLiEmafleCDaWxDX3w
8e7dve+7l3cv1LZt8h/jvA56vV7DNM20YRgE/jyRSOR+ytvwEgAxvVwui/BF+LTvCtjNwKvj/X8C
bgXPOHMEZl559HsTu93uPQi7jBiNRgMEx8PR0GIxRB+y2eze2gqQeAXoSCaqqu5bpsWIdyzGvvRr
BW7rdDo2I6ZSKeq7B8x0XV/bwJWAJEnHSMwBDUEQWq5GfsJthUJhlVuv11uckyiGgiH2RWK73RYR
b2cymbG7gnK5vIX9USwWI1yAI/KjLGK7teEI8HN1TizrnZWdRxxsNps8vI3YLpVKbB2EWB6XkMHz
gAlvriYRSW+app1Mpy/jSCRSRSyDUON5nuJGytaAHI/vVPv9p/FischivL96gEP2bGxorhVFqYXD
YQFCScwBYa9EKU1OlAkB+QLEU2AGaJ7PWKlUDiF2BBw4P9Mt/KUoije+5uAv9gGcjD6Kg4wu3AAA
AABJRU5ErkJggg==`
)
],
file2: [
'application_edit.png',
'image/png',
b64decode(
`
iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0
U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAJRSURBVBgZpcHda81xHMDx9+d3fudYzuYw
2RaZ5yTWolEiuZpCSjGJFEktUUr8A6ZxQZGHmDtqdrGUXHgoeZqSp1F2bLFWjtkOB8PZzvmd7+dj
v5XaBRfL6yVmxv+QjQeu7l25uuZYJmtxM0AVU8Wpw9RQU8w51AxzDqfKhFjwq6Mjdbj1RN0Zv2ZF
zaloUdwrL2Is4r+y7hRwxs8G5mUzPxmrwcA8hvnmjIZtcxmr3Y09hHwzJZQvOAwwNZyCYqgaThVX
MFzBCD7fJfv8MpHiKvaV3ePV2f07fMwIiSeIGeYJJoao4HmCiIeIQzPXifY+paJqO4lZi/nWPZ/k
rabjvlNHyANMBAQiBiqgakQMCunbxHJviM9bQeZdBzHJUzKhguLJlQnf1BghAmZ4gImAgAjk++8j
P56QmL2GXG8zsfFCz8skA1mQXKbaU3X8ISIgQsgDcun7FL7cJjFnLUMfLyLRr0SLS4hbhiup5Szd
19rpFYKAESKICCERoS95neyHmyTmbmAodQ4vGpAfmEn6YTtTahv4ODiRkGdOCUUAAUSE/uQNfqTa
KFu4jvynJiIxIzcwg/SjF1RsOk9R+QJMlZCvqvwhQFdbM4XvrynIVHpfn2ZSWYyhzHS+PUtSueUC
0cQ0QmpGyE9197TUnwzq1DnUKbXSxOb6S7xtPkjngzbGVVbzvS/FjaGt9DU8xlRRJdTCMDEzRjuy
Z1FwaFe9j+d4eecaPd1dPxNTSlfWHm1v5y/EzBitblXp4JLZ5f6yBbOwaK5tsD+9c33jq/f8w2+m
RSjOllPhkAAAAABJRU5ErkJggg==`
)
]
},
forms: { text: 'example text' }
},
'firefox3-2pnglongtext': {
data: b64decode(
`
LS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0xNDkwNDA0NDczOTc4NzE5MTAzMTc1NDcxMTc0
OA0KQ29udGVudC1EaXNwb3NpdGlvbjogZm9ybS1kYXRhOyBuYW1lPSJmaWxlMSI7IGZpbGVuYW1l
PSJhY2NlcHQucG5nIg0KQ29udGVudC1UeXBlOiBpbWFnZS9wbmcNCg0KiVBORw0KGgoAAAANSUhE
UgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUg
SW1hZ2VSZWFkeXHJZTwAAAKfSURBVDjLpZPrS1NhHMf9O3bOdmwDCWREIYKEUHsVJBI7mg3FvCxL
09290jZj2EyLMnJexkgpLbPUanNOberU5taUMnHZUULMvelCtWF0sW/n7MVMEiN64AsPD8/n83uu
cQDi/id/DBT4Dolypw/qsz0pTMbj/WHpiDgsdSUyUmeiPt2+V7SrIM+bSss8ySGdR4abQQv6lrui
6VxsRonrGCS9VEjSQ9E7CtiqdOZ4UuTqnBHO1X7YXl6Daa4yGq7vWO1D40wVDtj4kWQbn94myPGk
CDPdSesczE2sCZShwl8CzcwZ6NiUs6n2nYX99T1cnKqA2EKui6+TwphA5k4yqMayopU5mANV3lNQ
TBdCMVUA9VQh3GuDMHiVcLCS3J4jSLhCGmKCjBEx0xlshjXYhApfMZRP5CyYD+UkG08+xt+4wLVQ
ZA1tzxthm2tEfD3JxARH7QkbD1ZuozaggdZbxK5kAIsf5qGaKMTY2lAU/rH5HW3PLsEwUYy+YCcE
RmIjJpDcpzb6l7th9KtQ69fi09ePUej9l7cx2DJbD7UrG3r3afQHOyCo+V3QQzE35pvQvnAZukk5
zL5qRL59jsKbPzdheXoBZc4saFhBS6AO7V4zqCpiawuptwQG+UAa7Ct3UT0hh9p9EnXT5Vh6t4C2
2QaUDh6HwnECOmcO7K+6kW49DKqS2DrEZCtfuI+9GrNHg4fMHVSO5kE7nAPVkAxKBxcOzsajpS4Y
h4ohUPPWKTUh3PaQEptIOr6BiJjcZXCwktaAGfrRIpwblqOV3YKdhfXOIvBLeREWpnd8ynsaSJoy
ESFphwTtfjN6X1jRO2+FxWtCWksqBApeiFIR9K6fiTpPiigDoadqCEag5YUFKl6Yrciw0VOlhOiv
v/Ff8wtn0KzlebrUYwAAAABJRU5ErkJggg0KLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0x
NDkwNDA0NDczOTc4NzE5MTAzMTc1NDcxMTc0OA0KQ29udGVudC1EaXNwb3NpdGlvbjogZm9ybS1k
YXRhOyBuYW1lPSJmaWxlMiI7IGZpbGVuYW1lPSJhZGQucG5nIg0KQ29udGVudC1UeXBlOiBpbWFn
ZS9wbmcNCg0KiVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK
6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAJvSURBVDjLpZPrS5NhGIf9
W7YvBYOkhlkoqCklWChv2WyKik7blnNris72bi6dus0DLZ0TDxW1odtopDs4D8MDZuLU0kXq61Ci
jSIIasOvv94VTUfLiB74fXngup7nvrnvJABJ/5PfLnTTdcwOj4RsdYmo5glBWP6iOtzwvIKSWstI
0Wgx80SBblpKtE9KQs/We7EaWoT/8wbWP61gMmCH0lMDvokT4j25TiQU/ITFkek9Ow6+7WH2gwsm
ahCPdwyw75uw9HEO2gUZSkfyI9zBPCJOoJ2SMmg46N61YO/rNoa39Xi41oFuXysMfh36/Fp0b7bA
fWAH6RGi0HglWNCbzYgJaFjRv6zGuy+b9It96N3SQvNKiV9HvSaDfFEIxXItnPs23BzJQd6DDEVM
0OKsoVwBG/1VMzpXVWhbkUM2K4oJBDYuGmbKIJ0qxsAbHfRLzbjcnUbFBIpx/qH3vQv9b3U03IQ/
HfFkERTzfFj8w8jSpR7GBE123uFEYAzaDRIqX/2JAtJbDat/COkd7CNBva2cMvq0MGxp0PRSCPF8
BXjWG3FgNHc9XPT71Ojy3sMFdfJRCeKxEsVtKwFHwALZfCUk3tIfNR8XiJwc1LmL4dg141JPKtj3
WUdNFJqLGFVPC4OkR4BxajTWsChY64wmCnMxsWPCHcutKBxMVp5mxA1S+aMComToaqTRUQknLTH6
2kHOVEE+VQnjahscNCy0cMBWsSI0TCQcZc5ALkEYckL5A5noWSBhfm2AecMAjbcRWV0pUTh0HE64
TNf0mczcnnQyu/MilaFJCae1nw2fbz1DnVOxyGTlKeZft/Ff8x1BRssfACjTwQAAAABJRU5ErkJg
gg0KLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0xNDkwNDA0NDczOTc4NzE5MTAzMTc1NDcx
MTc0OA0KQ29udGVudC1EaXNwb3NpdGlvbjogZm9ybS1kYXRhOyBuYW1lPSJ0ZXh0Ig0KDQotLWxv
bmcgdGV4dA0KLS13aXRoIGJvdW5kYXJ5DQotLWxvb2thbGlrZXMtLQ0KLS0tLS0tLS0tLS0tLS0t
LS0tLS0tLS0tLS0tLS0xNDkwNDA0NDczOTc4NzE5MTAzMTc1NDcxMTc0OC0tDQo=`
),
boundary: '---------------------------14904044739787191031754711748',
files: {
file1: [
'accept.png',
'image/png',
b64decode(
`
iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0
U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAKfSURBVDjLpZPrS1NhHMf9O3bOdmwDCWRE
IYKEUHsVJBI7mg3FvCxL09290jZj2EyLMnJexkgpLbPUanNOberU5taUMnHZUULMvelCtWF0sW/n
7MVMEiN64AsPD8/n83uucQDi/id/DBT4Dolypw/qsz0pTMbj/WHpiDgsdSUyUmeiPt2+V7SrIM+b
Sss8ySGdR4abQQv6lrui6VxsRonrGCS9VEjSQ9E7CtiqdOZ4UuTqnBHO1X7YXl6Daa4yGq7vWO1D
40wVDtj4kWQbn94myPGkCDPdSesczE2sCZShwl8CzcwZ6NiUs6n2nYX99T1cnKqA2EKui6+TwphA
5k4yqMayopU5mANV3lNQTBdCMVUA9VQh3GuDMHiVcLCS3J4jSLhCGmKCjBEx0xlshjXYhApfMZRP
5CyYD+UkG08+xt+4wLVQZA1tzxthm2tEfD3JxARH7QkbD1ZuozaggdZbxK5kAIsf5qGaKMTY2lAU
/rH5HW3PLsEwUYy+YCcERmIjJpDcpzb6l7th9KtQ69fi09ePUej9l7cx2DJbD7UrG3r3afQHOyCo
+V3QQzE35pvQvnAZukk5zL5qRL59jsKbPzdheXoBZc4saFhBS6AO7V4zqCpiawuptwQG+UAa7Ct3
UT0hh9p9EnXT5Vh6t4C22QaUDh6HwnECOmcO7K+6kW49DKqS2DrEZCtfuI+9GrNHg4fMHVSO5kE7
nAPVkAxKBxcOzsajpS4Yh4ohUPPWKTUh3PaQEptIOr6BiJjcZXCwktaAGfrRIpwblqOV3YKdhfXO
IvBLeREWpnd8ynsaSJoyESFphwTtfjN6X1jRO2+FxWtCWksqBApeiFIR9K6fiTpPiigDoadqCEag
5YUFKl6Yrciw0VOlhOivv/Ff8wtn0KzlebrUYwAAAABJRU5ErkJggg==`
)
],
file2: [
'add.png',
'image/png',
b64decode(
`
iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0
U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAJvSURBVDjLpZPrS5NhGIf9W7YvBYOkhlko
qCklWChv2WyKik7blnNris72bi6dus0DLZ0TDxW1odtopDs4D8MDZuLU0kXq61CijSIIasOvv94V
TUfLiB74fXngup7nvrnvJABJ/5PfLnTTdcwOj4RsdYmo5glBWP6iOtzwvIKSWstI0Wgx80SBblpK
tE9KQs/We7EaWoT/8wbWP61gMmCH0lMDvokT4j25TiQU/ITFkek9Ow6+7WH2gwsmahCPdwyw75uw
9HEO2gUZSkfyI9zBPCJOoJ2SMmg46N61YO/rNoa39Xi41oFuXysMfh36/Fp0b7bAfWAH6RGi0Hgl
WNCbzYgJaFjRv6zGuy+b9It96N3SQvNKiV9HvSaDfFEIxXItnPs23BzJQd6DDEVM0OKsoVwBG/1V
MzpXVWhbkUM2K4oJBDYuGmbKIJ0qxsAbHfRLzbjcnUbFBIpx/qH3vQv9b3U03IQ/HfFkERTzfFj8
w8jSpR7GBE123uFEYAzaDRIqX/2JAtJbDat/COkd7CNBva2cMvq0MGxp0PRSCPF8BXjWG3FgNHc9
XPT71Ojy3sMFdfJRCeKxEsVtKwFHwALZfCUk3tIfNR8XiJwc1LmL4dg141JPKtj3WUdNFJqLGFVP
C4OkR4BxajTWsChY64wmCnMxsWPCHcutKBxMVp5mxA1S+aMComToaqTRUQknLTH62kHOVEE+VQnj
ahscNCy0cMBWsSI0TCQcZc5ALkEYckL5A5noWSBhfm2AecMAjbcRWV0pUTh0HE64TNf0mczcnnQy
u/MilaFJCae1nw2fbz1DnVOxyGTlKeZft/Ff8x1BRssfACjTwQAAAABJRU5ErkJggg==`
)
]
},
forms: { text: '--long text\r\n--with boundary\r\n--lookalikes--' }
},
'opera8-2png1txt': {
data: b64decode(
`
LS0tLS0tLS0tLS0tekVPOWpRS21MYzJDcTg4YzIzRHgxOQ0KQ29udGVudC1EaXNwb3NpdGlvbjog
Zm9ybS1kYXRhOyBuYW1lPSJmaWxlMSI7IGZpbGVuYW1lPSJhcnJvd19icmFuY2gucG5nIg0KQ29u
dGVudC1UeXBlOiBpbWFnZS9wbmcNCg0KiVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9h
AAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHY
SURBVDjLlVLPS1RxHJynpVu7KEn0Vt+2l6IO5qGCIsIwCPwD6hTUaSk6REoUHeoQ0qVAMrp0COpY
0SUIPVRgSl7ScCUTst6zIoqg0y7lvpnPt8MWKuuu29w+hxnmx8dzzmE5+l7mxk1u/a3Dd/ejDjSs
II/m3vjJ9MF0yt93ZuTkdD0CnnMO/WOnmsxsJp3yd2zfvA3mHOa+zuHTjy/zojrvHX1YqunAZE9M
lpUcZAaZQBNIZUg9XdPBP5wePuEO7eyGQXg29QL3jz3y1oqwbvkhCuYEOQMp/HeJohCbICMUVwr0
DvZcOnK9u7GmQNmBQLJCgORxkneqRmAs0BFmDi0bW9E72PPda/BikwWi0OEHkNR14MrewsTAZF+l
AAWZEH6LUCwUkUlntrS1tiG5IYlEc6LcjYjSYuncngtdhakbM5dXlhgTNEMYLqB9q49MKgsPjTBX
ntVgkDNIgmI1VY2Q7QzgJ9rx++ci3ofziBYiiELQEUAyhB/D29M3Zy+uIkDIhGYvgeKvIkbHxz6T
evzq6ut+ANh9fldetMn80OzZVVdgLFjBQ0tpEz68jcB4ifx3pQeictVXIEETnBPCKMLEwBIZAPJD
767V/ETGwsjzYYiC6vzEP9asLo3SGuQvAAAAAElFTkSuQmCCDQotLS0tLS0tLS0tLS16RU85alFL
bUxjMkNxODhjMjNEeDE5DQpDb250ZW50LURpc3Bvc2l0aW9uOiBmb3JtLWRhdGE7IG5hbWU9ImZp
bGUyIjsgZmlsZW5hbWU9ImF3YXJkX3N0YXJfYnJvbnplXzEucG5nIg0KQ29udGVudC1UeXBlOiBp
bWFnZS9wbmcNCg0KiVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/I
NwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAJvSURBVDjLhZNNSFRR
FIC/N++9eWMzhkl/ZJqFMQMRFvTvImkXSdKiVRAURBRRW1eZA9EqaNOiFlZEtQxKyrJwUS0K+qEQ
zaTE/AtLHR3HmffuvafFNINDWGdz7z2c7+Nyzr2WiFAIffaMBDW1+B0diAgYgxiDiCDG4DU1QfcL
os+fWAXGYUGIUsXiAliUFER+sBAhVCIIVB7QGtEat1oTbcwVz2LMfwR+gPg+oY0bEa3x6sHdUoVd
niMUj0M2i/j+PwVJa2QUu7YWp34D7mqNWdNApD6Ks24dpvcL4gfJRQXevbutjI4lGRzCS9iYukPo
5dvxVqWQvn6k/2uyoudd60LGEhG43VBGyI4j2ADZ7vDJ8DZ9Img4hw4cvO/3UZ1vH3p7lrWRLwGV
neD4y6G84NaOYSoTVYIFIiAGvXI3OWctJv0TW03jZb5gZSfzl9YBpMcIzUwdzQsuVR9EyR3TeCqm
6w5jZiZQMz8xsxOYzDTi50AMVngJNgrnUweRbwMPiLpHrOJDOl9Vh6HD7GyO52qa0VPj6MwUJpNC
5mYQS/DUJLH3zzRp1cqN8YulTUyODBBzt4X6Ou870z2I8ZHsHJLLYNQ8jusQ6+2exJf9BfivKdAy
mKZiaVdodhBRAagAjIbgzxp20lwb6Vp0jADYkQO6IpHfuoqInSJUVoE2HrpyRQ1tic2LC9p3lSHW
Ph2rJfL1MeVP2weWvHp8s3ziNZ49i1q6HrR1YHGBNnt1dG2Z++gC4TdvrqNkK1eHj7ljQ/ujHx6N
yPw8BFIiKPmNpKar7P7xb/zyT9P+o7OYvzzYSUt8U+TzxytodixEfgN3CFlQMNAcMgAAAABJRU5E
rkJggg0KLS0tLS0tLS0tLS0tekVPOWpRS21MYzJDcTg4YzIzRHgxOQ0KQ29udGVudC1EaXNwb3Np
dGlvbjogZm9ybS1kYXRhOyBuYW1lPSJ0ZXh0Ig0KDQpibGFmYXNlbCDDtsOkw7wNCi0tLS0tLS0t
LS0tLXpFTzlqUUttTGMyQ3E4OGMyM0R4MTktLQ0K`
),
boundary: '----------zEO9jQKmLc2Cq88c23Dx19',
files: {
file1: [
'arrow_branch.png',
'image/png',
b64decode(
`
iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0
U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHYSURBVDjLlVLPS1RxHJynpVu7KEn0Vt+2
l6IO5qGCIsIwCPwD6hTUaSk6REoUHeoQ0qVAMrp0COpY0SUIPVRgSl7ScCUTst6zIoqg0y7lvpnP
t8MWKuuu29w+hxnmx8dzzmE5+l7mxk1u/a3Dd/ejDjSsII/m3vjJ9MF0yt93ZuTkdD0CnnMO/WOn
msxsJp3yd2zfvA3mHOa+zuHTjy/zojrvHX1YqunAZE9MlpUcZAaZQBNIZUg9XdPBP5wePuEO7eyG
QXg29QL3jz3y1oqwbvkhCuYEOQMp/HeJohCbICMUVwr0DvZcOnK9u7GmQNmBQLJCgORxkneqRmAs
0BFmDi0bW9E72PPda/BikwWi0OEHkNR14MrewsTAZF+lAAWZEH6LUCwUkUlntrS1tiG5IYlEc6Lc
jYjSYuncngtdhakbM5dXlhgTNEMYLqB9q49MKgsPjTBXntVgkDNIgmI1VY2Q7QzgJ9rx++ci3ofz
iBYiiELQEUAyhB/D29M3Zy+uIkDIhGYvgeKvIkbHxz6Tevzq6ut+ANh9fldetMn80OzZVVdgLFjB
Q0tpEz68jcB4ifx3pQeictVXIEETnBPCKMLEwBIZAPJD767V/ETGwsjzYYiC6vzEP9asLo3SGuQv
AAAAAElFTkSuQmCC`
)
],
file2: [
'award_star_bronze_1.png',
'image/png',
b64decode(
`
iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0
U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAJvSURBVDjLhZNNSFRRFIC/N++9eWMzhkl/
ZJqFMQMRFvTvImkXSdKiVRAURBRRW1eZA9EqaNOiFlZEtQxKyrJwUS0K+qEQzaTE/AtLHR3Hmffu
vafFNINDWGdz7z2c7+Nyzr2WiFAIffaMBDW1+B0diAgYgxiDiCDG4DU1QfcLos+fWAXGYUGIUsXi
AliUFER+sBAhVCIIVB7QGtEat1oTbcwVz2LMfwR+gPg+oY0bEa3x6sHdUoVdniMUj0M2i/j+PwVJ
a2QUu7YWp34D7mqNWdNApD6Ks24dpvcL4gfJRQXevbutjI4lGRzCS9iYukPo5dvxVqWQvn6k/2uy
oudd60LGEhG43VBGyI4j2ADZ7vDJ8DZ9Img4hw4cvO/3UZ1vH3p7lrWRLwGVneD4y6G84NaOYSoT
VYIFIiAGvXI3OWctJv0TW03jZb5gZSfzl9YBpMcIzUwdzQsuVR9EyR3TeCqm6w5jZiZQMz8xsxOY
zDTi50AMVngJNgrnUweRbwMPiLpHrOJDOl9Vh6HD7GyO52qa0VPj6MwUJpNC5mYQS/DUJLH3zzRp
1cqN8YulTUyODBBzt4X6Ou870z2I8ZHsHJLLYNQ8jusQ6+2exJf9BfivKdAymKZiaVdodhBRAagA
jIbgzxp20lwb6Vp0jADYkQO6IpHfuoqInSJUVoE2HrpyRQ1tic2LC9p3lSHWPh2rJfL1MeVP2weW
vHp8s3ziNZ49i1q6HrR1YHGBNnt1dG2Z++gC4TdvrqNkK1eHj7ljQ/ujHx6NyPw8BFIiKPmNpKar
7P7xb/zyT9P+o7OYvzzYSUt8U+TzxytodixEfgN3CFlQMNAcMgAAAABJRU5ErkJggg==`
)
]
},
forms: { text: 'blafasel öäü' }
},
'webkit3-2png1txt': {
data: b64decode(
`
LS0tLS0tV2ViS2l0Rm9ybUJvdW5kYXJ5amRTRmhjQVJrOGZ5R055Ng0KQ29udGVudC1EaXNwb3Np
dGlvbjogZm9ybS1kYXRhOyBuYW1lPSJmaWxlMSI7IGZpbGVuYW1lPSJndGstYXBwbHkucG5nIg0K
Q29udGVudC1UeXBlOiBpbWFnZS9wbmcNCg0KiVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAACN
iR0NAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAN1wAADdcBQiibeAAAABl0RVh0U29mdHdhcmUA
d3d3Lmlua3NjYXBlLm9yZ5vuPBoAAANnSURBVDiNldJ9aJVVHAfw7znPuS/PvW4405WbLWfbsBuN
bramq5Tp7mLqIFPXINlwpAitaCAPjWKgBdXzR2TBpEZoadAyCVGndttCFNxqLXORK7x3y704NlzX
zfs8d89znuf0R/fKk03xHvjCOZxzPpzzO4cIIZBuC6nsGYmRrwFMWVw0hxV+PDVH0gVDKvNSRgZf
rm5+QCISOi58pY1MXhm1uHg+rPDfabqnoxJpKQ2snf/gwgKY3ut4pfodX/lTGwokRt4AgLTAkMoK
3cz7enVJg/fyTCdGE/3gwsTo+LBu2+J82qDE6IEXyrd7YvYwbpgjyPOtQHTikvhz+NKgsNGWFhhS
WU3uwqWPBx9aRwfjPTCFgXx5JY50tumWKbaFFS7uGQypLINKZH/tukb/kN6DSSOCFfO3oqu/3biZ
iH0ZVvjF1Np7AiVG31sdXO/P8GfhqtaLbE8BqOlBZ++xuMXFbudaljxBDnNJHbZlFwF407bFh6kr
hFRW7Jcztlc9Uee5HD+DaWsCTy/YgbaOvZpl2Y1hhU87QVLxvpQpMfpzfeXuZfmLA/Rw1wdaZOS3
Pm7aNQDGJUZ/qatqKs5etIj03TiKQv8aaFOWOHRm30+nm4zS229DmVs6Ulm6OW/50iD9G1Hsqnrb
t2lNwyoXYwMAPnk4N1D4aO4qEtW6wagHeZ4SfNP1mW6Zdt1c5WEE8Lll5qKCQbdiGIh/h+JlK6Wi
xcHM4z2fb9tUtkOO6hdw3Yzi2axdON33xaxuzLSGFf7HXCA1Dav+5Nn2Kyd7DyYK5bXw0QWIJM4j
7rqGmvKd8gwZw5D+I3K8jyGhmzj366lpi4uWOz0gEUIgpDKPxGjr/VlLanZubJknXLMYiH8Pjccw
K26C27Oouu8tfHysWbs6HnkxrPATdwVTLaSyzW63+8BLzzX6H1lSSrtjBzFpRPBkZi0mrk3Z7Z2t
P5xqMiruhP0PTKL5EqMnSgKr87eUvSqPGf3Ipsux53CDpie0QFjhf90NhBDiVlJ1LaqmcqXq2l/7
aU7826E94rWjQb3iXbYXgAzAC8ADwI1//zF1OkQIAUIIBSAlc6tfpkjr52XTj4SFi937eP3MmDAB
2I5YyaT63AmyuVDHmAAQt0FOzARg/aeGhBCS3EjnCBygMwKAnXL+AdDkiZ/xYgR3AAAAAElFTkSu
QmCCDQotLS0tLS1XZWJLaXRGb3JtQm91bmRhcnlqZFNGaGNBUms4ZnlHTnk2DQpDb250ZW50LURp
c3Bvc2l0aW9uOiBmb3JtLWRhdGE7IG5hbWU9ImZpbGUyIjsgZmlsZW5hbWU9Imd0ay1uby5wbmci
DQpDb250ZW50LVR5cGU6IGltYWdlL3BuZw0KDQqJUE5HDQoaCgAAAA1JSERSAAAAFAAAABQIBgAA
AI2JHQ0AAAAEc0JJVAgICAh8CGSIAAAACXBIWXMAAA3XAAAN1wFCKJt4AAAAGXRFWHRTb2Z0d2Fy
ZQB3d3cuaW5rc2NhcGUub3Jnm+48GgAAAzVJREFUOI2tlM9rG0cUxz8zu7OzsqhtyTIONDG2g9ue
UnIwFEqCwYUeTC+99u5T/4FAKKUEeuh/4FPvOZXiWw3GpRRcGjW0h1KwLLe4juOspJUlS95frwft
CkdJbh347o95bz+8mfedVSLC/zncNwUeKnVfw4YD6yncBXCgnsJeBruPRPZf952arPCBUhUL216p
tLm0vGxmq1X3rbk5AC6CgE67nTQbjTgaDHauYOtrkfYbgV8o9SHw/crKytR7d+5YDXhzc2hjEBGy
OCZutciU4s+nT68ajcYl8MlXIj+9AnygVMXA4draWqVWqaBLJcz09ChLBBGBXHEYImlK0G5zcHDQ
juF2UakuyBa2l27dmqqWywxOTpAkIWq1iILgFWVxzOXREZVymaXFxSkL2wVHFw0w1m6urq7asF7H
sZa01SINAiQIyIp7q0XaapEEAcp1CZ884Z3VVWus3Xyo1P1xlzVsvL2wYJLTUwhDdBiiHAedL1EV
+yxCJoJkGTpJkDAkOj3l5o0b5vD4eAPYd3M7rM+WSq7qdLCAOjtD+z46y1DXgJkIZNmIHUWj3E6H
melp14H1cYUZ3J31fZyTE1zA7fVw+n0cERSg8v2RUS5pPqeArNtlZmGBwqtjY+skwYig80lXBCff
5OvANFeSxzIRojge5+j8Uu9dXOD5Pt6o41jAz1W69uznMQ8wgOf79LpdNNTHwBT22r1ebDwPt0h8
DbQAFTADGGvp9PtxCntjYAa7zW43wVpca3HyZZsJaAF0C/k+4vs0wzDJYHcMfCSyHyfJzq/n50NT
raKVwhl1H3cCpAsphVut8tvz58M4SXaKn8X4pFzB1lG/P2gOBuhaDYxBJhqR5e8Yg56f53gwoNHr
Da9gq+CMz7JSauoz+HgFvr1trX+vXPZKUYSbJCMTA+K6xMYw8Dx+7Pfjw+Fw+Dt8/h38ALwQkeg6
cAaoLcLyp/BlVam1dz3PWdDaqbkjdwVpymmaZn9FUXouUn8M3zyDJvAC+PclYA6dBmpA5SO4dxM+
mIf3fVgCGMLfz+CPf+CXPfgZCIFz4ExEkpeWfH0opZzcKYUsI38nIy5D4BK4kgnAfwLblOaQdQsS
AAAAAElFTkSuQmCCDQotLS0tLS1XZWJLaXRGb3JtQm91bmRhcnlqZFNGaGNBUms4ZnlHTnk2DQpD
b250ZW50LURpc3Bvc2l0aW9uOiBmb3JtLWRhdGE7IG5hbWU9InRleHQiDQoNCnRoaXMgaXMgYW5v
dGhlciB0ZXh0IHdpdGggw7xtbMOkw7x0cw0KLS0tLS0tV2ViS2l0Rm9ybUJvdW5kYXJ5amRTRmhj
QVJrOGZ5R055Ni0tDQo=`
),
boundary: '----WebKitFormBoundaryjdSFhcARk8fyGNy6',
files: {
file1: [
'gtk-apply.png',
'image/png',
b64decode(
`
iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAACNiR0NAAAABHNCSVQICAgIfAhkiAAAAAlwSFlz
AAAN1wAADdcBQiibeAAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAANnSURB
VDiNldJ9aJVVHAfw7znPuS/PvW4405WbLWfbsBuNbramq5Tp7mLqIFPXINlwpAitaCAPjWKgBdXz
R2TBpEZoadAyCVGndttCFNxqLXORK7x3y704NlzXzfs8d89znuf0R/fKk03xHvjCOZxzPpzzO4cI
IZBuC6nsGYmRrwFMWVw0hxV+PDVH0gVDKvNSRgZfrm5+QCISOi58pY1MXhm1uHg+rPDfabqnoxJp
KQ2snf/gwgKY3ut4pfodX/lTGwokRt4AgLTAkMoK3cz7enVJg/fyTCdGE/3gwsTo+LBu2+J82qDE
6IEXyrd7YvYwbpgjyPOtQHTikvhz+NKgsNGWFhhSWU3uwqWPBx9aRwfjPTCFgXx5JY50tumWKbaF
FS7uGQypLINKZH/tukb/kN6DSSOCFfO3oqu/3biZiH0ZVvjF1Np7AiVG31sdXO/P8GfhqtaLbE8B
qOlBZ++xuMXFbudaljxBDnNJHbZlFwF407bFh6krhFRW7Jcztlc9Uee5HD+DaWsCTy/YgbaOvZpl
2Y1hhU87QVLxvpQpMfpzfeXuZfmLA/Rw1wdaZOS3Pm7aNQDGJUZ/qatqKs5etIj03TiKQv8aaFOW
OHRm30+nm4zS229DmVs6Ulm6OW/50iD9G1Hsqnrbt2lNwyoXYwMAPnk4N1D4aO4qEtW6wagHeZ4S
fNP1mW6Zdt1c5WEE8Lll5qKCQbdiGIh/h+JlK6WixcHM4z2fb9tUtkOO6hdw3Yzi2axdON33xaxu
zLSGFf7HXCA1Dav+5Nn2Kyd7DyYK5bXw0QWIJM4j7rqGmvKd8gwZw5D+I3K8jyGhmzj366lpi4uW
Oz0gEUIgpDKPxGjr/VlLanZubJknXLMYiH8PjccwK26C27Oouu8tfHysWbs6HnkxrPATdwVTLaSy
zW63+8BLzzX6H1lSSrtjBzFpRPBkZi0mrk3Z7Z2tP5xqMiruhP0PTKL5EqMnSgKr87eUvSqPGf3I
psux53CDpie0QFjhf90NhBDiVlJ1LaqmcqXq2l/7aU7826E94rWjQb3iXbYXgAzAC8ADwI1//zF1
OkQIAUIIBSAlc6tfpkjr52XTj4SFi937eP3MmDAB2I5YyaT63AmyuVDHmAAQt0FOzARg/aeGhBCS
3EjnCBygMwKAnXL+AdDkiZ/xYgR3AAAAAElFTkSuQmCC`
)
],
file2: [
'gtk-no.png',
'image/png',
b64decode(
`
iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAACNiR0NAAAABHNCSVQICAgIfAhkiAAAAAlwSFlz
AAAN1wAADdcBQiibeAAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAM1SURB
VDiNrZTPaxtHFMc/M7uzs7KobckyDjQxtoPbnlJyMBRKgsGFHkwvvfbuU/+BQCilBHrof+BT7zmV
4lsNxqUUXBo1tIdSsCy3uI7jrKSVJUveX68H7QpHSW4d+O6PeW8/vJn3nVUiwv853DcFHip1X8OG
A+sp3AVwoJ7CXga7j0T2X/edmqzwgVIVC9teqbS5tLxsZqtV9625OQAugoBOu500G404Ggx2rmDr
a5H2G4FfKPUh8P3KysrUe3fuWA14c3NoYxARsjgmbrXIlOLPp0+vGo3GJfDJVyI/vQJ8oFTFwOHa
2lqlVqmgSyXM9PQoSwQRgVxxGCJpStBuc3Bw0I7hdlGpLsgWtpdu3ZqqlssMTk6QJCFqtYiC4BVl
cczl0RGVcpmlxcUpC9sFRxcNMNZurq6u2rBex7GWtNUiDQIkCMiKe6tF2mqRBAHKdQmfPOGd1VVr
rN18qNT9cZc1bLy9sGCS01MIQ3QYohwHnS9RFfssQiaCZBk6SZAwJDo95eaNG+bw+HgD2HdzO6zP
lkqu6nSwgDo7Q/s+OstQ14CZCGTZiB1Fo9xOh5npadeB9XGFGdyd9X2ckxNcwO31cPp9HBEUoPL9
kVEuaT6ngKzbZWZhgcKrY2PrJMGIoPNJVwQn3+TrwDRXkscyEaI4Hufo/FLvXVzg+T7eqONYwM9V
uvbs5zEPMIDn+/S6XTTUx8AU9tq9Xmw8D7dIfA20ABUwAxhr6fT7cQp7Y2AGu81uN8FaXGtx8mWb
CWgBdAv5PuL7NMMwyWB3DHwksh8nyc6v5+dDU62ilcIZdR93AqQLKYVbrfLb8+fDOEl2ip/F+KRc
wdZRvz9oDgboWg2MQSYakeXvGIOen+d4MKDR6w2vYKvgjM+yUmrqM/h4Bb69ba1/r1z2SlGEmyQj
EwPiusTGMPA8fuz348PhcPg7fP4d/AC8EJHoOnAGqC3C8qfwZVWptXc9z1nQ2qm5I3cFacppmmZ/
RVF6LlJ/DN88gybwAvj3JWAOnQZqQOUjuHcTPpiH931YAhjC38/gj3/glz34GQiBc+BMRJKXlnx9
KKWc3CmFLCN/JyMuQ+ASuJIJwH8C25TmkHULEgAAAABJRU5ErkJggg==`
)
]
},
forms: { text: 'this is another text with ümläüts' }
},
'ie6-2png1txt': {
data: b64decode(
`
LS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS03ZDkxYjAzYTIwMTI4DQpDb250ZW50LURpc3Bv
c2l0aW9uOiBmb3JtLWRhdGE7IG5hbWU9ImZpbGUxIjsgZmlsZW5hbWU9IkM6XFB5dGhvbjI1XHd6
dGVzdFx3ZXJremV1Zy1tYWluXHRlc3RzXG11bHRpcGFydFxmaXJlZm94My0ycG5nMXR4dFxmaWxl
MS5wbmciDQpDb250ZW50LVR5cGU6IGltYWdlL3gtcG5nDQoNColQTkcNChoKAAAADUlIRFIAAAAQ
AAAAEAgGAAAAH/P/YQAAAARnQU1BAACvyDcFiukAAAAZdEVYdFNvZnR3YXJlAEFkb2JlIEltYWdl
UmVhZHlxyWU8AAABnUlEQVQ4y6VTMWvCQBS+qwEFB10KGaS1P6FDpw7SrVvzAwRRx04VCk4K6iAo
DhLXdhFcW9qhZCk4FQoW0gp2U4lQRDAUS4hJmn5Xgg2lsQ198PHu3b3vu5d3L9S2bfIf47wOer1e
wzTNtGEYBP48kUjkfsrb8BIAMb1cLovwRfi07wrYzcCr4/1/Am4FzzhzBGZeefR7E7vd7j0Iu4wY
jUYDBMfD0dBiMUQfstns3toKkHgF6EgmqqruW6bFiHcsxr70awVu63Q6NiOmUinquwfMdF1f28CV
gCRJx0jMAQ1BEFquRn7CbYVCYZVbr9dbnJMohoIh9kViu90WEW9nMpmxu4JyubyF/VEsFiNcgCPy
oyxiu7XhCPBzdU4s652VnUccbDabPLyN2C6VSmwdhFgel5DB84AJb64mEUlvmqadTKcv40gkUkUs
g1DjeZ7iRsrWgByP71T7/afxYrHIYry/eoBD9mxsaK4VRamFw2EBQknMAWGvRClNTpQJAfkCxFNg
Bmiez1ipVA4hdgQcOD/TLfylKIo3vubgL/YBnIw+ioOMLtwAAAAASUVORK5CYIINCi0tLS0tLS0t
LS0tLS0tLS0tLS0tLS0tLS0tLS0tN2Q5MWIwM2EyMDEyOA0KQ29udGVudC1EaXNwb3NpdGlvbjog
Zm9ybS1kYXRhOyBuYW1lPSJmaWxlMiI7IGZpbGVuYW1lPSJDOlxQeXRob24yNVx3enRlc3Rcd2Vy
a3pldWctbWFpblx0ZXN0c1xtdWx0aXBhcnRcZmlyZWZveDMtMnBuZzF0eHRcZmlsZTIucG5nIg0K
Q29udGVudC1UeXBlOiBpbWFnZS94LXBuZw0KDQqJUE5HDQoaCgAAAA1JSERSAAAAEAAAABAIBgAA
AB/z/2EAAAAEZ0FNQQAAr8g3BYrpAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccll
PAAAAlFJREFUGBmlwd1rzXEcwPH353d+51jO5jDZFpnnJNaiUSK5mkJKMYkUSS1RSvwDpnFBkYeY
O2p2sZRceCh5mpKnUXZssVaO2Q4Hw9nO+Z3v52O/ldoFF8vrJWbG/5CNB67uXbm65lgma3EzQBVT
xanD1FBTzDnUDHMOp8qEWPCroyN1uPVE3Rm/ZkXNqWhR3CsvYiziv7LuFHDGzwbmZTM/GavBwDyG
+eaMhm1zGavdjT2EfDMllC84DDA1nIJiqBpOFVcwXMEIPt8l+/wykeIq9pXd49XZ/Tt8zAiJJ4gZ
5gkmhqjgeYKIh4hDM9eJ9j6lomo7iVmL+dY9n+StpuO+U0fIA0wEBCIGKqBqRAwK6dvEcm+Iz1tB
5l0HMclTMqGC4smVCd/UGCECZniAiYCACOT77yM/npCYvYZcbzOx8ULPyyQDWZBcptpTdfwhIiBC
yANy6fsUvtwmMWctQx8vItGvRItLiFuGK6nlLN3X2ukVgoARIogIIRGhL3md7IebJOZuYCh1Di8a
kB+YSfphO1NqG/g4OJGQZ04JRQABRIT+5A1+pNooW7iO/KcmIjEjNzCD9KMXVGw6T1H5AkyVkK+q
/CFAV1szhe+vKchUel+fZlJZjKHMdL49S1K55QLRxDRCakbIT3X3tNSfDOrUOdQptdLE5vpLvG0+
SOeDNsZVVvO9L8WNoa30NTzGVFEl1MIwMTNGO7JnUXBoV72P53h55xo93V0/E1NKV9YebW/nL8TM
GK1uVengktnl/rIFs7Borm2wP71zfeOr9/zDb6ZFKM6WU+GQAAAAAElFTkSuQmCCDQotLS0tLS0t
LS0tLS0tLS0tLS0tLS0tLS0tLS0tLTdkOTFiMDNhMjAxMjgNCkNvbnRlbnQtRGlzcG9zaXRpb246
IGZvcm0tZGF0YTsgbmFtZT0idGV4dCINCg0KaWU2IHN1Y2tzIDotLw0KLS0tLS0tLS0tLS0tLS0t
LS0tLS0tLS0tLS0tLS03ZDkxYjAzYTIwMTI4LS0NCg==`
),
boundary: '---------------------------7d91b03a20128',
files: {
file1: [
'C:\\Python25\\wztest\\werkzeug-main\\tests\\multipart\\firefox3-2png1txt\\file1.png',
'image/x-png',
b64decode(
`
iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0
U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAGdSURBVDjLpVMxa8JAFL6rAQUHXQoZpLU/
oUOnDtKtW/MDBFHHThUKTgrqICgOEtd2EVxb2qFkKTgVChbSCnZTiVBEMBRLiEmafleCDaWxDX3w
8e7dve+7l3cv1LZt8h/jvA56vV7DNM20YRgE/jyRSOR+ytvwEgAxvVwui/BF+LTvCtjNwKvj/X8C
bgXPOHMEZl559HsTu93uPQi7jBiNRgMEx8PR0GIxRB+y2eze2gqQeAXoSCaqqu5bpsWIdyzGvvRr
BW7rdDo2I6ZSKeq7B8x0XV/bwJWAJEnHSMwBDUEQWq5GfsJthUJhlVuv11uckyiGgiH2RWK73RYR
b2cymbG7gnK5vIX9USwWI1yAI/KjLGK7teEI8HN1TizrnZWdRxxsNps8vI3YLpVKbB2EWB6XkMHz
gAlvriYRSW+app1Mpy/jSCRSRSyDUON5nuJGytaAHI/vVPv9p/FischivL96gEP2bGxorhVFqYXD
YQFCScwBYa9EKU1OlAkB+QLEU2AGaJ7PWKlUDiF2BBw4P9Mt/KUoije+5uAv9gGcjD6Kg4wu3AAA
AABJRU5ErkJggg==`
)
],
file2: [
'C:\\Python25\\wztest\\werkzeug-main\\tests\\multipart\\firefox3-2png1txt\\file2.png',
'image/x-png',
b64decode(
`
iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0
U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAJRSURBVBgZpcHda81xHMDx9+d3fudYzuYw
2RaZ5yTWolEiuZpCSjGJFEktUUr8A6ZxQZGHmDtqdrGUXHgoeZqSp1F2bLFWjtkOB8PZzvmd7+dj
v5XaBRfL6yVmxv+QjQeu7l25uuZYJmtxM0AVU8Wpw9RQU8w51AxzDqfKhFjwq6Mjdbj1RN0Zv2ZF
zaloUdwrL2Is4r+y7hRwxs8G5mUzPxmrwcA8hvnmjIZtcxmr3Y09hHwzJZQvOAwwNZyCYqgaThVX
MFzBCD7fJfv8MpHiKvaV3ePV2f07fMwIiSeIGeYJJoao4HmCiIeIQzPXifY+paJqO4lZi/nWPZ/k
rabjvlNHyANMBAQiBiqgakQMCunbxHJviM9bQeZdBzHJUzKhguLJlQnf1BghAmZ4gImAgAjk++8j
P56QmL2GXG8zsfFCz8skA1mQXKbaU3X8ISIgQsgDcun7FL7cJjFnLUMfLyLRr0SLS4hbhiup5Szd
19rpFYKAESKICCERoS95neyHmyTmbmAodQ4vGpAfmEn6YTtTahv4ODiRkGdOCUUAAUSE/uQNfqTa
KFu4jvynJiIxIzcwg/SjF1RsOk9R+QJMlZCvqvwhQFdbM4XvrynIVHpfn2ZSWYyhzHS+PUtSueUC
0cQ0QmpGyE9197TUnwzq1DnUKbXSxOb6S7xtPkjngzbGVVbzvS/FjaGt9DU8xlRRJdTCMDEzRjuy
Z1FwaFe9j+d4eecaPd1dPxNTSlfWHm1v5y/EzBitblXp4JLZ5f6yBbOwaK5tsD+9c33jq/f8w2+m
RSjOllPhkAAAAABJRU5ErkJggg==`
)
]
},
forms: { text: 'ie6 sucks :-/' }
}
}
const legacyBrowserTestCases = {
'firefox3-2png1txt': {
data: [
'-----------------------------186454651713519341951581030105\r\n',
'Content-Disposition: form-data; name="file1"; filename="anchor.png"\r\n',
'Content-Type: image/png\r\n',
'\r\n',
'[file1-data]\r\n',
'-----------------------------186454651713519341951581030105\r\n',
'Content-Disposition: form-data; name="file2"; filename="application_edit.png"\r\n',
'Content-Type: image/png\r\n',
'\r\n',
'[file2-data]\r\n',
'-----------------------------186454651713519341951581030105\r\n',
'Content-Disposition: form-data; name="text"\r\n',
'\r\n',
'[Text]\r\n',
'-----------------------------186454651713519341951581030105--\r\n'
].join(''),
boundary: '---------------------------186454651713519341951581030105',
files: {
file1: ['anchor.png', 'image/png', '[file1-data]'],
file2: ['application_edit.png', 'image/png', '[file2-data]']
},
forms: { text: '[Text]' }
},
'opera8-2png1txt': {
data: [
'------------zEO9jQKmLc2Cq88c23Dx19\r\n',
'Content-Disposition: form-data; name="file1"; filename="arrow_branch.png"\r\n',
'Content-Type: image/png\r\n',
'\r\n',
'[file1-data]\r\n',
'------------zEO9jQKmLc2Cq88c23Dx19\r\n',
'Content-Disposition: form-data; name="file2"; filename="award_star_bronze_1.png"\r\n',
'Content-Type: image/png\r\n',
'\r\n',
'[file2-data]\r\n',
'------------zEO9jQKmLc2Cq88c23Dx19\r\n',
'Content-Disposition: form-data; name="text"\r\n',
'\r\n',
'[Text]\r\n',
'------------zEO9jQKmLc2Cq88c23Dx19--\r\n'
].join(''),
boundary: '----------zEO9jQKmLc2Cq88c23Dx19',
files: {
file1: ['arrow_branch.png', 'image/png', '[file1-data]'],
file2: ['award_star_bronze_1.png', 'image/png', '[file2-data]']
},
forms: { text: '[Text]' }
},
'webkit3-2png1txt': {
data: [
'------WebKitFormBoundaryjdSFhcARk8fyGNy6\r\n',
'Content-Disposition: form-data; name="file1"; filename="gtk-apply.png"\r\n',
'Content-Type: image/png\r\n',
'\r\n',
'[file1-data]\r\n',
'------WebKitFormBoundaryjdSFhcARk8fyGNy6\r\n',
'Content-Disposition: form-data; name="file2"; filename="gtk-no.png"\r\n',
'Content-Type: image/png\r\n',
'\r\n',
'[file2-data]\r\n',
'------WebKitFormBoundaryjdSFhcARk8fyGNy6\r\n',
'Content-Disposition: form-data; name="text"\r\n',
'\r\n',
'[Text]\r\n',
'------WebKitFormBoundaryjdSFhcARk8fyGNy6--\r\n'
].join(''),
boundary: '----WebKitFormBoundaryjdSFhcARk8fyGNy6',
files: {
file1: ['gtk-apply.png', 'image/png', '[file1-data]'],
file2: ['gtk-no.png', 'image/png', '[file2-data]']
},
forms: { text: '[Text]' }
},
'ie6-2png1txt': {
data: [
'-----------------------------7d91b03a20128\r\n',
'Content-Disposition: form-data; name="file1"; filename="C:\\Python25\\wztest\\werkzeug-main\\tests\\multipart\\firefox3-2png1txt\\file1.png"\r\n',
'Content-Type: image/x-png\r\n',
'\r\n',
'[file1-data]\r\n',
'-----------------------------7d91b03a20128\r\n',
'Content-Disposition: form-data; name="file2"; filename="C:\\Python25\\wztest\\werkzeug-main\\tests\\multipart\\firefox3-2png1txt\\file2.png"\r\n',
'Content-Type: image/x-png\r\n',
'\r\n',
'[file2-data]\r\n',
'-----------------------------7d91b03a20128\r\n',
'Content-Disposition: form-data; name="text"\r\n',
'\r\n',
'[Text]\r\n',
'-----------------------------7d91b03a20128--\r\n'
].join(''),
boundary: '---------------------------7d91b03a20128',
files: {
file1: ['C:\\Python25\\wztest\\werkzeug-main\\tests\\multipart\\firefox3-2png1txt\\file1.png', 'image/x-png', '[file1-data]'],
file2: ['C:\\Python25\\wztest\\werkzeug-main\\tests\\multipart\\firefox3-2png1txt\\file2.png', 'image/x-png', '[file2-data]']
},
forms: { text: '[Text]' }
}
}
test('legacy tests', async (t) => {
for (const [name, body] of Object.entries(legacyTests)) {
await t.test(`parsing ${name}`, async (t) => {
const response = makeResponse(body.join(''), body[0].slice(2).trim())
await t.assert.doesNotReject(response.formData())
})
}
})
test('browser tests', async (t) => {
for (const [name, value] of Object.entries(browserTestCases)) {
await t.test(`parsing ${name}`, async (t) => {
const response = makeResponse(value.data, value.boundary)
const fd = await response.formData()
for (const [key, val] of fd.entries()) {
if (key in value.files) {
const [fileName, mimeType, body] = value.files[key]
t.assert.ok(val instanceof File)
t.assert.deepStrictEqual(fileName, val.name)
t.assert.deepStrictEqual(mimeType, val.type)
t.assert.deepStrictEqual(body, await val.text())
} else {
const expected = value.forms[key]
t.assert.deepStrictEqual(val, expected)
}
}
})
}
})
test('legacy browser tests', async (t) => {
for (const [name, value] of Object.entries(legacyBrowserTestCases)) {
await t.test(`parsing ${name}`, async (t) => {
const response = makeResponse(value.data, value.boundary)
const fd = await response.formData()
for (const [key, val] of fd.entries()) {
if (key in value.files) {
const [fileName, mimeType, body] = value.files[key]
t.assert.ok(val instanceof File)
t.assert.deepStrictEqual(fileName, val.name)
t.assert.deepStrictEqual(mimeType, val.type)
t.assert.deepStrictEqual(body, await val.text())
} else {
const expected = value.forms[key]
t.assert.deepStrictEqual(val, expected)
}
}
})
}
})
================================================
FILE: test/busboy/test-types-multipart-charsets.js
================================================
'use strict'
const { inspect } = require('node:util')
const { test } = require('node:test')
const { Response } = require('../..')
const input = Buffer.from([
'-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k',
'Content-Disposition: form-data; ' +
'name="upload_file_0"; filename="テスト.dat"',
'Content-Type: application/octet-stream',
'',
'A'.repeat(1023),
'-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k--'
].join('\r\n'))
const boundary = '---------------------------paZqsnEHRufoShdX6fh0lUhXBP4k'
const expected = [
{
type: 'file',
name: 'upload_file_0',
data: Buffer.from('A'.repeat(1023)),
info: {
filename: 'テスト.dat',
encoding: '7bit',
mimeType: 'application/octet-stream'
}
}
]
test('unicode filename', async (t) => {
const response = new Response(input, {
headers: {
'content-type': `multipart/form-data; boundary=${boundary}`
}
})
const fd = await response.formData()
const results = []
for (const [name, value] of fd) {
if (typeof value === 'string') { // field
results.push({
type: 'field',
name,
val: value,
info: {
encoding: '7bit',
mimeType: 'text/plain'
}
})
} else { // File
results.push({
type: 'file',
name,
data: Buffer.from(await value.arrayBuffer()),
info: {
filename: value.name,
encoding: '7bit',
mimeType: value.type
}
})
}
}
t.assert.deepStrictEqual(
results,
expected,
'Results mismatch.\n' +
`Parsed: ${inspect(results)}\n` +
`Expected: ${inspect(expected)}`
)
})
================================================
FILE: test/busboy/test-types-multipart.js
================================================
'use strict'
const { inspect } = require('node:util')
const { test } = require('node:test')
const { Response } = require('../..')
const active = new Map()
const tests = [
{
source: [
['-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k',
'Content-Disposition: form-data; name="file_name_0"',
'',
'super alpha file',
'-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k',
'Content-Disposition: form-data; name="file_name_1"',
'',
'super beta file',
'-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k',
'Content-Disposition: form-data; ' +
'name="upload_file_0"; filename="1k_a.dat"',
'Content-Type: application/octet-stream',
'',
'A'.repeat(1023),
'-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k',
'Content-Disposition: form-data; ' +
'name="upload_file_1"; filename="1k_b.dat"',
'Content-Type: application/octet-stream',
'',
'B'.repeat(1023),
'-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k--'
].join('\r\n')
],
boundary: '---------------------------paZqsnEHRufoShdX6fh0lUhXBP4k',
expected: [
{
type: 'field',
name: 'file_name_0',
val: 'super alpha file',
info: {
encoding: '7bit',
mimeType: 'text/plain'
}
},
{
type: 'field',
name: 'file_name_1',
val: 'super beta file',
info: {
encoding: '7bit',
mimeType: 'text/plain'
}
},
{
type: 'file',
name: 'upload_file_0',
data: Buffer.from('A'.repeat(1023)),
info: {
filename: '1k_a.dat',
encoding: '7bit',
mimeType: 'application/octet-stream'
}
},
{
type: 'file',
name: 'upload_file_1',
data: Buffer.from('B'.repeat(1023)),
info: {
filename: '1k_b.dat',
encoding: '7bit',
mimeType: 'application/octet-stream'
}
}
],
what: 'Fields and files'
},
{
source: [
['------WebKitFormBoundaryTB2MiQ36fnSJlrhY',
'Content-Disposition: form-data; name="cont"',
'',
'some random content',
'------WebKitFormBoundaryTB2MiQ36fnSJlrhY',
'Content-Disposition: form-data; name="pass"',
'',
'some random pass',
'------WebKitFormBoundaryTB2MiQ36fnSJlrhY',
'Content-Disposition: form-data; name="bit"',
'',
'2',
'------WebKitFormBoundaryTB2MiQ36fnSJlrhY--'
].join('\r\n')
],
boundary: '----WebKitFormBoundaryTB2MiQ36fnSJlrhY',
expected: [
{
type: 'field',
name: 'cont',
val: 'some random content',
info: {
encoding: '7bit',
mimeType: 'text/plain'
}
},
{
type: 'field',
name: 'pass',
val: 'some random pass',
info: {
encoding: '7bit',
mimeType: 'text/plain'
}
},
{
type: 'field',
name: 'bit',
val: '2',
info: {
encoding: '7bit',
mimeType: 'text/plain'
}
}
],
what: 'Fields only'
},
{
source: [
''
],
boundary: '----WebKitFormBoundaryTB2MiQ36fnSJlrhY',
expected: [
{ error: 'Unexpected end of form' }
],
what: 'No fields and no files'
},
{
source: [
['-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k',
'Content-Disposition: form-data; ' +
'name="upload_file_0"; filename="/tmp/1k_a.dat"',
'Content-Type: application/octet-stream',
'',
'ABCDEFGHIJKLMNOPQRSTUVWXYZ',
'-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k',
'Content-Disposition: form-data; ' +
'name="upload_file_1"; filename="C:\\files\\1k_b.dat"',
'Content-Type: application/octet-stream',
'',
'ABCDEFGHIJKLMNOPQRSTUVWXYZ',
'-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k',
'Content-Disposition: form-data; ' +
'name="upload_file_2"; filename="relative/1k_c.dat"',
'Content-Type: application/octet-stream',
'',
'ABCDEFGHIJKLMNOPQRSTUVWXYZ',
'-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k--'
].join('\r\n')
],
boundary: '---------------------------paZqsnEHRufoShdX6fh0lUhXBP4k',
expected: [
{
type: 'file',
name: 'upload_file_0',
data: Buffer.from('ABCDEFGHIJKLMNOPQRSTUVWXYZ'),
info: {
filename: '/tmp/1k_a.dat',
encoding: '7bit',
mimeType: 'application/octet-stream'
}
},
{
type: 'file',
name: 'upload_file_1',
data: Buffer.from('ABCDEFGHIJKLMNOPQRSTUVWXYZ'),
info: {
filename: 'C:\\files\\1k_b.dat',
encoding: '7bit',
mimeType: 'application/octet-stream'
}
},
{
type: 'file',
name: 'upload_file_2',
data: Buffer.from('ABCDEFGHIJKLMNOPQRSTUVWXYZ'),
info: {
filename: 'relative/1k_c.dat',
encoding: '7bit',
mimeType: 'application/octet-stream'
}
}
],
what: 'Files with filenames containing paths preserve path'
},
{
source: [
['-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k',
'Content-Disposition: form-data; ' +
'name="upload_file_0"; filename="/absolute/1k_a.dat"',
'Content-Type: application/octet-stream',
'',
'ABCDEFGHIJKLMNOPQRSTUVWXYZ',
'-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k',
'Content-Disposition: form-data; ' +
'name="upload_file_1"; filename="C:\\absolute\\1k_b.dat"',
'Content-Type: application/octet-stream',
'',
'ABCDEFGHIJKLMNOPQRSTUVWXYZ',
'-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k',
'Content-Disposition: form-data; ' +
'name="upload_file_2"; filename="relative/1k_c.dat"',
'Content-Type: application/octet-stream',
'',
'ABCDEFGHIJKLMNOPQRSTUVWXYZ',
'-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k--'
].join('\r\n')
],
boundary: '---------------------------paZqsnEHRufoShdX6fh0lUhXBP4k',
expected: [
{
type: 'file',
name: 'upload_file_0',
data: Buffer.from('ABCDEFGHIJKLMNOPQRSTUVWXYZ'),
info: {
filename: '/absolute/1k_a.dat',
encoding: '7bit',
mimeType: 'application/octet-stream'
}
},
{
type: 'file',
name: 'upload_file_1',
data: Buffer.from('ABCDEFGHIJKLMNOPQRSTUVWXYZ'),
info: {
filename: 'C:\\absolute\\1k_b.dat',
encoding: '7bit',
mimeType: 'application/octet-stream'
}
},
{
type: 'file',
name: 'upload_file_2',
data: Buffer.from('ABCDEFGHIJKLMNOPQRSTUVWXYZ'),
info: {
filename: 'relative/1k_c.dat',
encoding: '7bit',
mimeType: 'application/octet-stream'
}
}
],
what: 'Paths to be preserved'
},
{
source: [
['------WebKitFormBoundaryTB2MiQ36fnSJlrhY',
'Content-Disposition: form-data; name="cont"',
'Content-Type: ',
'',
'some random content',
'------WebKitFormBoundaryTB2MiQ36fnSJlrhY--'
].join('\r\n')
],
boundary: '----WebKitFormBoundaryTB2MiQ36fnSJlrhY',
expected: [
{
type: 'field',
name: 'cont',
val: 'some random content',
info: {
encoding: '7bit',
mimeType: 'text/plain'
}
}
],
what: 'Empty content-type defaults to text/plain'
},
{
source: [
['-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k',
'Content-Disposition: form-data; ' +
'name="file"; filename*="utf-8\'\'n%C3%A4me.txt"',
'Content-Type: application/octet-stream',
'',
'ABCDEFGHIJKLMNOPQRSTUVWXYZ',
'-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k--'
].join('\r\n')
],
boundary: '---------------------------paZqsnEHRufoShdX6fh0lUhXBP4k',
expected: [
{
type: 'file',
name: 'file',
data: Buffer.from('ABCDEFGHIJKLMNOPQRSTUVWXYZ'),
info: {
filename: 'utf-8\'\'n%C3%A4me.txt',
encoding: '7bit',
mimeType: 'application/octet-stream'
}
}
],
what: 'Unicode filenames'
},
{
source: [
['--asdasdasdasd\r\n',
'Content-Type: text/plain\r\n',
'Content-Disposition: form-data; name="foo"\r\n',
'\r\n',
'asd\r\n',
'--asdasdasdasd--'
].join(':)')
],
boundary: 'asdasdasdasd',
expected: [
{ error: 'Malformed part header' }
],
what: 'Stopped mid-header'
},
{
source: [
['------WebKitFormBoundaryTB2MiQ36fnSJlrhY',
'Content-Disposition: form-data; name="cont"',
'Content-Type: application/json',
'',
'{}',
'------WebKitFormBoundaryTB2MiQ36fnSJlrhY--'
].join('\r\n')
],
boundary: '----WebKitFormBoundaryTB2MiQ36fnSJlrhY',
expected: [
{
type: 'field',
name: 'cont',
val: '{}',
info: {
encoding: '7bit',
// TODO: there's no way to get the content-type of a field
mimeType: 'text/plain' // 'application/json'
}
}
],
what: 'content-type for fields'
},
{
source: [
'------WebKitFormBoundaryTB2MiQ36fnSJlrhY--'
],
boundary: '----WebKitFormBoundaryTB2MiQ36fnSJlrhY',
expected: [],
what: 'empty form'
},
{
source: [
['-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k',
'Content-Disposition: form-data; ' +
'name=upload_file_0; filename="1k_a.dat"',
'Content-Type: application/octet-stream',
'Content-Transfer-Encoding: binary',
'',
''
].join('\r\n')
],
boundary: '---------------------------paZqsnEHRufoShdX6fh0lUhXBP4k',
expected: [
{ error: 'Unexpected end of form' }
],
what: 'Stopped mid-file #1'
},
{
source: [
['-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k',
'Content-Disposition: form-data; ' +
'name=upload_file_0; filename="1k_a.dat"',
'Content-Type: application/octet-stream',
'',
'a'
].join('\r\n')
],
boundary: '---------------------------paZqsnEHRufoShdX6fh0lUhXBP4k',
expected: [
{ error: 'Unexpected end of form' }
],
what: 'Stopped mid-file #2'
},
{
source: [
['-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k',
'Content-Disposition: form-data; ' +
'name="upload_file_0"; filename="notes.txt"',
'Content-Type: text/plain; charset=utf8',
'',
'a',
'-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k--'
].join('\r\n')
],
boundary: '---------------------------paZqsnEHRufoShdX6fh0lUhXBP4k',
expected: [
{
type: 'file',
name: 'upload_file_0',
data: Buffer.from('a'),
info: {
filename: 'notes.txt',
encoding: '7bit',
mimeType: 'text/plain; charset=utf8'
}
}
],
what: 'Text file with charset'
},
{
source: [
['-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k',
'Content-Disposition: form-data; ' +
`name="upload_file_0"; filename="${'a'.repeat(64 * 1024)}.txt"`,
'Content-Type: text/plain; charset=utf8',
'',
'ab',
'-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k',
'Content-Disposition: form-data; ' +
'name="upload_file_1"; filename="notes2.txt"',
'Content-Type: text/plain; charset=utf8',
'',
'cd',
'-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k--'
].join('\r\n')
],
boundary: '---------------------------paZqsnEHRufoShdX6fh0lUhXBP4k',
expected: [
// TODO: the RFC does not mention the max size of a filename?
{
type: 'file',
name: 'upload_file_0',
data: Buffer.from('ab'),
info: {
filename: `${'a'.repeat(64 * 1024)}.txt`,
encoding: '7bit',
mimeType: 'text/plain; charset=utf8'
}
}, // { error: 'Malformed part header' },
{
type: 'file',
name: 'upload_file_1',
data: Buffer.from('cd'),
info: {
filename: 'notes2.txt',
encoding: '7bit',
mimeType: 'text/plain; charset=utf8'
}
}
],
what: 'Oversized part header'
},
{
source: [
['-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k',
'Content-Disposition: form-data; ' +
'name="upload_file_0"; filename="notes.txt"',
'Content-Type: text/plain; charset=utf8',
'',
'a'.repeat(31) + '\r'
].join('\r\n'),
'b'.repeat(40),
'\r\n-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k--'
],
boundary: '---------------------------paZqsnEHRufoShdX6fh0lUhXBP4k',
expected: [
{
type: 'file',
name: 'upload_file_0',
data: Buffer.from('a'.repeat(31) + '\r' + 'b'.repeat(40)),
info: {
filename: 'notes.txt',
encoding: '7bit',
mimeType: 'text/plain; charset=utf8'
}
}
],
what: 'Lookbehind data should not stall file streams'
},
{
source: [
['-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k',
'Content-Disposition: form-data; ' +
`name="upload_file_0"; filename="${'a'.repeat(8 * 1024)}.txt"`,
'Content-Type: text/plain; charset=utf8',
'',
'ab',
'-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k',
'Content-Disposition: form-data; ' +
`name="upload_file_1"; filename="${'b'.repeat(8 * 1024)}.txt"`,
'Content-Type: text/plain; charset=utf8',
'',
'cd',
'-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k',
'Content-Disposition: form-data; ' +
`name="upload_file_2"; filename="${'c'.repeat(8 * 1024)}.txt"`,
'Content-Type: text/plain; charset=utf8',
'',
'ef',
'-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k--'
].join('\r\n')
],
boundary: '---------------------------paZqsnEHRufoShdX6fh0lUhXBP4k',
expected: [
{
type: 'file',
name: 'upload_file_0',
data: Buffer.from('ab'),
info: {
filename: `${'a'.repeat(8 * 1024)}.txt`,
encoding: '7bit',
mimeType: 'text/plain; charset=utf8'
}
},
{
type: 'file',
name: 'upload_file_1',
data: Buffer.from('cd'),
info: {
filename: `${'b'.repeat(8 * 1024)}.txt`,
encoding: '7bit',
mimeType: 'text/plain; charset=utf8'
}
},
{
type: 'file',
name: 'upload_file_2',
data: Buffer.from('ef'),
info: {
filename: `${'c'.repeat(8 * 1024)}.txt`,
encoding: '7bit',
mimeType: 'text/plain; charset=utf8'
}
}
],
what: 'Large filename'
},
{
source: [
'\r\n--d1bf46b3-aa33-4061-b28d-6c5ced8b08ee\r\n',
'Content-Type: application/gzip\r\n' +
'Content-Encoding: gzip\r\n' +
'Content-Disposition: form-data; name="batch-1"; filename="batch-1"' +
'\r\n\r\n' +
'\r\n--d1bf46b3-aa33-4061-b28d-6c5ced8b08ee--'
],
boundary: 'd1bf46b3-aa33-4061-b28d-6c5ced8b08ee',
expected: [
{
type: 'file',
name: 'batch-1',
data: Buffer.alloc(0),
info: {
filename: 'batch-1',
encoding: '7bit',
mimeType: 'application/gzip'
}
}
],
what: 'Empty part'
}
]
test('FormData parsing tests', async (t) => {
for (const test of tests) {
active.set(test, 1)
const { what, boundary, source } = test
const body = source.reduce((a, b) => a + b, '')
const response = new Response(body, {
headers: {
'content-type': `multipart/form-data; boundary=${boundary}`
}
})
let fd
const results = []
try {
fd = await response.formData()
} catch (e) {
results.push({ error: e.message })
if (test.expected.length === 1 && test.expected[0].error) {
active.delete(test)
}
continue
}
for (const [name, value] of fd) {
if (typeof value === 'string') { // field
results.push({
type: 'field',
name,
val: value,
info: {
encoding: '7bit',
mimeType: 'text/plain'
}
})
} else { // File
results.push({
type: 'file',
name,
data: Buffer.from(await value.arrayBuffer()),
info: {
filename: value.name,
encoding: '7bit',
mimeType: value.type
}
})
}
}
active.delete(test)
t.assert.deepStrictEqual(
results,
test.expected,
`[${what}] Results mismatch.\n` +
`Parsed: ${inspect(results)}\n` +
`Expected: ${inspect(test.expected)}`
)
}
})
================================================
FILE: test/cache/cache.js
================================================
'use strict'
const { test } = require('node:test')
const { Cache } = require('../../lib/web/cache/cache')
const { caches, Response } = require('../../')
test('constructor', (t) => {
t.assert.throws(() => new Cache(null), {
name: 'TypeError',
message: 'TypeError: Illegal constructor'
})
})
// https://github.com/nodejs/undici/issues/4710
test('cache.match should work after garbage collection', async (t) => {
const cache = await caches.open('test-gc-cache')
t.after(async () => {
await caches.delete('test-gc-cache')
})
const url = 'https://example.com/test-gc'
const testData = { answer: 42 }
await cache.put(url, Response.json(testData))
// Call match multiple times with GC pressure between calls
// The bug manifests when the temporary Response object from fromInnerResponse()
// is garbage collected, which triggers the FinalizationRegistry to cancel
// the cached stream.
for (let i = 0; i < 20; i++) {
// Create significant memory pressure to trigger GC
// eslint-disable-next-line no-unused-vars
const garbage = Array.from({ length: 30000 }, () => ({ value: Math.random() }))
// Force GC if available (run with --expose-gc)
if (global.gc) {
global.gc()
}
// Delay to allow FinalizationRegistry callbacks to run
// The bug requires time for the GC to collect the temporary Response
// and for the finalization callback to cancel the stream
await new Promise((resolve) => setTimeout(resolve, 10))
// This should not throw "Body has already been consumed"
const match = await cache.match(url)
t.assert.ok(match, `Iteration ${i}: match should return a response`)
const result = await match.json()
t.assert.deepStrictEqual(result, testData, `Iteration ${i}: response body should match`)
}
})
================================================
FILE: test/cache/cachestorage.js
================================================
'use strict'
const { test } = require('node:test')
const { CacheStorage } = require('../../lib/web/cache/cachestorage')
test('constructor', (t) => {
t.assert.throws(() => new CacheStorage(null), {
name: 'TypeError',
message: 'TypeError: Illegal constructor'
})
})
================================================
FILE: test/cache/get-field-values.js
================================================
'use strict'
const { test } = require('node:test')
const { getFieldValues } = require('../../lib/web/cache/util')
test('getFieldValues', (t) => {
t.assert.throws(() => getFieldValues(null), {
name: 'AssertionError',
message: 'The expression evaluated to a falsy value:\n\n assert(header !== null)\n'
})
t.assert.deepStrictEqual(getFieldValues(''), [])
t.assert.deepStrictEqual(getFieldValues('foo'), ['foo'])
t.assert.deepStrictEqual(getFieldValues('invälid'), [])
t.assert.deepStrictEqual(getFieldValues('foo, bar'), ['foo', 'bar'])
t.assert.deepStrictEqual(getFieldValues('foo, bar, baz'), ['foo', 'bar', 'baz'])
t.assert.deepStrictEqual(getFieldValues('foo, bar, baz, '), ['foo', 'bar', 'baz'])
t.assert.deepStrictEqual(getFieldValues('foo, bar, baz, , '), ['foo', 'bar', 'baz'])
})
================================================
FILE: test/cache-interceptor/cache-store-test-utils.js
================================================
'use strict'
const { equal, notEqual, deepStrictEqual } = require('node:assert')
const { describe, test, after } = require('node:test')
const { Readable } = require('node:stream')
const { once } = require('node:events')
const FakeTimers = require('@sinonjs/fake-timers')
/**
* @typedef {import('../../types/cache-interceptor.d.ts').default.CacheStore} CacheStore
*
* @param {{ new(...any): CacheStore }} CacheStore
* @param {object} [options]
* @param {boolean} [options.skip]
*/
function cacheStoreTests (CacheStore, options) {
describe(CacheStore.prototype.constructor.name, () => {
test('matches interface', options, () => {
equal(typeof CacheStore.prototype.get, 'function')
equal(typeof CacheStore.prototype.createWriteStream, 'function')
equal(typeof CacheStore.prototype.delete, 'function')
})
test('caches request', options, async () => {
/**
* @type {import('../../types/cache-interceptor.d.ts').default.CacheKey}
*/
const key = {
origin: 'localhost',
path: '/',
method: 'GET',
headers: {}
}
/**
* @type {import('../../types/cache-interceptor.d.ts').default.CacheValue}
*/
const value = {
statusCode: 200,
statusMessage: '',
headers: { foo: 'bar' },
cacheControlDirectives: {},
cachedAt: Date.now(),
staleAt: Date.now() + 10000,
deleteAt: Date.now() + 20000
}
const body = [Buffer.from('asd'), Buffer.from('123')]
const store = new CacheStore()
// Sanity check
equal(await store.get(key), undefined)
// Write response to store
{
const writable = store.createWriteStream(key, value)
notEqual(writable, undefined)
writeBody(writable, body)
}
// Now let's try fetching the response from the store
{
const result = await store.get(structuredClone(key))
notEqual(result, undefined)
await compareGetResults(result, value, body)
}
/**
* Let's try out a request to a different resource to make sure it can
* differentiate between the two
* @type {import('../../types/cache-interceptor.d.ts').default.CacheKey}
*/
const anotherKey = {
origin: 'localhost',
path: '/asd',
method: 'GET',
headers: {}
}
/**
* @type {import('../../types/cache-interceptor.d.ts').default.CacheValue}
*/
const anotherValue = {
statusCode: 200,
statusMessage: '',
headers: { foo: 'bar' },
cacheControlDirectives: {},
cachedAt: Date.now(),
staleAt: Date.now() + 10000,
deleteAt: Date.now() + 20000
}
const anotherBody = [Buffer.from('asd'), Buffer.from('123')]
equal(store.get(anotherKey), undefined)
{
const writable = store.createWriteStream(anotherKey, anotherValue)
notEqual(writable, undefined)
writeBody(writable, anotherBody)
}
{
const result = await store.get(structuredClone(anotherKey))
notEqual(result, undefined)
await compareGetResults(result, anotherValue, anotherBody)
}
})
test('returns stale response before deleteAt', options, async () => {
const clock = FakeTimers.install({
shouldClearNativeTimers: true
})
after(() => clock.uninstall())
/**
* @type {import('../../types/cache-interceptor.d.ts').default.CacheKey}
*/
const key = {
origin: 'localhost',
path: '/',
method: 'GET',
headers: {}
}
/**
* @type {import('../../types/cache-interceptor.d.ts').default.CacheValue}
*/
const value = {
statusCode: 200,
statusMessage: '',
headers: { foo: 'bar' },
cacheControlDirectives: {},
cachedAt: Date.now(),
staleAt: Date.now() + 1000,
// deleteAt is different because stale-while-revalidate, stale-if-error, ...
deleteAt: Date.now() + 5000
}
const body = [Buffer.from('asd'), Buffer.from('123')]
const store = new CacheStore()
// Sanity check
equal(store.get(key), undefined)
{
const writable = store.createWriteStream(key, value)
notEqual(writable, undefined)
writeBody(writable, body)
}
clock.tick(1500)
{
const result = await store.get(structuredClone(key))
notEqual(result, undefined)
await compareGetResults(result, value, body)
}
clock.tick(6000)
// Past deleteAt, shouldn't be returned
equal(await store.get(key), undefined)
})
test('a stale request is overwritten', options, async () => {
const clock = FakeTimers.install({
shouldClearNativeTimers: true
})
after(() => clock.uninstall())
/**
* @type {import('../../types/cache-interceptor.d.ts').default.CacheKey}
*/
const key = {
origin: 'localhost',
path: '/',
method: 'GET',
headers: {}
}
/**
* @type {import('../../types/cache-interceptor.d.ts').default.CacheValue}
*/
const value = {
statusCode: 200,
statusMessage: '',
headers: { foo: 'bar' },
cacheControlDirectives: {},
cachedAt: Date.now(),
staleAt: Date.now() + 1000,
// deleteAt is different because stale-while-revalidate, stale-if-error, ...
deleteAt: Date.now() + 5000
}
const body = [Buffer.from('asd'), Buffer.from('123')]
const store = new CacheStore()
// Sanity check
equal(store.get(key), undefined)
{
const writable = store.createWriteStream(key, value)
notEqual(writable, undefined)
writeBody(writable, body)
}
clock.tick(1500)
{
const result = await store.get(structuredClone(key))
notEqual(result, undefined)
await compareGetResults(result, value, body)
}
/**
* @type {import('../../types/cache-interceptor.d.ts').default.CacheValue}
*/
const value2 = {
statusCode: 200,
statusMessage: '',
headers: { foo: 'baz' },
cacheControlDirectives: {},
cachedAt: Date.now(),
staleAt: Date.now() + 1000,
// deleteAt is different because stale-while-revalidate, stale-if-error, ...
deleteAt: Date.now() + 5000
}
const body2 = [Buffer.from('foo'), Buffer.from('123')]
{
const writable = store.createWriteStream(key, value2)
notEqual(writable, undefined)
writeBody(writable, body2)
}
{
const result = await store.get(structuredClone(key))
notEqual(result, undefined)
await compareGetResults(result, value2, body2)
}
})
test('vary directives used to decide which response to use', options, async () => {
/**
* @type {import('../../types/cache-interceptor.d.ts').default.CacheKey}
*/
const key = {
origin: 'localhost',
path: '/',
method: 'GET',
headers: {
'some-header': 'hello world'
}
}
/**
* @type {import('../../types/cache-interceptor.d.ts').default.CacheValue}
*/
const value = {
statusCode: 200,
statusMessage: '',
headers: { foo: 'bar' },
vary: {
'some-header': 'hello world'
},
cacheControlDirectives: {},
cachedAt: Date.now(),
staleAt: Date.now() + 1000,
deleteAt: Date.now() + 1000
}
const body = [Buffer.from('asd'), Buffer.from('123')]
const store = new CacheStore()
// Sanity check
equal(store.get(key), undefined)
{
const writable = store.createWriteStream(key, value)
notEqual(writable, undefined)
writeBody(writable, body)
}
{
const result = await store.get(structuredClone(key))
notEqual(result, undefined)
await compareGetResults(result, value, body)
}
/**
* Let's make another key to the same resource but with a different vary
* header
* @type {import('../../types/cache-interceptor.d.ts').default.CacheKey}
*/
const anotherKey = {
origin: 'localhost',
path: '/',
method: 'GET',
headers: {
'some-header': 'hello world2'
}
}
/**
* @type {import('../../types/cache-interceptor.d.ts').default.CacheValue}
*/
const anotherValue = {
statusCode: 200,
statusMessage: '',
headers: { foo: 'bar' },
vary: {
'some-header': 'hello world2'
},
cacheControlDirectives: {},
cachedAt: Date.now(),
staleAt: Date.now() + 1000,
deleteAt: Date.now() + 1000
}
const anotherBody = [Buffer.from('asd'), Buffer.from('123')]
equal(await store.get(anotherKey), undefined)
{
const writable = store.createWriteStream(anotherKey, anotherValue)
notEqual(writable, undefined)
writeBody(writable, anotherBody)
}
{
const result = await store.get(structuredClone(key))
notEqual(result, undefined)
await compareGetResults(result, value, body)
}
{
const result = await store.get(structuredClone(anotherKey))
notEqual(result, undefined)
await compareGetResults(result, anotherValue, anotherBody)
}
})
})
}
/**
* @param {import('node:stream').Writable} stream
* @param {Buffer[]} body
*/
function writeBody (stream, body) {
for (const chunk of body) {
stream.write(chunk)
}
stream.end()
return stream
}
/**
* @param {import('../../types/cache-interceptor.d.ts').default.GetResult} param0
* @returns {Promise}
*/
async function readBody ({ body }) {
if (!body) {
return undefined
}
if (typeof body === 'string') {
return [Buffer.from(body)]
}
if (body.constructor.name === 'Buffer') {
return [body]
}
const stream = Readable.from(body)
/**
* @type {Buffer[]}
*/
const streamedBody = []
stream.on('data', chunk => {
streamedBody.push(Buffer.from(chunk))
})
await once(stream, 'end')
return streamedBody
}
/**
* @param {Buffer[]} buffers
* @returns {Buffer}
*/
function joinBufferArray (buffers) {
const data = []
for (const buffer of buffers) {
buffer.forEach((chunk) => {
data.push(chunk)
})
}
return Buffer.from(data)
}
/**
* @param {import('../../types/cache-interceptor.d.ts').default.GetResult} actual
* @param {import('../../types/cache-interceptor.d.ts').default.CacheValue} expected
* @param {Buffer[]} expectedBody
*/
async function compareGetResults (actual, expected, expectedBody) {
const actualBody = await readBody(actual)
deepStrictEqual(
actualBody ? joinBufferArray(actualBody) : undefined,
joinBufferArray(expectedBody)
)
for (const key of Object.keys(expected)) {
deepStrictEqual(actual[key], expected[key])
}
}
module.exports = {
cacheStoreTests,
writeBody,
readBody,
compareGetResults
}
================================================
FILE: test/cache-interceptor/cache-tests-worker.mjs
================================================
'use strict'
import { styleText } from 'node:util'
import { exit } from 'node:process'
import { getResults, runTests as runTestSuite } from '../fixtures/cache-tests/test-engine/client/runner.mjs'
import { determineTestResult, testLookup } from '../fixtures/cache-tests/test-engine/lib/results.mjs'
import tests from '../fixtures/cache-tests/tests/index.mjs'
import { Agent, fetch, interceptors, setGlobalDispatcher } from '../../index.js'
import { runtimeFeatures } from '../../lib/util/runtime-features.js'
import MemoryCacheStore from '../../lib/cache/memory-cache-store.js'
if (!process.env.TEST_ENVIRONMENT) {
throw new Error('missing TEST_ENVIRONMENT')
}
if (!process.env.BASE_URL) {
throw new Error('missing BASE_URL')
}
/**
* @type {import('./cache-tests.mjs').TestEnvironment}
*/
const environment = JSON.parse(process.env.TEST_ENVIRONMENT)
if (environment.cacheStore) {
environment.opts.store = await makeCacheStore(environment.cacheStore)
}
// Start the test server
await import('../fixtures/cache-tests/test-engine/server/server.mjs')
// Output the testing setup
console.log('TEST ENVIRONMENT')
console.log(` BASE_URL: ${styleText('gray', process.env.BASE_URL)}`)
if (environment.opts.store) {
console.log(` store: ${styleText('gray', environment.opts.store?.constructor.name ?? 'undefined')}`)
}
if (environment.opts.methods) {
console.log(` methods: ${styleText('gray', JSON.stringify(environment.opts.methods) ?? 'undefined')}`)
}
if (environment.opts.cacheByDefault) {
console.log(` cacheByDefault: ${styleText('gray', `${environment.opts.cacheByDefault}`)}`)
}
if (environment.opts.type) {
console.log(` type: ${styleText('gray', environment.opts.type)}`)
}
if (environment.ignoredTests) {
console.log(` ignored tests: ${styleText('gray', JSON.stringify(environment.ignoredTests))}`)
}
// Setup the client
const client = new Agent().compose(interceptors.cache(environment.opts))
setGlobalDispatcher(client)
globalThis.fetch = fetch
// Run the suite
await runTestSuite(tests, true, process.env.BASE_URL)
let exitCode = 0
// Print the results
const stats = printResults(environment, getResults())
printStats(stats)
exit(exitCode)
/**
* @param {import('./cache-tests.mjs').TestEnvironment['cacheStore']} type
* @returns {Promise}
*/
async function makeCacheStore (type) {
const stores = {
MemoryCacheStore
}
if (runtimeFeatures.has('sqlite')) {
const { default: SqliteCacheStore } = await import('../../lib/cache/sqlite-cache-store.js')
stores.SqliteCacheStore = SqliteCacheStore
}
const Store = stores[type]
if (!Store) {
throw new TypeError(`unknown cache store: ${type}`)
}
return new Store()
}
/**
* @param {import('./cache-tests.mjs').TestEnvironment} environment
* @param {any} results
* @returns {import('./cache-tests.mjs').TestStats}
*/
function printResults (environment, results) {
/**
* @type {import('./cache-tests.mjs').TestStats}
*/
const stats = {
total: Object.keys(results).length - (environment.ignoredTests?.length || 0),
skipped: 0,
passed: 0,
failed: 0,
optionalFailed: 0,
setupFailed: 0,
testHarnessFailed: 0,
dependencyFailed: 0,
retried: 0
}
for (const testId in results) {
if (environment.ignoredTests?.includes(testId)) {
continue
}
const test = testLookup(tests, testId)
// eslint-disable-next-line no-unused-vars
const [code, _, icon] = determineTestResult(tests, testId, results, false)
let status
let color
switch (code) {
case '-':
status = 'skipped'
color = 'gray'
stats.skipped++
break
case '\uf058':
status = 'pass'
color = 'green'
stats.passed++
break
case '\uf057':
status = 'failed'
color = 'red'
stats.failed++
exitCode = 1
break
case '\uf05a':
status = 'failed (optional)'
color = 'yellow'
stats.optionalFailed++
break
case '\uf055':
status = 'yes'
color = 'green'
stats.passed++
break
case '\uf056':
status = 'no'
color = 'yellow'
stats.optionalFailed++
break
case '\uf059':
status = 'setup failure'
color = 'red'
stats.setupFailed++
break
case '\uf06a':
status = 'test harness failure'
color = 'red'
stats.testHarnessFailed++
break
case '\uf192':
status = 'dependency failure'
color = 'red'
stats.dependencyFailed++
break
case '\uf01e':
status = 'retry'
color = 'yellow'
stats.retried++
break
default:
status = 'unknown'
color = ['strikethrough', 'white']
break
}
if (process.env.CI && status !== 'failed') {
continue
}
console.log(`${icon} ${styleText(color, `${status} - ${test.name}`)} (${styleText('gray', testId)})`)
if (results[testId] !== true) {
const [type, message] = results[testId]
console.log(` ${styleText(color, `${type}: ${message}`)}`)
}
}
return stats
}
/**
* @param {import('./cache-tests.mjs').TestStats} stats
*/
function printStats (stats) {
const {
total,
skipped,
passed,
failed,
optionalFailed,
setupFailed,
testHarnessFailed,
dependencyFailed,
retried
} = stats
if (total < 0) {
throw new Error('Total tests cannot be negative')
}
console.log(`\n Total tests: ${total}`)
console.log(` ${styleText('gray', 'Skipped')}: ${skipped} (${((skipped / total) * 100).toFixed(1)}%)`)
console.log(` ${styleText('green', 'Passed')}: ${passed} (${((passed / total) * 100).toFixed(1)}%)`)
console.log(` ${styleText('red', 'Failed')}: ${failed} (${((failed / total) * 100).toFixed(1)}%)`)
console.log(` ${styleText('yellow', 'Failed (optional)')}: ${optionalFailed} (${((optionalFailed / total) * 100).toFixed(1)}%)`)
console.log(` ${styleText('red', 'Setup failed')}: ${setupFailed} (${((setupFailed / total) * 100).toFixed(1)}%)`)
console.log(`${styleText('red', 'Test Harness Failed')}: ${testHarnessFailed} (${((testHarnessFailed / total) * 100).toFixed(1)}%)`)
console.log(` ${styleText('red', 'Dependency Failed')}: ${dependencyFailed} (${((dependencyFailed / total) * 100).toFixed(1)}%)`)
console.log(` ${styleText('yellow', 'Retried')}: ${retried} (${((retried / total) * 100).toFixed(1)}%)`)
}
================================================
FILE: test/cache-interceptor/cache-tests.mjs
================================================
'use strict'
import { parseArgs, styleText } from 'node:util'
import { join } from 'node:path'
import { tmpdir } from 'node:os'
import { exit } from 'node:process'
import { fork } from 'node:child_process'
import { runtimeFeatures } from '../../lib/util/runtime-features.js'
/**
* @typedef {import('../../types/cache-interceptor.d.ts').default.CacheOptions} CacheOptions
*
* @typedef {{
* opts: CacheOptions,
* ignoredTests?: string[],
* cacheStore?: 'MemoryCacheStore' | 'SqliteCacheStore'
* }} TestEnvironment
*
* @typedef {{
* total: number,
* skipped: number,
* passed: number,
* failed: number,
* optionalFailed: number,
* setupFailed: number,
* testHarnessFailed: number,
* dependencyFailed: number,
* retried: number
* }} TestStats
*/
const CLI_OPTIONS = parseArgs({
options: {
// Cache type(s) to test
type: {
type: 'string',
multiple: true,
short: 't'
},
// Cache store(s) to test
store: {
type: 'string',
multiple: true,
short: 's'
},
// Only shows errors
ci: {
type: 'boolean'
}
}
})
/**
* @type {TestEnvironment}
*/
const BASE_TEST_ENVIRONMENT = {
opts: { methods: ['GET', 'HEAD'] },
ignoredTests: [
// Tests for invalid etags, goes against the spec
'conditional-etag-forward-unquoted',
'conditional-etag-strong-generate-unquoted',
// Responses with no-cache can be reused if they're revalidated (which is
// what we're doing)
'cc-resp-no-cache',
'cc-resp-no-cache-case-insensitive',
// We're not caching 304s currently
'304-etag-update-response-Cache-Control',
'304-etag-update-response-Content-Foo',
'304-etag-update-response-Test-Header',
'304-etag-update-response-X-Content-Foo',
'304-etag-update-response-X-Test-Header',
// We just trim whatever's in the decimal place off (i.e. 7200.0 -> 7200)
'age-parse-float',
// Broken?
'head-200-update',
'head-200-retain',
'head-410-update',
'stale-close-must-revalidate',
'stale-close-no-cache'
]
}
/**
* @type {TestEnvironment[]}
*/
const CACHE_TYPES = [
{
opts: { type: 'shared' },
ignoredTests: [
'freshness-max-age-s-maxage-private',
'freshness-max-age-s-maxage-private-multiple'
]
},
{
opts: { type: 'private' }
}
]
/**
* @type {TestEnvironment[]}
*/
const CACHE_STORES = [
{ opts: {}, cacheStore: 'MemoryCacheStore' }
]
if (runtimeFeatures.has('sqlite')) {
CACHE_STORES.push({ opts: {}, cacheStore: 'SqliteCacheStore' })
} else {
console.warn('Skipping SqliteCacheStore, node:sqlite not present')
}
const PROTOCOL = 'http'
const PORT = 8000
const testEnvironments = filterEnvironments(
buildTestEnvironments(0, [CACHE_TYPES, CACHE_STORES])
)
console.log(`Testing ${testEnvironments.length} environments\n`)
console.log(`PROTOCOL: ${styleText('gray', PROTOCOL)}`)
console.log('')
/**
* @type {Array]>>}
*/
const results = []
// Run all the tests in child processes because the test runner is a bit finicky
for (let i = 0; i < testEnvironments.length; i++) {
const environment = testEnvironments[i]
const port = PORT + i
const promise = new Promise((resolve) => {
const cacheTestsWorkerProcess = fork(join(import.meta.dirname, 'cache-tests-worker.mjs'), {
stdio: 'pipe',
env: {
NODE_OPTIONS: process.env.NODE_OPTIONS,
NODE_V8_COVERAGE: process.env.NODE_V8_COVERAGE,
TEST_ENVIRONMENT: JSON.stringify(environment),
BASE_URL: `${PROTOCOL}://localhost:${port}`,
CI: CLI_OPTIONS.values.ci ? 'true' : undefined,
npm_config_protocol: PROTOCOL,
npm_config_port: `${port}`,
npm_config_pidfile: join(tmpdir(), `http-cache-test-server-${i}.pid`)
}
})
const stdout = []
cacheTestsWorkerProcess.stdout.on('data', chunk => {
stdout.push(chunk)
})
cacheTestsWorkerProcess.stderr.on('error', chunk => {
stdout.push(chunk)
})
cacheTestsWorkerProcess.on('close', code => {
resolve([code, stdout])
})
})
results.push(promise)
}
// Status code so we can fail CI jobs if we need
let exitCode = 0
// Print the results of all the results in the order that they exist
for (const [code, stdout] of await Promise.all(results)) {
exitCode = code
for (const line of stdout) {
process.stdout.write(line)
}
console.log('')
}
exit(exitCode)
/**
* @param {number} idx
* @param {TestEnvironment[][]} testOptions
* @returns {TestEnvironment[]}
*/
function buildTestEnvironments (idx, testOptions) {
let baseEnvironments = testOptions[idx]
if (idx === 0) {
// We're at the beginning
baseEnvironments = baseEnvironments.map(
environment => joinEnvironments(BASE_TEST_ENVIRONMENT, environment))
}
if (idx + 1 >= testOptions.length) {
// We're at the end, nothing more to make a matrix out of
return baseEnvironments
}
/**
* @type {TestEnvironment[]}
*/
const environments = []
// Get all of the environments below us
const subEnvironments = buildTestEnvironments(idx + 1, testOptions)
for (const baseEnvironment of baseEnvironments) {
const combinedEnvironments = subEnvironments.map(
subEnvironment => joinEnvironments(baseEnvironment, subEnvironment))
environments.push(...combinedEnvironments)
}
return environments
}
/**
* @param {TestEnvironment} base
* @param {TestEnvironment} sub
* @returns {TestEnvironment}
*/
function joinEnvironments (base, sub) {
const ignoredTests = base.ignoredTests ?? []
if (sub.ignoredTests) {
ignoredTests.push(...sub.ignoredTests)
}
return {
opts: {
...base.opts,
...sub.opts
},
ignoredTests: ignoredTests.length > 0 ? ignoredTests : undefined,
cacheStore: sub.cacheStore
}
}
/**
* @param {TestEnvironment[]} environments
* @returns {TestEnvironment[]}
*/
function filterEnvironments (environments) {
const { values } = CLI_OPTIONS
if (values.type) {
environments = environments.filter(env =>
env.opts.type === undefined ||
values.type?.includes(env.opts.type)
)
}
if (values.store) {
environments = environments.filter(({ cacheStore }) => {
if (cacheStore === undefined) {
return false
}
const storeName = cacheStore
for (const allowedStore of values.store) {
if (storeName.match(allowedStore)) {
return true
}
}
return false
})
}
return environments
}
================================================
FILE: test/cache-interceptor/cache-utils.js
================================================
'use strict'
const { tspl } = require('@matteo.collina/tspl')
const { test } = require('node:test')
const { normalizeHeaders } = require('../../lib/util/cache')
test('normalizeHeaders handles plain object headers with polluted Object.prototype[Symbol.iterator]', (t) => {
const { strictEqual } = tspl(t, { plan: 2 })
const originalIterator = Object.prototype[Symbol.iterator]
// eslint-disable-next-line no-extend-native
Object.prototype[Symbol.iterator] = function * () {}
try {
const headers = normalizeHeaders({
headers: {
Authorization: 'Bearer token',
'X-Test': 'ok'
}
})
strictEqual(headers.authorization, 'Bearer token')
strictEqual(headers['x-test'], 'ok')
} finally {
if (originalIterator === undefined) {
delete Object.prototype[Symbol.iterator]
} else {
// eslint-disable-next-line no-extend-native
Object.prototype[Symbol.iterator] = originalIterator
}
}
})
test('normalizeHeaders handles headers from Map', (t) => {
const { strictEqual } = tspl(t, { plan: 1 })
const headers = normalizeHeaders({
headers: new Map([
['X-Test', 'ok']
])
})
strictEqual(headers['x-test'], 'ok')
})
================================================
FILE: test/cache-interceptor/memory-cache-store-tests.js
================================================
'use strict'
const { test } = require('node:test')
const { equal } = require('node:assert')
const MemoryCacheStore = require('../../lib/cache/memory-cache-store')
const { cacheStoreTests } = require('./cache-store-test-utils.js')
cacheStoreTests(MemoryCacheStore)
test('default limits prevent memory leaks', async () => {
const store = new MemoryCacheStore() // Uses new defaults
// Test that maxCount default (1024) is enforced
for (let i = 0; i < 1025; i++) {
const writeStream = store.createWriteStream(
{ origin: 'test', path: `/test-${i}`, method: 'GET' },
{
statusCode: 200,
statusMessage: 'OK',
headers: {},
cachedAt: Date.now(),
staleAt: Date.now() + 60000,
deleteAt: Date.now() + 120000
}
)
writeStream.write('test data')
writeStream.end()
}
// Should be full after exceeding maxCount default of 1024
equal(store.isFull(), true, 'Store should be full after exceeding maxCount default')
})
test('default maxEntrySize prevents large entries', async () => {
const store = new MemoryCacheStore() // Uses new defaults
// Create entry larger than default maxEntrySize (5MB)
const largeData = Buffer.allocUnsafe(5242881) // 5MB + 1 byte
const writeStream = store.createWriteStream(
{ origin: 'test', path: '/large', method: 'GET' },
{
statusCode: 200,
statusMessage: 'OK',
headers: {},
cachedAt: Date.now(),
staleAt: Date.now() + 60000,
deleteAt: Date.now() + 120000
}
)
writeStream.write(largeData)
writeStream.end()
// Entry should not be cached due to maxEntrySize limit
const result = store.get({ origin: 'test', path: '/large', method: 'GET', headers: {} })
equal(result, undefined, 'Large entry should not be cached due to maxEntrySize limit')
})
test('size getter returns correct total size', async () => {
const store = new MemoryCacheStore()
const testData = 'test data'
equal(store.size, 0, 'Initial size should be 0')
const writeStream = store.createWriteStream(
{ origin: 'test', path: '/', method: 'GET' },
{
statusCode: 200,
statusMessage: 'OK',
headers: {},
cachedAt: Date.now(),
staleAt: Date.now() + 1000,
deleteAt: Date.now() + 2000
}
)
writeStream.write(testData)
writeStream.end()
equal(store.size, testData.length, 'Size should match written data length')
})
test('isFull returns false when under limits', () => {
const store = new MemoryCacheStore({
maxSize: 1000,
maxCount: 10
})
equal(store.isFull(), false, 'Should not be full when empty')
})
test('isFull returns true when maxSize reached', async () => {
const maxSize = 10
const store = new MemoryCacheStore({ maxSize })
const testData = 'x'.repeat(maxSize + 1) // Exceed maxSize
const writeStream = store.createWriteStream(
{ origin: 'test', path: '/', method: 'GET' },
{
statusCode: 200,
statusMessage: 'OK',
headers: {},
cachedAt: Date.now(),
staleAt: Date.now() + 1000,
deleteAt: Date.now() + 2000
}
)
writeStream.write(testData)
writeStream.end()
equal(store.isFull(), true, 'Should be full when maxSize exceeded')
})
test('isFull returns true when maxCount reached', async () => {
const maxCount = 2
const store = new MemoryCacheStore({ maxCount })
// Add maxCount + 1 entries
for (let i = 0; i <= maxCount; i++) {
const writeStream = store.createWriteStream(
{ origin: 'test', path: `/${i}`, method: 'GET' },
{
statusCode: 200,
statusMessage: 'OK',
headers: {},
cachedAt: Date.now(),
staleAt: Date.now() + 1000,
deleteAt: Date.now() + 2000
}
)
writeStream.end('test')
}
equal(store.isFull(), true, 'Should be full when maxCount exceeded')
})
test('emits maxSizeExceeded event when limits exceeded', async () => {
const maxSize = 10
const store = new MemoryCacheStore({ maxSize })
let eventFired = false
let eventPayload = null
store.on('maxSizeExceeded', (payload) => {
eventFired = true
eventPayload = payload
})
const testData = 'x'.repeat(maxSize + 1) // Exceed maxSize
const writeStream = store.createWriteStream(
{ origin: 'test', path: '/', method: 'GET' },
{
statusCode: 200,
statusMessage: 'OK',
headers: {},
cachedAt: Date.now(),
staleAt: Date.now() + 1000,
deleteAt: Date.now() + 2000
}
)
writeStream.write(testData)
writeStream.end()
equal(eventFired, true, 'maxSizeExceeded event should fire')
equal(typeof eventPayload, 'object', 'Event should have payload')
equal(typeof eventPayload.size, 'number', 'Payload should have size')
equal(typeof eventPayload.maxSize, 'number', 'Payload should have maxSize')
equal(typeof eventPayload.count, 'number', 'Payload should have count')
equal(typeof eventPayload.maxCount, 'number', 'Payload should have maxCount')
})
================================================
FILE: test/cache-interceptor/sqlite-cache-store-tests.js
================================================
'use strict'
const { test } = require('node:test')
const { notEqual, strictEqual, deepStrictEqual } = require('node:assert')
const { rm } = require('node:fs/promises')
const { cacheStoreTests, writeBody, compareGetResults } = require('./cache-store-test-utils.js')
const { runtimeFeatures } = require('../../lib/util/runtime-features.js')
const SqliteCacheStore = require('../../lib/cache/sqlite-cache-store.js')
cacheStoreTests(SqliteCacheStore, { skip: runtimeFeatures.has('sqlite') === false })
test('SqliteCacheStore works nicely with multiple stores', { skip: runtimeFeatures.has('sqlite') === false }, async (t) => {
const SqliteCacheStore = require('../../lib/cache/sqlite-cache-store.js')
const sqliteLocation = 'cache-interceptor.sqlite'
const storeA = new SqliteCacheStore({
location: sqliteLocation
})
const storeB = new SqliteCacheStore({
location: sqliteLocation
})
t.after(async () => {
storeA.close()
storeB.close()
await rm(sqliteLocation)
})
/**
* @type {import('../../types/cache-interceptor.d.ts').default.CacheKey}
*/
const key = {
origin: 'localhost',
path: '/',
method: 'GET',
headers: {}
}
/**
* @type {import('../../types/cache-interceptor.d.ts').default.CacheValue}
*/
const value = {
statusCode: 200,
statusMessage: '',
headers: { foo: 'bar' },
cachedAt: Date.now(),
staleAt: Date.now() + 10000,
deleteAt: Date.now() + 20000
}
const body = [Buffer.from('asd'), Buffer.from('123')]
{
const writable = storeA.createWriteStream(key, value)
notEqual(writable, undefined)
writeBody(writable, body)
}
// Make sure we got the expected response from store a
{
const result = storeA.get(structuredClone(key))
notEqual(result, undefined)
await compareGetResults(result, value, body)
}
// Make sure we got the expected response from store b
{
const result = storeB.get(structuredClone(key))
notEqual(result, undefined)
await compareGetResults(result, value, body)
}
})
test('SqliteCacheStore maxEntries', { skip: runtimeFeatures.has('sqlite') === false }, async () => {
const SqliteCacheStore = require('../../lib/cache/sqlite-cache-store.js')
const store = new SqliteCacheStore({
maxCount: 10
})
for (let i = 0; i < 20; i++) {
/**
* @type {import('../../types/cache-interceptor.d.ts').default.CacheKey}
*/
const key = {
origin: 'localhost',
path: '/' + i,
method: 'GET',
headers: {}
}
/**
* @type {import('../../types/cache-interceptor.d.ts').default.CacheValue}
*/
const value = {
statusCode: 200,
statusMessage: '',
headers: { foo: 'bar' },
cachedAt: Date.now(),
staleAt: Date.now() + 10000,
deleteAt: Date.now() + 20000
}
const body = ['asd', '123']
const writable = store.createWriteStream(key, value)
notEqual(writable, undefined)
writeBody(writable, body)
}
strictEqual(store.size <= 11, true)
})
test('SqliteCacheStore two writes', { skip: runtimeFeatures.has('sqlite') === false }, async () => {
const SqliteCacheStore = require('../../lib/cache/sqlite-cache-store.js')
const store = new SqliteCacheStore({
maxCount: 10
})
/**
* @type {import('../../types/cache-interceptor.d.ts').default.CacheKey}
*/
const key = {
origin: 'localhost',
path: '/',
method: 'GET',
headers: {}
}
/**
* @type {import('../../types/cache-interceptor.d.ts').default.CacheValue}
*/
const value = {
statusCode: 200,
statusMessage: '',
headers: { foo: 'bar' },
cachedAt: Date.now(),
staleAt: Date.now() + 10000,
deleteAt: Date.now() + 20000
}
const body = ['asd', '123']
{
const writable = store.createWriteStream(key, value)
notEqual(writable, undefined)
writeBody(writable, body)
}
{
const writable = store.createWriteStream(key, value)
notEqual(writable, undefined)
writeBody(writable, body)
}
})
test('SqliteCacheStore write & read', { skip: runtimeFeatures.has('sqlite') === false }, async () => {
const SqliteCacheStore = require('../../lib/cache/sqlite-cache-store.js')
const store = new SqliteCacheStore({
maxCount: 10
})
/**
* @type {import('../../types/cache-interceptor.d.ts').default.CacheKey}
*/
const key = {
origin: 'localhost',
path: '/',
method: 'GET',
headers: {}
}
/**
* @type {import('../../types/cache-interceptor.d.ts').default.CacheValue & { body: Buffer }}
*/
const value = {
statusCode: 200,
statusMessage: '',
headers: { foo: 'bar' },
cacheControlDirectives: { 'max-stale': 0 },
cachedAt: Date.now(),
staleAt: Date.now() + 10000,
deleteAt: Date.now() + 20000,
body: Buffer.from('asd'),
etag: undefined,
vary: undefined
}
store.set(key, value)
deepStrictEqual(store.get(key), value)
})
================================================
FILE: test/cache-interceptor/utils.js
================================================
'use strict'
const { describe, test } = require('node:test')
const { deepStrictEqual, equal } = require('node:assert')
const { parseCacheControlHeader, parseVaryHeader, isEtagUsable } = require('../../lib/util/cache')
describe('parseCacheControlHeader', () => {
test('all directives are parsed properly when in their correct format', () => {
const directives = parseCacheControlHeader(
'max-stale=1, min-fresh=1, max-age=1, s-maxage=1, stale-while-revalidate=1, stale-if-error=1, public, private, no-store, no-cache, must-revalidate, proxy-revalidate, immutable, no-transform, must-understand, only-if-cached'
)
deepStrictEqual(directives, {
'max-stale': 1,
'min-fresh': 1,
'max-age': 1,
's-maxage': 1,
'stale-while-revalidate': 1,
'stale-if-error': 1,
public: true,
private: true,
'no-store': true,
'no-cache': true,
'must-revalidate': true,
'proxy-revalidate': true,
immutable: true,
'no-transform': true,
'must-understand': true,
'only-if-cached': true
})
})
test('handles weird spacings', () => {
const directives = parseCacheControlHeader(
'max-stale=1, min-fresh=1, max-age=1,s-maxage=1, stale-while-revalidate=1,stale-if-error=1,public,private'
)
deepStrictEqual(directives, {
'max-stale': 1,
'min-fresh': 1,
'max-age': 1,
's-maxage': 1,
'stale-while-revalidate': 1,
'stale-if-error': 1,
public: true,
private: true
})
})
test('unknown directives are ignored', () => {
const directives = parseCacheControlHeader('max-age=123, something-else=456')
deepStrictEqual(directives, { 'max-age': 123 })
})
test('directives with incorrect types are ignored', () => {
const directives = parseCacheControlHeader('max-age=true, only-if-cached=123')
deepStrictEqual(directives, {})
})
test('the last instance of a directive takes precedence', () => {
const directives = parseCacheControlHeader('max-age=1, max-age=2')
deepStrictEqual(directives, { 'max-age': 2 })
})
test('case insensitive', () => {
const directives = parseCacheControlHeader('Max-Age=123')
deepStrictEqual(directives, { 'max-age': 123 })
})
test('no-cache with headers', () => {
let directives = parseCacheControlHeader('max-age=10, no-cache=some-header, only-if-cached')
deepStrictEqual(directives, {
'max-age': 10,
'no-cache': [
'some-header'
],
'only-if-cached': true
})
directives = parseCacheControlHeader('max-age=10, no-cache="some-header", only-if-cached')
deepStrictEqual(directives, {
'max-age': 10,
'no-cache': [
'some-header'
],
'only-if-cached': true
})
directives = parseCacheControlHeader('max-age=10, no-cache="some-header, another-one", only-if-cached')
deepStrictEqual(directives, {
'max-age': 10,
'no-cache': [
'some-header',
'another-one'
],
'only-if-cached': true
})
})
test('private with headers', () => {
let directives = parseCacheControlHeader('max-age=10, private=some-header, only-if-cached')
deepStrictEqual(directives, {
'max-age': 10,
private: [
'some-header'
],
'only-if-cached': true
})
directives = parseCacheControlHeader('max-age=10, private="some-header", only-if-cached')
deepStrictEqual(directives, {
'max-age': 10,
private: [
'some-header'
],
'only-if-cached': true
})
directives = parseCacheControlHeader('max-age=10, private="some-header, another-one", only-if-cached')
deepStrictEqual(directives, {
'max-age': 10,
private: [
'some-header',
'another-one'
],
'only-if-cached': true
})
// Missing ending quote, invalid & should be skipped
directives = parseCacheControlHeader('max-age=10, private="some-header, another-one, only-if-cached')
deepStrictEqual(directives, {
'max-age': 10,
'only-if-cached': true
})
})
test('handles multiple headers correctly', () => {
// For requests like
// cache-control: max-stale=1
// cache-control: min-fresh-1
// ...
const directives = parseCacheControlHeader([
'max-stale=1',
'min-fresh=1',
'max-age=1',
's-maxage=1',
'stale-while-revalidate=1',
'stale-if-error=1',
'public',
'private',
'no-store',
'no-cache',
'must-revalidate',
'proxy-revalidate',
'immutable',
'no-transform',
'must-understand',
'only-if-cached'
])
deepStrictEqual(directives, {
'max-stale': 1,
'min-fresh': 1,
'max-age': 1,
's-maxage': 1,
'stale-while-revalidate': 1,
'stale-if-error': 1,
public: true,
private: true,
'no-store': true,
'no-cache': true,
'must-revalidate': true,
'proxy-revalidate': true,
immutable: true,
'no-transform': true,
'must-understand': true,
'only-if-cached': true
})
})
})
describe('parseVaryHeader', () => {
test('basic usage', () => {
const output = parseVaryHeader('some-header, another-one', {
'some-header': 'asd',
'another-one': '123',
'third-header': 'cool'
})
deepStrictEqual(output, {
'some-header': 'asd',
'another-one': '123'
})
})
test('handles weird spacings', () => {
const output = parseVaryHeader('some-header, another-one,something-else', {
'some-header': 'asd',
'another-one': '123',
'something-else': 'asd123',
'third-header': 'cool'
})
deepStrictEqual(output, {
'some-header': 'asd',
'another-one': '123',
'something-else': 'asd123'
})
})
test('handles multiple headers correctly', () => {
const output = parseVaryHeader(['some-header', 'another-one'], {
'some-header': 'asd',
'another-one': '123',
'third-header': 'cool'
})
deepStrictEqual(output, {
'some-header': 'asd',
'another-one': '123'
})
})
test('handles missing headers with null', () => {
const result = parseVaryHeader('Accept-Encoding, Authorization', {})
deepStrictEqual(result, {
'accept-encoding': null,
authorization: null
})
})
test('handles mix of present and missing headers', () => {
const result = parseVaryHeader('Accept-Encoding, Authorization', {
authorization: 'example-value'
})
deepStrictEqual(result, {
'accept-encoding': null,
authorization: 'example-value'
})
})
test('handles array input', () => {
const result = parseVaryHeader(['Accept-Encoding', 'Authorization'], {
'accept-encoding': 'gzip'
})
deepStrictEqual(result, {
'accept-encoding': 'gzip',
authorization: null
})
})
test('preserves existing * behavior', () => {
const headers = { accept: 'text/html' }
const result = parseVaryHeader('*', headers)
deepStrictEqual(result, headers)
})
})
describe('isEtagUsable', () => {
const valuesToTest = {
// Invalid etags
'': false,
asd: false,
'"W/"asd""': false,
'""asd""': false,
// Valid etags
'"asd"': true,
'W/"ads"': true,
// Spec deviations
'""': false,
'W/""': false
}
for (const key in valuesToTest) {
const expectedValue = valuesToTest[key]
test(`\`${key}\` = ${expectedValue}`, () => {
equal(isEtagUsable(key), expectedValue)
})
}
})
================================================
FILE: test/client-connect.js
================================================
'use strict'
const { tspl } = require('@matteo.collina/tspl')
const { test, after } = require('node:test')
const { once } = require('node:events')
const { Client, errors } = require('..')
const http = require('node:http')
const EE = require('node:events')
const { kBusy } = require('../lib/core/symbols')
// TODO: move to test/node-test/client-connect.js
test('connect aborted after connect', async (t) => {
t = tspl(t, { plan: 3 })
const signal = new EE()
const server = http.createServer({ joinDuplicateHeaders: true }, (req, res) => {
t.fail()
})
server.on('connect', (req, c, firstBodyChunk) => {
signal.emit('abort')
})
after(() => server.close())
server.listen(0)
await once(server, 'listening')
const client = new Client(`http://localhost:${server.address().port}`, {
pipelining: 3
})
after(() => client.close())
client.on('disconnect', () => {
if (!client.closed && !client.destroyed) {
t.fail('unexpected disconnect')
}
})
client.connect({
path: '/',
signal,
opaque: 'asd',
blocking: false
}, (err, { opaque }) => {
t.strictEqual(opaque, 'asd')
t.ok(err instanceof errors.RequestAbortedError)
})
t.strictEqual(client[kBusy], true)
await t.completed
})
================================================
FILE: test/client-head-reset-override.js
================================================
'use strict'
const { tspl } = require('@matteo.collina/tspl')
const { once } = require('node:events')
const { createServer } = require('node:http')
const { test, after } = require('node:test')
const { Client } = require('..')
test('override HEAD reset', async (t) => {
t = tspl(t, { plan: 4 })
const expected = 'testing123'
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
if (req.method === 'GET') {
res.write(expected)
}
res.end()
}).listen(0)
after(() => server.close())
await once(server, 'listening')
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.close())
let done
client.on('disconnect', () => {
if (!done) {
t.fail()
}
})
client.request({
path: '/',
method: 'HEAD',
reset: false
}, (err, res) => {
t.ifError(err)
res.body.resume()
})
client.request({
path: '/',
method: 'HEAD',
reset: false
}, (err, res) => {
t.ifError(err)
res.body.resume()
})
client.request({
path: '/',
method: 'GET',
reset: false
}, (err, res) => {
t.ifError(err)
let str = ''
res.body.on('data', (data) => {
str += data
}).on('end', () => {
t.strictEqual(str, expected)
done = true
t.end()
})
})
await t.completed
})
================================================
FILE: test/client-idempotent-body.js
================================================
'use strict'
const { tspl } = require('@matteo.collina/tspl')
const { test, after } = require('node:test')
const { Client } = require('..')
const { createServer } = require('node:http')
test('idempotent retry', async (t) => {
t = tspl(t, { plan: 11 })
const body = 'world'
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
let buf = ''
req.on('data', data => {
buf += data
}).on('end', () => {
t.strictEqual(buf, body)
res.end()
})
})
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`, {
pipelining: 2
})
after(() => client.close())
const _err = new Error()
for (let n = 0; n < 4; ++n) {
client.stream({
path: '/',
method: 'PUT',
idempotent: true,
blocking: false,
body
}, () => {
throw _err
}, (err) => {
t.strictEqual(err, _err)
})
}
})
await t.completed
})
================================================
FILE: test/client-keep-alive.js
================================================
'use strict'
const { tspl } = require('@matteo.collina/tspl')
const { test, after } = require('node:test')
const { once } = require('node:events')
const { Client } = require('..')
const { kConnect } = require('../lib/core/symbols')
const { createServer } = require('node:net')
const http = require('node:http')
const FakeTimers = require('@sinonjs/fake-timers')
test('keep-alive header', async (t) => {
t = tspl(t, { plan: 2 })
const server = createServer((socket) => {
socket.write('HTTP/1.1 200 OK\r\n')
socket.write('Content-Length: 0\r\n')
socket.write('Keep-Alive: timeout=2s\r\n')
socket.write('Connection: keep-alive\r\n')
socket.write('\r\n\r\n')
})
after(() => server.close())
await once(server.listen(0), 'listening')
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.close())
client.request({
path: '/',
method: 'GET'
}, (err, { body }) => {
t.ifError(err)
body.on('end', () => {
const timeout = setTimeout(() => {
t.fail()
}, 4e3)
client.on('disconnect', () => {
t.ok(true, 'pass')
clearTimeout(timeout)
})
}).resume()
})
await t.completed
})
test('keep-alive header 0', async (t) => {
t = tspl(t, { plan: 2 })
const clock = FakeTimers.install()
after(() => clock.uninstall())
const server = createServer((socket) => {
socket.write('HTTP/1.1 200 OK\r\n')
socket.write('Content-Length: 0\r\n')
socket.write('Keep-Alive: timeout=1s\r\n')
socket.write('Connection: keep-alive\r\n')
socket.write('\r\n\r\n')
})
after(() => server.close())
await once(server.listen(0), 'listening')
const client = new Client(`http://localhost:${server.address().port}`, {
keepAliveTimeoutThreshold: 500
})
after(() => client.close())
client.request({
path: '/',
method: 'GET'
}, (err, { body }) => {
t.ifError(err)
body.on('end', () => {
client.on('disconnect', () => {
t.ok(true, 'pass')
})
clock.tick(600)
}).resume()
})
await t.completed
})
test('keep-alive header 1', async (t) => {
t = tspl(t, { plan: 2 })
const server = createServer((socket) => {
socket.write('HTTP/1.1 200 OK\r\n')
socket.write('Content-Length: 0\r\n')
socket.write('Keep-Alive: timeout=1s\r\n')
socket.write('Connection: keep-alive\r\n')
socket.write('\r\n\r\n')
})
after(() => server.close())
await once(server.listen(0), 'listening')
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.close())
client.request({
path: '/',
method: 'GET'
}, (err, { body }) => {
t.ifError(err)
body.on('end', () => {
const timeout = setTimeout(() => {
t.fail()
}, 0)
client.on('disconnect', () => {
t.ok(true, 'pass')
clearTimeout(timeout)
})
}).resume()
})
await t.completed
})
test('keep-alive header no postfix', async (t) => {
t = tspl(t, { plan: 2 })
const server = createServer((socket) => {
socket.write('HTTP/1.1 200 OK\r\n')
socket.write('Content-Length: 0\r\n')
socket.write('Keep-Alive: timeout=2\r\n')
socket.write('Connection: keep-alive\r\n')
socket.write('\r\n\r\n')
})
after(() => server.close())
await once(server.listen(0), 'listening')
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.close())
client.request({
path: '/',
method: 'GET'
}, (err, { body }) => {
t.ifError(err)
body.on('end', () => {
const timeout = setTimeout(() => {
t.fail()
}, 4e3)
client.on('disconnect', () => {
t.ok(true, 'pass')
clearTimeout(timeout)
})
}).resume()
})
await t.completed
})
test('keep-alive not timeout', async (t) => {
t = tspl(t, { plan: 2 })
const clock = FakeTimers.install({
apis: ['setTimeout']
})
after(() => clock.uninstall())
const server = createServer((socket) => {
socket.write('HTTP/1.1 200 OK\r\n')
socket.write('Content-Length: 0\r\n')
socket.write('Keep-Alive: timeoutasdasd=1s\r\n')
socket.write('Connection: keep-alive\r\n')
socket.write('\r\n\r\n')
})
after(() => server.close())
await once(server.listen(0), 'listening')
const client = new Client(`http://localhost:${server.address().port}`, {
keepAliveTimeout: 1e3
})
after(() => client.close())
client.request({
path: '/',
method: 'GET'
}, (err, { body }) => {
t.ifError(err)
body.on('end', () => {
const timeout = setTimeout(t.fail, 3e3)
client.on('disconnect', () => {
t.ok(true, 'pass')
clearTimeout(timeout)
})
clock.tick(1000)
}).resume()
})
await t.completed
})
test('keep-alive threshold', async (t) => {
t = tspl(t, { plan: 2 })
const clock = FakeTimers.install({
apis: ['setTimeout']
})
after(() => clock.uninstall())
const server = createServer((socket) => {
socket.write('HTTP/1.1 200 OK\r\n')
socket.write('Content-Length: 0\r\n')
socket.write('Keep-Alive: timeout=30s\r\n')
socket.write('Connection: keep-alive\r\n')
socket.write('\r\n\r\n')
})
after(() => server.close())
await once(server.listen(0), 'listening')
const client = new Client(`http://localhost:${server.address().port}`, {
keepAliveTimeout: 30e3,
keepAliveTimeoutThreshold: 29e3
})
after(() => client.close())
client.request({
path: '/',
method: 'GET'
}, (err, { body }) => {
t.ifError(err)
body.on('end', () => {
const timeout = setTimeout(() => {
t.fail()
}, 5e3)
client.on('disconnect', () => {
t.ok(true, 'pass')
clearTimeout(timeout)
})
clock.tick(1000)
}).resume()
})
await t.completed
})
test('keep-alive max keepalive', async (t) => {
t = tspl(t, { plan: 2 })
const clock = FakeTimers.install({
apis: ['setTimeout']
})
after(() => clock.uninstall())
const server = createServer((socket) => {
socket.write('HTTP/1.1 200 OK\r\n')
socket.write('Content-Length: 0\r\n')
socket.write('Keep-Alive: timeout=30s\r\n')
socket.write('Connection: keep-alive\r\n')
socket.write('\r\n\r\n')
})
after(() => server.close())
await once(server.listen(0), 'listening')
const client = new Client(`http://localhost:${server.address().port}`, {
keepAliveTimeout: 30e3,
keepAliveMaxTimeout: 1e3
})
after(() => client.close())
client.request({
path: '/',
method: 'GET'
}, (err, { body }) => {
t.ifError(err)
body.on('end', () => {
const timeout = setTimeout(() => {
t.fail()
}, 3e3)
client.on('disconnect', () => {
t.ok(true, 'pass')
clearTimeout(timeout)
})
clock.tick(1000)
}).resume()
})
await t.completed
})
test('connection close', async (t) => {
t = tspl(t, { plan: 4 })
let close = false
const server = createServer((socket) => {
if (close) {
return
}
close = true
socket.write('HTTP/1.1 200 OK\r\n')
socket.write('Content-Length: 0\r\n')
socket.write('Connection: close\r\n')
socket.write('\r\n\r\n')
})
after(() => server.close())
await once(server.listen(0), 'listening')
const client = new Client(`http://localhost:${server.address().port}`, {
pipelining: 2
})
after(() => client.close())
client[kConnect](() => {
client.request({
path: '/',
method: 'GET'
}, (err, { body }) => {
t.ifError(err)
body.on('end', () => {
const timeout = setTimeout(() => {
t.fail()
}, 3e3)
client.once('disconnect', () => {
close = false
t.ok(true, 'pass')
clearTimeout(timeout)
})
}).resume()
})
client.request({
path: '/',
method: 'GET'
}, (err, { body }) => {
t.ifError(err)
body.on('end', () => {
const timeout = setTimeout(() => {
t.fail()
}, 3e3)
client.once('disconnect', () => {
t.ok(true, 'pass')
clearTimeout(timeout)
})
}).resume()
})
})
await t.completed
})
test('Disable keep alive', async (t) => {
t = tspl(t, { plan: 7 })
const ports = []
const server = http.createServer({ joinDuplicateHeaders: true }, (req, res) => {
t.strictEqual(ports.includes(req.socket.remotePort), false)
ports.push(req.socket.remotePort)
t.strictEqual(req.headers.connection, 'close')
res.writeHead(200, { connection: 'close' })
res.end()
})
after(() => server.close())
await once(server.listen(0), 'listening')
const client = new Client(`http://localhost:${server.address().port}`, { pipelining: 0 })
after(() => client.close())
client.request({
path: '/',
method: 'GET'
}, (err, { body }) => {
t.ifError(err)
body.on('end', () => {
client.request({
path: '/',
method: 'GET'
}, (err, { body }) => {
t.ifError(err)
body.on('end', () => {
t.ok(true, 'pass')
}).resume()
})
}).resume()
})
await t.completed
})
================================================
FILE: test/client-node-max-header-size.js
================================================
'use strict'
const { tspl } = require('@matteo.collina/tspl')
const { once } = require('node:events')
const { exec } = require('node:child_process')
const { test, before, after, describe } = require('node:test')
const { createServer } = require('node:http')
describe("Node.js' --max-http-header-size cli option", () => {
let server
before(async () => {
server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.writeHead(200, 'OK', {
'Content-Length': 2
})
res.write('OK')
res.end()
}).listen(0)
await once(server, 'listening')
})
after(() => {
server.closeAllConnections?.()
server.close()
})
test("respect Node.js' --max-http-header-size", async (t) => {
t = tspl(t, { plan: 6 })
const command = 'node --disable-warning=ExperimentalWarning -e "require(\'.\').request(\'http://localhost:' + server.address().port + '\')"'
exec(`${command} --max-http-header-size=1`, { stdio: 'pipe' }, (err, stdout, stderr) => {
t.strictEqual(err.code, 1)
t.strictEqual(stdout, '')
t.match(stderr, /UND_ERR_HEADERS_OVERFLOW/, '--max-http-header-size=1 should throw')
})
exec(command, { stdio: 'pipe' }, (err, stdout, stderr) => {
t.ifError(err)
t.strictEqual(stdout, '')
// Filter out debugger messages that may appear when running with --inspect
const filteredStderr = stderr.replace(/Debugger listening on ws:\/\/.*?\n/g, '')
.replace(/For help, see:.*?\n/g, '')
.replace(/Debugger attached\.\n/g, '')
.replace(/Waiting for the debugger to disconnect\.\.\.\n/g, '')
t.strictEqual(filteredStderr, '', 'default max-http-header-size should not throw')
})
await t.completed
})
test('--max-http-header-size with Client API', async (t) => {
t = tspl(t, { plan: 6 })
const command = 'node --disable-warning=ExperimentalWarning -e "new (require(\'.\').Client)(new URL(\'http://localhost:200\'))"'
exec(`${command} --max-http-header-size=0`, { stdio: 'pipe' }, (err, stdout, stderr) => {
t.strictEqual(err.code, 1)
t.strictEqual(stdout, '')
t.match(stderr, /http module not available or http.maxHeaderSize invalid/, '--max-http-header-size=0 should result in an Error when using the Client API')
})
exec(command, { stdio: 'pipe' }, (err, stdout, stderr) => {
t.ifError(err)
t.strictEqual(stdout, '')
// Filter out debugger messages that may appear when running with --inspect
const filteredStderr = stderr.replace(/Debugger listening on ws:\/\/.*?\n/g, '')
.replace(/For help, see:.*?\n/g, '')
.replace(/Debugger attached\.\n/g, '')
.replace(/Waiting for the debugger to disconnect\.\.\.\n/g, '')
t.strictEqual(filteredStderr, '', 'default max-http-header-size should not throw')
})
await t.completed
})
})
================================================
FILE: test/client-pipeline.js
================================================
'use strict'
const { tspl } = require('@matteo.collina/tspl')
const { test, after } = require('node:test')
const { Client, errors } = require('..')
const EE = require('node:events')
const { createServer } = require('node:http')
const {
pipeline,
Readable,
Transform,
Writable,
PassThrough
} = require('node:stream')
test('pipeline get', async (t) => {
t = tspl(t, { plan: 17 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
t.strictEqual('/', req.url)
t.strictEqual('GET', req.method)
t.strictEqual(`localhost:${server.address().port}`, req.headers.host)
t.strictEqual(undefined, req.headers['content-length'])
res.setHeader('Content-Type', 'text/plain')
res.end('hello')
})
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.close())
{
const bufs = []
const signal = new EE()
client.pipeline({ signal, path: '/', method: 'GET' }, ({ statusCode, headers, body }) => {
t.strictEqual(statusCode, 200)
t.strictEqual(headers['content-type'], 'text/plain')
t.strictEqual(signal.listenerCount('abort'), 1)
return body
})
.end()
.on('data', (buf) => {
bufs.push(buf)
})
.on('end', () => {
t.strictEqual('hello', Buffer.concat(bufs).toString('utf8'))
})
.on('close', () => {
t.strictEqual(signal.listenerCount('abort'), 0)
})
t.strictEqual(signal.listenerCount('abort'), 1)
}
{
const bufs = []
client.pipeline({ path: '/', method: 'GET' }, ({ statusCode, headers, body }) => {
t.strictEqual(statusCode, 200)
t.strictEqual(headers['content-type'], 'text/plain')
return body
})
.end()
.on('data', (buf) => {
bufs.push(buf)
})
.on('end', () => {
t.strictEqual('hello', Buffer.concat(bufs).toString('utf8'))
})
}
})
await t.completed
})
test('pipeline echo', async (t) => {
t = tspl(t, { plan: 2 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
req.pipe(res)
})
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.close())
let res = ''
const buf1 = Buffer.alloc(1e3).toString()
const buf2 = Buffer.alloc(1e6).toString()
pipeline(
new Readable({
read () {
this.push(buf1)
this.push(buf2)
this.push(null)
}
}),
client.pipeline({
path: '/',
method: 'PUT'
}, ({ body }) => {
return pipeline(body, new PassThrough(), () => {})
}),
new Writable({
write (chunk, encoding, callback) {
res += chunk.toString()
callback()
},
final (callback) {
t.strictEqual(res, buf1 + buf2)
callback()
}
}),
(err) => {
t.ifError(err)
}
)
})
await t.completed
})
test('pipeline ignore request body', async (t) => {
t = tspl(t, { plan: 2 })
let done
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.write('asd')
res.end()
done()
})
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.close())
let res = ''
const buf1 = Buffer.alloc(1e3).toString()
const buf2 = Buffer.alloc(1e6).toString()
pipeline(
new Readable({
read () {
this.push(buf1)
this.push(buf2)
done = () => this.push(null)
}
}),
client.pipeline({
path: '/',
method: 'PUT'
}, ({ body }) => {
return pipeline(body, new PassThrough(), () => {})
}),
new Writable({
write (chunk, encoding, callback) {
res += chunk.toString()
callback()
},
final (callback) {
t.strictEqual(res, 'asd')
callback()
}
}),
(err) => {
t.ifError(err)
}
)
})
await t.completed
})
test('pipeline invalid handler', async (t) => {
t = tspl(t, { plan: 1 })
const client = new Client('http://localhost:5000')
client.pipeline({}, null).on('error', (err) => {
t.ok(/handler/.test(err))
})
await t.completed
})
test('pipeline invalid handler return after destroy should not error', async (t) => {
t = tspl(t, { plan: 3 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
req.pipe(res)
})
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`, {
pipelining: 3
})
after(() => client.destroy())
const dup = client.pipeline({
path: '/',
method: 'GET'
}, ({ body }) => {
body.on('error', (err) => {
t.strictEqual(err.message, 'asd')
})
dup.destroy(new Error('asd'))
return {}
})
.on('error', (err) => {
t.strictEqual(err.message, 'asd')
})
.on('close', () => {
t.ok(true, 'pass')
})
.end()
})
await t.completed
})
test('pipeline error body', async (t) => {
t = tspl(t, { plan: 2 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
req.pipe(res)
})
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.close())
const buf = Buffer.alloc(1e6).toString()
pipeline(
new Readable({
read () {
this.push(buf)
}
}),
client.pipeline({
path: '/',
method: 'PUT'
}, ({ body }) => {
const pt = new PassThrough()
process.nextTick(() => {
pt.destroy(new Error('asd'))
})
body.on('error', (err) => {
t.ok(err)
})
return pipeline(body, pt, () => {})
}),
new PassThrough(),
(err) => {
t.ok(err)
}
)
})
await t.completed
})
test('pipeline destroy body', async (t) => {
t = tspl(t, { plan: 2 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
req.pipe(res)
})
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.close())
const buf = Buffer.alloc(1e6).toString()
pipeline(
new Readable({
read () {
this.push(buf)
}
}),
client.pipeline({
path: '/',
method: 'PUT'
}, ({ body }) => {
const pt = new PassThrough()
process.nextTick(() => {
pt.destroy()
})
body.on('error', (err) => {
t.ok(err)
})
return pipeline(body, pt, () => {})
}),
new PassThrough(),
(err) => {
t.ok(err)
}
)
})
await t.completed
})
test('pipeline backpressure', async (t) => {
t = tspl(t, { plan: 1 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
req.pipe(res)
})
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.destroy())
const buf = Buffer.alloc(1e6).toString()
const duplex = client.pipeline({
path: '/',
method: 'PUT'
}, ({ body }) => {
const pt = new PassThrough()
return pipeline(body, pt, () => {})
})
duplex.end(buf)
duplex.on('data', () => {
duplex.pause()
setImmediate(() => {
duplex.resume()
})
}).on('end', () => {
t.ok(true, 'pass')
})
})
await t.completed
})
test('pipeline invalid handler return', async (t) => {
t = tspl(t, { plan: 2 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
req.pipe(res)
})
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.destroy())
client.pipeline({
path: '/',
method: 'GET'
}, ({ body }) => {
// TODO: Should body cause unhandled exception?
body.on('error', () => {})
})
.on('error', (err) => {
t.ok(err instanceof errors.InvalidReturnValueError)
})
.end()
client.pipeline({
path: '/',
method: 'GET'
}, ({ body }) => {
// TODO: Should body cause unhandled exception?
body.on('error', () => {})
return {}
})
.on('error', (err) => {
t.ok(err instanceof errors.InvalidReturnValueError)
})
.end()
})
await t.completed
})
test('pipeline throw handler', async (t) => {
t = tspl(t, { plan: 1 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
req.pipe(res)
})
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.destroy())
client.pipeline({
path: '/',
method: 'GET'
}, ({ body }) => {
// TODO: Should body cause unhandled exception?
body.on('error', () => {})
throw new Error('asd')
})
.on('error', (err) => {
t.strictEqual(err.message, 'asd')
})
.end()
})
await t.completed
})
test('pipeline destroy and throw handler', async (t) => {
t = tspl(t, { plan: 2 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
req.pipe(res)
})
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.destroy())
const dup = client.pipeline({
path: '/',
method: 'GET'
}, ({ body }) => {
dup.destroy()
// TODO: Should body cause unhandled exception?
body.on('error', () => {})
throw new Error('asd')
})
.end()
.on('error', (err) => {
t.ok(err instanceof errors.RequestAbortedError)
})
.on('close', () => {
t.ok(true, 'pass')
})
})
await t.completed
})
test('pipeline abort res', async (t) => {
t = tspl(t, { plan: 2 })
let _res
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.write('asd')
_res = res
})
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.destroy())
client.pipeline({
path: '/',
method: 'GET'
}, ({ body }) => {
setImmediate(() => {
body.destroy()
_res.write('asdasdadasd')
const timeout = setTimeout(() => {
t.fail()
}, 100)
client.on('disconnect', () => {
clearTimeout(timeout)
t.ok(true, 'pass')
})
})
return body
})
.on('error', (err) => {
t.ok(err instanceof errors.RequestAbortedError)
})
.end()
})
await t.completed
})
test('pipeline abort server res', async (t) => {
t = tspl(t, { plan: 1 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.destroy()
})
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.destroy())
client.pipeline({
path: '/',
method: 'GET'
}, () => {
t.fail()
})
.on('error', (err) => {
t.ok(err instanceof errors.SocketError)
})
.end()
})
await t.completed
})
test('pipeline abort duplex', async (t) => {
t = tspl(t, { plan: 2 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.end()
})
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.destroy())
client.request({
path: '/',
method: 'PUT'
}, (err, data) => {
t.ifError(err)
data.body.resume()
client.pipeline({
path: '/',
method: 'PUT'
}, () => {
t.fail()
}).destroy().on('error', (err) => {
t.ok(err instanceof errors.RequestAbortedError)
})
})
})
await t.completed
})
test('pipeline abort piped res', async (t) => {
t = tspl(t, { plan: 1 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.write('asd')
})
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.destroy())
client.pipeline({
path: '/',
method: 'GET'
}, ({ body }) => {
const pt = new PassThrough()
setImmediate(() => {
pt.destroy()
})
return pipeline(body, pt, () => {})
})
.on('error', (err) => {
t.strictEqual(err.code, 'UND_ERR_ABORTED')
})
.end()
})
await t.completed
})
test('pipeline abort piped res 2', async (t) => {
t = tspl(t, { plan: 2 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.write('asd')
})
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.destroy())
client.pipeline({
path: '/',
method: 'GET'
}, ({ body }) => {
const pt = new PassThrough()
body.on('error', (err) => {
t.ok(err instanceof errors.RequestAbortedError)
})
setImmediate(() => {
pt.destroy()
})
body.pipe(pt)
return pt
})
.on('error', (err) => {
t.ok(err instanceof errors.RequestAbortedError)
})
.end()
})
await t.completed
})
test('pipeline abort piped res 3', async (t) => {
t = tspl(t, { plan: 2 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.write('asd')
})
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.destroy())
client.pipeline({
path: '/',
method: 'GET'
}, ({ body }) => {
const pt = new PassThrough()
body.on('error', (err) => {
t.strictEqual(err.message, 'asd')
})
setImmediate(() => {
pt.destroy(new Error('asd'))
})
body.pipe(pt)
return pt
})
.on('error', (err) => {
t.strictEqual(err.message, 'asd')
})
.end()
})
await t.completed
})
test('pipeline abort server res after headers', async (t) => {
t = tspl(t, { plan: 1 })
let _res
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.write('asd')
_res = res
})
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.destroy())
client.pipeline({
path: '/',
method: 'GET'
}, (data) => {
_res.destroy()
return data.body
})
.on('error', (err) => {
t.ok(err instanceof errors.SocketError)
})
.end()
})
await t.completed
})
test('pipeline w/ write abort server res after headers', async (t) => {
t = tspl(t, { plan: 1 })
let _res
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
req.pipe(res)
_res = res
})
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.destroy())
client.pipeline({
path: '/',
method: 'PUT'
}, (data) => {
_res.destroy()
return data.body
})
.on('error', (err) => {
t.ok(err instanceof errors.SocketError)
})
.resume()
.write('asd')
})
await t.completed
})
test('destroy in push', async (t) => {
t = tspl(t, { plan: 3 })
let _res
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.write('asd')
_res = res
})
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.close())
client.pipeline({ path: '/', method: 'GET' }, ({ body }) => {
body.once('data', () => {
_res.write('asd')
body.on('data', (buf) => {
body.destroy()
_res.end()
}).on('error', (err) => {
t.ok(err)
})
})
return body
}).on('error', (err) => {
t.ok(err)
}).resume().end()
client.pipeline({ path: '/', method: 'GET' }, ({ body }) => {
let buf = ''
body.on('data', (chunk) => {
buf = chunk.toString()
_res.end()
}).on('end', () => {
t.strictEqual('asd', buf)
})
return body
}).resume().end()
})
await t.completed
})
test('pipeline args validation', async (t) => {
t = tspl(t, { plan: 2 })
const client = new Client('http://localhost:5000')
const ret = client.pipeline(null, () => {})
ret.on('error', (err) => {
t.ok(/opts/.test(err.message))
t.ok(err instanceof errors.InvalidArgumentError)
})
await t.completed
})
test('pipeline factory throw not unhandled', async (t) => {
t = tspl(t, { plan: 1 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.write('asd')
})
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.destroy())
client.pipeline({
path: '/',
method: 'GET'
}, (data) => {
throw new Error('asd')
})
.on('error', (err) => {
t.ok(err)
})
.end()
})
await t.completed
})
test('pipeline destroy before dispatch', async (t) => {
t = tspl(t, { plan: 1 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.end('hello')
})
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.destroy())
client
.pipeline({ path: '/', method: 'GET' }, ({ body }) => {
return body
})
.on('error', (err) => {
t.ok(err)
})
.end()
.destroy()
})
await t.completed
})
test('pipeline legacy stream', async (t) => {
t = tspl(t, { plan: 1 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.write(Buffer.alloc(16e3))
setImmediate(() => {
res.end(Buffer.alloc(16e3))
})
})
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.close())
client
.pipeline({ path: '/', method: 'GET' }, ({ body }) => {
const pt = new PassThrough()
pt.pause = null
return body.pipe(pt)
})
.resume()
.on('end', () => {
t.ok(true, 'pass')
})
.end()
})
await t.completed
})
test('pipeline objectMode', async (t) => {
t = tspl(t, { plan: 1 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.end(JSON.stringify({ asd: 1 }))
})
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.close())
client
.pipeline({ path: '/', method: 'GET', objectMode: true }, ({ body }) => {
return pipeline(body, new Transform({
readableObjectMode: true,
transform (chunk, encoding, callback) {
callback(null, JSON.parse(chunk))
}
}), () => {})
})
.on('data', data => {
t.deepStrictEqual(data, { asd: 1 })
})
.end()
})
await t.completed
})
test('pipeline invalid opts', async (t) => {
t = tspl(t, { plan: 2 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.end(JSON.stringify({ asd: 1 }))
})
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.destroy())
client.close((err) => {
t.ifError(err)
})
client
.pipeline({ path: '/', method: 'GET', objectMode: true }, ({ body }) => {
t.fail()
})
.on('error', (err) => {
t.ok(err)
})
})
await t.completed
})
test('pipeline CONNECT throw', async (t) => {
t = tspl(t, { plan: 1 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.end('asd')
})
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.destroy())
client.pipeline({
path: '/',
method: 'CONNECT'
}, () => {
t.fail()
}).on('error', (err) => {
t.ok(err instanceof errors.InvalidArgumentError)
})
client.on('disconnect', () => {
t.fail()
})
})
await t.completed
})
test('pipeline body without destroy', async (t) => {
t = tspl(t, { plan: 1 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.end('asd')
})
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.destroy())
client.pipeline({
path: '/',
method: 'GET'
}, ({ body }) => {
const pt = new PassThrough({ autoDestroy: false })
pt.destroy = null
return body.pipe(pt)
})
.end()
.on('end', () => {
t.ok(true, 'pass')
})
.resume()
})
await t.completed
})
test('pipeline ignore 1xx', async (t) => {
t = tspl(t, { plan: 1 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.writeProcessing()
res.end('hello')
})
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.close())
let buf = ''
client.pipeline({
path: '/',
method: 'GET'
}, ({ body }) => body)
.on('data', (chunk) => {
buf += chunk
})
.on('end', () => {
t.strictEqual(buf, 'hello')
})
.end()
})
await t.completed
})
test('pipeline ignore 1xx and use onInfo', async (t) => {
t = tspl(t, { plan: 3 })
const infos = []
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.writeProcessing()
res.end('hello')
})
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.close())
let buf = ''
client.pipeline({
path: '/',
method: 'GET',
onInfo: (x) => {
infos.push(x)
}
}, ({ body }) => body)
.on('data', (chunk) => {
buf += chunk
})
.on('end', () => {
t.strictEqual(buf, 'hello')
t.strictEqual(infos.length, 1)
t.strictEqual(infos[0].statusCode, 102)
})
.end()
})
await t.completed
})
test('pipeline backpressure', async (t) => {
t = tspl(t, { plan: 1 })
const expected = Buffer.alloc(1e6).toString()
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.writeProcessing()
res.end(expected)
})
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.close())
let buf = ''
client.pipeline({
path: '/',
method: 'GET'
}, ({ body }) => body)
.end()
.pipe(new Transform({
highWaterMark: 1,
transform (chunk, encoding, callback) {
setImmediate(() => {
callback(null, chunk)
})
}
}))
.on('data', chunk => {
buf += chunk
})
.on('end', () => {
t.strictEqual(buf, expected)
})
})
await t.completed
})
test('pipeline abort after headers', async (t) => {
t = tspl(t, { plan: 1 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.writeProcessing()
res.write('asd')
setImmediate(() => {
res.write('asd')
})
})
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.destroy())
const signal = new EE()
client.pipeline({
path: '/',
method: 'GET',
signal
}, ({ body }) => {
process.nextTick(() => {
signal.emit('abort')
})
return body
})
.end()
.on('error', (err) => {
t.ok(err instanceof errors.RequestAbortedError)
})
})
await t.completed
})
================================================
FILE: test/client-pipelining.js
================================================
'use strict'
const { tspl } = require('@matteo.collina/tspl')
const { test, after } = require('node:test')
const { Client } = require('..')
const { createServer } = require('node:http')
const { finished, Readable } = require('node:stream')
const { kConnect } = require('../lib/core/symbols')
const EE = require('node:events')
const { kBusy, kRunning, kSize } = require('../lib/core/symbols')
const { maybeWrapStream, consts } = require('./utils/async-iterators')
test('20 times GET with pipelining 10', async (t) => {
const num = 20
t = tspl(t, { plan: 3 * num + 1 })
let count = 0
let countGreaterThanOne = false
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
count++
setTimeout(function () {
countGreaterThanOne = countGreaterThanOne || count > 1
res.end(req.url)
}, 10)
})
after(() => server.close())
// needed to check for a warning on the maxListeners on the socket
function onWarning (warning) {
if (!/ExperimentalWarning/.test(warning)) {
t.fail()
}
}
process.on('warning', onWarning)
after(() => {
process.removeListener('warning', onWarning)
})
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`, {
pipelining: 10
})
after(() => client.close())
client.on('disconnect', () => {
if (!client.closed && !client.destroyed) {
t.fail('unexpected disconnect')
}
})
for (let i = 0; i < num; i++) {
makeRequest(i)
}
function makeRequest (i) {
makeRequestAndExpectUrl(client, i, t, () => {
count--
if (i === num - 1) {
t.ok(countGreaterThanOne, 'seen more than one parallel request')
}
})
}
})
await t.completed
})
function makeRequestAndExpectUrl (client, i, t, cb) {
return client.request({ path: '/' + i, method: 'GET', blocking: false }, (err, { statusCode, headers, body }) => {
cb()
t.ifError(err)
t.strictEqual(statusCode, 200)
const bufs = []
body.on('data', (buf) => {
bufs.push(buf)
})
body.on('end', () => {
t.strictEqual('/' + i, Buffer.concat(bufs).toString('utf8'))
})
})
}
test('A client should enqueue as much as twice its pipelining factor', async (t) => {
const num = 10
let sent = 0
// x * 6 + 1 t.ok + 5 drain
t = tspl(t, { plan: num * 6 + 1 + 5 + 2 })
let count = 0
let countGreaterThanOne = false
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
count++
t.ok(count <= 5)
setTimeout(function () {
countGreaterThanOne = countGreaterThanOne || count > 1
res.end(req.url)
}, 10)
})
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`, {
pipelining: 2
})
after(() => client.close())
client.on('disconnect', () => {
if (!client.closed && !client.destroyed) {
t.fail('unexpected disconnect')
}
})
for (; sent < 2;) {
t.ok(client[kSize] <= client.pipelining, 'client is not full')
makeRequest()
t.ok(client[kSize] <= client.pipelining, 'we can send more requests')
}
t.ok(client[kBusy], 'client is busy')
t.ok(client[kSize] <= client.pipelining, 'client is full')
makeRequest()
t.ok(client[kBusy], 'we must stop now')
t.ok(client[kBusy], 'client is busy')
t.ok(client[kSize] > client.pipelining, 'client is full')
function makeRequest () {
makeRequestAndExpectUrl(client, sent++, t, () => {
count--
setImmediate(() => {
if (client[kSize] === 0) {
t.ok(countGreaterThanOne, 'seen more than one parallel request')
const start = sent
for (; sent < start + 2 && sent < num;) {
t.ok(client[kSize] <= client.pipelining, 'client is not full')
t.ok(makeRequest())
}
}
})
})
return client[kSize] <= client.pipelining
}
})
await t.completed
})
test('pipeline 1 is 1 active request', async (t) => {
t = tspl(t, { plan: 9 })
let res2
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.write('asd')
res2 = res
})
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`, {
pipelining: 1
})
after(() => client.destroy())
client.on('disconnect', () => {
if (!client.closed && !client.destroyed) {
t.fail('unexpected disconnect')
}
})
client.request({
path: '/',
method: 'GET'
}, (err, data) => {
t.strictEqual(client[kSize], 1)
t.ifError(err)
t.strictEqual(client.request({
path: '/',
method: 'GET'
}, (err, data) => {
t.ifError(err)
finished(data.body, (err) => {
t.ok(err)
client.close((err) => {
t.ifError(err)
})
})
data.body.destroy()
res2.end()
}), undefined)
data.body.resume()
res2.end()
})
t.ok(client[kSize] <= client.pipelining)
t.ok(client[kBusy])
t.strictEqual(client[kSize], 1)
})
await t.completed
})
test('pipelined chunked POST stream', async (t) => {
t = tspl(t, { plan: 4 + 8 + 8 })
let a = 0
let b = 0
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
req.on('data', chunk => {
// Make sure a and b don't interleave.
t.ok(a === 9 || b === 0)
res.write(chunk)
}).on('end', () => {
res.end()
})
})
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`, {
pipelining: 2
})
after(() => client.close())
client.on('disconnect', () => {
if (!client.closed && !client.destroyed) {
t.fail('unexpected disconnect')
}
})
client.request({
path: '/',
method: 'GET'
}, (err, { body }) => {
body.resume()
t.ifError(err)
})
client.request({
path: '/',
method: 'POST',
body: new Readable({
read () {
this.push(++a > 8 ? null : 'a')
}
})
}, (err, { body }) => {
body.resume()
t.ifError(err)
})
client.request({
path: '/',
method: 'GET'
}, (err, { body }) => {
body.resume()
t.ifError(err)
})
client.request({
path: '/',
method: 'POST',
body: new Readable({
read () {
this.push(++b > 8 ? null : 'b')
}
})
}, (err, { body }) => {
body.resume()
t.ifError(err)
})
})
await t.completed
})
test('pipelined chunked POST iterator', async (t) => {
t = tspl(t, { plan: 4 + 8 + 8 })
let a = 0
let b = 0
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
req.on('data', chunk => {
// Make sure a and b don't interleave.
t.ok(a === 9 || b === 0)
res.write(chunk)
}).on('end', () => {
res.end()
})
})
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`, {
pipelining: 2
})
after(() => client.close())
client.on('disconnect', () => {
if (!client.closed && !client.destroyed) {
t.fail('unexpected disconnect')
}
})
client.request({
path: '/',
method: 'GET'
}, (err, { body }) => {
body.resume()
t.ifError(err)
})
client.request({
path: '/',
method: 'POST',
body: (async function * () {
while (++a <= 8) {
yield 'a'
}
})()
}, (err, { body }) => {
body.resume()
t.ifError(err)
})
client.request({
path: '/',
method: 'GET'
}, (err, { body }) => {
body.resume()
t.ifError(err)
})
client.request({
path: '/',
method: 'POST',
body: (async function * () {
while (++b <= 8) {
yield 'b'
}
})()
}, (err, { body }) => {
body.resume()
t.ifError(err)
})
})
await t.completed
})
function errordInflightPost (bodyType) {
test(`errored POST body lets inflight complete ${bodyType}`, async (t) => {
t = tspl(t, { plan: 6 })
let serverRes
const server = createServer({ joinDuplicateHeaders: true })
server.on('request', (req, res) => {
serverRes = res
res.write('asd')
})
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`, {
pipelining: 2
})
after(() => client.destroy())
client.request({
path: '/',
method: 'GET'
}, (err, data) => {
t.ifError(err)
data.body
.resume()
.once('data', () => {
client.request({
path: '/',
method: 'POST',
opaque: 'asd',
body: maybeWrapStream(new Readable({
read () {
this.destroy(new Error('kaboom'))
}
}).once('error', (err) => {
t.ok(err)
}).on('error', () => {
// Readable emits error twice...
}), bodyType)
}, (err, data) => {
t.ok(err)
t.strictEqual(data.opaque, 'asd')
})
client.close((err) => {
t.ifError(err)
})
serverRes.end()
})
.on('end', () => {
t.ok(true, 'pass')
})
})
})
await t.completed
})
}
errordInflightPost(consts.STREAM)
errordInflightPost(consts.ASYNC_ITERATOR)
test('pipelining non-idempotent', async (t) => {
t = tspl(t, { plan: 4 })
const server = createServer({ joinDuplicateHeaders: true })
server.on('request', (req, res) => {
setTimeout(() => {
res.end('asd')
}, 10)
})
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`, {
pipelining: 2
})
after(() => client.close())
client.on('disconnect', () => {
if (!client.closed && !client.destroyed) {
t.fail('unexpected disconnect')
}
})
let ended = false
client.request({
path: '/',
method: 'GET'
}, (err, data) => {
t.ifError(err)
data.body
.resume()
.on('end', () => {
t.ok(true, 'pass')
ended = true
})
})
client.request({
path: '/',
method: 'GET',
idempotent: false
}, (err, data) => {
t.ifError(err)
t.strictEqual(ended, true)
data.body.resume()
})
})
await t.completed
})
function pipeliningNonIdempotentWithBody (bodyType) {
test(`pipelining non-idempotent w body ${bodyType}`, async (t) => {
t = tspl(t, { plan: 4 })
const server = createServer({ joinDuplicateHeaders: true })
server.on('request', (req, res) => {
setImmediate(() => {
res.end('asd')
})
})
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`, {
pipelining: 2
})
after(() => client.close())
client.on('disconnect', () => {
if (!client.closed && !client.destroyed) {
t.fail('unexpected disconnect')
}
})
let ended = false
let reading = false
client.request({
path: '/',
method: 'POST',
body: maybeWrapStream(new Readable({
read () {
if (reading) {
return
}
reading = true
this.push('asd')
setImmediate(() => {
this.push(null)
ended = true
})
}
}), bodyType)
}, (err, data) => {
t.ifError(err)
data.body
.resume()
.on('end', () => {
t.ok(true, 'pass')
})
})
client.request({
path: '/',
method: 'GET',
idempotent: false
}, (err, data) => {
t.ifError(err)
t.strictEqual(ended, true)
data.body.resume()
})
})
await t.completed
})
}
pipeliningNonIdempotentWithBody(consts.STREAM)
pipeliningNonIdempotentWithBody(consts.ASYNC_ITERATOR)
function pipeliningHeadBusy (bodyType) {
test(`pipelining HEAD busy ${bodyType}`, async (t) => {
t = tspl(t, { plan: 7 })
const server = createServer({ joinDuplicateHeaders: true })
server.on('request', (req, res) => {
res.end('asd')
})
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`, {
pipelining: 10
})
after(() => client.close())
client[kConnect](() => {
let ended = false
client.once('disconnect', () => {
t.strictEqual(ended, true)
})
{
const body = new Readable({
read () { }
})
client.request({
path: '/',
method: 'GET',
body: maybeWrapStream(body, bodyType)
}, (err, data) => {
t.ifError(err)
data.body
.resume()
.on('end', () => {
t.ok(true, 'pass')
})
})
body.push(null)
t.strictEqual(client[kBusy], true)
}
{
const body = new Readable({
read () { }
})
client.request({
path: '/',
method: 'HEAD',
body: maybeWrapStream(body, bodyType)
}, (err, data) => {
t.ifError(err)
data.body
.resume()
.on('end', () => {
ended = true
t.ok(true, 'pass')
})
})
body.push(null)
t.strictEqual(client[kBusy], true)
}
})
})
await t.completed
})
}
pipeliningHeadBusy(consts.STREAM)
pipeliningHeadBusy(consts.ASYNC_ITERATOR)
test('pipelining empty pipeline before reset', async (t) => {
t = tspl(t, { plan: 8 })
let c = 0
const server = createServer({ joinDuplicateHeaders: true })
server.on('request', (req, res) => {
if (c++ === 0) {
res.end('asd')
} else {
setTimeout(() => {
res.end('asd')
}, 100)
}
})
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`, {
pipelining: 10
})
after(() => client.close())
client[kConnect](() => {
let ended = false
client.once('disconnect', () => {
t.strictEqual(ended, true)
})
client.request({
path: '/',
method: 'GET',
blocking: false
}, (err, data) => {
t.ifError(err)
data.body
.resume()
.on('end', () => {
t.ok(true, 'pass')
})
})
t.strictEqual(client[kBusy], false)
client.request({
path: '/',
method: 'HEAD',
body: 'asd'
}, (err, data) => {
t.ifError(err)
data.body
.resume()
.on('end', () => {
ended = true
t.ok(true, 'pass')
})
})
t.strictEqual(client[kBusy], true)
t.strictEqual(client[kRunning], 2)
})
})
await t.completed
})
function pipeliningIdempotentBusy (bodyType) {
test(`pipelining idempotent busy ${bodyType}`, async (t) => {
t = tspl(t, { plan: 12 })
const server = createServer({ joinDuplicateHeaders: true })
server.on('request', (req, res) => {
res.end('asd')
})
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`, {
pipelining: 10
})
after(() => client.close())
{
const body = new Readable({
read () { }
})
client.request({
path: '/',
method: 'GET',
body: maybeWrapStream(body, bodyType)
}, (err, data) => {
t.ifError(err)
data.body
.resume()
.on('end', () => {
t.ok(true, 'pass')
})
})
body.push(null)
t.strictEqual(client[kBusy], true)
}
client[kConnect](() => {
{
const body = new Readable({
read () { }
})
client.request({
path: '/',
method: 'GET',
body: maybeWrapStream(body, bodyType)
}, (err, data) => {
t.ifError(err)
data.body
.resume()
.on('end', () => {
t.ok(true, 'pass')
})
})
body.push(null)
t.strictEqual(client[kBusy], true)
}
{
const signal = new EE()
const body = new Readable({
read () { }
})
client.request({
path: '/',
method: 'GET',
body: maybeWrapStream(body, bodyType),
signal
}, (err, data) => {
t.ok(err)
})
t.strictEqual(client[kBusy], true)
signal.emit('abort')
t.strictEqual(client[kBusy], true)
}
{
const body = new Readable({
read () { }
})
client.request({
path: '/',
method: 'GET',
idempotent: false,
body: maybeWrapStream(body, bodyType)
}, (err, data) => {
t.ifError(err)
data.body
.resume()
.on('end', () => {
t.ok(true, 'pass')
})
})
body.push(null)
t.strictEqual(client[kBusy], true)
}
})
})
await t.completed
})
}
pipeliningIdempotentBusy(consts.STREAM)
pipeliningIdempotentBusy(consts.ASYNC_ITERATOR)
test('pipelining blocked', async (t) => {
t = tspl(t, { plan: 6 })
const server = createServer({ joinDuplicateHeaders: true })
let blocking = true
let count = 0
server.on('request', (req, res) => {
t.ok(!count || !blocking)
count++
setImmediate(() => {
res.end('asd')
})
})
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`, {
pipelining: 10
})
after(() => client.close())
client.on('disconnect', () => {
if (!client.closed && !client.destroyed) {
t.fail('unexpected disconnect')
}
})
client.request({
path: '/',
method: 'GET',
blocking: true
}, (err, data) => {
t.ifError(err)
blocking = false
data.body
.resume()
.on('end', () => {
t.ok(true, 'pass')
})
})
client.request({
path: '/',
method: 'GET'
}, (err, data) => {
t.ifError(err)
data.body
.resume()
.on('end', () => {
t.ok(true, 'pass')
})
})
})
await t.completed
})
================================================
FILE: test/client-post.js
================================================
'use strict'
const { tspl } = require('@matteo.collina/tspl')
const { test, after } = require('node:test')
const { once } = require('node:events')
const { Client } = require('..')
const { createServer } = require('node:http')
test('request post blob', async (t) => {
t = tspl(t, { plan: 3 })
const server = createServer({ joinDuplicateHeaders: true }, async (req, res) => {
t.strictEqual(req.headers['content-type'], 'application/json')
let str = ''
for await (const chunk of req) {
str += chunk
}
t.strictEqual(str, 'asd')
res.end()
})
after(server.close.bind(server))
server.listen(0)
await once(server, 'listening')
const client = new Client(`http://localhost:${server.address().port}`)
after(client.destroy.bind(client))
client.on('disconnect', () => {
if (!client.closed && !client.destroyed) {
t.fail('unexpected disconnect')
}
})
client.request({
path: '/',
method: 'GET',
body: new Blob(['asd'], {
type: 'application/json'
})
}, (err, data) => {
t.ifError(err)
data.body.resume().on('end', () => {
t.end()
})
})
await t.completed
})
test('request post arrayBuffer', async (t) => {
t = tspl(t, { plan: 3 })
const server = createServer({ joinDuplicateHeaders: true }, async (req, res) => {
let str = ''
for await (const chunk of req) {
str += chunk
}
t.strictEqual(str, 'asd')
res.end()
})
after(() => server.close())
server.listen(0)
await once(server, 'listening')
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.destroy())
client.on('disconnect', () => {
if (!client.closed && !client.destroyed) {
t.fail('unexpected disconnect')
}
})
const buf = Buffer.from('asd')
const dst = new ArrayBuffer(buf.byteLength)
buf.copy(new Uint8Array(dst))
client.request({
path: '/',
method: 'GET',
body: dst
}, (err, data) => {
t.ifError(err)
data.body.resume().on('end', () => {
t.ok(true, 'pass')
})
})
await t.completed
})
================================================
FILE: test/client-reconnect.js
================================================
'use strict'
const { tspl } = require('@matteo.collina/tspl')
const { test, after } = require('node:test')
const { once } = require('node:events')
const { Client } = require('..')
const { createServer } = require('node:http')
const FakeTimers = require('@sinonjs/fake-timers')
const timers = require('../lib/util/timers')
test('multiple reconnect', async (t) => {
t = tspl(t, { plan: 5 })
let n = 0
const clock = FakeTimers.install()
after(() => clock.uninstall())
const orgTimers = { ...timers }
Object.assign(timers, { setTimeout, clearTimeout })
after(() => {
Object.assign(timers, orgTimers)
})
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
n === 0 ? res.destroy() : res.end('ok')
})
after(() => server.close())
server.listen(0)
await once(server, 'listening')
const client = new Client(`http://localhost:${server.address().port}`)
after(client.destroy.bind(client))
client.request({ path: '/', method: 'GET' }, (err, data) => {
t.ok(err)
t.strictEqual(err.code, 'UND_ERR_SOCKET')
})
client.request({ path: '/', method: 'GET' }, (err, data) => {
t.ifError(err)
data.body
.resume()
.on('end', () => {
t.ok(true, 'pass')
})
})
client.on('disconnect', () => {
if (++n === 1) {
t.ok(true, 'pass')
}
process.nextTick(() => {
clock.tick(1000)
})
})
await t.completed
})
================================================
FILE: test/client-request.js
================================================
/* globals AbortController */
'use strict'
const { tspl } = require('@matteo.collina/tspl')
const { test, after, describe, before } = require('node:test')
const { Client, errors } = require('..')
const { createServer } = require('node:http')
const EE = require('node:events')
const { kConnect } = require('../lib/core/symbols')
const { Readable } = require('node:stream')
const net = require('node:net')
const { promisify } = require('node:util')
const { NotSupportedError, InvalidArgumentError, AbortError } = require('../lib/core/errors')
const { parseFormDataString } = require('./utils/formdata')
test('request dump head', async (t) => {
t = tspl(t, { plan: 3 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.setHeader('content-length', 5 * 100)
res.flushHeaders()
res.write('hello'.repeat(100))
})
after(() => {
server.closeAllConnections?.()
server.close()
})
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.destroy())
let dumped = false
client.on('disconnect', () => {
t.strictEqual(dumped, true)
})
client.request({
path: '/',
method: 'HEAD'
}, (err, { body }) => {
t.ifError(err)
body.dump({ limit: 1 }).then(() => {
dumped = true
t.ok(true, 'pass')
})
})
})
await t.completed
})
test('request dump big', async (t) => {
t = tspl(t, { plan: 3 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.setHeader('content-length', 999999999)
while (res.write('asd')) {
// Do nothing...
}
})
after(() => {
server.closeAllConnections?.()
server.close()
})
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.destroy())
let dumped = false
client.on('disconnect', () => {
t.strictEqual(dumped, true)
})
client.request({
path: '/',
method: 'GET'
}, (err, { body }) => {
t.ifError(err)
body.on('data', () => t.fail())
body.dump().then(() => {
dumped = true
t.ok(true, 'pass')
})
})
})
await t.completed
})
test('request dump', async (t) => {
t = tspl(t, { plan: 3 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.shouldKeepAlive = false
res.setHeader('content-length', 5)
res.end('hello')
})
after(() => {
server.closeAllConnections?.()
server.close()
})
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.destroy())
let dumped = false
client.on('disconnect', () => {
t.strictEqual(dumped, true)
})
client.request({
path: '/',
method: 'GET'
}, (err, { body }) => {
t.ifError(err)
body.dump().then(() => {
dumped = true
t.ok(true, 'pass')
})
})
})
await t.completed
})
test('request dump with abort signal', async (t) => {
t = tspl(t, { plan: 10 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.write('hello')
})
after(() => {
server.closeAllConnections?.()
server.close()
})
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.destroy())
client.request({
path: '/',
method: 'GET'
}, (err, { body }) => {
t.ifError(err)
const ac = new AbortController()
body.dump({ signal: ac.signal }).catch((err) => {
t.strictEqual(err.name, 'AbortError')
t.strictEqual(err.message, 'This operation was aborted')
const stackLines = err.stack.split('\n').map((l) => l.trim())
t.ok(stackLines[0].startsWith('AbortError: This operation was aborted'))
t.ok(stackLines[1].startsWith('at new DOMException'))
t.ok(stackLines[2].startsWith('at AbortController.abort'))
t.ok(/client-request.js/.test(stackLines[3]))
t.ok(stackLines[4].startsWith('at RequestHandler.runInAsyncScope'))
t.ok(stackLines[5].startsWith('at RequestHandler.onHeaders'))
t.ok(stackLines[6].startsWith('at Request.onHeaders'))
server.close()
})
ac.abort()
})
})
await t.completed
})
test('request dump with POJO as invalid signal', async (t) => {
t = tspl(t, { plan: 9 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.write('hello')
})
after(() => {
server.closeAllConnections?.()
server.close()
})
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.destroy())
client.request({
path: '/',
method: 'GET'
}, (err, { body }) => {
t.ifError(err)
body.dump({ signal: {} }).catch((err) => {
t.strictEqual(err.name, 'InvalidArgumentError')
t.strictEqual(err.message, 'signal must be an AbortSignal')
const stackLines = err.stack.split('\n').map((l) => l.trim())
t.ok(stackLines[0].startsWith('InvalidArgumentError: signal must be an AbortSignal'))
t.ok(stackLines[1].startsWith('at BodyReadable.dump'))
t.ok(/client-request.js/.test(stackLines[2]))
t.ok(stackLines[3].startsWith('at RequestHandler.runInAsyncScope'))
t.ok(stackLines[4].startsWith('at RequestHandler.onHeaders'))
t.ok(stackLines[5].startsWith('at Request.onHeaders'))
server.close()
})
})
})
await t.completed
})
test('request dump with aborted signal', async (t) => {
t = tspl(t, { plan: 8 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.write('hello')
})
after(() => {
server.closeAllConnections?.()
server.close()
})
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.destroy())
client.request({
path: '/',
method: 'GET'
}, (err, { body }) => {
t.ifError(err)
const ac = new AbortController()
ac.abort(new AbortError('This operation was with purpose aborted'))
body.dump({ signal: ac.signal }).catch((err) => {
t.strictEqual(err.name, 'AbortError')
t.strictEqual(err.message, 'This operation was with purpose aborted')
const stackLines = err.stack.split('\n').map((l) => l.trim())
t.ok(stackLines[0].startsWith('AbortError: This operation was with purpose aborted'))
t.ok(/client-request.js/.test(stackLines[1]))
t.ok(stackLines[2].startsWith('at RequestHandler.runInAsyncScope'))
t.ok(stackLines[3].startsWith('at RequestHandler.onHeaders'))
t.ok(stackLines[4].startsWith('at Request.onHeaders'))
server.close()
})
ac.abort()
})
})
await t.completed
})
test('request hwm', async (t) => {
t = tspl(t, { plan: 2 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.write('hello')
})
after(() => {
server.closeAllConnections?.()
server.close()
})
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.destroy())
client.request({
path: '/',
method: 'GET',
highWaterMark: 1000
}, (err, { body }) => {
t.ifError(err)
t.deepStrictEqual(body.readableHighWaterMark, 1000)
body.dump()
})
})
await t.completed
})
test('request abort before headers', async (t) => {
t = tspl(t, { plan: 6 })
const signal = new EE()
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.end('hello')
signal.emit('abort')
})
after(() => {
server.closeAllConnections?.()
server.close()
})
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.destroy())
client[kConnect](() => {
client.request({
path: '/',
method: 'GET',
signal
}, (err) => {
t.ok(err instanceof errors.RequestAbortedError)
t.strictEqual(signal.listenerCount('abort'), 0)
})
t.strictEqual(signal.listenerCount('abort'), 1)
client.request({
path: '/',
method: 'GET',
signal
}, (err) => {
t.ok(err instanceof errors.RequestAbortedError)
t.strictEqual(signal.listenerCount('abort'), 0)
})
t.strictEqual(signal.listenerCount('abort'), 2)
})
})
await t.completed
})
test('request body destroyed on invalid callback', async (t) => {
t = tspl(t, { plan: 1 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
})
after(() => {
server.closeAllConnections?.()
server.close()
})
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.destroy())
const body = new Readable({
read () { }
})
try {
client.request({
path: '/',
method: 'GET',
body
}, null)
} catch (err) {
t.strictEqual(body.destroyed, true)
}
})
await t.completed
})
test('trailers', async (t) => {
t = tspl(t, { plan: 1 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.writeHead(200, { Trailer: 'Content-MD5' })
res.addTrailers({ 'Content-MD5': 'test' })
res.end()
})
after(() => {
server.closeAllConnections?.()
server.close()
})
server.listen(0, async () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.close())
const { body, trailers } = await client.request({
path: '/',
method: 'GET'
})
body
.on('data', () => t.fail())
.on('end', () => {
t.deepStrictEqual(trailers, { 'content-md5': 'test' })
})
})
await t.completed
})
test('destroy socket abruptly', async (t) => {
t = tspl(t, { plan: 2 })
const server = net.createServer({ joinDuplicateHeaders: true }, (socket) => {
const lines = [
'HTTP/1.1 200 OK',
'Date: Sat, 09 Oct 2010 14:28:02 GMT',
'Connection: close',
'',
'the body'
]
socket.end(lines.join('\r\n'))
// Unfortunately calling destroy synchronously might get us flaky results,
// therefore we delay it to the next event loop run.
setImmediate(socket.destroy.bind(socket))
})
after(() => {
server.closeAllConnections?.()
server.close()
})
await promisify(server.listen.bind(server))(0)
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.close())
const { statusCode, body } = await client.request({
path: '/',
method: 'GET'
})
t.strictEqual(statusCode, 200)
body.setEncoding('utf8')
let actual = ''
for await (const chunk of body) {
actual += chunk
}
t.strictEqual(actual, 'the body')
})
test('destroy socket abruptly with keep-alive', async (t) => {
t = tspl(t, { plan: 2 })
const server = net.createServer({ joinDuplicateHeaders: true }, (socket) => {
const lines = [
'HTTP/1.1 200 OK',
'Date: Sat, 09 Oct 2010 14:28:02 GMT',
'Connection: keep-alive',
'Content-Length: 42',
'',
'the body'
]
socket.end(lines.join('\r\n'))
// Unfortunately calling destroy synchronously might get us flaky results,
// therefore we delay it to the next event loop run.
setImmediate(socket.destroy.bind(socket))
})
after(() => {
server.closeAllConnections?.()
server.close()
})
await promisify(server.listen.bind(server))(0)
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.close())
const { statusCode, body } = await client.request({
path: '/',
method: 'GET'
})
t.strictEqual(statusCode, 200)
body.setEncoding('utf8')
try {
/* eslint-disable */
for await (const _ of body) {
// empty on purpose
}
/* eslint-enable */
t.fail('no error')
} catch (err) {
t.ok(true, 'error happened')
}
})
test('request json', async (t) => {
t = tspl(t, { plan: 1 })
const obj = { asd: true }
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.end(JSON.stringify(obj))
})
after(() => {
server.closeAllConnections?.()
server.close()
})
server.listen(0, async () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.destroy())
const { body } = await client.request({
path: '/',
method: 'GET'
})
t.deepStrictEqual(obj, await body.json())
})
await t.completed
})
test('request long multibyte json', async (t) => {
t = tspl(t, { plan: 1 })
const obj = { asd: 'あ'.repeat(100000) }
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.end(JSON.stringify(obj))
})
after(() => {
server.closeAllConnections?.()
server.close()
})
server.listen(0, async () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.destroy())
const { body } = await client.request({
path: '/',
method: 'GET'
})
t.deepStrictEqual(obj, await body.json())
})
await t.completed
})
test('request text', async (t) => {
t = tspl(t, { plan: 1 })
const obj = { asd: true }
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.end(JSON.stringify(obj))
})
after(() => {
server.closeAllConnections?.()
server.close()
})
server.listen(0, async () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.destroy())
const { body } = await client.request({
path: '/',
method: 'GET'
})
t.strictEqual(JSON.stringify(obj), await body.text())
})
await t.completed
})
describe('headers', () => {
describe('invalid headers', () => {
test('invalid header value - array with string with invalid character', async (t) => {
t = tspl(t, { plan: 1 })
const client = new Client('http://localhost:8080')
after(() => client.destroy())
t.rejects(client.request({
path: '/',
method: 'GET',
headers: { name: ['test\0'] }
}), new InvalidArgumentError('invalid name header'))
await t.completed
})
test('invalid header value - array with POJO', async (t) => {
t = tspl(t, { plan: 1 })
const client = new Client('http://localhost:8080')
after(() => client.destroy())
t.rejects(client.request({
path: '/',
method: 'GET',
headers: { name: [{}] }
}), new InvalidArgumentError('invalid name header'))
await t.completed
})
test('invalid header value - string with invalid character', async (t) => {
t = tspl(t, { plan: 1 })
const client = new Client('http://localhost:8080')
after(() => client.destroy())
t.rejects(client.request({
path: '/',
method: 'GET',
headers: { name: 'test\0' }
}), new InvalidArgumentError('invalid name header'))
await t.completed
})
test('invalid header value - object', async (t) => {
t = tspl(t, { plan: 1 })
const client = new Client('http://localhost:8080')
after(() => client.destroy())
t.rejects(client.request({
path: '/',
method: 'GET',
headers: { name: new Date() }
}), new InvalidArgumentError('invalid name header'))
await t.completed
})
})
describe('array', () => {
let serverAddress
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.end(JSON.stringify(req.headers))
})
before(async () => {
server.listen(0)
await EE.once(server, 'listening')
serverAddress = `localhost:${server.address().port}`
})
after(() => {
server.closeAllConnections()
server.close()
})
test('empty host header', async (t) => {
t = tspl(t, { plan: 4 })
const client = new Client(`http://${serverAddress}`)
after(() => client.destroy())
const testCase = async (expected, actual) => {
const { body } = await client.request({
path: '/',
method: 'GET',
headers: expected
})
const result = await body.json()
t.deepStrictEqual(result, { ...result, ...actual })
}
await testCase({ key: [null] }, { key: '' })
await testCase({ key: ['test'] }, { key: 'test' })
await testCase({ key: ['test', 'true'] }, { key: 'test, true' })
await testCase({ key: ['test', true] }, { key: 'test, true' })
await t.completed
})
})
describe('host', () => {
let serverAddress
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.end(req.headers.host)
})
before(async () => {
server.listen(0)
await EE.once(server, 'listening')
serverAddress = `localhost:${server.address().port}`
})
after(() => {
server.closeAllConnections()
server.close()
})
test('invalid host header', async (t) => {
t = tspl(t, { plan: 1 })
const client = new Client(`http://${serverAddress}`)
after(() => client.destroy())
t.rejects(client.request({
path: '/',
method: 'GET',
headers: {
host: [
'www.example.com'
]
}
}), new InvalidArgumentError('invalid host header'))
await t.completed
})
test('empty host header', async (t) => {
t = tspl(t, { plan: 3 })
const client = new Client(`http://${serverAddress}`)
after(() => client.destroy())
const getWithHost = async (host, wanted) => {
const { body } = await client.request({
path: '/',
method: 'GET',
headers: { host }
})
t.strictEqual(await body.text(), wanted)
}
await getWithHost('test', 'test')
await getWithHost(undefined, serverAddress)
await getWithHost('', '')
await t.completed
})
})
})
test('request long multibyte text', async (t) => {
t = tspl(t, { plan: 1 })
const obj = { asd: 'あ'.repeat(100000) }
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.end(JSON.stringify(obj))
})
after(() => {
server.closeAllConnections?.()
server.close()
})
server.listen(0, async () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.destroy())
const { body } = await client.request({
path: '/',
method: 'GET'
})
t.strictEqual(JSON.stringify(obj), await body.text())
})
await t.completed
})
test('request blob', async (t) => {
t = tspl(t, { plan: 2 })
const obj = { asd: true }
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.setHeader('Content-Type', 'application/json')
res.end(JSON.stringify(obj))
})
after(() => {
server.closeAllConnections?.()
server.close()
})
server.listen(0, async () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.destroy())
const { body } = await client.request({
path: '/',
method: 'GET'
})
const blob = await body.blob()
t.deepStrictEqual(obj, JSON.parse(await blob.text()))
t.strictEqual(blob.type, 'application/json')
})
await t.completed
})
test('request arrayBuffer', async (t) => {
t = tspl(t, { plan: 2 })
const obj = { asd: true }
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.end(JSON.stringify(obj))
})
after(() => {
server.closeAllConnections?.()
server.close()
})
server.listen(0, async () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.destroy())
const { body } = await client.request({
path: '/',
method: 'GET'
})
const ab = await body.arrayBuffer()
t.deepStrictEqual(Buffer.from(JSON.stringify(obj)), Buffer.from(ab))
t.ok(ab instanceof ArrayBuffer)
})
await t.completed
})
test('request bytes', async (t) => {
t = tspl(t, { plan: 2 })
const obj = { asd: true }
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.end(JSON.stringify(obj))
})
after(() => {
server.closeAllConnections?.()
server.close()
})
server.listen(0, async () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.destroy())
const { body } = await client.request({
path: '/',
method: 'GET'
})
const bytes = await body.bytes()
t.deepStrictEqual(new TextEncoder().encode(JSON.stringify(obj)), bytes)
t.ok(bytes instanceof Uint8Array)
})
await t.completed
})
test('request body', async (t) => {
t = tspl(t, { plan: 1 })
const obj = { asd: true }
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.end(JSON.stringify(obj))
})
after(() => {
server.closeAllConnections?.()
server.close()
})
server.listen(0, async () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.destroy())
const { body } = await client.request({
path: '/',
method: 'GET'
})
let x = ''
for await (const chunk of body.body) {
x += Buffer.from(chunk)
}
t.strictEqual(JSON.stringify(obj), x)
})
await t.completed
})
test('request post body no missing data', async (t) => {
t = tspl(t, { plan: 2 })
const server = createServer({ joinDuplicateHeaders: true }, async (req, res) => {
let ret = ''
for await (const chunk of req) {
ret += chunk
}
t.strictEqual(ret, 'asd')
res.end()
})
after(() => {
server.closeAllConnections?.()
server.close()
})
server.listen(0, async () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.destroy())
const { body } = await client.request({
path: '/',
method: 'GET',
body: new Readable({
read () {
this.push('asd')
this.push(null)
}
})
})
await body.text()
t.ok(true, 'pass')
})
await t.completed
})
test('request post body no extra data handler', async (t) => {
t = tspl(t, { plan: 3 })
const server = createServer({ joinDuplicateHeaders: true }, async (req, res) => {
let ret = ''
for await (const chunk of req) {
ret += chunk
}
t.strictEqual(ret, 'asd')
res.end()
})
after(() => {
server.closeAllConnections?.()
server.close()
})
server.listen(0, async () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.destroy())
const reqBody = new Readable({
read () {
this.push('asd')
this.push(null)
}
})
process.nextTick(() => {
t.strictEqual(reqBody.listenerCount('data'), 0)
})
const { body } = await client.request({
path: '/',
method: 'GET',
body: reqBody
})
await body.text()
t.ok(true, 'pass')
})
await t.completed
})
test('request with onInfo callback', async (t) => {
t = tspl(t, { plan: 3 })
const infos = []
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.writeProcessing()
res.setHeader('Content-Type', 'application/json')
res.end(JSON.stringify({ foo: 'bar' }))
})
after(() => {
server.closeAllConnections?.()
server.close()
})
server.listen(0, async () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.destroy())
await client.request({
path: '/',
method: 'GET',
onInfo: (x) => { infos.push(x) }
})
t.strictEqual(infos.length, 1)
t.strictEqual(infos[0].statusCode, 102)
t.ok(true, 'pass')
})
await t.completed
})
test('request with onInfo callback but socket is destroyed before end of response', async (t) => {
t = tspl(t, { plan: 5 })
const infos = []
let response
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
response = res
res.writeProcessing()
})
after(() => {
server.closeAllConnections?.()
server.close()
})
server.listen(0, async () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.destroy())
try {
await client.request({
path: '/',
method: 'GET',
onInfo: (x) => {
infos.push(x)
response.destroy()
}
})
t.fail()
} catch (e) {
t.ok(e)
t.strictEqual(e.message, 'other side closed')
}
t.strictEqual(infos.length, 1)
t.strictEqual(infos[0].statusCode, 102)
t.ok(true, 'pass')
})
await t.completed
})
test('request onInfo callback headers parsing', async (t) => {
t = tspl(t, { plan: 4 })
const infos = []
const server = net.createServer({ joinDuplicateHeaders: true }, (socket) => {
const lines = [
'HTTP/1.1 103 Early Hints',
'Link: ; rel=preload; as=style',
'',
'HTTP/1.1 200 OK',
'Date: Sat, 09 Oct 2010 14:28:02 GMT',
'Connection: close',
'',
'the body'
]
socket.end(lines.join('\r\n'))
})
after(() => {
server.closeAllConnections?.()
server.close()
})
await promisify(server.listen.bind(server))(0)
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.close())
const { body } = await client.request({
path: '/',
method: 'GET',
onInfo: (x) => { infos.push(x) }
})
await body.dump()
t.strictEqual(infos.length, 1)
t.strictEqual(infos[0].statusCode, 103)
t.deepStrictEqual(infos[0].headers, { link: '; rel=preload; as=style' })
t.ok(true, 'pass')
})
test('request raw responseHeaders', async (t) => {
t = tspl(t, { plan: 4 })
const infos = []
const server = net.createServer({ joinDuplicateHeaders: true }, (socket) => {
const lines = [
'HTTP/1.1 103 Early Hints',
'Link: ; rel=preload; as=style',
'',
'HTTP/1.1 200 OK',
'Date: Sat, 09 Oct 2010 14:28:02 GMT',
'Connection: close',
'',
'the body'
]
socket.end(lines.join('\r\n'))
})
after(() => {
server.closeAllConnections?.()
server.close()
})
await promisify(server.listen.bind(server))(0)
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.close())
const { body, headers } = await client.request({
path: '/',
method: 'GET',
responseHeaders: 'raw',
onInfo: (x) => { infos.push(x) }
})
await body.dump()
t.strictEqual(infos.length, 1)
t.deepStrictEqual(infos[0].headers, ['Link', '; rel=preload; as=style'])
t.deepStrictEqual(headers, ['Date', 'Sat, 09 Oct 2010 14:28:02 GMT', 'Connection', 'close'])
t.ok(true, 'pass')
})
test('request formData', async (t) => {
t = tspl(t, { plan: 1 })
const obj = { asd: true }
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.end(JSON.stringify(obj))
})
after(() => {
server.closeAllConnections?.()
server.close()
})
server.listen(0, async () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.destroy())
const { body } = await client.request({
path: '/',
method: 'GET'
})
try {
await body.formData()
t.fail('should throw NotSupportedError')
} catch (error) {
t.ok(error instanceof NotSupportedError)
}
})
await t.completed
})
test('request text2', async (t) => {
t = tspl(t, { plan: 2 })
const obj = { asd: true }
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.end(JSON.stringify(obj))
})
after(() => {
server.closeAllConnections?.()
server.close()
})
server.listen(0, async () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.destroy())
const { body } = await client.request({
path: '/',
method: 'GET'
})
const p = body.text()
let ret = ''
body.on('data', chunk => {
ret += chunk
}).on('end', () => {
t.strictEqual(JSON.stringify(obj), ret)
})
t.strictEqual(JSON.stringify(obj), await p)
})
await t.completed
})
test('request with FormData body', async (t) => {
const { FormData } = require('../')
const fd = new FormData()
fd.set('key', 'value')
fd.set('file', new Blob(['Hello, world!']), 'hello_world.txt')
const server = createServer({ joinDuplicateHeaders: true }, async (req, res) => {
const contentType = req.headers['content-type']
// ensure we received a multipart/form-data header
t.ok(/^multipart\/form-data; boundary=-+formdata-undici-0\d+$/.test(contentType))
const chunks = []
for await (const chunk of req) {
chunks.push(chunk)
}
const { fileMap, fields } = await parseFormDataString(
Buffer.concat(chunks),
contentType
)
t.deepStrictEqual(fields[0], { key: 'key', value: 'value' })
t.ok(fileMap.has('file'))
t.strictEqual(fileMap.get('file').data.toString(), 'Hello, world!')
t.deepStrictEqual(fileMap.get('file').info, {
filename: 'hello_world.txt',
encoding: '7bit',
mimeType: 'application/octet-stream'
})
return res.end()
})
after(() => {
server.closeAllConnections?.()
server.close()
})
server.listen(0, async () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.destroy())
await client.request({
path: '/',
method: 'POST',
body: fd
})
t.end()
})
await t.completed
})
test('request post body Buffer from string', async (t) => {
t = tspl(t, { plan: 2 })
const requestBody = Buffer.from('abcdefghijklmnopqrstuvwxyz')
const server = createServer({ joinDuplicateHeaders: true }, async (req, res) => {
let ret = ''
for await (const chunk of req) {
ret += chunk
}
t.strictEqual(ret, 'abcdefghijklmnopqrstuvwxyz')
res.end()
})
after(() => {
server.closeAllConnections?.()
server.close()
})
server.listen(0, async () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.destroy())
const { body } = await client.request({
path: '/',
method: 'POST',
body: requestBody
})
await body.text()
t.ok(true, 'pass')
})
await t.completed
})
test('request post body Buffer from buffer', async (t) => {
t = tspl(t, { plan: 2 })
const fullBuffer = new TextEncoder().encode('abcdefghijklmnopqrstuvwxyz')
const requestBody = Buffer.from(fullBuffer.buffer, 8, 16)
const server = createServer({ joinDuplicateHeaders: true }, async (req, res) => {
let ret = ''
for await (const chunk of req) {
ret += chunk
}
t.strictEqual(ret, 'ijklmnopqrstuvwx')
res.end()
})
after(() => {
server.closeAllConnections?.()
server.close()
})
server.listen(0, async () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.destroy())
const { body } = await client.request({
path: '/',
method: 'POST',
body: requestBody
})
await body.text()
t.ok(true, 'pass')
})
await t.completed
})
test('request post body Uint8Array', async (t) => {
t = tspl(t, { plan: 2 })
const fullBuffer = new TextEncoder().encode('abcdefghijklmnopqrstuvwxyz')
const requestBody = new Uint8Array(fullBuffer.buffer, 8, 16)
const server = createServer({ joinDuplicateHeaders: true }, async (req, res) => {
let ret = ''
for await (const chunk of req) {
ret += chunk
}
t.strictEqual(ret, 'ijklmnopqrstuvwx')
res.end()
})
after(() => {
server.closeAllConnections?.()
server.close()
})
server.listen(0, async () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.destroy())
const { body } = await client.request({
path: '/',
method: 'POST',
body: requestBody
})
await body.text()
t.ok(true, 'pass')
})
await t.completed
})
test('request post body Uint32Array', async (t) => {
t = tspl(t, { plan: 2 })
const fullBuffer = new TextEncoder().encode('abcdefghijklmnopqrstuvwxyz')
const requestBody = new Uint32Array(fullBuffer.buffer, 8, 4)
const server = createServer({ joinDuplicateHeaders: true }, async (req, res) => {
let ret = ''
for await (const chunk of req) {
ret += chunk
}
t.strictEqual(ret, 'ijklmnopqrstuvwx')
res.end()
})
after(() => {
server.closeAllConnections?.()
server.close()
})
server.listen(0, async () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.destroy())
const { body } = await client.request({
path: '/',
method: 'POST',
body: requestBody
})
await body.text()
t.ok(true, 'pass')
})
await t.completed
})
test('request post body Float64Array', async (t) => {
t = tspl(t, { plan: 2 })
const fullBuffer = new TextEncoder().encode('abcdefghijklmnopqrstuvwxyz')
const requestBody = new Float64Array(fullBuffer.buffer, 8, 2)
const server = createServer({ joinDuplicateHeaders: true }, async (req, res) => {
let ret = ''
for await (const chunk of req) {
ret += chunk
}
t.strictEqual(ret, 'ijklmnopqrstuvwx')
res.end()
})
after(() => {
server.closeAllConnections?.()
server.close()
})
server.listen(0, async () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.destroy())
const { body } = await client.request({
path: '/',
method: 'POST',
body: requestBody
})
await body.text()
t.ok(true, 'pass')
})
await t.completed
})
test('request post body BigUint64Array', async (t) => {
t = tspl(t, { plan: 2 })
const fullBuffer = new TextEncoder().encode('abcdefghijklmnopqrstuvwxyz')
const requestBody = new BigUint64Array(fullBuffer.buffer, 8, 2)
const server = createServer({ joinDuplicateHeaders: true }, async (req, res) => {
let ret = ''
for await (const chunk of req) {
ret += chunk
}
t.strictEqual(ret, 'ijklmnopqrstuvwx')
res.end()
})
after(() => {
server.closeAllConnections?.()
server.close()
})
server.listen(0, async () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.destroy())
const { body } = await client.request({
path: '/',
method: 'POST',
body: requestBody
})
await body.text()
t.ok(true, 'pass')
})
await t.completed
})
test('request post body DataView', async (t) => {
t = tspl(t, { plan: 2 })
const fullBuffer = new TextEncoder().encode('abcdefghijklmnopqrstuvwxyz')
const requestBody = new DataView(fullBuffer.buffer, 8, 16)
const server = createServer({ joinDuplicateHeaders: true }, async (req, res) => {
let ret = ''
for await (const chunk of req) {
ret += chunk
}
t.strictEqual(ret, 'ijklmnopqrstuvwx')
res.end()
})
after(() => {
server.closeAllConnections?.()
server.close()
})
server.listen(0, async () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.destroy())
const { body } = await client.request({
path: '/',
method: 'POST',
body: requestBody
})
await body.text()
t.ok(true, 'pass')
})
await t.completed
})
test('request multibyte json with setEncoding', async (t) => {
t = tspl(t, { plan: 1 })
const asd = Buffer.from('あいうえお')
const data = JSON.stringify({ asd })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.write(data.slice(0, 1))
setTimeout(() => {
res.write(data.slice(1))
res.end()
}, 100)
})
after(server.close.bind(server))
server.listen(0, async () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(client.destroy.bind(client))
const { body } = await client.request({
path: '/',
method: 'GET'
})
body.setEncoding('utf8')
t.deepStrictEqual(JSON.parse(data), await body.json())
})
await t.completed
})
test('request multibyte text with setEncoding', async (t) => {
t = tspl(t, { plan: 1 })
const data = Buffer.from('あいうえお')
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.write(data.slice(0, 1))
setTimeout(() => {
res.write(data.slice(1))
res.end()
}, 100)
})
after(server.close.bind(server))
server.listen(0, async () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(client.destroy.bind(client))
const { body } = await client.request({
path: '/',
method: 'GET'
})
body.setEncoding('utf8')
t.deepStrictEqual(data.toString('utf8'), await body.text())
})
await t.completed
})
test('request multibyte text with setEncoding', async (t) => {
t = tspl(t, { plan: 1 })
const data = Buffer.from('あいうえお')
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.write(data.slice(0, 1))
setTimeout(() => {
res.write(data.slice(1))
res.end()
}, 100)
})
after(server.close.bind(server))
server.listen(0, async () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(client.destroy.bind(client))
const { body } = await client.request({
path: '/',
method: 'GET'
})
body.setEncoding('hex')
t.deepStrictEqual(data.toString('hex'), await body.text())
})
await t.completed
})
test('#3736 - Aborted Response (without consuming body)', async (t) => {
const plan = tspl(t, { plan: 1 })
const controller = new AbortController()
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
setTimeout(() => {
res.writeHead(200, 'ok', {
'content-type': 'text/plain'
})
res.write('hello from server')
res.end()
}, 100)
})
server.listen(0)
await EE.once(server, 'listening')
const client = new Client(`http://localhost:${server.address().port}`)
after(server.close.bind(server))
after(client.destroy.bind(client))
const { signal } = controller
const promise = client.request({
path: '/',
method: 'GET',
signal
})
controller.abort()
await plan.rejects(promise, { message: 'This operation was aborted' })
await plan.completed
})
================================================
FILE: test/client-stream.js
================================================
'use strict'
const { tspl } = require('@matteo.collina/tspl')
const { test, after } = require('node:test')
const { Client, errors } = require('..')
const { createServer } = require('node:http')
const { PassThrough, Writable, Readable } = require('node:stream')
const EE = require('node:events')
test('stream get', async (t) => {
t = tspl(t, { plan: 9 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
t.strictEqual('/', req.url)
t.strictEqual('GET', req.method)
t.strictEqual(`localhost:${server.address().port}`, req.headers.host)
res.setHeader('content-type', 'text/plain')
res.end('hello')
})
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.close())
client.on('disconnect', () => {
if (!client.closed && !client.destroyed) {
t.fail('unexpected disconnect')
}
})
const signal = new EE()
client.stream({
signal,
path: '/',
method: 'GET',
opaque: new PassThrough()
}, ({ statusCode, headers, opaque: pt }) => {
t.strictEqual(statusCode, 200)
t.strictEqual(headers['content-type'], 'text/plain')
const bufs = []
pt.on('data', (buf) => {
bufs.push(buf)
})
pt.on('end', () => {
t.strictEqual('hello', Buffer.concat(bufs).toString('utf8'))
})
return pt
}, (err) => {
t.strictEqual(signal.listenerCount('abort'), 0)
t.ifError(err)
})
t.strictEqual(signal.listenerCount('abort'), 1)
})
await t.completed
})
test('stream promise get', async (t) => {
t = tspl(t, { plan: 6 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
t.strictEqual('/', req.url)
t.strictEqual('GET', req.method)
t.strictEqual(`localhost:${server.address().port}`, req.headers.host)
res.setHeader('content-type', 'text/plain')
res.end('hello')
})
after(() => server.close())
server.listen(0, async () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.close())
client.on('disconnect', () => {
if (!client.closed && !client.destroyed) {
t.fail('unexpected disconnect')
}
})
await client.stream({
path: '/',
method: 'GET',
opaque: new PassThrough()
}, ({ statusCode, headers, opaque: pt }) => {
t.strictEqual(statusCode, 200)
t.strictEqual(headers['content-type'], 'text/plain')
const bufs = []
pt.on('data', (buf) => {
bufs.push(buf)
})
pt.on('end', () => {
t.strictEqual('hello', Buffer.concat(bufs).toString('utf8'))
})
return pt
})
})
await t.completed
})
test('stream GET destroy res', async (t) => {
t = tspl(t, { plan: 14 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
t.strictEqual('/', req.url)
t.strictEqual('GET', req.method)
t.strictEqual(`localhost:${server.address().port}`, req.headers.host)
res.setHeader('content-type', 'text/plain')
res.end('hello')
})
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.close())
client.stream({
path: '/',
method: 'GET'
}, ({ statusCode, headers }) => {
t.strictEqual(statusCode, 200)
t.strictEqual(headers['content-type'], 'text/plain')
const pt = new PassThrough()
.on('error', (err) => {
t.ok(err)
})
.on('data', () => {
pt.destroy(new Error('kaboom'))
})
return pt
}, (err) => {
t.ok(err)
})
client.stream({
path: '/',
method: 'GET'
}, ({ statusCode, headers }) => {
t.strictEqual(statusCode, 200)
t.strictEqual(headers['content-type'], 'text/plain')
let ret = ''
const pt = new PassThrough()
pt.on('data', chunk => {
ret += chunk
}).on('end', () => {
t.strictEqual(ret, 'hello')
})
return pt
}, (err) => {
t.ifError(err)
})
})
await t.completed
})
test('stream GET remote destroy', async (t) => {
t = tspl(t, { plan: 4 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.write('asd')
setImmediate(() => {
res.destroy()
})
})
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.close())
client.stream({
path: '/',
method: 'GET'
}, () => {
const pt = new PassThrough()
pt.on('error', (err) => {
t.ok(err)
})
return pt
}, (err) => {
t.ok(err)
})
client.stream({
path: '/',
method: 'GET'
}, () => {
const pt = new PassThrough()
pt.on('error', (err) => {
t.ok(err)
})
return pt
}).catch((err) => {
t.ok(err)
})
})
await t.completed
})
test('stream response resume back pressure and non standard error', async (t) => {
t = tspl(t, { plan: 5 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.write(Buffer.alloc(1e3))
setImmediate(() => {
res.write(Buffer.alloc(1e7))
res.end()
})
})
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.close())
const pt = new PassThrough()
client.stream({
path: '/',
method: 'GET'
}, () => {
pt.on('data', () => {
pt.emit('error', new Error('kaboom'))
}).once('error', (err) => {
t.strictEqual(err.message, 'kaboom')
})
return pt
}, (err) => {
t.ok(err)
t.strictEqual(pt.destroyed, true)
})
client.once('disconnect', (err) => {
t.ok(err)
})
client.stream({
path: '/',
method: 'GET'
}, () => {
const pt = new PassThrough()
pt.resume()
return pt
}, (err) => {
t.ifError(err)
})
})
await t.completed
})
test('stream waits only for writable side', async (t) => {
t = tspl(t, { plan: 2 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.end(Buffer.alloc(1e3))
})
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.close())
client.on('disconnect', () => {
if (!client.closed && !client.destroyed) {
t.fail('unexpected disconnect')
}
})
const pt = new PassThrough({ autoDestroy: false })
client.stream({
path: '/',
method: 'GET'
}, () => pt, (err) => {
t.ifError(err)
t.strictEqual(pt.destroyed, false)
})
})
await t.completed
})
test('stream args validation', async (t) => {
t = tspl(t, { plan: 3 })
const client = new Client('http://localhost:5000')
client.stream({
path: '/',
method: 'GET'
}, null, (err) => {
t.ok(err instanceof errors.InvalidArgumentError)
})
client.stream(null, null, (err) => {
t.ok(err instanceof errors.InvalidArgumentError)
})
try {
client.stream(null, null, 'asd')
} catch (err) {
t.ok(err instanceof errors.InvalidArgumentError)
}
})
test('stream args validation promise', async (t) => {
t = tspl(t, { plan: 2 })
const client = new Client('http://localhost:5000')
client.stream({
path: '/',
method: 'GET'
}, null).catch((err) => {
t.ok(err instanceof errors.InvalidArgumentError)
})
client.stream(null, null).catch((err) => {
t.ok(err instanceof errors.InvalidArgumentError)
})
await t.completed
})
test('stream destroy if not readable', async (t) => {
t = tspl(t, { plan: 2 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.end()
})
after(() => server.close())
const pt = new PassThrough()
pt.readable = false
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(client.destroy.bind(client))
client.on('disconnect', () => {
if (!client.closed && !client.destroyed) {
t.fail('unexpected disconnect')
}
})
client.stream({
path: '/',
method: 'GET'
}, () => {
return pt
}, (err) => {
t.ifError(err)
t.strictEqual(pt.destroyed, true)
})
})
await t.completed
})
test('stream server side destroy', async (t) => {
t = tspl(t, { plan: 1 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.destroy()
})
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(client.destroy.bind(client))
client.stream({
path: '/',
method: 'GET'
}, () => {
t.fail()
}, (err) => {
t.ok(err instanceof errors.SocketError)
})
})
await t.completed
})
test('stream invalid return', async (t) => {
t = tspl(t, { plan: 1 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.write('asd')
})
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(client.destroy.bind(client))
client.stream({
path: '/',
method: 'GET'
}, () => {
return {}
}, (err) => {
t.ok(err instanceof errors.InvalidReturnValueError)
})
})
await t.completed
})
test('stream body without destroy', async (t) => {
t = tspl(t, { plan: 1 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.end('asd')
})
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(client.destroy.bind(client))
client.on('disconnect', () => {
if (!client.closed && !client.destroyed) {
t.fail('unexpected disconnect')
}
})
client.stream({
path: '/',
method: 'GET'
}, () => {
const pt = new PassThrough({ autoDestroy: false })
pt.destroy = null
pt.resume()
return pt
}, (err) => {
t.ifError(err)
})
})
await t.completed
})
test('stream factory abort', async (t) => {
t = tspl(t, { plan: 3 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.end('asd')
})
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(client.destroy.bind(client))
const signal = new EE()
client.stream({
path: '/',
method: 'GET',
signal
}, () => {
signal.emit('abort')
return new PassThrough()
}, (err) => {
t.strictEqual(signal.listenerCount('abort'), 0)
t.ok(err instanceof errors.RequestAbortedError)
})
t.strictEqual(signal.listenerCount('abort'), 1)
})
await t.completed
})
test('stream factory throw', async (t) => {
t = tspl(t, { plan: 3 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.end('asd')
})
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(client.destroy.bind(client))
client.stream({
path: '/',
method: 'GET'
}, () => {
throw new Error('asd')
}, (err) => {
t.strictEqual(err.message, 'asd')
})
client.stream({
path: '/',
method: 'GET'
}, () => {
throw new Error('asd')
}, (err) => {
t.strictEqual(err.message, 'asd')
})
client.stream({
path: '/',
method: 'GET'
}, () => {
return new PassThrough()
}, (err) => {
t.ifError(err)
})
})
await t.completed
})
test('stream CONNECT throw', async (t) => {
t = tspl(t, { plan: 1 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.end('asd')
})
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(client.destroy.bind(client))
client.stream({
path: '/',
method: 'CONNECT'
}, () => {
}, (err) => {
t.ok(err instanceof errors.InvalidArgumentError)
})
})
await t.completed
})
test('stream abort after complete', async (t) => {
t = tspl(t, { plan: 1 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.end('asd')
})
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(client.destroy.bind(client))
client.on('disconnect', () => {
if (!client.closed && !client.destroyed) {
t.fail('unexpected disconnect')
}
})
const pt = new PassThrough()
const signal = new EE()
client.stream({
path: '/',
method: 'GET',
signal
}, () => {
return pt
}, (err) => {
t.ifError(err)
signal.emit('abort')
})
})
await t.completed
})
test('stream abort before dispatch', async (t) => {
t = tspl(t, { plan: 1 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.end('asd')
})
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(client.destroy.bind(client))
const pt = new PassThrough()
const signal = new EE()
client.stream({
path: '/',
method: 'GET',
signal
}, () => {
return pt
}, (err) => {
t.ok(err instanceof errors.RequestAbortedError)
})
signal.emit('abort')
})
await t.completed
})
test('trailers', async (t) => {
t = tspl(t, { plan: 2 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.writeHead(200, { Trailer: 'Content-MD5' })
res.addTrailers({ 'Content-MD5': 'test' })
res.end()
})
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.close())
client.on('disconnect', () => {
if (!client.closed && !client.destroyed) {
t.fail('unexpected disconnect')
}
})
client.stream({
path: '/',
method: 'GET'
}, () => new PassThrough(), (err, data) => {
t.ifError(err)
t.deepStrictEqual(data.trailers, { 'content-md5': 'test' })
})
})
await t.completed
})
test('stream ignore 1xx', async (t) => {
t = tspl(t, { plan: 2 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.writeProcessing()
res.end('hello')
})
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.close())
client.on('disconnect', () => {
if (!client.closed && !client.destroyed) {
t.fail('unexpected disconnect')
}
})
let buf = ''
client.stream({
path: '/',
method: 'GET'
}, () => new Writable({
write (chunk, encoding, callback) {
buf += chunk
callback()
}
}), (err, data) => {
t.ifError(err)
t.strictEqual(buf, 'hello')
})
})
await t.completed
})
test('stream ignore 1xx and use onInfo', async (t) => {
t = tspl(t, { plan: 4 })
const infos = []
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.writeProcessing()
res.end('hello')
})
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.close())
client.on('disconnect', () => {
if (!client.closed && !client.destroyed) {
t.fail('unexpected disconnect')
}
})
let buf = ''
client.stream({
path: '/',
method: 'GET',
onInfo: (x) => {
infos.push(x)
}
}, () => new Writable({
write (chunk, encoding, callback) {
buf += chunk
callback()
}
}), (err, data) => {
t.ifError(err)
t.strictEqual(buf, 'hello')
t.strictEqual(infos.length, 1)
t.strictEqual(infos[0].statusCode, 102)
})
})
await t.completed
})
test('stream backpressure', async (t) => {
t = tspl(t, { plan: 2 })
const expected = Buffer.alloc(1e6).toString()
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.writeProcessing()
res.end(expected)
})
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.close())
client.on('disconnect', () => {
if (!client.closed && !client.destroyed) {
t.fail('unexpected disconnect')
}
})
let buf = ''
client.stream({
path: '/',
method: 'GET'
}, () => new Writable({
highWaterMark: 1,
write (chunk, encoding, callback) {
buf += chunk
process.nextTick(callback)
}
}), (err, data) => {
t.ifError(err)
t.strictEqual(buf, expected)
})
})
await t.completed
})
test('stream body destroyed on invalid callback', async (t) => {
t = tspl(t, { plan: 1 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
})
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(client.destroy.bind(client))
const body = new Readable({
read () { }
})
try {
client.stream({
path: '/',
method: 'GET',
body
}, () => { }, null)
} catch (err) {
t.strictEqual(body.destroyed, true)
}
})
await t.completed
})
test('stream needDrain', async (t) => {
t = tspl(t, { plan: 3 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.end(Buffer.alloc(4096))
})
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => {
client.destroy()
})
client.on('disconnect', () => {
if (!client.closed && !client.destroyed) {
t.fail('unexpected disconnect')
}
})
const dst = new PassThrough()
dst.pause()
if (dst.writableNeedDrain === undefined) {
Object.defineProperty(dst, 'writableNeedDrain', {
get () {
return this._writableState.needDrain
}
})
}
while (dst.write(Buffer.alloc(4096))) {
// Do nothing.
}
const orgWrite = dst.write
dst.write = () => t.fail()
const p = client.stream({
path: '/',
method: 'GET'
}, () => {
t.strictEqual(dst._writableState.needDrain, true)
t.strictEqual(dst.writableNeedDrain, true)
setImmediate(() => {
dst.write = (...args) => {
orgWrite.call(dst, ...args)
}
dst.resume()
})
return dst
})
p.then(() => {
t.ok(true, 'pass')
})
})
await t.completed
})
test('stream legacy needDrain', async (t) => {
t = tspl(t, { plan: 3 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.end(Buffer.alloc(4096))
})
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => {
client.destroy()
})
client.on('disconnect', () => {
if (!client.closed && !client.destroyed) {
t.fail('unexpected disconnect')
}
})
const dst = new PassThrough()
dst.pause()
if (dst.writableNeedDrain !== undefined) {
Object.defineProperty(dst, 'writableNeedDrain', {
get () {
}
})
}
while (dst.write(Buffer.alloc(4096))) {
// Do nothing
}
const orgWrite = dst.write
dst.write = () => t.fail()
const p = client.stream({
path: '/',
method: 'GET'
}, () => {
t.strictEqual(dst._writableState.needDrain, true)
t.strictEqual(dst.writableNeedDrain, undefined)
setImmediate(() => {
dst.write = (...args) => {
orgWrite.call(dst, ...args)
}
dst.resume()
})
return dst
})
p.then(() => {
t.ok(true, 'pass')
})
})
await t.completed
})
================================================
FILE: test/client-timeout.js
================================================
'use strict'
const { tspl } = require('@matteo.collina/tspl')
const { test, after } = require('node:test')
const { Client, errors } = require('..')
const { createServer } = require('node:http')
const { Readable } = require('node:stream')
const FakeTimers = require('@sinonjs/fake-timers')
const timers = require('../lib/util/timers')
test('refresh timeout on pause', async (t) => {
t = tspl(t, { plan: 1 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.flushHeaders()
})
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`, {
bodyTimeout: 500
})
after(() => client.destroy())
client.dispatch({
path: '/',
method: 'GET'
}, {
onConnect () {
},
onHeaders (statusCode, headers, resume) {
setTimeout(() => {
resume()
}, 1000)
return false
},
onData () {
},
onComplete () {
},
onError (err) {
t.ok(err instanceof errors.BodyTimeoutError)
}
})
})
await t.completed
})
test('start headers timeout after request body', async (t) => {
t = tspl(t, { plan: 2 })
const clock = FakeTimers.install({ shouldClearNativeTimers: true })
after(() => clock.uninstall())
const orgTimers = { ...timers }
Object.assign(timers, { setTimeout, clearTimeout })
after(() => {
Object.assign(timers, orgTimers)
})
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
})
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`, {
bodyTimeout: 0,
headersTimeout: 100
})
after(() => client.destroy())
const body = new Readable({ read () {} })
client.dispatch({
path: '/',
body,
method: 'GET'
}, {
onConnect () {
process.nextTick(() => {
clock.tick(200)
})
queueMicrotask(() => {
body.push(null)
body.on('end', () => {
clock.tick(200)
})
})
},
onHeaders (statusCode, headers, resume) {
},
onData () {
},
onComplete () {
},
onError (err) {
t.equal(body.readableEnded, true)
t.ok(err instanceof errors.HeadersTimeoutError)
}
})
})
await t.completed
})
test('start headers timeout after async iterator request body', async (t) => {
t = tspl(t, { plan: 1 })
const clock = FakeTimers.install({ shouldClearNativeTimers: true })
after(() => clock.uninstall())
const orgTimers = { ...timers }
Object.assign(timers, { setTimeout, clearTimeout })
after(() => {
Object.assign(timers, orgTimers)
})
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
})
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`, {
bodyTimeout: 0,
headersTimeout: 100
})
after(() => client.destroy())
let res
const body = (async function * () {
await new Promise((resolve) => { res = resolve })
process.nextTick(() => {
clock.tick(200)
})
})()
client.dispatch({
path: '/',
body,
method: 'GET'
}, {
onConnect () {
process.nextTick(() => {
clock.tick(200)
})
queueMicrotask(() => {
res()
})
},
onHeaders (statusCode, headers, resume) {
},
onData () {
},
onComplete () {
},
onError (err) {
t.ok(err instanceof errors.HeadersTimeoutError)
}
})
})
await t.completed
})
test('parser resume with no body timeout', async (t) => {
t = tspl(t, { plan: 1 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.end('asd')
})
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`, {
bodyTimeout: 0
})
after(() => client.destroy())
client.on('disconnect', () => {
if (!client.closed && !client.destroyed) {
t.fail('unexpected disconnect')
}
})
client.dispatch({
path: '/',
method: 'GET'
}, {
onConnect () {
},
onHeaders (statusCode, headers, resume) {
setTimeout(resume, 2000)
return false
},
onData () {
},
onComplete () {
t.ok(true, 'pass')
},
onError (err) {
t.ifError(err)
}
})
})
await t.completed
})
================================================
FILE: test/client-unref.js
================================================
'use strict'
const { Worker, isMainThread, workerData } = require('node:worker_threads')
if (isMainThread) {
const { tspl } = require('@matteo.collina/tspl')
const { test, after } = require('node:test')
const { once } = require('node:events')
const { createServer } = require('node:http')
test('client automatically closes itself when idle', async t => {
t = tspl(t, { plan: 1 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.end()
})
after(server.close.bind(server))
server.keepAliveTimeout = 9999
server.listen(0)
await once(server, 'listening')
const url = `http://localhost:${server.address().port}`
const worker = new Worker(__filename, { workerData: { url } })
worker.on('exit', code => {
t.strictEqual(code, 0)
})
await t.completed
})
test('client automatically closes itself if the server is not there', async t => {
t = tspl(t, { plan: 1 })
const url = 'http://localhost:4242' // hopefully empty port
const worker = new Worker(__filename, { workerData: { url } })
worker.on('exit', code => {
t.strictEqual(code, 0)
})
await t.completed
})
} else {
const { Client } = require('..')
const client = new Client(workerData.url)
client.request({ path: '/', method: 'GET' }, () => {
// We do not care about Errors
setTimeout(() => {
throw new Error()
}, 1e3).unref()
})
}
================================================
FILE: test/client-upgrade.js
================================================
'use strict'
const { tspl } = require('@matteo.collina/tspl')
const { test, after } = require('node:test')
const { Client, errors } = require('..')
const net = require('node:net')
const http = require('node:http')
const EE = require('node:events')
const { kBusy } = require('../lib/core/symbols')
test('basic upgrade', async (t) => {
t = tspl(t, { plan: 6 })
const server = net.createServer({ joinDuplicateHeaders: true }, (c) => {
c.on('data', (d) => {
t.ok(/upgrade: websocket/i.test(d))
c.write('HTTP/1.1 101\r\n')
c.write('hello: world\r\n')
c.write('connection: upgrade\r\n')
c.write('upgrade: websocket\r\n')
c.write('\r\n')
c.write('Body')
})
c.on('end', () => {
c.end()
})
})
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.close())
const signal = new EE()
client.upgrade({
signal,
path: '/',
method: 'GET',
protocol: 'Websocket'
}, (err, data) => {
t.ifError(err)
t.strictEqual(signal.listenerCount('abort'), 0)
const { headers, socket } = data
let recvData = ''
data.socket.on('data', (d) => {
recvData += d
})
socket.on('close', () => {
t.strictEqual(recvData.toString(), 'Body')
})
t.deepStrictEqual(headers, {
hello: 'world',
connection: 'upgrade',
upgrade: 'websocket'
})
socket.end()
})
t.strictEqual(signal.listenerCount('abort'), 1)
})
await t.completed
})
test('basic upgrade promise', async (t) => {
t = tspl(t, { plan: 2 })
const server = net.createServer({ joinDuplicateHeaders: true }, (c) => {
c.on('data', (d) => {
c.write('HTTP/1.1 101\r\n')
c.write('hello: world\r\n')
c.write('connection: upgrade\r\n')
c.write('upgrade: websocket\r\n')
c.write('\r\n')
c.write('Body')
})
c.on('end', () => {
c.end()
})
})
after(() => server.close())
server.listen(0, async () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.close())
const { headers, socket } = await client.upgrade({
path: '/',
method: 'GET',
protocol: 'Websocket'
})
let recvData = ''
socket.on('data', (d) => {
recvData += d
})
socket.on('close', () => {
t.strictEqual(recvData.toString(), 'Body')
})
t.deepStrictEqual(headers, {
hello: 'world',
connection: 'upgrade',
upgrade: 'websocket'
})
socket.end()
})
await t.completed
})
test('upgrade error', async (t) => {
t = tspl(t, { plan: 1 })
const server = net.createServer({ joinDuplicateHeaders: true }, (c) => {
c.on('data', (d) => {
c.write('HTTP/1.1 101\r\n')
c.write('hello: world\r\n')
c.write('connection: upgrade\r\n')
c.write('\r\n')
c.write('Body')
})
c.on('error', () => {
// Whether we get an error, end or close is undefined.
// Ignore error.
})
})
after(() => server.close())
server.listen(0, async () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.close())
try {
await client.upgrade({
path: '/',
method: 'GET',
protocol: 'Websocket'
})
} catch (err) {
t.ok(err)
}
})
await t.completed
})
test('upgrade invalid opts', async (t) => {
t = tspl(t, { plan: 6 })
const client = new Client('http://localhost:5432')
client.upgrade(null, err => {
t.ok(err instanceof errors.InvalidArgumentError)
t.strictEqual(err.message, 'invalid opts')
})
try {
client.upgrade(null, null)
t.fail()
} catch (err) {
t.ok(err instanceof errors.InvalidArgumentError)
t.strictEqual(err.message, 'invalid opts')
}
try {
client.upgrade({ path: '/' }, null)
t.fail()
} catch (err) {
t.ok(err instanceof errors.InvalidArgumentError)
t.strictEqual(err.message, 'invalid callback')
}
})
test('basic upgrade2', async (t) => {
t = tspl(t, { plan: 3 })
const server = http.createServer({ joinDuplicateHeaders: true })
server.on('upgrade', (req, c, head) => {
c.write('HTTP/1.1 101\r\n')
c.write('hello: world\r\n')
c.write('connection: upgrade\r\n')
c.write('upgrade: websocket\r\n')
c.write('\r\n')
c.write('Body')
c.end()
})
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.close())
client.upgrade({
path: '/',
method: 'GET',
protocol: 'Websocket'
}, (err, data) => {
t.ifError(err)
const { headers, socket } = data
let recvData = ''
data.socket.on('data', (d) => {
recvData += d
})
socket.on('close', () => {
t.strictEqual(recvData.toString(), 'Body')
})
t.deepStrictEqual(headers, {
hello: 'world',
connection: 'upgrade',
upgrade: 'websocket'
})
socket.end()
})
})
await t.completed
})
test('upgrade wait for empty pipeline', async (t) => {
t = tspl(t, { plan: 7 })
let canConnect = false
const server = http.createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.end()
canConnect = true
})
server.on('upgrade', (req, c, firstBodyChunk) => {
t.strictEqual(canConnect, true)
c.write('HTTP/1.1 101\r\n')
c.write('hello: world\r\n')
c.write('connection: upgrade\r\n')
c.write('upgrade: websocket\r\n')
c.write('\r\n')
c.write('Body')
c.end()
})
after(() => server.close())
server.listen(0, async () => {
const client = new Client(`http://localhost:${server.address().port}`, {
pipelining: 3
})
after(() => client.close())
client.request({
path: '/',
method: 'GET',
blocking: false
}, (err) => {
t.ifError(err)
})
client.once('connect', () => {
process.nextTick(() => {
t.strictEqual(client[kBusy], false)
client.upgrade({
path: '/'
}, (err, { socket }) => {
t.ifError(err)
let recvData = ''
socket.on('data', (d) => {
recvData += d
})
socket.on('end', () => {
t.strictEqual(recvData.toString(), 'Body')
})
socket.write('Body')
socket.end()
})
t.strictEqual(client[kBusy], true)
client.request({
path: '/',
method: 'GET'
}, (err) => {
t.ifError(err)
})
})
})
})
await t.completed
})
test('upgrade aborted', async (t) => {
t = tspl(t, { plan: 6 })
const server = http.createServer({ joinDuplicateHeaders: true }, (req, res) => {
t.fail()
})
server.on('upgrade', (req, c, firstBodyChunk) => {
t.fail()
})
after(() => server.close())
server.listen(0, async () => {
const client = new Client(`http://localhost:${server.address().port}`, {
pipelining: 3
})
after(() => client.destroy())
const signal = new EE()
client.upgrade({
path: '/',
signal,
opaque: 'asd'
}, (err, { opaque }) => {
t.strictEqual(opaque, 'asd')
t.ok(err instanceof errors.RequestAbortedError)
t.strictEqual(signal.listenerCount('abort'), 0)
})
t.strictEqual(client[kBusy], true)
t.strictEqual(signal.listenerCount('abort'), 1)
signal.emit('abort')
client.close(() => {
t.ok(true, 'pass')
})
})
await t.completed
})
test('basic aborted after res', async (t) => {
t = tspl(t, { plan: 1 })
const signal = new EE()
const server = http.createServer({ joinDuplicateHeaders: true })
server.on('upgrade', (req, c, head) => {
c.write('HTTP/1.1 101\r\n')
c.write('hello: world\r\n')
c.write('connection: upgrade\r\n')
c.write('upgrade: websocket\r\n')
c.write('\r\n')
c.write('Body')
c.end()
c.on('error', () => {
})
signal.emit('abort')
})
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.close())
client.upgrade({
path: '/',
method: 'GET',
protocol: 'Websocket',
signal
}, (err) => {
t.ok(err instanceof errors.RequestAbortedError)
})
})
await t.completed
})
test('basic upgrade error', async (t) => {
t = tspl(t, { plan: 2 })
const server = net.createServer({ joinDuplicateHeaders: true }, (c) => {
c.on('data', (d) => {
c.write('HTTP/1.1 101\r\n')
c.write('hello: world\r\n')
c.write('connection: upgrade\r\n')
c.write('upgrade: websocket\r\n')
c.write('\r\n')
c.write('Body')
})
c.on('error', () => {
})
})
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.close())
const _err = new Error()
client.upgrade({
path: '/',
method: 'GET',
protocol: 'Websocket'
}, (err, data) => {
t.ifError(err)
data.socket.on('error', (err) => {
t.strictEqual(err, _err)
})
throw _err
})
})
await t.completed
})
test('upgrade disconnect', async (t) => {
t = tspl(t, { plan: 3 })
const server = net.createServer(connection => {
connection.destroy()
})
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.close())
client.on('disconnect', (origin, [self], error) => {
t.strictEqual(client, self)
t.ok(error instanceof Error)
})
client
.upgrade({ path: '/', method: 'GET' })
.then(() => {
t.fail()
})
.catch(error => {
t.ok(error instanceof Error)
})
})
await t.completed
})
test('upgrade invalid signal', async (t) => {
t = tspl(t, { plan: 2 })
const server = net.createServer({ joinDuplicateHeaders: true }, () => {
t.fail()
})
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.destroy())
client.on('disconnect', () => {
t.fail()
})
client.upgrade({
path: '/',
method: 'GET',
protocol: 'Websocket',
signal: 'error',
opaque: 'asd'
}, (err, { opaque }) => {
t.strictEqual(opaque, 'asd')
t.ok(err instanceof errors.InvalidArgumentError)
})
})
await t.completed
})
================================================
FILE: test/client-wasm.js
================================================
'use strict'
const { tspl } = require('@matteo.collina/tspl')
const { describe, test } = require('node:test')
;[
['generic', require('../lib/llhttp/llhttp-wasm.js')],
['simd', require('../lib/llhttp/llhttp_simd-wasm.js')]
].forEach(([name, llhttp]) => {
describe(name, () => {
test('can compile the wasm code', async () => {
await WebAssembly.compile(llhttp)
})
test('can instantiate the wasm code', async () => {
const mod = await WebAssembly.compile(llhttp)
await WebAssembly.instantiate(mod, {
env: {
wasm_on_url: () => { },
wasm_on_status: () => { },
wasm_on_message_begin: () => { },
wasm_on_header_field: () => { },
wasm_on_header_value: () => { },
wasm_on_headers_complete: () => { },
wasm_on_body: () => { },
wasm_on_message_complete: () => { }
}
})
})
describe('exports', async () => {
const mod = await WebAssembly.compile(llhttp)
const instance = await WebAssembly.instantiate(mod, {
env: {
wasm_on_url: () => { },
wasm_on_status: () => { },
wasm_on_message_begin: () => { },
wasm_on_header_field: () => { },
wasm_on_header_value: () => { },
wasm_on_headers_complete: () => { },
wasm_on_body: () => { },
wasm_on_message_complete: () => { }
}
})
test('has the required exports', async (t) => {
const requiredExports = [
'memory',
'_initialize',
'__indirect_function_table',
'llhttp_init',
'llhttp_should_keep_alive',
'llhttp_alloc',
'malloc',
'llhttp_free',
'free',
'llhttp_get_type',
'llhttp_get_http_major',
'llhttp_get_http_minor',
'llhttp_get_method',
'llhttp_get_status_code',
'llhttp_get_upgrade',
'llhttp_reset',
'llhttp_execute',
'llhttp_settings_init',
'llhttp_finish',
'llhttp_pause',
'llhttp_resume',
'llhttp_resume_after_upgrade',
'llhttp_get_errno',
'llhttp_get_error_reason',
'llhttp_set_error_reason',
'llhttp_get_error_pos',
'llhttp_errno_name',
'llhttp_method_name',
'llhttp_status_name',
'llhttp_set_lenient_headers',
'llhttp_set_lenient_chunked_length',
'llhttp_set_lenient_keep_alive',
'llhttp_set_lenient_transfer_encoding',
'llhttp_set_lenient_version',
'llhttp_set_lenient_data_after_close',
'llhttp_set_lenient_optional_lf_after_cr',
'llhttp_set_lenient_optional_crlf_after_chunk',
'llhttp_set_lenient_optional_cr_before_lf',
'llhttp_set_lenient_spaces_after_chunk_size',
'llhttp_message_needs_eof'
]
t = tspl(t, { plan: requiredExports.length })
for (const key of requiredExports) {
t.ok(key in instance.exports, `${key} is exported`)
}
await t.completed
})
test('instance.exports.memory', async (t) => {
t = tspl(t, { plan: 1 })
t.ok(instance.exports.memory instanceof WebAssembly.Memory, 'memory is present')
})
// _initialize
test('instance.exports._initialize', async (t) => {
t = tspl(t, { plan: 2 })
t.ok(typeof instance.exports._initialize === 'function', '_initialize is present')
t.strictEqual(instance.exports._initialize.length, 0, '_initialize has the right number of arguments')
})
// __indirect_function_table
test('instance.exports.__indirect_function_table', async (t) => {
t = tspl(t, { plan: 1 })
t.ok(instance.exports.__indirect_function_table instanceof WebAssembly.Table, '__indirect_function_table is present')
})
// malloc
test('instance.exports.malloc', async (t) => {
t = tspl(t, { plan: 2 })
t.ok(typeof instance.exports.malloc === 'function', 'malloc is present')
t.strictEqual(instance.exports.malloc.length, 1, 'malloc has the right number of arguments')
})
// free
test('instance.exports.free', async (t) => {
t = tspl(t, { plan: 2 })
t.ok(typeof instance.exports.free === 'function', 'free is present')
t.strictEqual(instance.exports.free.length, 1, 'free has the right number of arguments')
})
// llhttp_init
test('instance.exports.llhttp_init', async (t) => {
t = tspl(t, { plan: 2 })
t.ok(typeof instance.exports.llhttp_init === 'function', 'llhttp_init is present')
t.strictEqual(instance.exports.llhttp_init.length, 3, 'llhttp_init has the right number of arguments')
})
// llhttp_alloc
test('instance.exports.llhttp_alloc', async (t) => {
t = tspl(t, { plan: 2 })
t.ok(typeof instance.exports.llhttp_alloc === 'function', 'llhttp_alloc is present')
t.strictEqual(instance.exports.llhttp_alloc.length, 1, 'llhttp_alloc has the right number of arguments')
})
// llhttp_free
test('instance.exports.llhttp_free', async (t) => {
t = tspl(t, { plan: 2 })
t.ok(typeof instance.exports.llhttp_free === 'function', 'llhttp_free is present')
t.strictEqual(instance.exports.llhttp_free.length, 1, 'llhttp_free has the right number of arguments')
})
// llhttp_get_type
test('instance.exports.llhttp_get_type', async (t) => {
t = tspl(t, { plan: 2 })
t.ok(typeof instance.exports.llhttp_get_type === 'function', 'llhttp_get_type is present')
t.strictEqual(instance.exports.llhttp_get_type.length, 1, 'llhttp_get_type has the right number of arguments')
})
// llhttp_should_keep_alive
test('instance.exports.llhttp_should_keep_alive', async (t) => {
t = tspl(t, { plan: 2 })
t.ok(typeof instance.exports.llhttp_should_keep_alive === 'function', 'llhttp_should_keep_alive is present')
t.strictEqual(instance.exports.llhttp_should_keep_alive.length, 1, 'llhttp_should_keep_alive has the right number of arguments')
})
// llhttp_get_http_major
test('instance.exports.llhttp_get_http_major', async (t) => {
t = tspl(t, { plan: 2 })
t.ok(typeof instance.exports.llhttp_get_http_major === 'function', 'llhttp_get_http_major is present')
t.strictEqual(instance.exports.llhttp_get_http_major.length, 1, 'llhttp_get_http_major has the right number of arguments')
})
// llhttp_get_http_minor
test('instance.exports.llhttp_get_http_minor', async (t) => {
t = tspl(t, { plan: 2 })
t.ok(typeof instance.exports.llhttp_get_http_minor === 'function', 'llhttp_get_http_minor is present')
t.strictEqual(instance.exports.llhttp_get_http_minor.length, 1, 'llhttp_get_http_minor has the right number of arguments')
})
// llhttp_get_method
test('instance.exports.llhttp_get_method', async (t) => {
t = tspl(t, { plan: 2 })
t.ok(typeof instance.exports.llhttp_get_method === 'function', 'llhttp_get_method is present')
t.strictEqual(instance.exports.llhttp_get_method.length, 1, 'llhttp_get_method has the right number of arguments')
})
// llhttp_get_status_code
test('instance.exports.llhttp_get_status_code', async (t) => {
t = tspl(t, { plan: 2 })
t.ok(typeof instance.exports.llhttp_get_status_code === 'function', 'llhttp_get_status_code is present')
t.strictEqual(instance.exports.llhttp_get_status_code.length, 1, 'llhttp_get_status_code has the right number of arguments')
})
// llhttp_get_upgrade
test('instance.exports.llhttp_get_upgrade', async (t) => {
t = tspl(t, { plan: 2 })
t.ok(typeof instance.exports.llhttp_get_upgrade === 'function', 'llhttp_get_upgrade is present')
t.strictEqual(instance.exports.llhttp_get_upgrade.length, 1, 'llhttp_get_upgrade has the right number of arguments')
})
// llhttp_reset
test('instance.exports.llhttp_reset', async (t) => {
t = tspl(t, { plan: 2 })
t.ok(typeof instance.exports.llhttp_reset === 'function', 'llhttp_reset is present')
t.strictEqual(instance.exports.llhttp_reset.length, 1, 'llhttp_reset has the right number of arguments')
})
// llhttp_execute
test('instance.exports.llhttp_execute', async (t) => {
t = tspl(t, { plan: 2 })
t.ok(typeof instance.exports.llhttp_execute === 'function', 'llhttp_execute is present')
t.strictEqual(instance.exports.llhttp_execute.length, 3, 'llhttp_execute has the right number of arguments')
})
// llhttp_settings_init
test('instance.exports.llhttp_settings_init', async (t) => {
t = tspl(t, { plan: 2 })
t.ok(typeof instance.exports.llhttp_settings_init === 'function', 'llhttp_settings_init is present')
t.strictEqual(instance.exports.llhttp_settings_init.length, 1, 'llhttp_settings_init has the right number of arguments')
})
// llhttp_finish
test('instance.exports.llhttp_finish', async (t) => {
t = tspl(t, { plan: 2 })
t.ok(typeof instance.exports.llhttp_finish === 'function', 'llhttp_finish is present')
t.strictEqual(instance.exports.llhttp_finish.length, 1, 'llhttp_finish has the right number of arguments')
})
// llhttp_pause
test('instance.exports.llhttp_pause', async (t) => {
t = tspl(t, { plan: 2 })
t.ok(typeof instance.exports.llhttp_pause === 'function', 'llhttp_pause is present')
t.strictEqual(instance.exports.llhttp_pause.length, 1, 'llhttp_pause has the right number of arguments')
})
// llhttp_resume
test('instance.exports.llhttp_resume', async (t) => {
t = tspl(t, { plan: 2 })
t.ok(typeof instance.exports.llhttp_resume === 'function', 'llhttp_resume is present')
t.strictEqual(instance.exports.llhttp_resume.length, 1, 'llhttp_resume has the right number of arguments')
})
// llhttp_resume_after_upgrade
test('instance.exports.llhttp_resume_after_upgrade', async (t) => {
t = tspl(t, { plan: 2 })
t.ok(typeof instance.exports.llhttp_resume_after_upgrade === 'function', 'llhttp_resume_after_upgrade is present')
t.strictEqual(instance.exports.llhttp_resume_after_upgrade.length, 1, 'llhttp_resume_after_upgrade has the right number of arguments')
})
// llhttp_get_errno
test('instance.exports.llhttp_get_errno', async (t) => {
t = tspl(t, { plan: 2 })
t.ok(typeof instance.exports.llhttp_get_errno === 'function', 'llhttp_get_errno is present')
t.strictEqual(instance.exports.llhttp_get_errno.length, 1, 'llhttp_get_errno has the right number of arguments')
})
// llhttp_get_error_reason
test('instance.exports.llhttp_get_error_reason', async (t) => {
t = tspl(t, { plan: 2 })
t.ok(typeof instance.exports.llhttp_get_error_reason === 'function', 'llhttp_get_error_reason is present')
t.strictEqual(instance.exports.llhttp_get_error_reason.length, 1, 'llhttp_get_error_reason has the right number of arguments')
})
// llhttp_set_error_reason
test('instance.exports.llhttp_set_error_reason', async (t) => {
t = tspl(t, { plan: 2 })
t.ok(typeof instance.exports.llhttp_set_error_reason === 'function', 'llhttp_set_error_reason is present')
t.strictEqual(instance.exports.llhttp_set_error_reason.length, 2, 'llhttp_set_error_reason has the right number of arguments')
})
// llhttp_get_error_pos
test('instance.exports.llhttp_get_error_pos', async (t) => {
t = tspl(t, { plan: 2 })
t.ok(typeof instance.exports.llhttp_get_error_pos === 'function', 'llhttp_get_error_pos is present')
t.strictEqual(instance.exports.llhttp_get_error_pos.length, 1, 'llhttp_get_error_pos has the right number of arguments')
})
// llhttp_errno_name
test('instance.exports.llhttp_errno_name', async (t) => {
t = tspl(t, { plan: 2 })
t.ok(typeof instance.exports.llhttp_errno_name === 'function', 'llhttp_errno_name is present')
t.strictEqual(instance.exports.llhttp_errno_name.length, 1, 'llhttp_errno_name has the right number of arguments')
})
// llhttp_method_name
test('instance.exports.llhttp_method_name', async (t) => {
t = tspl(t, { plan: 2 })
t.ok(typeof instance.exports.llhttp_method_name === 'function', 'llhttp_method_name is present')
t.strictEqual(instance.exports.llhttp_method_name.length, 1, 'llhttp_method_name has the right number of arguments')
})
// llhttp_status_name
test('instance.exports.llhttp_status_name', async (t) => {
t = tspl(t, { plan: 2 })
t.ok(typeof instance.exports.llhttp_status_name === 'function', 'llhttp_status_name is present')
t.strictEqual(instance.exports.llhttp_status_name.length, 1, 'llhttp_status_name has the right number of arguments')
})
// llhttp_set_lenient_headers
test('instance.exports.llhttp_set_lenient_headers', async (t) => {
t = tspl(t, { plan: 2 })
t.ok(typeof instance.exports.llhttp_set_lenient_headers === 'function', 'llhttp_set_lenient_headers is present')
t.strictEqual(instance.exports.llhttp_set_lenient_headers.length, 2, 'llhttp_set_lenient_headers has the right number of arguments')
})
// llhttp_set_lenient_chunked_length
test('instance.exports.llhttp_set_lenient_chunked_length', async (t) => {
t = tspl(t, { plan: 2 })
t.ok(typeof instance.exports.llhttp_set_lenient_chunked_length === 'function', 'llhttp_set_lenient_chunked_length is present')
t.strictEqual(instance.exports.llhttp_set_lenient_chunked_length.length, 2, 'llhttp_set_lenient_chunked_length has the right number of arguments')
})
// llhttp_set_lenient_keep_alive
test('instance.exports.llhttp_set_lenient_keep_alive', async (t) => {
t = tspl(t, { plan: 2 })
t.ok(typeof instance.exports.llhttp_set_lenient_keep_alive === 'function', 'llhttp_set_lenient_keep_alive is present')
t.strictEqual(instance.exports.llhttp_set_lenient_keep_alive.length, 2, 'llhttp_set_lenient_keep_alive has the right number of arguments')
})
// llhttp_set_lenient_transfer_encoding
test('instance.exports.llhttp_set_lenient_transfer_encoding', async (t) => {
t = tspl(t, { plan: 2 })
t.ok(typeof instance.exports.llhttp_set_lenient_transfer_encoding === 'function', 'llhttp_set_lenient_transfer_encoding is present')
t.strictEqual(instance.exports.llhttp_set_lenient_transfer_encoding.length, 2, 'llhttp_set_lenient_transfer_encoding has the right number of arguments')
})
// llhttp_message_needs_eof
test('instance.exports.llhttp_message_needs_eof', async (t) => {
t = tspl(t, { plan: 2 })
t.ok(typeof instance.exports.llhttp_message_needs_eof === 'function', 'llhttp_message_needs_eof is present')
t.strictEqual(instance.exports.llhttp_message_needs_eof.length, 1, 'llhttp_message_needs_eof has the right number of arguments')
})
})
})
})
================================================
FILE: test/client-write-max-listeners.js
================================================
'use strict'
const { tspl } = require('@matteo.collina/tspl')
const { test, after } = require('node:test')
const { once } = require('node:events')
const { Client } = require('..')
const { createServer } = require('node:http')
const { Readable } = require('node:stream')
test('socket close listener does not leak', async (t) => {
t = tspl(t, { plan: 32 })
const server = createServer({ joinDuplicateHeaders: true })
server.on('request', (req, res) => {
res.end('hello')
})
after(() => server.close())
const makeBody = () => {
return new Readable({
read () {
process.nextTick(() => {
this.push(null)
})
}
})
}
const onRequest = (err, data) => {
t.ifError(err)
data.body.on('end', () => t.ok(true, 'pass')).resume()
}
function onWarning (warning) {
if (!/ExperimentalWarning/.test(warning)) {
t.fail()
}
}
process.on('warning', onWarning)
after(() => {
process.removeListener('warning', onWarning)
})
server.listen(0)
await once(server, 'listening')
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.destroy())
client.on('disconnect', () => {
if (!client.closed && !client.destroyed) {
t.fail('unexpected disconnect')
}
})
for (let n = 0; n < 16; ++n) {
client.request({ path: '/', method: 'GET', body: makeBody() }, onRequest)
}
await t.completed
})
================================================
FILE: test/client.js
================================================
'use strict'
const { tspl } = require('@matteo.collina/tspl')
const { readFileSync, createReadStream } = require('node:fs')
const { createServer } = require('node:http')
const { Readable, PassThrough } = require('node:stream')
const { test, after } = require('node:test')
const { Client, errors } = require('..')
const { kSocket } = require('../lib/core/symbols')
const { wrapWithAsyncIterable } = require('./utils/async-iterators')
const EE = require('node:events')
const { kUrl, kSize, kConnect, kBusy, kConnected, kRunning } = require('../lib/core/symbols')
const hasIPv6 = (() => {
const iFaces = require('node:os').networkInterfaces()
const re = process.platform === 'win32' ? /Loopback Pseudo-Interface/ : /lo/
return Object.keys(iFaces).some(
(name) => re.test(name) && iFaces[name].some(({ family }) => family === 'IPv6')
)
})()
test('basic get', async (t) => {
t = tspl(t, { plan: 24 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
t.strictEqual('/', req.url)
t.strictEqual('GET', req.method)
t.strictEqual(`localhost:${server.address().port}`, req.headers.host)
t.strictEqual(undefined, req.headers.foo)
t.strictEqual('bar', req.headers.bar)
t.strictEqual(undefined, req.headers['content-length'])
res.setHeader('Content-Type', 'text/plain')
res.end('hello')
})
after(() => server.close())
const reqHeaders = {
foo: undefined,
bar: 'bar'
}
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`, {
keepAliveTimeout: 300e3
})
after(() => client.close())
t.strictEqual(client[kUrl].origin, `http://localhost:${server.address().port}`)
const signal = new EE()
client.request({
signal,
path: '/',
method: 'GET',
headers: reqHeaders
}, (err, data) => {
t.ifError(err)
const { statusCode, headers, body } = data
t.strictEqual(statusCode, 200)
t.strictEqual(signal.listenerCount('abort'), 1)
t.strictEqual(headers['content-type'], 'text/plain')
const bufs = []
body.on('data', (buf) => {
bufs.push(buf)
})
body.on('close', () => {
t.strictEqual(signal.listenerCount('abort'), 0)
t.strictEqual('hello', Buffer.concat(bufs).toString('utf8'))
})
})
t.strictEqual(signal.listenerCount('abort'), 1)
client.request({
path: '/',
method: 'GET',
headers: reqHeaders
}, (err, { statusCode, headers, body }) => {
t.ifError(err)
t.strictEqual(statusCode, 200)
t.strictEqual(headers['content-type'], 'text/plain')
const bufs = []
body.on('data', (buf) => {
bufs.push(buf)
})
body.on('end', () => {
t.strictEqual('hello', Buffer.concat(bufs).toString('utf8'))
})
})
})
await t.completed
})
test('passes socketPath to custom connect function', async (t) => {
t = tspl(t, { plan: 2 })
const connectError = new Error('custom connect error')
const socketPath = '/var/run/test.sock'
const client = new Client('http://localhost', {
socketPath,
connect (opts, cb) {
t.strictEqual(opts.socketPath, socketPath)
cb(connectError, null)
}
})
after(() => client.close())
client.request({
path: '/',
method: 'GET'
}, (err) => {
t.strictEqual(err, connectError)
})
await t.completed
})
test('basic get with custom request.reset=true', async (t) => {
t = tspl(t, { plan: 26 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
t.strictEqual('/', req.url)
t.strictEqual('GET', req.method)
t.strictEqual(`localhost:${server.address().port}`, req.headers.host)
t.strictEqual(req.headers.connection, 'close')
t.strictEqual(undefined, req.headers.foo)
t.strictEqual('bar', req.headers.bar)
t.strictEqual(undefined, req.headers['content-length'])
res.setHeader('Content-Type', 'text/plain')
res.end('hello')
})
after(() => server.close())
const reqHeaders = {
foo: undefined,
bar: 'bar'
}
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`, {})
after(() => client.close())
t.strictEqual(client[kUrl].origin, `http://localhost:${server.address().port}`)
const signal = new EE()
client.request({
signal,
path: '/',
method: 'GET',
reset: true,
headers: reqHeaders
}, (err, data) => {
t.ifError(err)
const { statusCode, headers, body } = data
t.strictEqual(statusCode, 200)
t.strictEqual(signal.listenerCount('abort'), 1)
t.strictEqual(headers['content-type'], 'text/plain')
const bufs = []
body.on('data', (buf) => {
bufs.push(buf)
})
body.on('close', () => {
t.strictEqual(signal.listenerCount('abort'), 0)
t.strictEqual('hello', Buffer.concat(bufs).toString('utf8'))
})
})
t.strictEqual(signal.listenerCount('abort'), 1)
client.request({
path: '/',
reset: true,
method: 'GET',
headers: reqHeaders
}, (err, { statusCode, headers, body }) => {
t.ifError(err)
t.strictEqual(statusCode, 200)
t.strictEqual(headers['content-type'], 'text/plain')
const bufs = []
body.on('data', (buf) => {
bufs.push(buf)
})
body.on('end', () => {
t.strictEqual('hello', Buffer.concat(bufs).toString('utf8'))
})
})
})
await t.completed
})
test('basic get with query params', async (t) => {
t = tspl(t, { plan: 4 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
const searchParamsObject = buildParams(req.url)
t.deepStrictEqual(searchParamsObject, {
bool: 'true',
foo: '1',
bar: 'bar',
'%60~%3A%24%2C%2B%5B%5D%40%5E*()-': '%60~%3A%24%2C%2B%5B%5D%40%5E*()-',
multi: ['1', '2'],
nullVal: '',
undefinedVal: ''
})
res.statusCode = 200
res.end('hello')
})
after(() => server.close())
const query = {
bool: true,
foo: 1,
bar: 'bar',
nullVal: null,
undefinedVal: undefined,
'`~:$,+[]@^*()-': '`~:$,+[]@^*()-',
multi: [1, 2]
}
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`, {
keepAliveTimeout: 300e3
})
after(() => client.close())
const signal = new EE()
client.request({
signal,
path: '/',
method: 'GET',
query
}, (err, data) => {
t.ifError(err)
const { statusCode } = data
t.strictEqual(statusCode, 200)
})
t.strictEqual(signal.listenerCount('abort'), 1)
})
await t.completed
})
test('basic get with query params fails if url includes hashmark', async (t) => {
t = tspl(t, { plan: 1 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
t.fail()
})
after(() => server.close())
const query = {
foo: 1,
bar: 'bar',
multi: [1, 2]
}
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`, {
keepAliveTimeout: 300e3
})
after(() => client.close())
const signal = new EE()
client.request({
signal,
path: '/#',
method: 'GET',
query
}, (err, data) => {
t.strictEqual(err.message, 'Query params cannot be passed when url already contains "?" or "#".')
})
})
await t.completed
})
test('basic get with empty query params', async (t) => {
t = tspl(t, { plan: 4 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
const searchParamsObject = buildParams(req.url)
t.deepStrictEqual(searchParamsObject, {})
res.statusCode = 200
res.end('hello')
})
after(() => server.close())
const query = {}
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`, {
keepAliveTimeout: 300e3
})
after(() => client.close())
const signal = new EE()
client.request({
signal,
path: '/',
method: 'GET',
query
}, (err, data) => {
t.ifError(err)
const { statusCode } = data
t.strictEqual(statusCode, 200)
})
t.strictEqual(signal.listenerCount('abort'), 1)
})
await t.completed
})
test('basic get with query params partially in path', async (t) => {
t = tspl(t, { plan: 1 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
t.fail()
})
after(() => server.close())
const query = {
foo: 1
}
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`, {
keepAliveTimeout: 300e3
})
after(() => client.close())
const signal = new EE()
client.request({
signal,
path: '/?bar=2',
method: 'GET',
query
}, (err, data) => {
t.strictEqual(err.message, 'Query params cannot be passed when url already contains "?" or "#".')
})
})
await t.completed
})
test('using throwOnError should throw (request)', async (t) => {
t = tspl(t, { plan: 2 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.statusCode = 400
res.end('hello')
})
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`, {
keepAliveTimeout: 300e3
})
after(() => client.close())
const signal = new EE()
client.request({
signal,
path: '/',
method: 'GET',
throwOnError: true
}, (err) => {
t.strictEqual(err.message, 'invalid throwOnError')
t.strictEqual(err.code, 'UND_ERR_INVALID_ARG')
})
})
await t.completed
})
test('using throwOnError should throw (stream)', async (t) => {
t = tspl(t, { plan: 2 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.statusCode = 400
res.end('hello')
})
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`, {
keepAliveTimeout: 300e3
})
after(() => client.close())
client.stream({
path: '/',
method: 'GET',
throwOnError: true,
opaque: new PassThrough()
}, ({ opaque: pt }) => {
pt.on('data', () => {
t.fail()
})
return pt
}, err => {
t.strictEqual(err.message, 'invalid throwOnError')
t.strictEqual(err.code, 'UND_ERR_INVALID_ARG')
})
})
await t.completed
})
test('basic head', async (t) => {
t = tspl(t, { plan: 14 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
t.strictEqual('/123', req.url)
t.strictEqual('HEAD', req.method)
t.strictEqual(`localhost:${server.address().port}`, req.headers.host)
res.setHeader('content-type', 'text/plain')
res.end('hello')
})
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.close())
client.request({ path: '/123', method: 'HEAD' }, (err, { statusCode, headers, body }) => {
t.ifError(err)
t.strictEqual(statusCode, 200)
t.strictEqual(headers['content-type'], 'text/plain')
body
.resume()
.on('end', () => {
t.ok(true, 'pass')
})
})
client.request({ path: '/123', method: 'HEAD' }, (err, { statusCode, headers, body }) => {
t.ifError(err)
t.strictEqual(statusCode, 200)
t.strictEqual(headers['content-type'], 'text/plain')
body
.resume()
.on('end', () => {
t.ok(true, 'pass')
})
})
})
await t.completed
})
test('basic head (IPv6)', { skip: !hasIPv6 }, async (t) => {
t = tspl(t, { plan: 10 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
t.strictEqual('/123', req.url)
t.strictEqual('HEAD', req.method)
t.strictEqual(`[::1]:${server.address().port}`, req.headers.host)
res.setHeader('content-type', 'text/plain')
res.end('hello')
})
after(() => server.close())
server.listen(0, '::', () => {
const client = new Client(`http://[::1]:${server.address().port}`)
after(() => client.close())
client.request({ path: '/123', method: 'HEAD' }, (err, { statusCode, headers, body }) => {
t.ifError(err)
t.strictEqual(statusCode, 200)
t.strictEqual(headers['content-type'], 'text/plain')
body
.resume()
.on('end', () => {
t.ok(true, 'pass')
})
})
client.request({ path: '/123', method: 'HEAD' }, (err, { statusCode, headers, body }) => {
t.ifError(err)
t.strictEqual(statusCode, 200)
t.strictEqual(headers['content-type'], 'text/plain')
body
.resume()
.on('end', () => {
t.ok(true, 'pass')
})
})
})
await t.completed
})
test('get with host header', async (t) => {
t = tspl(t, { plan: 7 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
t.strictEqual('/', req.url)
t.strictEqual('GET', req.method)
t.strictEqual('example.com', req.headers.host)
res.setHeader('content-type', 'text/plain')
res.end('hello from ' + req.headers.host)
})
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.close())
client.request({ path: '/', method: 'GET', headers: { host: 'example.com' } }, (err, { statusCode, headers, body }) => {
t.ifError(err)
t.strictEqual(statusCode, 200)
t.strictEqual(headers['content-type'], 'text/plain')
const bufs = []
body.on('data', (buf) => {
bufs.push(buf)
})
body.on('end', () => {
t.strictEqual('hello from example.com', Buffer.concat(bufs).toString('utf8'))
})
})
})
await t.completed
})
test('get with host header (IPv6)', { skip: !hasIPv6 }, async (t) => {
t = tspl(t, { plan: 7 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
t.strictEqual('/', req.url)
t.strictEqual('GET', req.method)
t.strictEqual('[::1]', req.headers.host)
res.setHeader('content-type', 'text/plain')
res.end('hello from ' + req.headers.host)
})
after(() => server.close())
server.listen(0, '::', () => {
const client = new Client(`http://[::1]:${server.address().port}`)
after(() => client.close())
client.request({ path: '/', method: 'GET', headers: { host: '[::1]' } }, (err, { statusCode, headers, body }) => {
t.ifError(err)
t.strictEqual(statusCode, 200)
t.strictEqual(headers['content-type'], 'text/plain')
const bufs = []
body.on('data', (buf) => {
bufs.push(buf)
})
body.on('end', () => {
t.strictEqual('hello from [::1]', Buffer.concat(bufs).toString('utf8'))
})
})
})
await t.completed
})
test('head with host header', async (t) => {
t = tspl(t, { plan: 7 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
t.strictEqual('/', req.url)
t.strictEqual('HEAD', req.method)
t.strictEqual('example.com', req.headers.host)
res.setHeader('content-type', 'text/plain')
res.end('hello from ' + req.headers.host)
})
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.close())
client.request({ path: '/', method: 'HEAD', headers: { host: 'example.com' } }, (err, { statusCode, headers, body }) => {
t.ifError(err)
t.strictEqual(statusCode, 200)
t.strictEqual(headers['content-type'], 'text/plain')
body
.resume()
.on('end', () => {
t.ok(true, 'pass')
})
})
})
await t.completed
})
function postServer (t, expected) {
return function (req, res) {
t.strictEqual(req.url, '/')
t.strictEqual(req.method, 'POST')
t.notStrictEqual(req.headers['content-length'], null)
req.setEncoding('utf8')
let data = ''
req.on('data', function (d) { data += d })
req.on('end', () => {
t.strictEqual(data, expected)
res.end('hello')
})
}
}
test('basic POST with string', async (t) => {
t = tspl(t, { plan: 7 })
const expected = readFileSync(__filename, 'utf8')
const server = createServer({ joinDuplicateHeaders: true }, postServer(t, expected))
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.close())
client.request({ path: '/', method: 'POST', body: expected }, (err, data) => {
t.ifError(err)
t.strictEqual(data.statusCode, 200)
const bufs = []
data.body
.on('data', (buf) => {
bufs.push(buf)
})
.on('end', () => {
t.strictEqual('hello', Buffer.concat(bufs).toString('utf8'))
})
})
})
await t.completed
})
test('basic POST with empty string', async (t) => {
t = tspl(t, { plan: 7 })
const server = createServer({ joinDuplicateHeaders: true }, postServer(t, ''))
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.close())
client.request({ path: '/', method: 'POST', body: '' }, (err, { statusCode, headers, body }) => {
t.ifError(err)
t.strictEqual(statusCode, 200)
const bufs = []
body.on('data', (buf) => {
bufs.push(buf)
})
body.on('end', () => {
t.strictEqual('hello', Buffer.concat(bufs).toString('utf8'))
})
})
})
await t.completed
})
test('basic POST with string and content-length', async (t) => {
t = tspl(t, { plan: 7 })
const expected = readFileSync(__filename, 'utf8')
const server = createServer({ joinDuplicateHeaders: true }, postServer(t, expected))
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.close())
client.request({
path: '/',
method: 'POST',
headers: {
'content-length': Buffer.byteLength(expected)
},
body: expected
}, (err, { statusCode, headers, body }) => {
t.ifError(err)
t.strictEqual(statusCode, 200)
const bufs = []
body.on('data', (buf) => {
bufs.push(buf)
})
body.on('end', () => {
t.strictEqual('hello', Buffer.concat(bufs).toString('utf8'))
})
})
})
await t.completed
})
test('basic POST with Buffer', async (t) => {
t = tspl(t, { plan: 7 })
const expected = readFileSync(__filename)
const server = createServer({ joinDuplicateHeaders: true }, postServer(t, expected.toString()))
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.close())
client.request({ path: '/', method: 'POST', body: expected }, (err, { statusCode, headers, body }) => {
t.ifError(err)
t.strictEqual(statusCode, 200)
const bufs = []
body.on('data', (buf) => {
bufs.push(buf)
})
body.on('end', () => {
t.strictEqual('hello', Buffer.concat(bufs).toString('utf8'))
})
})
})
await t.completed
})
test('basic POST with stream', async (t) => {
t = tspl(t, { plan: 7 })
const expected = readFileSync(__filename, 'utf8')
const server = createServer({ joinDuplicateHeaders: true }, postServer(t, expected))
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.close())
client.request({
path: '/',
method: 'POST',
headers: {
'content-length': Buffer.byteLength(expected)
},
headersTimeout: 0,
body: createReadStream(__filename)
}, (err, { statusCode, headers, body }) => {
t.ifError(err)
t.strictEqual(statusCode, 200)
const bufs = []
body.on('data', (buf) => {
bufs.push(buf)
})
body.on('end', () => {
t.strictEqual('hello', Buffer.concat(bufs).toString('utf8'))
})
})
})
await t.completed
})
test('basic POST with paused stream', async (t) => {
t = tspl(t, { plan: 7 })
const expected = readFileSync(__filename, 'utf8')
const server = createServer({ joinDuplicateHeaders: true }, postServer(t, expected))
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.close())
const stream = createReadStream(__filename)
stream.pause()
client.request({
path: '/',
method: 'POST',
headers: {
'content-length': Buffer.byteLength(expected)
},
headersTimeout: 0,
body: stream
}, (err, { statusCode, headers, body }) => {
t.ifError(err)
t.strictEqual(statusCode, 200)
const bufs = []
body.on('data', (buf) => {
bufs.push(buf)
})
body.on('end', () => {
t.strictEqual('hello', Buffer.concat(bufs).toString('utf8'))
})
})
})
await t.completed
})
test('basic POST with custom stream', async (t) => {
t = tspl(t, { plan: 4 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
req.resume().on('end', () => {
res.end('hello')
})
})
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.close())
const body = new EE()
body.pipe = () => {}
client.request({
path: '/',
method: 'POST',
headersTimeout: 0,
body
}, (err, data) => {
t.ifError(err)
t.strictEqual(data.statusCode, 200)
const bufs = []
data.body.on('data', (buf) => {
bufs.push(buf)
})
data.body.on('end', () => {
t.strictEqual('hello', Buffer.concat(bufs).toString('utf8'))
})
})
t.deepStrictEqual(client[kBusy], true)
body.on('close', () => {
body.emit('end')
})
client.on('connect', () => {
setImmediate(() => {
body.emit('data', '')
while (!client[kSocket]._writableState.needDrain) {
body.emit('data', Buffer.alloc(4096))
}
client[kSocket].on('drain', () => {
body.emit('data', Buffer.alloc(4096))
body.emit('close')
})
})
})
})
await t.completed
})
test('basic POST with iterator', async (t) => {
t = tspl(t, { plan: 3 })
const expected = 'hello'
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
req.resume().on('end', () => {
res.end(expected)
})
})
after(() => server.close())
const iterable = {
[Symbol.iterator]: function * () {
for (let i = 0; i < expected.length - 1; i++) {
yield expected[i]
}
return expected[expected.length - 1]
}
}
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.close())
client.request({
path: '/',
method: 'POST',
requestTimeout: 0,
body: iterable
}, (err, { statusCode, body }) => {
t.ifError(err)
t.strictEqual(statusCode, 200)
const bufs = []
body.on('data', (buf) => {
bufs.push(buf)
})
body.on('end', () => {
t.strictEqual('hello', Buffer.concat(bufs).toString('utf8'))
})
})
})
await t.completed
})
test('basic POST with iterator with invalid data', async (t) => {
t = tspl(t, { plan: 1 })
const server = createServer({ joinDuplicateHeaders: true }, () => {})
after(() => server.close())
const iterable = {
[Symbol.iterator]: function * () {
yield 0
}
}
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.close())
client.request({
path: '/',
method: 'POST',
requestTimeout: 0,
body: iterable
}, err => {
t.ok(err instanceof TypeError)
})
})
await t.completed
})
test('basic POST with async iterator', async (t) => {
t = tspl(t, { plan: 7 })
const expected = readFileSync(__filename, 'utf8')
const server = createServer({ joinDuplicateHeaders: true }, postServer(t, expected))
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.close())
client.request({
path: '/',
method: 'POST',
headers: {
'content-length': Buffer.byteLength(expected)
},
headersTimeout: 0,
body: wrapWithAsyncIterable(createReadStream(__filename))
}, (err, { statusCode, headers, body }) => {
t.ifError(err)
t.strictEqual(statusCode, 200)
const bufs = []
body.on('data', (buf) => {
bufs.push(buf)
})
body.on('end', () => {
t.strictEqual('hello', Buffer.concat(bufs).toString('utf8'))
})
})
})
await t.completed
})
test('basic POST with transfer encoding: chunked', async (t) => {
t = tspl(t, { plan: 8 })
let body
const server = createServer({ joinDuplicateHeaders: true }, function (req, res) {
t.strictEqual(req.url, '/')
t.strictEqual(req.method, 'POST')
t.strictEqual(req.headers['content-length'], undefined)
t.strictEqual(req.headers['transfer-encoding'], 'chunked')
body.push(null)
req.setEncoding('utf8')
let data = ''
req.on('data', function (d) { data += d })
req.on('end', () => {
t.strictEqual(data, 'asd')
res.end('hello')
})
})
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.close())
body = new Readable({
read () { }
})
body.push('asd')
client.request({
path: '/',
method: 'POST',
// no content-length header
body
}, (err, { statusCode, headers, body }) => {
t.ifError(err)
t.strictEqual(statusCode, 200)
const bufs = []
body.on('data', (buf) => {
bufs.push(buf)
})
body.on('end', () => {
t.strictEqual('hello', Buffer.concat(bufs).toString('utf8'))
})
})
})
await t.completed
})
test('basic POST with empty stream', async (t) => {
t = tspl(t, { plan: 4 })
const server = createServer({ joinDuplicateHeaders: true }, function (req, res) {
t.deepStrictEqual(req.headers['content-length'], '0')
req.pipe(res)
})
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.close())
const body = new Readable({
autoDestroy: false,
read () {
},
destroy (err, callback) {
callback(!this._readableState.endEmitted ? new Error('asd') : err)
}
}).on('end', () => {
process.nextTick(() => {
t.strictEqual(body.destroyed, true)
})
})
body.push(null)
client.request({
path: '/',
method: 'POST',
body
}, (err, { statusCode, headers, body }) => {
t.ifError(err)
body
.on('data', () => {
t.fail()
})
.on('end', () => {
t.ok(true, 'pass')
})
})
})
await t.completed
})
test('10 times GET', async (t) => {
const num = 10
t = tspl(t, { plan: 3 * num })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.end(req.url)
})
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.close())
for (let i = 0; i < num; i++) {
makeRequest(i)
}
function makeRequest (i) {
client.request({ path: '/' + i, method: 'GET' }, (err, { statusCode, headers, body }) => {
t.ifError(err)
t.strictEqual(statusCode, 200)
const bufs = []
body.on('data', (buf) => {
bufs.push(buf)
})
body.on('end', () => {
t.strictEqual('/' + i, Buffer.concat(bufs).toString('utf8'))
})
})
}
})
await t.completed
})
test('10 times HEAD', async (t) => {
const num = 10
t = tspl(t, { plan: num * 3 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.end(req.url)
})
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.close())
for (let i = 0; i < num; i++) {
makeRequest(i)
}
function makeRequest (i) {
client.request({ path: '/' + i, method: 'HEAD' }, (err, { statusCode, headers, body }) => {
t.ifError(err)
t.strictEqual(statusCode, 200)
body
.resume()
.on('end', () => {
t.ok(true, 'pass')
})
})
}
})
await t.completed
})
test('Set-Cookie', async (t) => {
t = tspl(t, { plan: 4 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.setHeader('content-type', 'text/plain')
res.setHeader('Set-Cookie', ['a cookie', 'another cookie', 'more cookies'])
res.end('hello')
})
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.close())
client.request({ path: '/', method: 'GET' }, (err, { statusCode, headers, body }) => {
t.ifError(err)
t.strictEqual(statusCode, 200)
t.deepStrictEqual(headers['set-cookie'], ['a cookie', 'another cookie', 'more cookies'])
const bufs = []
body.on('data', (buf) => {
bufs.push(buf)
})
body.on('end', () => {
t.strictEqual('hello', Buffer.concat(bufs).toString('utf8'))
})
})
})
await t.completed
})
test('ignore request header mutations', async (t) => {
t = tspl(t, { plan: 2 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
t.strictEqual(req.headers.test, 'test')
res.end()
})
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.close())
const headers = { test: 'test' }
client.request({
path: '/',
method: 'GET',
headers
}, (err, { body }) => {
t.ifError(err)
body.resume()
})
headers.test = 'asd'
})
await t.completed
})
test('url-like url', async (t) => {
t = tspl(t, { plan: 1 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.end()
})
after(() => server.close())
server.listen(0, () => {
const client = new Client({
hostname: 'localhost',
port: server.address().port,
protocol: 'http:'
})
after(() => client.close())
client.request({ path: '/', method: 'GET' }, (err, data) => {
t.ifError(err)
data.body.resume()
})
})
await t.completed
})
test('an absolute url as path', async (t) => {
t = tspl(t, { plan: 2 })
const path = 'http://example.com'
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
t.strictEqual(req.url, path)
res.end()
})
after(() => server.close())
server.listen(0, () => {
const client = new Client({
hostname: 'localhost',
port: server.address().port,
protocol: 'http:'
})
after(() => client.close())
client.request({ path, method: 'GET' }, (err, data) => {
t.ifError(err)
data.body.resume()
})
})
await t.completed
})
test('multiple destroy callback', async (t) => {
t = tspl(t, { plan: 4 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.end()
})
after(() => server.close())
server.listen(0, () => {
const client = new Client({
hostname: 'localhost',
port: server.address().port,
protocol: 'http:'
})
after(() => client.destroy())
client.request({ path: '/', method: 'GET' }, (err, data) => {
t.ifError(err)
data.body
.resume()
.on('error', (err) => {
t.ok(err instanceof Error)
})
client.destroy(new Error(), (err) => {
t.ifError(err)
})
client.destroy(new Error(), (err) => {
t.ifError(err)
})
})
})
await t.completed
})
test('only one streaming req at a time', async (t) => {
t = tspl(t, { plan: 7 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
req.pipe(res)
})
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`, {
pipelining: 4
})
after(() => client.destroy())
client.request({
path: '/',
method: 'GET'
}, (err, data) => {
t.ifError(err)
data.body.resume()
client.request({
path: '/',
method: 'GET'
}, (err, data) => {
t.ifError(err)
data.body.resume()
})
client.request({
path: '/',
method: 'PUT',
idempotent: true,
body: new Readable({
read () {
setImmediate(() => {
t.strictEqual(client[kBusy], true)
this.push(null)
})
}
}).on('resume', () => {
t.strictEqual(client[kSize], 1)
})
}, (err, data) => {
t.ifError(err)
data.body
.resume()
.on('end', () => {
t.ok(true, 'pass')
})
})
t.strictEqual(client[kBusy], true)
})
})
await t.completed
})
test('only one async iterating req at a time', async (t) => {
t = tspl(t, { plan: 6 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
req.pipe(res)
})
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`, {
pipelining: 4
})
after(() => client.destroy())
client.request({
path: '/',
method: 'GET'
}, (err, data) => {
t.ifError(err)
data.body.resume()
client.request({
path: '/',
method: 'GET'
}, (err, data) => {
t.ifError(err)
data.body.resume()
})
const body = wrapWithAsyncIterable(new Readable({
read () {
setImmediate(() => {
t.strictEqual(client[kBusy], true)
this.push(null)
})
}
}))
client.request({
path: '/',
method: 'PUT',
idempotent: true,
body
}, (err, data) => {
t.ifError(err)
data.body
.resume()
.on('end', () => {
t.ok(true, 'pass')
})
})
t.strictEqual(client[kBusy], true)
})
})
await t.completed
})
test('300 requests succeed', async (t) => {
t = tspl(t, { plan: 300 * 3 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.end('asd')
})
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.destroy())
for (let n = 0; n < 300; ++n) {
client.request({
path: '/',
method: 'GET'
}, (err, data) => {
t.ifError(err)
data.body.on('data', (chunk) => {
t.strictEqual(chunk.toString(), 'asd')
}).on('end', () => {
t.ok(true, 'pass')
})
})
}
})
await t.completed
})
test('request args validation', async (t) => {
t = tspl(t, { plan: 2 })
const client = new Client('http://localhost:5000')
client.request(null, (err) => {
t.ok(err instanceof errors.InvalidArgumentError)
})
try {
client.request(null, 'asd')
} catch (err) {
t.ok(err instanceof errors.InvalidArgumentError)
}
await t.completed
})
test('request args validation promise', async (t) => {
t = tspl(t, { plan: 1 })
const client = new Client('http://localhost:5000')
client.request(null).catch((err) => {
t.ok(err instanceof errors.InvalidArgumentError)
})
await t.completed
})
test('increase pipelining', async (t) => {
t = tspl(t, { plan: 4 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
req.resume()
})
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.destroy())
client.request({
path: '/',
method: 'GET',
blocking: false
}, () => {
if (!client.destroyed) {
t.fail()
}
})
client.request({
path: '/',
method: 'GET',
blocking: false
}, () => {
if (!client.destroyed) {
t.fail()
}
})
t.strictEqual(client[kRunning], 0)
client.on('connect', () => {
t.strictEqual(client[kRunning], 0)
process.nextTick(() => {
t.strictEqual(client[kRunning], 1)
client.pipelining = 3
t.strictEqual(client[kRunning], 2)
})
})
})
await t.completed
})
test('destroy in push', async (t) => {
t = tspl(t, { plan: 4 })
let _res
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.write('asd')
_res = res
})
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.close())
client.request({ path: '/', method: 'GET' }, (err, { body }) => {
t.ifError(err)
body.once('data', () => {
_res.write('asd')
body.on('data', (buf) => {
body.destroy()
_res.end()
}).on('error', (err) => {
t.ok(err)
})
})
})
client.request({ path: '/', method: 'GET' }, (err, { body }) => {
t.ifError(err)
let buf = ''
body.on('data', (chunk) => {
buf = chunk.toString()
_res.end()
}).on('end', () => {
t.strictEqual('asd', buf)
})
})
})
await t.completed
})
test('non recoverable socket error fails pending request', async (t) => {
t = tspl(t, { plan: 2 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.end()
})
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.close())
client.request({ path: '/', method: 'GET' }, (err, data) => {
t.strictEqual(err.message, 'kaboom')
})
client.request({ path: '/', method: 'GET' }, (err, data) => {
t.strictEqual(err.message, 'kaboom')
})
client.on('connect', () => {
client[kSocket].destroy(new Error('kaboom'))
})
})
await t.completed
})
test('POST empty with error', async (t) => {
t = tspl(t, { plan: 1 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
req.pipe(res)
})
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.close())
const body = new Readable({
read () {
}
})
body.push(null)
client.on('connect', () => {
process.nextTick(() => {
body.emit('error', new Error('asd'))
})
})
client.request({ path: '/', method: 'POST', body }, (err, data) => {
t.strictEqual(err.message, 'asd')
})
})
await t.completed
})
test('busy', async (t) => {
t = tspl(t, { plan: 2 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
req.pipe(res)
})
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`, {
pipelining: 1
})
after(() => client.close())
client[kConnect](() => {
client.request({
path: '/',
method: 'GET'
}, (err) => {
t.ifError(err)
})
t.strictEqual(client[kBusy], true)
})
})
await t.completed
})
test('connected', async (t) => {
t = tspl(t, { plan: 7 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
// needed so that disconnect is emitted
res.setHeader('connection', 'close')
req.pipe(res)
})
after(() => server.close())
server.listen(0, () => {
const url = new URL(`http://localhost:${server.address().port}`)
const client = new Client(url, {
pipelining: 1
})
after(() => client.close())
client.on('connect', (origin, [self]) => {
t.strictEqual(origin, url)
t.strictEqual(client, self)
})
client.on('disconnect', (origin, [self]) => {
t.strictEqual(origin, url)
t.strictEqual(client, self)
})
t.strictEqual(client[kConnected], false)
client[kConnect](() => {
client.request({
path: '/',
method: 'GET'
}, (err) => {
t.ifError(err)
})
t.strictEqual(client[kConnected], true)
})
})
await t.completed
})
test('emit disconnect after destroy', async t => {
t = tspl(t, { plan: 4 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
req.pipe(res)
})
after(() => server.close())
server.listen(0, () => {
const url = new URL(`http://localhost:${server.address().port}`)
const client = new Client(url)
t.strictEqual(client[kConnected], false)
client[kConnect](() => {
t.strictEqual(client[kConnected], true)
let disconnected = false
client.on('disconnect', () => {
disconnected = true
t.ok(true, 'pass')
})
client.destroy(() => {
t.strictEqual(disconnected, true)
})
})
})
await t.completed
})
test('end response before request', async t => {
t = tspl(t, { plan: 2 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.end()
})
after(() => server.close())
server.listen(0, async () => {
const client = new Client(`http://localhost:${server.address().port}`)
const readable = new Readable({
read () {
this.push('asd')
}
})
const { body } = await client.request({
method: 'GET',
path: '/',
body: readable
})
body
.on('error', () => {
t.fail()
})
.on('end', () => {
t.ok(true, 'pass')
})
.resume()
client.on('disconnect', (url, targets, err) => {
t.strictEqual(err.code, 'UND_ERR_INFO')
})
})
await t.completed
})
test('parser pause with no body timeout', async (t) => {
t = tspl(t, { plan: 2 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
let counter = 0
const t = setInterval(() => {
counter++
const payload = Buffer.alloc(counter * 4096).fill(0)
if (counter === 3) {
clearInterval(t)
res.end(payload)
} else {
res.write(payload)
}
}, 20)
})
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`, {
bodyTimeout: 0
})
after(() => client.close())
client.request({ path: '/', method: 'GET' }, (err, { statusCode, body }) => {
t.ifError(err)
t.strictEqual(statusCode, 200)
body.resume()
})
})
await t.completed
})
test('TypedArray and DataView body', async (t) => {
t = tspl(t, { plan: 3 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
t.strictEqual(req.headers['content-length'], '8')
res.end()
})
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`, {
bodyTimeout: 0
})
after(() => client.close())
const body = Uint8Array.from(Buffer.alloc(8))
client.request({ path: '/', method: 'POST', body }, (err, { statusCode, body }) => {
t.ifError(err)
t.strictEqual(statusCode, 200)
body.resume()
})
})
await t.completed
})
test('async iterator empty chunk continues', async (t) => {
t = tspl(t, { plan: 5 })
const serverChunks = ['hello', 'world']
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
let str = ''
let i = 0
req.on('data', (chunk) => {
const content = chunk.toString()
t.strictEqual(serverChunks[i++], content)
str += content
}).on('end', () => {
t.strictEqual(str, serverChunks.join(''))
res.end()
})
})
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`, {
bodyTimeout: 0
})
after(() => client.close())
const body = (async function * () {
yield serverChunks[0]
yield ''
yield serverChunks[1]
})()
client.request({ path: '/', method: 'POST', body }, (err, { statusCode, body }) => {
t.ifError(err)
t.strictEqual(statusCode, 200)
body.resume()
})
})
await t.completed
})
test('async iterator error from server destroys early', async (t) => {
t = tspl(t, { plan: 3 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
req.on('data', (chunk) => {
res.destroy()
})
})
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`, {
bodyTimeout: 0
})
after(() => client.close())
let gotDestroyed
const body = (async function * () {
try {
const promise = new Promise(resolve => {
gotDestroyed = resolve
})
yield 'hello'
await promise
yield 'inner-value'
t.fail('should not get here, iterator should be destroyed')
} finally {
t.ok(true, 'pass')
}
})()
client.request({ path: '/', method: 'POST', body }, (err, { statusCode, body }) => {
t.ok(err)
t.strictEqual(statusCode, undefined)
gotDestroyed()
})
})
await t.completed
})
test('regular iterator error from server closes early', async (t) => {
t = tspl(t, { plan: 3 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
req.on('data', () => {
process.nextTick(() => {
res.destroy()
})
})
})
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`, {
bodyTimeout: 0
})
after(() => client.close())
let gotDestroyed = false
const body = (function * () {
try {
yield 'start'
while (!gotDestroyed) {
yield 'zzz'
// for eslint
gotDestroyed = gotDestroyed || false
}
yield 'zzz'
t.fail('should not get here, iterator should be destroyed')
yield 'zzz'
} finally {
t.ok(true, 'pass')
}
})()
client.request({ path: '/', method: 'POST', body }, (err, { statusCode, body }) => {
t.ok(err)
t.strictEqual(statusCode, undefined)
gotDestroyed = true
})
})
await t.completed
})
test('async iterator early return closes early', async (t) => {
t = tspl(t, { plan: 3 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
req.on('data', () => {
res.writeHead(200)
res.end()
})
})
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`, {
bodyTimeout: 0
})
after(() => client.close())
let gotDestroyed
const body = (async function * () {
try {
const promise = new Promise(resolve => {
gotDestroyed = resolve
})
yield 'hello'
await promise
yield 'inner-value'
t.fail('should not get here, iterator should be destroyed')
} finally {
t.ok(true, 'pass')
}
})()
client.request({ path: '/', method: 'POST', body }, (err, { statusCode, body }) => {
t.ifError(err)
t.strictEqual(statusCode, 200)
gotDestroyed()
})
})
await t.completed
})
test('async iterator yield unsupported TypedArray', {
skip: !!require('node:stream')._isArrayBufferView
}, async (t) => {
t = tspl(t, { plan: 3 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
req.on('end', () => {
res.writeHead(200)
res.end()
})
})
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`, {
bodyTimeout: 0
})
after(() => client.close())
const body = (async function * () {
try {
yield new Int32Array([1])
t.fail('should not get here, iterator should be destroyed')
} finally {
t.ok(true, 'pass')
}
})()
client.request({ path: '/', method: 'POST', body }, (err) => {
t.ok(err)
t.strictEqual(err.code, 'ERR_INVALID_ARG_TYPE')
})
})
await t.completed
})
test('async iterator yield object error', async (t) => {
t = tspl(t, { plan: 3 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
req.on('end', () => {
res.writeHead(200)
res.end()
})
})
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`, {
bodyTimeout: 0
})
after(() => client.close())
const body = (async function * () {
try {
yield {}
t.fail('should not get here, iterator should be destroyed')
} finally {
t.ok(true, 'pass')
}
})()
client.request({ path: '/', method: 'POST', body }, (err) => {
t.ok(err)
t.strictEqual(err.code, 'ERR_INVALID_ARG_TYPE')
})
})
await t.completed
})
test('Successfully get a Response when neither a Transfer-Encoding or Content-Length header is present', async (t) => {
t = tspl(t, { plan: 4 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
req.on('data', (data) => {
})
req.on('end', () => {
res.removeHeader('transfer-encoding')
res.writeHead(200, {
// Header isn't actually necessary, but tells node to close after response
connection: 'close',
foo: 'bar'
})
res.flushHeaders()
res.end('a response body')
})
})
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.close())
client.request({ path: '/', method: 'GET' }, (err, { body, headers }) => {
t.ifError(err)
t.equal(headers['content-length'], undefined)
t.equal(headers['transfer-encoding'], undefined)
const bufs = []
body.on('error', () => {
t.fail('Closing the connection is valid')
})
body.on('data', (buf) => {
bufs.push(buf)
})
body.on('end', () => {
t.equal('a response body', Buffer.concat(bufs).toString('utf8'))
})
})
})
await t.completed
})
function buildParams (path) {
const cleanPath = path.replace('/?', '').replace('/', '').split('&')
const builtParams = cleanPath.reduce((acc, entry) => {
const [key, value] = entry.split('=')
if (key.length === 0) {
return acc
}
if (acc[key]) {
if (Array.isArray(acc[key])) {
acc[key].push(value)
} else {
acc[key] = [acc[key], value]
}
} else {
acc[key] = value
}
return acc
}, {})
return builtParams
}
test('\\r\\n in Headers', async (t) => {
t = tspl(t, { plan: 1 })
const reqHeaders = {
bar: '\r\nbar'
}
const client = new Client('http://localhost:4242', {
keepAliveTimeout: 300e3
})
after(() => client.close())
client.request({
path: '/',
method: 'GET',
headers: reqHeaders
}, (err) => {
t.strictEqual(err.message, 'invalid bar header')
})
})
test('\\r in Headers', async (t) => {
t = tspl(t, { plan: 1 })
const reqHeaders = {
bar: '\rbar'
}
const client = new Client('http://localhost:4242', {
keepAliveTimeout: 300e3
})
after(() => client.close())
client.request({
path: '/',
method: 'GET',
headers: reqHeaders
}, (err) => {
t.strictEqual(err.message, 'invalid bar header')
})
})
test('\\n in Headers', async (t) => {
t = tspl(t, { plan: 1 })
const reqHeaders = {
bar: '\nbar'
}
const client = new Client('http://localhost:4242', {
keepAliveTimeout: 300e3
})
after(() => client.close())
client.request({
path: '/',
method: 'GET',
headers: reqHeaders
}, (err) => {
t.strictEqual(err.message, 'invalid bar header')
})
})
test('\\n in Headers', async (t) => {
t = tspl(t, { plan: 1 })
const reqHeaders = {
'\nbar': 'foo'
}
const client = new Client('http://localhost:4242', {
keepAliveTimeout: 300e3
})
after(() => client.close())
client.request({
path: '/',
method: 'GET',
headers: reqHeaders
}, (err) => {
t.strictEqual(err.message, 'invalid header key')
})
})
test('\\n in Path', async (t) => {
t = tspl(t, { plan: 1 })
const client = new Client('http://localhost:4242', {
keepAliveTimeout: 300e3
})
after(() => client.close())
client.request({
path: '/\n',
method: 'GET'
}, (err) => {
t.strictEqual(err.message, 'invalid request path')
})
})
test('\\n in Method', async (t) => {
t = tspl(t, { plan: 1 })
const client = new Client('http://localhost:4242', {
keepAliveTimeout: 300e3
})
after(() => client.close())
client.request({
path: '/',
method: 'GET\n'
}, (err) => {
t.strictEqual(err.message, 'invalid request method')
})
})
test('stats', async (t) => {
t = tspl(t, { plan: 3 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
t.strictEqual('/', req.url)
t.strictEqual('GET', req.method)
t.strictEqual(`localhost:${server.address().port}`, req.headers.host)
res.setHeader('Content-Type', 'text/plain')
res.end('hello')
})
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.close())
client.request({
path: '/',
method: 'GET'
}, (err, data) => {
t.ifError(err)
t.strictEqual(client.stats.connected, true)
t.strictEqual(client.stats.pending, 0)
t.strictEqual(client.stats.running, 1)
})
})
await t.completed
})
================================================
FILE: test/close-and-destroy.js
================================================
'use strict'
const { tspl } = require('@matteo.collina/tspl')
const { test, after } = require('node:test')
const { Client, errors } = require('..')
const { createServer } = require('node:http')
const { kSocket, kSize } = require('../lib/core/symbols')
test('close waits for queued requests to finish', async (t) => {
t = tspl(t, { plan: 16 })
const server = createServer({ joinDuplicateHeaders: true })
server.on('request', (req, res) => {
t.ok(true, 'request received')
res.end('hello')
})
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.destroy())
client.request({ path: '/', method: 'GET' }, function (err, data) {
onRequest(err, data)
client.request({ path: '/', method: 'GET' }, onRequest)
client.request({ path: '/', method: 'GET' }, onRequest)
client.request({ path: '/', method: 'GET' }, onRequest)
// needed because the next element in the queue will be called
// after the current function completes
process.nextTick(function () {
client.close()
})
})
})
function onRequest (err, { statusCode, headers, body }) {
t.ifError(err)
t.strictEqual(statusCode, 200)
const bufs = []
body.on('data', (buf) => {
bufs.push(buf)
})
body.on('end', () => {
t.strictEqual('hello', Buffer.concat(bufs).toString('utf8'))
})
}
await t.completed
})
test('destroy invoked all pending callbacks', async (t) => {
t = tspl(t, { plan: 4 })
const server = createServer({ joinDuplicateHeaders: true })
server.on('request', (req, res) => {
res.write('hello')
})
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`, {
pipelining: 2
})
after(() => client.destroy())
client.request({ path: '/', method: 'GET' }, (err, data) => {
t.ifError(err)
data.body.on('error', (err) => {
t.ok(err)
}).resume()
client.destroy()
})
client.request({ path: '/', method: 'GET' }, (err) => {
t.ok(err instanceof errors.ClientDestroyedError)
})
client.request({ path: '/', method: 'GET' }, (err) => {
t.ok(err instanceof errors.ClientDestroyedError)
})
})
await t.completed
})
test('destroy invoked all pending callbacks ticked', async (t) => {
t = tspl(t, { plan: 4 })
const server = createServer({ joinDuplicateHeaders: true })
server.on('request', (req, res) => {
res.write('hello')
})
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`, {
pipelining: 2
})
after(() => client.destroy())
let ticked = false
client.request({ path: '/', method: 'GET' }, (err) => {
t.strictEqual(ticked, true)
t.ok(err instanceof errors.ClientDestroyedError)
})
client.request({ path: '/', method: 'GET' }, (err) => {
t.strictEqual(ticked, true)
t.ok(err instanceof errors.ClientDestroyedError)
})
client.destroy()
ticked = true
})
await t.completed
})
test('close waits until socket is destroyed', async (t) => {
t = tspl(t, { plan: 4 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.end(req.url)
})
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.destroy())
makeRequest()
client.once('connect', () => {
let done = false
client[kSocket].on('close', () => {
done = true
})
client.close((err) => {
t.ifError(err)
t.strictEqual(client.closed, true)
t.strictEqual(done, true)
})
})
function makeRequest () {
client.request({ path: '/', method: 'GET' }, (err, data) => {
t.ifError(err)
})
return client[kSize] <= client.pipelining
}
})
await t.completed
})
test('close should still reconnect', async (t) => {
t = tspl(t, { plan: 6 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.end(req.url)
})
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.destroy())
t.ok(makeRequest())
t.ok(!makeRequest())
client.close((err) => {
t.ifError(err)
t.strictEqual(client.closed, true)
})
client.once('connect', () => {
client[kSocket].destroy()
})
function makeRequest () {
client.request({ path: '/', method: 'GET' }, (err, data) => {
t.ifError(err)
data.body.resume()
})
return client[kSize] <= client.pipelining
}
})
await t.completed
})
test('close should call callback once finished', async (t) => {
t = tspl(t, { plan: 6 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
setImmediate(function () {
res.end(req.url)
})
})
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.destroy())
t.ok(makeRequest())
t.ok(!makeRequest())
client.close((err) => {
t.ifError(err)
t.strictEqual(client.closed, true)
})
function makeRequest () {
client.request({ path: '/', method: 'GET' }, (err, data) => {
t.ifError(err)
data.body.resume()
})
return client[kSize] <= client.pipelining
}
})
await t.completed
})
test('closed and destroyed errors', async (t) => {
t = tspl(t, { plan: 4 })
const client = new Client('http://localhost:4000')
after(() => client.destroy())
client.request({ path: '/', method: 'GET' }, (err) => {
t.ok(err)
})
client.close((err) => {
t.ifError(err)
})
client.request({ path: '/', method: 'GET' }, (err) => {
t.ok(err instanceof errors.ClientClosedError)
client.destroy()
client.request({ path: '/', method: 'GET' }, (err) => {
t.ok(err instanceof errors.ClientDestroyedError)
})
})
await t.completed
})
test('close after and destroy should error', async (t) => {
t = tspl(t, { plan: 2 })
const client = new Client('http://localhost:4000')
after(() => client.destroy())
client.destroy()
client.close((err) => {
t.ok(err instanceof errors.ClientDestroyedError)
})
client.close().catch((err) => {
t.ok(err instanceof errors.ClientDestroyedError)
})
await t.completed
})
test('close socket and reconnect after maxRequestsPerClient reached', async (t) => {
t = tspl(t, { plan: 1 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.end(req.url)
})
after(() => server.close())
server.listen(0, async () => {
let connections = 0
server.on('connection', () => {
connections++
})
const client = new Client(
`http://localhost:${server.address().port}`,
{ maxRequestsPerClient: 2 }
)
after(() => client.destroy())
await makeRequest()
await makeRequest()
await makeRequest()
await makeRequest()
t.strictEqual(connections, 2)
function makeRequest () {
return client.request({ path: '/', method: 'GET' })
}
})
await t.completed
})
test('close socket and reconnect after maxRequestsPerClient reached (async)', async (t) => {
t = tspl(t, { plan: 1 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.end(req.url)
})
after(() => server.close())
server.listen(0, async () => {
let connections = 0
server.on('connection', () => {
connections++
})
const client = new Client(
`http://localhost:${server.address().port}`,
{ maxRequestsPerClient: 2 }
)
after(() => client.destroy())
await Promise.all([
makeRequest(),
makeRequest(),
makeRequest(),
makeRequest()
])
t.strictEqual(connections, 2)
function makeRequest () {
return client.request({ path: '/', method: 'GET' })
}
})
await t.completed
})
test('should not close socket when no maxRequestsPerClient is provided', async (t) => {
t = tspl(t, { plan: 1 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.end(req.url)
})
after(() => server.close())
server.listen(0, async () => {
let connections = 0
server.on('connection', () => {
connections++
})
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.destroy())
await makeRequest()
await makeRequest()
await makeRequest()
await makeRequest()
t.strictEqual(connections, 1)
function makeRequest () {
return client.request({ path: '/', method: 'GET' })
}
})
await t.completed
})
================================================
FILE: test/connect-abort.js
================================================
'use strict'
const { tspl } = require('@matteo.collina/tspl')
const { test } = require('node:test')
const { Client } = require('..')
const { PassThrough } = require('node:stream')
test('connect-abort', async t => {
t = tspl(t, { plan: 2 })
const client = new Client('http://localhost:1234', {
connect: (_, cb) => {
client.destroy()
cb(null, new PassThrough({
destroy (err, cb) {
t.strictEqual(err.name, 'ClientDestroyedError')
cb(null)
}
}))
}
})
client.request({
path: '/',
method: 'GET'
}, (err, data) => {
t.strictEqual(err.name, 'ClientDestroyedError')
})
await t.completed
})
================================================
FILE: test/connect-errconnect.js
================================================
'use strict'
const { tspl } = require('@matteo.collina/tspl')
const { test, after } = require('node:test')
const { Client } = require('..')
const net = require('node:net')
test('connect-connectionError', async t => {
t = tspl(t, { plan: 2 })
const client = new Client('http://localhost:9000')
after(() => client.close())
client.once('connectionError', () => {
t.ok(true, 'pass')
})
const _err = new Error('kaboom')
net.connect = function (options) {
const socket = new net.Socket(options)
setImmediate(() => {
socket.destroy(_err)
})
return socket
}
client.request({
path: '/',
method: 'GET'
}, (err) => {
t.strictEqual(err, _err)
})
await t.completed
})
================================================
FILE: test/connect-pre-shared-session.js
================================================
'use strict'
const { tspl } = require('@matteo.collina/tspl')
const { test, after, mock } = require('node:test')
const { Client } = require('..')
const { createServer } = require('node:https')
const pem = require('@metcoder95/https-pem')
const tls = require('node:tls')
test('custom session passed to client will be used in tls connect call', async (t) => {
t = tspl(t, { plan: 6 })
const mockConnect = mock.method(tls, 'connect')
const server = createServer({ ...pem, joinDuplicateHeaders: true }, (req, res) => {
t.strictEqual('/', req.url)
t.strictEqual('GET', req.method)
res.setHeader('content-type', 'text/plain')
res.end('hello')
})
after(() => server.close())
server.listen(0, async () => {
const session = Buffer.from('test-session')
const client = new Client(`https://localhost:${server.address().port}`, {
connect: {
rejectUnauthorized: false,
session
}
})
after(() => client.close())
client.on('disconnect', () => {
if (!client.closed && !client.destroyed) {
t.fail('unexpected disconnect')
}
})
const { statusCode, headers, body } = await client.request({
path: '/',
method: 'GET'
})
t.strictEqual(statusCode, 200)
t.strictEqual(headers['content-type'], 'text/plain')
const responseText = await body.text()
t.strictEqual('hello', responseText)
const connectSession = mockConnect.mock.calls[0].arguments[0].session
t.strictEqual(connectSession, session)
})
await t.completed
})
================================================
FILE: test/connect-timeout.js
================================================
'use strict'
const { tspl } = require('@matteo.collina/tspl')
const { test, after, describe } = require('node:test')
const { Client, Pool, errors } = require('..')
const net = require('node:net')
const assert = require('node:assert')
const skip = !!process.env.CITGM
// Using describe instead of test to avoid the timeout
describe('prioritize socket errors over timeouts', { skip }, async () => {
const t = tspl({ ...assert, after: () => {} }, { plan: 2 })
const client = new Pool('http://foorbar.invalid:1234', { connectTimeout: 1 })
client.request({ method: 'GET', path: '/foobar' })
.then(() => t.fail())
.catch((err) => {
t.strictEqual(err.code, 'ENOTFOUND')
t.strictEqual(err.code !== 'UND_ERR_CONNECT_TIMEOUT', true)
})
// block for 1s which is enough for the dns lookup to complete and the
// Timeout to fire
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, Number(1000))
await t.completed
})
// mock net.connect to avoid the dns lookup
net.connect = function (options) {
return new net.Socket(options)
}
test('connect-timeout', { skip }, async t => {
t = tspl(t, { plan: 3 })
const client = new Client('http://localhost:9000', {
connectTimeout: 1e3
})
after(() => client.close())
const timeout = setTimeout(() => {
t.fail()
}, 2e3)
client.request({
path: '/',
method: 'GET'
}, (err) => {
t.ok(err instanceof errors.ConnectTimeoutError)
t.strictEqual(err.code, 'UND_ERR_CONNECT_TIMEOUT')
t.strictEqual(err.message, 'Connect Timeout Error (attempted address: localhost:9000, timeout: 1000ms)')
clearTimeout(timeout)
})
await t.completed
})
test('connect-timeout', { skip }, async t => {
t = tspl(t, { plan: 3 })
const client = new Pool('http://localhost:9000', {
connectTimeout: 1e3
})
after(() => client.close())
const timeout = setTimeout(() => {
t.fail()
}, 2e3)
client.request({
path: '/',
method: 'GET'
}, (err) => {
t.ok(err instanceof errors.ConnectTimeoutError)
t.strictEqual(err.code, 'UND_ERR_CONNECT_TIMEOUT')
t.strictEqual(err.message, 'Connect Timeout Error (attempted address: localhost:9000, timeout: 1000ms)')
clearTimeout(timeout)
})
await t.completed
})
================================================
FILE: test/content-length.js
================================================
'use strict'
const { tspl } = require('@matteo.collina/tspl')
const { test, after } = require('node:test')
const { Client, errors } = require('..')
const { createServer } = require('node:http')
const { Readable } = require('node:stream')
const { maybeWrapStream, consts } = require('./utils/async-iterators')
test('request invalid content-length', async (t) => {
t = tspl(t, { plan: 7 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.end()
})
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.close())
client.request({
path: '/',
method: 'PUT',
headers: {
'content-length': 10
},
body: 'asd'
}, (err, data) => {
t.ok(err instanceof errors.RequestContentLengthMismatchError)
})
client.request({
path: '/',
method: 'PUT',
headers: {
'content-length': 10
},
body: 'asdasdasdasdasdasda'
}, (err, data) => {
t.ok(err instanceof errors.RequestContentLengthMismatchError)
})
client.request({
path: '/',
method: 'PUT',
headers: {
'content-length': 10
},
body: Buffer.alloc(9)
}, (err, data) => {
t.ok(err instanceof errors.RequestContentLengthMismatchError)
})
client.request({
path: '/',
method: 'PUT',
headers: {
'content-length': 10
},
body: Buffer.alloc(11)
}, (err, data) => {
t.ok(err instanceof errors.RequestContentLengthMismatchError)
})
client.request({
path: '/',
method: 'GET',
headers: {
'content-length': 4
},
body: ['asd']
}, (err, data) => {
t.ok(err instanceof errors.RequestContentLengthMismatchError)
})
client.request({
path: '/',
method: 'GET',
headers: {
'content-length': 4
},
body: ['asasdasdasdd']
}, (err, data) => {
t.ok(err instanceof errors.RequestContentLengthMismatchError)
})
client.request({
path: '/',
method: 'DELETE',
headers: {
'content-length': 4
},
body: ['asasdasdasdd']
}, (err, data) => {
t.ok(err instanceof errors.RequestContentLengthMismatchError)
})
})
await t.completed
})
function invalidContentLength (bodyType) {
test(`request streaming ${bodyType} invalid content-length`, async (t) => {
t = tspl(t, { plan: 4 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.end()
})
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.close())
client.once('disconnect', () => {
t.ok(true, 'pass')
client.once('disconnect', () => {
t.ok(true, 'pass')
})
})
client.request({
path: '/',
method: 'PUT',
headers: {
'content-length': 10
},
body: maybeWrapStream(new Readable({
read () {
setImmediate(() => {
this.push('asdasdasdkajsdnasdkjasnd')
this.push(null)
})
}
}), bodyType)
}, (err, data) => {
t.ok(err instanceof errors.RequestContentLengthMismatchError)
})
client.request({
path: '/',
method: 'PUT',
headers: {
'content-length': 10
},
body: maybeWrapStream(new Readable({
read () {
setImmediate(() => {
this.push('asd')
this.push(null)
})
}
}), bodyType)
}, (err, data) => {
t.ok(err instanceof errors.RequestContentLengthMismatchError)
})
})
await t.completed
})
}
invalidContentLength(consts.STREAM)
invalidContentLength(consts.ASYNC_ITERATOR)
function zeroContentLength (bodyType) {
test(`request ${bodyType} streaming data when content-length=0`, async (t) => {
t = tspl(t, { plan: 1 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.end()
})
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.close())
client.request({
path: '/',
method: 'PUT',
headers: {
'content-length': 0
},
body: maybeWrapStream(new Readable({
read () {
setImmediate(() => {
this.push('asdasdasdkajsdnasdkjasnd')
this.push(null)
})
}
}), bodyType)
}, (err, data) => {
t.ok(err instanceof errors.RequestContentLengthMismatchError)
})
})
await t.completed
})
}
zeroContentLength(consts.STREAM)
zeroContentLength(consts.ASYNC_ITERATOR)
test('request streaming no body data when content-length=0', async (t) => {
t = tspl(t, { plan: 2 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
req.pipe(res)
})
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.close())
client.on('disconnect', () => {
if (!client.closed && !client.destroyed) {
t.fail('unexpected disconnect')
}
})
client.request({
path: '/',
method: 'PUT',
headers: {
'content-length': 0
}
}, (err, data) => {
t.ifError(err)
data.body
.on('data', () => {
t.fail()
})
.on('end', () => {
t.ok(true, 'pass')
})
})
})
await t.completed
})
test('response invalid content length with close', async (t) => {
t = tspl(t, { plan: 3 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.writeHead(200, {
'content-length': 10
})
res.end('123')
})
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`, {
pipelining: 0
})
after(() => client.close())
client.on('disconnect', (origin, client, err) => {
t.strictEqual(err.code, 'UND_ERR_RES_CONTENT_LENGTH_MISMATCH')
})
client.request({
path: '/',
method: 'GET'
}, (err, data) => {
t.ifError(err)
data.body
.on('end', () => {
t.fail()
})
.on('error', (err) => {
t.strictEqual(err.code, 'UND_ERR_RES_CONTENT_LENGTH_MISMATCH')
})
.resume()
})
})
await t.completed
})
test('request streaming with Readable.from(buf)', async (t) => {
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
req.pipe(res)
})
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.close())
client.on('disconnect', () => {
if (!client.closed && !client.destroyed) {
t.fail('unexpected disconnect')
}
})
client.request({
path: '/',
method: 'PUT',
body: Readable.from(Buffer.from('hello'))
}, (err, data) => {
const chunks = []
t.ifError(err)
data.body
.on('data', (chunk) => {
chunks.push(chunk)
})
.on('end', () => {
t.strictEqual(Buffer.concat(chunks).toString(), 'hello')
t.ok(true, 'pass')
t.end()
})
})
})
await t.completed
})
test('request DELETE, content-length=0, with body', async (t) => {
t = tspl(t, { plan: 5 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.shouldKeepAlive = false
res.end()
})
server.on('request', (req, res) => {
t.strictEqual(req.headers['content-length'], undefined)
})
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.close())
client.request({
path: '/',
method: 'DELETE',
headers: {
'content-length': 0
},
body: new Readable({
read () {
this.push('asd')
this.push(null)
}
})
}, (err) => {
t.ok(err instanceof errors.RequestContentLengthMismatchError)
})
client.request({
path: '/',
method: 'DELETE',
headers: {
'content-length': 0
}
}, (err, resp) => {
t.strictEqual(resp.headers['content-length'], '0')
t.ifError(err)
})
client.on('disconnect', () => {
t.ok(true, 'pass')
})
})
await t.completed
})
test('content-length shouldSendContentLength=false', async (t) => {
t = tspl(t, { plan: 15 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.end()
})
server.on('request', (req, res) => {
switch (req.url) {
case '/put0':
t.strictEqual(req.headers['content-length'], '0')
break
case '/head':
t.strictEqual(req.headers['content-length'], undefined)
break
case '/get':
t.strictEqual(req.headers['content-length'], undefined)
break
}
})
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.close())
client.request({
path: '/put0',
method: 'PUT',
headers: {
'content-length': 0
}
}, (err, resp) => {
t.strictEqual(resp.headers['content-length'], '0')
t.ifError(err)
})
client.request({
path: '/head',
method: 'HEAD',
headers: {
'content-length': 10
}
}, (err, resp) => {
t.strictEqual(resp.headers['content-length'], undefined)
t.ifError(err)
})
client.request({
path: '/get',
method: 'GET',
headers: {
'content-length': 0
}
}, (err) => {
t.ifError(err)
})
client.request({
path: '/',
method: 'GET',
headers: {
'content-length': 4
},
body: new Readable({
read () {
this.push('asd')
this.push(null)
}
})
}, (err) => {
t.ifError(err)
})
client.request({
path: '/',
method: 'GET',
headers: {
'content-length': 4
},
body: new Readable({
read () {
this.push('asasdasdasdd')
this.push(null)
}
})
}, (err) => {
t.ifError(err)
})
client.request({
path: '/',
method: 'HEAD',
headers: {
'content-length': 4
},
body: new Readable({
read () {
this.push('asasdasdasdd')
this.push(null)
}
})
}, (err) => {
t.ifError(err)
})
client.on('disconnect', () => {
t.ok(true, 'pass')
})
})
await t.completed
})
================================================
FILE: test/cookie/cookies.js
================================================
// MIT License
//
// Copyright 2018-2022 the Deno authors.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
'use strict'
const { test } = require('node:test')
const assert = require('node:assert')
const {
deleteCookie,
getCookies,
getSetCookies,
setCookie,
Headers
} = require('../..')
// https://raw.githubusercontent.com/denoland/deno_std/b4239898d6c6b4cdbfd659a4ea1838cf4e656336/http/cookie_test.ts
test('Cookie parser', () => {
let headers = new Headers()
assert.deepEqual(getCookies(headers), {})
headers = new Headers()
headers.set('Cookie', 'foo=bar')
assert.deepEqual(getCookies(headers), { foo: 'bar' })
headers = new Headers()
headers.set('Cookie', 'full=of ; tasty=chocolate')
assert.deepEqual(getCookies(headers), { full: 'of ', tasty: 'chocolate' })
headers = new Headers()
headers.set('Cookie', 'igot=99; problems=but...')
assert.deepEqual(getCookies(headers), { igot: '99', problems: 'but...' })
headers = new Headers()
headers.set('Cookie', 'PREF=al=en-GB&f1=123; wide=1; SID=123')
assert.deepEqual(getCookies(headers), {
PREF: 'al=en-GB&f1=123',
wide: '1',
SID: '123'
})
})
test('Cookie Name Validation', () => {
const tokens = [
'"id"',
'id\t',
'i\td',
'i d',
'i;d',
'{id}',
'[id]',
'"',
'id\u0091'
]
const headers = new Headers()
tokens.forEach((name) => {
assert.throws(
() => {
setCookie(headers, {
name,
value: 'Cat',
httpOnly: true,
secure: true,
maxAge: 3
})
},
new Error('Invalid cookie name')
)
})
})
test('Cookie Value Validation', () => {
const tokens = [
'1f\tWa',
'\t',
'1f Wa',
'1f;Wa',
'"1fWa',
'1f\\Wa',
'1f"Wa',
'"',
'1fWa\u0005',
'1f\u0091Wa'
]
const headers = new Headers()
tokens.forEach((value) => {
assert.throws(
() => {
setCookie(
headers,
{
name: 'Space',
value,
httpOnly: true,
secure: true,
maxAge: 3
}
)
},
new Error('Invalid cookie value'),
"RFC2616 cookie 'Space'"
)
})
assert.throws(
() => {
setCookie(headers, {
name: 'location',
value: 'United Kingdom'
})
},
new Error('Invalid cookie value'),
"RFC2616 cookie 'location' cannot contain character ' '"
)
})
test('Cookie Path Validation', () => {
const path = '/;domain=sub.domain.com'
const headers = new Headers()
assert.throws(
() => {
setCookie(headers, {
name: 'Space',
value: 'Cat',
httpOnly: true,
secure: true,
path,
maxAge: 3
})
},
new Error('Invalid cookie path'),
path + ": Invalid cookie path char ';'"
)
})
test('Cookie Domain Validation', () => {
const tokens = ['-domain.com', 'domain.org.', 'domain.org-']
const headers = new Headers()
tokens.forEach((domain) => {
assert.throws(
() => {
setCookie(headers, {
name: 'Space',
value: 'Cat',
httpOnly: true,
secure: true,
domain,
maxAge: 3
})
},
new Error('Invalid cookie domain'),
'Invalid first/last char in cookie domain: ' + domain
)
})
})
test('Cookie Delete', () => {
let headers = new Headers()
deleteCookie(headers, 'deno')
assert.equal(
headers.get('Set-Cookie'),
'deno=; Expires=Thu, 01 Jan 1970 00:00:00 GMT'
)
headers = new Headers()
setCookie(headers, {
name: 'Space',
value: 'Cat',
domain: 'deno.land',
path: '/'
})
deleteCookie(headers, 'Space', { domain: '', path: '' })
assert.equal(
headers.get('Set-Cookie'),
'Space=Cat; Domain=deno.land; Path=/, Space=; Expires=Thu, 01 Jan 1970 00:00:00 GMT'
)
})
test('Cookie Set', () => {
let headers = new Headers()
setCookie(headers, { name: 'Space', value: 'Cat' })
assert.equal(headers.get('Set-Cookie'), 'Space=Cat')
headers = new Headers()
setCookie(headers, { name: 'Space', value: 'Cat', secure: true })
assert.equal(headers.get('Set-Cookie'), 'Space=Cat; Secure')
headers = new Headers()
setCookie(headers, { name: 'Space', value: 'Cat', httpOnly: true })
assert.equal(headers.get('Set-Cookie'), 'Space=Cat; HttpOnly')
headers = new Headers()
setCookie(headers, {
name: 'Space',
value: 'Cat',
httpOnly: true,
secure: true
})
assert.equal(headers.get('Set-Cookie'), 'Space=Cat; Secure; HttpOnly')
headers = new Headers()
setCookie(headers, {
name: 'Space',
value: 'Cat',
httpOnly: true,
secure: true,
maxAge: 2
})
assert.equal(
headers.get('Set-Cookie'),
'Space=Cat; Secure; HttpOnly; Max-Age=2'
)
headers = new Headers()
setCookie(headers, {
name: 'Space',
value: 'Cat',
httpOnly: true,
secure: true,
maxAge: 0
})
assert.equal(
headers.get('Set-Cookie'),
'Space=Cat; Secure; HttpOnly; Max-Age=0'
)
let error = false
headers = new Headers()
try {
setCookie(headers, {
name: 'Space',
value: 'Cat',
httpOnly: true,
secure: true,
maxAge: -1
})
} catch {
error = true
}
assert.ok(error)
headers = new Headers()
setCookie(headers, {
name: 'Space',
value: 'Cat',
httpOnly: true,
secure: true,
maxAge: 2,
domain: 'deno.land'
})
assert.equal(
headers.get('Set-Cookie'),
'Space=Cat; Secure; HttpOnly; Max-Age=2; Domain=deno.land'
)
headers = new Headers()
setCookie(headers, {
name: 'Space',
value: 'Cat',
httpOnly: true,
secure: true,
maxAge: 2,
domain: 'deno.land',
sameSite: 'Strict'
})
assert.equal(
headers.get('Set-Cookie'),
'Space=Cat; Secure; HttpOnly; Max-Age=2; Domain=deno.land; ' +
'SameSite=Strict'
)
headers = new Headers()
setCookie(headers, {
name: 'Space',
value: 'Cat',
httpOnly: true,
secure: true,
maxAge: 2,
domain: 'deno.land',
sameSite: 'Lax'
})
assert.equal(
headers.get('Set-Cookie'),
'Space=Cat; Secure; HttpOnly; Max-Age=2; Domain=deno.land; SameSite=Lax'
)
headers = new Headers()
setCookie(headers, {
name: 'Space',
value: 'Cat',
httpOnly: true,
secure: true,
maxAge: 2,
domain: 'deno.land',
path: '/'
})
assert.equal(
headers.get('Set-Cookie'),
'Space=Cat; Secure; HttpOnly; Max-Age=2; Domain=deno.land; Path=/'
)
headers = new Headers()
setCookie(headers, {
name: 'Space',
value: 'Cat',
httpOnly: true,
secure: true,
maxAge: 2,
domain: 'deno.land',
path: '/',
unparsed: ['unparsed=keyvalue', 'batman=Bruce']
})
assert.equal(
headers.get('Set-Cookie'),
'Space=Cat; Secure; HttpOnly; Max-Age=2; Domain=deno.land; Path=/; ' +
'unparsed=keyvalue; batman=Bruce'
)
headers = new Headers()
setCookie(headers, {
name: 'Space',
value: 'Cat',
httpOnly: true,
secure: true,
maxAge: 2,
domain: 'deno.land',
path: '/',
expires: new Date(Date.UTC(1983, 0, 7, 15, 32))
})
assert.equal(
headers.get('Set-Cookie'),
'Space=Cat; Secure; HttpOnly; Max-Age=2; Domain=deno.land; Path=/; ' +
'Expires=Fri, 07 Jan 1983 15:32:00 GMT'
)
headers = new Headers()
setCookie(headers, {
name: 'Space',
value: 'Cat',
expires: Date.UTC(1983, 0, 7, 15, 32)
})
assert.equal(
headers.get('Set-Cookie'),
'Space=Cat; Expires=Fri, 07 Jan 1983 15:32:00 GMT'
)
headers = new Headers()
setCookie(headers, { name: '__Secure-Kitty', value: 'Meow' })
assert.equal(headers.get('Set-Cookie'), '__Secure-Kitty=Meow; Secure')
headers = new Headers()
setCookie(headers, {
name: '__Host-Kitty',
value: 'Meow',
domain: 'deno.land'
})
assert.equal(
headers.get('Set-Cookie'),
'__Host-Kitty=Meow; Secure; Path=/'
)
headers = new Headers()
setCookie(headers, { name: 'cookie-1', value: 'value-1', secure: true })
setCookie(headers, { name: 'cookie-2', value: 'value-2', maxAge: 3600 })
assert.equal(
headers.get('Set-Cookie'),
'cookie-1=value-1; Secure, cookie-2=value-2; Max-Age=3600'
)
headers = new Headers()
setCookie(headers, { name: '', value: '' })
assert.equal(headers.get('Set-Cookie'), null)
})
test('Set-Cookie parser', () => {
let headers = new Headers({ 'set-cookie': 'Space=Cat' })
assert.deepEqual(getSetCookies(headers), [{
name: 'Space',
value: 'Cat'
}])
headers = new Headers({ 'set-cookie': 'Space=Cat; Secure' })
assert.deepEqual(getSetCookies(headers), [{
name: 'Space',
value: 'Cat',
secure: true
}])
headers = new Headers({ 'set-cookie': 'Space=Cat; HttpOnly' })
assert.deepEqual(getSetCookies(headers), [{
name: 'Space',
value: 'Cat',
httpOnly: true
}])
headers = new Headers({ 'set-cookie': 'Space=Cat; Secure; HttpOnly' })
assert.deepEqual(getSetCookies(headers), [{
name: 'Space',
value: 'Cat',
secure: true,
httpOnly: true
}])
headers = new Headers({
'set-cookie': 'Space=Cat; Secure; HttpOnly; Max-Age=2'
})
assert.deepEqual(getSetCookies(headers), [{
name: 'Space',
value: 'Cat',
secure: true,
httpOnly: true,
maxAge: 2
}])
headers = new Headers({
'set-cookie': 'Space=Cat; Secure; HttpOnly; Max-Age=0'
})
assert.deepEqual(getSetCookies(headers), [{
name: 'Space',
value: 'Cat',
secure: true,
httpOnly: true,
maxAge: 0
}])
headers = new Headers({
'set-cookie': 'Space=Cat; Secure; HttpOnly; Max-Age=-1'
})
assert.deepEqual(getSetCookies(headers), [{
name: 'Space',
value: 'Cat',
secure: true,
httpOnly: true
}])
headers = new Headers({
'set-cookie': 'Space=Cat; Secure; HttpOnly; Max-Age=2; Domain=deno.land'
})
assert.deepEqual(getSetCookies(headers), [{
name: 'Space',
value: 'Cat',
secure: true,
httpOnly: true,
maxAge: 2,
domain: 'deno.land'
}])
headers = new Headers({
'set-cookie':
'Space=Cat; Secure; HttpOnly; Max-Age=2; Domain=deno.land; SameSite=Strict'
})
assert.deepEqual(getSetCookies(headers), [{
name: 'Space',
value: 'Cat',
secure: true,
httpOnly: true,
maxAge: 2,
domain: 'deno.land',
sameSite: 'Strict'
}])
headers = new Headers({
'set-cookie':
'Space=Cat; Secure; HttpOnly; Max-Age=2; Domain=deno.land; SameSite=Lax'
})
assert.deepEqual(getSetCookies(headers), [{
name: 'Space',
value: 'Cat',
secure: true,
httpOnly: true,
maxAge: 2,
domain: 'deno.land',
sameSite: 'Lax'
}])
headers = new Headers({
'set-cookie':
'Space=Cat; Secure; HttpOnly; Max-Age=2; Domain=deno.land; Path=/'
})
assert.deepEqual(getSetCookies(headers), [{
name: 'Space',
value: 'Cat',
secure: true,
httpOnly: true,
maxAge: 2,
domain: 'deno.land',
path: '/'
}])
headers = new Headers({
'set-cookie':
'Space=Cat; Secure; HttpOnly; Max-Age=2; Domain=deno.land; Path=/; unparsed=keyvalue; batman=Bruce'
})
assert.deepEqual(getSetCookies(headers), [{
name: 'Space',
value: 'Cat',
secure: true,
httpOnly: true,
maxAge: 2,
domain: 'deno.land',
path: '/',
unparsed: ['unparsed=keyvalue', 'batman=Bruce']
}])
headers = new Headers({
'set-cookie':
'Space=Cat; Secure; HttpOnly; Max-Age=2; Domain=deno.land; Path=/; ' +
'Expires=Fri, 07 Jan 1983 15:32:00 GMT'
})
assert.deepEqual(getSetCookies(headers), [{
name: 'Space',
value: 'Cat',
secure: true,
httpOnly: true,
maxAge: 2,
domain: 'deno.land',
path: '/',
expires: new Date(Date.UTC(1983, 0, 7, 15, 32))
}])
headers = new Headers({ 'set-cookie': '__Secure-Kitty=Meow; Secure' })
assert.deepEqual(getSetCookies(headers), [{
name: '__Secure-Kitty',
value: 'Meow',
secure: true
}])
headers = new Headers({ 'set-cookie': '__Secure-Kitty=Meow' })
assert.deepEqual(getSetCookies(headers), [{
name: '__Secure-Kitty',
value: 'Meow'
}])
headers = new Headers({
'set-cookie': '__Host-Kitty=Meow; Secure; Path=/'
})
assert.deepEqual(getSetCookies(headers), [{
name: '__Host-Kitty',
value: 'Meow',
secure: true,
path: '/'
}])
headers = new Headers({ 'set-cookie': '__Host-Kitty=Meow; Path=/' })
assert.deepEqual(getSetCookies(headers), [{
name: '__Host-Kitty',
value: 'Meow',
path: '/'
}])
headers = new Headers({
'set-cookie': '__Host-Kitty=Meow; Secure; Domain=deno.land; Path=/'
})
assert.deepEqual(getSetCookies(headers), [{
name: '__Host-Kitty',
value: 'Meow',
secure: true,
domain: 'deno.land',
path: '/'
}])
headers = new Headers({
'set-cookie': '__Host-Kitty=Meow; Secure; Path=/not-root'
})
assert.deepEqual(getSetCookies(headers), [{
name: '__Host-Kitty',
value: 'Meow',
secure: true,
path: '/not-root'
}])
headers = new Headers([
['set-cookie', 'cookie-1=value-1; Secure'],
['set-cookie', 'cookie-2=value-2; Max-Age=3600']
])
assert.deepEqual(getSetCookies(headers), [
{ name: 'cookie-1', value: 'value-1', secure: true },
{ name: 'cookie-2', value: 'value-2', maxAge: 3600 }
])
headers = new Headers()
assert.deepEqual(getSetCookies(headers), [])
})
test('Cookie setCookie throws if headers is not of type Headers', () => {
class Headers {
[Symbol.toStringTag] = 'CustomHeaders'
}
const headers = new Headers()
assert.throws(
() => {
setCookie(headers, {
name: 'key',
value: 'Cat',
httpOnly: true,
secure: true,
maxAge: 3
})
},
new TypeError('Illegal invocation')
)
})
test('Cookie setCookie does not throw if headers is an instance of undici owns Headers class', () => {
const headers = new Headers()
setCookie(headers, {
name: 'key',
value: 'Cat',
httpOnly: true,
secure: true,
maxAge: 3
})
})
test('Cookie setCookie does not throw if headers is an instance of the global Headers class', { skip: !globalThis.Headers }, () => {
const headers = new globalThis.Headers()
setCookie(headers, {
name: 'key',
value: 'Cat',
httpOnly: true,
secure: true,
maxAge: 3
})
})
test('Cookie getCookies throws if headers is not of type Headers', () => {
class Headers {
[Symbol.toStringTag] = 'CustomHeaders'
}
const headers = new Headers()
assert.throws(
() => {
getCookies(headers)
},
new TypeError('Illegal invocation')
)
})
test('Cookie getCookies does not throw if headers is an instance of undici owns Headers class', () => {
const headers = new Headers()
getCookies(headers)
})
test('Cookie getCookie does not throw if headers is an instance of the global Headers class', { skip: !globalThis.Headers }, () => {
const headers = new globalThis.Headers()
getCookies(headers)
})
test('Cookie getSetCookies throws if headers is not of type Headers', () => {
class Headers {
[Symbol.toStringTag] = 'CustomHeaders'
}
const headers = new Headers({ 'set-cookie': 'Space=Cat' })
assert.throws(
() => {
getSetCookies(headers)
},
new TypeError('Illegal invocation')
)
})
test('Cookie getSetCookies does not throw if headers is an instance of undici owns Headers class', () => {
const headers = new Headers({ 'set-cookie': 'Space=Cat' })
getSetCookies(headers)
})
test('Cookie setCookie does not throw if headers is an instance of the global Headers class', { skip: !globalThis.Headers }, () => {
const headers = new globalThis.Headers({ 'set-cookie': 'Space=Cat' })
getSetCookies(headers)
})
test('Cookie deleteCookie throws if headers is not of type Headers', () => {
class Headers {
[Symbol.toStringTag] = 'CustomHeaders'
}
const headers = new Headers()
assert.throws(
() => {
deleteCookie(headers, 'deno')
},
new TypeError('Illegal invocation')
)
})
test('Cookie deleteCookie does not throw if headers is an instance of undici owns Headers class', () => {
const headers = new Headers()
deleteCookie(headers, 'deno')
})
test('Cookie getCookie does not throw if headers is an instance of the global Headers class', { skip: !globalThis.Headers }, () => {
const headers = new globalThis.Headers()
deleteCookie(headers, 'deno')
})
================================================
FILE: test/cookie/global-headers.js
================================================
'use strict'
const { describe, test } = require('node:test')
const assert = require('node:assert')
const {
deleteCookie,
getCookies,
getSetCookies,
setCookie
} = require('../..')
describe('Using global Headers', async () => {
test('deleteCookies', { skip: !globalThis.Headers }, () => {
const headers = new globalThis.Headers()
assert.equal(headers.get('set-cookie'), null)
deleteCookie(headers, 'undici')
assert.equal(headers.get('set-cookie'), 'undici=; Expires=Thu, 01 Jan 1970 00:00:00 GMT')
})
test('getCookies', { skip: !globalThis.Headers }, () => {
const headers = new globalThis.Headers({
cookie: 'get=cookies; and=attributes'
})
assert.deepEqual(getCookies(headers), { get: 'cookies', and: 'attributes' })
})
test('getSetCookies', { skip: !globalThis.Headers }, () => {
const headers = new globalThis.Headers({
'set-cookie': 'undici=getSetCookies; Secure'
})
const supportsCookies = headers.getSetCookie()
if (!supportsCookies) {
assert.deepEqual(getSetCookies(headers), [])
} else {
assert.deepEqual(getSetCookies(headers), [
{
name: 'undici',
value: 'getSetCookies',
secure: true
}
])
}
})
test('setCookie', { skip: !globalThis.Headers }, () => {
const headers = new globalThis.Headers()
setCookie(headers, { name: 'undici', value: 'setCookie' })
assert.equal(headers.get('Set-Cookie'), 'undici=setCookie')
})
})
describe('Headers check is not too lax', { skip: !globalThis.Headers }, () => {
class Headers { }
Object.defineProperty(globalThis.Headers.prototype, Symbol.toStringTag, {
value: 'Headers',
configurable: true
})
assert.throws(() => getCookies(new Headers()), { code: 'ERR_INVALID_THIS' })
assert.throws(() => getSetCookies(new Headers()), { code: 'ERR_INVALID_THIS' })
assert.throws(() => setCookie(new Headers(), { name: 'a', value: 'b' }), { code: 'ERR_INVALID_THIS' })
assert.throws(() => deleteCookie(new Headers(), 'name'), { code: 'ERR_INVALID_THIS' })
})
================================================
FILE: test/cookie/is-ctl-excluding-htab.js
================================================
'use strict'
const { test, describe } = require('node:test')
const { strictEqual } = require('node:assert')
const {
isCTLExcludingHtab
} = require('../../lib/web/cookies/util')
describe('isCTLExcludingHtab', () => {
test('should return false for 0x00 - 0x08 characters', () => {
strictEqual(isCTLExcludingHtab('\x00'), true)
strictEqual(isCTLExcludingHtab('\x01'), true)
strictEqual(isCTLExcludingHtab('\x02'), true)
strictEqual(isCTLExcludingHtab('\x03'), true)
strictEqual(isCTLExcludingHtab('\x04'), true)
strictEqual(isCTLExcludingHtab('\x05'), true)
strictEqual(isCTLExcludingHtab('\x06'), true)
strictEqual(isCTLExcludingHtab('\x07'), true)
strictEqual(isCTLExcludingHtab('\x08'), true)
})
test('should return false for 0x09 HTAB character', () => {
strictEqual(isCTLExcludingHtab('\x09'), false)
})
test('should return false for 0x0A - 0x1F characters', () => {
strictEqual(isCTLExcludingHtab('\x0A'), true)
strictEqual(isCTLExcludingHtab('\x0B'), true)
strictEqual(isCTLExcludingHtab('\x0C'), true)
strictEqual(isCTLExcludingHtab('\x0D'), true)
strictEqual(isCTLExcludingHtab('\x0E'), true)
strictEqual(isCTLExcludingHtab('\x0F'), true)
strictEqual(isCTLExcludingHtab('\x10'), true)
strictEqual(isCTLExcludingHtab('\x11'), true)
strictEqual(isCTLExcludingHtab('\x12'), true)
strictEqual(isCTLExcludingHtab('\x13'), true)
strictEqual(isCTLExcludingHtab('\x14'), true)
strictEqual(isCTLExcludingHtab('\x15'), true)
strictEqual(isCTLExcludingHtab('\x16'), true)
strictEqual(isCTLExcludingHtab('\x17'), true)
strictEqual(isCTLExcludingHtab('\x18'), true)
strictEqual(isCTLExcludingHtab('\x19'), true)
strictEqual(isCTLExcludingHtab('\x1A'), true)
strictEqual(isCTLExcludingHtab('\x1B'), true)
strictEqual(isCTLExcludingHtab('\x1C'), true)
strictEqual(isCTLExcludingHtab('\x1D'), true)
strictEqual(isCTLExcludingHtab('\x1E'), true)
strictEqual(isCTLExcludingHtab('\x1F'), true)
})
test('should return false for a 0x7F character', t => {
strictEqual(isCTLExcludingHtab('\x7F'), true)
})
test('should return false for a 0x20 / space character', t => {
strictEqual(isCTLExcludingHtab(' '), false)
})
test('should return false for a printable character', t => {
strictEqual(isCTLExcludingHtab('A'), false)
strictEqual(isCTLExcludingHtab('Z'), false)
strictEqual(isCTLExcludingHtab('a'), false)
strictEqual(isCTLExcludingHtab('z'), false)
strictEqual(isCTLExcludingHtab('!'), false)
})
test('should return false for an empty string', () => {
strictEqual(isCTLExcludingHtab(''), false)
})
test('all printable characters (0x20 - 0x7E)', () => {
for (let i = 0x20; i < 0x7F; i++) {
strictEqual(isCTLExcludingHtab(String.fromCharCode(i)), false)
}
})
test('valid case', () => {
strictEqual(isCTLExcludingHtab('Space=Cat; Secure; HttpOnly; Max-Age=2'), false)
})
test('invalid case', () => {
strictEqual(isCTLExcludingHtab('Space=Cat; Secure; HttpOnly; Max-Age=2\x7F'), true)
})
})
================================================
FILE: test/cookie/npm-cookie.js
================================================
'use strict'
// (The MIT License)
//
// Copyright (c) 2012-2014 Roman Shtylman
// Copyright (c) 2015 Douglas Christopher Wilson
//
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// 'Software'), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
// IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
// CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
// TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
// SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
const { describe, it } = require('node:test')
const assert = require('node:assert')
const { parseCookie } = require('../..')
describe('parseCookie(str)', function () {
it('should parse cookie string to object', function () {
assert.deepStrictEqual(parseCookie('foo=bar'), { name: 'foo', value: 'bar' })
assert.deepStrictEqual(parseCookie('foo=123'), { name: 'foo', value: '123' })
})
it('should ignore OWS', function () {
assert.deepStrictEqual(parseCookie('FOO = bar; baz = raz'), {
name: 'FOO',
value: 'bar',
unparsed: ['baz=raz']
})
})
it('should parse cookie with empty value', function () {
assert.deepStrictEqual(parseCookie('foo=; bar='), { name: 'foo', value: '', unparsed: ['bar='] })
})
it('should parse cookie with minimum length', function () {
assert.deepStrictEqual(parseCookie('f='), { name: 'f', value: '' })
assert.deepStrictEqual(parseCookie('f=;b='), { name: 'f', value: '', unparsed: ['b='] })
})
it('should URL-decode values', function () {
assert.deepStrictEqual(parseCookie('foo="bar=123456789&name=Magic+Mouse"'), {
name: 'foo',
value: '"bar=123456789&name=Magic+Mouse"'
})
assert.deepStrictEqual(parseCookie('email=%20%22%2c%3b%2f'), { name: 'email', value: ' ",;/' })
})
it('should trim whitespace around key and value', function () {
assert.deepStrictEqual(parseCookie(' foo = "bar" '), { name: 'foo', value: '"bar"' })
assert.deepStrictEqual(parseCookie(' foo = bar ; fizz = buzz '), {
name: 'foo',
value: 'bar',
unparsed: ['fizz=buzz']
})
assert.deepStrictEqual(parseCookie(' foo = " a b c " '), { name: 'foo', value: '" a b c "' })
assert.deepStrictEqual(parseCookie(' = bar '), { name: '', value: 'bar' })
assert.deepStrictEqual(parseCookie(' foo = '), { name: 'foo', value: '' })
assert.deepStrictEqual(parseCookie(' = '), { name: '', value: '' })
assert.deepStrictEqual(parseCookie('\tfoo\t=\tbar\t'), { name: 'foo', value: 'bar' })
})
it('should return original value on escape error', function () {
assert.deepStrictEqual(parseCookie('foo=%1;bar=bar'), { name: 'foo', value: '%1', unparsed: ['bar=bar'] })
})
it('should ignore cookies without value', function () {
assert.deepStrictEqual(parseCookie('foo=bar;fizz ; buzz'), { name: 'foo', value: 'bar', unparsed: ['fizz=', 'buzz='] })
assert.deepStrictEqual(parseCookie(' fizz; foo= bar'), { name: '', value: 'fizz', unparsed: ['foo=bar'] })
})
it('should ignore duplicate cookies', function () {
assert.deepStrictEqual(parseCookie('foo=%1;bar=bar;foo=boo'), {
name: 'foo',
value: '%1',
unparsed: ['bar=bar', 'foo=boo']
})
assert.deepStrictEqual(parseCookie('foo=false;bar=bar;foo=true'), {
name: 'foo',
value: 'false',
unparsed: ['bar=bar', 'foo=true']
})
assert.deepStrictEqual(parseCookie('foo=;bar=bar;foo=boo'), {
name: 'foo',
value: '',
unparsed: ['bar=bar', 'foo=boo']
})
})
it('should parse native properties', function () {
assert.deepStrictEqual(parseCookie('toString=foo;valueOf=bar'), {
name: 'toString',
unparsed: [
'valueOf=bar'
],
value: 'foo'
})
})
})
================================================
FILE: test/cookie/to-imf-date.js
================================================
'use strict'
const { test, describe } = require('node:test')
const { strictEqual } = require('node:assert')
const {
toIMFDate
} = require('../../lib/web/cookies/util')
describe('toIMFDate', () => {
test('should return the same as Date.prototype.toGMTString()', () => {
for (let i = 1; i <= 1e6; i *= 2) {
const date = new Date(i)
strictEqual(toIMFDate(date), date.toGMTString())
}
for (let i = 0; i <= 1e6; i++) {
const date = new Date(Math.trunc(Math.random() * 8640000000000000))
strictEqual(toIMFDate(date), date.toGMTString())
}
})
})
================================================
FILE: test/cookie/validate-cookie-name.js
================================================
'use strict'
const { test, describe } = require('node:test')
const { throws, strictEqual } = require('node:assert')
const {
validateCookieName
} = require('../../lib/web/cookies/util')
describe('validateCookieName', () => {
test('should throw for CTLs', () => {
throws(() => validateCookieName('\x00'), new Error('Invalid cookie name'))
throws(() => validateCookieName('\x01'), new Error('Invalid cookie name'))
throws(() => validateCookieName('\x02'), new Error('Invalid cookie name'))
throws(() => validateCookieName('\x03'), new Error('Invalid cookie name'))
throws(() => validateCookieName('\x04'), new Error('Invalid cookie name'))
throws(() => validateCookieName('\x05'), new Error('Invalid cookie name'))
throws(() => validateCookieName('\x06'), new Error('Invalid cookie name'))
throws(() => validateCookieName('\x07'), new Error('Invalid cookie name'))
throws(() => validateCookieName('\x08'), new Error('Invalid cookie name'))
throws(() => validateCookieName('\x09'), new Error('Invalid cookie name'))
throws(() => validateCookieName('\x0A'), new Error('Invalid cookie name'))
throws(() => validateCookieName('\x0B'), new Error('Invalid cookie name'))
throws(() => validateCookieName('\x0C'), new Error('Invalid cookie name'))
throws(() => validateCookieName('\x0D'), new Error('Invalid cookie name'))
throws(() => validateCookieName('\x0E'), new Error('Invalid cookie name'))
throws(() => validateCookieName('\x0F'), new Error('Invalid cookie name'))
throws(() => validateCookieName('\x10'), new Error('Invalid cookie name'))
throws(() => validateCookieName('\x11'), new Error('Invalid cookie name'))
throws(() => validateCookieName('\x12'), new Error('Invalid cookie name'))
throws(() => validateCookieName('\x13'), new Error('Invalid cookie name'))
throws(() => validateCookieName('\x14'), new Error('Invalid cookie name'))
throws(() => validateCookieName('\x15'), new Error('Invalid cookie name'))
throws(() => validateCookieName('\x16'), new Error('Invalid cookie name'))
throws(() => validateCookieName('\x17'), new Error('Invalid cookie name'))
throws(() => validateCookieName('\x18'), new Error('Invalid cookie name'))
throws(() => validateCookieName('\x19'), new Error('Invalid cookie name'))
throws(() => validateCookieName('\x1A'), new Error('Invalid cookie name'))
throws(() => validateCookieName('\x1B'), new Error('Invalid cookie name'))
throws(() => validateCookieName('\x1C'), new Error('Invalid cookie name'))
throws(() => validateCookieName('\x1D'), new Error('Invalid cookie name'))
throws(() => validateCookieName('\x1E'), new Error('Invalid cookie name'))
throws(() => validateCookieName('\x1F'), new Error('Invalid cookie name'))
throws(() => validateCookieName('\x7F'), new Error('Invalid cookie name'))
})
test('should throw for " " character', () => {
throws(() => validateCookieName(' '), new Error('Invalid cookie name'))
})
test('should throw for Horizontal Tab character', () => {
throws(() => validateCookieName('\t'), new Error('Invalid cookie name'))
})
test('should throw for ; character', () => {
throws(() => validateCookieName(';'), new Error('Invalid cookie name'))
})
test('should throw for " character', () => {
throws(() => validateCookieName('"'), new Error('Invalid cookie name'))
})
test('should throw for , character', () => {
throws(() => validateCookieName(','), new Error('Invalid cookie name'))
})
test('should throw for \\ character', () => {
throws(() => validateCookieName('\\'), new Error('Invalid cookie name'))
})
test('should throw for ( character', () => {
throws(() => validateCookieName('('), new Error('Invalid cookie name'))
})
test('should throw for ) character', () => {
throws(() => validateCookieName(')'), new Error('Invalid cookie name'))
})
test('should throw for < character', () => {
throws(() => validateCookieName('<'), new Error('Invalid cookie name'))
})
test('should throw for > character', () => {
throws(() => validateCookieName('>'), new Error('Invalid cookie name'))
})
test('should throw for @ character', () => {
throws(() => validateCookieName('@'), new Error('Invalid cookie name'))
})
test('should throw for : character', () => {
throws(() => validateCookieName(':'), new Error('Invalid cookie name'))
})
test('should throw for / character', () => {
throws(() => validateCookieName('/'), new Error('Invalid cookie name'))
})
test('should throw for [ character', () => {
throws(() => validateCookieName('['), new Error('Invalid cookie name'))
})
test('should throw for ] character', () => {
throws(() => validateCookieName(']'), new Error('Invalid cookie name'))
})
test('should throw for ? character', () => {
throws(() => validateCookieName('?'), new Error('Invalid cookie name'))
})
test('should throw for = character', () => {
throws(() => validateCookieName('='), new Error('Invalid cookie name'))
})
test('should throw for { character', () => {
throws(() => validateCookieName('{'), new Error('Invalid cookie name'))
})
test('should throw for } character', () => {
throws(() => validateCookieName('}'), new Error('Invalid cookie name'))
})
test('should pass for a printable character', t => {
strictEqual(validateCookieName('A'), undefined)
strictEqual(validateCookieName('Z'), undefined)
strictEqual(validateCookieName('a'), undefined)
strictEqual(validateCookieName('z'), undefined)
strictEqual(validateCookieName('!'), undefined)
})
})
================================================
FILE: test/cookie/validate-cookie-path.js
================================================
'use strict'
const { test, describe } = require('node:test')
const { throws, strictEqual } = require('node:assert')
const {
validateCookiePath
} = require('../../lib/web/cookies/util')
describe('validateCookiePath', () => {
test('should throw for CTLs', () => {
throws(() => validateCookiePath('\x00'))
throws(() => validateCookiePath('\x01'))
throws(() => validateCookiePath('\x02'))
throws(() => validateCookiePath('\x03'))
throws(() => validateCookiePath('\x04'))
throws(() => validateCookiePath('\x05'))
throws(() => validateCookiePath('\x06'))
throws(() => validateCookiePath('\x07'))
throws(() => validateCookiePath('\x08'))
throws(() => validateCookiePath('\x09'))
throws(() => validateCookiePath('\x0A'))
throws(() => validateCookiePath('\x0B'))
throws(() => validateCookiePath('\x0C'))
throws(() => validateCookiePath('\x0D'))
throws(() => validateCookiePath('\x0E'))
throws(() => validateCookiePath('\x0F'))
throws(() => validateCookiePath('\x10'))
throws(() => validateCookiePath('\x11'))
throws(() => validateCookiePath('\x12'))
throws(() => validateCookiePath('\x13'))
throws(() => validateCookiePath('\x14'))
throws(() => validateCookiePath('\x15'))
throws(() => validateCookiePath('\x16'))
throws(() => validateCookiePath('\x17'))
throws(() => validateCookiePath('\x18'))
throws(() => validateCookiePath('\x19'))
throws(() => validateCookiePath('\x1A'))
throws(() => validateCookiePath('\x1B'))
throws(() => validateCookiePath('\x1C'))
throws(() => validateCookiePath('\x1D'))
throws(() => validateCookiePath('\x1E'))
throws(() => validateCookiePath('\x1F'))
throws(() => validateCookiePath('\x7F'))
})
test('should throw for ; character', () => {
throws(() => validateCookiePath(';'))
})
test('should pass for a printable character', t => {
strictEqual(validateCookiePath('A'), undefined)
strictEqual(validateCookiePath('Z'), undefined)
strictEqual(validateCookiePath('a'), undefined)
strictEqual(validateCookiePath('z'), undefined)
strictEqual(validateCookiePath('!'), undefined)
strictEqual(validateCookiePath(' '), undefined)
})
})
================================================
FILE: test/cookie/validate-cookie-value.js
================================================
'use strict'
const { test, describe } = require('node:test')
const { throws, strictEqual } = require('node:assert')
const {
validateCookieValue
} = require('../../lib/web/cookies/util')
describe('validateCookieValue', () => {
test('should throw for CTLs', () => {
throws(() => validateCookieValue('\x00'), new Error('Invalid cookie value'))
throws(() => validateCookieValue('\x01'), new Error('Invalid cookie value'))
throws(() => validateCookieValue('\x02'), new Error('Invalid cookie value'))
throws(() => validateCookieValue('\x03'), new Error('Invalid cookie value'))
throws(() => validateCookieValue('\x04'), new Error('Invalid cookie value'))
throws(() => validateCookieValue('\x05'), new Error('Invalid cookie value'))
throws(() => validateCookieValue('\x06'), new Error('Invalid cookie value'))
throws(() => validateCookieValue('\x07'), new Error('Invalid cookie value'))
throws(() => validateCookieValue('\x08'), new Error('Invalid cookie value'))
throws(() => validateCookieValue('\x09'), new Error('Invalid cookie value'))
throws(() => validateCookieValue('\x0A'), new Error('Invalid cookie value'))
throws(() => validateCookieValue('\x0B'), new Error('Invalid cookie value'))
throws(() => validateCookieValue('\x0C'), new Error('Invalid cookie value'))
throws(() => validateCookieValue('\x0D'), new Error('Invalid cookie value'))
throws(() => validateCookieValue('\x0E'), new Error('Invalid cookie value'))
throws(() => validateCookieValue('\x0F'), new Error('Invalid cookie value'))
throws(() => validateCookieValue('\x10'), new Error('Invalid cookie value'))
throws(() => validateCookieValue('\x11'), new Error('Invalid cookie value'))
throws(() => validateCookieValue('\x12'), new Error('Invalid cookie value'))
throws(() => validateCookieValue('\x13'), new Error('Invalid cookie value'))
throws(() => validateCookieValue('\x14'), new Error('Invalid cookie value'))
throws(() => validateCookieValue('\x15'), new Error('Invalid cookie value'))
throws(() => validateCookieValue('\x16'), new Error('Invalid cookie value'))
throws(() => validateCookieValue('\x17'), new Error('Invalid cookie value'))
throws(() => validateCookieValue('\x18'), new Error('Invalid cookie value'))
throws(() => validateCookieValue('\x19'), new Error('Invalid cookie value'))
throws(() => validateCookieValue('\x1A'), new Error('Invalid cookie value'))
throws(() => validateCookieValue('\x1B'), new Error('Invalid cookie value'))
throws(() => validateCookieValue('\x1C'), new Error('Invalid cookie value'))
throws(() => validateCookieValue('\x1D'), new Error('Invalid cookie value'))
throws(() => validateCookieValue('\x1E'), new Error('Invalid cookie value'))
throws(() => validateCookieValue('\x1F'), new Error('Invalid cookie value'))
throws(() => validateCookieValue('\x7F'), new Error('Invalid cookie value'))
})
test('should throw for ; character', () => {
throws(() => validateCookieValue(';'), new Error('Invalid cookie value'))
})
test('should throw for " character', () => {
throws(() => validateCookieValue('"'), new Error('Invalid cookie value'))
})
test('should throw for , character', () => {
throws(() => validateCookieValue(','), new Error('Invalid cookie value'))
})
test('should throw for \\ character', () => {
throws(() => validateCookieValue('\\'), new Error('Invalid cookie value'))
})
test('should pass for a printable character', t => {
strictEqual(validateCookieValue('A'), undefined)
strictEqual(validateCookieValue('Z'), undefined)
strictEqual(validateCookieValue('a'), undefined)
strictEqual(validateCookieValue('z'), undefined)
strictEqual(validateCookieValue('!'), undefined)
strictEqual(validateCookieValue('='), undefined)
})
test('should handle strings wrapped in DQUOTE', t => {
strictEqual(validateCookieValue('""'), undefined)
strictEqual(validateCookieValue('"helloworld"'), undefined)
throws(() => validateCookieValue('"'), new Error('Invalid cookie value'))
throws(() => validateCookieValue('"""'), new Error('Invalid cookie value'))
})
})
================================================
FILE: test/decorator-handler.js
================================================
'use strict'
const { tspl } = require('@matteo.collina/tspl')
const { describe, test } = require('node:test')
const DecoratorHandler = require('../lib/handler/decorator-handler')
describe('DecoratorHandler', () => {
test('should throw if provided handler is not an object', t => {
t = tspl(t, { plan: 4 })
t.throws(
() => new DecoratorHandler(null),
new TypeError('handler must be an object')
)
t.throws(
() => new DecoratorHandler('string'),
new TypeError('handler must be an object')
)
t.throws(
() => new DecoratorHandler(null),
new TypeError('handler must be an object')
)
t.throws(
() => new DecoratorHandler('string'),
new TypeError('handler must be an object')
)
})
describe('wrap', () => {
const Handler = class {
#handler = null
constructor (handler) {
this.#handler = handler
}
onConnect (abort, context) {
return this.#handler?.onConnect?.(abort, context)
}
onHeaders (statusCode, rawHeaders, resume, statusMessage) {
return this.#handler?.onHeaders?.(statusCode, rawHeaders, resume, statusMessage)
}
onUpgrade (statusCode, rawHeaders, socket) {
return this.#handler?.onUpgrade?.(statusCode, rawHeaders, socket)
}
onData (data) {
return this.#handler?.onData?.(data)
}
onComplete (trailers) {
return this.#handler?.onComplete?.(trailers)
}
onError (err) {
return this.#handler?.onError?.(err)
}
}
const Controller = class {
#controller = null
constructor (controller) {
this.#controller = controller
}
abort (reason) {
return this.#controller?.abort?.(reason)
}
resume () {
return this.#controller?.resume?.()
}
pause () {
return this.#controller?.pause?.()
}
}
describe('#onConnect', () => {
test('should delegate onConnect-method', t => {
t = tspl(t, { plan: 2 })
const handler = new Handler(
{
onConnect: (abort, ctx) => {
t.equal(typeof abort, 'function')
t.equal(typeof ctx, 'object')
}
})
const decorator = new DecoratorHandler(handler)
decorator.onRequestStart(new Controller(), {})
})
test('should not throw if onConnect-method is not defined in the handler', t => {
t = tspl(t, { plan: 1 })
const decorator = new DecoratorHandler({})
t.doesNotThrow(() => decorator.onRequestStart())
})
})
describe('#onHeaders', () => {
test('should delegate onHeaders-method', t => {
t = tspl(t, { plan: 4 })
const handler = new Handler(
{
onHeaders: (statusCode, headers, resume, statusMessage) => {
t.equal(statusCode, '200')
t.equal(`${headers[0].toString('utf-8')}: ${headers[1].toString('utf-8')}`, 'content-type: application/json')
t.equal(typeof resume, 'function')
t.equal(statusMessage, 'OK')
}
})
const decorator = new DecoratorHandler(handler)
decorator.onResponseStart(new Controller(), 200, {
'content-type': 'application/json'
}, 'OK')
})
test('should not throw if onHeaders-method is not defined in the handler', t => {
t = tspl(t, { plan: 1 })
const decorator = new DecoratorHandler({})
t.doesNotThrow(() => decorator.onResponseStart(new Controller(), 200, {
'content-type': 'application/json'
}))
})
})
describe('#onUpgrade', () => {
test('should delegate onUpgrade-method', t => {
t = tspl(t, { plan: 3 })
const handler = new Handler(
{
onUpgrade: (statusCode, headers, socket) => {
t.equal(statusCode, 301)
t.equal(`${headers[0].toString('utf-8')}: ${headers[1].toString('utf-8')}`, 'content-type: application/json')
t.equal(typeof socket, 'object')
}
})
const decorator = new DecoratorHandler(handler)
decorator.onRequestUpgrade(new Controller(), 301, {
'content-type': 'application/json'
}, {})
})
test('should not throw if onUpgrade-method is not defined in the handler', t => {
t = tspl(t, { plan: 1 })
const decorator = new DecoratorHandler({})
t.doesNotThrow(() => decorator.onRequestUpgrade(new Controller(), 301, {
'content-type': 'application/json'
}))
})
})
describe('#onData', () => {
test('should delegate onData-method', t => {
t = tspl(t, { plan: 1 })
const handler = new Handler(
{
onData: (chunk) => {
t.equal('chunk', chunk)
}
})
const decorator = new DecoratorHandler(handler)
decorator.onResponseData(new Controller(), 'chunk')
})
test('should not throw if onData-method is not defined in the handler', t => {
t = tspl(t, { plan: 1 })
const decorator = new DecoratorHandler({})
t.doesNotThrow(() => decorator.onResponseData(new Controller(), 'chunk'))
})
})
describe('#onComplete', () => {
test('should delegate onComplete-method', t => {
t = tspl(t, { plan: 1 })
const handler = new Handler(
{
onComplete: (trailers) => {
t.equal(`${trailers[0].toString('utf-8')}: ${trailers[1].toString('utf-8')}`, 'x-trailer: trailer')
}
})
const decorator = new DecoratorHandler(handler)
decorator.onResponseEnd(new Controller(), { 'x-trailer': 'trailer' })
})
test('should not throw if onComplete-method is not defined in the handler', t => {
t = tspl(t, { plan: 1 })
const decorator = new DecoratorHandler({})
t.doesNotThrow(() => decorator.onResponseEnd(new Controller(), { 'x-trailer': 'trailer' }))
})
})
describe('#onError', () => {
test('should delegate onError-method', t => {
t = tspl(t, { plan: 1 })
const handler = new Handler(
{
onError: (err) => {
t.equal(err.message, 'Oops!')
}
})
const decorator = new DecoratorHandler(handler)
decorator.onResponseError(new Controller(), new Error('Oops!'))
})
test('should throw if onError-method is not defined in the handler', t => {
t = tspl(t, { plan: 1 })
const decorator = new DecoratorHandler({})
t.throws(() => decorator.onResponseError(new Controller(), new Error('Oops!')))
})
})
})
describe('no-wrap', () => {
const Handler = class {
#handler = null
constructor (handler) {
this.#handler = handler
}
onRequestStart (controller, context) {
return this.#handler?.onRequestStart?.(controller, context)
}
onRequestUpgrade (controller, statusCode, headers, socket) {
return this.#handler?.onRequestUpgrade?.(controller, statusCode, headers, socket)
}
onResponseStart (controller, statusCode, headers, statusMessage) {
return this.#handler?.onResponseStart?.(controller, statusCode, headers, statusMessage)
}
onResponseData (controller, data) {
return this.#handler?.onResponseData?.(controller, data)
}
onResponseEnd (controller, trailers) {
return this.#handler?.onResponseEnd?.(controller, trailers)
}
onResponseError (controller, err) {
return this.#handler?.onResponseError?.(controller, err)
}
}
const Controller = class {
#controller = null
constructor (controller) {
this.#controller = controller
}
abort (reason) {
return this.#controller?.abort?.(reason)
}
resume () {
return this.#controller?.resume?.()
}
pause () {
return this.#controller?.pause?.()
}
}
describe('#onRequestStart', () => {
test('should delegate onRequestStart-method', t => {
t = tspl(t, { plan: 2 })
const handler = new Handler(
{
onRequestStart: (controller, ctx) => {
t.equal(controller.constructor, Controller)
t.equal(typeof ctx, 'object')
}
})
const decorator = new DecoratorHandler(handler)
decorator.onRequestStart(new Controller(), {})
})
test('should not throw if onRequestStart-method is not defined in the handler', t => {
t = tspl(t, { plan: 1 })
const decorator = new DecoratorHandler({})
t.doesNotThrow(() => decorator.onRequestStart())
})
})
describe('#onRequestUpgrade', () => {
test('should delegate onRequestUpgrade-method', t => {
t = tspl(t, { plan: 4 })
const handler = new Handler(
{
onRequestUpgrade: (controller, statusCode, headers, socket) => {
t.equal(controller.constructor, Controller)
t.equal(statusCode, 301)
t.equal(headers['content-type'], 'application/json')
t.equal(typeof socket, 'object')
}
})
const decorator = new DecoratorHandler(handler)
decorator.onRequestUpgrade(new Controller(), 301, {
'content-type': 'application/json'
}, {})
})
test('should not throw if onRequestUpgrade-method is not defined in the handler', t => {
t = tspl(t, { plan: 1 })
const decorator = new DecoratorHandler({})
t.doesNotThrow(() => decorator.onRequestUpgrade(new Controller(), 301, {
'content-type': 'application/json'
}, {}))
})
})
describe('#onResponseStart', () => {
test('should delegate onResponseStart-method', t => {
t = tspl(t, { plan: 4 })
const handler = new Handler(
{
onResponseStart: (controller, statusCode, headers, message) => {
t.equal(controller.constructor, Controller)
t.equal(statusCode, 200)
t.equal(headers['content-type'], 'application/json')
t.equal(message, 'OK')
}
})
const decorator = new DecoratorHandler(handler)
decorator.onResponseStart(new Controller(), 200, {
'content-type': 'application/json'
}, 'OK')
})
test('should not throw if onResponseStart-method is not defined in the handler', t => {
t = tspl(t, { plan: 1 })
const decorator = new DecoratorHandler({})
t.doesNotThrow(() => decorator.onResponseStart(new Controller(), 200, {
'content-type': 'application/json'
}, 'OK'))
})
})
describe('#onResponseData', () => {
test('should delegate onResponseData-method', t => {
t = tspl(t, { plan: 2 })
const handler = new Handler(
{
onResponseData: (controller, chunk) => {
t.equal(controller.constructor, Controller)
t.equal('chunk', chunk)
}
})
const decorator = new DecoratorHandler(handler)
decorator.onResponseData(new Controller(), 'chunk')
})
test('should not throw if onResponseData-method is not defined in the handler', t => {
t = tspl(t, { plan: 1 })
const decorator = new DecoratorHandler({})
t.doesNotThrow(() => decorator.onResponseData(new Controller(), 'chunk'))
})
})
describe('#onResponseEnd', () => {
test('should delegate onResponseEnd-method', t => {
t = tspl(t, { plan: 2 })
const handler = new Handler(
{
onResponseEnd: (controller, trailers) => {
t.equal(controller.constructor, Controller)
t.equal(trailers['x-trailer'], 'trailer')
}
})
const decorator = new DecoratorHandler(handler)
decorator.onResponseEnd(new Controller(), { 'x-trailer': 'trailer' })
})
test('should not throw if onResponseEnd-method is not defined in the handler', t => {
t = tspl(t, { plan: 1 })
const decorator = new DecoratorHandler({})
t.doesNotThrow(() => decorator.onResponseEnd(new Controller(), { 'x-trailer': 'trailer' }))
})
})
describe('#onResponseError', () => {
test('should delegate onError-method', t => {
t = tspl(t, { plan: 2 })
const handler = new Handler(
{
onResponseError: (controller, err) => {
t.equal(controller.constructor, Controller)
t.equal(err.message, 'Oops!')
}
})
const decorator = new DecoratorHandler(handler)
decorator.onResponseError(new Controller(), new Error('Oops!'))
})
test('should throw if onError-method is not defined in the handler', t => {
t = tspl(t, { plan: 1 })
const decorator = new DecoratorHandler({
// To hin and not wrap the instance
onRequestStart: () => {}
})
t.doesNotThrow(() => decorator.onResponseError(new Controller()))
})
})
})
})
================================================
FILE: test/dispatcher.js
================================================
'use strict'
const { tspl } = require('@matteo.collina/tspl')
const { test } = require('node:test')
const Dispatcher = require('../lib/dispatcher/dispatcher')
class PoorImplementation extends Dispatcher {}
test('dispatcher implementation', (t) => {
t = tspl(t, { plan: 6 })
const dispatcher = new Dispatcher()
t.throws(() => dispatcher.dispatch(), Error, 'throws on unimplemented dispatch')
t.throws(() => dispatcher.close(), Error, 'throws on unimplemented close')
t.throws(() => dispatcher.destroy(), Error, 'throws on unimplemented destroy')
const poorImplementation = new PoorImplementation()
t.throws(() => poorImplementation.dispatch(), Error, 'throws on unimplemented dispatch')
t.throws(() => poorImplementation.close(), Error, 'throws on unimplemented close')
t.throws(() => poorImplementation.destroy(), Error, 'throws on unimplemented destroy')
})
test('dispatcher.compose', (t) => {
t = tspl(t, { plan: 7 })
const dispatcher = new Dispatcher()
const interceptor = () => (opts, handler) => {}
// Should return a new dispatcher
t.ok(dispatcher.compose(interceptor) !== dispatcher)
t.throws(() => dispatcher.dispatch({}), Error, 'invalid interceptor')
t.throws(() => dispatcher.dispatch(() => null), Error, 'invalid interceptor')
t.throws(() => dispatcher.dispatch(dispatch => dispatch, () => () => {}, Error, 'invalid interceptor'))
const composed = dispatcher.compose(interceptor)
t.equal(typeof composed.dispatch, 'function', 'returns an object with a dispatch method')
t.equal(typeof composed.close, 'function', 'returns an object with a close method')
t.equal(typeof composed.destroy, 'function', 'returns an object with a destroy method')
})
================================================
FILE: test/env-http-proxy-agent-nodejs-bundle.js
================================================
'use strict'
const { tspl } = require('@matteo.collina/tspl')
const { describe, test, after, before } = require('node:test')
const { EnvHttpProxyAgent, setGlobalDispatcher } = require('../index-fetch')
const http = require('node:http')
const net = require('node:net')
const { once } = require('node:events')
const env = { ...process.env }
describe('EnvHttpProxyAgent and setGlobalDispatcher', () => {
before(() => {
['HTTP_PROXY', 'http_proxy', 'HTTPS_PROXY', 'https_proxy', 'NO_PROXY', 'no_proxy'].forEach((varname) => {
delete process.env[varname]
})
})
after(() => {
process.env = { ...env }
})
test('should work with global fetch from undici bundled with Node.js', async (t) => {
const { strictEqual } = tspl(t, { plan: 3 })
// Instead of using mocks, start a real server and a minimal proxy server
// in order to exercise the actual paths in EnvHttpProxyAgent from the
// Node.js bundle.
const server = http.createServer({ joinDuplicateHeaders: true }, (req, res) => { res.end('Hello world') })
server.on('error', err => { console.log('Server error', err) })
server.listen(0)
await once(server, 'listening')
t.after(() => {
server.closeAllConnections?.()
server.close()
})
const proxy = http.createServer({ joinDuplicateHeaders: true })
proxy.on('connect', (req, clientSocket, head) => {
// Check that the proxy is actually used to tunnel the request sent below.
const [hostname, port] = req.url.split(':')
strictEqual(hostname, 'localhost')
strictEqual(port, server.address().port.toString())
const serverSocket = net.connect(port, hostname, () => {
clientSocket.write(
'HTTP/1.1 200 Connection Established\r\n' +
'Proxy-agent: Node.js-Proxy\r\n' +
'\r\n'
)
serverSocket.write(head)
clientSocket.pipe(serverSocket)
serverSocket.pipe(clientSocket)
})
serverSocket.on('error', () => {
clientSocket.write('HTTP/1.1 500 Connection Error\r\n\r\n')
clientSocket.end()
})
})
proxy.on('error', (err) => { console.log('Proxy error', err) })
proxy.listen(0)
await once(proxy, 'listening')
t.after(() => {
proxy.closeAllConnections?.()
proxy.close()
})
// Use setGlobalDispatcher and EnvHttpProxyAgent from Node.js
// and make sure that they work together.
const proxyAddress = `http://localhost:${proxy.address().port}`
const serverAddress = `http://localhost:${server.address().port}`
process.env.http_proxy = proxyAddress
setGlobalDispatcher(new EnvHttpProxyAgent())
// eslint-disable-next-line no-restricted-globals
const res = await fetch(serverAddress)
strictEqual(await res.text(), 'Hello world')
})
})
================================================
FILE: test/env-http-proxy-agent.js
================================================
'use strict'
const { tspl } = require('@matteo.collina/tspl')
const { test, describe, after, beforeEach } = require('node:test')
const { EnvHttpProxyAgent, ProxyAgent, Agent, fetch, MockAgent } = require('..')
const { kNoProxyAgent, kHttpProxyAgent, kHttpsProxyAgent, kClosed, kDestroyed, kProxy } = require('../lib/core/symbols')
const env = { ...process.env }
beforeEach(() => {
['HTTP_PROXY', 'http_proxy', 'HTTPS_PROXY', 'https_proxy', 'NO_PROXY', 'no_proxy'].forEach((varname) => {
delete process.env[varname]
})
})
after(() => {
process.env = { ...env }
})
test('does not create any proxy agents if http_proxy and https_proxy are not set', async (t) => {
t = tspl(t, { plan: 4 })
const dispatcher = new EnvHttpProxyAgent()
t.ok(dispatcher[kNoProxyAgent] instanceof Agent)
t.ok(!(dispatcher[kNoProxyAgent] instanceof ProxyAgent))
t.deepStrictEqual(dispatcher[kHttpProxyAgent], dispatcher[kNoProxyAgent])
t.deepStrictEqual(dispatcher[kHttpsProxyAgent], dispatcher[kNoProxyAgent])
return dispatcher.close()
})
test('creates one proxy agent for both http and https when only http_proxy is defined', async (t) => {
t = tspl(t, { plan: 5 })
process.env.http_proxy = 'http://example.com:8080'
const dispatcher = new EnvHttpProxyAgent()
t.ok(dispatcher[kNoProxyAgent] instanceof Agent)
t.ok(!(dispatcher[kNoProxyAgent] instanceof ProxyAgent))
t.ok(dispatcher[kHttpProxyAgent] instanceof ProxyAgent)
t.equal(dispatcher[kHttpProxyAgent][kProxy].uri, 'http://example.com:8080/')
t.deepStrictEqual(dispatcher[kHttpsProxyAgent], dispatcher[kHttpProxyAgent])
return dispatcher.close()
})
test('creates separate proxy agent for http and https when http_proxy and https_proxy are set', async (t) => {
t = tspl(t, { plan: 6 })
process.env.http_proxy = 'http://example.com:8080'
process.env.https_proxy = 'http://example.com:8443'
const dispatcher = new EnvHttpProxyAgent()
t.ok(dispatcher[kNoProxyAgent] instanceof Agent)
t.ok(!(dispatcher[kNoProxyAgent] instanceof ProxyAgent))
t.ok(dispatcher[kHttpProxyAgent] instanceof ProxyAgent)
t.equal(dispatcher[kHttpProxyAgent][kProxy].uri, 'http://example.com:8080/')
t.ok(dispatcher[kHttpsProxyAgent] instanceof ProxyAgent)
t.equal(dispatcher[kHttpsProxyAgent][kProxy].uri, 'http://example.com:8443/')
return dispatcher.close()
})
test('handles uppercase HTTP_PROXY and HTTPS_PROXY', async (t) => {
t = tspl(t, { plan: 6 })
process.env.HTTP_PROXY = 'http://example.com:8080'
process.env.HTTPS_PROXY = 'http://example.com:8443'
const dispatcher = new EnvHttpProxyAgent()
t.ok(dispatcher[kNoProxyAgent] instanceof Agent)
t.ok(!(dispatcher[kNoProxyAgent] instanceof ProxyAgent))
t.ok(dispatcher[kHttpProxyAgent] instanceof ProxyAgent)
t.equal(dispatcher[kHttpProxyAgent][kProxy].uri, 'http://example.com:8080/')
t.ok(dispatcher[kHttpsProxyAgent] instanceof ProxyAgent)
t.equal(dispatcher[kHttpsProxyAgent][kProxy].uri, 'http://example.com:8443/')
return dispatcher.close()
})
test('accepts httpProxy and httpsProxy options', async (t) => {
t = tspl(t, { plan: 6 })
const opts = {
httpProxy: 'http://example.com:8080',
httpsProxy: 'http://example.com:8443'
}
const dispatcher = new EnvHttpProxyAgent(opts)
t.ok(dispatcher[kNoProxyAgent] instanceof Agent)
t.ok(!(dispatcher[kNoProxyAgent] instanceof ProxyAgent))
t.ok(dispatcher[kHttpProxyAgent] instanceof ProxyAgent)
t.equal(dispatcher[kHttpProxyAgent][kProxy].uri, 'http://example.com:8080/')
t.ok(dispatcher[kHttpsProxyAgent] instanceof ProxyAgent)
t.equal(dispatcher[kHttpsProxyAgent][kProxy].uri, 'http://example.com:8443/')
return dispatcher.close()
})
test('prefers options over env vars', async (t) => {
t = tspl(t, { plan: 2 })
const opts = {
httpProxy: 'http://opts.example.com:8080',
httpsProxy: 'http://opts.example.com:8443'
}
process.env.http_proxy = 'http://lower.example.com:8080'
process.env.https_proxy = 'http://lower.example.com:8443'
process.env.HTTP_PROXY = 'http://upper.example.com:8080'
process.env.HTTPS_PROXY = 'http://upper.example.com:8443'
const dispatcher = new EnvHttpProxyAgent(opts)
t.equal(dispatcher[kHttpProxyAgent][kProxy].uri, 'http://opts.example.com:8080/')
t.equal(dispatcher[kHttpsProxyAgent][kProxy].uri, 'http://opts.example.com:8443/')
return dispatcher.close()
})
test('prefers lowercase over uppercase env vars', async (t) => {
t = tspl(t, { plan: 2 })
process.env.HTTP_PROXY = 'http://upper.example.com:8080'
process.env.HTTPS_PROXY = 'http://upper.example.com:8443'
process.env.http_proxy = 'http://lower.example.com:8080'
process.env.https_proxy = 'http://lower.example.com:8443'
const dispatcher = new EnvHttpProxyAgent()
t.equal(dispatcher[kHttpProxyAgent][kProxy].uri, 'http://lower.example.com:8080/')
t.equal(dispatcher[kHttpsProxyAgent][kProxy].uri, 'http://lower.example.com:8443/')
return dispatcher.close()
})
test('prefers lowercase over uppercase env vars even when empty', async (t) => {
t = tspl(t, { plan: 2 })
process.env.HTTP_PROXY = 'http://upper.example.com:8080'
process.env.HTTP_PROXY = 'http://upper.example.com:8443'
process.env.http_proxy = ''
process.env.https_proxy = ''
const dispatcher = new EnvHttpProxyAgent()
t.deepStrictEqual(dispatcher[kHttpProxyAgent], dispatcher[kNoProxyAgent])
t.deepStrictEqual(dispatcher[kHttpsProxyAgent], dispatcher[kNoProxyAgent])
return dispatcher.close()
})
test('creates a proxy agent only for https when only https_proxy is set', async (t) => {
t = tspl(t, { plan: 5 })
process.env.https_proxy = 'http://example.com:8443'
const dispatcher = new EnvHttpProxyAgent()
t.ok(dispatcher[kNoProxyAgent] instanceof Agent)
t.ok(!(dispatcher[kNoProxyAgent] instanceof ProxyAgent))
t.deepStrictEqual(dispatcher[kHttpProxyAgent], dispatcher[kNoProxyAgent])
t.ok(dispatcher[kHttpsProxyAgent] instanceof ProxyAgent)
t.equal(dispatcher[kHttpsProxyAgent][kProxy].uri, 'http://example.com:8443/')
return dispatcher.close()
})
test('closes all agents', async (t) => {
t = tspl(t, { plan: 3 })
process.env.http_proxy = 'http://example.com:8080'
process.env.https_proxy = 'http://example.com:8443'
const dispatcher = new EnvHttpProxyAgent()
await dispatcher.close()
t.ok(dispatcher[kNoProxyAgent][kClosed])
t.ok(dispatcher[kHttpProxyAgent][kClosed])
t.ok(dispatcher[kHttpsProxyAgent][kClosed])
})
test('destroys all agents', async (t) => {
t = tspl(t, { plan: 3 })
process.env.http_proxy = 'http://example.com:8080'
process.env.https_proxy = 'http://example.com:8443'
const dispatcher = new EnvHttpProxyAgent()
await dispatcher.destroy()
t.ok(dispatcher[kNoProxyAgent][kDestroyed])
t.ok(dispatcher[kHttpProxyAgent][kDestroyed])
t.ok(dispatcher[kHttpsProxyAgent][kDestroyed])
})
const createEnvHttpProxyAgentWithMocks = (plan = 1, opts = {}) => {
const factory = (origin) => {
const mockAgent = new MockAgent()
const mockPool = mockAgent.get(origin)
let i = 0
while (i < plan) {
mockPool.intercept({ path: /.*/ }).reply(200, 'OK')
i++
}
return mockPool
}
process.env.http_proxy = 'http://localhost:8080'
process.env.https_proxy = 'http://localhost:8443'
const dispatcher = new EnvHttpProxyAgent({ ...opts, factory })
const agentSymbols = [kNoProxyAgent, kHttpProxyAgent, kHttpsProxyAgent]
agentSymbols.forEach((agentSymbol) => {
const originalDispatch = dispatcher[agentSymbol].dispatch
dispatcher[agentSymbol].dispatch = function () {
dispatcher[agentSymbol].dispatch.called = true
return originalDispatch.apply(this, arguments)
}
dispatcher[agentSymbol].dispatch.called = false
})
const usesProxyAgent = async (agent, url) => {
await fetch(url, { dispatcher })
const result = agentSymbols.every((agentSymbol) => agent === agentSymbol
? dispatcher[agentSymbol].dispatch.called === true
: dispatcher[agentSymbol].dispatch.called === false)
agentSymbols.forEach((agentSymbol) => {
dispatcher[agentSymbol].dispatch.called = false
})
return result
}
const doesNotProxy = usesProxyAgent.bind(this, kNoProxyAgent)
return {
dispatcher,
doesNotProxy,
usesProxyAgent
}
}
test('uses the appropriate proxy for the protocol', async (t) => {
t = tspl(t, { plan: 2 })
const { dispatcher, usesProxyAgent } = createEnvHttpProxyAgentWithMocks()
t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://example.com/'))
t.ok(await usesProxyAgent(kHttpsProxyAgent, 'https://example.com/'))
return dispatcher.close()
})
describe('no_proxy', () => {
test('set to *', async (t) => {
t = tspl(t, { plan: 2 })
process.env.no_proxy = '*'
const { dispatcher, doesNotProxy } = createEnvHttpProxyAgentWithMocks(2)
t.ok(await doesNotProxy('https://example.com'))
t.ok(await doesNotProxy('http://example.com'))
return dispatcher.close()
})
test('set but empty', async (t) => {
t = tspl(t, { plan: 1 })
process.env.no_proxy = ''
const { dispatcher, usesProxyAgent } = createEnvHttpProxyAgentWithMocks()
t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://example.com'))
return dispatcher.close()
})
test('no entries (comma)', async (t) => {
t = tspl(t, { plan: 1 })
process.env.no_proxy = ','
const { dispatcher, usesProxyAgent } = createEnvHttpProxyAgentWithMocks()
t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://example.com'))
return dispatcher.close()
})
test('no entries (whitespace)', async (t) => {
t = tspl(t, { plan: 1 })
process.env.no_proxy = ' '
const { dispatcher, usesProxyAgent } = createEnvHttpProxyAgentWithMocks()
t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://example.com'))
return dispatcher.close()
})
test('no entries (multiple whitespace / commas)', async (t) => {
t = tspl(t, { plan: 1 })
process.env.no_proxy = ',\t,,,\n, ,\r'
const { dispatcher, usesProxyAgent } = createEnvHttpProxyAgentWithMocks()
t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://example.com'))
return dispatcher.close()
})
test('single host', async (t) => {
t = tspl(t, { plan: 9 })
process.env.no_proxy = 'example'
const { dispatcher, doesNotProxy, usesProxyAgent } = createEnvHttpProxyAgentWithMocks(9)
t.ok(await doesNotProxy('http://example'))
t.ok(await doesNotProxy('http://example:80'))
t.ok(await doesNotProxy('http://example:0'))
t.ok(await doesNotProxy('http://example:1337'))
t.ok(await doesNotProxy('http://sub.example'))
t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://prefexample'))
t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://example.no'))
t.ok(await doesNotProxy('http://a.b.example'))
t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://host/example'))
return dispatcher.close()
})
test('as an option', async (t) => {
t = tspl(t, { plan: 9 })
const { dispatcher, doesNotProxy, usesProxyAgent } = createEnvHttpProxyAgentWithMocks(9, { noProxy: 'example' })
t.ok(await doesNotProxy('http://example'))
t.ok(await doesNotProxy('http://example:80'))
t.ok(await doesNotProxy('http://example:0'))
t.ok(await doesNotProxy('http://example:1337'))
t.ok(await doesNotProxy('http://sub.example'))
t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://prefexample'))
t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://example.no'))
t.ok(await doesNotProxy('http://a.b.example'))
t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://host/example'))
return dispatcher.close()
})
test('subdomain', async (t) => {
t = tspl(t, { plan: 8 })
process.env.no_proxy = 'sub.example'
const { dispatcher, doesNotProxy, usesProxyAgent } = createEnvHttpProxyAgentWithMocks(8)
t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://example'))
t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://example:80'))
t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://example:0'))
t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://example:1337'))
t.ok(await doesNotProxy('http://sub.example'))
t.ok(await doesNotProxy('http://no.sub.example'))
t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://sub-example'))
t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://example.sub'))
return dispatcher.close()
})
test('host + port', async (t) => {
t = tspl(t, { plan: 13 })
process.env.no_proxy = 'example:80, localhost:3000'
const { dispatcher, doesNotProxy, usesProxyAgent } = createEnvHttpProxyAgentWithMocks(13)
t.ok(await doesNotProxy('http://example'))
t.ok(await doesNotProxy('http://example:80'))
t.ok(await doesNotProxy('http://sub.example:80'))
t.ok(await doesNotProxy('http://example:0'))
t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://example:1337'))
t.ok(await doesNotProxy('http://sub.example'))
t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://prefexample'))
t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://example.no'))
t.ok(await doesNotProxy('http://a.b.example'))
t.ok(await doesNotProxy('http://localhost:3000/'))
t.ok(await doesNotProxy('https://localhost:3000/'))
t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://localhost:3001/'))
t.ok(await usesProxyAgent(kHttpsProxyAgent, 'https://localhost:3001/'))
return dispatcher.close()
})
test('host suffix - leading dot stripped', async (t) => {
t = tspl(t, { plan: 9 })
process.env.no_proxy = '.example'
const { dispatcher, doesNotProxy, usesProxyAgent } = createEnvHttpProxyAgentWithMocks(9)
t.ok(await doesNotProxy('http://example'))
t.ok(await doesNotProxy('http://example:80'))
t.ok(await doesNotProxy('http://example:1337'))
t.ok(await doesNotProxy('http://sub.example'))
t.ok(await doesNotProxy('http://sub.example:80'))
t.ok(await doesNotProxy('http://sub.example:1337'))
t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://prefexample'))
t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://example.no'))
t.ok(await doesNotProxy('http://a.b.example'))
return dispatcher.close()
})
test('host suffix with *. - leading dot with asterisk stripped', async (t) => {
t = tspl(t, { plan: 9 })
process.env.no_proxy = '*.example'
const { dispatcher, doesNotProxy, usesProxyAgent } = createEnvHttpProxyAgentWithMocks(9)
t.ok(await doesNotProxy('http://example'))
t.ok(await doesNotProxy('http://example:80'))
t.ok(await doesNotProxy('http://example:1337'))
t.ok(await doesNotProxy('http://sub.example'))
t.ok(await doesNotProxy('http://sub.example:80'))
t.ok(await doesNotProxy('http://sub.example:1337'))
t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://prefexample'))
t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://example.no'))
t.ok(await doesNotProxy('http://a.b.example'))
return dispatcher.close()
})
test('substring suffix are NOT supported', async (t) => {
t = tspl(t, { plan: 6 })
process.env.no_proxy = '*example'
const { dispatcher, usesProxyAgent } = createEnvHttpProxyAgentWithMocks(6)
t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://example'))
t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://sub.example'))
t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://sub.example'))
t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://prefexample'))
t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://x.prefexample'))
t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://a.b.example'))
return dispatcher.close()
})
test('arbitrary wildcards are NOT supported', async (t) => {
t = tspl(t, { plan: 6 })
process.env.no_proxy = '.*example'
const { dispatcher, usesProxyAgent } = createEnvHttpProxyAgentWithMocks(6)
t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://example'))
t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://sub.example'))
t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://sub.example'))
t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://prefexample'))
t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://x.prefexample'))
t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://a.b.example'))
return dispatcher.close()
})
test('IP addresses', async (t) => {
t = tspl(t, { plan: 12 })
process.env.no_proxy = '[::1],[::2]:80,10.0.0.1,10.0.0.2:80'
const { dispatcher, doesNotProxy, usesProxyAgent } = createEnvHttpProxyAgentWithMocks(12)
t.ok(await doesNotProxy('http://[::1]/'))
t.ok(await doesNotProxy('http://[::1]:80/'))
t.ok(await doesNotProxy('http://[::1]:1337/'))
t.ok(await doesNotProxy('http://[::2]/'))
t.ok(await doesNotProxy('http://[::2]:80/'))
t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://[::2]:1337/'))
t.ok(await doesNotProxy('http://10.0.0.1/'))
t.ok(await doesNotProxy('http://10.0.0.1:80/'))
t.ok(await doesNotProxy('http://10.0.0.1:1337/'))
t.ok(await doesNotProxy('http://10.0.0.2/'))
t.ok(await doesNotProxy('http://10.0.0.2:80/'))
t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://10.0.0.2:1337/'))
return dispatcher.close()
})
test('CIDR is NOT supported', async (t) => {
t = tspl(t, { plan: 2 })
process.env.no_proxy = '127.0.0.1/32'
const { dispatcher, usesProxyAgent } = createEnvHttpProxyAgentWithMocks(2)
t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://127.0.0.1'))
t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://127.0.0.1/32'))
return dispatcher.close()
})
test('127.0.0.1 does NOT match localhost', async (t) => {
t = tspl(t, { plan: 2 })
process.env.no_proxy = '127.0.0.1'
const { dispatcher, doesNotProxy, usesProxyAgent } = createEnvHttpProxyAgentWithMocks(2)
t.ok(await doesNotProxy('http://127.0.0.1'))
t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://localhost'))
return dispatcher.close()
})
test('protocols that have a default port', async (t) => {
t = tspl(t, { plan: 6 })
process.env.no_proxy = 'xxx:21,xxx:70,xxx:80,xxx:443'
const { dispatcher, doesNotProxy, usesProxyAgent } = createEnvHttpProxyAgentWithMocks(6)
t.ok(await doesNotProxy('http://xxx'))
t.ok(await doesNotProxy('http://xxx:80'))
t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://xxx:1337'))
t.ok(await doesNotProxy('https://xxx'))
t.ok(await doesNotProxy('https://xxx:443'))
t.ok(await usesProxyAgent(kHttpsProxyAgent, 'https://xxx:1337'))
return dispatcher.close()
})
test('should not be case sensitive', async (t) => {
t = tspl(t, { plan: 6 })
process.env.NO_PROXY = 'XXX YYY ZzZ'
const { dispatcher, doesNotProxy } = createEnvHttpProxyAgentWithMocks(6)
t.ok(await doesNotProxy('http://xxx'))
t.ok(await doesNotProxy('http://XXX'))
t.ok(await doesNotProxy('http://yyy'))
t.ok(await doesNotProxy('http://YYY'))
t.ok(await doesNotProxy('http://ZzZ'))
t.ok(await doesNotProxy('http://zZz'))
return dispatcher.close()
})
test('prefers lowercase over uppercase', async (t) => {
t = tspl(t, { plan: 2 })
process.env.NO_PROXY = 'another.com'
process.env.no_proxy = 'example.com'
const { dispatcher, doesNotProxy, usesProxyAgent } = createEnvHttpProxyAgentWithMocks(6)
t.ok(await doesNotProxy('http://example.com'))
t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://another.com'))
return dispatcher.close()
})
test('prefers lowercase over uppercase even when it is empty', async (t) => {
t = tspl(t, { plan: 1 })
process.env.NO_PROXY = 'example.com'
process.env.no_proxy = ''
const { dispatcher, usesProxyAgent } = createEnvHttpProxyAgentWithMocks()
t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://example.com'))
return dispatcher.close()
})
test('handles env var changes', async (t) => {
t = tspl(t, { plan: 4 })
process.env.no_proxy = 'example.com'
const { dispatcher, doesNotProxy, usesProxyAgent } = createEnvHttpProxyAgentWithMocks(4)
t.ok(await doesNotProxy('http://example.com'))
t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://another.com'))
process.env.no_proxy = 'another.com'
t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://example.com'))
t.ok(await doesNotProxy('http://another.com'))
return dispatcher.close()
})
test('ignores env var changes when set via config', async (t) => {
t = tspl(t, { plan: 4 })
const { dispatcher, doesNotProxy, usesProxyAgent } = createEnvHttpProxyAgentWithMocks(4, { noProxy: 'example.com' })
t.ok(await doesNotProxy('http://example.com'))
t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://another.com'))
process.env.no_proxy = 'another.com'
t.ok(await doesNotProxy('http://example.com'))
t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://another.com'))
return dispatcher.close()
})
})
================================================
FILE: test/errors.js
================================================
'use strict'
const { tspl } = require('@matteo.collina/tspl')
const { describe, test } = require('node:test')
const errors = require('../lib/core/errors')
const createScenario = (ErrorClass, defaultMessage, name, code) => ({
ErrorClass,
defaultMessage,
name,
code
})
const scenarios = [
createScenario(errors.UndiciError, '', 'UndiciError', 'UND_ERR'),
createScenario(errors.ConnectTimeoutError, 'Connect Timeout Error', 'ConnectTimeoutError', 'UND_ERR_CONNECT_TIMEOUT'),
createScenario(errors.HeadersTimeoutError, 'Headers Timeout Error', 'HeadersTimeoutError', 'UND_ERR_HEADERS_TIMEOUT'),
createScenario(errors.HeadersOverflowError, 'Headers Overflow Error', 'HeadersOverflowError', 'UND_ERR_HEADERS_OVERFLOW'),
createScenario(errors.InvalidArgumentError, 'Invalid Argument Error', 'InvalidArgumentError', 'UND_ERR_INVALID_ARG'),
createScenario(errors.InvalidReturnValueError, 'Invalid Return Value Error', 'InvalidReturnValueError', 'UND_ERR_INVALID_RETURN_VALUE'),
createScenario(errors.RequestAbortedError, 'Request aborted', 'AbortError', 'UND_ERR_ABORTED'),
createScenario(errors.InformationalError, 'Request information', 'InformationalError', 'UND_ERR_INFO'),
createScenario(errors.RequestContentLengthMismatchError, 'Request body length does not match content-length header', 'RequestContentLengthMismatchError', 'UND_ERR_REQ_CONTENT_LENGTH_MISMATCH'),
createScenario(errors.ClientDestroyedError, 'The client is destroyed', 'ClientDestroyedError', 'UND_ERR_DESTROYED'),
createScenario(errors.ClientClosedError, 'The client is closed', 'ClientClosedError', 'UND_ERR_CLOSED'),
createScenario(errors.SocketError, 'Socket error', 'SocketError', 'UND_ERR_SOCKET'),
createScenario(errors.NotSupportedError, 'Not supported error', 'NotSupportedError', 'UND_ERR_NOT_SUPPORTED'),
createScenario(errors.ResponseContentLengthMismatchError, 'Response body length does not match content-length header', 'ResponseContentLengthMismatchError', 'UND_ERR_RES_CONTENT_LENGTH_MISMATCH'),
createScenario(errors.ResponseExceededMaxSizeError, 'Response content exceeded max size', 'ResponseExceededMaxSizeError', 'UND_ERR_RES_EXCEEDED_MAX_SIZE')
]
scenarios.forEach(scenario => {
describe(scenario.name, () => {
const SAMPLE_MESSAGE = 'sample message'
const errorWithDefaultMessage = () => new scenario.ErrorClass()
const errorWithProvidedMessage = () => new scenario.ErrorClass(SAMPLE_MESSAGE)
test('should use default message', t => {
t = tspl(t, { plan: 1 })
const error = errorWithDefaultMessage()
t.strictEqual(error.message, scenario.defaultMessage)
})
test('should use provided message', t => {
t = tspl(t, { plan: 1 })
const error = errorWithProvidedMessage()
t.strictEqual(error.message, SAMPLE_MESSAGE)
})
test('should have proper fields', t => {
t = tspl(t, { plan: 6 })
const errorInstances = [errorWithDefaultMessage(), errorWithProvidedMessage()]
errorInstances.forEach(error => {
t.strictEqual(error.name, scenario.name)
t.strictEqual(error.code, scenario.code)
t.ok(error.stack)
})
})
})
})
describe('Default HTTPParseError Codes', () => {
test('code and data should be undefined when not set', t => {
t = tspl(t, { plan: 2 })
const error = new errors.HTTPParserError('HTTPParserError')
t.strictEqual(error.code, undefined)
t.strictEqual(error.data, undefined)
})
})
================================================
FILE: test/esm-wrapper.js
================================================
'use strict'
;(async () => {
try {
await import('./utils/esm-wrapper.mjs')
} catch (e) {
if (e.message === 'Not supported') {
require('node:test') // shows skipped
return
}
console.error(e.stack)
process.exitCode = 1
}
})()
================================================
FILE: test/eventsource/eventsource-attributes.js
================================================
'use strict'
const { once } = require('node:events')
const http = require('node:http')
const { test, describe, before, after } = require('node:test')
const { EventSource } = require('../../lib/web/eventsource/eventsource')
describe('EventSource - eventhandler idl', () => {
let server
let port
before(async () => {
server = http.createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.writeHead(200, 'dummy')
})
await once(server.listen(0), 'listening')
port = server.address().port
})
after(() => { server.close() })
const eventhandlerIdl = ['onmessage', 'onerror', 'onopen']
eventhandlerIdl.forEach((type) => {
test(`Should properly configure the ${type} eventhandler idl`, (t) => {
const eventSourceInstance = new EventSource(`http://localhost:${port}`)
// Eventsource eventhandler idl is by default null,
t.assert.strictEqual(eventSourceInstance[type], null)
// The eventhandler idl is by default not enumerable.
t.assert.strictEqual(Object.prototype.propertyIsEnumerable.call(eventSourceInstance, type), false)
// The eventhandler idl ignores non-functions.
eventSourceInstance[type] = 7
t.assert.strictEqual(EventSource[type], undefined)
// The eventhandler idl accepts functions.
function fn () {
t.assert.fail('Should not have called the eventhandler')
}
eventSourceInstance[type] = fn
t.assert.strictEqual(eventSourceInstance[type], fn)
// The eventhandler idl can be set to another function.
function fn2 () { }
eventSourceInstance[type] = fn2
t.assert.strictEqual(eventSourceInstance[type], fn2)
// The eventhandler idl overrides the previous function.
eventSourceInstance.dispatchEvent(new Event(type))
eventSourceInstance.close()
})
})
})
describe('EventSource - constants', () => {
[
['CONNECTING', 0],
['OPEN', 1],
['CLOSED', 2]
].forEach((config) => {
test(`Should expose the ${config[0]} constant`, (t) => {
const [constant, value] = config
// EventSource exposes the constant.
t.assert.strictEqual(Object.hasOwn(EventSource, constant), true)
// The value is properly set.
t.assert.strictEqual(EventSource[constant], value)
// The constant is enumerable.
t.assert.strictEqual(Object.prototype.propertyIsEnumerable.call(EventSource, constant), true)
// The constant is not writable.
try {
EventSource[constant] = 666
} catch (e) {
t.assert.strictEqual(e instanceof TypeError, true)
}
// The constant is not configurable.
try {
delete EventSource[constant]
} catch (e) {
t.assert.strictEqual(e instanceof TypeError, true)
}
t.assert.strictEqual(EventSource[constant], value)
})
})
})
================================================
FILE: test/eventsource/eventsource-close.js
================================================
'use strict'
const { once } = require('node:events')
const http = require('node:http')
const { test, describe, after } = require('node:test')
const { EventSource } = require('../../lib/web/eventsource/eventsource')
describe('EventSource - close', () => {
test('should not emit error when closing the EventSource Instance', async (t) => {
t.plan(1)
const server = http.createServer({ joinDuplicateHeaders: true }, (req, res) => {
t.assert.strictEqual(req.headers.connection, 'keep-alive')
res.writeHead(200, 'OK', { 'Content-Type': 'text/event-stream' })
res.write('data: hello\n\n')
res.on('close', () => {
server.close()
})
})
await once(server.listen(0), 'listening')
const port = server.address().port
const eventSourceInstance = new EventSource(`http://localhost:${port}`)
eventSourceInstance.onopen = () => {
eventSourceInstance.close()
}
eventSourceInstance.onerror = () => {
t.assert.fail('Should not have errored')
}
await once(server, 'close')
})
test('should set readyState to CLOSED', async (t) => {
t.plan(3)
const server = http.createServer({ joinDuplicateHeaders: true }, (req, res) => {
t.assert.strictEqual(req.headers.connection, 'keep-alive')
res.writeHead(200, 'OK', { 'Content-Type': 'text/event-stream' })
res.write('data: hello\n\n')
})
after(() => server.close())
await once(server.listen(0), 'listening')
const port = server.address().port
const eventSourceInstance = new EventSource(`http://localhost:${port}`)
eventSourceInstance.onopen = () => {
t.assert.strictEqual(eventSourceInstance.readyState, EventSource.OPEN)
eventSourceInstance.close()
t.assert.strictEqual(eventSourceInstance.readyState, EventSource.CLOSED)
server.close()
}
eventSourceInstance.onerror = () => {
t.assert.fail('Should not have errored')
}
await once(server, 'close')
})
})
================================================
FILE: test/eventsource/eventsource-connect.js
================================================
'use strict'
const { once } = require('node:events')
const http = require('node:http')
const { test, describe, after } = require('node:test')
const FakeTimers = require('@sinonjs/fake-timers')
const { EventSource, defaultReconnectionTime } = require('../../lib/web/eventsource/eventsource')
const { randomInt } = require('node:crypto')
describe('EventSource - sending correct request headers', () => {
test('should send request with connection keep-alive', async (t) => {
const server = http.createServer({ joinDuplicateHeaders: true }, (req, res) => {
t.assert.strictEqual(req.headers.connection, 'keep-alive')
res.writeHead(200, 'OK', { 'Content-Type': 'text/event-stream' })
res.end()
})
await once(server.listen(0), 'listening')
const port = server.address().port
const eventSourceInstance = new EventSource(`http://localhost:${port}`)
eventSourceInstance.onopen = (t) => {
eventSourceInstance.close()
server.close()
}
eventSourceInstance.onerror = (t) => {
t.assert.fail('Should not have errored')
}
})
test('should send request with sec-fetch-mode set to cors', async (t) => {
const server = http.createServer({ joinDuplicateHeaders: true }, (req, res) => {
t.assert.strictEqual(req.headers['sec-fetch-mode'], 'cors')
res.writeHead(200, 'OK', { 'Content-Type': 'text/event-stream' })
res.end()
})
await once(server.listen(0), 'listening')
const port = server.address().port
const eventSourceInstance = new EventSource(`http://localhost:${port}`)
eventSourceInstance.onopen = (t) => {
eventSourceInstance.close()
server.close()
}
eventSourceInstance.onerror = (t) => {
t.assert.fail('Should not have errored')
}
})
test('should send request with pragma and cache-control set to no-cache', async (t) => {
const server = http.createServer({ joinDuplicateHeaders: true }, (req, res) => {
t.assert.strictEqual(req.headers['cache-control'], 'no-cache')
t.assert.strictEqual(req.headers.pragma, 'no-cache')
res.writeHead(200, 'OK', { 'Content-Type': 'text/event-stream' })
res.end()
})
await once(server.listen(0), 'listening')
const port = server.address().port
const eventSourceInstance = new EventSource(`http://localhost:${port}`)
eventSourceInstance.onopen = (t) => {
eventSourceInstance.close()
server.close()
}
eventSourceInstance.onerror = (t) => {
t.assert.fail('Should not have errored')
}
})
test('should send request with accept text/event-stream', async (t) => {
const server = http.createServer({ joinDuplicateHeaders: true }, (req, res) => {
t.assert.strictEqual(req.headers.accept, 'text/event-stream')
res.writeHead(200, 'OK', { 'Content-Type': 'text/event-stream' })
res.end()
})
await once(server.listen(0), 'listening')
const port = server.address().port
const eventSourceInstance = new EventSource(`http://localhost:${port}`)
eventSourceInstance.onopen = (t) => {
eventSourceInstance.close()
server.close()
}
eventSourceInstance.onerror = (t) => {
t.assert.fail('Should not have errored')
}
})
})
describe('EventSource - received response must have content-type to be text/event-stream', () => {
test('should send request with accept text/event-stream', async (t) => {
const server = http.createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.writeHead(200, 'OK', { 'Content-Type': 'text/event-stream' })
res.end()
})
await once(server.listen(0), 'listening')
const port = server.address().port
const eventSourceInstance = new EventSource(`http://localhost:${port}`)
eventSourceInstance.onopen = (t) => {
eventSourceInstance.close()
server.close()
}
eventSourceInstance.onerror = (t) => {
t.assert.fail('Should not have errored')
}
})
test('should send request with accept text/event-stream;', async (t) => {
const server = http.createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.writeHead(200, 'OK', { 'Content-Type': 'text/event-stream;' })
res.end()
})
await once(server.listen(0), 'listening')
const port = server.address().port
const eventSourceInstance = new EventSource(`http://localhost:${port}`)
eventSourceInstance.onopen = (t) => {
eventSourceInstance.close()
server.close()
}
eventSourceInstance.onerror = (t) => {
t.assert.fail('Should not have errored')
}
})
test('should handle content-type text/event-stream;charset=UTF-8 properly', async (t) => {
const server = http.createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.writeHead(200, 'OK', { 'Content-Type': 'text/event-stream;charset=UTF-8' })
res.end()
})
await once(server.listen(0), 'listening')
const port = server.address().port
const eventSourceInstance = new EventSource(`http://localhost:${port}`)
eventSourceInstance.onopen = (t) => {
eventSourceInstance.close()
server.close()
}
eventSourceInstance.onerror = (t) => {
t.assert.fail('Should not have errored')
}
})
test('should throw if content-type is text/html properly', async (t) => {
const server = http.createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.writeHead(200, 'OK', { 'Content-Type': 'text/html' })
res.end()
})
await once(server.listen(0), 'listening')
const port = server.address().port
const eventSourceInstance = new EventSource(`http://localhost:${port}`)
eventSourceInstance.onopen = (t) => {
t.assert.fail('Should not have opened')
}
eventSourceInstance.onerror = (t) => {
eventSourceInstance.close()
server.close()
}
})
test('should try to connect again if server is unreachable', async (t) => {
const clock = FakeTimers.install()
after(() => clock.uninstall())
const reconnectionTime = defaultReconnectionTime
const domain = 'bad.n' + randomInt(1e10).toString(36) + '.proxy'
const eventSourceInstance = new EventSource(`http://${domain}`)
const onerrorCalls = []
eventSourceInstance.onerror = (error) => {
onerrorCalls.push(error)
}
clock.tick(reconnectionTime)
await once(eventSourceInstance, 'error')
const start = Date.now()
clock.tick(reconnectionTime)
await once(eventSourceInstance, 'error')
clock.tick(reconnectionTime)
await once(eventSourceInstance, 'error')
clock.tick(reconnectionTime)
await once(eventSourceInstance, 'error')
const end = Date.now()
eventSourceInstance.close()
t.assert.strictEqual(onerrorCalls.length, 4, 'Expected 4 error events')
t.assert.strictEqual(end - start, 3 * reconnectionTime, `Expected reconnection to happen after ${3 * reconnectionTime}ms, but took ${end - start}ms`)
})
test('should try to connect again if server is unreachable, configure reconnectionTime', async (t) => {
const reconnectionTime = 1000
const clock = FakeTimers.install()
after(() => clock.uninstall())
const domain = 'bad.n' + randomInt(1e10).toString(36) + '.proxy'
const eventSourceInstance = new EventSource(`http://${domain}`, {
node: {
reconnectionTime
}
})
const onerrorCalls = []
eventSourceInstance.onerror = (error) => {
onerrorCalls.push(error)
}
await once(eventSourceInstance, 'error')
const start = Date.now()
clock.tick(reconnectionTime)
await once(eventSourceInstance, 'error')
clock.tick(reconnectionTime)
await once(eventSourceInstance, 'error')
clock.tick(reconnectionTime)
await once(eventSourceInstance, 'error')
const end = Date.now()
eventSourceInstance.close()
t.assert.strictEqual(onerrorCalls.length, 4, 'Expected 4 error events')
t.assert.strictEqual(end - start, 3 * reconnectionTime, `Expected reconnection to happen after ${3 * reconnectionTime}ms, but took ${end - start}ms`)
})
})
================================================
FILE: test/eventsource/eventsource-constructor-stringify.js
================================================
'use strict'
const { once } = require('node:events')
const http = require('node:http')
const { test, describe } = require('node:test')
const { EventSource } = require('../../lib/web/eventsource/eventsource')
describe('EventSource - constructor stringify', () => {
test('should stringify argument', async (t) => {
const server = http.createServer({ joinDuplicateHeaders: true }, (req, res) => {
t.assert.strictEqual(req.headers.connection, 'keep-alive')
res.writeHead(200, 'OK', { 'Content-Type': 'text/event-stream' })
res.end()
})
await once(server.listen(0), 'listening')
const port = server.address().port
const eventSourceInstance = new EventSource({ toString: function () { return `http://localhost:${port}` } })
eventSourceInstance.onopen = () => {
eventSourceInstance.close()
server.close()
}
eventSourceInstance.onerror = () => {
t.assert.fail('Should not have errored')
}
})
})
================================================
FILE: test/eventsource/eventsource-constructor.js
================================================
'use strict'
const { once } = require('node:events')
const http = require('node:http')
const { test, describe } = require('node:test')
const { EventSource } = require('../../lib/web/eventsource/eventsource')
describe('EventSource - withCredentials', () => {
test('withCredentials should be false by default', async (t) => {
const server = http.createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.writeHead(200, 'OK', { 'Content-Type': 'text/event-stream' })
res.end()
})
await once(server.listen(0), 'listening')
const port = server.address().port
const eventSourceInstance = new EventSource(`http://localhost:${port}`)
eventSourceInstance.onopen = () => {
t.assert.strictEqual(eventSourceInstance.withCredentials, false)
eventSourceInstance.close()
server.close()
}
eventSourceInstance.onerror = () => {
t.assert.fail('Should not have errored')
}
})
test('withCredentials can be set to true', async (t) => {
const server = http.createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.writeHead(200, 'OK', { 'Content-Type': 'text/event-stream' })
res.end()
})
await once(server.listen(0), 'listening')
const port = server.address().port
const eventSourceInstance = new EventSource(`http://localhost:${port}`, { withCredentials: true })
eventSourceInstance.onopen = () => {
t.assert.strictEqual(eventSourceInstance.withCredentials, true)
eventSourceInstance.close()
server.close()
}
eventSourceInstance.onerror = () => {
t.assert.fail('Should not have errored')
}
})
})
================================================
FILE: test/eventsource/eventsource-custom-dispatcher.js
================================================
'use strict'
const { createServer } = require('node:http')
const { Agent, EventSource } = require('../..')
const { test } = require('node:test')
test('EventSource allows setting custom dispatcher.', (t, done) => {
t.plan(1)
const server = createServer({ joinDuplicateHeaders: true }, async (req, res) => {
res.writeHead(200, 'OK', { 'Content-Type': 'text/event-stream' })
t.assert.deepStrictEqual(req.headers['x-customer-header'], 'hello world')
res.end()
done()
})
t.after(() => {
server.close()
})
server.listen(0, () => {
class CustomHeaderAgent extends Agent {
dispatch (opts) {
opts.headers['x-customer-header'] = 'hello world'
return super.dispatch(...arguments)
}
}
const eventSourceInstance = new EventSource(`http://localhost:${server.address().port}`, {
dispatcher: new CustomHeaderAgent()
})
t.after(() => {
eventSourceInstance.close()
})
})
})
test('EventSource allows setting custom dispatcher in EventSourceDict.', (t, done) => {
t.plan(1)
const server = createServer({ joinDuplicateHeaders: true }, async (req, res) => {
res.writeHead(200, 'OK', { 'Content-Type': 'text/event-stream' })
t.assert.deepStrictEqual(req.headers['x-customer-header'], 'hello world')
res.end()
done()
})
t.after(() => {
server.close()
})
server.listen(0, () => {
class CustomHeaderAgent extends Agent {
dispatch (opts) {
opts.headers['x-customer-header'] = 'hello world'
return super.dispatch(...arguments)
}
}
const eventSourceInstance = new EventSource(`http://localhost:${server.address().port}`, {
node: {
dispatcher: new CustomHeaderAgent()
}
})
t.after(() => {
eventSourceInstance.close()
})
})
})
================================================
FILE: test/eventsource/eventsource-message.js
================================================
'use strict'
const { once } = require('node:events')
const http = require('node:http')
const { test, describe, after } = require('node:test')
const { EventSource, defaultReconnectionTime } = require('../../lib/web/eventsource/eventsource')
const FakeTimers = require('@sinonjs/fake-timers')
describe('EventSource - message', () => {
test('Should not emit a message if only retry field was sent', (t, done) => {
t.plan(2)
const server = http.createServer({ joinDuplicateHeaders: true }, async (req, res) => {
res.writeHead(200, 'OK', { 'Content-Type': 'text/event-stream' })
res.write('retry: 100\n\n')
setTimeout(() => res.end(), 100)
})
after(() => server.close())
server.listen(0, () => {
const port = server.address().port
const start = Date.now()
let connectionCount = 0
const eventSourceInstance = new EventSource(`http://localhost:${port}`)
eventSourceInstance.onopen = () => {
if (++connectionCount === 2) {
t.assert.ok(Date.now() - start >= 100)
t.assert.ok(Date.now() - start < 1000)
eventSourceInstance.close()
done()
}
}
eventSourceInstance.onmessage = () => {
t.assert.fail('Should not have received a message')
eventSourceInstance.close()
}
})
})
test('Should not emit a message if no data is provided', async (t) => {
t.plan(1)
const server = http.createServer({ joinDuplicateHeaders: true }, async (req, res) => {
res.writeHead(200, 'OK', { 'Content-Type': 'text/event-stream' })
res.write('event:message\n\n')
setTimeout(() => res.end(), 100)
})
after(() => server.close())
await once(server.listen(0), 'listening')
const port = server.address().port
const eventSourceInstance = new EventSource(`http://localhost:${port}`)
eventSourceInstance.onmessage = () => {
t.assert.fail('Should not have received a message')
eventSourceInstance.close()
}
eventSourceInstance.close()
t.assert.ok('Should not have received a message')
})
test('Should emit a custom type message if data is provided', (t, done) => {
t.plan(1)
const server = http.createServer({ joinDuplicateHeaders: true }, async (req, res) => {
res.writeHead(200, 'OK', { 'Content-Type': 'text/event-stream' })
res.write('event:custom\ndata:test\n\n')
setTimeout(() => res.end(), 100)
})
after(() => server.close())
server.listen(0, () => {
const port = server.address().port
const eventSourceInstance = new EventSource(`http://localhost:${port}`)
eventSourceInstance.addEventListener('custom', () => {
t.assert.ok(true)
done()
eventSourceInstance.close()
})
})
})
test('Should emit a message event if data is provided', (t, done) => {
t.plan(1)
const server = http.createServer({ joinDuplicateHeaders: true }, async (req, res) => {
res.writeHead(200, 'OK', { 'Content-Type': 'text/event-stream' })
res.write('data:test\n\n')
setTimeout(() => res.end(), 100)
})
after(() => server.close())
server.listen(0, () => {
const port = server.address().port
const eventSourceInstance = new EventSource(`http://localhost:${port}`)
eventSourceInstance.addEventListener('message', () => {
t.assert.ok(true)
eventSourceInstance.close()
done()
})
})
})
test('Should emit a message event if data as a field is provided', (t, done) => {
t.plan(1)
const server = http.createServer({ joinDuplicateHeaders: true }, async (req, res) => {
res.writeHead(200, 'OK', { 'Content-Type': 'text/event-stream' })
res.write('data\n\n')
setTimeout(() => res.end(), 100)
})
after(() => server.close())
server.listen(0, () => {
const port = server.address().port
const eventSourceInstance = new EventSource(`http://localhost:${port}`)
eventSourceInstance.addEventListener('message', () => {
t.assert.ok(true)
eventSourceInstance.close()
done()
})
})
})
test('Should emit a custom message event if data is empty', (t, done) => {
t.plan(1)
const server = http.createServer({ joinDuplicateHeaders: true }, async (req, res) => {
res.writeHead(200, 'OK', { 'Content-Type': 'text/event-stream' })
res.write('event:custom\ndata:\n\n')
setTimeout(() => res.end(), 100)
})
after(() => server.close())
server.listen(0, () => {
const port = server.address().port
const eventSourceInstance = new EventSource(`http://localhost:${port}`)
eventSourceInstance.addEventListener('custom', () => {
t.assert.ok(true)
eventSourceInstance.close()
done()
})
})
})
test('Should emit a message event if data is empty', (t, done) => {
t.plan(1)
const server = http.createServer({ joinDuplicateHeaders: true }, async (req, res) => {
res.writeHead(200, 'OK', { 'Content-Type': 'text/event-stream' })
res.write('data:\n\n')
setTimeout(() => res.end(), 100)
})
after(() => server.close())
server.listen(0, () => {
const port = server.address().port
const eventSourceInstance = new EventSource(`http://localhost:${port}`)
eventSourceInstance.addEventListener('message', () => {
t.assert.ok(true)
eventSourceInstance.close()
done()
})
})
})
test('Should emit a custom message event if data only as a field is provided', (t, done) => {
t.plan(1)
const server = http.createServer({ joinDuplicateHeaders: true }, async (req, res) => {
res.writeHead(200, 'OK', { 'Content-Type': 'text/event-stream' })
res.write('event:custom\ndata\n\n')
setTimeout(() => res.end(), 100)
})
after(() => server.close())
server.listen(0, () => {
const port = server.address().port
const eventSourceInstance = new EventSource(`http://localhost:${port}`)
eventSourceInstance.addEventListener('custom', () => {
t.assert.ok(true)
eventSourceInstance.close()
done()
})
})
})
test('Should not emit a custom type message if no data is provided', (t, done) => {
const clock = FakeTimers.install()
after(() => clock.uninstall())
t.plan(1)
const server = http.createServer({ joinDuplicateHeaders: true }, async (req, res) => {
res.writeHead(200, 'OK', { 'Content-Type': 'text/event-stream' })
res.write('event:custom\n\n')
setTimeout(() => res.end(), 100)
})
let reconnectionCount = 0
after(() => server.close())
server.listen(0, async () => {
const port = server.address().port
const eventSourceInstance = new EventSource(`http://localhost:${port}`)
eventSourceInstance.onopen = () => {
if (++reconnectionCount === 2) {
t.assert.ok(true)
eventSourceInstance.close()
done()
}
}
eventSourceInstance.addEventListener('custom', () => {
t.assert.fail('Should not have received a message')
eventSourceInstance.close()
})
await once(eventSourceInstance, 'open')
clock.tick(defaultReconnectionTime)
await once(eventSourceInstance, 'error')
clock.tick(defaultReconnectionTime)
await once(eventSourceInstance, 'open')
clock.tick(defaultReconnectionTime)
})
})
})
================================================
FILE: test/eventsource/eventsource-properties.js
================================================
'use strict'
const { test } = require('node:test')
const { EventSource } = require('../..') // assuming the test is in test/eventsource/
test('EventSource.prototype properties are configured correctly', (t) => {
const props = Object.entries(Object.getOwnPropertyDescriptors(EventSource.prototype))
for (const [key, value] of props) {
if (key !== 'constructor') {
t.assert.ok(value.enumerable, `${key} is not enumerable`)
}
}
})
================================================
FILE: test/eventsource/eventsource-reconnect.js
================================================
'use strict'
const { once } = require('node:events')
const http = require('node:http')
const { test, describe, after } = require('node:test')
const FakeTimers = require('@sinonjs/fake-timers')
const { EventSource, defaultReconnectionTime } = require('../../lib/web/eventsource/eventsource')
describe('EventSource - reconnect', () => {
test('Should reconnect on connection closed by server', (t, done) => {
t.plan(1)
const clock = FakeTimers.install()
after(() => clock.uninstall())
const server = http.createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.writeHead(200, 'OK', { 'Content-Type': 'text/event-stream' })
res.end()
})
after(() => server.close())
server.listen(0, async () => {
const port = server.address().port
const eventSourceInstance = new EventSource(`http://localhost:${port}`)
let connectionCount = 0
eventSourceInstance.onopen = () => {
if (++connectionCount === 2) {
eventSourceInstance.close()
t.assert.ok(true)
done()
}
}
await once(eventSourceInstance, 'open')
clock.tick(10)
await once(eventSourceInstance, 'error')
clock.tick(defaultReconnectionTime)
})
})
test('Should reconnect on with reconnection timeout', (t, done) => {
t.plan(2)
const clock = FakeTimers.install()
after(() => clock.uninstall())
const server = http.createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.writeHead(200, 'OK', { 'Content-Type': 'text/event-stream' })
res.end()
})
after(() => server.close())
server.listen(0, async () => {
const port = server.address().port
const start = Date.now()
const eventSourceInstance = new EventSource(`http://localhost:${port}`)
let connectionCount = 0
eventSourceInstance.onopen = () => {
if (++connectionCount === 2) {
t.assert.ok(Date.now() - start >= defaultReconnectionTime)
eventSourceInstance.close()
t.assert.ok(true)
done()
}
}
await once(eventSourceInstance, 'open')
clock.tick(10)
await once(eventSourceInstance, 'error')
clock.tick(defaultReconnectionTime)
})
})
test('Should reconnect on with modified reconnection timeout', (t, done) => {
t.plan(3)
const clock = FakeTimers.install()
after(() => clock.uninstall())
const server = http.createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.writeHead(200, 'OK', { 'Content-Type': 'text/event-stream' })
res.write('retry: 100\n\n')
res.end()
})
after(() => server.close())
server.listen(0, async () => {
const port = server.address().port
const start = Date.now()
const eventSourceInstance = new EventSource(`http://localhost:${port}`)
let connectionCount = 0
eventSourceInstance.onopen = () => {
if (++connectionCount === 2) {
t.assert.ok(Date.now() - start >= 100)
t.assert.ok(Date.now() - start < 1000)
eventSourceInstance.close()
t.assert.ok(true)
done()
}
}
await once(eventSourceInstance, 'open')
clock.tick(10)
await once(eventSourceInstance, 'error')
clock.tick(100)
})
})
test('Should reconnect and send lastEventId', async (t) => {
t.plan(1)
const clock = FakeTimers.install()
after(() => clock.uninstall())
let requestCount = 0
const server = http.createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.writeHead(200, 'OK', { 'Content-Type': 'text/event-stream' })
res.write('id: 1337\n\n')
if (++requestCount === 2) {
t.assert.strictEqual(req.headers['last-event-id'], '1337')
}
res.end()
})
after(() => server.close())
await once(server.listen(0), 'listening')
const port = server.address().port
const eventSourceInstance = new EventSource(`http://localhost:${port}`)
await once(eventSourceInstance, 'open')
clock.tick(10)
await once(eventSourceInstance, 'error')
clock.tick(defaultReconnectionTime)
await once(eventSourceInstance, 'open')
})
})
================================================
FILE: test/eventsource/eventsource-redirecting.js
================================================
'use strict'
const { once } = require('node:events')
const http = require('node:http')
const { test, describe } = require('node:test')
const { EventSource } = require('../../lib/web/eventsource/eventsource')
describe('EventSource - redirecting', () => {
[301, 302, 307, 308].forEach((statusCode) => {
test(`Should redirect on ${statusCode} status code`, async (t) => {
const server = http.createServer({ joinDuplicateHeaders: true }, (req, res) => {
if (res.req.url === '/redirect') {
res.writeHead(statusCode, undefined, { Location: '/target' })
res.end()
} else if (res.req.url === '/target') {
res.writeHead(200, 'dummy', { 'Content-Type': 'text/event-stream' })
res.end()
}
})
await once(server.listen(0), 'listening')
const port = server.address().port
const eventSourceInstance = new EventSource(`http://localhost:${port}/redirect`)
eventSourceInstance.onerror = (e) => {
t.assert.fail('Should not have errored')
}
eventSourceInstance.onopen = () => {
t.assert.strictEqual(eventSourceInstance.url, `http://localhost:${port}/redirect`)
eventSourceInstance.close()
server.close()
}
})
})
test('Stop trying to connect when getting a 204 response', async (t) => {
const server = http.createServer({ joinDuplicateHeaders: true }, (req, res) => {
if (res.req.url === '/redirect') {
res.writeHead(301, undefined, { Location: '/target' })
res.end()
} else if (res.req.url === '/target') {
res.writeHead(204, 'OK')
res.end()
}
})
await once(server.listen(0), 'listening')
const port = server.address().port
const eventSourceInstance = new EventSource(`http://localhost:${port}/redirect`)
eventSourceInstance.onerror = (event) => {
t.assert.strictEqual(eventSourceInstance.url, `http://localhost:${port}/redirect`)
t.assert.strictEqual(eventSourceInstance.readyState, EventSource.CLOSED)
server.close()
}
eventSourceInstance.onopen = () => {
t.assert.fail('Should not have opened')
}
})
test('Throw when missing a Location header', async (t) => {
const server = http.createServer({ joinDuplicateHeaders: true }, (req, res) => {
if (res.req.url === '/redirect') {
res.writeHead(301, undefined)
res.end()
} else if (res.req.url === '/target') {
res.writeHead(204, 'OK')
res.end()
}
})
await once(server.listen(0), 'listening')
const port = server.address().port
const eventSourceInstance = new EventSource(`http://localhost:${port}/redirect`)
eventSourceInstance.onerror = () => {
t.assert.strictEqual(eventSourceInstance.url, `http://localhost:${port}/redirect`)
t.assert.strictEqual(eventSourceInstance.readyState, EventSource.CLOSED)
server.close()
}
})
test('Should set origin attribute of messages after redirecting', async (t) => {
const targetServer = http.createServer({ joinDuplicateHeaders: true }, (req, res) => {
if (res.req.url === '/target') {
res.writeHead(200, undefined, { 'Content-Type': 'text/event-stream' })
res.write('event: message\ndata: test\n\n')
}
})
await once(targetServer.listen(0), 'listening')
const targetPort = targetServer.address().port
const sourceServer = http.createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.writeHead(301, undefined, { Location: `http://127.0.0.1:${targetPort}/target` })
res.end()
})
await once(sourceServer.listen(0), 'listening')
const sourcePort = sourceServer.address().port
const eventSourceInstance = new EventSource(`http://127.0.0.1:${sourcePort}/redirect`)
eventSourceInstance.onmessage = (event) => {
t.assert.strictEqual(event.origin, `http://127.0.0.1:${targetPort}`)
eventSourceInstance.close()
targetServer.close()
sourceServer.close()
}
eventSourceInstance.onerror = (e) => {
t.assert.fail('Should not have errored')
}
})
})
================================================
FILE: test/eventsource/eventsource-request-status-error.js
================================================
'use strict'
const { once } = require('node:events')
const http = require('node:http')
const { test, describe } = require('node:test')
const { EventSource } = require('../../lib/web/eventsource/eventsource')
describe('EventSource - status error', () => {
[204, 205, 210, 299, 404, 410, 503].forEach((statusCode) => {
test(`Should error on ${statusCode} status code`, async (t) => {
const server = http.createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.writeHead(statusCode, 'dummy', { 'Content-Type': 'text/event-stream' })
res.end()
})
await once(server.listen(0), 'listening')
const port = server.address().port
const eventSourceInstance = new EventSource(`http://localhost:${port}`)
eventSourceInstance.onerror = (e) => {
t.assert.strictEqual(this.readyState, this.CLOSED)
eventSourceInstance.close()
server.close()
}
eventSourceInstance.onmessage = () => {
t.assert.fail('Should not have received a message')
}
eventSourceInstance.onopen = () => {
t.assert.fail('Should not have opened')
}
})
})
})
================================================
FILE: test/eventsource/eventsource-stream-bom.js
================================================
'use strict'
const { test, describe } = require('node:test')
const { EventSourceStream } = require('../../lib/web/eventsource/eventsource-stream')
describe('EventSourceStream - handle BOM', () => {
test('Remove BOM from the beginning of the stream. 1 byte chunks', (t) => {
const dataField = 'data: Hello'
const content = Buffer.from(`\uFEFF${dataField}`, 'utf8')
const stream = new EventSourceStream()
stream.parseLine = function (line) {
t.assert.strictEqual(line.byteLength, dataField.length)
t.assert.strictEqual(line.toString(), dataField)
}
for (let i = 0; i < content.length; i++) {
stream.write(Buffer.from([content[i]]))
}
})
test('Remove BOM from the beginning of the stream. 2 byte chunks', (t) => {
const dataField = 'data: Hello'
const content = Buffer.from(`\uFEFF${dataField}`, 'utf8')
const stream = new EventSourceStream()
stream.parseLine = function (line) {
t.assert.strictEqual(line.byteLength, dataField.length)
t.assert.strictEqual(line.toString(), dataField)
}
for (let i = 0; i < content.length; i += 2) {
stream.write(Buffer.from([content[i], content[i + 1]]))
}
})
test('Remove BOM from the beginning of the stream. 3 byte chunks', (t) => {
const dataField = 'data: Hello'
const content = Buffer.from(`\uFEFF${dataField}`, 'utf8')
const stream = new EventSourceStream()
stream.parseLine = function (line) {
t.assert.strictEqual(line.byteLength, dataField.length)
t.assert.strictEqual(line.toString(), dataField)
}
for (let i = 0; i < content.length; i += 3) {
stream.write(Buffer.from([content[i], content[i + 1], content[i + 2]]))
}
})
test('Remove BOM from the beginning of the stream. 4 byte chunks', (t) => {
const dataField = 'data: Hello'
const content = Buffer.from(`\uFEFF${dataField}`, 'utf8')
const stream = new EventSourceStream()
stream.parseLine = function (line) {
t.assert.strictEqual(line.byteLength, dataField.length)
t.assert.strictEqual(line.toString(), dataField)
}
for (let i = 0; i < content.length; i += 4) {
stream.write(Buffer.from([content[i], content[i + 1], content[i + 2], content[i + 3]]))
}
})
test('Not containing BOM from the beginning of the stream. 1 byte chunks', (t) => {
const dataField = 'data: Hello'
const content = Buffer.from(`${dataField}`, 'utf8')
const stream = new EventSourceStream()
stream.parseLine = function (line) {
t.assert.strictEqual(line.byteLength, dataField.length)
t.assert.strictEqual(line.toString(), dataField)
}
for (let i = 0; i < content.length; i += 1) {
stream.write(Buffer.from([content[i]]))
}
})
test('Not containing BOM from the beginning of the stream. 2 byte chunks', (t) => {
const dataField = 'data: Hello'
const content = Buffer.from(`${dataField}`, 'utf8')
const stream = new EventSourceStream()
stream.parseLine = function (line) {
t.assert.strictEqual(line.byteLength, dataField.length)
t.assert.strictEqual(line.toString(), dataField)
}
for (let i = 0; i < content.length; i += 2) {
stream.write(Buffer.from([content[i], content[i + 1]]))
}
})
test('Not containing BOM from the beginning of the stream. 3 byte chunks', (t) => {
const dataField = 'data: Hello'
const content = Buffer.from(`${dataField}`, 'utf8')
const stream = new EventSourceStream()
stream.parseLine = function (line) {
t.assert.strictEqual(line.byteLength, dataField.length)
t.assert.strictEqual(line.toString(), dataField)
}
for (let i = 0; i < content.length; i += 3) {
stream.write(Buffer.from([content[i], content[i + 1], content[i + 2]]))
}
})
test('Not containing BOM from the beginning of the stream. 4 byte chunks', (t) => {
const dataField = 'data: Hello'
const content = Buffer.from(`${dataField}`, 'utf8')
const stream = new EventSourceStream()
stream.parseLine = function (line) {
t.assert.strictEqual(line.byteLength, dataField.length)
t.assert.strictEqual(line.toString(), dataField)
}
for (let i = 0; i < content.length; i += 4) {
stream.write(Buffer.from([content[i], content[i + 1], content[i + 2], content[i + 3]]))
}
})
})
================================================
FILE: test/eventsource/eventsource-stream-parse-line.js
================================================
'use strict'
const { test, describe } = require('node:test')
const { EventSourceStream } = require('../../lib/web/eventsource/eventsource-stream')
describe('EventSourceStream - parseLine', () => {
const defaultEventSourceSettings = {
origin: 'example.com',
reconnectionTime: 1000
}
test('Should push an unmodified event when line is empty', (t) => {
const stream = new EventSourceStream({
eventSourceSettings: {
...defaultEventSourceSettings
}
})
const event = {}
stream.parseLine(Buffer.from('', 'utf8'), event)
t.assert.strictEqual(typeof event, 'object')
t.assert.strictEqual(Object.keys(event).length, 0)
t.assert.strictEqual(event.data, undefined)
t.assert.strictEqual(event.id, undefined)
t.assert.strictEqual(event.event, undefined)
t.assert.strictEqual(event.retry, undefined)
})
test('Should set the data field with empty string if not containing data', (t) => {
const stream = new EventSourceStream({
eventSourceSettings: {
...defaultEventSourceSettings
}
})
const event = {}
stream.parseLine(Buffer.from('data:', 'utf8'), event)
t.assert.strictEqual(typeof event, 'object')
t.assert.strictEqual(Object.keys(event).length, 1)
t.assert.strictEqual(event.data, '')
t.assert.strictEqual(event.id, undefined)
t.assert.strictEqual(event.event, undefined)
t.assert.strictEqual(event.retry, undefined)
})
test('Should set the data field with empty string if not containing data (containing space after colon)', (t) => {
const stream = new EventSourceStream({
eventSourceSettings: {
...defaultEventSourceSettings
}
})
const event = {}
stream.parseLine(Buffer.from('data: ', 'utf8'), event)
t.assert.strictEqual(typeof event, 'object')
t.assert.strictEqual(Object.keys(event).length, 1)
t.assert.strictEqual(event.data, '')
t.assert.strictEqual(event.id, undefined)
t.assert.strictEqual(event.event, undefined)
t.assert.strictEqual(event.retry, undefined)
})
test('Should set the data field with a string containing space if having more than one space after colon', (t) => {
const stream = new EventSourceStream({
eventSourceSettings: {
...defaultEventSourceSettings
}
})
const event = {}
stream.parseLine(Buffer.from('data: ', 'utf8'), event)
t.assert.strictEqual(typeof event, 'object')
t.assert.strictEqual(Object.keys(event).length, 1)
t.assert.strictEqual(event.data, ' ')
t.assert.strictEqual(event.id, undefined)
t.assert.strictEqual(event.event, undefined)
t.assert.strictEqual(event.retry, undefined)
})
test('Should set value properly, even if the line contains multiple colons', (t) => {
const stream = new EventSourceStream({
eventSourceSettings: {
...defaultEventSourceSettings
}
})
const event = {}
stream.parseLine(Buffer.from('data: : ', 'utf8'), event)
t.assert.strictEqual(typeof event, 'object')
t.assert.strictEqual(Object.keys(event).length, 1)
t.assert.strictEqual(event.data, ': ')
t.assert.strictEqual(event.id, undefined)
t.assert.strictEqual(event.event, undefined)
t.assert.strictEqual(event.retry, undefined)
})
test('Should set the data field when containing data', (t) => {
const stream = new EventSourceStream({
eventSourceSettings: {
...defaultEventSourceSettings
}
})
const event = {}
stream.parseLine(Buffer.from('data: Hello', 'utf8'), event)
t.assert.strictEqual(typeof event, 'object')
t.assert.strictEqual(Object.keys(event).length, 1)
t.assert.strictEqual(event.data, 'Hello')
t.assert.strictEqual(event.id, undefined)
t.assert.strictEqual(event.event, undefined)
t.assert.strictEqual(event.retry, undefined)
})
test('Should ignore comments', (t) => {
const stream = new EventSourceStream({
eventSourceSettings: {
...defaultEventSourceSettings
}
})
const event = {}
stream.parseLine(Buffer.from(':comment', 'utf8'), event)
t.assert.strictEqual(typeof event, 'object')
t.assert.strictEqual(Object.keys(event).length, 0)
t.assert.strictEqual(event.data, undefined)
t.assert.strictEqual(event.id, undefined)
t.assert.strictEqual(event.event, undefined)
t.assert.strictEqual(event.retry, undefined)
})
test('Should set retry field', (t) => {
const stream = new EventSourceStream({
eventSourceSettings: {
...defaultEventSourceSettings
}
})
const event = {}
stream.parseLine(Buffer.from('retry: 1000', 'utf8'), event)
t.assert.strictEqual(typeof event, 'object')
t.assert.strictEqual(Object.keys(event).length, 1)
t.assert.strictEqual(event.data, undefined)
t.assert.strictEqual(event.id, undefined)
t.assert.strictEqual(event.event, undefined)
t.assert.strictEqual(event.retry, '1000')
})
test('Should set id field', (t) => {
const stream = new EventSourceStream({
eventSourceSettings: {
...defaultEventSourceSettings
}
})
const event = {}
stream.parseLine(Buffer.from('id: 1234', 'utf8'), event)
t.assert.strictEqual(typeof event, 'object')
t.assert.strictEqual(Object.keys(event).length, 1)
t.assert.strictEqual(event.data, undefined)
t.assert.strictEqual(event.id, '1234')
t.assert.strictEqual(event.event, undefined)
t.assert.strictEqual(event.retry, undefined)
})
test('Should set id field', (t) => {
const stream = new EventSourceStream({
eventSourceSettings: {
...defaultEventSourceSettings
}
})
const event = {}
stream.parseLine(Buffer.from('event: custom', 'utf8'), event)
t.assert.strictEqual(typeof event, 'object')
t.assert.strictEqual(Object.keys(event).length, 1)
t.assert.strictEqual(event.data, undefined)
t.assert.strictEqual(event.id, undefined)
t.assert.strictEqual(event.event, 'custom')
t.assert.strictEqual(event.retry, undefined)
})
test('Should ignore invalid field', (t) => {
const stream = new EventSourceStream({
eventSourceSettings: {
...defaultEventSourceSettings
}
})
const event = {}
stream.parseLine(Buffer.from('comment: invalid', 'utf8'), event)
t.assert.strictEqual(typeof event, 'object')
t.assert.strictEqual(Object.keys(event).length, 0)
t.assert.strictEqual(event.data, undefined)
t.assert.strictEqual(event.id, undefined)
t.assert.strictEqual(event.event, undefined)
t.assert.strictEqual(event.retry, undefined)
})
test('bogus retry', (t) => {
const stream = new EventSourceStream({
eventSourceSettings: {
...defaultEventSourceSettings
}
})
const event = {}
'retry:3000\nretry:1000x\ndata:x'.split('\n').forEach((line) => {
stream.parseLine(Buffer.from(line, 'utf8'), event)
})
t.assert.strictEqual(typeof event, 'object')
t.assert.strictEqual(Object.keys(event).length, 2)
t.assert.strictEqual(event.data, 'x')
t.assert.strictEqual(event.id, undefined)
t.assert.strictEqual(event.event, undefined)
t.assert.strictEqual(event.retry, '3000')
})
test('bogus id', (t) => {
const stream = new EventSourceStream({
eventSourceSettings: {
...defaultEventSourceSettings
}
})
const event = {}
'id:3000\nid:30\x000\ndata:x'.split('\n').forEach((line) => {
stream.parseLine(Buffer.from(line, 'utf8'), event)
})
t.assert.strictEqual(typeof event, 'object')
t.assert.strictEqual(Object.keys(event).length, 2)
t.assert.strictEqual(event.data, 'x')
t.assert.strictEqual(event.id, '3000')
t.assert.strictEqual(event.event, undefined)
t.assert.strictEqual(event.retry, undefined)
})
test('empty event', (t) => {
const stream = new EventSourceStream({
eventSourceSettings: {
...defaultEventSourceSettings
}
})
const event = {}
'event: \ndata:data'.split('\n').forEach((line) => {
stream.parseLine(Buffer.from(line, 'utf8'), event)
})
t.assert.strictEqual(typeof event, 'object')
t.assert.strictEqual(Object.keys(event).length, 1)
t.assert.strictEqual(event.data, 'data')
t.assert.strictEqual(event.id, undefined)
t.assert.strictEqual(event.event, undefined)
t.assert.strictEqual(event.retry, undefined)
})
})
================================================
FILE: test/eventsource/eventsource-stream-process-event.js
================================================
'use strict'
const { test, describe } = require('node:test')
const { EventSourceStream } = require('../../lib/web/eventsource/eventsource-stream')
describe('EventSourceStream - processEvent', () => {
const defaultEventSourceSettings = {
origin: 'example.com',
reconnectionTime: 1000
}
test('Should set the defined origin as the origin of the MessageEvent', (t) => {
const stream = new EventSourceStream({
eventSourceSettings: {
...defaultEventSourceSettings
}
})
stream.on('data', (event) => {
t.assert.strictEqual(typeof event, 'object')
t.assert.strictEqual(event.type, 'message')
t.assert.strictEqual(event.options.data, null)
t.assert.strictEqual(event.options.lastEventId, undefined)
t.assert.strictEqual(event.options.origin, 'example.com')
t.assert.strictEqual(stream.state.reconnectionTime, 1000)
})
stream.on('error', (error) => {
t.assert.fail(error)
})
stream.processEvent({})
})
test('Should set reconnectionTime to 4000 if event contains retry field', (t) => {
const stream = new EventSourceStream({
eventSourceSettings: {
...defaultEventSourceSettings
}
})
stream.processEvent({
retry: '4000'
})
t.assert.strictEqual(stream.state.reconnectionTime, 4000)
})
test('Dispatches a MessageEvent with data', (t) => {
const stream = new EventSourceStream({
eventSourceSettings: {
...defaultEventSourceSettings
}
})
stream.on('data', (event) => {
t.assert.strictEqual(typeof event, 'object')
t.assert.strictEqual(event.type, 'message')
t.assert.strictEqual(event.options.data, 'Hello')
t.assert.strictEqual(event.options.lastEventId, undefined)
t.assert.strictEqual(event.options.origin, 'example.com')
t.assert.strictEqual(stream.state.reconnectionTime, 1000)
})
stream.on('error', (error) => {
t.assert.fail(error)
})
stream.processEvent({
data: 'Hello'
})
})
test('Dispatches a MessageEvent with lastEventId, when event contains id field', (t) => {
const stream = new EventSourceStream({
eventSourceSettings: {
...defaultEventSourceSettings
}
})
stream.on('data', (event) => {
t.assert.strictEqual(typeof event, 'object')
t.assert.strictEqual(event.type, 'message')
t.assert.strictEqual(event.options.data, null)
t.assert.strictEqual(event.options.lastEventId, '1234')
t.assert.strictEqual(event.options.origin, 'example.com')
t.assert.strictEqual(stream.state.reconnectionTime, 1000)
})
stream.processEvent({
id: '1234'
})
})
test('Dispatches a MessageEvent with lastEventId, reusing the persisted', (t) => {
// lastEventId
const stream = new EventSourceStream({
eventSourceSettings: {
...defaultEventSourceSettings,
lastEventId: '1234'
}
})
stream.on('data', (event) => {
t.assert.strictEqual(typeof event, 'object')
t.assert.strictEqual(event.type, 'message')
t.assert.strictEqual(event.options.data, null)
t.assert.strictEqual(event.options.lastEventId, '1234')
t.assert.strictEqual(event.options.origin, 'example.com')
t.assert.strictEqual(stream.state.reconnectionTime, 1000)
})
stream.processEvent({})
})
test('Dispatches a MessageEvent with type custom, when event contains type field', (t) => {
const stream = new EventSourceStream({
eventSourceSettings: {
...defaultEventSourceSettings
}
})
stream.on('data', (event) => {
t.assert.strictEqual(typeof event, 'object')
t.assert.strictEqual(event.type, 'custom')
t.assert.strictEqual(event.options.data, null)
t.assert.strictEqual(event.options.lastEventId, undefined)
t.assert.strictEqual(event.options.origin, 'example.com')
t.assert.strictEqual(stream.state.reconnectionTime, 1000)
})
stream.processEvent({
event: 'custom'
})
})
})
================================================
FILE: test/eventsource/eventsource-stream.js
================================================
'use strict'
const { test, describe } = require('node:test')
const { EventSourceStream } = require('../../lib/web/eventsource/eventsource-stream')
describe('EventSourceStream', () => {
test('ignore empty chunks', (t) => {
const stream = new EventSourceStream()
stream.processEvent = function (event) {
t.assert.fail()
}
stream.write(Buffer.alloc(0))
})
test('Simple event with data field.', (t) => {
const content = Buffer.from('data: Hello\n\n', 'utf8')
const stream = new EventSourceStream()
stream.processEvent = function (event) {
t.assert.strictEqual(typeof event, 'object')
t.assert.strictEqual(event.event, undefined)
t.assert.strictEqual(event.data, 'Hello')
t.assert.strictEqual(event.id, undefined)
t.assert.strictEqual(event.retry, undefined)
}
for (let i = 0; i < content.length; i++) {
stream.write(Buffer.from([content[i]]))
}
})
test('Should also process CR as EOL.', (t) => {
const content = Buffer.from('data: Hello\r\r', 'utf8')
const stream = new EventSourceStream()
stream.processEvent = function (event) {
t.assert.strictEqual(typeof event, 'object')
t.assert.strictEqual(event.event, undefined)
t.assert.strictEqual(event.data, 'Hello')
t.assert.strictEqual(event.id, undefined)
t.assert.strictEqual(event.retry, undefined)
}
for (let i = 0; i < content.length; i++) {
stream.write(Buffer.from([content[i]]))
}
})
test('Should also process CRLF as EOL.', (t) => {
const content = Buffer.from('data: Hello\r\n\r\n', 'utf8')
const stream = new EventSourceStream()
stream.processEvent = function (event) {
t.assert.strictEqual(typeof event, 'object')
t.assert.strictEqual(event.event, undefined)
t.assert.strictEqual(event.data, 'Hello')
t.assert.strictEqual(event.id, undefined)
t.assert.strictEqual(event.retry, undefined)
}
for (let i = 0; i < content.length; i++) {
stream.write(Buffer.from([content[i]]))
}
})
test('Should also process mixed CR and CRLF as EOL.', (t) => {
const content = Buffer.from('data: Hello\r\r\n', 'utf8')
const stream = new EventSourceStream()
stream.processEvent = function (event) {
t.assert.strictEqual(typeof event, 'object')
t.assert.strictEqual(event.event, undefined)
t.assert.strictEqual(event.data, 'Hello')
t.assert.strictEqual(event.id, undefined)
t.assert.strictEqual(event.retry, undefined)
}
for (let i = 0; i < content.length; i++) {
stream.write(Buffer.from([content[i]]))
}
})
test('Should also process mixed LF and CRLF as EOL.', (t) => {
const content = Buffer.from('data: Hello\n\r\n', 'utf8')
const stream = new EventSourceStream()
stream.processEvent = function (event) {
t.assert.strictEqual(typeof event, 'object')
t.assert.strictEqual(event.event, undefined)
t.assert.strictEqual(event.data, 'Hello')
t.assert.strictEqual(event.id, undefined)
t.assert.strictEqual(event.retry, undefined)
}
for (let i = 0; i < content.length; i++) {
stream.write(Buffer.from([content[i]]))
}
})
test('Should ignore comments', (t) => {
const content = Buffer.from(':data: Hello\n\n', 'utf8')
const stream = new EventSourceStream()
stream.processEvent = function (event) {
t.assert.strictEqual(typeof event, 'object')
t.assert.strictEqual(event.event, undefined)
t.assert.strictEqual(event.data, undefined)
t.assert.strictEqual(event.id, undefined)
t.assert.strictEqual(event.retry, undefined)
}
for (let i = 0; i < content.length; i++) {
stream.write(Buffer.from([content[i]]))
}
})
test('Should fire two events.', (t) => {
// @see https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation
const content = Buffer.from('data\n\ndata\ndata\n\ndata:', 'utf8')
const stream = new EventSourceStream()
let count = 0
stream.processEvent = function (event) {
switch (count) {
case 0: {
t.assert.strictEqual(typeof event, 'object')
t.assert.strictEqual(event.event, undefined)
t.assert.strictEqual(event.data, '')
t.assert.strictEqual(event.id, undefined)
t.assert.strictEqual(event.retry, undefined)
break
}
case 1: {
t.assert.strictEqual(typeof event, 'object')
t.assert.strictEqual(event.event, undefined)
t.assert.strictEqual(event.data, '\n')
t.assert.strictEqual(event.id, undefined)
t.assert.strictEqual(event.retry, undefined)
}
}
count++
}
for (let i = 0; i < content.length; i++) {
stream.write(Buffer.from([content[i]]))
}
})
test('Should fire two identical events.', (t) => {
// @see https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation
const content = Buffer.from('data:test\n\ndata: test\n\n', 'utf8')
const stream = new EventSourceStream()
stream.processEvent = function (event) {
t.assert.strictEqual(typeof event, 'object')
t.assert.strictEqual(event.event, undefined)
t.assert.strictEqual(event.data, 'test')
t.assert.strictEqual(event.id, undefined)
t.assert.strictEqual(event.retry, undefined)
}
for (let i = 0; i < content.length; i++) {
stream.write(Buffer.from([content[i]]))
}
})
test('ignores empty comments', (t) => {
const content = Buffer.from('data: Hello\n\n:\n\ndata: World\n\n', 'utf8')
const stream = new EventSourceStream()
let count = 0
stream.processEvent = function (event) {
switch (count) {
case 0: {
t.assert.strictEqual(typeof event, 'object')
t.assert.strictEqual(event.event, undefined)
t.assert.strictEqual(event.data, 'Hello')
t.assert.strictEqual(event.id, undefined)
t.assert.strictEqual(event.retry, undefined)
break
}
case 1: {
t.assert.strictEqual(typeof event, 'object')
t.assert.strictEqual(event.event, undefined)
t.assert.strictEqual(event.data, 'World')
t.assert.strictEqual(event.id, undefined)
t.assert.strictEqual(event.retry, undefined)
break
}
default: {
t.assert.fail()
}
}
count++
}
stream.write(content)
})
test('comment fest', (t) => {
const longstring = new Array(2 * 1024 + 1).join('x')
const content = Buffer.from(`data:1\r:\0\n:\r\ndata:2\n:${longstring}\rdata:3\n:data:fail\r:${longstring}\ndata:4\n\n`, 'utf8')
const stream = new EventSourceStream()
stream.processEvent = function (event) {
t.assert.strictEqual(typeof event, 'object')
t.assert.strictEqual(event.event, undefined)
t.assert.strictEqual(event.data, '1\n2\n3\n4')
t.assert.strictEqual(event.id, undefined)
t.assert.strictEqual(event.retry, undefined)
}
stream.write(content)
})
test('comment fest', (t) => {
const content = Buffer.from('data:\n\ndata\ndata\n\ndata:test\n\n', 'utf8')
const stream = new EventSourceStream()
let count = 0
stream.processEvent = function (event) {
t.assert.strictEqual(typeof event, 'object')
t.assert.strictEqual(event.event, undefined)
t.assert.strictEqual(event.id, undefined)
t.assert.strictEqual(event.retry, undefined)
switch (count) {
case 0: {
t.assert.strictEqual(event.data, '')
break
}
case 1: {
t.assert.strictEqual(event.data, '\n')
break
}
case 2: {
t.assert.strictEqual(event.data, 'test')
break
}
default: {
t.assert.fail()
}
}
count++
}
stream.write(content)
})
test('newline test', (t) => {
const content = Buffer.from('data:test\r\ndata\ndata:test\r\n\r\n', 'utf8')
const stream = new EventSourceStream()
stream.processEvent = function (event) {
t.assert.strictEqual(typeof event, 'object')
t.assert.strictEqual(event.event, undefined)
t.assert.strictEqual(event.id, undefined)
t.assert.strictEqual(event.retry, undefined)
t.assert.strictEqual(event.data, 'test\n\ntest')
}
stream.write(content)
})
test('newline test', (t) => {
const content = Buffer.from('data:test\n data\ndata\nfoobar:xxx\njustsometext\n:thisisacommentyay\ndata:test\n\n', 'utf8')
const stream = new EventSourceStream()
stream.processEvent = function (event) {
t.assert.strictEqual(typeof event, 'object')
t.assert.strictEqual(event.event, undefined)
t.assert.strictEqual(event.id, undefined)
t.assert.strictEqual(event.retry, undefined)
t.assert.strictEqual(event.data, 'test\n\ntest')
}
stream.write(content)
})
test('newline test', (t) => {
const content = Buffer.from('data:test\n data\ndata\nfoobar:xxx\njustsometext\n:thisisacommentyay\ndata:test\n\n', 'utf8')
const stream = new EventSourceStream()
stream.processEvent = function (event) {
t.assert.strictEqual(typeof event, 'object')
t.assert.strictEqual(event.event, undefined)
t.assert.strictEqual(event.id, undefined)
t.assert.strictEqual(event.retry, undefined)
t.assert.strictEqual(event.data, 'test\n\ntest')
}
stream.write(content)
})
})
================================================
FILE: test/eventsource/eventsource.js
================================================
'use strict'
const { test, describe } = require('node:test')
const { EventSource } = require('../../lib/web/eventsource/eventsource')
describe('EventSource - constructor', () => {
test('Not providing url argument should throw', (t) => {
t.assert.throws(() => new EventSource(), TypeError)
})
test('Throw DOMException if URL is invalid', (t) => {
t.assert.throws(() => new EventSource('http:'), { message: /Invalid URL/ })
})
})
================================================
FILE: test/eventsource/util.js
================================================
'use strict'
const { test } = require('node:test')
const { isASCIINumber, isValidLastEventId } = require('../../lib/web/eventsource/util')
test('isValidLastEventId', (t) => {
t.assert.strictEqual(isValidLastEventId('valid'), true)
t.assert.strictEqual(isValidLastEventId('in\u0000valid'), false)
t.assert.strictEqual(isValidLastEventId('in\x00valid'), false)
t.assert.strictEqual(isValidLastEventId('…'), true)
})
test('isASCIINumber', (t) => {
t.assert.strictEqual(isASCIINumber('123'), true)
t.assert.strictEqual(isASCIINumber(''), false)
t.assert.strictEqual(isASCIINumber('123a'), false)
})
================================================
FILE: test/examples.js
================================================
'use strict'
const { tspl } = require('@matteo.collina/tspl')
const { createServer } = require('node:http')
const { test, after } = require('node:test')
const { once } = require('node:events')
const examples = require('../docs/examples/request.js')
test('request examples', async (t) => {
t = tspl(t, { plan: 7 })
let lastReq
const exampleServer = createServer({ joinDuplicateHeaders: true }, (req, res) => {
lastReq = req
if (req.method === 'DELETE') {
res.statusCode = 204
return res.end()
} else if (req.method === 'POST') {
res.statusCode = 200
if (req.url === '/json') {
res.setHeader('content-type', 'application/json')
res.end('{"hello":"JSON Response"}')
} else {
res.end('hello=form')
}
} else {
res.statusCode = 200
res.end('hello')
}
})
const errorServer = createServer({ joinDuplicateHeaders: true }, (req, res) => {
lastReq = req
res.statusCode = 400
res.setHeader('content-type', 'application/json')
res.end('{"error":"an error"}')
})
after(() => exampleServer.close())
after(() => errorServer.close())
exampleServer.listen(0)
errorServer.listen(0)
await Promise.all([
once(exampleServer, 'listening'),
once(errorServer, 'listening')
])
await examples.getRequest(exampleServer.address().port)
t.strictEqual(lastReq.method, 'GET')
await examples.postJSONRequest(exampleServer.address().port)
t.strictEqual(lastReq.method, 'POST')
t.strictEqual(lastReq.headers['content-type'], 'application/json')
await examples.postFormRequest(exampleServer.address().port)
t.strictEqual(lastReq.method, 'POST')
t.strictEqual(lastReq.headers['content-type'], 'application/x-www-form-urlencoded')
await examples.deleteRequest(exampleServer.address().port)
t.strictEqual(lastReq.method, 'DELETE')
await examples.deleteRequest(errorServer.address().port)
t.strictEqual(lastReq.method, 'DELETE')
await t.completed
})
================================================
FILE: test/fetch/401-statuscode-no-infinite-loop.js
================================================
'use strict'
const { fetch } = require('../..')
const { createServer } = require('node:http')
const { once } = require('node:events')
const { test } = require('node:test')
const assert = require('node:assert')
const { closeServerAsPromise } = require('../utils/node-http')
test('Receiving a 401 status code should not cause infinite retry loop', async (t) => {
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.statusCode = 401
res.end('Unauthorized')
}).listen(0)
t.after(closeServerAsPromise(server))
await once(server, 'listening')
const response = await fetch(`http://localhost:${server.address().port}`)
assert.strictEqual(response.status, 401)
})
================================================
FILE: test/fetch/407-statuscode-window-null.js
================================================
'use strict'
const { fetch } = require('../..')
const { createServer } = require('node:http')
const { once } = require('node:events')
const { test } = require('node:test')
const { closeServerAsPromise } = require('../utils/node-http')
test('Receiving a 407 status code w/ a window option present should reject', async (t) => {
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.statusCode = 407
res.end()
}).listen(0)
t.after(closeServerAsPromise(server))
await once(server, 'listening')
// if init.window exists, the spec tells us to set request.window to 'no-window',
// which later causes the request to be rejected if the status code is 407
await t.assert.rejects(fetch(`http://localhost:${server.address().port}`, { window: null }))
})
================================================
FILE: test/fetch/abort.js
================================================
'use strict'
const { test } = require('node:test')
const { fetch } = require('../..')
const { createServer } = require('node:http')
const { once } = require('node:events')
const { closeServerAsPromise } = require('../utils/node-http')
test('allows aborting with custom errors', async (t) => {
const server = createServer({ joinDuplicateHeaders: true }).listen(0)
t.after(closeServerAsPromise(server))
await once(server, 'listening')
await t.test('Using AbortSignal.timeout with cause', async (t) => {
t.plan(2)
try {
await fetch(`http://localhost:${server.address().port}`, {
signal: AbortSignal.timeout(50)
})
t.assert.fail('should throw')
} catch (err) {
if (err.name === 'TypeError') {
const cause = err.cause
t.assert.strictEqual(cause.name, 'HeadersTimeoutError')
t.assert.strictEqual(cause.code, 'UND_ERR_HEADERS_TIMEOUT')
} else if (err.name === 'TimeoutError') {
t.assert.strictEqual(err.code, DOMException.TIMEOUT_ERR)
t.assert.strictEqual(err.cause, undefined)
} else {
throw err
}
}
})
t.test('Error defaults to an AbortError DOMException', async () => {
const ac = new AbortController()
ac.abort() // no reason
await t.assert.rejects(
fetch(`http://localhost:${server.address().port}`, {
signal: ac.signal
}),
{
name: 'AbortError',
code: DOMException.ABORT_ERR
}
)
})
})
================================================
FILE: test/fetch/abort2.js
================================================
'use strict'
const { test } = require('node:test')
const { fetch } = require('../..')
const { createServer } = require('node:http')
const { once } = require('node:events')
const { closeServerAsPromise } = require('../utils/node-http')
/* global AbortController */
test('parallel fetch with the same AbortController works as expected', async (t) => {
const body = {
fixes: 1389,
bug: 'Ensure request is not aborted before enqueueing bytes into stream.'
}
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.statusCode = 200
res.end(JSON.stringify(body))
})
t.after(closeServerAsPromise(server))
const abortController = new AbortController()
async function makeRequest () {
const result = await fetch(`http://localhost:${server.address().port}`, {
signal: abortController.signal
}).then(response => response.json())
abortController.abort()
return result
}
server.listen(0)
await once(server, 'listening')
const requests = Array.from({ length: 10 }, makeRequest)
const result = await Promise.allSettled(requests)
// since the requests are running parallel, any of them could resolve first.
// therefore we cannot rely on the order of the requests sent.
const { resolved, rejected } = result.reduce((a, b) => {
if (b.status === 'rejected') {
a.rejected.push(b)
} else {
a.resolved.push(b)
}
return a
}, { resolved: [], rejected: [] })
t.assert.strictEqual(rejected.length, 9) // out of 10 requests, only 1 should succeed
t.assert.strictEqual(resolved.length, 1)
t.assert.ok(rejected.every(rej => rej.reason?.code === DOMException.ABORT_ERR))
t.assert.deepStrictEqual(resolved[0].value, body)
})
================================================
FILE: test/fetch/about-uri.js
================================================
'use strict'
const { test } = require('node:test')
const { fetch } = require('../..')
test('fetching about: uris', async (t) => {
await t.test('about:blank', async () => {
await t.assert.rejects(fetch('about:blank'))
})
await t.test('All other about: urls should return an error', async () => {
try {
await fetch('about:config')
t.assert.fail('fetching about:config should fail')
} catch (e) {
t.assert.ok(e, 'this error was expected')
}
})
})
================================================
FILE: test/fetch/blob-uri.js
================================================
'use strict'
const { test } = require('node:test')
const { fetch } = require('../..')
test('fetching blob: uris', async (t) => {
const blobContents = 'hello world'
/** @type {Blob} */
let blob
/** @type {string} */
let objectURL
t.beforeEach(() => {
blob = new Blob([blobContents])
objectURL = URL.createObjectURL(blob)
})
await t.test('a normal fetch request works', async () => {
const res = await fetch(objectURL)
t.assert.strictEqual(blobContents, await res.text())
t.assert.strictEqual(blob.type, res.headers.get('Content-Type'))
t.assert.strictEqual(`${blob.size}`, res.headers.get('Content-Length'))
})
await t.test('a range fetch request returns the inclusive byte range', async () => {
const res = await fetch(objectURL, {
headers: {
Range: 'bytes=1-3'
}
})
t.assert.strictEqual(res.status, 206)
t.assert.strictEqual(await res.text(), blobContents.slice(1, 4))
t.assert.strictEqual(res.headers.get('Content-Length'), '3')
t.assert.strictEqual(res.headers.get('Content-Range'), `bytes 1-3/${blob.size}`)
})
await t.test('non-GET method to blob: fails', async () => {
try {
await fetch(objectURL, {
method: 'POST'
})
t.assert.fail('expected POST to blob: uri to fail')
} catch (e) {
t.assert.ok(e, 'Got the expected error')
}
})
// https://github.com/web-platform-tests/wpt/blob/7b0ebaccc62b566a1965396e5be7bb2bc06f841f/FileAPI/url/resources/fetch-tests.js#L36-L41
await t.test('fetching revoked URL should fail', async () => {
URL.revokeObjectURL(objectURL)
try {
await fetch(objectURL)
t.assert.fail('expected revoked blob: url to fail')
} catch (e) {
t.assert.ok(e, 'Got the expected error')
}
})
// https://github.com/web-platform-tests/wpt/blob/7b0ebaccc62b566a1965396e5be7bb2bc06f841f/FileAPI/url/resources/fetch-tests.js#L28-L34
await t.test('works with a fragment', async () => {
const res = await fetch(objectURL + '#fragment')
t.assert.strictEqual(blobContents, await res.text())
})
// https://github.com/web-platform-tests/wpt/blob/7b0ebaccc62b566a1965396e5be7bb2bc06f841f/FileAPI/url/resources/fetch-tests.js#L52-L56
await t.test('Appending a query string to blob: url should cause fetch to fail', async () => {
try {
await fetch(objectURL + '?querystring')
t.assert.fail('expected ?querystring blob: url to fail')
} catch (e) {
t.assert.ok(e, 'Got the expected error')
}
})
// https://github.com/web-platform-tests/wpt/blob/7b0ebaccc62b566a1965396e5be7bb2bc06f841f/FileAPI/url/resources/fetch-tests.js#L58-L62
await t.test('Appending a path should cause fetch to fail', async () => {
try {
await fetch(objectURL + '/path')
t.assert.fail('expected /path blob: url to fail')
} catch (e) {
t.assert.ok(e, 'Got the expected error')
}
})
// https://github.com/web-platform-tests/wpt/blob/7b0ebaccc62b566a1965396e5be7bb2bc06f841f/FileAPI/url/resources/fetch-tests.js#L64-L70
await t.test('these http methods should fail', async () => {
for (const method of ['HEAD', 'POST', 'DELETE', 'OPTIONS', 'PUT', 'CUSTOM']) {
try {
await fetch(objectURL, { method })
t.assert.fail(`${method} fetch should have failed`)
} catch (e) {
t.assert.ok(e, `${method} blob url - test succeeded`)
}
}
})
})
================================================
FILE: test/fetch/bundle.js
================================================
'use strict'
const { test } = require('node:test')
const { Response, Request, FormData, Headers, MessageEvent, CloseEvent, ErrorEvent } = require('../../undici-fetch')
test('bundle sets constructor.name and .name properly', (t) => {
t.assert.strictEqual(new Response().constructor.name, 'Response')
t.assert.strictEqual(Response.name, 'Response')
t.assert.strictEqual(new Request('http://a').constructor.name, 'Request')
t.assert.strictEqual(Request.name, 'Request')
t.assert.strictEqual(new Headers().constructor.name, 'Headers')
t.assert.strictEqual(Headers.name, 'Headers')
t.assert.strictEqual(new FormData().constructor.name, 'FormData')
t.assert.strictEqual(FormData.name, 'FormData')
})
test('regression test for https://github.com/nodejs/node/issues/50263', (t) => {
const request = new Request('https://a', {
headers: {
test: 'abc'
},
method: 'POST'
})
const request1 = new Request(request, { body: 'does not matter' })
t.assert.strictEqual(request1.headers.get('test'), 'abc')
})
test('WebSocket related events are exported', (t) => {
t.assert.deepStrictEqual(typeof CloseEvent, 'function')
t.assert.deepStrictEqual(typeof MessageEvent, 'function')
t.assert.deepStrictEqual(typeof ErrorEvent, 'function')
})
================================================
FILE: test/fetch/client-error-stack-trace.js
================================================
'use strict'
const { test, after } = require('node:test')
const { sep, basename, join } = require('node:path')
const { fetch, setGlobalDispatcher, getGlobalDispatcher, Agent } = require('../..')
const projectFolder = basename(join(__dirname, '..', '..'))
const { fetch: fetchIndex } = require('../../index-fetch')
const previousDispatcher = getGlobalDispatcher()
setGlobalDispatcher(new Agent({
headersTimeout: 500,
connectTimeout: 500
}))
after(() => {
setGlobalDispatcher(previousDispatcher)
})
test('FETCH: request errors and prints trimmed stack trace', async (t) => {
try {
await fetch('http://a.com')
} catch (error) {
const stackLines = error.stack.split('\n')
t.assert.ok(stackLines[0].includes('TypeError: fetch failed'))
t.assert.ok(stackLines.some(line => line.includes(`lib${sep}web${sep}fetch${sep}index.js`)))
t.assert.ok(stackLines.some(line => line.includes(`${projectFolder}${sep}index.js`)))
t.assert.ok(stackLines.some(line => line.includes(__filename)))
}
})
test('FETCH-index: request errors and prints trimmed stack trace', async (t) => {
try {
await fetchIndex('http://a.com')
} catch (error) {
const stackLines = error.stack.split('\n')
t.assert.ok(stackLines[0].includes('TypeError: fetch failed'))
t.assert.ok(stackLines.some(line => line.includes(`lib${sep}web${sep}fetch${sep}index.js`)))
t.assert.ok(stackLines.some(line => line.includes(`${projectFolder}${sep}index-fetch.js`)))
t.assert.ok(stackLines.some(line => line.includes(__filename)))
}
})
================================================
FILE: test/fetch/client-fetch.js
================================================
/* globals AbortController */
'use strict'
const { test, after } = require('node:test')
const { createServer } = require('node:http')
const { fetch, Response, Request, FormData } = require('../..')
const { Client, setGlobalDispatcher, getGlobalDispatcher, Agent } = require('../..')
const nodeFetch = require('../../index-fetch')
const { once } = require('node:events')
const { gzipSync } = require('node:zlib')
const { promisify } = require('node:util')
const { randomFillSync, createHash } = require('node:crypto')
const { closeServerAsPromise } = require('../utils/node-http')
const previousDispatcher = getGlobalDispatcher()
setGlobalDispatcher(new Agent({
keepAliveTimeout: 1,
keepAliveMaxTimeout: 1
}))
after(() => {
setGlobalDispatcher(previousDispatcher)
})
test('function signature', (t) => {
t.plan(2)
t.assert.strictEqual(fetch.name, 'fetch')
t.assert.strictEqual(fetch.length, 1)
})
test('args validation', async (t) => {
t.plan(2)
await t.assert.rejects(fetch(), TypeError)
await t.assert.rejects(fetch('ftp://unsupported'), TypeError)
})
test('request json', (t, done) => {
t.plan(1)
const obj = { asd: true }
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.end(JSON.stringify(obj))
})
t.after(closeServerAsPromise(server))
server.listen(0, async () => {
const body = await fetch(`http://localhost:${server.address().port}`)
t.assert.deepStrictEqual(obj, await body.json())
done()
})
})
test('request text', (t, done) => {
t.plan(1)
const obj = { asd: true }
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.end(JSON.stringify(obj))
})
t.after(closeServerAsPromise(server))
server.listen(0, async () => {
const body = await fetch(`http://localhost:${server.address().port}`)
t.assert.strictEqual(JSON.stringify(obj), await body.text())
done()
})
})
test('request arrayBuffer', (t, done) => {
t.plan(1)
const obj = { asd: true }
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.end(JSON.stringify(obj))
})
t.after(closeServerAsPromise(server))
server.listen(0, async () => {
const body = await fetch(`http://localhost:${server.address().port}`)
t.assert.deepStrictEqual(Buffer.from(JSON.stringify(obj)), Buffer.from(await body.arrayBuffer()))
done()
})
})
test('should set type of blob object to the value of the `Content-Type` header from response', (t, done) => {
t.plan(1)
const obj = { asd: true }
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.setHeader('Content-Type', 'application/json')
res.end(JSON.stringify(obj))
})
t.after(closeServerAsPromise(server))
server.listen(0, async () => {
const response = await fetch(`http://localhost:${server.address().port}`)
t.assert.strictEqual('application/json', (await response.blob()).type)
done()
})
})
test('pre aborted with readable request body', (t, done) => {
t.plan(2)
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
})
t.after(closeServerAsPromise(server))
server.listen(0, async () => {
const ac = new AbortController()
ac.abort()
await fetch(`http://localhost:${server.address().port}`, {
signal: ac.signal,
method: 'POST',
body: new ReadableStream({
async cancel (reason) {
t.assert.strictEqual(reason.name, 'AbortError')
}
}),
duplex: 'half'
}).catch(err => {
t.assert.strictEqual(err.name, 'AbortError')
}).finally(done)
})
})
test('pre aborted with closed readable request body', (t, done) => {
t.plan(2)
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
})
t.after(closeServerAsPromise(server))
server.listen(0, async () => {
const ac = new AbortController()
ac.abort()
const body = new ReadableStream({
async start (c) {
t.assert.ok(true)
c.close()
},
async cancel (reason) {
t.assert.fail()
}
})
queueMicrotask(() => {
fetch(`http://localhost:${server.address().port}`, {
signal: ac.signal,
method: 'POST',
body,
duplex: 'half'
}).catch(err => {
t.assert.strictEqual(err.name, 'AbortError')
}).finally(done)
})
})
})
test('unsupported formData 1', (t, done) => {
t.plan(1)
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.setHeader('content-type', 'asdasdsad')
res.end()
})
t.after(closeServerAsPromise(server))
server.listen(0, () => {
fetch(`http://localhost:${server.address().port}`)
.then(res => res.formData())
.catch(err => {
t.assert.strictEqual(err.name, 'TypeError')
})
.finally(done)
})
})
test('multipart formdata not base64', async (t) => {
t.plan(2)
// Construct example form data, with text and blob fields
const formData = new FormData()
formData.append('field1', 'value1')
const blob = new Blob(['example\ntext file'], { type: 'text/plain' })
formData.append('field2', blob, 'file.txt')
const tempRes = new Response(formData)
const boundary = tempRes.headers.get('content-type').split('boundary=')[1]
const formRaw = await tempRes.text()
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.setHeader('content-type', 'multipart/form-data; boundary=' + boundary)
res.write(formRaw)
res.end()
})
t.after(closeServerAsPromise(server))
const listen = promisify(server.listen.bind(server))
await listen(0)
const res = await fetch(`http://localhost:${server.address().port}`)
const form = await res.formData()
t.assert.strictEqual(form.get('field1'), 'value1')
const text = await form.get('field2').text()
t.assert.strictEqual(text, 'example\ntext file')
})
test('multipart formdata base64', (t, done) => {
t.plan(1)
// Example form data with base64 encoding
const data = randomFillSync(Buffer.alloc(256))
const formRaw =
'------formdata-undici-0.5786922755719377\r\n' +
'Content-Disposition: form-data; name="file"; filename="test.txt"\r\n' +
'Content-Type: application/octet-stream\r\n' +
'Content-Transfer-Encoding: base64\r\n' +
'\r\n' +
data.toString('base64') +
'\r\n' +
'------formdata-undici-0.5786922755719377--'
const server = createServer({ joinDuplicateHeaders: true }, async (req, res) => {
res.setHeader('content-type', 'multipart/form-data; boundary=----formdata-undici-0.5786922755719377')
for (let offset = 0; offset < formRaw.length;) {
res.write(formRaw.slice(offset, offset += 2))
await new Promise(resolve => setTimeout(resolve))
}
res.end()
})
t.after(closeServerAsPromise(server))
server.listen(0, () => {
fetch(`http://localhost:${server.address().port}`)
.then(res => res.formData())
.then(form => form.get('file').arrayBuffer())
.then(buffer => createHash('sha256').update(Buffer.from(buffer)).digest('base64'))
.then(digest => {
t.assert.strictEqual(createHash('sha256').update(data).digest('base64'), digest)
})
.finally(done)
})
})
test('multipart fromdata non-ascii filed names', async (t) => {
t.plan(1)
const request = new Request('http://localhost', {
method: 'POST',
headers: {
'Content-Type': 'multipart/form-data; boundary=----formdata-undici-0.6204674738279623'
},
body:
'------formdata-undici-0.6204674738279623\r\n' +
'Content-Disposition: form-data; name="fiŝo"\r\n' +
'\r\n' +
'value1\r\n' +
'------formdata-undici-0.6204674738279623--'
})
const form = await request.formData()
t.assert.strictEqual(form.get('fiŝo'), 'value1')
})
test('busboy emit error', async (t) => {
t.plan(1)
const formData = new FormData()
formData.append('field1', 'value1')
const tempRes = new Response(formData)
const formRaw = await tempRes.text()
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.setHeader('content-type', 'multipart/form-data; boundary=wrongboundary')
res.write(formRaw)
res.end()
})
t.after(closeServerAsPromise(server))
const listen = promisify(server.listen.bind(server))
await listen(0)
const res = await fetch(`http://localhost:${server.address().port}`)
await t.assert.rejects(res.formData(), 'Unexpected end of multipart data')
})
// https://github.com/nodejs/undici/issues/2244
test('parsing formData preserve full path on files', async (t) => {
t.plan(1)
const formData = new FormData()
formData.append('field1', new File(['foo'], 'a/b/c/foo.txt'))
const tempRes = new Response(formData)
const form = await tempRes.formData()
t.assert.strictEqual(form.get('field1').name, 'a/b/c/foo.txt')
})
test('urlencoded formData', (t, done) => {
t.plan(2)
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.setHeader('content-type', 'application/x-www-form-urlencoded')
res.end('field1=value1&field2=value2')
})
t.after(closeServerAsPromise(server))
server.listen(0, () => {
fetch(`http://localhost:${server.address().port}`)
.then(res => res.formData())
.then(formData => {
t.assert.strictEqual(formData.get('field1'), 'value1')
t.assert.strictEqual(formData.get('field2'), 'value2')
})
.finally(done)
})
})
test('text with BOM', (t, done) => {
t.plan(1)
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.setHeader('content-type', 'application/x-www-form-urlencoded')
res.end('\uFEFFtest=\uFEFF')
})
t.after(closeServerAsPromise(server))
server.listen(0, () => {
fetch(`http://localhost:${server.address().port}`)
.then(res => res.text())
.then(text => {
t.assert.strictEqual(text, 'test=\uFEFF')
})
.finally(done)
})
})
test('formData with BOM', (t, done) => {
t.plan(1)
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.setHeader('content-type', 'application/x-www-form-urlencoded')
res.end('\uFEFFtest=\uFEFF')
})
t.after(closeServerAsPromise(server))
server.listen(0, () => {
fetch(`http://localhost:${server.address().port}`)
.then(res => res.formData())
.then(formData => {
t.assert.strictEqual(formData.get('\uFEFFtest'), '\uFEFF')
})
.finally(done)
})
})
test('locked blob body', (t, done) => {
t.plan(1)
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.end()
})
t.after(closeServerAsPromise(server))
server.listen(0, async () => {
const res = await fetch(`http://localhost:${server.address().port}`)
const reader = res.body.getReader()
res.blob().catch(err => {
t.assert.strictEqual(err.message, 'Body is unusable: Body has already been read')
reader.cancel()
}).finally(done)
})
})
test('disturbed blob body', (t, done) => {
t.plan(2)
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.end()
})
t.after(closeServerAsPromise(server))
server.listen(0, async () => {
const res = await fetch(`http://localhost:${server.address().port}`)
await res.blob().then(() => {
t.assert.ok(true)
})
await res.blob().catch(err => {
t.assert.strictEqual(err.message, 'Body is unusable: Body has already been read')
})
done()
})
})
test('redirect with body', (t, done) => {
t.plan(3)
let count = 0
const server = createServer({ joinDuplicateHeaders: true }, async (req, res) => {
let body = ''
for await (const chunk of req) {
body += chunk
}
t.assert.strictEqual(body, 'asd')
if (count++ === 0) {
res.setHeader('location', 'asd')
res.statusCode = 302
res.end()
} else {
res.end(String(count))
}
})
t.after(closeServerAsPromise(server))
server.listen(0, async () => {
const res = await fetch(`http://localhost:${server.address().port}`, {
method: 'PUT',
body: 'asd'
})
t.assert.strictEqual(await res.text(), '2')
done()
})
})
test('redirect with stream', (t, done) => {
t.plan(3)
const location = '/asd'
const body = 'hello!'
const server = createServer({ joinDuplicateHeaders: true }, async (req, res) => {
res.writeHead(302, { location })
let count = 0
const l = setInterval(() => {
res.write(body[count++])
if (count === body.length) {
res.end()
clearInterval(l)
}
}, 50)
})
t.after(closeServerAsPromise(server))
server.listen(0, async () => {
const res = await fetch(`http://localhost:${server.address().port}`, {
redirect: 'manual'
})
t.assert.strictEqual(res.status, 302)
t.assert.strictEqual(res.headers.get('location'), location)
t.assert.strictEqual(await res.text(), body)
done()
})
})
test('fail to extract locked body', (t) => {
t.plan(1)
const stream = new ReadableStream({})
const reader = stream.getReader()
try {
// eslint-disable-next-line
new Response(stream)
} catch (err) {
t.assert.strictEqual(err.name, 'TypeError')
}
reader.cancel()
})
test('fail to extract locked body', (t) => {
t.plan(1)
const stream = new ReadableStream({})
const reader = stream.getReader()
try {
// eslint-disable-next-line
new Request('http://asd', {
method: 'PUT',
body: stream,
keepalive: true
})
} catch (err) {
t.assert.strictEqual(err.message, 'keepalive')
}
reader.cancel()
})
test('post FormData with Blob', (t, done) => {
t.plan(1)
const body = new FormData()
body.append('field1', new Blob(['asd1']))
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
req.pipe(res)
})
t.after(closeServerAsPromise(server))
server.listen(0, async () => {
const res = await fetch(`http://localhost:${server.address().port}`, {
method: 'PUT',
body
})
t.assert.ok(/asd1/.test(await res.text()))
done()
})
})
test('post FormData with File', (t, done) => {
t.plan(2)
const body = new FormData()
body.append('field1', new File(['asd1'], 'filename123'))
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
req.pipe(res)
})
t.after(closeServerAsPromise(server))
server.listen(0, async () => {
const res = await fetch(`http://localhost:${server.address().port}`, {
method: 'PUT',
body
})
const result = await res.text()
t.assert.ok(/asd1/.test(result))
t.assert.ok(/filename123/.test(result))
done()
})
})
test('invalid url', async (t) => {
t.plan(1)
try {
await fetch('http://invalid')
} catch (e) {
t.assert.match(e.cause.message, /invalid/)
}
})
test('custom agent', (t, done) => {
t.plan(2)
const obj = { asd: true }
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.end(JSON.stringify(obj))
})
t.after(closeServerAsPromise(server))
server.listen(0, async () => {
const dispatcher = new Client('http://localhost:' + server.address().port, {
keepAliveTimeout: 1,
keepAliveMaxTimeout: 1
})
const oldDispatch = dispatcher.dispatch
dispatcher.dispatch = function (options, handler) {
t.assert.ok(true)
return oldDispatch.call(this, options, handler)
}
const body = await fetch(`http://localhost:${server.address().port}`, {
dispatcher
})
t.assert.deepStrictEqual(obj, await body.json())
done()
})
})
test('custom agent node fetch', (t, done) => {
t.plan(2)
const obj = { asd: true }
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.end(JSON.stringify(obj))
})
t.after(closeServerAsPromise(server))
server.listen(0, async () => {
const dispatcher = new Client('http://localhost:' + server.address().port, {
keepAliveTimeout: 1,
keepAliveMaxTimeout: 1
})
const oldDispatch = dispatcher.dispatch
dispatcher.dispatch = function (options, handler) {
t.assert.ok(true)
return oldDispatch.call(this, options, handler)
}
const body = await nodeFetch.fetch(`http://localhost:${server.address().port}`, {
dispatcher
})
t.assert.deepStrictEqual(obj, await body.json())
done()
})
})
test('error on redirect', (t, done) => {
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.statusCode = 302
res.end()
})
t.after(closeServerAsPromise(server))
server.listen(0, async () => {
const errorCause = await fetch(`http://localhost:${server.address().port}`, {
redirect: 'error'
}).catch((e) => e.cause)
t.assert.strictEqual(errorCause.message, 'unexpected redirect')
done()
})
})
// https://github.com/nodejs/undici/issues/1527
test('fetching with Request object - issue #1527', async (t) => {
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
t.assert.ok(true)
res.end()
}).listen(0)
t.after(closeServerAsPromise(server))
await once(server, 'listening')
const body = JSON.stringify({ foo: 'bar' })
const request = new Request(`http://localhost:${server.address().port}`, {
method: 'POST',
body
})
await t.assert.doesNotReject(fetch(request))
})
test('do not decode redirect body', (t, done) => {
t.plan(3)
const obj = { asd: true }
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
if (req.url === '/resource') {
t.assert.ok(true)
res.statusCode = 301
res.setHeader('location', '/resource/')
// Some dumb http servers set the content-encoding gzip
// even if there is no response
res.setHeader('content-encoding', 'gzip')
res.end()
return
}
t.assert.ok(true)
res.setHeader('content-encoding', 'gzip')
res.end(gzipSync(JSON.stringify(obj)))
})
t.after(closeServerAsPromise(server))
server.listen(0, async () => {
const body = await fetch(`http://localhost:${server.address().port}/resource`)
t.assert.strictEqual(JSON.stringify(obj), await body.text())
done()
})
})
test('decode non-redirect body with location header', (t, done) => {
t.plan(2)
const obj = { asd: true }
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
t.assert.ok(true)
res.statusCode = 201
res.setHeader('location', '/resource/')
res.setHeader('content-encoding', 'gzip')
res.end(gzipSync(JSON.stringify(obj)))
})
t.after(closeServerAsPromise(server))
server.listen(0, async () => {
const body = await fetch(`http://localhost:${server.address().port}/resource`)
t.assert.strictEqual(JSON.stringify(obj), await body.text())
done()
})
})
test('Receiving non-Latin1 headers', async (t) => {
const ContentDisposition = [
'inline; filename=rock&roll.png',
'inline; filename="rock\'n\'roll.png"',
'inline; filename="image â\x80\x94 copy (1).png"; filename*=UTF-8\'\'image%20%E2%80%94%20copy%20(1).png',
'inline; filename="_å\x9C\x96ç\x89\x87_ð\x9F\x96¼_image_.png"; filename*=UTF-8\'\'_%E5%9C%96%E7%89%87_%F0%9F%96%BC_image_.png',
'inline; filename="100 % loading&perf.png"; filename*=UTF-8\'\'100%20%25%20loading%26perf.png'
]
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
for (let i = 0; i < ContentDisposition.length; i++) {
res.setHeader(`Content-Disposition-${i + 1}`, ContentDisposition[i])
}
res.end()
}).listen(0)
t.after(closeServerAsPromise(server))
await once(server, 'listening')
const url = `http://localhost:${server.address().port}`
const response = await fetch(url, { method: 'HEAD' })
const cdHeaders = [...response.headers]
.filter(([k]) => k.startsWith('content-disposition'))
.map(([, v]) => v)
const lengths = cdHeaders.map(h => h.length)
t.assert.deepStrictEqual(cdHeaders, ContentDisposition)
t.assert.deepStrictEqual(lengths, [30, 34, 94, 104, 90])
})
================================================
FILE: test/fetch/client-node-max-header-size.js
================================================
'use strict'
const { exec } = require('node:child_process')
const { once } = require('node:events')
const { createServer } = require('node:http')
const { test, describe, before, after } = require('node:test')
describe('fetch respects --max-http-header-size', () => {
let server
before(async () => {
server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.writeHead(200, 'OK', {
'Content-Length': 2
})
res.write('OK')
res.end()
}).listen(0)
await once(server, 'listening')
})
after(() => {
server.closeAllConnections?.()
server.close()
})
test("respect Node.js' --max-http-header-size", (t, done) => {
t.plan(6)
const command = 'node -e "require(\'./undici-fetch.js\').fetch(\'http://localhost:' + server.address().port + '\')"'
exec(`${command} --max-http-header-size=1`, { stdio: 'pipe' }, (err, stdout, stderr) => {
t.assert.strictEqual(err.code, 1)
t.assert.strictEqual(stdout, '')
t.assert.match(stderr, /UND_ERR_HEADERS_OVERFLOW/, '--max-http-header-size=1 should throw')
exec(command, { stdio: 'pipe' }, (err, stdout, stderr) => {
t.assert.ifError(err)
t.assert.strictEqual(stdout, '')
t.assert.strictEqual(stderr, '', 'default max-http-header-size should not throw')
done()
})
})
})
})
================================================
FILE: test/fetch/content-length.js
================================================
'use strict'
const { test } = require('node:test')
const { createServer } = require('node:http')
const { once } = require('node:events')
const { fetch, FormData } = require('../..')
const { closeServerAsPromise } = require('../utils/node-http')
// https://github.com/nodejs/undici/issues/1783
test('Content-Length is set when using a FormData body with fetch', async (t) => {
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
// TODO: check the length's value once the boundary has a fixed length
t.assert.ok('content-length' in req.headers) // request has content-length header
t.assert.ok(!Number.isNaN(Number(req.headers['content-length'])))
res.end()
}).listen(0)
await once(server, 'listening')
t.after(closeServerAsPromise(server))
const fd = new FormData()
fd.set('file', new Blob(['hello world 👋'], { type: 'text/plain' }), 'readme.md')
fd.set('string', 'some string value')
await fetch(`http://localhost:${server.address().port}`, {
method: 'POST',
body: fd
})
})
================================================
FILE: test/fetch/cookies.js
================================================
'use strict'
const { once } = require('node:events')
const { createServer } = require('node:http')
const { test, describe, before, after } = require('node:test')
const { stringify: qsStringify } = require('node:querystring')
const { Client, fetch, Headers } = require('../..')
const pem = require('@metcoder95/https-pem')
const { createSecureServer } = require('node:http2')
describe('cookies', () => {
let server
before(() => {
server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
const searchParams = new URL(req.url, 'http://localhost').searchParams
if (searchParams.has('set-cookie')) {
res.setHeader('set-cookie', searchParams.get('set-cookie'))
}
res.end(req.headers.cookie)
})
return once(server.listen(0), 'listening')
})
after(() => {
server.close()
return once(server, 'close')
})
test('Can receive set-cookie headers from a server using fetch - issue #1262', async (t) => {
const query = qsStringify({
'set-cookie': 'name=value; Domain=example.com'
})
const response = await fetch(`http://localhost:${server.address().port}?${query}`)
t.assert.strictEqual(response.headers.get('set-cookie'), 'name=value; Domain=example.com')
t.assert.strictEqual(await response.text(), '')
const response2 = await fetch(`http://localhost:${server.address().port}?${query}`, {
credentials: 'include'
})
t.assert.strictEqual(response2.headers.get('set-cookie'), 'name=value; Domain=example.com')
t.assert.strictEqual(await response2.text(), '')
})
test('Can send cookies to a server with fetch - issue #1463', async (t) => {
const headersInit = [
new Headers([['cookie', 'value']]),
{ cookie: 'value' },
[['cookie', 'value']]
]
for (const headers of headersInit) {
const response = await fetch(`http://localhost:${server.address().port}`, { headers })
const text = await response.text()
t.assert.strictEqual(text, 'value')
}
})
test('Cookie header is delimited with a semicolon rather than a comma - issue #1905', async (t) => {
const response = await fetch(`http://localhost:${server.address().port}`, {
headers: [
['cookie', 'FOO=lorem-ipsum-dolor-sit-amet'],
['cookie', 'BAR=the-quick-brown-fox']
]
})
t.assert.strictEqual(await response.text(), 'FOO=lorem-ipsum-dolor-sit-amet; BAR=the-quick-brown-fox')
})
test('Can receive set-cookie headers from a http2 server using fetch - issue #2885', async (t) => {
const server = createSecureServer(pem)
server.on('stream', (stream, headers) => {
stream.respond({
'content-type': 'text/plain; charset=utf-8',
'x-method': headers[':method'],
'set-cookie': 'Space=Cat; Secure; HttpOnly',
':status': 200
})
stream.end('test')
})
await once(server.listen(0), 'listening')
const client = new Client(`https://localhost:${server.address().port}`, {
connect: {
rejectUnauthorized: false
},
allowH2: true
})
const response = await fetch(
`https://localhost:${server.address().port}/`,
// Needs to be passed to disable the reject unauthorized
{
method: 'GET',
dispatcher: client,
headers: {
'content-type': 'text-plain'
}
}
)
t.assert.deepStrictEqual(response.headers.getSetCookie(), ['Space=Cat; Secure; HttpOnly'])
t.assert.strictEqual(await response.text(), 'test')
await client.close()
await new Promise((resolve, reject) => server.close(err => err ? reject(err) : resolve()))
})
})
================================================
FILE: test/fetch/data-uri.js
================================================
'use strict'
const { test } = require('node:test')
const {
URLSerializer,
stringPercentDecode,
parseMIMEType,
collectAnHTTPQuotedString
} = require('../../lib/web/fetch/data-url')
const { fetch } = require('../..')
test('https://url.spec.whatwg.org/#concept-url-serializer', async (t) => {
await t.test('url scheme gets appended', (t) => {
const url = new URL('https://www.google.com/')
const serialized = URLSerializer(url)
t.assert.ok(serialized.startsWith(url.protocol))
})
await t.test('non-null url host with authentication', (t) => {
const url = new URL('https://username:password@google.com')
const serialized = URLSerializer(url)
t.assert.ok(serialized.includes(`//${url.username}:${url.password}`))
t.assert.ok(serialized.endsWith('@google.com/'))
})
await t.test('null url host', (t) => {
for (const url of ['web+demo:/.//not-a-host/', 'web+demo:/path/..//not-a-host/']) {
t.assert.strictEqual(
URLSerializer(new URL(url)),
'web+demo:/.//not-a-host/'
)
}
})
await t.test('url with query works', (t) => {
t.assert.strictEqual(
URLSerializer(new URL('https://www.google.com/?fetch=undici')),
'https://www.google.com/?fetch=undici'
)
})
await t.test('exclude fragment', (t) => {
t.assert.strictEqual(
URLSerializer(new URL('https://www.google.com/#frag')),
'https://www.google.com/#frag'
)
t.assert.strictEqual(
URLSerializer(new URL('https://www.google.com/#frag'), true),
'https://www.google.com/'
)
})
})
test('https://url.spec.whatwg.org/#string-percent-decode', async (t) => {
await t.test('encodes %{2} in range properly', (t) => {
const input = '%FF'
const percentDecoded = stringPercentDecode(input)
t.assert.deepStrictEqual(percentDecoded, new Uint8Array([255]))
})
await t.test('encodes %{2} not in range properly', (t) => {
const input = 'Hello %XD World'
const percentDecoded = stringPercentDecode(input)
const expected = [...input].map(c => c.charCodeAt(0))
t.assert.deepStrictEqual(percentDecoded, new Uint8Array(expected))
})
await t.test('normal string works', (t) => {
const input = 'Hello world'
const percentDecoded = stringPercentDecode(input)
const expected = [...input].map(c => c.charCodeAt(0))
t.assert.deepStrictEqual(percentDecoded, Uint8Array.from(expected))
})
})
test('https://mimesniff.spec.whatwg.org/#parse-a-mime-type', (t) => {
t.assert.deepStrictEqual(parseMIMEType('text/plain'), {
type: 'text',
subtype: 'plain',
parameters: new Map(),
essence: 'text/plain'
})
t.assert.deepStrictEqual(parseMIMEType('text/html;charset="shift_jis"iso-2022-jp'), {
type: 'text',
subtype: 'html',
parameters: new Map([['charset', 'shift_jis']]),
essence: 'text/html'
})
t.assert.deepStrictEqual(parseMIMEType('application/javascript'), {
type: 'application',
subtype: 'javascript',
parameters: new Map(),
essence: 'application/javascript'
})
})
test('https://fetch.spec.whatwg.org/#collect-an-http-quoted-string', async (t) => {
// https://fetch.spec.whatwg.org/#example-http-quoted-string
await t.test('first', (t) => {
const position = { position: 0 }
t.assert.strictEqual(collectAnHTTPQuotedString('"\\', {
position: 0
}), '"\\')
t.assert.strictEqual(collectAnHTTPQuotedString('"\\', position, true), '\\')
t.assert.strictEqual(position.position, 2)
})
await t.test('second', (t) => {
const position = { position: 0 }
const input = '"Hello" World'
t.assert.strictEqual(collectAnHTTPQuotedString(input, {
position: 0
}), '"Hello"')
t.assert.strictEqual(collectAnHTTPQuotedString(input, position, true), 'Hello')
t.assert.strictEqual(position.position, 7)
})
})
// https://github.com/nodejs/undici/issues/1574
test('too long base64 url', async (t) => {
const inputStr = 'a'.repeat(1 << 20)
const base64 = Buffer.from(inputStr).toString('base64')
const dataURIPrefix = 'data:application/octet-stream;base64,'
const dataURL = dataURIPrefix + base64
try {
const res = await fetch(dataURL)
const buf = await res.arrayBuffer()
const outputStr = Buffer.from(buf).toString('ascii')
t.assert.strictEqual(outputStr, inputStr)
} catch (e) {
t.assert.fail(`failed to fetch ${dataURL}`)
}
})
test('https://domain.com/#', (t) => {
t.plan(1)
const domain = 'https://domain.com/#a'
const serialized = URLSerializer(new URL(domain))
t.assert.strictEqual(serialized, domain)
})
test('https://domain.com/?', (t) => {
t.plan(1)
const domain = 'https://domain.com/?a=b'
const serialized = URLSerializer(new URL(domain))
t.assert.strictEqual(serialized, domain)
})
// https://github.com/nodejs/undici/issues/2474
test('hash url', (t) => {
t.plan(1)
const domain = 'https://domain.com/#a#b'
const url = new URL(domain)
const serialized = URLSerializer(url, true)
t.assert.strictEqual(serialized, url.href.substring(0, url.href.length - url.hash.length))
})
// https://github.com/nodejs/undici/issues/2474
test('data url that includes the hash', async (t) => {
t.plan(1)
const dataURL = 'data:,node#js#'
try {
const res = await fetch(dataURL)
t.assert.strictEqual(await res.text(), 'node')
} catch (error) {
t.assert.fail(`failed to fetch ${dataURL}`)
}
})
================================================
FILE: test/fetch/encoding.js
================================================
'use strict'
const { once } = require('node:events')
const { createServer } = require('node:http')
const { test, before, after, describe } = require('node:test')
const { fetch, Client } = require('../..')
describe('content-encoding handling', () => {
const gzipDeflateText = Buffer.from('H4sIAAAAAAAAA6uY89nj7MmT1wM5zuuf8gxkYZCfx5IFACQ8u/wVAAAA', 'base64')
const zstdText = Buffer.from('KLUv/QBYaQAASGVsbG8sIFdvcmxkIQ==', 'base64')
let server
before(async () => {
server = createServer({
noDelay: true
}, (req, res) => {
res.socket.setNoDelay(true)
if (
req.headers['accept-encoding'] === 'deflate, gzip' ||
req.headers['accept-encoding'] === 'DeFlAtE, GzIp'
) {
res.writeHead(200,
{
'Content-Encoding': 'deflate, gzip',
'Content-Type': 'text/plain'
}
)
res.flushHeaders()
res.end(gzipDeflateText)
} else if (req.headers['accept-encoding'] === 'zstd') {
res.writeHead(200,
{
'Content-Encoding': 'zstd',
'Content-Type': 'text/plain'
}
)
res.flushHeaders()
res.end(zstdText)
} else {
res.writeHead(200,
{
'Content-Type': 'text/plain'
}
)
res.flushHeaders()
res.end('Hello, World!')
}
})
await once(server.listen(0), 'listening')
})
after(() => {
server.closeAllConnections?.()
server.close()
})
test('content-encoding header', async (t) => {
const response = await fetch(`http://localhost:${server.address().port}`, {
keepalive: false,
headers: { 'accept-encoding': 'deflate, gzip' }
})
t.assert.strictEqual(response.headers.get('content-encoding'), 'deflate, gzip')
t.assert.strictEqual(response.headers.get('content-type'), 'text/plain')
t.assert.strictEqual(await response.text(), 'Hello, World!')
})
test('content-encoding header is case-iNsENsITIve', async (t) => {
const response = await fetch(`http://localhost:${server.address().port}`, {
keepalive: false,
headers: { 'accept-encoding': 'DeFlAtE, GzIp' }
})
t.assert.strictEqual(response.headers.get('content-encoding'), 'deflate, gzip')
t.assert.strictEqual(response.headers.get('content-type'), 'text/plain')
t.assert.strictEqual(await response.text(), 'Hello, World!')
})
test('should decompress zstandard response',
{ skip: typeof require('node:zlib').createZstdDecompress !== 'function' },
async (t) => {
const response = await fetch(`http://localhost:${server.address().port}`, {
keepalive: false,
headers: { 'accept-encoding': 'zstd' }
})
t.assert.strictEqual(response.headers.get('content-encoding'), 'zstd')
t.assert.strictEqual(response.headers.get('content-type'), 'text/plain')
t.assert.strictEqual(await response.text(), 'Hello, World!')
})
})
describe('content-encoding chain limit', () => {
// CVE fix: Limit the number of content-encodings to prevent resource exhaustion
// Similar to urllib3 (GHSA-gm62-xv2j-4w53) and curl (CVE-2022-32206)
const MAX_CONTENT_ENCODINGS = 5
let server
before(async () => {
server = createServer({
noDelay: true
}, (req, res) => {
res.socket.setNoDelay(true)
const encodingCount = parseInt(req.headers['x-encoding-count'] || '1', 10)
const encodings = Array(encodingCount).fill('identity').join(', ')
res.writeHead(200, {
'Content-Encoding': encodings,
'Content-Type': 'text/plain'
})
res.flushHeaders()
res.end('test')
})
await once(server.listen(0, '127.0.0.1'), 'listening')
})
after(() => {
server.closeAllConnections?.()
server.close()
})
test(`should allow exactly ${MAX_CONTENT_ENCODINGS} content-encodings`, async (t) => {
const client = new Client(`http://127.0.0.1:${server.address().port}`)
t.after(() => client.close())
const response = await fetch(`http://127.0.0.1:${server.address().port}`, {
dispatcher: client,
keepalive: false,
headers: { 'x-encoding-count': String(MAX_CONTENT_ENCODINGS) }
})
t.assert.strictEqual(response.status, 200)
// identity encoding is a no-op, so the body should be passed through
t.assert.strictEqual(await response.text(), 'test')
})
test(`should reject more than ${MAX_CONTENT_ENCODINGS} content-encodings`, async (t) => {
const client = new Client(`http://127.0.0.1:${server.address().port}`)
t.after(() => client.close())
await t.assert.rejects(
fetch(`http://127.0.0.1:${server.address().port}`, {
dispatcher: client,
keepalive: false,
headers: { 'x-encoding-count': String(MAX_CONTENT_ENCODINGS + 1) }
}),
(err) => {
t.assert.ok(err.cause?.message.includes('content-encoding'))
return true
}
)
})
test('should reject excessive content-encoding chains', async (t) => {
const client = new Client(`http://127.0.0.1:${server.address().port}`)
t.after(() => client.close())
await t.assert.rejects(
fetch(`http://127.0.0.1:${server.address().port}`, {
dispatcher: client,
keepalive: false,
headers: { 'x-encoding-count': '100' }
}),
(err) => {
t.assert.ok(err.cause?.message.includes('content-encoding'))
return true
}
)
})
})
================================================
FILE: test/fetch/exiting.js
================================================
'use strict'
const { test } = require('node:test')
const { fetch } = require('../..')
const { createServer } = require('node:http')
const { closeServerAsPromise } = require('../utils/node-http')
const { once } = require('node:events')
const { createDeferredPromise } = require('../../lib/util/promise')
test('abort the request on the other side if the stream is canceled', async (t) => {
t.plan(1)
const promise = createDeferredPromise()
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.writeHead(200)
res.write('hello')
req.on('aborted', () => {
t.assert.ok('aborted')
promise.resolve()
})
// Let's not end the response on purpose
})
t.after(closeServerAsPromise(server))
await once(server.listen(0), 'listening')
const url = new URL(`http://127.0.0.1:${server.address().port}`)
const response = await fetch(url)
const reader = response.body.getReader()
try {
await reader.read()
} finally {
reader.releaseLock()
await response.body.cancel()
}
await promise.promise
})
================================================
FILE: test/fetch/export-env-proxy-agent.js
================================================
'use strict'
const { test } = require('node:test')
const undiciFetch = require('../../undici-fetch')
test('EnvHttpProxyAgent should be part of Node.js bundle', (t) => {
t.assert.strictEqual(typeof undiciFetch.EnvHttpProxyAgent, 'function')
t.assert.strictEqual(typeof undiciFetch.getGlobalDispatcher, 'function')
t.assert.strictEqual(typeof undiciFetch.setGlobalDispatcher, 'function')
const agent = new undiciFetch.EnvHttpProxyAgent()
const previousDispatcher = undiciFetch.getGlobalDispatcher()
undiciFetch.setGlobalDispatcher(agent)
t.after(() => {
undiciFetch.setGlobalDispatcher(previousDispatcher)
})
t.assert.strictEqual(undiciFetch.getGlobalDispatcher(), agent)
})
================================================
FILE: test/fetch/fetch-leak.js
================================================
'use strict'
const { test } = require('node:test')
const { fetch } = require('../..')
const { createServer } = require('node:http')
const { closeServerAsPromise } = require('../utils/node-http')
const hasGC = typeof global.gc !== 'undefined'
test('do not leak', (t, done) => {
if (!hasGC) {
throw new Error('gc is not available. Run with \'--expose-gc\'.')
}
t.plan(1)
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.end()
})
t.after(closeServerAsPromise(server))
let url
let isDone = false
server.listen(0, function attack () {
if (isDone) {
return
}
url ??= new URL(`http://127.0.0.1:${server.address().port}`)
const controller = new AbortController()
fetch(url, { signal: controller.signal })
.then(res => res.arrayBuffer())
.catch(() => {})
.then(attack)
})
let prev = Infinity
let count = 0
const interval = setInterval(() => {
isDone = true
global.gc()
const next = process.memoryUsage().heapUsed
if (next <= prev) {
t.assert.ok(true)
done()
} else if (count++ > 20) {
t.assert.fail()
} else {
prev = next
}
}, 1e3)
t.after(() => clearInterval(interval))
})
================================================
FILE: test/fetch/fetch-timeouts.js
================================================
'use strict'
const { test } = require('node:test')
const { fetch, Agent } = require('../..')
const timers = require('../../lib/util/timers')
const { createServer } = require('node:http')
const FakeTimers = require('@sinonjs/fake-timers')
const { closeServerAsPromise } = require('../utils/node-http')
test('Fetch very long request, timeout overridden so no error', (t, done) => {
const minutes = 6
const msToDelay = 1000 * 60 * minutes
t.plan(1)
const clock = FakeTimers.install()
t.after(clock.uninstall.bind(clock))
const orgTimers = { ...timers }
Object.assign(timers, { setTimeout, clearTimeout })
t.after(() => {
Object.assign(timers, orgTimers)
})
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
setTimeout(() => {
res.end('hello')
}, msToDelay)
clock.tick(msToDelay + 1)
})
t.after(closeServerAsPromise(server))
server.listen(0, () => {
fetch(`http://localhost:${server.address().port}`, {
path: '/',
method: 'GET',
dispatcher: new Agent({
headersTimeout: 0,
connectTimeout: 0,
bodyTimeout: 0
})
})
.then((response) => response.text())
.then((response) => {
t.assert.strictEqual('hello', response)
done()
})
.catch((err) => {
// This should not happen, a timeout error should not occur
throw err
})
clock.tick(msToDelay - 1)
})
})
================================================
FILE: test/fetch/fetch-url-after-redirect.js
================================================
'use strict'
const { test } = require('node:test')
const { createServer } = require('node:http')
const { fetch } = require('../..')
const { closeServerAsPromise } = require('../utils/node-http')
const { promisify } = require('node:util')
test('after redirecting the url of the response is set to the target url', async (t) => {
// redirect-1 -> redirect-2 -> target
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
switch (res.req.url) {
case '/redirect-1':
res.writeHead(302, undefined, { Location: '/redirect-2' })
res.end()
break
case '/redirect-2':
res.writeHead(302, undefined, { Location: '/redirect-3' })
res.end()
break
case '/redirect-3':
res.writeHead(302, undefined, { Location: '/target' })
res.end()
break
case '/target':
res.writeHead(200, 'dummy', { 'Content-Type': 'text/plain' })
res.end()
break
}
})
t.after(closeServerAsPromise(server))
const listenAsync = promisify(server.listen.bind(server))
await listenAsync(0)
const { port } = server.address()
const response = await fetch(`http://127.0.0.1:${port}/redirect-1`)
t.assert.strictEqual(response.url, `http://127.0.0.1:${port}/target`)
})
test('location header with non-ASCII character redirects to a properly encoded url', async (t) => {
// redirect -> %EC%95%88%EB%85%95 (안녕), not %C3%AC%C2%95%C2%88%C3%AB%C2%85%C2%95
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
if (res.req.url.endsWith('/redirect')) {
res.writeHead(302, undefined, { Location: `/${Buffer.from('안녕').toString('binary')}` })
res.end()
} else {
res.writeHead(200, 'dummy', { 'Content-Type': 'text/plain' })
res.end()
}
})
t.after(closeServerAsPromise(server))
const listenAsync = promisify(server.listen.bind(server))
await listenAsync(0)
const { port } = server.address()
const response = await fetch(`http://127.0.0.1:${port}/redirect`)
t.assert.strictEqual(response.url, `http://127.0.0.1:${port}/${encodeURIComponent('안녕')}`)
})
================================================
FILE: test/fetch/fire-and-forget.js
================================================
'use strict'
const { randomFillSync } = require('node:crypto')
const { setTimeout: sleep, setImmediate: nextTick } = require('node:timers/promises')
const { test } = require('node:test')
const { fetch, Request, Response, Agent, setGlobalDispatcher, getGlobalDispatcher } = require('../..')
const { createServer } = require('node:http')
const { closeServerAsPromise } = require('../utils/node-http')
const blob = randomFillSync(new Uint8Array(1024 * 512))
const hasGC = typeof global.gc !== 'undefined'
// https://github.com/nodejs/undici/issues/4150
test('test finalizer cloned request', async () => {
if (!hasGC) {
throw new Error('gc is not available. Run with \'--expose-gc\'.')
}
const request = new Request('http://localhost', { method: 'POST', body: 'Hello' })
request.clone()
await nextTick()
// eslint-disable-next-line no-undef
gc()
await nextTick()
await request.arrayBuffer() // check consume body
})
test('test finalizer cloned response', async () => {
if (!hasGC) {
throw new Error('gc is not available. Run with \'--expose-gc\'.')
}
const response = new Response('Hello')
response.clone()
await nextTick()
// eslint-disable-next-line no-undef
gc()
await nextTick()
await response.arrayBuffer() // check consume body
})
// https://github.com/nodejs/undici/pull/4803
test('should not call cancel() during GC (new Response)', async () => {
if (!hasGC) {
throw new Error('gc is not available. Run with \'--expose-gc\'.')
}
let response = new Response(new ReadableStream({
start () {},
pull (ctrl) {
ctrl.enqueue(new Uint8Array([72, 101, 108, 108, 111])) // Hello
},
cancel () {
throw new Error('should be unreachable')
}
}))
let cloned = response.clone()
const body = response.body
await nextTick()
cloned.body.cancel()
cloned = null
response = null
await nextTick()
// eslint-disable-next-line no-undef
gc()
await nextTick(); // handle 'uncaughtException' event
(function () {})(body) // save a reference without triggering linter warnings
})
test('does not need the body to be consumed to continue', { timeout: 180_000 }, async (t) => {
if (!hasGC) {
throw new Error('gc is not available. Run with \'--expose-gc\'.')
}
const agent = new Agent({
keepAliveMaxTimeout: 10,
keepAliveTimeoutThreshold: 10
})
const previousDispatcher = getGlobalDispatcher()
setGlobalDispatcher(agent)
t.after(() => {
setGlobalDispatcher(previousDispatcher)
})
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.writeHead(200)
res.end(blob)
})
t.after(closeServerAsPromise(server))
await new Promise((resolve) => {
server.listen(0, resolve)
})
const url = new URL(`http://127.0.0.1:${server.address().port}`)
const batch = 50
const delay = 0
let total = 0
while (total < 5000) {
// eslint-disable-next-line no-undef
gc(true)
const array = new Array(batch)
for (let i = 0; i < batch; i += 2) {
array[i] = fetch(url).catch(() => {})
array[i + 1] = fetch(url).then(r => r.clone()).catch(() => {})
}
await Promise.all(array)
await sleep(delay)
console.log(
'RSS',
(process.memoryUsage.rss() / 1024 / 1024) | 0,
'MB after',
(total += batch) + ' fetch() requests'
)
}
})
================================================
FILE: test/fetch/formdata-inspect-custom.js
================================================
'use strict'
const { FormData } = require('../../')
const { inspect } = require('node:util')
const { test } = require('node:test')
test('FormData class custom inspection', (t) => {
const formData = new FormData()
formData.append('username', 'john_doe')
formData.append('email', 'john@example.com')
const expectedOutput = "FormData {\n username: 'john_doe',\n email: 'john@example.com'\n}"
t.assert.deepStrictEqual(inspect(formData), expectedOutput)
})
================================================
FILE: test/fetch/formdata.js
================================================
'use strict'
const { test } = require('node:test')
const { FormData, Response, Request } = require('../../')
const { isFormDataLike } = require('../../lib/core/util')
test('arg validation', (t) => {
const form = new FormData()
// constructor
t.assert.throws(() => {
// eslint-disable-next-line
new FormData('asd')
}, TypeError)
// append
t.assert.throws(() => {
FormData.prototype.append.call(null)
}, TypeError)
t.assert.throws(() => {
form.append()
}, TypeError)
t.assert.throws(() => {
form.append('k', 'not usv', '')
}, TypeError)
// delete
t.assert.throws(() => {
FormData.prototype.delete.call(null)
}, TypeError)
t.assert.throws(() => {
form.delete()
}, TypeError)
// get
t.assert.throws(() => {
FormData.prototype.get.call(null)
}, TypeError)
t.assert.throws(() => {
form.get()
}, TypeError)
// getAll
t.assert.throws(() => {
FormData.prototype.getAll.call(null)
}, TypeError)
t.assert.throws(() => {
form.getAll()
}, TypeError)
// has
t.assert.throws(() => {
FormData.prototype.has.call(null)
}, TypeError)
t.assert.throws(() => {
form.has()
}, TypeError)
// set
t.assert.throws(() => {
FormData.prototype.set.call(null)
}, TypeError)
t.assert.throws(() => {
form.set('k')
}, TypeError)
t.assert.throws(() => {
form.set('k', 'not usv', '')
}, TypeError)
// iterator
t.assert.throws(() => {
Reflect.apply(FormData.prototype[Symbol.iterator], null)
}, TypeError)
// toStringTag
t.assert.doesNotThrow(() => {
FormData.prototype[Symbol.toStringTag].charAt(0)
})
})
test('set blob', (t) => {
const form = new FormData()
form.set('key', new Blob([]), undefined)
t.assert.strictEqual(form.get('key').name, 'blob')
form.set('key1', new Blob([]), null)
t.assert.strictEqual(form.get('key1').name, 'null')
})
test('append file', (t) => {
const form = new FormData()
form.set('asd', new File([], 'asd1', { type: 'text/plain' }), 'asd2')
form.append('asd2', new File([], 'asd1'), 'asd2')
t.assert.strictEqual(form.has('asd'), true)
t.assert.strictEqual(form.has('asd2'), true)
t.assert.strictEqual(form.get('asd').name, 'asd2')
t.assert.strictEqual(form.get('asd2').name, 'asd2')
t.assert.strictEqual(form.get('asd').type, 'text/plain')
form.delete('asd')
t.assert.strictEqual(form.get('asd'), null)
t.assert.strictEqual(form.has('asd2'), true)
t.assert.strictEqual(form.has('asd'), false)
})
test('append blob', async (t) => {
const form = new FormData()
form.set('asd', new Blob(['asd1'], { type: 'text/plain' }))
t.assert.strictEqual(form.has('asd'), true)
t.assert.strictEqual(form.get('asd').type, 'text/plain')
t.assert.strictEqual(await form.get('asd').text(), 'asd1')
form.delete('asd')
t.assert.strictEqual(form.get('asd'), null)
form.append('key', new Blob([]), undefined)
t.assert.strictEqual(form.get('key').name, 'blob')
form.append('key1', new Blob([]), null)
t.assert.strictEqual(form.get('key1').name, 'null')
})
test('append string', (t) => {
const form = new FormData()
form.set('k1', 'v1')
form.set('k2', 'v2')
t.assert.deepStrictEqual([...form], [['k1', 'v1'], ['k2', 'v2']])
t.assert.strictEqual(form.has('k1'), true)
t.assert.strictEqual(form.get('k1'), 'v1')
form.append('k1', 'v1+')
t.assert.deepStrictEqual(form.getAll('k1'), ['v1', 'v1+'])
form.set('k2', 'v1++')
t.assert.strictEqual(form.get('k2'), 'v1++')
form.delete('asd')
t.assert.strictEqual(form.get('asd'), null)
})
test('formData.entries', async (t) => {
const form = new FormData()
await t.test('with 0 entries', (t) => {
t.plan(1)
const entries = [...form.entries()]
t.assert.deepStrictEqual(entries, [])
})
await t.test('with 1+ entries', (t) => {
t.plan(2)
form.set('k1', 'v1')
form.set('k2', 'v2')
const entries = [...form.entries()]
const entries2 = [...form.entries()]
t.assert.deepStrictEqual(entries, [['k1', 'v1'], ['k2', 'v2']])
t.assert.deepStrictEqual(entries, entries2)
})
})
test('formData.keys', async (t) => {
const form = new FormData()
await t.test('with 0 keys', (t) => {
t.plan(1)
const keys = [...form.entries()]
t.assert.deepStrictEqual(keys, [])
})
await t.test('with 1+ keys', (t) => {
t.plan(2)
form.set('k1', 'v1')
form.set('k2', 'v2')
const keys = [...form.keys()]
const keys2 = [...form.keys()]
t.assert.deepStrictEqual(keys, ['k1', 'k2'])
t.assert.deepStrictEqual(keys, keys2)
})
})
test('formData.values', async (t) => {
const form = new FormData()
await t.test('with 0 values', (t) => {
t.plan(1)
const values = [...form.values()]
t.assert.deepStrictEqual(values, [])
})
await t.test('with 1+ values', (t) => {
t.plan(2)
form.set('k1', 'v1')
form.set('k2', 'v2')
const values = [...form.values()]
const values2 = [...form.values()]
t.assert.deepStrictEqual(values, ['v1', 'v2'])
t.assert.deepStrictEqual(values, values2)
})
})
test('formData forEach', async (t) => {
await t.test('invalid arguments', (t) => {
t.assert.throws(() => {
FormData.prototype.forEach.call({})
}, TypeError('Illegal invocation'))
t.assert.throws(() => {
const fd = new FormData()
fd.forEach({})
}, TypeError)
})
await t.test('with a callback', (t) => {
const fd = new FormData()
fd.set('a', 'b')
fd.set('c', 'd')
let i = 0
fd.forEach((value, key, self) => {
if (i++ === 0) {
t.assert.strictEqual(value, 'b')
t.assert.strictEqual(key, 'a')
} else {
t.assert.strictEqual(value, 'd')
t.assert.strictEqual(key, 'c')
}
t.assert.strictEqual(fd, self)
})
})
await t.test('with a thisArg', (t) => {
const fd = new FormData()
fd.set('b', 'a')
fd.forEach(function (value, key, self) {
t.assert.strictEqual(this, globalThis)
t.assert.strictEqual(fd, self)
t.assert.strictEqual(key, 'b')
t.assert.strictEqual(value, 'a')
})
const thisArg = Symbol('thisArg')
fd.forEach(function () {
t.assert.strictEqual(this, thisArg)
}, thisArg)
})
})
test('formData toStringTag', (t) => {
const form = new FormData()
t.assert.strictEqual(form[Symbol.toStringTag], 'FormData')
t.assert.strictEqual(FormData.prototype[Symbol.toStringTag], 'FormData')
})
test('formData.constructor.name', (t) => {
const form = new FormData()
t.assert.strictEqual(form.constructor.name, 'FormData')
})
test('formData should be an instance of FormData', async (t) => {
await t.test('Invalid class FormData', (t) => {
class FormData {
constructor () {
this.data = []
}
append (key, value) {
this.data.push([key, value])
}
get (key) {
return this.data.find(([k]) => k === key)
}
}
const form = new FormData()
t.assert.strictEqual(isFormDataLike(form), false)
})
await t.test('Invalid function FormData', (t) => {
function FormData () {
const data = []
return {
append (key, value) {
data.push([key, value])
},
get (key) {
return data.find(([k]) => k === key)
}
}
}
const form = new FormData()
t.assert.strictEqual(isFormDataLike(form), false)
})
await t.test('Valid FormData', (t) => {
const form = new FormData()
t.assert.strictEqual(isFormDataLike(form), true)
})
})
test('FormData should be compatible with third-party libraries', (t) => {
t.plan(1)
class FormData {
constructor () {
this.data = []
}
get [Symbol.toStringTag] () {
return 'FormData'
}
append () {}
delete () {}
get () {}
getAll () {}
has () {}
set () {}
entries () {}
keys () {}
values () {}
forEach () {}
}
const form = new FormData()
t.assert.strictEqual(isFormDataLike(form), true)
})
test('arguments', (t) => {
t.assert.strictEqual(FormData.length, 0)
t.assert.strictEqual(FormData.prototype.append.length, 2)
t.assert.strictEqual(FormData.prototype.delete.length, 1)
t.assert.strictEqual(FormData.prototype.get.length, 1)
t.assert.strictEqual(FormData.prototype.getAll.length, 1)
t.assert.strictEqual(FormData.prototype.has.length, 1)
t.assert.strictEqual(FormData.prototype.set.length, 2)
})
// https://github.com/nodejs/undici/pull/1814
test('FormData returned from bodyMixin.formData is not a clone', async (t) => {
const fd = new FormData()
fd.set('foo', 'bar')
const res = new Response(fd)
fd.set('foo', 'foo')
const fd2 = await res.formData()
t.assert.strictEqual(fd2.get('foo'), 'bar')
t.assert.strictEqual(fd.get('foo'), 'foo')
fd2.set('foo', 'baz')
t.assert.strictEqual(fd2.get('foo'), 'baz')
t.assert.strictEqual(fd.get('foo'), 'foo')
})
test('.formData() with multipart/form-data body that ends with --\r\n', async (t) => {
const request = new Request('http://localhost', {
method: 'POST',
headers: {
'Content-Type': 'multipart/form-data; boundary=----formdata-undici-0.6204674738279623'
},
body:
'------formdata-undici-0.6204674738279623\r\n' +
'Content-Disposition: form-data; name="fiŝo"\r\n' +
'\r\n' +
'value1\r\n' +
'------formdata-undici-0.6204674738279623--\r\n'
})
await request.formData()
})
================================================
FILE: test/fetch/general.js
================================================
'use strict'
const { test } = require('node:test')
const {
FormData,
Headers,
Request,
Response
} = require('../../index')
test('Symbol.toStringTag descriptor', (t) => {
for (const cls of [
FormData,
Headers,
Request,
Response
]) {
const desc = Object.getOwnPropertyDescriptor(cls.prototype, Symbol.toStringTag)
t.assert.deepStrictEqual(desc, {
value: cls.name,
writable: false,
enumerable: false,
configurable: true
})
}
})
================================================
FILE: test/fetch/headers-case.js
================================================
'use strict'
const { fetch, Headers, Request } = require('../..')
const { createServer } = require('node:http')
const { once } = require('node:events')
const { test } = require('node:test')
const { closeServerAsPromise } = require('../utils/node-http')
test('Headers retain keys case-sensitive', async (t) => {
t.plan(4)
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
t.assert.ok(req.rawHeaders.includes('Content-Type'))
res.end()
}).listen(0)
t.after(closeServerAsPromise(server))
await once(server, 'listening')
const url = `http://localhost:${server.address().port}`
for (const headers of [
new Headers([['Content-Type', 'text/plain']]),
{ 'Content-Type': 'text/plain' },
[['Content-Type', 'text/plain']]
]) {
await fetch(url, { headers })
}
// see https://github.com/nodejs/undici/pull/3183
await fetch(new Request(url, { headers: [['Content-Type', 'text/plain']] }), { method: 'GET' })
})
================================================
FILE: test/fetch/headers-inspect-custom.js
================================================
'use strict'
const { Headers } = require('../../lib/web/fetch/headers')
const { test } = require('node:test')
const util = require('node:util')
test('Headers class custom inspection', (t) => {
const headers = new Headers()
headers.set('Content-Type', 'application/json')
headers.set('Authorization', 'Bearer token')
const inspectedOutput = util.inspect(headers, { depth: 1 })
const expectedOutput = "Headers { 'Content-Type': 'application/json', Authorization: 'Bearer token' }"
t.assert.strictEqual(inspectedOutput, expectedOutput)
})
================================================
FILE: test/fetch/headers.js
================================================
'use strict'
const { test } = require('node:test')
const { Headers, fill, setHeadersGuard } = require('../../lib/web/fetch/headers')
const { once } = require('node:events')
const { fetch } = require('../..')
const { createServer } = require('node:http')
const { closeServerAsPromise } = require('../utils/node-http')
test('Headers initialization', async (t) => {
await t.test('allows undefined', (t) => {
t.plan(1)
t.assert.doesNotThrow(() => new Headers())
})
await t.test('with array of header entries', async (t) => {
await t.test('fails on invalid array-based init', (t) => {
t.plan(3)
t.assert.throws(
() => new Headers([['undici', 'fetch'], ['fetch']]),
TypeError('Headers constructor: expected name/value pair to be length 2, found 1.')
)
t.assert.throws(() => new Headers(['undici', 'fetch', 'fetch']), TypeError)
t.assert.throws(
() => new Headers([0, 1, 2]),
TypeError('Headers constructor: init[0] (0) is not iterable.')
)
})
await t.test('allows even length init', (t) => {
t.plan(1)
const init = [['undici', 'fetch'], ['fetch', 'undici']]
t.assert.doesNotThrow(() => new Headers(init))
})
await t.test('fails for event flattened init', (t) => {
t.plan(1)
const init = ['undici', 'fetch', 'fetch', 'undici']
t.assert.throws(
() => new Headers(init),
TypeError('Headers constructor: init[0] ("undici") is not iterable.')
)
})
})
await t.test('with object of header entries', (t) => {
t.plan(1)
const init = {
undici: 'fetch',
fetch: 'undici'
}
t.assert.doesNotThrow(() => new Headers(init))
})
await t.test('fails silently if a boxed primitive object is passed', (t) => {
t.plan(3)
/* eslint-disable no-new-wrappers */
t.assert.doesNotThrow(() => new Headers(new Number()))
t.assert.doesNotThrow(() => new Headers(new Boolean()))
t.assert.doesNotThrow(() => new Headers(new String()))
/* eslint-enable no-new-wrappers */
})
await t.test('fails if primitive is passed', (t) => {
t.plan(2)
const expectedTypeError = TypeError
t.assert.throws(() => new Headers(1), expectedTypeError)
t.assert.throws(() => new Headers('1'), expectedTypeError)
})
await t.test('allows some weird stuff (because of webidl)', (t) => {
t.assert.doesNotThrow(() => {
new Headers(function () {}) // eslint-disable-line no-new
})
t.assert.doesNotThrow(() => {
new Headers(Function) // eslint-disable-line no-new
})
})
await t.test('allows a myriad of header values to be passed', (t) => {
t.plan(4)
// Headers constructor uses Headers.append
t.assert.doesNotThrow(() => new Headers([
['a', ['b', 'c']],
['d', ['e', 'f']]
]), 'allows any array values')
t.assert.doesNotThrow(() => new Headers([
['key', null]
]), 'allows null values')
t.assert.throws(() => new Headers([
['key']
]), 'throws when 2 arguments are not passed')
t.assert.throws(() => new Headers([
['key', 'value', 'value2']
]), 'throws when too many arguments are passed')
})
await t.test('accepts headers as objects with array values', (t) => {
t.plan(1)
const headers = new Headers({
c: '5',
b: ['3', '4'],
a: ['1', '2']
})
t.assert.deepStrictEqual([...headers.entries()], [
['a', '1,2'],
['b', '3,4'],
['c', '5']
])
})
})
test('Headers append', async (t) => {
await t.test('adds valid header entry to instance', (t) => {
t.plan(2)
const headers = new Headers()
const name = 'undici'
const value = 'fetch'
t.assert.doesNotThrow(() => headers.append(name, value))
t.assert.strictEqual(headers.get(name), value)
})
await t.test('adds valid header to existing entry', (t) => {
t.plan(4)
const headers = new Headers()
const name = 'undici'
const value1 = 'fetch1'
const value2 = 'fetch2'
const value3 = 'fetch3'
headers.append(name, value1)
t.assert.strictEqual(headers.get(name), value1)
t.assert.doesNotThrow(() => headers.append(name, value2))
t.assert.doesNotThrow(() => headers.append(name, value3))
t.assert.strictEqual(headers.get(name), [value1, value2, value3].join(', '))
})
await t.test('throws on invalid entry', (t) => {
t.plan(3)
const headers = new Headers()
t.assert.throws(() => headers.append(), 'throws on missing name and value')
t.assert.throws(() => headers.append('undici'), 'throws on missing value')
t.assert.throws(() => headers.append('invalid @ header ? name', 'valid value'), 'throws on invalid name')
})
})
test('Headers delete', async (t) => {
await t.test('deletes valid header entry from instance', (t) => {
t.plan(3)
const headers = new Headers()
const name = 'undici'
const value = 'fetch'
headers.append(name, value)
t.assert.strictEqual(headers.get(name), value)
t.assert.doesNotThrow(() => headers.delete(name))
t.assert.strictEqual(headers.get(name), null)
})
await t.test('does not mutate internal list when no match is found', (t) => {
t.plan(3)
const headers = new Headers()
const name = 'undici'
const value = 'fetch'
headers.append(name, value)
t.assert.strictEqual(headers.get(name), value)
t.assert.doesNotThrow(() => headers.delete('not-undici'))
t.assert.strictEqual(headers.get(name), value)
})
await t.test('throws on invalid entry', (t) => {
t.plan(2)
const headers = new Headers()
t.assert.throws(() => headers.delete(), 'throws on missing namee')
t.assert.throws(() => headers.delete('invalid @ header ? name'), 'throws on invalid name')
})
// https://github.com/nodejs/undici/issues/2429
await t.test('`Headers#delete` returns undefined', (t) => {
t.plan(2)
const headers = new Headers({ test: 'test' })
t.assert.strictEqual(headers.delete('test'), undefined)
t.assert.strictEqual(headers.delete('test2'), undefined)
})
})
test('Headers get', async (t) => {
await t.test('returns null if not found in instance', (t) => {
t.plan(1)
const headers = new Headers()
headers.append('undici', 'fetch')
t.assert.strictEqual(headers.get('not-undici'), null)
})
await t.test('returns header values from valid header name', (t) => {
t.plan(2)
const headers = new Headers()
const name = 'undici'; const value1 = 'fetch1'; const value2 = 'fetch2'
headers.append(name, value1)
t.assert.strictEqual(headers.get(name), value1)
headers.append(name, value2)
t.assert.strictEqual(headers.get(name), [value1, value2].join(', '))
})
await t.test('throws on invalid entry', (t) => {
t.plan(2)
const headers = new Headers()
t.assert.throws(() => headers.get(), 'throws on missing name')
t.assert.throws(() => headers.get('invalid @ header ? name'), 'throws on invalid name')
})
})
test('Headers has', async (t) => {
await t.test('returns boolean existence for a header name', (t) => {
t.plan(2)
const headers = new Headers()
const name = 'undici'
headers.append('not-undici', 'fetch')
t.assert.strictEqual(headers.has(name), false)
headers.append(name, 'fetch')
t.assert.strictEqual(headers.has(name), true)
})
await t.test('throws on invalid entry', (t) => {
t.plan(2)
const headers = new Headers()
t.assert.throws(() => headers.has(), 'throws on missing name')
t.assert.throws(() => headers.has('invalid @ header ? name'), 'throws on invalid name')
})
})
test('Headers set', async (t) => {
await t.test('sets valid header entry to instance', (t) => {
t.plan(2)
const headers = new Headers()
const name = 'undici'
const value = 'fetch'
headers.append('not-undici', 'fetch')
t.assert.doesNotThrow(() => headers.set(name, value))
t.assert.strictEqual(headers.get(name), value)
})
await t.test('overwrites existing entry', (t) => {
t.plan(4)
const headers = new Headers()
const name = 'undici'
const value1 = 'fetch1'
const value2 = 'fetch2'
t.assert.doesNotThrow(() => headers.set(name, value1))
t.assert.strictEqual(headers.get(name), value1)
t.assert.doesNotThrow(() => headers.set(name, value2))
t.assert.strictEqual(headers.get(name), value2)
})
await t.test('allows setting a myriad of values', (t) => {
t.plan(4)
const headers = new Headers()
t.assert.doesNotThrow(() => headers.set('a', ['b', 'c']), 'sets array values properly')
t.assert.doesNotThrow(() => headers.set('b', null), 'allows setting null values')
t.assert.throws(() => headers.set('c'), 'throws when 2 arguments are not passed')
t.assert.doesNotThrow(() => headers.set('c', 'd', 'e'), 'ignores extra arguments')
})
await t.test('throws on invalid entry', (t) => {
t.plan(3)
const headers = new Headers()
t.assert.throws(() => headers.set(), 'throws on missing name and value')
t.assert.throws(() => headers.set('undici'), 'throws on missing value')
t.assert.throws(() => headers.set('invalid @ header ? name', 'valid value'), 'throws on invalid name')
})
// https://github.com/nodejs/undici/issues/2431
await t.test('`Headers#set` returns undefined', (t) => {
t.plan(2)
const headers = new Headers()
t.assert.strictEqual(headers.set('a', 'b'), undefined)
t.assert.ok(!(headers.set('c', 'd') instanceof Map))
})
})
test('Headers forEach', async (t) => {
const headers = new Headers([['a', 'b'], ['c', 'd']])
await t.test('standard', (t) => {
t.assert.strictEqual(typeof headers.forEach, 'function')
headers.forEach((value, key, headerInstance) => {
t.assert.ok(value === 'b' || value === 'd')
t.assert.ok(key === 'a' || key === 'c')
t.assert.strictEqual(headers, headerInstance)
})
})
await t.test('when no thisArg is set, it is globalThis', (t) => {
headers.forEach(function () {
t.assert.strictEqual(this, globalThis)
})
})
await t.test('with thisArg', (t) => {
const thisArg = { a: Math.random() }
headers.forEach(function () {
t.assert.strictEqual(this, thisArg)
}, thisArg)
})
})
test('Headers as Iterable', async (t) => {
await t.test('should freeze values while iterating', (t) => {
t.plan(1)
const init = [
['foo', '123'],
['bar', '456']
]
const expected = [
['foo', '123'],
['x-x-bar', '456']
]
const headers = new Headers(init)
for (const [key, val] of headers) {
headers.delete(key)
headers.set(`x-${key}`, val)
}
t.assert.deepStrictEqual([...headers], expected)
})
await t.test('returns combined and sorted entries using .forEach()', (t) => {
t.plan(8)
const init = [
['a', '1'],
['b', '2'],
['c', '3'],
['abc', '4'],
['b', '5']
]
const expected = [
['a', '1'],
['abc', '4'],
['b', '2, 5'],
['c', '3']
]
const headers = new Headers(init)
const that = {}
let i = 0
headers.forEach(function (value, key, _headers) {
t.assert.deepStrictEqual(expected[i++], [key, value])
t.assert.strictEqual(this, that)
}, that)
})
await t.test('returns combined and sorted entries using .entries()', (t) => {
t.plan(4)
const init = [
['a', '1'],
['b', '2'],
['c', '3'],
['abc', '4'],
['b', '5']
]
const expected = [
['a', '1'],
['abc', '4'],
['b', '2, 5'],
['c', '3']
]
const headers = new Headers(init)
let i = 0
for (const header of headers.entries()) {
t.assert.deepStrictEqual(header, expected[i++])
}
})
await t.test('returns combined and sorted keys using .keys()', (t) => {
t.plan(4)
const init = [
['a', '1'],
['b', '2'],
['c', '3'],
['abc', '4'],
['b', '5']
]
const expected = ['a', 'abc', 'b', 'c']
const headers = new Headers(init)
let i = 0
for (const key of headers.keys()) {
t.assert.strictEqual(key, expected[i++])
}
})
await t.test('returns combined and sorted values using .values()', (t) => {
t.plan(4)
const init = [
['a', '1'],
['b', '2'],
['c', '3'],
['abc', '4'],
['b', '5']
]
const expected = ['1', '4', '2, 5', '3']
const headers = new Headers(init)
let i = 0
for (const value of headers.values()) {
t.assert.strictEqual(value, expected[i++])
}
})
await t.test('returns combined and sorted entries using for...of loop', (t) => {
t.plan(5)
const init = [
['a', '1'],
['b', '2'],
['c', '3'],
['abc', '4'],
['b', '5'],
['d', ['6', '7']]
]
const expected = [
['a', '1'],
['abc', '4'],
['b', '2, 5'],
['c', '3'],
['d', '6,7']
]
let i = 0
for (const header of new Headers(init)) {
t.assert.deepStrictEqual(header, expected[i++])
}
})
await t.test('validate append ordering', (t) => {
t.plan(1)
const headers = new Headers([['b', '2'], ['c', '3'], ['e', '5']])
headers.append('d', '4')
headers.append('a', '1')
headers.append('f', '6')
headers.append('c', '7')
headers.append('abc', '8')
const expected = [...new Map([
['a', '1'],
['abc', '8'],
['b', '2'],
['c', '3, 7'],
['d', '4'],
['e', '5'],
['f', '6']
])]
t.assert.deepStrictEqual([...headers], expected)
})
await t.test('always use the same prototype Iterator', (t) => {
const HeadersIteratorNext = Function.call.bind(new Headers()[Symbol.iterator]().next)
const init = [
['a', '1'],
['b', '2']
]
const headers = new Headers(init)
const iterator = headers[Symbol.iterator]()
t.assert.deepStrictEqual(HeadersIteratorNext(iterator), { value: init[0], done: false })
t.assert.deepStrictEqual(HeadersIteratorNext(iterator), { value: init[1], done: false })
t.assert.deepStrictEqual(HeadersIteratorNext(iterator), { value: undefined, done: true })
})
})
test('arg validation', (t) => {
// fill
t.assert.throws(() => {
fill({}, 0)
}, TypeError)
const headers = new Headers()
// constructor
t.assert.throws(() => {
// eslint-disable-next-line
new Headers(0)
}, TypeError)
// get [Symbol.toStringTag]
t.assert.doesNotThrow(() => {
Object.prototype.toString.call(Headers.prototype)
})
// toString
t.assert.doesNotThrow(() => {
Headers.prototype.toString.call(null)
})
// append
t.assert.throws(() => {
Headers.prototype.append.call(null)
}, TypeError)
t.assert.throws(() => {
headers.append()
}, TypeError)
// delete
t.assert.throws(() => {
Headers.prototype.delete.call(null)
}, TypeError)
t.assert.throws(() => {
headers.delete()
}, TypeError)
// get
t.assert.throws(() => {
Headers.prototype.get.call(null)
}, TypeError)
t.assert.throws(() => {
headers.get()
}, TypeError)
// has
t.assert.throws(() => {
Headers.prototype.has.call(null)
}, TypeError)
t.assert.throws(() => {
headers.has()
}, TypeError)
// set
t.assert.throws(() => {
Headers.prototype.set.call(null)
}, TypeError)
t.assert.throws(() => {
headers.set()
}, TypeError)
// forEach
t.assert.throws(() => {
Headers.prototype.forEach.call(null)
}, TypeError)
t.assert.throws(() => {
headers.forEach()
}, TypeError)
t.assert.throws(() => {
headers.forEach(1)
}, TypeError)
// inspect
t.assert.throws(() => {
Headers.prototype[Symbol.for('nodejs.util.inspect.custom')].call(null)
}, TypeError)
})
test('function signature verification', async (t) => {
await t.test('function length', (t) => {
t.assert.strictEqual(Headers.prototype.append.length, 2)
t.assert.strictEqual(Headers.prototype.constructor.length, 0)
t.assert.strictEqual(Headers.prototype.delete.length, 1)
t.assert.strictEqual(Headers.prototype.entries.length, 0)
t.assert.strictEqual(Headers.prototype.forEach.length, 1)
t.assert.strictEqual(Headers.prototype.get.length, 1)
t.assert.strictEqual(Headers.prototype.has.length, 1)
t.assert.strictEqual(Headers.prototype.keys.length, 0)
t.assert.strictEqual(Headers.prototype.set.length, 2)
t.assert.strictEqual(Headers.prototype.values.length, 0)
t.assert.strictEqual(Headers.prototype[Symbol.iterator].length, 0)
t.assert.strictEqual(Headers.prototype.toString.length, 0)
})
await t.test('function equality', (t) => {
t.assert.strictEqual(Headers.prototype.entries, Headers.prototype[Symbol.iterator])
t.assert.strictEqual(Headers.prototype.toString, Object.prototype.toString)
})
await t.test('toString and Symbol.toStringTag', (t) => {
t.assert.strictEqual(Object.prototype.toString.call(Headers.prototype), '[object Headers]')
t.assert.strictEqual(Headers.prototype[Symbol.toStringTag], 'Headers')
t.assert.strictEqual(Headers.prototype.toString.call(null), '[object Null]')
})
})
test('various init paths of Headers', (t) => {
const h1 = new Headers()
const h2 = new Headers({})
const h3 = new Headers(undefined)
t.assert.strictEqual([...h1.entries()].length, 0)
t.assert.strictEqual([...h2.entries()].length, 0)
t.assert.strictEqual([...h3.entries()].length, 0)
})
test('immutable guard', (t) => {
const headers = new Headers()
headers.set('key', 'val')
setHeadersGuard(headers, 'immutable')
t.assert.throws(() => {
headers.set('asd', 'asd')
})
t.assert.throws(() => {
headers.append('asd', 'asd')
})
t.assert.throws(() => {
headers.delete('asd')
})
t.assert.strictEqual(headers.get('key'), 'val')
t.assert.strictEqual(headers.has('key'), true)
})
test('request-no-cors guard', (t) => {
const headers = new Headers()
setHeadersGuard(headers, 'request-no-cors')
t.assert.doesNotThrow(() => { headers.set('key', 'val') })
t.assert.doesNotThrow(() => { headers.append('key', 'val') })
})
test('invalid headers', (t) => {
t.assert.doesNotThrow(() => new Headers({ "abcdefghijklmnopqrstuvwxyz0123456789!#$%&'*+-.^_`|~": 'test' }))
const chars = '"(),/:;<=>?@[\\]{}'.split('')
for (const char of chars) {
t.assert.throws(() => new Headers({ [char]: 'test' }), TypeError, `The string "${char}" should throw an error.`)
}
for (const byte of ['\r', '\n', '\t', ' ', String.fromCharCode(128), '']) {
t.assert.throws(() => {
new Headers().set(byte, 'test')
}, TypeError, 'invalid header name')
}
for (const byte of [
'\0',
'\r',
'\n'
]) {
t.assert.throws(() => {
new Headers().set('a', `a${byte}b`)
}, TypeError, 'not allowed at all in header value')
}
t.assert.doesNotThrow(() => {
new Headers().set('a', '\r')
})
t.assert.doesNotThrow(() => {
new Headers().set('a', '\n')
})
t.assert.throws(() => {
new Headers().set('a', Symbol('symbol'))
}, TypeError, 'symbols should throw')
})
test('headers that might cause a ReDoS', (t) => {
t.assert.doesNotThrow(() => {
// This test will time out if the ReDoS attack is successful.
const headers = new Headers()
const attack = 'a' + '\t'.repeat(500_000) + '\ta'
headers.append('fhqwhgads', attack)
})
})
test('Headers.prototype.getSetCookie', async (t) => {
await t.test('Mutating the returned list does not affect the set-cookie list', (t) => {
const h = new Headers([
['set-cookie', 'a=b'],
['set-cookie', 'c=d']
])
const old = h.getSetCookie()
h.getSetCookie().push('oh=no')
const now = h.getSetCookie()
t.assert.deepStrictEqual(old, now)
})
// https://github.com/nodejs/undici/issues/1935
await t.test('When Headers are cloned, so are the cookies (single entry)', async (t) => {
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.setHeader('Set-Cookie', 'test=onetwo')
res.end('Hello World!')
}).listen(0)
await once(server, 'listening')
t.after(closeServerAsPromise(server))
const res = await fetch(`http://localhost:${server.address().port}`)
const entries = Object.fromEntries(res.headers.entries())
t.assert.deepStrictEqual(res.headers.getSetCookie(), ['test=onetwo'])
t.assert.ok('set-cookie' in entries)
})
await t.test('When Headers are cloned, so are the cookies (multiple entries)', async (t) => {
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.setHeader('Set-Cookie', ['test=onetwo', 'test=onetwothree'])
res.end('Hello World!')
}).listen(0)
await once(server, 'listening')
t.after(closeServerAsPromise(server))
const res = await fetch(`http://localhost:${server.address().port}`)
const entries = Object.fromEntries(res.headers.entries())
t.assert.deepStrictEqual(res.headers.getSetCookie(), ['test=onetwo', 'test=onetwothree'])
t.assert.ok('set-cookie' in entries)
})
await t.test('When Headers are cloned, so are the cookies (Headers constructor)', (t) => {
const headers = new Headers([['set-cookie', 'a'], ['set-cookie', 'b']])
t.assert.deepStrictEqual([...headers], [...new Headers(headers)])
})
})
test('When the value is updated, update the cache', (t) => {
t.plan(2)
const expected = [['a', 'a'], ['b', 'b'], ['c', 'c']]
const headers = new Headers(expected)
t.assert.deepStrictEqual([...headers], expected)
headers.append('d', 'd')
t.assert.deepStrictEqual([...headers], [...expected, ['d', 'd']])
})
test('Symbol.iterator is only accessed once', (t) => {
t.plan(1)
const dict = new Proxy({}, {
get () {
t.assert.ok(true)
return function * () {}
}
})
new Headers(dict) // eslint-disable-line no-new
})
test('Invalid Symbol.iterators', (t) => {
t.plan(3)
t.assert.throws(() => new Headers({ [Symbol.iterator]: null }), TypeError)
t.assert.throws(() => new Headers({ [Symbol.iterator]: undefined }), TypeError)
t.assert.throws(() => {
const obj = { [Symbol.iterator]: null }
Object.defineProperty(obj, Symbol.iterator, { enumerable: false })
new Headers(obj) // eslint-disable-line no-new
}, TypeError)
})
// https://github.com/nodejs/undici/issues/3829
test('Invalid key/value records passed to constructor (issue #3829)', (t) => {
t.assert.throws(
() => new Headers({ [Symbol('x-fake-header')]: '??' }),
new TypeError('Headers constructor: Key Symbol(x-fake-header) in init is a symbol, which cannot be converted to a ByteString.')
)
t.assert.throws(
() => new Headers({ 'x-fake-header': Symbol('why is this here?') }),
new TypeError('Headers constructor: init["x-fake-header"] is a symbol, which cannot be converted to a ByteString.')
)
})
================================================
FILE: test/fetch/headerslist-sortedarray.js
================================================
'use strict'
const { test } = require('node:test')
const { HeadersList, compareHeaderName } = require('../../lib/web/fetch/headers')
const characters = 'abcdefghijklmnopqrstuvwxyz0123456789'
const charactersLength = characters.length
function generateAsciiString (length) {
let result = ''
for (let i = 0; i < length; ++i) {
result += characters[Math.floor(Math.random() * charactersLength)]
}
return result
}
const SORT_RUN = 4000
test('toSortedArray (fast-path)', (t) => {
for (let i = 0; i < SORT_RUN; ++i) {
const headersList = new HeadersList()
for (let j = 0; j < 32; ++j) {
headersList.append(generateAsciiString(4), generateAsciiString(4))
}
t.assert.deepStrictEqual(headersList.toSortedArray(), [...headersList].sort(compareHeaderName))
}
})
test('toSortedArray (slow-path)', (t) => {
for (let i = 0; i < SORT_RUN; ++i) {
const headersList = new HeadersList()
for (let j = 0; j < 64; ++j) {
headersList.append(generateAsciiString(4), generateAsciiString(4))
}
t.assert.deepStrictEqual(headersList.toSortedArray(), [...headersList].sort(compareHeaderName))
}
})
================================================
FILE: test/fetch/http2.js
================================================
'use strict'
const { createSecureServer } = require('node:http2')
const { createServer } = require('node:http')
const { createReadStream, readFileSync } = require('node:fs')
const { once } = require('node:events')
const { Readable } = require('node:stream')
const { test } = require('node:test')
const pem = require('@metcoder95/https-pem')
const { Client, fetch, Headers } = require('../..')
const { closeClientAndServerAsPromise } = require('../utils/node-http')
test('[Fetch] Issue#2311', async (t) => {
const expectedBody = 'hello from client!'
const server = createSecureServer(await pem.generate({ opts: { keySize: 2048 } }), async (req, res) => {
let body = ''
req.setEncoding('utf8')
res.writeHead(200, {
'content-type': 'text/plain; charset=utf-8',
'x-custom-h2': req.headers['x-my-header']
})
for await (const chunk of req) {
body += chunk
}
res.end(body)
})
t.plan(2)
server.listen()
await once(server, 'listening')
const client = new Client(`https://localhost:${server.address().port}`, {
connect: {
rejectUnauthorized: false
},
allowH2: true
})
const response = await fetch(
`https://localhost:${server.address().port}/`,
// Needs to be passed to disable the reject unauthorized
{
method: 'POST',
dispatcher: client,
headers: {
'x-my-header': 'foo',
'content-type': 'text-plain'
},
body: expectedBody
}
)
const responseBody = await response.text()
t.after(closeClientAndServerAsPromise(client, server))
t.assert.strictEqual(responseBody, expectedBody)
t.assert.strictEqual(response.headers.get('x-custom-h2'), 'foo')
})
test('[Fetch] Simple GET with h2', async (t) => {
const server = createSecureServer(await pem.generate({ opts: { keySize: 2048 } }))
const expectedRequestBody = 'hello h2!'
server.on('stream', async (stream, headers) => {
stream.respond({
'content-type': 'text/plain; charset=utf-8',
'x-custom-h2': headers['x-my-header'],
'x-method': headers[':method'],
':status': 200
})
stream.end(expectedRequestBody)
})
t.plan(5)
server.listen()
await once(server, 'listening')
const client = new Client(`https://localhost:${server.address().port}`, {
connect: {
rejectUnauthorized: false
},
allowH2: true
})
const response = await fetch(
`https://localhost:${server.address().port}/`,
// Needs to be passed to disable the reject unauthorized
{
method: 'GET',
dispatcher: client,
headers: {
'x-my-header': 'foo',
'content-type': 'text-plain'
}
}
)
const responseBody = await response.text()
t.after(closeClientAndServerAsPromise(client, server))
t.assert.strictEqual(responseBody, expectedRequestBody)
t.assert.strictEqual(response.headers.get('x-method'), 'GET')
t.assert.strictEqual(response.headers.get('x-custom-h2'), 'foo')
// https://github.com/nodejs/undici/issues/2415
t.assert.throws(() => {
response.headers.get(':status')
}, TypeError)
// See https://fetch.spec.whatwg.org/#concept-response-status-message
t.assert.strictEqual(response.statusText, '')
})
test('[Fetch] Should handle h2 request with body (string or buffer)', async (t) => {
const server = createSecureServer(await pem.generate({ opts: { keySize: 2048 } }))
const expectedBody = 'hello from client!'
const expectedRequestBody = 'hello h2!'
const requestBody = []
server.on('stream', async (stream, headers) => {
stream.on('data', chunk => requestBody.push(chunk))
stream.respond({
'content-type': 'text/plain; charset=utf-8',
'x-custom-h2': headers['x-my-header'],
':status': 200
})
stream.end(expectedRequestBody)
})
t.plan(2)
server.listen()
await once(server, 'listening')
const client = new Client(`https://localhost:${server.address().port}`, {
connect: {
rejectUnauthorized: false
},
allowH2: true
})
const response = await fetch(
`https://localhost:${server.address().port}/`,
// Needs to be passed to disable the reject unauthorized
{
method: 'POST',
dispatcher: client,
headers: {
'x-my-header': 'foo',
'content-type': 'text-plain'
},
body: expectedBody
}
)
const responseBody = await response.text()
t.after(closeClientAndServerAsPromise(client, server))
t.assert.strictEqual(Buffer.concat(requestBody).toString('utf-8'), expectedBody)
t.assert.strictEqual(responseBody, expectedRequestBody)
})
// Skipping for now, there is something odd in the way the body is handled
test(
'[Fetch] Should handle h2 request with body (stream)',
async (t) => {
const server = createSecureServer(await pem.generate({ opts: { keySize: 2048 } }))
const expectedBody = readFileSync(__filename, 'utf-8')
const stream = createReadStream(__filename)
const requestChunks = []
t.plan(8)
server.on('stream', async (stream, headers) => {
t.assert.strictEqual(headers[':method'], 'PUT')
t.assert.strictEqual(headers[':path'], '/')
t.assert.strictEqual(headers[':scheme'], 'https')
stream.respond({
'content-type': 'text/plain; charset=utf-8',
'x-custom-h2': headers['x-my-header'],
':status': 200
})
for await (const chunk of stream) {
requestChunks.push(chunk)
}
stream.end('hello h2!')
})
server.listen(0)
await once(server, 'listening')
const client = new Client(`https://localhost:${server.address().port}`, {
connect: {
rejectUnauthorized: false
},
allowH2: true
})
t.after(closeClientAndServerAsPromise(client, server))
const response = await fetch(
`https://localhost:${server.address().port}/`,
// Needs to be passed to disable the reject unauthorized
{
method: 'PUT',
dispatcher: client,
headers: {
'x-my-header': 'foo',
'content-type': 'text-plain'
},
body: Readable.toWeb(stream),
duplex: 'half'
}
)
const responseBody = await response.text()
t.assert.strictEqual(response.status, 200)
t.assert.strictEqual(response.headers.get('content-type'), 'text/plain; charset=utf-8')
t.assert.strictEqual(response.headers.get('x-custom-h2'), 'foo')
t.assert.strictEqual(responseBody, 'hello h2!')
t.assert.strictEqual(Buffer.concat(requestChunks).toString('utf-8'), expectedBody)
}
)
test('Should handle h2 request with body (Blob)', async (t) => {
const server = createSecureServer(await pem.generate({ opts: { keySize: 2048 } }))
const expectedBody = 'asd'
const requestChunks = []
const body = new Blob(['asd'], {
type: 'text/plain'
})
t.plan(8)
server.on('stream', async (stream, headers) => {
t.assert.strictEqual(headers[':method'], 'POST')
t.assert.strictEqual(headers[':path'], '/')
t.assert.strictEqual(headers[':scheme'], 'https')
stream.on('data', chunk => requestChunks.push(chunk))
stream.respond({
'content-type': 'text/plain; charset=utf-8',
'x-custom-h2': headers['x-my-header'],
':status': 200
})
stream.end('hello h2!')
})
server.listen(0)
await once(server, 'listening')
const client = new Client(`https://localhost:${server.address().port}`, {
connect: {
rejectUnauthorized: false
},
allowH2: true
})
t.after(closeClientAndServerAsPromise(client, server))
const response = await fetch(
`https://localhost:${server.address().port}/`,
// Needs to be passed to disable the reject unauthorized
{
body,
method: 'POST',
dispatcher: client,
headers: {
'x-my-header': 'foo',
'content-type': 'text-plain'
}
}
)
const responseBody = await response.arrayBuffer()
t.assert.strictEqual(response.status, 200)
t.assert.strictEqual(response.headers.get('content-type'), 'text/plain; charset=utf-8')
t.assert.strictEqual(response.headers.get('x-custom-h2'), 'foo')
t.assert.strictEqual(new TextDecoder().decode(responseBody).toString(), 'hello h2!')
t.assert.strictEqual(Buffer.concat(requestChunks).toString('utf-8'), expectedBody)
})
test(
'Should handle h2 request with body (Blob:ArrayBuffer)',
async (t) => {
const server = createSecureServer(await pem.generate({ opts: { keySize: 2048 } }))
const expectedBody = 'hello'
const requestChunks = []
const expectedResponseBody = { hello: 'h2' }
const buf = Buffer.from(expectedBody)
const body = new ArrayBuffer(buf.byteLength)
buf.copy(new Uint8Array(body))
t.plan(8)
server.on('stream', async (stream, headers) => {
t.assert.strictEqual(headers[':method'], 'PUT')
t.assert.strictEqual(headers[':path'], '/')
t.assert.strictEqual(headers[':scheme'], 'https')
stream.on('data', chunk => requestChunks.push(chunk))
stream.respond({
'content-type': 'application/json',
'x-custom-h2': headers['x-my-header'],
':status': 200
})
stream.end(JSON.stringify(expectedResponseBody))
})
server.listen(0)
await once(server, 'listening')
const client = new Client(`https://localhost:${server.address().port}`, {
connect: {
rejectUnauthorized: false
},
allowH2: true
})
t.after(closeClientAndServerAsPromise(client, server))
const response = await fetch(
`https://localhost:${server.address().port}/`,
// Needs to be passed to disable the reject unauthorized
{
body,
method: 'PUT',
dispatcher: client,
headers: {
'x-my-header': 'foo',
'content-type': 'text-plain'
}
}
)
const responseBody = await response.json()
t.assert.strictEqual(response.status, 200)
t.assert.strictEqual(response.headers.get('content-type'), 'application/json')
t.assert.strictEqual(response.headers.get('x-custom-h2'), 'foo')
t.assert.deepStrictEqual(responseBody, expectedResponseBody)
t.assert.strictEqual(Buffer.concat(requestChunks).toString('utf-8'), expectedBody)
}
)
test('Issue#2415', async (t) => {
t.plan(1)
const server = createSecureServer(await pem.generate({ opts: { keySize: 2048 } }))
server.on('stream', async (stream, headers) => {
stream.respond({
':status': 200
})
stream.end('test')
})
server.listen()
await once(server, 'listening')
const client = new Client(`https://localhost:${server.address().port}`, {
connect: {
rejectUnauthorized: false
},
allowH2: true
})
const response = await fetch(
`https://localhost:${server.address().port}/`,
// Needs to be passed to disable the reject unauthorized
{
method: 'GET',
dispatcher: client
}
)
await response.text()
t.after(closeClientAndServerAsPromise(client, server))
t.assert.doesNotThrow(() => new Headers(response.headers))
})
test('Issue #2386', async (t) => {
const server = createSecureServer(await pem.generate({ opts: { keySize: 2048 } }))
const body = Buffer.from('hello')
const requestChunks = []
const expectedResponseBody = { hello: 'h2' }
const controller = new AbortController()
const signal = controller.signal
t.plan(4)
server.on('stream', async (stream, headers) => {
t.assert.strictEqual(headers[':method'], 'PUT')
t.assert.strictEqual(headers[':path'], '/')
t.assert.strictEqual(headers[':scheme'], 'https')
stream.on('data', chunk => requestChunks.push(chunk))
stream.respond({
'content-type': 'application/json',
'x-custom-h2': headers['x-my-header'],
':status': 200
})
stream.end(JSON.stringify(expectedResponseBody))
})
server.listen(0)
await once(server, 'listening')
const client = new Client(`https://localhost:${server.address().port}`, {
connect: {
rejectUnauthorized: false
},
allowH2: true
})
t.after(closeClientAndServerAsPromise(client, server))
await fetch(
`https://localhost:${server.address().port}/`,
// Needs to be passed to disable the reject unauthorized
{
body,
signal,
method: 'PUT',
dispatcher: client,
headers: {
'x-my-header': 'foo',
'content-type': 'text-plain'
}
}
)
controller.abort()
t.assert.ok(true)
})
test('Issue #3046', async (t) => {
const server = createSecureServer(await pem.generate({ opts: { keySize: 2048 } }))
t.plan(6)
server.on('stream', async (stream, headers) => {
t.assert.strictEqual(headers[':method'], 'GET')
t.assert.strictEqual(headers[':path'], '/')
t.assert.strictEqual(headers[':scheme'], 'https')
stream.respond({
'set-cookie': ['hello=world', 'foo=bar'],
'content-type': 'text/html; charset=utf-8',
':status': 200
})
stream.end('Hello World
')
})
server.listen(0)
await once(server, 'listening')
const client = new Client(`https://localhost:${server.address().port}`, {
connect: {
rejectUnauthorized: false
},
allowH2: true
})
t.after(closeClientAndServerAsPromise(client, server))
const response = await fetch(
`https://localhost:${server.address().port}/`,
// Needs to be passed to disable the reject unauthorized
{
method: 'GET',
dispatcher: client
}
)
t.assert.strictEqual(response.status, 200)
t.assert.strictEqual(response.headers.get('content-type'), 'text/html; charset=utf-8')
t.assert.deepStrictEqual(response.headers.getSetCookie(), ['hello=world', 'foo=bar'])
})
// The two following tests ensure that empty POST requests have a Content-Length of 0
// specified, both with and without HTTP/2 enabled.
// The RFC 9110 (see https://httpwg.org/specs/rfc9110.html#field.content-length)
// states it SHOULD have one for methods like POST that define a meaning for enclosed content.
test('[Fetch] Empty POST without h2 has Content-Length', async (t) => {
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.statusCode = 200
res.end(`content-length:${req.headers['content-length']}`)
}).listen(0)
const client = new Client(`http://localhost:${server.address().port}`)
t.after(async () => {
server.close()
await client.close()
})
t.plan(1)
await once(server, 'listening')
const response = await fetch(
`http://localhost:${server.address().port}/`, {
method: 'POST',
dispatcher: client
}
)
const responseBody = await response.text()
t.assert.strictEqual(responseBody, `content-length:${0}`)
})
test('[Fetch] Empty POST with h2 has Content-Length', async (t) => {
const server = createSecureServer(await pem.generate({ opts: { keySize: 2048 } }))
server.on('stream', async (stream, headers) => {
stream.respond({
'content-type': 'text/plain; charset=utf-8',
':status': 200
})
stream.end(`content-length:${headers['content-length']}`)
})
t.plan(1)
server.listen()
await once(server, 'listening')
const client = new Client(`https://localhost:${server.address().port}`, {
connect: {
rejectUnauthorized: false
},
allowH2: true
})
t.after(closeClientAndServerAsPromise(client, server))
const response = await fetch(
`https://localhost:${server.address().port}/`,
// Needs to be passed to disable the reject unauthorized
{
method: 'POST',
dispatcher: client
}
)
const responseBody = await response.text()
t.assert.strictEqual(responseBody, `content-length:${0}`)
})
================================================
FILE: test/fetch/includes-credentials.js
================================================
'use strict'
const { test } = require('node:test')
const { includesCredentials } = require('../../lib/web/fetch/util')
test('includesCredentials returns true for URL with both username and password', () => {
const url = new URL('http://user:pass@example.com')
require('node:assert').strictEqual(includesCredentials(url), true)
})
test('includesCredentials returns true for URL with only username', () => {
const url = new URL('http://user@example.com')
require('node:assert').strictEqual(includesCredentials(url), true)
})
test('includesCredentials returns true for URL with only password', () => {
const url = new URL('http://:pass@example.com')
require('node:assert').strictEqual(includesCredentials(url), true)
})
test('includesCredentials returns false for URL with no credentials', () => {
const url = new URL('http://example.com')
require('node:assert').strictEqual(includesCredentials(url), false)
})
================================================
FILE: test/fetch/integrity.js
================================================
'use strict'
const { test, after } = require('node:test')
const { createServer } = require('node:http')
const { gzipSync } = require('node:zlib')
const { fetch, setGlobalDispatcher, getGlobalDispatcher, Agent } = require('../..')
const { once } = require('node:events')
const { closeServerAsPromise } = require('../utils/node-http')
const { runtimeFeatures } = require('../../lib/util/runtime-features')
const previousDispatcher = getGlobalDispatcher()
setGlobalDispatcher(new Agent({
keepAliveTimeout: 1,
keepAliveMaxTimeout: 1
}))
after(() => {
setGlobalDispatcher(previousDispatcher)
})
const skip = runtimeFeatures.has('crypto') === false
test('request with correct integrity checksum', { skip }, async (t) => {
const body = 'Hello world!'
const hash = 'wFNeS+K3n/2TKRMFQ2v4iTFOSj+uwF7P/Lt98xrZ5Ro='
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.end(body)
})
t.after(closeServerAsPromise(server))
await once(server.listen(0), 'listening')
const response = await fetch(`http://localhost:${server.address().port}`, {
integrity: `sha256-${hash}`
})
t.assert.strictEqual(body, await response.text())
})
test('request with wrong integrity checksum', { skip }, async (t) => {
const body = 'Hello world!'
const hash = 'c0535e4be2b79ffd93291305436bf889314e4a3faec05ecffcbb7df31ad9e51b'
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.end(body)
}).listen(0)
t.after(closeServerAsPromise(server))
await once(server, 'listening')
const expectedError = new TypeError('fetch failed', {
cause: new Error('integrity mismatch')
})
await t.assert.rejects(fetch(`http://localhost:${server.address().port}`, {
integrity: `sha256-${hash}`
}), expectedError)
})
test('request with integrity checksum on encoded body', { skip }, async (t) => {
const body = 'Hello world!'
const hash = 'wFNeS+K3n/2TKRMFQ2v4iTFOSj+uwF7P/Lt98xrZ5Ro='
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.setHeader('content-encoding', 'gzip')
res.end(gzipSync(body))
})
t.after(closeServerAsPromise(server))
await once(server.listen(0), 'listening')
const response = await fetch(`http://localhost:${server.address().port}`, {
integrity: `sha256-${hash}`
})
t.assert.strictEqual(body, await response.text())
})
test('request with a totally incorrect integrity', { skip }, async (t) => {
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.end()
}).listen(0)
t.after(closeServerAsPromise(server))
await once(server, 'listening')
await t.assert.doesNotReject(fetch(`http://localhost:${server.address().port}`, {
integrity: 'what-integrityisthis'
}))
})
test('request with mixed in/valid integrities', { skip }, async (t) => {
const body = 'Hello world!'
const hash = 'wFNeS+K3n/2TKRMFQ2v4iTFOSj+uwF7P/Lt98xrZ5Ro='
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.end(body)
}).listen(0)
t.after(closeServerAsPromise(server))
await once(server, 'listening')
await t.assert.doesNotReject(fetch(`http://localhost:${server.address().port}`, {
integrity: `invalid-integrity sha256-${hash}`
}))
})
test('request with sha384 hash', { skip }, async (t) => {
const body = 'Hello world!'
const hash = 'hiVfosNuSzCWnq4X3DTHcsvr38WLWEA5AL6HYU6xo0uHgCY/JV615lypu7hkHMz+'
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.end(body)
}).listen(0)
t.after(closeServerAsPromise(server))
await once(server, 'listening')
// request should succeed
await t.assert.doesNotReject(fetch(`http://localhost:${server.address().port}`, {
integrity: `sha384-${hash}`
}))
// request should fail
await t.assert.rejects(fetch(`http://localhost:${server.address().port}`, {
integrity: 'sha384-ypeBEsobvcr6wjGzmiPcTaeG7/gUfE5yuYB3ha/uSLs='
}))
})
test('request with sha512 hash', { skip }, async (t) => {
const body = 'Hello world!'
const hash = '9s3ioPgZMUzd5V/CJ9jX2uPSjMVWIioKitZtkcytSq1glPUXohgjYMmqz2o9wyMWLLb9jN/+2w/gOPVehf+1tg=='
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.end(body)
}).listen(0)
t.after(closeServerAsPromise(server))
await once(server, 'listening')
// request should succeed
await t.assert.doesNotReject(fetch(`http://localhost:${server.address().port}`, {
integrity: `sha512-${hash}`
}))
// request should fail
await t.assert.rejects(fetch(`http://localhost:${server.address().port}`, {
integrity: 'sha512-ypeBEsobvcr6wjGzmiPcTaeG7/gUfE5yuYB3ha/uSLs='
}))
})
test('request with sha512 hash', { skip }, async (t) => {
const body = 'Hello world!'
const hash384 = 'hiVfosNuSzCWnq4X3DTHcsvr38WLWEA5AL6HYU6xo0uHgCY/JV615lypu7hkHMz+'
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.end(body)
}).listen(0)
t.after(closeServerAsPromise(server))
await once(server, 'listening')
// request should fail
await t.assert.rejects(fetch(`http://localhost:${server.address().port}`, {
integrity: `sha512-${hash384} sha384-${hash384}`
}))
})
test('request with correct integrity checksum (base64url)', { skip }, async (t) => {
t.plan(1)
const body = 'Hello world!'
const hash = 'wFNeS-K3n_2TKRMFQ2v4iTFOSj-uwF7P_Lt98xrZ5Ro'
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.end(body)
})
after(closeServerAsPromise(server))
await once(server.listen(0), 'listening')
const response = await fetch(`http://localhost:${server.address().port}`, {
integrity: `sha256-${hash}`
})
t.assert.strictEqual(body, await response.text())
})
test('request with incorrect integrity checksum (base64url)', { skip }, async (t) => {
t.plan(1)
const body = 'Hello world!'
// base64url for 'invalid' sha256
const hash = '8SNNdReNiSoTOkEDVaWpkM910vM-uiXVdZQ9TfYy86Q'
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.end(body)
})
after(closeServerAsPromise(server))
await once(server.listen(0), 'listening')
await t.assert.rejects(fetch(`http://localhost:${server.address().port}`, {
integrity: `sha256-${hash}`
}))
})
test('request with incorrect integrity checksum (only dash)', { skip }, async (t) => {
t.plan(1)
const body = 'Hello world!'
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.end(body)
})
after(closeServerAsPromise(server))
await once(server.listen(0), 'listening')
await t.assert.rejects(fetch(`http://localhost:${server.address().port}`, {
integrity: 'sha256--'
}))
})
test('request with incorrect integrity checksum (non-ascii character)', { skip }, async (t) => {
t.plan(1)
const body = 'Hello world!'
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.end(body)
})
after(closeServerAsPromise(server))
await once(server.listen(0), 'listening')
await t.assert.rejects(() => fetch(`http://localhost:${server.address().port}`, {
integrity: 'sha256-ä'
}))
})
test('request with incorrect stronger integrity checksum (non-ascii character)', { skip }, async (t) => {
t.plan(2)
const body = 'Hello world!'
const sha256 = 'wFNeS+K3n/2TKRMFQ2v4iTFOSj+uwF7P/Lt98xrZ5Ro='
const sha384 = 'ä'
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.end(body)
})
after(closeServerAsPromise(server))
await once(server.listen(0), 'listening')
await t.assert.rejects(() => fetch(`http://localhost:${server.address().port}`, {
integrity: `sha256-${sha256} sha384-${sha384}`
}))
await t.assert.rejects(() => fetch(`http://localhost:${server.address().port}`, {
integrity: `sha384-${sha384} sha256-${sha256}`
}))
})
test('request with correct integrity checksum (base64). mixed', { skip }, async (t) => {
t.plan(6)
const body = 'Hello world!'
const sha256 = 'wFNeS+K3n/2TKRMFQ2v4iTFOSj+uwF7P/Lt98xrZ5Ro='
const sha384 = 'hiVfosNuSzCWnq4X3DTHcsvr38WLWEA5AL6HYU6xo0uHgCY/JV615lypu7hkHMz+'
const sha512 = '9s3ioPgZMUzd5V/CJ9jX2uPSjMVWIioKitZtkcytSq1glPUXohgjYMmqz2o9wyMWLLb9jN/+2w/gOPVehf+1tg=='
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.end(body)
})
after(closeServerAsPromise(server))
await once(server.listen(0), 'listening')
let response
response = await fetch(`http://localhost:${server.address().port}`, {
integrity: `sha256-${sha256} sha512-${sha512}`
})
t.assert.strictEqual(body, await response.text())
response = await fetch(`http://localhost:${server.address().port}`, {
integrity: `sha512-${sha512} sha256-${sha256}`
})
t.assert.strictEqual(body, await response.text())
response = await fetch(`http://localhost:${server.address().port}`, {
integrity: `sha384-${sha384} sha512-${sha512}`
})
t.assert.strictEqual(body, await response.text())
response = await fetch(`http://localhost:${server.address().port}`, {
integrity: `sha384-${sha384} sha512-${sha512}`
})
t.assert.strictEqual(body, await response.text())
response = await fetch(`http://localhost:${server.address().port}`, {
integrity: `sha256-${sha256} sha384-${sha384}`
})
t.assert.strictEqual(body, await response.text())
response = await fetch(`http://localhost:${server.address().port}`, {
integrity: `sha384-${sha384} sha256-${sha256}`
})
t.assert.strictEqual(body, await response.text())
})
test('request with correct integrity checksum (base64url). mixed', { skip }, async (t) => {
t.plan(6)
const body = 'Hello world!'
const sha256 = 'wFNeS-K3n_2TKRMFQ2v4iTFOSj-uwF7P_Lt98xrZ5Ro'
const sha384 = 'hiVfosNuSzCWnq4X3DTHcsvr38WLWEA5AL6HYU6xo0uHgCY_JV615lypu7hkHMz-'
const sha512 = '9s3ioPgZMUzd5V_CJ9jX2uPSjMVWIioKitZtkcytSq1glPUXohgjYMmqz2o9wyMWLLb9jN_-2w_gOPVehf-1tg'
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.end(body)
})
after(closeServerAsPromise(server))
await once(server.listen(0), 'listening')
let response
response = await fetch(`http://localhost:${server.address().port}`, {
integrity: `sha256-${sha256} sha512-${sha512}`
})
t.assert.strictEqual(body, await response.text())
response = await fetch(`http://localhost:${server.address().port}`, {
integrity: `sha512-${sha512} sha256-${sha256}`
})
t.assert.strictEqual(body, await response.text())
response = await fetch(`http://localhost:${server.address().port}`, {
integrity: `sha384-${sha384} sha512-${sha512}`
})
t.assert.strictEqual(body, await response.text())
response = await fetch(`http://localhost:${server.address().port}`, {
integrity: `sha384-${sha384} sha512-${sha512}`
})
t.assert.strictEqual(body, await response.text())
response = await fetch(`http://localhost:${server.address().port}`, {
integrity: `sha256-${sha256} sha384-${sha384}`
})
t.assert.strictEqual(body, await response.text())
response = await fetch(`http://localhost:${server.address().port}`, {
integrity: `sha384-${sha384} sha256-${sha256}`
})
t.assert.strictEqual(body, await response.text())
})
================================================
FILE: test/fetch/issue-1447.js
================================================
'use strict'
const { test } = require('node:test')
const undici = require('../..')
const { fetch: theoreticalGlobalFetch } = require('../../undici-fetch')
test('Mocking works with both fetches', async (t) => {
t.plan(3)
const mockAgent = new undici.MockAgent()
const body = JSON.stringify({ foo: 'bar' })
mockAgent.disableNetConnect()
const previousDispatcher = undici.getGlobalDispatcher()
undici.setGlobalDispatcher(mockAgent)
t.after(() => {
undici.setGlobalDispatcher(previousDispatcher)
})
const pool = mockAgent.get('https://example.com')
pool.intercept({
path: '/path',
method: 'POST',
body (bodyString) {
t.assert.strictEqual(bodyString, body)
return true
}
}).reply(200, { ok: 1 }).times(2)
const url = new URL('https://example.com/path').href
// undici fetch from node_modules
await undici.fetch(url, {
method: 'POST',
body
})
// the global fetch bundled with esbuild
await theoreticalGlobalFetch(url, {
method: 'POST',
body
})
})
================================================
FILE: test/fetch/issue-1711.js
================================================
'use strict'
const { once } = require('node:events')
const { createServer } = require('node:http')
const { test } = require('node:test')
const { fetch } = require('../..')
test('Redirecting a bunch does not cause a MaxListenersExceededWarning', async (t) => {
let redirects = 0
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
if (redirects === 15) {
res.end('Okay goodbye')
return
}
res.writeHead(302, {
Location: `/${redirects++}`
})
res.end()
}).listen(0)
t.after(server.close.bind(server))
await once(server, 'listening')
process.emitWarning = t.assert.fail.bind(t)
const url = `http://localhost:${server.address().port}`
const response = await fetch(url, { redirect: 'follow' })
t.assert.deepStrictEqual(response.url, `${url}/${redirects - 1}`)
})
test(
'aborting a Stream throws',
() => {
return new Promise((resolve, reject) => {
const httpServer = createServer({ joinDuplicateHeaders: true }, (request, response) => {
response.end(new Uint8Array(20000))
}).listen(async () => {
const serverAddress = httpServer.address()
if (typeof serverAddress === 'object') {
const abortController = new AbortController()
const readStream = (await fetch(`http://localhost:${serverAddress?.port}`, { signal: abortController.signal })).arrayBuffer()
abortController.abort()
setTimeout(reject)
try {
await readStream
} catch {
httpServer.close()
resolve()
}
}
})
})
}
)
================================================
FILE: test/fetch/issue-2009.js
================================================
'use strict'
const { test } = require('node:test')
const { fetch } = require('../..')
const { createServer } = require('node:http')
const { once } = require('node:events')
const { closeServerAsPromise } = require('../utils/node-http')
test('issue 2009', async (t) => {
t.plan(10)
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.setHeader('a', 'b')
res.flushHeaders()
res.socket.end()
}).listen(0)
t.after(closeServerAsPromise(server))
await once(server, 'listening')
for (let i = 0; i < 10; i++) {
await t.assert.doesNotReject(
fetch(`http://localhost:${server.address().port}`).then(
async (resp) => {
await resp.body.cancel('Some message')
}
)
)
}
})
================================================
FILE: test/fetch/issue-2021.js
================================================
'use strict'
const { test } = require('node:test')
const { once } = require('node:events')
const { createServer } = require('node:http')
const { fetch } = require('../..')
const { closeServerAsPromise } = require('../utils/node-http')
// https://github.com/nodejs/undici/issues/2021
test('content-length header is removed on redirect', async (t) => {
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
if (req.url === '/redirect') {
res.writeHead(302, { Location: '/redirect2' })
res.end()
return
}
res.end()
}).listen(0).unref()
t.after(closeServerAsPromise(server))
await once(server, 'listening')
const body = 'a+b+c'
await t.assert.doesNotReject(fetch(`http://localhost:${server.address().port}/redirect`, {
method: 'POST',
body,
headers: {
'content-length': Buffer.byteLength(body)
}
}))
})
================================================
FILE: test/fetch/issue-2171.js
================================================
'use strict'
const { fetch } = require('../..')
const { once } = require('node:events')
const { createServer } = require('node:http')
const { test } = require('node:test')
const { closeServerAsPromise } = require('../utils/node-http')
test('error reason is forwarded - issue #2171', async (t) => {
const server = createServer({ joinDuplicateHeaders: true }, () => {}).listen(0)
t.after(closeServerAsPromise(server))
await once(server, 'listening')
const timeout = AbortSignal.timeout(100)
await t.assert.rejects(
fetch(`http://localhost:${server.address().port}`, {
signal: timeout
}),
{
name: 'TimeoutError',
code: DOMException.TIMEOUT_ERR
}
)
})
================================================
FILE: test/fetch/issue-2242.js
================================================
'use strict'
const { beforeEach, describe, it } = require('node:test')
const { fetch } = require('../..')
const nodeFetch = require('../../index-fetch')
describe('Issue #2242', () => {
['Already aborted', null, false, true, 123, Symbol('Some reason')].forEach(
(reason) =>
describe(`when an already-aborted signal's reason is \`${String(
reason
)}\``, () => {
let signal
beforeEach(() => {
signal = AbortSignal.abort(reason)
})
it('rejects with that reason ', async (t) => {
await t.assert.rejects(fetch('http://localhost', { signal }), (err) => {
t.assert.strictEqual(err, reason)
return true
})
})
it('rejects with that reason (from index-fetch)', async (t) => {
await t.assert.rejects(
nodeFetch.fetch('http://localhost', { signal }),
(err) => {
t.assert.strictEqual(err, reason)
return true
}
)
})
})
)
describe("when an already-aborted signal's reason is `undefined`", () => {
let signal
beforeEach(() => {
signal = AbortSignal.abort(undefined)
})
it('rejects with an `AbortError`', async (t) => {
await t.assert.rejects(
fetch('http://localhost', { signal }),
new DOMException('This operation was aborted', 'AbortError')
)
})
it('rejects with an `AbortError` (from index-fetch)', async (t) => {
await t.assert.rejects(
nodeFetch.fetch('http://localhost', { signal }),
new DOMException('This operation was aborted', 'AbortError')
)
})
})
})
================================================
FILE: test/fetch/issue-2294-patch-method.js
================================================
'use strict'
const { test, after } = require('node:test')
const { Request } = require('../..')
test('Using `patch` method emits a warning.', (t) => {
t.plan(1)
const { emitWarning } = process
after(() => {
process.emitWarning = emitWarning
})
process.emitWarning = (warning, options) => {
t.assert.strictEqual(options.code, 'UNDICI-FETCH-patch')
}
// eslint-disable-next-line no-new
new Request('https://a', { method: 'patch' })
})
================================================
FILE: test/fetch/issue-2318.js
================================================
'use strict'
const { test } = require('node:test')
const { once } = require('node:events')
const { createServer } = require('node:http')
const { fetch } = require('../..')
const { closeServerAsPromise } = require('../utils/node-http')
test('Undici overrides user-provided `Host` header', async (t) => {
t.plan(1)
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
t.assert.strictEqual(req.headers.host, `localhost:${server.address().port}`)
res.end()
}).listen(0)
t.after(closeServerAsPromise(server))
await once(server, 'listening')
await fetch(`http://localhost:${server.address().port}`, {
headers: {
host: 'www.idk.org'
}
})
})
================================================
FILE: test/fetch/issue-2828.js
================================================
'use strict'
const { once } = require('node:events')
const { createServer } = require('node:http')
const { test } = require('node:test')
const { Agent, Request, fetch } = require('../..')
test('issue #2828, dispatcher is allowed in RequestInit options', async (t) => {
t.plan(1)
class CustomAgent extends Agent {
dispatch (options, handler) {
options.headers['x-my-header'] = 'hello'
return super.dispatch(...arguments)
}
}
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
t.assert.deepStrictEqual(req.headers['x-my-header'], 'hello')
res.end()
}).listen(0)
t.after(server.close.bind(server))
await once(server, 'listening')
const request = new Request(`http://localhost:${server.address().port}`, {
dispatcher: new CustomAgent()
})
await fetch(request)
})
================================================
FILE: test/fetch/issue-2898-comment.js
================================================
'use strict'
const { once } = require('node:events')
const { createServer } = require('node:http')
const { test } = require('node:test')
const { Agent, Request, fetch } = require('../..')
test('issue #2828, RequestInit dispatcher options overrides Request input dispatcher', async (t) => {
t.plan(2)
class CustomAgentA extends Agent {
dispatch (options, handler) {
options.headers['x-my-header-a'] = 'hello'
return super.dispatch(...arguments)
}
}
class CustomAgentB extends Agent {
dispatch (options, handler) {
options.headers['x-my-header-b'] = 'world'
return super.dispatch(...arguments)
}
}
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
t.assert.strictEqual(req.headers['x-my-header-a'], undefined)
t.assert.strictEqual(req.headers['x-my-header-b'], 'world')
res.end()
}).listen(0)
t.after(server.close.bind(server))
await once(server, 'listening')
const request = new Request(`http://localhost:${server.address().port}`, {
dispatcher: new CustomAgentA()
})
await fetch(request, {
dispatcher: new CustomAgentB()
})
})
================================================
FILE: test/fetch/issue-2898.js
================================================
'use strict'
const { once } = require('node:events')
const { createServer } = require('node:http')
const { test } = require('node:test')
const { fetch } = require('../..')
// https://github.com/nodejs/undici/issues/2898
test('421 requests with a body work as expected', async (t) => {
const expected = 'This is a 421 Misdirected Request response.'
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.statusCode = 421
res.end(expected)
}).listen(0)
t.after(server.close.bind(server))
await once(server, 'listening')
for (const body of [
'hello',
new Uint8Array(Buffer.from('helloworld', 'utf-8'))
]) {
const response = await fetch(`http://localhost:${server.address().port}`, {
method: 'POST',
body
})
t.assert.deepStrictEqual(response.status, 421)
t.assert.deepStrictEqual(await response.text(), expected)
}
})
================================================
FILE: test/fetch/issue-3267.js
================================================
'use strict'
const { Headers } = require('../..')
const { test } = require('node:test')
test('Spreading a Headers object yields 0 symbols', (t) => {
const baseHeaders = { 'x-foo': 'bar' }
const requestHeaders = new Headers({ 'Content-Type': 'application/json' })
const headers = {
...baseHeaders,
...requestHeaders
}
t.assert.deepStrictEqual(headers, { 'x-foo': 'bar' })
t.assert.doesNotThrow(() => new Headers(headers))
})
================================================
FILE: test/fetch/issue-3334.js
================================================
'use strict'
const { test } = require('node:test')
const { once } = require('node:events')
const { createServer } = require('node:http')
const { fetch } = require('../..')
test('a non-empty origin is not appended (issue #3334)', async (t) => {
t.plan(1)
const origin = 'https://origin.example.com'
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
t.assert.strictEqual(req.headers.origin, origin)
res.end()
}).listen(0)
t.after(server.close.bind(server))
await once(server, 'listening')
await fetch(`http://localhost:${server.address().port}`, {
headers: { origin },
body: '',
method: 'POST',
redirect: 'error'
})
})
================================================
FILE: test/fetch/issue-3616.js
================================================
'use strict'
const { createServer } = require('node:http')
const { describe, test, after } = require('node:test')
const { fetch } = require('../..')
const { once } = require('node:events')
describe('https://github.com/nodejs/undici/issues/3616', () => {
const cases = [
'x-gzip',
'gzip',
'deflate',
'br'
]
for (const encoding of cases) {
test(encoding, async t => {
t.plan(2)
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.writeHead(200, {
'Content-Length': '0',
Connection: 'close',
'Content-Encoding': encoding
})
res.end()
})
after(() => {
server.close()
})
server.listen(0)
await once(server, 'listening')
const result = await fetch(`http://localhost:${server.address().port}/`)
t.assert.ok(result.body.getReader())
process.on('uncaughtException', (reason) => {
t.assert.fail('Uncaught Exception:', reason, encoding)
})
await new Promise(resolve => setTimeout(resolve, 100))
t.assert.ok(true)
})
}
})
================================================
FILE: test/fetch/issue-3624.js
================================================
'use strict'
const { test } = require('node:test')
const { once } = require('node:events')
const { createServer } = require('node:http')
const { fetch, FormData } = require('../..')
// https://github.com/nodejs/undici/issues/3624
test('crlf is appended to formdata body (issue #3624)', async (t) => {
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
req.pipe(res)
}).listen(0)
t.after(server.close.bind(server))
await once(server, 'listening')
const fd = new FormData()
fd.set('a', 'b')
fd.set('c', new File(['d'], 'd.txt.exe'), 'd.txt.exe')
const response = await fetch(`http://localhost:${server.address().port}`, {
body: fd,
method: 'POST'
})
t.assert.ok((await response.text()).endsWith('\r\n'))
})
================================================
FILE: test/fetch/issue-3630.js
================================================
'use strict'
const { test } = require('node:test')
const { Request, Agent } = require('../..')
const { getRequestDispatcher } = require('../../lib/web/fetch/request')
test('Cloned request should inherit its dispatcher', (t) => {
const agent = new Agent()
const request = new Request('https://a', { dispatcher: agent })
t.assert.strictEqual(getRequestDispatcher(request), agent)
})
================================================
FILE: test/fetch/issue-3767.js
================================================
'use strict'
const { once } = require('node:events')
const { createServer } = require('node:http')
const { test } = require('node:test')
const { fetch } = require('../..')
// https://github.com/nodejs/undici/issues/3767
test('referrerPolicy unsafe-url is respected', async (t) => {
t.plan(1)
const referrer = 'https://google.com/hello/world'
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
t.assert.deepEqual(req.headers.referer, referrer)
res.end()
}).listen(0)
t.after(server.close.bind(server))
await once(server, 'listening')
await fetch(`http://localhost:${server.address().port}`, {
referrer,
referrerPolicy: 'unsafe-url'
})
})
================================================
FILE: test/fetch/issue-4105.js
================================================
'use strict'
const { once } = require('node:events')
const { createServer } = require('node:http')
const { test } = require('node:test')
const { fetch } = require('../..')
const { PerformanceObserver } = require('node:perf_hooks')
const { createDeferredPromise } = require('../../lib/util/promise')
const isAtLeastv22 = process.versions.node.split('.').map(Number)[0] >= 22
// https://github.com/nodejs/undici/issues/4105
test('markResourceTiming responseStatus is set', { skip: !isAtLeastv22 }, async (t) => {
t.plan(1)
const promise = createDeferredPromise()
const server = createServer((req, res) => {
res.statusCode = 200
res.end('Hello World')
}).listen(0)
t.after(server.close.bind(server))
await once(server, 'listening')
new PerformanceObserver(items => {
items.getEntries().forEach(entry => {
t.assert.strictEqual(entry.responseStatus, 200)
promise.resolve()
})
}).observe({ type: 'resource', buffered: true })
const response = await fetch(`http://localhost:${server.address().port}`)
await response.text()
await promise.promise
})
================================================
FILE: test/fetch/issue-4627.js
================================================
'use strict'
// Regression test for https://github.com/nodejs/undici/issues/4627
// Fetch abort may not take effect when fetch init.redirect = 'error'
// causing SSE connection leak
const { test } = require('node:test')
const { fetch } = require('../..')
const { createServer } = require('node:http')
const { once } = require('node:events')
const { closeServerAsPromise } = require('../utils/node-http')
// This test requires --expose-gc flag
const hasGC = typeof global.gc === 'function'
test('abort should work with redirect: error', { skip: !hasGC, timeout: 3000 }, async (t) => {
let connectionClosed = false
let messagesReceivedAfterAbort = 0
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
Connection: 'keep-alive'
})
// Send data every 20ms for faster test
const interval = setInterval(() => {
res.write(`data: ${Date.now()}\n\n`)
}, 20)
res.on('close', () => {
connectionClosed = true
clearInterval(interval)
})
})
t.after(closeServerAsPromise(server))
await once(server.listen(0), 'listening')
const port = server.address().port
const ac = new AbortController()
const response = await fetch(`http://localhost:${port}/sse`, {
signal: ac.signal,
redirect: 'error'
})
let aborted = false
// Start reading the stream in background
const readPromise = (async () => {
try {
const reader = response.body.getReader()
while (true) {
const { done } = await reader.read()
if (done) break
if (aborted) {
messagesReceivedAfterAbort++
if (messagesReceivedAfterAbort >= 3) {
// Bug confirmed - received multiple messages after abort
reader.cancel()
break
}
}
}
} catch (err) {
// AbortError is expected
if (err.name !== 'AbortError' && err.message !== 'aborted' && !err.message?.includes('cancel')) {
throw err
}
}
})()
// Wait for some data to be received
await new Promise(resolve => setTimeout(resolve, 100))
// Trigger GC to potentially collect the AbortController
global.gc()
// Abort the request
aborted = true
ac.abort()
// Wait for the read to complete or timeout
const timeout = new Promise((_resolve, reject) =>
setTimeout(() => reject(new Error('Read did not complete in time')), 1000)
)
try {
await Promise.race([readPromise, timeout])
} catch (e) {
// If timed out, that's also a bug indication
if (e.message === 'Read did not complete in time') {
messagesReceivedAfterAbort = 999 // Force failure
} else {
throw e
}
}
t.assert.strictEqual(messagesReceivedAfterAbort, 0, 'No data should be received after abort')
// Give some time for the connection to close
await new Promise(resolve => setTimeout(resolve, 100))
t.assert.ok(connectionClosed, 'Server connection should be closed after abort')
})
test('abort should work without redirect: error (control test)', { skip: !hasGC, timeout: 3000 }, async (t) => {
let connectionClosed = false
let messagesReceivedAfterAbort = 0
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
Connection: 'keep-alive'
})
// Send data every 20ms
const interval = setInterval(() => {
res.write(`data: ${Date.now()}\n\n`)
}, 20)
res.on('close', () => {
connectionClosed = true
clearInterval(interval)
})
})
t.after(closeServerAsPromise(server))
await once(server.listen(0), 'listening')
const port = server.address().port
const ac = new AbortController()
// Without redirect: 'error' - this should work correctly
const response = await fetch(`http://localhost:${port}/sse`, {
signal: ac.signal
})
let aborted = false
// Start reading the stream in background
const readPromise = (async () => {
try {
const reader = response.body.getReader()
while (true) {
const { done } = await reader.read()
if (done) break
if (aborted) {
messagesReceivedAfterAbort++
if (messagesReceivedAfterAbort >= 3) {
reader.cancel()
break
}
}
}
} catch (err) {
// AbortError is expected
if (err.name !== 'AbortError' && err.message !== 'aborted' && !err.message?.includes('cancel')) {
throw err
}
}
})()
// Wait for some data to be received
await new Promise(resolve => setTimeout(resolve, 100))
// Trigger GC
global.gc()
// Abort the request
aborted = true
ac.abort()
// Wait for the read to complete
await readPromise
// Give some time for the connection to close
await new Promise(resolve => setTimeout(resolve, 100))
t.assert.strictEqual(messagesReceivedAfterAbort, 0, 'No data should be received after abort')
t.assert.ok(connectionClosed, 'Server connection should be closed after abort')
})
================================================
FILE: test/fetch/issue-4647.js
================================================
'use strict'
const { createServer } = require('node:http')
const { test } = require('node:test')
const { fetch } = require('../..')
// https://github.com/nodejs/undici/issues/4647
test('fetch with mode: no-cors does not hang', async (t) => {
const a = createServer((req, res) => {
res.writeHead(200).end()
}).listen(0)
const b = createServer((req, res) => {
res.writeHead(301, { Location: `http://localhost:${a.address().port}${req.url}` }).end()
}).listen(0)
t.after(() => {
a.close()
b.close()
})
await fetch(`http://localhost:${b.address().port}/abc`, { mode: 'no-cors' })
})
================================================
FILE: test/fetch/issue-4789.js
================================================
'use strict'
const { createServer } = require('node:http')
const { test } = require('node:test')
const { once } = require('node:events')
const { fetch } = require('../..')
// https://github.com/nodejs/undici/issues/4789
test('transferred buffers and extractBody works', { skip: !ArrayBuffer.prototype.transfer }, async (t) => {
const server = createServer((req, res) => {
if (req.url === '/') {
res.writeHead(307, undefined, {
location: '/test'
})
res.end()
return
}
req.pipe(res).on('end', res.end.bind(res))
}).listen(0)
t.after(server.close.bind(server))
await once(server, 'listening')
{
const response = await fetch(`http://localhost:${server.address().port}`, {
method: 'POST',
body: new TextEncoder().encode('test')
})
t.assert.strictEqual(await response.text(), 'test')
}
{
const response = await fetch(`http://localhost:${server.address().port}`, {
method: 'POST',
body: Buffer.from('test')
})
t.assert.strictEqual(await response.text(), 'test')
}
{
const buffer = new TextEncoder().encode('test')
buffer.buffer.transfer()
const response = await fetch(`http://localhost:${server.address().port}`, {
method: 'POST',
body: buffer
})
// https://webidl.spec.whatwg.org/#dfn-get-buffer-source-copy
// "If IsDetachedBuffer(jsArrayBuffer) is true, then return the empty byte sequence."
t.assert.strictEqual(await response.text(), '')
}
})
================================================
FILE: test/fetch/issue-4799.js
================================================
'use strict'
const { test } = require('node:test')
const { fetch } = require('../..')
const { createServer } = require('node:http')
const { once } = require('node:events')
const { closeServerAsPromise } = require('../utils/node-http')
test('response clone + abort should return AbortError, not TypeError', async (t) => {
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.statusCode = 200
res.setHeader('Content-Type', 'application/json')
res.end(JSON.stringify({ message: 'hello' }))
})
t.after(closeServerAsPromise(server))
server.listen(0)
await once(server, 'listening')
const controller = new AbortController()
const response = await fetch(`http://localhost:${server.address().port}`, {
signal: controller.signal
})
// Clone the response before aborting
const clonedResponse = response.clone()
// Abort after cloning
controller.abort()
// Both original and cloned response should reject with AbortError
await t.test('original response should reject with AbortError', async () => {
await t.assert.rejects(
response.text(),
{
name: 'AbortError',
code: DOMException.ABORT_ERR
}
)
})
await t.test('cloned response should reject with AbortError', async () => {
await t.assert.rejects(
clonedResponse.text(),
{
name: 'AbortError',
code: DOMException.ABORT_ERR
}
)
})
})
test('response without clone + abort should still return AbortError', async (t) => {
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.statusCode = 200
res.setHeader('Content-Type', 'application/json')
res.end(JSON.stringify({ message: 'hello' }))
})
t.after(closeServerAsPromise(server))
server.listen(0)
await once(server, 'listening')
const controller = new AbortController()
const response = await fetch(`http://localhost:${server.address().port}`, {
signal: controller.signal
})
// Abort without cloning
controller.abort()
await t.assert.rejects(
response.text(),
{
name: 'AbortError',
code: DOMException.ABORT_ERR
}
)
})
test('response bodyUsed after clone and abort', async (t) => {
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.statusCode = 200
res.setHeader('Content-Type', 'application/json')
res.end(JSON.stringify({ message: 'hello' }))
})
t.after(closeServerAsPromise(server))
server.listen(0)
await once(server, 'listening')
const controller = new AbortController()
const response = await fetch(`http://localhost:${server.address().port}`, {
signal: controller.signal
})
t.assert.strictEqual(response.bodyUsed, false)
const clonedResponse = response.clone()
t.assert.strictEqual(response.bodyUsed, false)
t.assert.strictEqual(clonedResponse.bodyUsed, false)
controller.abort()
// Erroring a stream (see: https://fetch.spec.whatwg.org/#abort-fetch step 5) does not disturb it.
t.assert.strictEqual(response.bodyUsed, false)
t.assert.strictEqual(clonedResponse.bodyUsed, false)
})
================================================
FILE: test/fetch/issue-4836.js
================================================
'use strict'
const { test } = require('node:test')
const { createServer } = require('node:http')
const { once } = require('node:events')
const { fetch } = require('../..')
const { closeServerAsPromise } = require('../utils/node-http')
// https://github.com/nodejs/undici/issues/4836
test('fetch preserves trailing ? in request URL', async (t) => {
const server = createServer((req, res) => {
res.end(req.url)
}).listen(0)
await once(server, 'listening')
t.after(closeServerAsPromise(server))
const base = `http://localhost:${server.address().port}`
const cases = [
['/echo', '/echo'],
['/echo?', '/echo?'],
['/echo?a=b', '/echo?a=b'],
['/echo?a=b&c=d', '/echo?a=b&c=d'],
['/echo?#frag', '/echo?'],
['/echo#?', '/echo'],
['/echo#frag?bar', '/echo']
]
for (const [path, expected] of cases) {
await t.test(`path: ${path} → ${expected}`, async (t) => {
const res = await fetch(`${base}${path}`)
const body = await res.text()
t.assert.strictEqual(body, expected)
})
}
})
================================================
FILE: test/fetch/issue-4897.js
================================================
'use strict'
const { test } = require('node:test')
const { fetch } = require('../..')
function createAssertingDispatcher (t, expectedPath) {
return {
dispatch (opts, handler) {
t.assert.strictEqual(opts.path, expectedPath)
handler.onError(new Error('stop'))
return true
}
}
}
async function assertPath (t, url, expectedPath) {
const dispatcher = createAssertingDispatcher(t, expectedPath)
await t.assert.rejects(fetch(url, { dispatcher }), (err) => {
t.assert.strictEqual(err.cause?.message, 'stop')
return true
})
}
// https://github.com/nodejs/undici/issues/4897
test('fetch path extraction does not match hostnames inside scheme', async (t) => {
const hosts = ['h', 't', 'p', 'ht', 'tp', 'tt']
for (const scheme of ['http', 'https']) {
for (const host of hosts) {
await t.test(`${scheme}://${host}/test?a=b#frag`, async (t) => {
await assertPath(t, `${scheme}://${host}/test?a=b#frag`, '/test?a=b')
})
}
}
})
================================================
FILE: test/fetch/issue-node-46525.js
================================================
'use strict'
const { once } = require('node:events')
const { createServer } = require('node:http')
const { test } = require('node:test')
const { fetch } = require('../..')
// https://github.com/nodejs/node/issues/46525
test('No warning when reusing AbortController', async (t) => {
function onWarning () {
throw new Error('Got warning')
}
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => res.end()).listen(0)
await once(server, 'listening')
process.on('warning', onWarning)
t.after(() => {
process.off('warning', onWarning)
return server.close()
})
const controller = new AbortController()
for (let i = 0; i < 15; i++) {
await fetch(`http://localhost:${server.address().port}`, { signal: controller.signal })
}
})
================================================
FILE: test/fetch/issue-rsshub-15532.js
================================================
'use strict'
const { once } = require('node:events')
const { createServer } = require('node:http')
const { test } = require('node:test')
const { fetch } = require('../..')
// https://github.com/DIYgod/RSSHub/issues/15532
test('An invalid Origin header is not set', async (t) => {
t.plan(1)
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
t.assert.deepStrictEqual(req.headers.origin, undefined)
res.end()
}).listen(0)
await once(server, 'listening')
t.after(server.close.bind(server))
await fetch(`http://localhost:${server.address().port}`, {
method: 'POST'
})
})
================================================
FILE: test/fetch/iterators.js
================================================
'use strict'
const { test } = require('node:test')
const { Headers, FormData } = require('../..')
test('Implements " Iterator" properly', async (t) => {
await t.test('all Headers iterators implement Headers Iterator', () => {
const headers = new Headers([['a', 'b'], ['c', 'd']])
for (const iterable of ['keys', 'values', 'entries', Symbol.iterator]) {
const gen = headers[iterable]()
// https://tc39.es/ecma262/#sec-%25iteratorprototype%25-object
const IteratorPrototype = Object.getPrototypeOf(Object.getPrototypeOf([][Symbol.iterator]()))
const iteratorProto = Object.getPrototypeOf(gen)
t.assert.ok(gen.constructor === IteratorPrototype.constructor)
t.assert.ok(gen.prototype === undefined)
// eslint-disable-next-line no-proto
t.assert.strictEqual(gen.__proto__[Symbol.toStringTag], 'Headers Iterator')
// https://github.com/node-fetch/node-fetch/issues/1119#issuecomment-100222049
t.assert.ok(!(Headers.prototype[iterable] instanceof function * () {}.constructor))
// eslint-disable-next-line no-proto
t.assert.ok(gen.__proto__.next.__proto__ === Function.prototype)
// https://webidl.spec.whatwg.org/#dfn-iterator-prototype-object
// "The [[Prototype]] internal slot of an iterator prototype object must be %IteratorPrototype%."
t.assert.strictEqual(gen[Symbol.iterator], IteratorPrototype[Symbol.iterator])
t.assert.strictEqual(Object.getPrototypeOf(iteratorProto), IteratorPrototype)
}
})
await t.test('all FormData iterators implement FormData Iterator', () => {
const fd = new FormData()
for (const iterable of ['keys', 'values', 'entries', Symbol.iterator]) {
const gen = fd[iterable]()
// https://tc39.es/ecma262/#sec-%25iteratorprototype%25-object
const IteratorPrototype = Object.getPrototypeOf(Object.getPrototypeOf([][Symbol.iterator]()))
const iteratorProto = Object.getPrototypeOf(gen)
t.assert.ok(gen.constructor === IteratorPrototype.constructor)
t.assert.ok(gen.prototype === undefined)
// eslint-disable-next-line no-proto
t.assert.strictEqual(gen.__proto__[Symbol.toStringTag], 'FormData Iterator')
// https://github.com/node-fetch/node-fetch/issues/1119#issuecomment-100222049
t.assert.ok(!(Headers.prototype[iterable] instanceof function * () {}.constructor))
// eslint-disable-next-line no-proto
t.assert.ok(gen.__proto__.next.__proto__ === Function.prototype)
// https://webidl.spec.whatwg.org/#dfn-iterator-prototype-object
// "The [[Prototype]] internal slot of an iterator prototype object must be %IteratorPrototype%."
t.assert.strictEqual(gen[Symbol.iterator], IteratorPrototype[Symbol.iterator])
t.assert.strictEqual(Object.getPrototypeOf(iteratorProto), IteratorPrototype)
}
})
await t.test('Iterator symbols are properly set', async (t) => {
await t.test('Headers', () => {
const headers = new Headers([['a', 'b'], ['c', 'd']])
const gen = headers.entries()
t.assert.strictEqual(typeof gen[Symbol.toStringTag], 'string')
t.assert.strictEqual(typeof gen[Symbol.iterator], 'function')
})
await t.test('FormData', () => {
const fd = new FormData()
const gen = fd.entries()
t.assert.strictEqual(typeof gen[Symbol.toStringTag], 'string')
t.assert.strictEqual(typeof gen[Symbol.iterator], 'function')
})
})
await t.test('Iterator does not inherit Generator prototype methods', async (t) => {
await t.test('Headers', () => {
const headers = new Headers([['a', 'b'], ['c', 'd']])
const gen = headers.entries()
t.assert.strictEqual(gen.return, undefined)
t.assert.strictEqual(gen.throw, undefined)
t.assert.strictEqual(typeof gen.next, 'function')
})
await t.test('FormData', () => {
const fd = new FormData()
const gen = fd.entries()
t.assert.strictEqual(gen.return, undefined)
t.assert.strictEqual(gen.throw, undefined)
t.assert.strictEqual(typeof gen.next, 'function')
})
})
await t.test('Symbol.iterator', () => {
// Headers
const headerValues = new Headers([['a', 'b']]).entries()[Symbol.iterator]()
t.assert.deepStrictEqual(Array.from(headerValues), [['a', 'b']])
// FormData
const formdata = new FormData()
formdata.set('a', 'b')
const formdataValues = formdata.entries()[Symbol.iterator]()
t.assert.deepStrictEqual(Array.from(formdataValues), [['a', 'b']])
})
await t.test('brand check', () => {
// Headers
t.assert.throws(() => {
const gen = new Headers().entries()
// eslint-disable-next-line no-proto
gen.__proto__.next()
}, TypeError)
// FormData
t.assert.throws(() => {
const gen = new FormData().entries()
// eslint-disable-next-line no-proto
gen.__proto__.next()
}, TypeError)
})
})
================================================
FILE: test/fetch/long-lived-abort-controller.js
================================================
'use strict'
const http = require('node:http')
const { fetch } = require('../../')
const { once, setMaxListeners } = require('node:events')
const { test } = require('node:test')
const { closeServerAsPromise } = require('../utils/node-http')
test('long-lived-abort-controller', async (t) => {
const server = http.createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.writeHead(200, { 'Content-Type': 'text/plain' })
res.end('Hello World!')
})
await once(server.listen(0), 'listening')
t.after(closeServerAsPromise(server))
let emittedWarning = ''
function onWarning (warning) {
emittedWarning = warning
}
process.on('warning', onWarning)
t.after(() => {
process.off('warning', onWarning)
})
const controller = new AbortController()
setMaxListeners(1500, controller.signal)
// The maxListener is set to 1500 in request.js.
// we set it to 2000 to make sure that we are not leaking event listeners.
// Unfortunately we are relying on GC and implementation details here.
for (let i = 0; i < 2000; i++) {
// make request
const res = await fetch(`http://localhost:${server.address().port}`, {
signal: controller.signal
})
// drain body
await res.text()
// eslint-disable-next-line no-undef
gc()
}
t.assert.strictEqual(emittedWarning, '')
})
================================================
FILE: test/fetch/max-listeners.js
================================================
'use strict'
const { setMaxListeners, getMaxListeners, defaultMaxListeners } = require('node:events')
const { test } = require('node:test')
const { Request } = require('../..')
test('test max listeners', (t) => {
const controller = new AbortController()
setMaxListeners(Infinity, controller.signal)
for (let i = 0; i <= defaultMaxListeners; i++) {
// eslint-disable-next-line no-new
new Request('http://asd', { signal: controller.signal })
}
t.assert.strictEqual(getMaxListeners(controller.signal), Infinity)
})
================================================
FILE: test/fetch/pull-dont-push.js
================================================
'use strict'
const { test } = require('node:test')
const { fetch } = require('../..')
const { createServer } = require('node:http')
const { once } = require('node:events')
const { Readable, pipeline } = require('node:stream')
const { setTimeout: sleep } = require('node:timers/promises')
const { closeServerAsPromise } = require('../utils/node-http')
test('pull dont\'t push', async (t) => {
let count = 0
let socket
const max = 1_000_000
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.statusCode = 200
socket = res.socket
// infinite stream
const stream = new Readable({
read () {
this.push('a')
if (count++ > max) {
this.push(null)
}
}
})
pipeline(stream, res, () => {})
})
t.after(closeServerAsPromise(server))
server.listen(0)
await once(server, 'listening')
const res = await fetch(`http://localhost:${server.address().port}`)
// Some time is needed to fill the buffer
await sleep(1000)
socket.destroy()
t.assert.strictEqual(count < max, true) // the stream should be closed before the max
// consume the stream
try {
/* eslint-disable-next-line no-unused-vars */
for await (const chunk of res.body) {
// process._rawDebug('chunk', chunk)
}
} catch {}
})
================================================
FILE: test/fetch/readable-stream-from.js
================================================
'use strict'
const { test } = require('node:test')
const { Response } = require('../..')
// https://github.com/nodejs/node/issues/56474
test('ReadableStream empty enqueue then other enqueued', async (t) => {
const iterable = {
async * [Symbol.asyncIterator] () {
yield ''
yield '3'
yield '4'
}
}
const response = new Response(iterable)
t.assert.deepStrictEqual(await response.text(), '34')
})
test('ReadableStream empty enqueue', async (t) => {
const iterable = {
async * [Symbol.asyncIterator] () {
yield ''
}
}
const response = new Response(iterable)
t.assert.deepStrictEqual(await response.text(), '')
})
================================================
FILE: test/fetch/redirect-cross-origin-header.js
================================================
'use strict'
const { test } = require('node:test')
const { createServer } = require('node:http')
const { once } = require('node:events')
const { fetch } = require('../..')
test('Cross-origin redirects clear forbidden headers', async (t) => {
t.plan(6)
const server1 = createServer({ joinDuplicateHeaders: true }, (req, res) => {
t.assert.strictEqual(req.headers.cookie, undefined)
t.assert.strictEqual(req.headers.authorization, undefined)
t.assert.strictEqual(req.headers['proxy-authorization'], undefined)
res.end('redirected')
}).listen(0)
const server2 = createServer({ joinDuplicateHeaders: true }, (req, res) => {
t.assert.strictEqual(req.headers.authorization, 'test')
t.assert.strictEqual(req.headers.cookie, 'ddd=dddd')
res.writeHead(302, {
...req.headers,
Location: `http://localhost:${server1.address().port}`
})
res.end()
}).listen(0)
t.after(() => {
server1.close()
server2.close()
})
await Promise.all([
once(server1, 'listening'),
once(server2, 'listening')
])
const res = await fetch(`http://localhost:${server2.address().port}`, {
headers: {
Authorization: 'test',
Cookie: 'ddd=dddd',
'Proxy-Authorization': 'test'
}
})
const text = await res.text()
t.assert.strictEqual(text, 'redirected')
})
================================================
FILE: test/fetch/redirect.js
================================================
'use strict'
const { test } = require('node:test')
const { createServer } = require('node:http')
const { once } = require('node:events')
const { fetch } = require('../..')
const { closeServerAsPromise } = require('../utils/node-http')
// https://github.com/nodejs/undici/issues/1776
test('Redirecting with a body does not cancel the current request - #1776', async (t) => {
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
if (req.url === '/redirect') {
res.statusCode = 301
res.setHeader('location', '/redirect/')
res.write('Moved Permanently')
setTimeout(() => res.end(), 500)
return
}
res.write(req.url)
res.end()
}).listen(0)
t.after(closeServerAsPromise(server))
await once(server, 'listening')
const resp = await fetch(`http://localhost:${server.address().port}/redirect`)
t.assert.strictEqual(await resp.text(), '/redirect/')
t.assert.ok(resp.redirected)
})
test('Redirecting with an empty body does not throw an error - #2027', async (t) => {
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
if (req.url === '/redirect') {
res.statusCode = 307
res.setHeader('location', '/redirect/')
res.write('Moved Permanently')
res.end()
return
}
res.write(req.url)
res.end()
}).listen(0)
t.after(closeServerAsPromise(server))
await once(server, 'listening')
const resp = await fetch(`http://localhost:${server.address().port}/redirect`, { method: 'PUT', body: '' })
t.assert.strictEqual(await resp.text(), '/redirect/')
t.assert.ok(resp.redirected)
})
test('Redirecting with a body does not fail to write body - #2543', async (t) => {
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
if (req.url === '/redirect') {
res.writeHead(307, { location: '/target' })
res.write('Moved Permanently')
setTimeout(() => res.end(), 500)
} else {
let body = ''
req.on('data', (chunk) => { body += chunk })
req.on('end', () => t.assert.strictEqual(body, 'body'))
res.write('ok')
res.end()
}
}).listen(0)
t.after(closeServerAsPromise(server))
await once(server, 'listening')
const resp = await fetch(`http://localhost:${server.address().port}/redirect`, {
method: 'POST',
body: 'body'
})
t.assert.strictEqual(await resp.text(), 'ok')
t.assert.ok(resp.redirected)
})
================================================
FILE: test/fetch/referrrer-policy.js
================================================
'use strict'
const { once } = require('node:events')
const { createServer } = require('node:http')
const { describe, test } = require('node:test')
const { fetch } = require('../..')
describe('referrer-policy', () => {
;[
[
'should ignore empty string as policy',
'origin, asdas, asdaw34, no-referrer,,',
'no-referrer'
],
[
'should set referrer policy from response headers on redirect',
'origin',
'origin'
],
[
'should select the first valid police',
'asdas, origin',
'origin'
],
[
'should select the first valid policy #2',
'no-referrer, asdas, origin, 0943sd',
'origin'
],
[
'should pick the last fallback over invalid policy tokens',
'origin, asdas, asdaw34',
'origin'
],
[
'should set not change request referrer policy if no Referrer-Policy from initial redirect response',
null,
'strict-origin-when-cross-origin'
],
[
'should set not change request referrer policy if the policy is a non-valid Referrer Policy',
'asdasd',
'strict-origin-when-cross-origin'
],
[
'should set not change request referrer policy if the policy is a non-valid Referrer Policy #2',
'asdasd, asdasa, 12daw,',
'strict-origin-when-cross-origin'
],
[
'referrer policy is origin',
'origin',
'origin'
],
[
'referrer policy is no-referrer',
'no-referrer',
'no-referrer'
],
[
'referrer policy is strict-origin-when-cross-origin',
'strict-origin-when-cross-origin',
'strict-origin-when-cross-origin'
],
[
'referrer policy is unsafe-url',
'unsafe-url',
'unsafe-url'
]
].forEach(([title, responseReferrerPolicy, expectedReferrerPolicy, referrer]) => {
test(title, async (t) => {
t.plan(1)
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
switch (res.req.url) {
case '/redirect':
res.writeHead(302, undefined, {
Location: '/target',
'referrer-policy': responseReferrerPolicy
})
res.end()
break
case '/target':
switch (expectedReferrerPolicy) {
case 'no-referrer':
t.assert.strictEqual(req.headers['referer'], undefined)
break
case 'origin':
t.assert.strictEqual(req.headers['referer'], `http://127.0.0.1:${port}/`)
break
case 'strict-origin-when-cross-origin':
t.assert.strictEqual(req.headers['referer'], `http://127.0.0.1:${port}/index.html?test=1`)
break
case 'unsafe-url':
t.assert.strictEqual(req.headers['referer'], `http://127.0.0.1:${port}/index.html?test=1`)
break
}
res.writeHead(200, 'dummy', { 'Content-Type': 'text/plain' })
res.end()
break
}
})
server.listen(0)
await once(server, 'listening')
const { port } = server.address()
await fetch(`http://127.0.0.1:${port}/redirect`, {
referrer: referrer || `http://127.0.0.1:${port}/index.html?test=1`
})
server.closeAllConnections()
server.closeIdleConnections()
server.close()
await once(server, 'close')
})
})
})
================================================
FILE: test/fetch/relative-url.js
================================================
'use strict'
const { test, afterEach } = require('node:test')
const { createServer } = require('node:http')
const { once } = require('node:events')
const {
getGlobalOrigin,
setGlobalOrigin,
Response,
Request,
fetch
} = require('../..')
const { closeServerAsPromise } = require('../utils/node-http')
afterEach(() => setGlobalOrigin(undefined))
test('setGlobalOrigin & getGlobalOrigin', (t) => {
t.assert.strictEqual(getGlobalOrigin(), undefined)
setGlobalOrigin('http://localhost:3000')
t.assert.deepStrictEqual(getGlobalOrigin(), new URL('http://localhost:3000'))
setGlobalOrigin(undefined)
t.assert.strictEqual(getGlobalOrigin(), undefined)
setGlobalOrigin(new URL('http://localhost:3000'))
t.assert.deepStrictEqual(getGlobalOrigin(), new URL('http://localhost:3000'))
t.assert.throws(() => {
setGlobalOrigin('invalid.url')
}, TypeError)
t.assert.throws(() => {
setGlobalOrigin('wss://invalid.protocol')
}, TypeError)
t.assert.throws(() => setGlobalOrigin(true))
})
test('Response.redirect', (t) => {
t.assert.throws(() => {
Response.redirect('/relative/path', 302)
}, TypeError('Failed to parse URL from /relative/path'))
t.assert.doesNotThrow(() => {
setGlobalOrigin('http://localhost:3000')
Response.redirect('/relative/path', 302)
})
setGlobalOrigin('http://localhost:3000')
const response = Response.redirect('/relative/path', 302)
// See step #7 of https://fetch.spec.whatwg.org/#dom-response-redirect
t.assert.strictEqual(response.headers.get('location'), 'http://localhost:3000/relative/path')
})
test('new Request', (t) => {
t.assert.throws(
() => new Request('/relative/path'),
TypeError('Failed to parse URL from /relative/path')
)
t.assert.doesNotThrow(() => {
setGlobalOrigin('http://localhost:3000')
// eslint-disable-next-line no-new
new Request('/relative/path')
})
setGlobalOrigin('http://localhost:3000')
const request = new Request('/relative/path')
t.assert.strictEqual(request.url, 'http://localhost:3000/relative/path')
})
test('fetch', async (t) => {
await t.assert.rejects(fetch('/relative/path'), TypeError('Failed to parse URL from /relative/path'))
await t.test('Basic fetch', async (t) => {
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
t.assert.strictEqual(req.url, '/relative/path')
res.end()
}).listen(0)
setGlobalOrigin(`http://localhost:${server.address().port}`)
t.after(closeServerAsPromise(server))
await once(server, 'listening')
await t.assert.doesNotReject(fetch('/relative/path'))
})
await t.test('fetch return', async (t) => {
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
t.assert.strictEqual(req.url, '/relative/path')
res.end()
}).listen(0)
setGlobalOrigin(`http://localhost:${server.address().port}`)
t.after(closeServerAsPromise(server))
await once(server, 'listening')
const response = await fetch('/relative/path')
t.assert.strictEqual(response.url, `http://localhost:${server.address().port}/relative/path`)
})
})
================================================
FILE: test/fetch/request-inspect-custom.js
================================================
'use strict'
const { describe, it } = require('node:test')
const util = require('node:util')
const { Request } = require('../../')
describe('Request custom inspection', () => {
it('should return a custom inspect output', (t) => {
const request = new Request('https://example.com/api', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
})
const inspectedOutput = util.inspect(request)
const expectedOutput = "Request {\n method: 'POST',\n url: 'https://example.com/api',\n headers: Headers { 'Content-Type': 'application/json' },\n destination: '',\n referrer: 'about:client',\n referrerPolicy: '',\n mode: 'cors',\n credentials: 'same-origin',\n cache: 'default',\n redirect: 'follow',\n integrity: '',\n keepalive: false,\n isReloadNavigation: false,\n isHistoryNavigation: false,\n signal: AbortSignal { aborted: false }\n}"
t.assert.strictEqual(inspectedOutput, expectedOutput)
})
})
================================================
FILE: test/fetch/request.js
================================================
/* globals AbortController */
'use strict'
const { test } = require('node:test')
const {
Request,
Headers,
fetch
} = require('../../')
test('arg validation', async (t) => {
// constructor
t.assert.throws(() => {
// eslint-disable-next-line
new Request()
}, TypeError)
t.assert.throws(() => {
// eslint-disable-next-line
new Request('http://asd', 0)
}, TypeError)
t.assert.throws(() => {
const url = new URL('http://asd')
url.password = 'asd'
// eslint-disable-next-line
new Request(url)
}, TypeError)
t.assert.throws(() => {
const url = new URL('http://asd')
url.username = 'asd'
// eslint-disable-next-line
new Request(url)
}, TypeError)
t.assert.doesNotThrow(() => {
// eslint-disable-next-line
new Request('http://asd', undefined)
}, TypeError)
t.assert.throws(() => {
// eslint-disable-next-line
new Request('http://asd', {
window: {}
})
}, TypeError)
t.assert.throws(() => {
// eslint-disable-next-line
new Request('http://asd', {
window: 1
})
}, TypeError)
t.assert.throws(() => {
// eslint-disable-next-line
new Request('http://asd', {
mode: 'navigate'
})
})
t.assert.throws(() => {
// eslint-disable-next-line
new Request('http://asd', {
referrerPolicy: 'agjhagna'
})
}, TypeError)
t.assert.throws(() => {
// eslint-disable-next-line
new Request('http://asd', {
mode: 'agjhagna'
})
}, TypeError)
t.assert.throws(() => {
// eslint-disable-next-line
new Request('http://asd', {
credentials: 'agjhagna'
})
}, TypeError)
t.assert.throws(() => {
// eslint-disable-next-line
new Request('http://asd', {
cache: 'agjhagna'
})
}, TypeError)
t.assert.throws(() => {
// eslint-disable-next-line
new Request('http://asd', {
method: 'agjhagnaöööö'
})
}, TypeError)
t.assert.throws(() => {
// eslint-disable-next-line
new Request('http://asd', {
method: 'TRACE'
})
}, TypeError)
t.assert.throws(() => {
Request.prototype.destination.toString()
}, TypeError)
t.assert.throws(() => {
Request.prototype.referrer.toString()
}, TypeError)
t.assert.throws(() => {
Request.prototype.referrerPolicy.toString()
}, TypeError)
t.assert.throws(() => {
Request.prototype.mode.toString()
}, TypeError)
t.assert.throws(() => {
Request.prototype.credentials.toString()
}, TypeError)
t.assert.throws(() => {
Request.prototype.cache.toString()
}, TypeError)
t.assert.throws(() => {
Request.prototype.redirect.toString()
}, TypeError)
t.assert.throws(() => {
Request.prototype.integrity.toString()
}, TypeError)
t.assert.throws(() => {
Request.prototype.keepalive.toString()
}, TypeError)
t.assert.throws(() => {
Request.prototype.isReloadNavigation.toString()
}, TypeError)
t.assert.throws(() => {
Request.prototype.isHistoryNavigation.toString()
}, TypeError)
t.assert.throws(() => {
Request.prototype.signal.toString()
}, TypeError)
t.assert.throws(() => {
// eslint-disable-next-line no-unused-expressions
Request.prototype.body
}, TypeError)
t.assert.throws(() => {
// eslint-disable-next-line no-unused-expressions
Request.prototype.bodyUsed
}, TypeError)
t.assert.throws(() => {
Request.prototype.clone.call(null)
}, TypeError)
t.assert.doesNotThrow(() => {
Request.prototype[Symbol.toStringTag].charAt(0)
})
for (const method of [
'text',
'json',
'arrayBuffer',
'blob',
'formData'
]) {
await t.assert.rejects(async () => {
await new Request('http://localhost')[method].call({
blob () {
return {
text () {
return Promise.resolve('emulating this')
}
}
}
})
}, TypeError)
}
})
test('undefined window', (t) => {
t.assert.doesNotThrow(() => new Request('http://asd', { window: undefined }))
})
test('undefined body', (t) => {
const req = new Request('http://asd', { body: undefined })
t.assert.strictEqual(req.body, null)
})
test('undefined method', (t) => {
const req = new Request('http://asd', { method: undefined })
t.assert.strictEqual(req.method, 'GET')
})
test('undefined headers', (t) => {
const req = new Request('http://asd', { headers: undefined })
t.assert.strictEqual([...req.headers.entries()].length, 0)
})
test('undefined referrer', (t) => {
const req = new Request('http://asd', { referrer: undefined })
t.assert.strictEqual(req.referrer, 'about:client')
})
test('undefined referrerPolicy', (t) => {
const req = new Request('http://asd', { referrerPolicy: undefined })
t.assert.strictEqual(req.referrerPolicy, '')
})
test('undefined mode', (t) => {
const req = new Request('http://asd', { mode: undefined })
t.assert.strictEqual(req.mode, 'cors')
})
test('undefined credentials', (t) => {
const req = new Request('http://asd', { credentials: undefined })
t.assert.strictEqual(req.credentials, 'same-origin')
})
test('undefined cache', (t) => {
const req = new Request('http://asd', { cache: undefined })
t.assert.strictEqual(req.cache, 'default')
})
test('undefined redirect', (t) => {
const req = new Request('http://asd', { redirect: undefined })
t.assert.strictEqual(req.redirect, 'follow')
})
test('undefined keepalive', (t) => {
const req = new Request('http://asd', { keepalive: undefined })
t.assert.strictEqual(req.keepalive, false)
})
test('undefined integrity', (t) => {
const req = new Request('http://asd', { integrity: undefined })
t.assert.strictEqual(req.integrity, '')
})
test('null integrity', (t) => {
const req = new Request('http://asd', { integrity: null })
t.assert.strictEqual(req.integrity, 'null')
})
test('undefined signal', (t) => {
const req = new Request('http://asd', { signal: undefined })
t.assert.strictEqual(req.signal.aborted, false)
})
test('pre aborted signal', (t) => {
const ac = new AbortController()
ac.abort('gwak')
const req = new Request('http://asd', { signal: ac.signal })
t.assert.strictEqual(req.signal.aborted, true)
t.assert.strictEqual(req.signal.reason, 'gwak')
})
test('post aborted signal', (t) => {
t.plan(2)
const ac = new AbortController()
const req = new Request('http://asd', { signal: ac.signal })
t.assert.strictEqual(req.signal.aborted, false)
ac.signal.addEventListener('abort', () => {
t.assert.strictEqual(req.signal.reason, 'gwak')
}, { once: true })
ac.abort('gwak')
})
test('pre aborted signal cloned', (t) => {
const ac = new AbortController()
ac.abort('gwak')
const req = new Request('http://asd', { signal: ac.signal }).clone()
t.assert.strictEqual(req.signal.aborted, true)
t.assert.strictEqual(req.signal.reason, 'gwak')
})
test('URLSearchParams body with Headers object - issue #1407', async (t) => {
const body = new URLSearchParams({
abc: 123
})
const request = new Request(
'http://localhost',
{
method: 'POST',
body,
headers: {
Authorization: 'test'
}
}
)
t.assert.strictEqual(request.headers.get('content-type'), 'application/x-www-form-urlencoded;charset=UTF-8')
t.assert.strictEqual(request.headers.get('authorization'), 'test')
t.assert.strictEqual(await request.text(), 'abc=123')
})
test('post aborted signal cloned', (t) => {
t.plan(2)
const ac = new AbortController()
const req = new Request('http://asd', { signal: ac.signal }).clone()
t.assert.strictEqual(req.signal.aborted, false)
ac.signal.addEventListener('abort', () => {
t.assert.strictEqual(req.signal.reason, 'gwak')
}, { once: true })
ac.abort('gwak')
})
test('Passing headers in init', async (t) => {
// https://github.com/nodejs/undici/issues/1400
await t.test('Headers instance', (t) => {
const req = new Request('http://localhost', {
headers: new Headers({ key: 'value' })
})
t.assert.strictEqual(req.headers.get('key'), 'value')
})
await t.test('key:value object', (t) => {
const req = new Request('http://localhost', {
headers: { key: 'value' }
})
t.assert.strictEqual(req.headers.get('key'), 'value')
})
await t.test('[key, value][]', (t) => {
const req = new Request('http://localhost', {
headers: [['key', 'value']]
})
t.assert.strictEqual(req.headers.get('key'), 'value')
})
})
test('Symbol.toStringTag', (t) => {
const req = new Request('http://localhost')
t.assert.strictEqual(req[Symbol.toStringTag], 'Request')
t.assert.strictEqual(Request.prototype[Symbol.toStringTag], 'Request')
})
test('invalid RequestInit values', (t) => {
/* eslint-disable no-new */
t.assert.throws(() => {
new Request('http://l', { mode: 'CoRs' })
}, TypeError, 'not exact case = error')
t.assert.throws(() => {
new Request('http://l', { mode: 'random' })
}, TypeError)
t.assert.throws(() => {
new Request('http://l', { credentials: 'OMIt' })
}, TypeError, 'not exact case = error')
t.assert.throws(() => {
new Request('http://l', { credentials: 'random' })
}, TypeError)
t.assert.throws(() => {
new Request('http://l', { cache: 'DeFaULt' })
}, TypeError, 'not exact case = error')
t.assert.throws(() => {
new Request('http://l', { cache: 'random' })
}, TypeError)
t.assert.throws(() => {
new Request('http://l', { redirect: 'FOllOW' })
}, TypeError, 'not exact case = error')
t.assert.throws(() => {
new Request('http://l', { redirect: 'random' })
}, TypeError)
/* eslint-enable no-new */
})
test('RequestInit.signal option', async (t) => {
t.assert.throws(() => {
// eslint-disable-next-line no-new
new Request('http://asd', {
signal: true
})
}, TypeError)
await t.assert.rejects(fetch('http://asd', {
signal: false
}), TypeError)
})
// https://github.com/nodejs/undici/issues/2050
test('set-cookie headers get cleared when passing a Request as first param', (t) => {
const req1 = new Request('http://localhost', {
headers: {
'set-cookie': 'a=1'
}
})
t.assert.deepStrictEqual([...req1.headers], [['set-cookie', 'a=1']])
const req2 = new Request(req1, { headers: {} })
t.assert.deepStrictEqual([...req1.headers], [['set-cookie', 'a=1']])
t.assert.deepStrictEqual([...req2.headers], [])
t.assert.deepStrictEqual(req2.headers.getSetCookie(), [])
})
// https://github.com/nodejs/undici/issues/2124
test('request.referrer', (t) => {
for (const referrer of ['about://client', 'about://client:1234']) {
const request = new Request('http://a', { referrer })
t.assert.strictEqual(request.referrer, 'about:client')
}
})
// https://github.com/nodejs/undici/issues/2445
test('Clone the set-cookie header when Request is passed as the first parameter and no header is passed.', (t) => {
const request = new Request('http://localhost', { headers: { 'set-cookie': 'A' } })
const request2 = new Request(request)
t.assert.deepStrictEqual([...request.headers], [['set-cookie', 'A']])
request2.headers.append('set-cookie', 'B')
t.assert.deepStrictEqual([...request.headers], [['set-cookie', 'A']])
t.assert.strictEqual(request.headers.getSetCookie().join(', '), request.headers.get('set-cookie'))
t.assert.strictEqual(request2.headers.getSetCookie().join(', '), request2.headers.get('set-cookie'))
})
// Tests for optimization introduced in https://github.com/nodejs/undici/pull/2456
test('keys to object prototypes method', (t) => {
const request = new Request('http://localhost', { method: 'hasOwnProperty' })
t.assert.ok(typeof request.method === 'string')
})
// https://github.com/nodejs/undici/issues/2465
test('Issue#2465', async (t) => {
t.plan(1)
const request = new Request('http://localhost', { body: new SharedArrayBuffer(0), method: 'POST' })
t.assert.strictEqual(await request.text(), '[object SharedArrayBuffer]')
})
================================================
FILE: test/fetch/resource-timing.js
================================================
'use strict'
const { test } = require('node:test')
const { createServer } = require('node:http')
const { fetch, Agent } = require('../..')
const { closeServerAsPromise } = require('../utils/node-http')
const {
PerformanceObserver,
performance
} = require('node:perf_hooks')
test('should create a PerformanceResourceTiming after each fetch request', (t, done) => {
t.plan(8)
const obs = new PerformanceObserver(list => {
const expectedResourceEntryName = `http://localhost:${server.address().port}/`
const entries = list.getEntries()
t.assert.strictEqual(entries.length, 1)
const [entry] = entries
t.assert.strictEqual(entry.name, expectedResourceEntryName)
t.assert.strictEqual(entry.entryType, 'resource')
t.assert.ok(entry.duration >= 0)
t.assert.ok(entry.startTime >= 0)
const entriesByName = list.getEntriesByName(expectedResourceEntryName)
t.assert.strictEqual(entriesByName.length, 1)
t.assert.deepStrictEqual(entriesByName[0], entry)
obs.disconnect()
performance.clearResourceTimings()
done()
})
obs.observe({ entryTypes: ['resource'] })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.end('ok')
}).listen(0, async () => {
const body = await fetch(`http://localhost:${server.address().port}`)
t.assert.strictEqual('ok', await body.text())
})
t.after(closeServerAsPromise(server))
})
test('should include encodedBodySize in performance entry', (t, done) => {
t.plan(4)
const obs = new PerformanceObserver(list => {
const [entry] = list.getEntries()
t.assert.strictEqual(entry.encodedBodySize, 2)
t.assert.strictEqual(entry.decodedBodySize, 2)
t.assert.strictEqual(entry.transferSize, 2 + 300)
obs.disconnect()
performance.clearResourceTimings()
done()
})
obs.observe({ entryTypes: ['resource'] })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.end('ok')
}).listen(0, async () => {
const body = await fetch(`http://localhost:${server.address().port}`)
t.assert.strictEqual('ok', await body.text())
})
t.after(closeServerAsPromise(server))
})
test('timing entries should be in order', (t, done) => {
t.plan(13)
const obs = new PerformanceObserver(list => {
const [entry] = list.getEntries()
t.assert.ok(entry.startTime > 0)
t.assert.ok(entry.fetchStart >= entry.startTime)
t.assert.ok(entry.domainLookupStart >= entry.fetchStart)
t.assert.ok(entry.domainLookupEnd >= entry.domainLookupStart)
t.assert.ok(entry.connectStart >= entry.domainLookupEnd)
t.assert.ok(entry.connectEnd >= entry.connectStart)
t.assert.ok(entry.requestStart >= entry.connectEnd)
t.assert.ok(entry.responseStart >= entry.requestStart)
t.assert.ok(entry.responseEnd >= entry.responseStart)
t.assert.ok(entry.duration > 0)
t.assert.ok(entry.redirectStart === 0)
t.assert.ok(entry.redirectEnd === 0)
obs.disconnect()
performance.clearResourceTimings()
done()
})
obs.observe({ entryTypes: ['resource'] })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.end('ok')
}).listen(0, async () => {
const body = await fetch(`http://localhost:${server.address().port}/redirect`)
t.assert.strictEqual('ok', await body.text())
})
t.after(closeServerAsPromise(server))
})
test('redirect timing entries should be included when redirecting', (t, done) => {
t.plan(4)
const obs = new PerformanceObserver(list => {
const [entry] = list.getEntries()
t.assert.ok(entry.redirectStart >= entry.startTime)
t.assert.ok(entry.redirectEnd >= entry.redirectStart)
t.assert.ok(entry.connectStart >= entry.redirectEnd)
obs.disconnect()
performance.clearResourceTimings()
done()
})
obs.observe({ entryTypes: ['resource'] })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
if (req.url === '/redirect') {
res.statusCode = 307
res.setHeader('location', '/redirect/')
res.end()
return
}
res.end('ok')
}).listen(0, async () => {
const body = await fetch(`http://localhost:${server.address().port}/redirect`)
t.assert.strictEqual('ok', await body.text())
})
t.after(closeServerAsPromise(server))
})
test('responseStart should be greater than 0 with composed interceptor', (t, done) => {
t.plan(4)
const obs = new PerformanceObserver(list => {
const [entry] = list.getEntries()
t.assert.ok(entry.requestStart > 0, `requestStart should be > 0, got ${entry.requestStart}`)
t.assert.ok(entry.responseStart > 0, `responseStart should be > 0, got ${entry.responseStart}`)
t.assert.ok(entry.responseStart >= entry.requestStart, 'responseStart should be >= requestStart')
obs.disconnect()
performance.clearResourceTimings()
done()
})
obs.observe({ entryTypes: ['resource'] })
const dispatcher = new Agent().compose((dispatch) => (opts, handler) => dispatch(opts, handler))
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.end('ok')
}).listen(0, async () => {
const body = await fetch(`http://localhost:${server.address().port}`, { dispatcher })
t.assert.strictEqual('ok', await body.text())
})
t.after(closeServerAsPromise(server))
})
================================================
FILE: test/fetch/response-inspect-custom.js
================================================
'use strict'
const { describe, it } = require('node:test')
const util = require('node:util')
const { Response } = require('../../')
describe('Response custom inspection', () => {
it('should return a custom inspect output', (t) => {
const response = new Response(null)
const inspectedOutput = util.inspect(response, {
depth: null,
getters: true
})
const expectedOutput = `Response {
status: 200,
statusText: '',
headers: Headers {},
body: null,
bodyUsed: false,
ok: true,
redirected: false,
type: 'default',
url: ''
}`
t.assert.strictEqual(inspectedOutput, expectedOutput)
})
})
================================================
FILE: test/fetch/response-json.js
================================================
'use strict'
const { test } = require('node:test')
const { Response } = require('../../')
// https://github.com/web-platform-tests/wpt/pull/32825/
const APPLICATION_JSON = 'application/json'
const FOO_BAR = 'foo/bar'
const INIT_TESTS = [
[undefined, 200, '', APPLICATION_JSON, {}],
[{ status: 400 }, 400, '', APPLICATION_JSON, {}],
[{ statusText: 'foo' }, 200, 'foo', APPLICATION_JSON, {}],
[{ headers: {} }, 200, '', APPLICATION_JSON, {}],
[{ headers: { 'content-type': FOO_BAR } }, 200, '', FOO_BAR, {}],
[{ headers: { 'x-foo': 'bar' } }, 200, '', APPLICATION_JSON, { 'x-foo': 'bar' }]
]
test('Check response returned by static json() with init', async (t) => {
for (const [init, expectedStatus, expectedStatusText, expectedContentType, expectedHeaders] of INIT_TESTS) {
const response = Response.json('hello world', init)
t.assert.strictEqual(response.type, 'default', "Response's type is default")
t.assert.strictEqual(response.status, expectedStatus, "Response's status is " + expectedStatus)
t.assert.strictEqual(response.statusText, expectedStatusText, "Response's statusText is " + JSON.stringify(expectedStatusText))
t.assert.strictEqual(response.headers.get('content-type'), expectedContentType, "Response's content-type is " + expectedContentType)
for (const key in expectedHeaders) {
t.assert.strictEqual(response.headers.get(key), expectedHeaders[key], "Response's header " + key + ' is ' + JSON.stringify(expectedHeaders[key]))
}
const data = await response.json()
t.assert.strictEqual(data, 'hello world', "Response's body is 'hello world'")
}
})
test('Throws TypeError when calling static json() with an invalid status', (t) => {
const nullBodyStatus = [204, 205, 304]
for (const status of nullBodyStatus) {
t.assert.throws(() => {
Response.json('hello world', { status })
}, TypeError, `Throws TypeError when calling static json() with a status of ${status}`)
}
})
test('Check static json() encodes JSON objects correctly', async (t) => {
const response = Response.json({ foo: 'bar' })
const data = await response.json()
t.assert.strictEqual(typeof data, 'object', "Response's json body is an object")
t.assert.strictEqual(data.foo, 'bar', "Response's json body is { foo: 'bar' }")
})
test('Check static json() throws when data is not encodable', (t) => {
t.assert.throws(() => {
Response.json(Symbol('foo'))
}, TypeError)
})
test('Check static json() throws when data is circular', (t) => {
const a = { b: 1 }
a.a = a
t.assert.throws(() => {
Response.json(a)
}, TypeError)
})
test('Check static json() propagates JSON serializer errors', (t) => {
class CustomError extends Error {
name = 'CustomError'
}
t.assert.throws(() => {
Response.json({ get foo () { throw new CustomError('bar') } })
}, CustomError)
})
// note: these tests are not part of any WPTs
test('unserializable values', (t) => {
t.assert.throws(() => {
Response.json(Symbol('symbol'))
}, TypeError)
t.assert.throws(() => {
Response.json(undefined)
}, TypeError)
t.assert.throws(() => {
Response.json()
}, TypeError)
})
test('invalid init', (t) => {
t.assert.throws(() => {
Response.json(null, 3)
}, TypeError)
})
================================================
FILE: test/fetch/response.js
================================================
'use strict'
const { describe, test } = require('node:test')
const { setImmediate } = require('node:timers/promises')
const { AsyncLocalStorage } = require('node:async_hooks')
const {
Response,
FormData
} = require('../../')
test('arg validation', async (t) => {
// constructor
t.assert.throws(() => {
// eslint-disable-next-line
new Response(null, 0)
}, TypeError)
t.assert.throws(() => {
// eslint-disable-next-line
new Response(null, {
status: 99
})
}, RangeError)
t.assert.throws(() => {
// eslint-disable-next-line
new Response(null, {
status: 600
})
}, RangeError)
t.assert.throws(() => {
// eslint-disable-next-line
new Response(null, {
status: '600'
})
}, RangeError)
t.assert.throws(() => {
// eslint-disable-next-line
new Response(null, {
statusText: '\u0000'
})
}, TypeError)
for (const nullStatus of [204, 205, 304]) {
t.assert.throws(() => {
// eslint-disable-next-line
new Response(new ArrayBuffer(16), {
status: nullStatus
})
}, TypeError)
}
t.assert.doesNotThrow(() => {
Response.prototype[Symbol.toStringTag].charAt(0)
}, TypeError)
t.assert.throws(() => {
Response.prototype.type.toString()
}, TypeError)
t.assert.throws(() => {
Response.prototype.url.toString()
}, TypeError)
t.assert.throws(() => {
Response.prototype.redirected.toString()
}, TypeError)
t.assert.throws(() => {
Response.prototype.status.toString()
}, TypeError)
t.assert.throws(() => {
Response.prototype.ok.toString()
}, TypeError)
t.assert.throws(() => {
Response.prototype.statusText.toString()
}, TypeError)
t.assert.throws(() => {
Response.prototype.headers.toString()
}, TypeError)
t.assert.throws(() => {
// eslint-disable-next-line no-unused-expressions
Response.prototype.body
}, TypeError)
t.assert.throws(() => {
// eslint-disable-next-line no-unused-expressions
Response.prototype.bodyUsed
}, TypeError)
t.assert.throws(() => {
Response.prototype.clone.call(null)
}, TypeError)
await t.assert.rejects(
new Response('http://localhost').text.call({
blob () {
return {
text () {
return Promise.resolve('emulating response.blob()')
}
}
}
}), TypeError)
})
test('response clone', (t) => {
// https://github.com/nodejs/undici/issues/1122
const response1 = new Response(null, { status: 201 })
const response2 = new Response(undefined, { status: 201 })
t.assert.deepStrictEqual(response1.body, response1.clone().body)
t.assert.deepStrictEqual(response2.body, response2.clone().body)
t.assert.strictEqual(response2.body, null)
})
test('Symbol.toStringTag', (t) => {
const resp = new Response()
t.assert.strictEqual(resp[Symbol.toStringTag], 'Response')
t.assert.strictEqual(Response.prototype[Symbol.toStringTag], 'Response')
})
test('async iterable body', async (t) => {
const asyncIterable = {
async * [Symbol.asyncIterator] () {
yield 'a'
yield 'b'
yield 'c'
}
}
const response = new Response(asyncIterable)
t.assert.strictEqual(await response.text(), 'abc')
})
// https://github.com/nodejs/node/pull/43752#issuecomment-1179678544
test('Modifying headers using Headers.prototype.set', (t) => {
const response = new Response('body', {
headers: {
'content-type': 'test/test',
'Content-Encoding': 'hello/world'
}
})
const response2 = response.clone()
response.headers.set('content-type', 'application/wasm')
response.headers.set('Content-Encoding', 'world/hello')
t.assert.strictEqual(response.headers.get('content-type'), 'application/wasm')
t.assert.strictEqual(response.headers.get('Content-Encoding'), 'world/hello')
response2.headers.delete('content-type')
response2.headers.delete('Content-Encoding')
t.assert.strictEqual(response2.headers.get('content-type'), null)
t.assert.strictEqual(response2.headers.get('Content-Encoding'), null)
})
// https://github.com/nodejs/node/issues/43838
describe('constructing a Response with a ReadableStream body', () => {
const text = '{"foo":"bar"}'
const uint8 = new TextEncoder().encode(text)
test('Readable stream with Uint8Array chunks', async (t) => {
const readable = new ReadableStream({
start (controller) {
controller.enqueue(uint8)
controller.close()
}
})
const response1 = new Response(readable)
const response2 = response1.clone()
const response3 = response1.clone()
t.assert.strictEqual(await response1.text(), text)
t.assert.deepStrictEqual(await response2.arrayBuffer(), uint8.buffer)
t.assert.deepStrictEqual(await response3.json(), JSON.parse(text))
})
test('.arrayBuffer() correctly clones multiple buffers', async (t) => {
const buffer = Buffer.allocUnsafeSlow(2 * 1024 - 2)
const readable = new ReadableStream({
start (controller) {
for (let i = 0; i < buffer.length; i += 128) {
controller.enqueue(buffer.slice(i, i + 128))
}
controller.close()
}
})
const response = new Response(readable)
t.assert.deepStrictEqual(await response.arrayBuffer(), buffer.buffer)
})
test('Readable stream with non-Uint8Array chunks', async (t) => {
const readable = new ReadableStream({
start (controller) {
controller.enqueue(text) // string
controller.close()
}
})
const response = new Response(readable)
await t.assert.rejects(response.text(), TypeError)
})
test('Readable with ArrayBuffer chunk still throws', async (t) => {
const readable = new ReadableStream({
start (controller) {
controller.enqueue(uint8.buffer)
controller.close()
}
})
const response1 = new Response(readable)
const response2 = response1.clone()
const response3 = response1.clone()
const response4 = response1.clone()
await t.assert.rejects(response1.arrayBuffer(), TypeError)
await t.assert.rejects(response2.text(), TypeError)
await t.assert.rejects(response3.json(), TypeError)
await t.assert.rejects(response4.blob(), TypeError)
})
})
// https://github.com/nodejs/undici/issues/2465
test('Issue#2465', async (t) => {
t.plan(1)
const response = new Response(new SharedArrayBuffer(0))
t.assert.strictEqual(await response.text(), '[object SharedArrayBuffer]')
})
describe('Check the Content-Type of invalid formData', () => {
test('_application/x-www-form-urlencoded', async (t) => {
t.plan(1)
const response = new Response('x=y', { headers: { 'content-type': '_application/x-www-form-urlencoded' } })
await t.assert.rejects(response.formData(), TypeError)
})
test('_multipart/form-data', async (t) => {
t.plan(1)
const formData = new FormData()
formData.append('x', 'y')
const response = new Response(formData, { headers: { 'content-type': '_multipart/form-data' } })
await t.assert.rejects(response.formData(), TypeError)
})
test('application/x-www-form-urlencoded_', async (t) => {
t.plan(1)
const response = new Response('x=y', { headers: { 'content-type': 'application/x-www-form-urlencoded_' } })
await t.assert.rejects(response.formData(), TypeError)
})
test('multipart/form-data_', async (t) => {
t.plan(1)
const formData = new FormData()
formData.append('x', 'y')
const response = new Response(formData, { headers: { 'content-type': 'multipart/form-data_' } })
await t.assert.rejects(response.formData(), TypeError)
})
})
test('clone body garbage collection', async (t) => {
if (typeof global.gc === 'undefined') {
throw new Error('gc is not available. Run with \'--expose-gc\'.')
}
const asyncLocalStorage = new AsyncLocalStorage()
let ref
await new Promise(resolve => {
asyncLocalStorage.run(new Map(), async () => {
const res = new Response('hello world')
const clone = res.clone()
asyncLocalStorage.getStore().set('key', clone)
ref = new WeakRef(clone.body)
await res.text()
await clone.text() // consume body
resolve()
})
})
await setImmediate()
global.gc()
const cloneBody = ref.deref()
t.assert.strictEqual(cloneBody, undefined, 'clone body was not garbage collected')
})
================================================
FILE: test/fetch/spread.js
================================================
'use strict'
const undici = require('../..')
const { test } = require('node:test')
const { inspect } = require('node:util')
test('spreading web classes yields empty objects', (t) => {
for (const object of [
new undici.FormData(),
new undici.Response(null),
new undici.Request('http://a')
]) {
t.assert.deepStrictEqual({ ...object }, {})
}
})
test('Objects only have an expected set of symbols on their prototypes', (t) => {
const allowedSymbols = [
Symbol.iterator,
Symbol.toStringTag,
inspect.custom
]
for (const object of [
undici.FormData,
undici.Response,
undici.Request,
undici.Headers,
undici.WebSocket,
undici.MessageEvent,
undici.CloseEvent,
undici.ErrorEvent,
undici.EventSource
]) {
const symbols = Object.keys(Object.getOwnPropertyDescriptors(object.prototype))
.filter(v => typeof v === 'symbol')
t.assert.ok(symbols.every(symbol => allowedSymbols.includes(symbol)))
}
})
================================================
FILE: test/fetch/user-agent.js
================================================
'use strict'
const { test } = require('node:test')
const events = require('node:events')
const http = require('node:http')
const undici = require('../../')
const { closeServerAsPromise } = require('../utils/node-http')
const nodeBuild = require('../../undici-fetch.js')
test('user-agent defaults correctly', async (t) => {
const server = http.createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.end(JSON.stringify({ userAgentHeader: req.headers['user-agent'] }))
})
t.after(closeServerAsPromise(server))
server.listen(0)
await events.once(server, 'listening')
const url = `http://localhost:${server.address().port}`
const [nodeBuildJSON, undiciJSON] = await Promise.all([
nodeBuild.fetch(url).then((body) => body.json()),
undici.fetch(url).then((body) => body.json())
])
t.assert.strictEqual(nodeBuildJSON.userAgentHeader, 'node')
t.assert.strictEqual(undiciJSON.userAgentHeader, 'undici')
})
test('set user-agent for fetch', async (t) => {
const server = http.createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.end(JSON.stringify({ userAgentHeader: req.headers['user-agent'] }))
})
t.after(closeServerAsPromise(server))
server.listen(0)
await events.once(server, 'listening')
const url = `http://localhost:${server.address().port}`
const [nodeBuildJSON, undiciJSON] = await Promise.all([
nodeBuild.fetch(url, { headers: { 'user-agent': 'AcmeCo Crawler - acme.co - node@acme.co' } }).then((body) => body.json()),
undici.fetch(url, {
headers: { 'user-agent': 'AcmeCo Crawler - acme.co - undici@acme.co' }
}).then((body) => body.json())
])
t.assert.strictEqual(nodeBuildJSON.userAgentHeader, 'AcmeCo Crawler - acme.co - node@acme.co')
t.assert.strictEqual(undiciJSON.userAgentHeader, 'AcmeCo Crawler - acme.co - undici@acme.co')
})
================================================
FILE: test/fetch/util.js
================================================
'use strict'
const { describe, test } = require('node:test')
const util = require('../../lib/web/fetch/util')
const { HeadersList } = require('../../lib/web/fetch/headers')
test('responseURL', (t) => {
t.plan(2)
t.assert.ok(util.responseURL({
urlList: [
new URL('http://asd'),
new URL('http://fgh')
]
}))
t.assert.ok(!util.responseURL({
urlList: []
}))
})
test('responseLocationURL', (t) => {
t.plan(3)
const acceptHeaderList = new HeadersList()
acceptHeaderList.append('Accept', '*/*')
const locationHeaderList = new HeadersList()
locationHeaderList.append('Location', 'http://asd')
t.assert.ok(!util.responseLocationURL({
status: 200
}))
t.assert.ok(!util.responseLocationURL({
status: 301,
headersList: acceptHeaderList
}))
t.assert.ok(util.responseLocationURL({
status: 301,
headersList: locationHeaderList,
urlList: [
new URL('http://asd'),
new URL('http://fgh')
]
}))
})
test('requestBadPort', (t) => {
t.plan(3)
t.assert.strictEqual('allowed', util.requestBadPort({
urlList: [new URL('https://asd')]
}))
t.assert.strictEqual('blocked', util.requestBadPort({
urlList: [new URL('http://asd:7')]
}))
t.assert.strictEqual('blocked', util.requestBadPort({
urlList: [new URL('https://asd:7')]
}))
})
// https://html.spec.whatwg.org/multipage/origin.html#same-origin
// look at examples
test('sameOrigin', async (t) => {
await t.test('first test', (t) => {
const A = {
protocol: 'https:',
hostname: 'example.org',
port: ''
}
const B = {
protocol: 'https:',
hostname: 'example.org',
port: ''
}
t.assert.ok(util.sameOrigin(A, B))
})
await t.test('second test', (t) => {
const A = {
protocol: 'https:',
hostname: 'example.org',
port: '314'
}
const B = {
protocol: 'https:',
hostname: 'example.org',
port: '420'
}
t.assert.ok(!util.sameOrigin(A, B))
})
await t.test('obviously shouldn\'t be equal', (t) => {
t.assert.ok(!util.sameOrigin(
{ protocol: 'http:', hostname: 'example.org' },
{ protocol: 'https:', hostname: 'example.org' }
))
t.assert.ok(!util.sameOrigin(
{ protocol: 'https:', hostname: 'example.org' },
{ protocol: 'https:', hostname: 'example.com' }
))
})
await t.test('file:// urls', (t) => {
// urls with opaque origins should return true
const a = new URL('file:///C:/undici')
const b = new URL('file:///var/undici')
t.assert.ok(util.sameOrigin(a, b))
})
})
test('isURLPotentiallyTrustworthy', (t) => {
// https://datatracker.ietf.org/doc/html/draft-ietf-dnsop-let-localhost-be-localhost#section-5.2
const valid = [
'http://localhost',
'http://localhost.',
'http://127.0.0.1',
'http://[::1]',
'https://something.com',
'wss://hello.com',
'data:text/plain;base64,randomstring',
'about:blank',
'about:srcdoc',
'http://subdomain.localhost',
'http://subdomain.localhost.',
'http://adb.localhost',
'http://localhost.localhost',
'blob:http://example.com/550e8400-e29b-41d4-a716-446655440000'
]
const invalid = [
'http://localhost.example.com',
'http://subdomain.localhost.example.com',
'file:///link/to/file.txt',
'http://121.3.4.5:55',
'null:8080',
'something:8080'
]
t.plan(valid.length + invalid.length + 1)
t.assert.ok(!util.isURLPotentiallyTrustworthy('string'))
for (const url of valid) {
const instance = new URL(url)
t.assert.ok(util.isURLPotentiallyTrustworthy(instance), instance)
}
for (const url of invalid) {
const instance = new URL(url)
t.assert.ok(!util.isURLPotentiallyTrustworthy(instance))
}
})
describe('setRequestReferrerPolicyOnRedirect', () => {
[
[
'should ignore empty string as policy',
'origin, asdas, asdaw34, no-referrer,,',
'no-referrer'
],
[
'should set referrer policy from response headers on redirect',
'origin',
'origin'
],
[
'should select the first valid policy from a response',
'asdas, origin',
'origin'
],
[
'should select the first valid policy from a response#2',
'no-referrer, asdas, origin, 0943sd',
'origin'
],
[
'should pick the last fallback over invalid policy tokens',
'origin, asdas, asdaw34',
'origin'
],
[
'should set not change request referrer policy if no Referrer-Policy from initial redirect response',
null,
'no-referrer, strict-origin-when-cross-origin'
],
[
'should set not change request referrer policy if the policy is a non-valid Referrer Policy',
'asdasd',
'no-referrer, strict-origin-when-cross-origin'
],
[
'should set not change request referrer policy if the policy is a non-valid Referrer Policy #2',
'asdasd, asdasa, 12daw,',
'no-referrer, strict-origin-when-cross-origin'
]
].forEach(([title, responseReferrerPolicy, expected]) => {
test(title, (t) => {
t.plan(1)
const request = {
referrerPolicy: 'no-referrer, strict-origin-when-cross-origin'
}
const actualResponse = {
headersList: new HeadersList()
}
actualResponse.headersList.append('Connection', 'close')
actualResponse.headersList.append('Location', 'https://some-location.com/redirect')
if (responseReferrerPolicy) {
actualResponse.headersList.append('Referrer-Policy', responseReferrerPolicy)
}
util.setRequestReferrerPolicyOnRedirect(request, actualResponse)
t.assert.strictEqual(request.referrerPolicy, expected)
})
})
})
describe('urlHasHttpsScheme', () => {
const { urlHasHttpsScheme } = util
test('should return false for http url', (t) => {
t.assert.strictEqual(urlHasHttpsScheme('http://example.com'), false)
})
test('should return true for https url', (t) => {
t.assert.strictEqual(urlHasHttpsScheme('https://example.com'), true)
})
test('should return false for http object', (t) => {
t.assert.strictEqual(urlHasHttpsScheme({ protocol: 'http:' }), false)
})
test('should return true for https object', (t) => {
t.assert.strictEqual(urlHasHttpsScheme({ protocol: 'https:' }), true)
})
})
describe('isValidHeaderValue', () => {
const { isValidHeaderValue } = util
test('should return true for valid string', (t) => {
t.assert.strictEqual(isValidHeaderValue('valid123'), true)
t.assert.strictEqual(isValidHeaderValue('va lid123'), true)
t.assert.strictEqual(isValidHeaderValue('va\tlid123'), true)
})
test('should return false for string containing NUL', (t) => {
t.assert.strictEqual(isValidHeaderValue('invalid\0'), false)
t.assert.strictEqual(isValidHeaderValue('in\0valid'), false)
t.assert.strictEqual(isValidHeaderValue('\0invalid'), false)
})
test('should return false for string containing CR', (t) => {
t.assert.strictEqual(isValidHeaderValue('invalid\r'), false)
t.assert.strictEqual(isValidHeaderValue('in\rvalid'), false)
t.assert.strictEqual(isValidHeaderValue('\rinvalid'), false)
})
test('should return false for string containing LF', (t) => {
t.assert.strictEqual(isValidHeaderValue('invalid\n'), false)
t.assert.strictEqual(isValidHeaderValue('in\nvalid'), false)
t.assert.strictEqual(isValidHeaderValue('\ninvalid'), false)
})
test('should return false for string with leading TAB', (t) => {
t.assert.strictEqual(isValidHeaderValue('\tinvalid'), false)
})
test('should return false for string with trailing TAB', (t) => {
t.assert.strictEqual(isValidHeaderValue('invalid\t'), false)
})
test('should return false for string with leading SPACE', (t) => {
t.assert.strictEqual(isValidHeaderValue(' invalid'), false)
})
test('should return false for string with trailing SPACE', (t) => {
t.assert.strictEqual(isValidHeaderValue('invalid '), false)
})
})
describe('isOriginIPPotentiallyTrustworthy()', () => {
[
['', false],
['0000:0000:0000:0000:0000:0000:0000:0001', true],
['0001:0000:0000:0000:0000:0000:0000:0001', false],
['0000:0000:0000:0000:0000:0000::0001', true],
['0001:0000:0000:0000:0000:0000::0001', false],
['0000:0000:0001:0000:0000:0000::0001', false],
['0000:0000:0000:0000:0000::0001', true],
['0000:0000:0000:0000::0001', true],
['0000:0000:0000::0001', true],
['0000:0000::0001', true],
['0000::0001', true],
['::1001', false],
['::0001', true],
['::0011', false],
['::1', true],
['[::1]', true],
['[::1', false],
['::1]', false],
['::2', false],
['::', false],
['126.0.0.0', false],
['127.0.0.0', true],
['127.0.0.1', true],
['127.255.255.255', true],
['128.255.255.255', false]
].forEach(([ip, expected]) => {
test(`${ip} is ${expected ? '' : 'not '}potentially trustworthy`, (t) => {
t.assert.strictEqual(util.isOriginIPPotentiallyTrustworthy(ip), expected)
})
})
})
================================================
FILE: test/fixed-queue.js
================================================
'use strict'
const { tspl } = require('@matteo.collina/tspl')
const { test } = require('node:test')
const FixedQueue = require('../lib/dispatcher/fixed-queue')
test('fixed queue 1', (t) => {
t = tspl(t, { plan: 5 })
const queue = new FixedQueue()
t.strictEqual(queue.head, queue.tail)
t.ok(queue.isEmpty())
queue.push('a')
t.ok(!queue.isEmpty())
t.strictEqual(queue.shift(), 'a')
t.strictEqual(queue.shift(), null)
})
test('fixed queue 2', (t) => {
t = tspl(t, { plan: 7 + 2047 })
const queue = new FixedQueue()
for (let i = 0; i < 2047; i++) {
queue.push('a')
}
t.ok(queue.head.isFull())
queue.push('a')
t.ok(!queue.head.isFull())
t.notEqual(queue.head, queue.tail)
for (let i = 0; i < 2047; i++) {
t.strictEqual(queue.shift(), 'a')
}
t.strictEqual(queue.head, queue.tail)
t.ok(!queue.isEmpty())
t.strictEqual(queue.shift(), 'a')
t.ok(queue.isEmpty())
})
================================================
FILE: test/fixtures/ca.pem
================================================
-----BEGIN CERTIFICATE-----
MIIF1zCCA7+gAwIBAgIUCZzRXzKGblWJpjDUDX+847p1PGMwDQYJKoZIhvcNAQEL
BQAwejELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMQswCQYDVQQHDAJTRjEPMA0G
A1UECgwGSm95ZW50MRAwDgYDVQQLDAdOb2RlLmpzMQwwCgYDVQQDDANjYTExIDAe
BgkqhkiG9w0BCQEWEXJ5QHRpbnljbG91ZHMub3JnMCAXDTI0MTAwMTA3NDMzNloY
DzMwMjQwMjAyMDc0MzM2WjB6MQswCQYDVQQGEwJVUzELMAkGA1UECAwCQ0ExCzAJ
BgNVBAcMAlNGMQ8wDQYDVQQKDAZKb3llbnQxEDAOBgNVBAsMB05vZGUuanMxDDAK
BgNVBAMMA2NhMTEgMB4GCSqGSIb3DQEJARYRcnlAdGlueWNsb3Vkcy5vcmcwggIi
MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCbfGWGTmAiFP94HuNdDqINvAyB
ci7xsqa2OgL5sx/0mHpsJKV3DggNZreBn/DGDKqBjgkKJhZ3ZTBjrzsGfKXunj6n
srpOPdm8EicMT7kV4nXvD16q7j2m0QYiUhzc9+gb+9uNmO220ZJUDhKm/LNuwBfR
lAJ7WEaAVt9o1isGhTe95iFHpLNsj4nQ79XQZGoql8WsheRYaRBsgYDsccgfCvhH
3/H+IZN1Zn5ITq9+WmUAu17q40vc4DSrpNWhIJY/CZGgg8tIHSYx6xbAD7CaHb2N
sJwFbCre/Mpk5gRwh83/RCBryZ8ETBysSTs+XCJbQFMgHr0RuSL0BTqSe+Kc2RaP
oMytGkosULd91nG6PIP6KXBCzICpUhqvxDMmX4HFZ6E7iqbKoOnhbWWLROFEwGm4
mWDws2Cf20XrhVDMcusm1lZUVv707EeS7KaxbXbtut9egkdb+u8xAkhlJV877G0p
1LYpwkKul7Rb/WtF1pMXz8kVLkiBQ8neAnIwYqycD+AWPD72yi2l25Lva1ORzdnY
/3+iE3qq9G7D9Wymj60BzEIDfgWdQ7hbREX7AvgHb/jUwXNI3keUoMKm0y8LSVCn
anJjttduMvKEY4LUBrQmIkJIijnXJqfnTzahssnhMli6TaBDhgKFXCtufS+OhPjK
6gklbY03T5oG5dpvEwIDAQABo1MwUTAdBgNVHQ4EFgQUMii3SZU8I+FEmIBfkoo/
E3rMG+cwHwYDVR0jBBgwFoAUMii3SZU8I+FEmIBfkoo/E3rMG+cwDwYDVR0TAQH/
BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAgEACipNr6ibOCtfvfRyCM2XMgeW7FF3
KtNzZLqm+/0RwXiPZwxxYI2XVeTXLrZfrsBEK0oimeQhV/RCPe7t9ILGSNvPa8+Q
HqrPt90FxQGiCSrUhgIz+VhbKRd9OJaTiOR/dnqA1To9TPnxjwBX2iEGbAyX5eqy
bdeDuC0pB+2dSkJ9FtwaHjQfcBBlkk2xSHvvkWCVpd53xXBhVPRjzXPkTk1AOl9e
uDDtaUAKndofh4I17IAYHrRUgLsFf/xrHfIGHFqhkVOz+iTHdKDD8wLMZlr6DVlk
yNOdlIC1XZrvTsr4SyiMxvuNaArAePG26udlaoYznd8fU4hbp+4Nn1QCNpn3brVx
vee5+Yz8zEv3iUGl+B5rjAdW3mcpB3qijKGdBF8qROBt6qYkmuMZEJP1oeI9LItX
v6hpWRVA+9jP6Zjt56W/B+2ETKdIFg6eQBbGDkyAu7cv7OMsq/YstVN/HPxFg/p3
rdxNVwqcnJ07cCVSnrbxdUHhL/Vcw8mBfDjez4BZUrFqen5O6r+WY1sM86Ex7IV5
QTbRgaKiDW4SmqTu4++VOeHKp3pjm9UyFHB1jrPxJbm+P2lLn41n7LUU7Q35ce8D
xBoDu3SIeoaF/e54+o4Pn0WDjs0zTV4YDMI2Zkt/QK5fLPx0VQBrxDl4MkcN7DnC
1UV2bT78VPpeGn8=
-----END CERTIFICATE-----
================================================
FILE: test/fixtures/cert.pem
================================================
-----BEGIN CERTIFICATE-----
MIIFyTCCA7GgAwIBAgIUVxjGOc+76Ux6YyeJUVSmTCrp7CowDQYJKoZIhvcNAQEL
BQAwejELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMQswCQYDVQQHDAJTRjEPMA0G
A1UECgwGSm95ZW50MRAwDgYDVQQLDAdOb2RlLmpzMQwwCgYDVQQDDANjYTExIDAe
BgkqhkiG9w0BCQEWEXJ5QHRpbnljbG91ZHMub3JnMCAXDTI0MTAwMTA3NDMzNloY
DzMwMjQwMjAyMDc0MzM2WjB9MQswCQYDVQQGEwJVUzELMAkGA1UECAwCQ0ExCzAJ
BgNVBAcMAlNGMQ8wDQYDVQQKDAZKb3llbnQxEDAOBgNVBAsMB05vZGUuanMxDzAN
BgNVBAMMBmFnZW50MTEgMB4GCSqGSIb3DQEJARYRcnlAdGlueWNsb3Vkcy5vcmcw
ggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCwNx4WYcYywrczaqneQ7n8
5Y+dXT06dh9uunJyg42UEzKQ+Oa3uiFR8mrNd2P9zgPgdu/je94TU0su7h7xRHz+
tIsr8S5FpeFNRzqe6q/20Qv2dJ+ZVqRvrJ0j9Kva2qgp5YGuD6e1ivcepJHHs7Cg
T6XLliKkEaaxkX4p/9pp4vwsKV0bL92qhhWrWGxtoTDts9D/hBncTZf2WSRS26uC
3XWnZqSx4gYRbb/1uFVdNOlGlqbypEMwpFOu7uYhA6o/Sj6euzzFrQlc3vjsGNSx
LhW/uTFWF6ou9Zqwa4d3g9yxVCFQEAnfZzUGmo6DKu3wn2vFfaCS/2qN55LYZCq3
VJpziPUFHlu8iPSEn1s3U8vwSqfehbjynQ45DjWeFkI9gBtAUGMJ0iXVgfyivO53
Jgvc3+pA621h216dcdn5hPilzHXQYS+xDv1DcM9wNbbZVee847N/88Xbi/FPOCIM
qWVEihYq8aaKlLzXfETUaDFufmxx0m1hP7RjrklPunAgzRou9ombdVkVhnmTHH/n
OqRjY7uwNXj0eW7wwZDdPxnGSBZV8ePUzzWDjEV6VMoaitI+lzfOUf+e/mZrQVof
TMSynhFNnLssNqg5HKe4P45D0bWjz93+X0vYpNrKeFZSeHpTZYGESQ0A6baoGvw5
LqgcT0aWxezzYF7IRBKvRwIDAQABo0IwQDAdBgNVHQ4EFgQUUj4+P8JxihhKlG1q
zZP9KTqQhNwwHwYDVR0jBBgwFoAUMii3SZU8I+FEmIBfkoo/E3rMG+cwDQYJKoZI
hvcNAQELBQADggIBAFwoYo6NKF9fyjI29341PQoivLT8QzD72nnoFtdemmDOPARE
AKJtOyrVc/H0w4CtolK+gjTazVvVwv5FLZsRtvqoWGuzSGdgANGskHonT8iOZLyQ
chwB0oC6iyyGmXkDnAAlsR7vp6duJRaHI9uDrO9SqRSbVF2TP5kdSzKoVK44t+bP
c7/Cp5T9PBssHpXuq2y3vxFHAjJDnwuw8mXd1CSYw6GtDYj/eVMNukOwa1wZkDH2
o32V9c9oNceIFuI9O0F52H76U7Hnl7FGIO6BL67yeapkWTOl38j97+KHsXuMYe4f
kVJnT6uUPuwva1zSc/X8Db9ZjAPG82nI9puMYZEQugjgdIB8PnkRbgwFXUvAXJ3U
0CzymCnth0UviSsU0zluz87oOS8KH9jWI8Ul4d6wmiPRgwdt/sc/VvJ04RzM0v6s
WmsGxjc3ff5rV5Cn/EF/s8nPjoVSlimoxrlmEIKz8tI1lHyccpDK7TzYdup4Z7Oy
6Bt+7+PAyl974U4ptgSozjaKnOsw9OGIo9g6g4te9D5EDiHOC32Mja47i7UaM8en
nmGH7W0L1Fj26CELlsrs5Chm0JXCyKxPcJK7pyKLAFOhXFYp5YsFyI2fGDmrQI58
WLChV8nOTHWo1XrzKhTNB4tLPSXa6AcRYLEHpU0kbZyTC2La9zwyHVCnPMbn
-----END CERTIFICATE-----
================================================
FILE: test/fixtures/docker/dante/Dockerfile
================================================
FROM alpine:latest
# Install Dante SOCKS server
RUN apk add --no-cache dante-server
# Create dante user
RUN adduser -D -s /bin/false dante
# Copy configuration
COPY danted.conf /etc/danted.conf
# Create log directory
RUN mkdir -p /var/log/dante && chown dante:dante /var/log/dante
# Expose SOCKS port
EXPOSE 1080
# Run Dante
CMD ["danted", "-f", "/etc/danted.conf"]
================================================
FILE: test/fixtures/docker/dante/danted.conf
================================================
# Dante SOCKS5 server configuration for testing
# Log settings
logoutput: /var/log/dante/danted.log
debug: 1
# Network interface configuration
internal: 0.0.0.0 port = 1080
external: eth0
# Authentication methods
socksmethod: none
socksmethod: username
# User for username/password auth
user.privileged: root
user.unprivileged: dante
# Client access rules
client pass {
from: 0.0.0.0/0 to: 0.0.0.0/0
log: error
}
# SOCKS rules
socks pass {
from: 0.0.0.0/0 to: 0.0.0.0/0
protocol: tcp udp
log: error
}
# Route rules
route {
from: 0.0.0.0/0 to: 0.0.0.0/0 via: eth0
protocol: tcp udp
proxyprotocol: socks_v5
}
================================================
FILE: test/fixtures/docker/docker-compose.yml
================================================
services:
# SOCKS5 proxy without authentication
socks5-no-auth:
image: serjs/go-socks5-proxy:latest
container_name: socks5-no-auth
environment:
- PROXY_PORT=1080
- REQUIRE_AUTH=false
ports:
- "1080:1080"
networks:
- test-network
# SOCKS5 proxy with username/password authentication
socks5-auth:
image: serjs/go-socks5-proxy:latest
container_name: socks5-auth
environment:
- PROXY_USER=testuser
- PROXY_PASSWORD=testpass
- PROXY_PORT=1081
ports:
- "1081:1081"
networks:
- test-network
# Alternative: Dante SOCKS5 server (more configurable)
dante-socks5:
build:
context: ./dante
dockerfile: Dockerfile
container_name: dante-socks5
ports:
- "1082:1080"
networks:
- test-network
volumes:
- ./dante/danted.conf:/etc/danted.conf:ro
# HTTP test server
http-server:
image: node:20-alpine
container_name: http-test-server
working_dir: /app
volumes:
- ../servers/http-server.js:/app/server.js:ro
command: node server.js
ports:
- "8080:8080"
networks:
- test-network
# HTTPS test server
https-server:
image: node:20-alpine
container_name: https-test-server
working_dir: /app
volumes:
- ../servers/https-server.js:/app/server.js:ro
- ../certs:/app/certs:ro
command: node server.js
ports:
- "8443:8443"
networks:
- test-network
# Echo server for testing
echo-server:
image: ealen/echo-server:latest
container_name: echo-server
environment:
- PORT=3000
ports:
- "3000:3000"
networks:
- test-network
# Blocked target (for testing connection failures)
blocked-server:
image: alpine:latest
container_name: blocked-server
command: sleep infinity
networks:
- isolated-network
networks:
test-network:
driver: bridge
isolated-network:
driver: bridge
internal: true
================================================
FILE: test/fixtures/duplicate-debug.js
================================================
'use strict'
const { createServer } = require('node:http')
const { request } = require('../..')
// Simulate the scenario where diagnostics module is loaded multiple times
// This mimics having both Node.js built-in undici and undici as dependency
delete require.cache[require.resolve('../../lib/core/diagnostics.js')]
require('../../lib/core/diagnostics.js')
const server = createServer({ joinDuplicateHeaders: true }, (_req, res) => {
res.writeHead(200, { 'Content-Type': 'text/plain' })
res.end('hello world')
})
server.listen(0, () => {
const { port, address, family } = server.address()
const hostname = family === 'IPv6' ? `[${address}]` : address
request(`http://${hostname}:${port}`)
.then(res => res.body.dump())
.then(() => {
server.close()
})
})
================================================
FILE: test/fixtures/fetch.js
================================================
'use strict'
const { createServer } = require('node:http')
const { fetch } = require('../..')
const server = createServer({ joinDuplicateHeaders: true }, (_req, res) => {
res.writeHead(200, { 'Content-Type': 'text/plain' })
res.end('hello world')
})
server.listen(0, () => {
const { port, address, family } = server.address()
const hostname = family === 'IPv6' ? `[${address}]` : address
fetch(`http://${hostname}:${port}`)
.then(
res => res.body.cancel(),
() => {}
)
.then(() => {
server.close()
})
})
================================================
FILE: test/fixtures/interceptors/retry-event-loop.js
================================================
'use strict'
const { createServer } = require('node:http')
const { once } = require('node:events')
const {
Client,
interceptors: { retry }
} = require('../../..')
const server = createServer({ joinDuplicateHeaders: true })
server.on('request', (req, res) => {
res.writeHead(418, { 'Content-Type': 'text/plain' })
res.end('teapot')
})
server.listen(0)
once(server, 'listening').then(() => {
const client = new Client(
`http://localhost:${server.address().port}`
).compose(
retry({
maxTimeout: 1000,
maxRetries: 3,
statusCodes: [418]
})
)
return client.request({
method: 'GET',
path: '/'
})
})
================================================
FILE: test/fixtures/key.pem
================================================
-----BEGIN PRIVATE KEY-----
MIIJQgIBADANBgkqhkiG9w0BAQEFAASCCSwwggkoAgEAAoICAQCwNx4WYcYywrcz
aqneQ7n85Y+dXT06dh9uunJyg42UEzKQ+Oa3uiFR8mrNd2P9zgPgdu/je94TU0su
7h7xRHz+tIsr8S5FpeFNRzqe6q/20Qv2dJ+ZVqRvrJ0j9Kva2qgp5YGuD6e1ivce
pJHHs7CgT6XLliKkEaaxkX4p/9pp4vwsKV0bL92qhhWrWGxtoTDts9D/hBncTZf2
WSRS26uC3XWnZqSx4gYRbb/1uFVdNOlGlqbypEMwpFOu7uYhA6o/Sj6euzzFrQlc
3vjsGNSxLhW/uTFWF6ou9Zqwa4d3g9yxVCFQEAnfZzUGmo6DKu3wn2vFfaCS/2qN
55LYZCq3VJpziPUFHlu8iPSEn1s3U8vwSqfehbjynQ45DjWeFkI9gBtAUGMJ0iXV
gfyivO53Jgvc3+pA621h216dcdn5hPilzHXQYS+xDv1DcM9wNbbZVee847N/88Xb
i/FPOCIMqWVEihYq8aaKlLzXfETUaDFufmxx0m1hP7RjrklPunAgzRou9ombdVkV
hnmTHH/nOqRjY7uwNXj0eW7wwZDdPxnGSBZV8ePUzzWDjEV6VMoaitI+lzfOUf+e
/mZrQVofTMSynhFNnLssNqg5HKe4P45D0bWjz93+X0vYpNrKeFZSeHpTZYGESQ0A
6baoGvw5LqgcT0aWxezzYF7IRBKvRwIDAQABAoICABfIoK15reQdBtgQPfQrZPd+
znb5ZjG1TsHFtXvCSMIjIzCQ/6btnuCuHP81bZAMldZehztHdS5bkCq55gA/c7V3
Dc+1Aj9RR8sD4aQgXfasuXYewInUOWZ/QEhhli54U7kv6mRhZYvpwTfoE2sGVEEW
7vQ/A9bsMPkHf6VQjJy9D7cwMApi2ALTjSouyZe0aWOz4PIT1N+4s1mDJ5VtY8VK
eb5J6tG9hX8ltoKGSjNF2HR4Eflu9Uij7U2Pngz3rytSrIgFEotFsx1PVP6czVxK
sZHKf5+0mvoymRnVsZeOeyOODN7/Ay4dgnktNC39Bddz1Pp3XcxpX+reRiIhxuf0
0LXk4DUt1w7wNOaa15adg6v38oxAkq7kOidxTs5+hOQzafyODGYa3Vw+KaAb1le9
NZikgBXWLirXhlyDF5YbfAKsk+8JhtJc/BRVp4DNdUqz45jFb+VRM9soXhwK/2Za
/PC9I3w5ejz8d0Dd11sT9ySI0A8jv5qtxRbqvzbSYeZav1cAkWCqkIpdjKhdGknO
ywWae1CDU0pFdNx9iSbuse7bTS7SAqXuGLTZQtZECigP2vjW1x5N1ZT/Mas0UmR7
EUgzuNNA87GnlQPKOnBsGo79RiKtoKrO7FJjYR/43M4aDlg0MSEi1s5kcwVOk8L9
3wZi/g2gq6EGtcwhOBAdAoIBAQDyy8OqZHq0tG6qqkJaedSdj1aW+fdjtVv/Vxbd
R5R98JONRwwYjppdi7U1sbcRFsqgmR0fVyPrLvx+KemqTK6PA6J49XlwmL/DuYPS
3y9Va4Fl6HkaiaUG3pK5U3Z0jDUgNnCjTXTgghHp15ooepQNFIc0EwhGBDIOPQME
0ecQD9sFW7a9H0I4u1HnjLnul+ofGf2vfwBkI/n5mNjn7k3tng2zvlLd1cHbgdou
O3dc5nEyCMCdqDze0S9GS9mf2rC3IQWNsCV7aA5pdHWxpYTz3qu7fIhVckHK5s8k
M5joOjxG70fX/z14L50Gb4G02rLWOEoDy2iyYSKmvIO3X5mlAoIBAQC5zGi2/Abm
9l0ZJZTNctwwm0hB9/Ux4C4CrPhi9kqd2Z+i+XsyDrCQ0h/ZSKt3VHdpnItlCpK2
PaSQ9iFS1DUmxxBrZ1hyJjgw9WyXlzAbFfTo4iUt7qJMiOztfsubap9VzbkknE4G
MHQ/hDRPMkh6pwpIHjqRYbg4JNxRey8GtO7zwhDKdURi4paoboWw0VDYxy1MIz9g
d3lE+vr4QDB7cANFJRYaBI3YDxaxpSLSXoOmhbhxD3+11iCDbQaGYIORb23ilESK
3//alSeIhaWzAb2+hwTEI4P7foLKInVx7i/W6kTTlJ2rZumTGUGWIMjoEmQ+w2KO
DBAYwSNlm9l7AoIBAQDQ7Hogg3n7SU/5V6zlQfSs6AzwuYQhrovNetlX7CJhBMVT
SpGkCAHZAUEbRSNsdxpBe7/NmiR0Weg3gEVrn7SNp+kFAOZQ93/8IgTHTfnjHTEp
yhN7vHnfIWNMSf+iZovIflAKlbo+/m3/tOEYd/IyFzoIm2ABL9cK3YFdgmm8Loif
Yb4rm1xWiQn/n97W6q4xuSHNBBIIGdUe7GGpoiw4jkroIpwX+7pm8qQWKGGb9Ufu
cA2fHIfUjFiLuvU3Uu3Bh47Jz4tRV8cfA3HLPczcNP29xXljXYAz4szYL/YhzwrT
V0+RFDeG1iHeydDpGU/Oen1mKoCbDm7M32bQQllpAoIBADnEK+p4gUzd3CQtYw5d
X8hc/yJDjaBsKuH6FV/vY1OgjdmF55+woYTlT7Gmvmjjghz75vsLRoISuE+5trKh
98SOr7Q09XLIH0BZjeGzx+kj8nlVlmmpgBx7le5hNbykcdWjmKShVEDoX7w/xmO5
Jn+73558h4kb8MLD8xwCSKS1LHXtKHtJ6nE0MdM8SaSn75L2mkbJzrKXcsTXo5/7
lRdLxDiDR1PfhppeVpf019bAO/5SJP5B61sFsCYsh5LP/xgApRGFN6pV6p5zMU9o
/hOhvvS11e2FfUt8Ef32qL07aPRQ8gU2d68K2CQ7/gBHQS+mSDSbWtD/PyHzKqY0
xnECggEAf9IVxlK1IX2FsFbNIG1CCVDkj55w5+jHuVdYAEUYIDwQEUMYhQ3GpgsC
DgFWyddlz1ni8gGvZgwvIZTNA0vz3SEKuOAbcdKfqRspuKIksfHnYikr2UUOBH5a
2hCUirh6UC/5cN0FBC0KV9fJTqiKoUqCI4HEWD6uMPzv892rP7Q80CAkAdalm9Ui
k8ZSfwrqfvAx/iE84VrAKKRxhegyQ2+KYZGc0EMlWXz6/GjrVyUKFqDVjaidmlEj
HBXxEWVKcsTit22GsU4Pl8mZS8DRIZm+wwIp60uP98VtXXBiPPEkea1t9D4T5Gi7
zhkPVDbGIYpzFskiOGNjvnvBhwVdVg==
-----END PRIVATE KEY-----
================================================
FILE: test/fixtures/socks5-test-server.js
================================================
'use strict'
const net = require('node:net')
const { AUTH_METHODS, REPLY_CODES } = require('../../lib/core/socks5-client')
/**
* Test SOCKS5 server for unit tests
* Implements SOCKS5 protocol with optional authentication
*/
class TestSocks5Server {
constructor (options = {}) {
this.options = options
this.server = null
this.connections = new Set()
this.requireAuth = options.requireAuth || false
this.validCredentials = options.credentials || { username: 'test', password: 'pass' }
}
async listen (port = 0) {
return new Promise((resolve, reject) => {
this.server = net.createServer((socket) => {
this.connections.add(socket)
this.handleConnection(socket)
socket.on('close', () => {
this.connections.delete(socket)
})
})
this.server.listen(port, (err) => {
if (err) {
reject(err)
} else {
resolve(this.server.address())
}
})
})
}
handleConnection (socket) {
let state = 'handshake'
let buffer = Buffer.alloc(0)
socket.on('data', (data) => {
buffer = Buffer.concat([buffer, data])
if (state === 'handshake') {
this.handleHandshake(socket, buffer, (newBuffer, method) => {
buffer = newBuffer
if (method === AUTH_METHODS.NO_AUTH) {
state = 'connect'
} else if (method === AUTH_METHODS.USERNAME_PASSWORD) {
state = 'auth'
}
})
} else if (state === 'auth') {
this.handleAuth(socket, buffer, (newBuffer, success) => {
buffer = newBuffer
if (success) {
state = 'connect'
} else {
socket.end()
}
})
} else if (state === 'connect') {
this.handleConnect(socket, buffer, (newBuffer) => {
buffer = newBuffer
state = 'relay'
})
}
})
socket.on('error', () => {
// Handle socket errors silently
})
}
handleHandshake (socket, buffer, callback) {
if (buffer.length >= 2) {
const version = buffer[0]
const nmethods = buffer[1]
if (version === 0x05 && buffer.length >= 2 + nmethods) {
const methods = Array.from(buffer.subarray(2, 2 + nmethods))
// Select authentication method
let selectedMethod
if (this.requireAuth && methods.includes(AUTH_METHODS.USERNAME_PASSWORD)) {
selectedMethod = AUTH_METHODS.USERNAME_PASSWORD
} else if (!this.requireAuth && methods.includes(AUTH_METHODS.NO_AUTH)) {
selectedMethod = AUTH_METHODS.NO_AUTH
} else {
selectedMethod = AUTH_METHODS.NO_ACCEPTABLE
}
socket.write(Buffer.from([0x05, selectedMethod]))
callback(buffer.subarray(2 + nmethods), selectedMethod)
}
}
}
handleAuth (socket, buffer, callback) {
if (buffer.length >= 2) {
const version = buffer[0]
if (version !== 0x01) {
socket.write(Buffer.from([0x01, 0x01])) // Failure
callback(buffer, false)
return
}
const usernameLen = buffer[1]
if (buffer.length >= 3 + usernameLen) {
const username = buffer.subarray(2, 2 + usernameLen).toString()
const passwordLen = buffer[2 + usernameLen]
if (buffer.length >= 3 + usernameLen + passwordLen) {
const password = buffer.subarray(3 + usernameLen, 3 + usernameLen + passwordLen).toString()
const success = username === this.validCredentials.username &&
password === this.validCredentials.password
socket.write(Buffer.from([0x01, success ? 0x00 : 0x01]))
callback(buffer.subarray(3 + usernameLen + passwordLen), success)
}
}
}
}
handleConnect (socket, buffer, callback) {
if (buffer.length >= 4) {
const version = buffer[0]
const cmd = buffer[1]
const atyp = buffer[3]
if (version === 0x05 && cmd === 0x01) {
let addressLength = 0
if (atyp === 0x01) {
addressLength = 4 // IPv4
} else if (atyp === 0x03) {
if (buffer.length >= 5) {
addressLength = 1 + buffer[4] // Domain length + domain
} else {
return // Not enough data
}
} else if (atyp === 0x04) {
addressLength = 16 // IPv6
}
if (buffer.length >= 4 + addressLength + 2) {
// Extract target address and port
let targetHost
let offset = 4
if (atyp === 0x01) {
targetHost = Array.from(buffer.subarray(offset, offset + 4)).join('.')
offset += 4
} else if (atyp === 0x03) {
const domainLen = buffer[offset]
offset += 1
targetHost = buffer.subarray(offset, offset + domainLen).toString()
offset += domainLen
}
const targetPort = buffer.readUInt16BE(offset)
// Simulate connection failure if requested
if (this.options.simulateFailure) {
const response = Buffer.concat([
Buffer.from([0x05, REPLY_CODES.CONNECTION_REFUSED, 0x00, 0x01]),
Buffer.from([0, 0, 0, 0]),
Buffer.from([0, 0])
])
socket.write(response)
socket.end()
return
}
// Connect to target
const targetSocket = net.connect(targetPort, targetHost)
targetSocket.on('connect', () => {
// Send success response
const response = Buffer.concat([
Buffer.from([0x05, 0x00, 0x00, 0x01]), // VER, REP, RSV, ATYP
Buffer.from([127, 0, 0, 1]), // Bind address (localhost)
Buffer.allocUnsafe(2) // Bind port
])
response.writeUInt16BE(targetPort, response.length - 2)
socket.write(response)
// Start relaying data
socket.pipe(targetSocket)
targetSocket.pipe(socket)
callback(buffer.subarray(4 + addressLength + 2))
})
targetSocket.on('error', () => {
// Send connection refused
const response = Buffer.concat([
Buffer.from([0x05, REPLY_CODES.CONNECTION_REFUSED, 0x00, 0x01]),
Buffer.from([0, 0, 0, 0]),
Buffer.from([0, 0])
])
socket.write(response)
socket.end()
})
}
}
}
}
async close () {
if (this.server) {
// Close all connections
for (const socket of this.connections) {
socket.destroy()
}
return new Promise((resolve) => {
this.server.close(resolve)
})
}
}
}
module.exports = { TestSocks5Server }
================================================
FILE: test/fixtures/undici.js
================================================
'use strict'
const { createServer } = require('node:http')
const { request } = require('../..')
const server = createServer({ joinDuplicateHeaders: true }, (_req, res) => {
res.writeHead(200, { 'Content-Type': 'text/plain' })
res.end('hello world')
})
server.listen(0, () => {
const { port, address, family } = server.address()
const hostname = family === 'IPv6' ? `[${address}]` : address
request(`http://${hostname}:${port}`)
.then(res => res.body.dump())
.then(() => {
server.close()
})
})
================================================
FILE: test/fixtures/websocket.js
================================================
'use strict'
const { WebSocketServer } = require('ws')
const { WebSocket } = require('../..')
const server = new WebSocketServer({ port: 0 })
server.on('connection', ws => {
ws.close(1000, 'goodbye')
})
server.on('listening', () => {
const { port } = server.address()
const ws = new WebSocket(`ws://localhost:${port}`, 'chat')
ws.addEventListener('close', () => {
server.close()
})
})
================================================
FILE: test/fuzzing/client/client-fuzz-body.js
================================================
'use strict'
const { request, errors } = require('../../..')
const acceptableCodes = [
'ERR_INVALID_ARG_TYPE'
]
async function fuzz (address, results, buf) {
const body = buf
results.body = body
try {
const data = await request(address, { body })
data.body.destroy().on('error', () => {})
} catch (err) {
results.err = err
// Handle any undici errors
if (Object.values(errors).some(undiciError => err instanceof undiciError)) {
// Okay error
} else if (!acceptableCodes.includes(err.code)) {
throw err
}
}
}
module.exports = fuzz
================================================
FILE: test/fuzzing/client/client-fuzz-headers.js
================================================
'use strict'
const { request, errors } = require('../../..')
const acceptableCodes = [
'ERR_INVALID_ARG_TYPE'
]
async function fuzz (address, results, buf) {
const headers = { buf: buf.toString() }
results.body = headers
try {
const data = await request(address, { headers })
data.body.destroy().on('error', () => {})
} catch (err) {
results.err = err
// Handle any undici errors
if (Object.values(errors).some(undiciError => err instanceof undiciError)) {
// Okay error
} else if (!acceptableCodes.includes(err.code)) {
throw err
}
}
}
module.exports = fuzz
================================================
FILE: test/fuzzing/client/client-fuzz-options.js
================================================
'use strict'
const { request, errors } = require('../../..')
const acceptableCodes = [
'ERR_INVALID_URL',
// These are included because '\\ABC' is interpreted as a Windows UNC path and can cause these errors.
'ENOTFOUND',
'EAI_AGAIN',
'ECONNREFUSED'
]
async function fuzz (address, results, buf) {
const optionKeys = ['body', 'path', 'method', 'opaque', 'upgrade', buf]
const options = {}
for (const optionKey of optionKeys) {
if (Math.random() < 0.5) {
options[optionKey] = buf.toString()
}
}
results.options = options
try {
const data = await request(address, options)
data.body.destroy().on('error', () => {})
} catch (err) {
results.err = err
// Handle any undici errors
if (Object.values(errors).some(undiciError => err instanceof undiciError)) {
// Okay error
} else if (!acceptableCodes.includes(err.code)) {
throw err
}
}
}
module.exports = fuzz
================================================
FILE: test/fuzzing/client/index.js
================================================
'use strict'
module.exports = {
clientFuzzBody: require('./client-fuzz-body'),
clientFuzzHeaders: require('./client-fuzz-headers'),
clientFuzzOptions: require('./client-fuzz-options')
}
================================================
FILE: test/fuzzing/fuzzing.test.js
================================================
'use strict'
const { once } = require('node:events')
const fc = require('fast-check')
const netServer = require('./server')
const { describe, before, after, test } = require('node:test')
const {
clientFuzzBody,
clientFuzzHeaders,
clientFuzzOptions
} = require('./client')
// Detect if running in CI (here we use GitHub Workflows)
// https://docs.github.com/en/actions/learn-github-actions/variables#default-environment-variables
const isCI = process.env.CI === 'true'
fc.configureGlobal({
interruptAfterTimeLimit: isCI ? 60_000 /* 1 minute */ : 10_000 /* 10 seconds */,
numRuns: Number.MAX_SAFE_INTEGER
})
describe('fuzzing', { timeout: 600_000 /* 10 minutes */ }, () => {
before(async () => {
netServer.listen(0)
await once(netServer, 'listening')
})
after(() => {
netServer.close()
})
test('body', async () => {
const address = `http://localhost:${netServer.address().port}`
await fc.assert(
fc.asyncProperty(fc.uint8Array(), async (body) => {
body = Buffer.from(body)
const results = {}
await clientFuzzBody(address, results, body)
})
)
})
test('headers', async () => {
const address = `http://localhost:${netServer.address().port}`
await fc.assert(
fc.asyncProperty(fc.uint8Array(), async (body) => {
body = Buffer.from(body)
const results = {}
await clientFuzzHeaders(address, results, body)
})
)
})
test('options', async () => {
const address = `http://localhost:${netServer.address().port}`
await fc.assert(
fc.asyncProperty(fc.uint8Array(), async (body) => {
body = Buffer.from(body)
const results = {}
await clientFuzzOptions(address, results, body)
})
)
})
})
================================================
FILE: test/fuzzing/server/index.js
================================================
'use strict'
const net = require('node:net')
const serverFuzzFns = [
require('./server-fuzz-append-data'),
require('./server-fuzz-split-data')
]
const netServer = net.createServer(socket => {
socket.on('data', data => {
serverFuzzFns[(Math.random() * 2) | 0](socket, data)
})
})
module.exports = netServer
================================================
FILE: test/fuzzing/server/server-fuzz-append-data.js
================================================
'use strict'
function appendData (socket, data) {
socket.end('HTTP/1.1 200 OK' + data)
}
module.exports = appendData
================================================
FILE: test/fuzzing/server/server-fuzz-split-data.js
================================================
'use strict'
function splitData (socket, data) {
const lines = [
'HTTP/1.1 200 OK',
'Date: Sat, 09 Oct 2010 14:28:02 GMT',
'Connection: close',
'',
data
]
for (const line of lines.join('\r\n').split(data)) {
socket.write(line)
}
socket.end()
}
module.exports = splitData
================================================
FILE: test/gc.js
================================================
'use strict'
/* global WeakRef, FinalizationRegistry */
const { tspl } = require('@matteo.collina/tspl')
const { test, after } = require('node:test')
const { createServer } = require('node:net')
const { Client, Pool } = require('..')
const hasGC = typeof global.gc !== 'undefined'
if (hasGC) {
setInterval(() => {
global.gc()
}, 100).unref()
}
test('gc should collect the client if, and only if, there are no active sockets', async t => {
if (!hasGC) {
throw new Error('gc is not available. Run with \'--expose-gc\'.')
}
t = tspl(t, { plan: 4 })
const server = createServer((socket) => {
socket.write('HTTP/1.1 200 OK\r\n')
socket.write('Content-Length: 0\r\n')
socket.write('Keep-Alive: timeout=1s\r\n')
socket.write('Connection: keep-alive\r\n')
socket.write('\r\n\r\n')
})
after(() => server.close())
let weakRef
let disconnected = false
const registry = new FinalizationRegistry((data) => {
t.strictEqual(data, 'test')
t.strictEqual(disconnected, true)
t.strictEqual(weakRef.deref(), undefined)
})
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`, {
keepAliveTimeoutThreshold: 100
})
client.once('disconnect', () => {
disconnected = true
})
weakRef = new WeakRef(client)
registry.register(client, 'test')
client.request({
path: '/',
method: 'GET'
}, (err, { body }) => {
t.ifError(err)
body.resume()
})
})
await t.completed
})
test('gc should collect the pool if, and only if, there are no active sockets', async t => {
if (!hasGC) {
throw new Error('gc is not available. Run with \'--expose-gc\'.')
}
t = tspl(t, { plan: 4 })
const server = createServer((socket) => {
socket.write('HTTP/1.1 200 OK\r\n')
socket.write('Content-Length: 0\r\n')
socket.write('Keep-Alive: timeout=1s\r\n')
socket.write('Connection: keep-alive\r\n')
socket.write('\r\n\r\n')
})
after(() => server.close())
let weakRef
let disconnected = false
const registry = new FinalizationRegistry((data) => {
t.strictEqual(data, 'test')
t.strictEqual(disconnected, true)
t.strictEqual(weakRef.deref(), undefined)
})
server.listen(0, () => {
const pool = new Pool(`http://localhost:${server.address().port}`, {
connections: 1,
keepAliveTimeoutThreshold: 500
})
pool.once('disconnect', () => {
disconnected = true
})
weakRef = new WeakRef(pool)
registry.register(pool, 'test')
pool.request({
path: '/',
method: 'GET'
}, (err, { body }) => {
t.ifError(err)
body.resume()
})
})
await t.completed
})
================================================
FILE: test/get-head-body.js
================================================
'use strict'
const { tspl } = require('@matteo.collina/tspl')
const { test, after } = require('node:test')
const { Client } = require('..')
const { createServer } = require('node:http')
const { Readable } = require('node:stream')
const { kConnect } = require('../lib/core/symbols')
const { kBusy } = require('../lib/core/symbols')
const { wrapWithAsyncIterable } = require('./utils/async-iterators')
test('GET and HEAD with body should reset connection', async (t) => {
t = tspl(t, { plan: 8 + 2 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.end('asd')
})
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.destroy())
client.on('disconnect', () => {
t.ok(true, 'pass')
})
client.request({
path: '/',
body: 'asd',
method: 'GET'
}, (err, data) => {
t.ifError(err)
data.body.resume()
})
const emptyBody = new Readable({
read () {}
})
emptyBody.push(null)
client.request({
path: '/',
body: emptyBody,
method: 'GET'
}, (err, data) => {
t.ifError(err)
data.body.resume()
})
client.request({
path: '/',
body: new Readable({
read () {
this.push(null)
}
}),
method: 'GET'
}, (err, data) => {
t.ifError(err)
data.body.resume()
})
client.request({
path: '/',
body: new Readable({
read () {
this.push('asd')
this.push(null)
}
}),
method: 'GET'
}, (err, data) => {
t.ifError(err)
data.body.resume()
})
client.request({
path: '/',
body: [],
method: 'GET'
}, (err, data) => {
t.ifError(err)
data.body.resume()
})
client.request({
path: '/',
body: wrapWithAsyncIterable(new Readable({
read () {
this.push(null)
}
})),
method: 'GET'
}, (err, data) => {
t.ifError(err)
data.body.resume()
})
client.request({
path: '/',
body: wrapWithAsyncIterable(new Readable({
read () {
this.push('asd')
this.push(null)
}
})),
method: 'GET'
}, (err, data) => {
t.ifError(err)
data.body.resume()
})
})
await t.completed
})
// TODO: Avoid external dependency.
// test('GET with body should work when target parses body as request', async (t) => {
// t = tspl(t, { plan: 4 })
// // This URL will send double responses when receiving a
// // GET request with body.
// const client = new Client('http://feeds.bbci.co.uk')
// after(() => client.close())
// client.request({ method: 'GET', path: '/news/rss.xml', body: 'asd' }, (err, data) => {
// t.ifError(err)
// t.strictEqual(data.statusCode, 200)
// data.body.resume()
// })
// client.request({ method: 'GET', path: '/news/rss.xml', body: 'asd' }, (err, data) => {
// t.ifError(err)
// t.strictEqual(data.statusCode, 200)
// data.body.resume()
// })
// await t.completed
// })
test('HEAD should reset connection', async (t) => {
t = tspl(t, { plan: 8 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.end('asd')
})
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.destroy())
client.once('disconnect', () => {
t.ok(true, 'pass')
})
client.request({
path: '/',
method: 'HEAD'
}, (err, data) => {
t.ifError(err)
data.body.resume()
})
t.strictEqual(client[kBusy], true)
client.request({
path: '/',
method: 'HEAD'
}, (err, data) => {
t.ifError(err)
data.body.resume()
client.once('disconnect', () => {
client[kConnect](() => {
client.request({
path: '/',
method: 'HEAD'
}, (err, data) => {
t.ifError(err)
data.body.resume()
data.body.on('end', () => {
t.ok(true, 'pass')
})
})
t.strictEqual(client[kBusy], true)
})
})
})
t.strictEqual(client[kBusy], true)
})
await t.completed
})
================================================
FILE: test/h2c-client.js
================================================
'use strict'
const { createServer, createSecureServer } = require('node:http2')
const { once } = require('node:events')
const { test } = require('node:test')
const { tspl } = require('@matteo.collina/tspl')
const pem = require('@metcoder95/https-pem')
const { H2CClient, Client } = require('..')
test('Should throw if no h2c origin', async t => {
const planner = tspl(t, { plan: 1 })
planner.throws(() => new H2CClient('https://localhost/'))
await planner.completed
})
test('Should throw if pipelining greather than concurrent streams', async t => {
const planner = tspl(t, { plan: 1 })
planner.throws(() => new H2CClient('http://localhost/', { pipelining: 10, maxConcurrentStreams: 5 }))
await planner.completed
})
test('Should support h2c connection', async t => {
const planner = tspl(t, { plan: 6 })
let authority = ''
const server = createServer((req, res) => {
planner.equal(req.headers[':authority'], authority)
planner.equal(req.headers[':method'], 'GET')
planner.equal(req.headers[':path'], '/')
planner.equal(req.headers[':scheme'], 'http')
res.writeHead(200)
res.end('Hello, world!')
})
server.listen()
await once(server, 'listening')
authority = `localhost:${server.address().port}`
const client = new H2CClient(`http://${authority}/`)
t.after(() => client.close())
t.after(() => server.close())
const response = await client
.request({ path: '/', method: 'GET' })
planner.equal(response.statusCode, 200)
planner.equal(await response.body.text(), 'Hello, world!')
})
test('Should support h2c connection with body', async t => {
const planner = tspl(t, { plan: 3 })
const bodyChunks = []
const server = createServer((req, res) => {
req.on('data', chunk => bodyChunks.push(chunk))
req.on('end', () => {
res.end('Hello, world!')
})
res.writeHead(200, {
'Content-Type': 'text/plain'
})
})
server.listen()
await once(server, 'listening')
const client = new H2CClient(`http://localhost:${server.address().port}/`)
t.after(() => client.close())
t.after(() => server.close())
const response = await client.request({
path: '/',
method: 'POST',
body: 'Hello, world!'
})
planner.equal(response.statusCode, 200)
planner.equal(await response.body.text(), 'Hello, world!')
planner.equal(Buffer.concat(bodyChunks).toString(), 'Hello, world!')
})
test('Should reject request if not h2c supported', async t => {
const planner = tspl(t, { plan: 1 })
const server = createSecureServer(await pem.generate({ opts: { keySize: 2048 } }), (req, res) => {
res.writeHead(200)
res.end('Hello, world!')
})
server.listen()
await once(server, 'listening')
const client = new H2CClient(`http://localhost:${server.address().port}/`)
t.after(() => client.close())
t.after(() => server.close())
planner.rejects(
client.request({ path: '/', method: 'GET' }),
'SocketError: other side closed'
)
})
test('Connect to h2c server over a unix domain socket', { skip: process.platform === 'win32' }, async t => {
const planner = tspl(t, { plan: 6 })
const { mkdtemp, rm } = require('node:fs/promises')
const { join } = require('node:path')
const { tmpdir } = require('node:os')
const tmpDir = await mkdtemp(join(tmpdir(), 'h2c-client-'))
const socketPath = join(tmpDir, 'server.sock')
const authority = 'localhost'
const server = createServer((req, res) => {
planner.equal(req.headers[':authority'], authority)
planner.equal(req.headers[':method'], 'GET')
planner.equal(req.headers[':path'], '/')
planner.equal(req.headers[':scheme'], 'http')
res.writeHead(200)
res.end('Hello, world!')
})
server.listen(socketPath)
await once(server, 'listening')
const client = new H2CClient(`http://${authority}/`, {
socketPath
})
const response = await client.request({ path: '/', method: 'GET' })
planner.equal(response.statusCode, 200)
planner.equal(await response.body.text(), 'Hello, world!')
t.after(async () => {
await rm(tmpDir, { recursive: true })
client.close()
server.close()
})
})
test('Should throw if bad useH2c has been passed', async t => {
t = tspl(t, { plan: 1 })
t.throws(() => {
// eslint-disable-next-line
new Client('https://localhost:1000', {
useH2c: 'true'
})
}, {
message: 'useH2c must be a valid boolean value'
})
await t.completed
})
================================================
FILE: test/headers-as-array.js
================================================
'use strict'
const { tspl } = require('@matteo.collina/tspl')
const { test, after } = require('node:test')
const { Client, errors } = require('..')
const { createServer } = require('node:http')
test('handle headers as array', async (t) => {
t = tspl(t, { plan: 3 })
const headers = ['a', '1', 'b', '2', 'c', '3']
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
t.strictEqual(req.headers.a, '1')
t.strictEqual(req.headers.b, '2')
t.strictEqual(req.headers.c, '3')
res.end()
})
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.close())
client.on('disconnect', () => {
if (!client.closed && !client.destroyed) {
t.fail('unexpected disconnect')
}
})
client.request({
path: '/',
method: 'GET',
headers
}, () => { })
})
await t.completed
})
test('handle multi-valued headers as array', async (t) => {
t = tspl(t, { plan: 4 })
const headers = ['a', '1', 'b', '2', 'c', '3', 'd', '4', 'd', '5']
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
t.strictEqual(req.headers.a, '1')
t.strictEqual(req.headers.b, '2')
t.strictEqual(req.headers.c, '3')
t.strictEqual(req.headers.d, '4, 5')
res.end()
})
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.close())
client.on('disconnect', () => {
if (!client.closed && !client.destroyed) {
t.fail('unexpected disconnect')
}
})
client.request({
path: '/',
method: 'GET',
headers
}, () => { })
})
await t.completed
})
test('handle headers with array', async (t) => {
t = tspl(t, { plan: 4 })
const headers = { a: '1', b: '2', c: '3', d: ['4'] }
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
t.strictEqual(req.headers.a, '1')
t.strictEqual(req.headers.b, '2')
t.strictEqual(req.headers.c, '3')
t.strictEqual(req.headers.d, '4')
res.end()
})
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.close())
client.on('disconnect', () => {
if (!client.closed && !client.destroyed) {
t.fail('unexpected disconnect')
}
})
client.request({
path: '/',
method: 'GET',
headers
}, () => { })
})
await t.completed
})
test('handle multi-valued headers', async (t) => {
t = tspl(t, { plan: 4 })
const headers = { a: '1', b: '2', c: '3', d: ['4', '5'] }
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
t.strictEqual(req.headers.a, '1')
t.strictEqual(req.headers.b, '2')
t.strictEqual(req.headers.c, '3')
t.strictEqual(req.headers.d, '4, 5')
res.end()
})
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.close())
client.on('disconnect', () => {
if (!client.closed && !client.destroyed) {
t.fail('unexpected disconnect')
}
})
client.request({
path: '/',
method: 'GET',
headers
}, () => { })
})
await t.completed
})
test('fail if headers array is odd', async (t) => {
t = tspl(t, { plan: 2 })
const headers = ['a', '1', 'b', '2', 'c', '3', 'd']
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => { res.end() })
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.close())
client.on('disconnect', () => {
if (!client.closed && !client.destroyed) {
t.fail('unexpected disconnect')
}
})
client.request({
path: '/',
method: 'GET',
headers
}, (err) => {
t.ok(err instanceof errors.InvalidArgumentError)
t.strictEqual(err.message, 'headers array must be even')
})
})
await t.completed
})
test('fail if headers is not an object or an array', async (t) => {
t = tspl(t, { plan: 2 })
const headers = 'not an object or an array'
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => { res.end() })
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.close())
client.on('disconnect', () => {
if (!client.closed && !client.destroyed) {
t.fail('unexpected disconnect')
}
})
client.request({
path: '/',
method: 'GET',
headers
}, (err) => {
t.ok(err instanceof errors.InvalidArgumentError)
t.strictEqual(err.message, 'headers must be an object or an array')
})
})
await t.completed
})
test('fail if duplicate content-length headers (different case)', async (t) => {
t = tspl(t, { plan: 2 })
const headers = ['Content-Length', '5', 'content-length', '0']
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => { res.end() })
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.close())
client.request({
path: '/',
method: 'POST',
headers,
body: 'hello'
}, (err) => {
t.ok(err instanceof errors.InvalidArgumentError)
t.strictEqual(err.message, 'duplicate content-length header')
})
})
await t.completed
})
test('fail if duplicate content-length headers (same case)', async (t) => {
t = tspl(t, { plan: 2 })
const headers = ['content-length', '5', 'content-length', '0']
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => { res.end() })
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.close())
client.request({
path: '/',
method: 'POST',
headers,
body: 'hello'
}, (err) => {
t.ok(err instanceof errors.InvalidArgumentError)
t.strictEqual(err.message, 'duplicate content-length header')
})
})
await t.completed
})
test('fail if duplicate host headers (different case)', async (t) => {
t = tspl(t, { plan: 2 })
const headers = ['Host', 'example.com', 'host', 'evil.com']
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => { res.end() })
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.close())
client.request({
path: '/',
method: 'GET',
headers
}, (err) => {
t.ok(err instanceof errors.InvalidArgumentError)
t.strictEqual(err.message, 'duplicate host header')
})
})
await t.completed
})
test('fail if duplicate host headers (same case)', async (t) => {
t = tspl(t, { plan: 2 })
const headers = ['host', 'example.com', 'host', 'evil.com']
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => { res.end() })
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.close())
client.request({
path: '/',
method: 'GET',
headers
}, (err) => {
t.ok(err instanceof errors.InvalidArgumentError)
t.strictEqual(err.message, 'duplicate host header')
})
})
await t.completed
})
================================================
FILE: test/headers-crlf.js
================================================
'use strict'
const { tspl } = require('@matteo.collina/tspl')
const { test, after } = require('node:test')
const { once } = require('node:events')
const { Client } = require('..')
const { createServer } = require('node:http')
test('CRLF Injection in Nodejs ‘undici’ via host', async (t) => {
t = tspl(t, { plan: 1 })
const server = createServer({ joinDuplicateHeaders: true }, async (req, res) => {
res.end()
})
after(() => server.close())
server.listen(0)
await once(server, 'listening')
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.close())
client.on('disconnect', () => {
if (!client.closed && !client.destroyed) {
t.fail('unexpected disconnect')
}
})
const unsanitizedContentTypeInput = '12 \r\n\r\naaa:aaa'
try {
const { body } = await client.request({
path: '/',
method: 'POST',
headers: {
'content-type': 'application/json',
host: unsanitizedContentTypeInput
},
body: 'asd'
})
await body.dump()
} catch (err) {
t.strictEqual(err.code, 'UND_ERR_INVALID_ARG')
}
await t.completed
})
================================================
FILE: test/http-100.js
================================================
'use strict'
const { tspl } = require('@matteo.collina/tspl')
const { test, after } = require('node:test')
const { Client, errors } = require('..')
const { createServer } = require('node:http')
const net = require('node:net')
const { once } = require('node:events')
test('ignore informational response', async (t) => {
t = tspl(t, { plan: 2 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.writeProcessing()
req.pipe(res)
})
after(() => server.close())
server.listen(0)
await once(server, 'listening')
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.close())
client.on('disconnect', () => {
if (!client.closed && !client.destroyed) {
t.fail('unexpected disconnect')
}
})
client.request({
path: '/',
method: 'POST',
body: 'hello'
}, (err, response) => {
t.ifError(err)
const bufs = []
response.body.on('data', (buf) => {
bufs.push(buf)
})
response.body.on('end', () => {
t.strictEqual('hello', Buffer.concat(bufs).toString('utf8'))
})
})
await t.completed
})
test('error 103 body', async (t) => {
t = tspl(t, { plan: 2 })
const server = net.createServer({ joinDuplicateHeaders: true }, (socket) => {
socket.write('HTTP/1.1 103 Early Hints\r\n')
socket.write('Content-Length: 1\r\n')
socket.write('\r\n')
socket.write('a\r\n')
})
server.listen(0)
await once(server, 'listening')
const client = new Client(`http://localhost:${server.address().port}`)
after(() => server.close())
after(() => client.close())
client.on('disconnect', () => {
t.ok(true, 'pass')
})
t.rejects(client.request({
path: '/',
method: 'GET'
}), errors.HTTPParserError)
await t.completed
})
test('error 100 body', async (t) => {
t = tspl(t, { plan: 2 })
const server = net.createServer({ joinDuplicateHeaders: true }, (socket) => {
socket.write('HTTP/1.1 100 Early Hints\r\n')
socket.write('\r\n')
})
after(() => server.close())
server.listen(0)
await once(server, 'listening')
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.close())
client.request({
path: '/',
method: 'GET'
}, (err) => {
t.strictEqual(err.message, 'bad response')
})
client.on('disconnect', () => {
t.ok(true, 'pass')
})
await t.completed
})
test('error 101 upgrade', async (t) => {
t = tspl(t, { plan: 2 })
const server = net.createServer({ joinDuplicateHeaders: true }, (socket) => {
socket.write('HTTP/1.1 101 Switching Protocols\r\nUpgrade: example/1\r\nConnection: Upgrade\r\n')
socket.write('\r\n')
})
after(() => server.close())
server.listen(0)
await once(server, 'listening')
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.close())
client.request({
path: '/',
method: 'GET'
}, (err) => {
t.strictEqual(err.message, 'bad upgrade')
})
client.on('disconnect', () => {
t.ok(true, 'pass')
})
await t.completed
})
test('1xx response without timeouts', async t => {
t = tspl(t, { plan: 2 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.writeProcessing()
setTimeout(() => req.pipe(res), 2000)
})
after(() => server.close())
server.listen(0)
await once(server, 'listening')
const client = new Client(`http://localhost:${server.address().port}`, {
bodyTimeout: 0,
headersTimeout: 0
})
after(() => client.close())
client.on('disconnect', () => {
if (!client.closed && !client.destroyed) {
t.fail('unexpected disconnect')
}
})
client.request({
path: '/',
method: 'POST',
body: 'hello'
}, (err, response) => {
t.ifError(err)
const bufs = []
response.body.on('data', (buf) => {
bufs.push(buf)
})
response.body.on('end', () => {
t.strictEqual('hello', Buffer.concat(bufs).toString('utf8'))
})
})
await t.completed
})
================================================
FILE: test/http-req-destroy.js
================================================
'use strict'
const { tspl } = require('@matteo.collina/tspl')
const { test, after } = require('node:test')
const undici = require('..')
const { createServer } = require('node:http')
const { Readable } = require('node:stream')
const { maybeWrapStream, consts } = require('./utils/async-iterators')
function doNotKillReqSocket (bodyType) {
test(`do not kill req socket ${bodyType}`, async (t) => {
t = tspl(t, { plan: 3 })
const server1 = createServer({ joinDuplicateHeaders: true }, (req, res) => {
const client = new undici.Client(`http://localhost:${server2.address().port}`)
after(() => client.close())
client.request({
path: '/',
method: 'POST',
body: req
}, (err, response) => {
t.ifError(err)
setTimeout(() => {
response.body.on('data', buf => {
res.write(buf)
setTimeout(() => {
res.end()
}, 100)
})
}, 100)
})
})
after(() => server1.close())
const server2 = createServer({ joinDuplicateHeaders: true }, (req, res) => {
setTimeout(() => {
req.pipe(res)
}, 100)
})
after(() => server2.close())
server1.listen(0, () => {
const client = new undici.Client(`http://localhost:${server1.address().port}`)
after(() => client.close())
const r = new Readable({ read () {} })
r.push('hello')
client.request({
path: '/',
method: 'POST',
body: maybeWrapStream(r, bodyType)
}, (err, response) => {
t.ifError(err)
const bufs = []
response.body.on('data', (buf) => {
bufs.push(buf)
r.push(null)
})
response.body.on('end', () => {
t.strictEqual('hello', Buffer.concat(bufs).toString('utf8'))
})
})
})
server2.listen(0)
await t.completed
})
}
doNotKillReqSocket(consts.STREAM)
doNotKillReqSocket(consts.ASYNC_ITERATOR)
================================================
FILE: test/http2-abort.js
================================================
'use strict'
const { tspl } = require('@matteo.collina/tspl')
const { test, after } = require('node:test')
const { createSecureServer } = require('node:http2')
const { once } = require('node:events')
const pem = require('@metcoder95/https-pem')
const { Client } = require('..')
test('#2364 - Concurrent aborts', async t => {
t = tspl(t, { plan: 10 })
const server = createSecureServer(await pem.generate({ opts: { keySize: 2048 } }))
server.on('stream', (stream, headers, _flags, rawHeaders) => {
setTimeout(() => {
stream.respond({
'content-type': 'text/plain; charset=utf-8',
'x-custom-h2': 'hello',
':status': 200
})
stream.end('hello h2!')
}, 100)
})
after(() => server.close())
await once(server.listen(0), 'listening')
const client = new Client(`https://localhost:${server.address().port}`, {
connect: {
rejectUnauthorized: false
},
allowH2: true
})
after(() => client.close())
const signal = AbortSignal.timeout(100)
client.request(
{
path: '/1',
method: 'GET',
headers: {
'x-my-header': 'foo'
}
},
(err, response) => {
t.ifError(err)
t.strictEqual(
response.headers['content-type'],
'text/plain; charset=utf-8'
)
t.strictEqual(response.headers['x-custom-h2'], 'hello')
t.strictEqual(response.statusCode, 200)
}
)
client.request(
{
path: '/2',
method: 'GET',
headers: {
'x-my-header': 'foo'
},
signal
},
(err, response) => {
t.strictEqual(err.name, 'TimeoutError')
}
)
client.request(
{
path: '/3',
method: 'GET',
headers: {
'x-my-header': 'foo'
}
},
(err, response) => {
t.ifError(err)
t.strictEqual(
response.headers['content-type'],
'text/plain; charset=utf-8'
)
t.strictEqual(response.headers['x-custom-h2'], 'hello')
t.strictEqual(response.statusCode, 200)
}
)
client.request(
{
path: '/4',
method: 'GET',
headers: {
'x-my-header': 'foo'
},
signal
},
(err, response) => {
t.strictEqual(err.name, 'TimeoutError')
}
)
await t.completed
})
test('#2364 - Concurrent aborts (2nd variant)', async t => {
t = tspl(t, { plan: 10 })
const server = createSecureServer(await pem.generate({ opts: { keySize: 2048 } }))
let counter = 0
server.on('stream', (stream, headers, _flags, rawHeaders) => {
counter++
if (counter % 2 === 0) {
setTimeout(() => {
if (stream.destroyed) {
return
}
stream.respond({
'content-type': 'text/plain; charset=utf-8',
'x-custom-h2': 'hello',
':status': 200
})
stream.end('hello h2!')
}, 400)
return
}
stream.respond({
'content-type': 'text/plain; charset=utf-8',
'x-custom-h2': 'hello',
':status': 200
})
stream.end('hello h2!')
})
after(() => server.close())
await once(server.listen(0), 'listening')
const client = new Client(`https://localhost:${server.address().port}`, {
connect: {
rejectUnauthorized: false
},
allowH2: true
})
after(() => client.close())
const signal = AbortSignal.timeout(300)
client.request(
{
path: '/1',
method: 'GET',
headers: {
'x-my-header': 'foo'
}
},
(err, response) => {
t.ifError(err)
t.strictEqual(
response.headers['content-type'],
'text/plain; charset=utf-8'
)
t.strictEqual(response.headers['x-custom-h2'], 'hello')
t.strictEqual(response.statusCode, 200)
}
)
client.request(
{
path: '/2',
method: 'GET',
headers: {
'x-my-header': 'foo'
},
signal
},
(err, response) => {
t.strictEqual(err.name, 'TimeoutError')
}
)
client.request(
{
path: '/3',
method: 'GET',
headers: {
'x-my-header': 'foo'
}
},
(err, response) => {
t.ifError(err)
t.strictEqual(
response.headers['content-type'],
'text/plain; charset=utf-8'
)
t.strictEqual(response.headers['x-custom-h2'], 'hello')
t.strictEqual(response.statusCode, 200)
}
)
client.request(
{
path: '/4',
method: 'GET',
headers: {
'x-my-header': 'foo'
},
signal
},
(err, response) => {
t.strictEqual(err.name, 'TimeoutError')
}
)
await t.completed
})
================================================
FILE: test/http2-agent.js
================================================
'use strict'
const { tspl } = require('@matteo.collina/tspl')
const { test, after } = require('node:test')
const { createSecureServer } = require('node:http2')
const { once } = require('node:events')
const pem = require('@metcoder95/https-pem')
const { Agent } = require('..')
test('Agent should support H2 connection', async t => {
t = tspl(t, { plan: 6 })
const body = []
const server = createSecureServer(await pem.generate({ opts: { keySize: 2048 } }))
server.on('stream', (stream, headers) => {
t.strictEqual(headers['x-my-header'], 'foo')
t.strictEqual(headers[':method'], 'GET')
stream.respond({
'content-type': 'text/plain; charset=utf-8',
'x-custom-h2': 'hello',
':status': 200
})
stream.end('hello h2!')
})
after(() => server.close())
await once(server.listen(0), 'listening')
const client = new Agent({
connect: {
rejectUnauthorized: false
},
allowH2: true
})
after(() => client.close())
const response = await client.request({
origin: `https://localhost:${server.address().port}`,
path: '/',
method: 'GET',
headers: {
'x-my-header': 'foo'
}
})
response.body.on('data', chunk => {
body.push(chunk)
})
await once(response.body, 'end')
t.strictEqual(response.statusCode, 200)
t.strictEqual(response.headers['content-type'], 'text/plain; charset=utf-8')
t.strictEqual(response.headers['x-custom-h2'], 'hello')
t.strictEqual(Buffer.concat(body).toString('utf8'), 'hello h2!')
await t.completed
})
================================================
FILE: test/http2-alpn.js
================================================
'use strict'
const { tspl } = require('@matteo.collina/tspl')
const { test, after } = require('node:test')
const https = require('node:https')
const { once } = require('node:events')
const { createSecureServer } = require('node:http2')
const { readFileSync } = require('node:fs')
const { join } = require('node:path')
const { Client } = require('..')
// get the crypto fixtures
const key = readFileSync(join(__dirname, 'fixtures', 'key.pem'), 'utf8')
const cert = readFileSync(join(__dirname, 'fixtures', 'cert.pem'), 'utf8')
const ca = readFileSync(join(__dirname, 'fixtures', 'ca.pem'), 'utf8')
test('Should upgrade to HTTP/2 when HTTPS/1 is available for GET', async (t) => {
t = tspl(t, { plan: 10 })
const body = []
const httpsBody = []
// create the server and server stream handler
const server = createSecureServer(
{
key,
cert,
allowHTTP1: true
},
(req, res) => {
const { socket: { alpnProtocol } } = req.httpVersion === '2.0' ? req.stream.session : req
// handle http/1 requests
res.writeHead(200, {
'content-type': 'application/json; charset=utf-8',
'x-custom-request-header': req.headers['x-custom-request-header'] || '',
'x-custom-response-header': `using ${req.httpVersion}`
})
res.end(JSON.stringify({
alpnProtocol,
httpVersion: req.httpVersion
}))
}
)
// close the server on teardown
after(() => server.close())
await once(server.listen(0), 'listening')
// set the port
const port = server.address().port
// test undici against http/2
const client = new Client(`https://localhost:${port}`, {
connect: {
ca,
servername: 'agent1'
},
allowH2: true
})
// close the client on teardown
after(() => client.close())
// make an undici request using where it wants http/2
const response = await client.request({
path: '/',
method: 'GET',
headers: {
'x-custom-request-header': 'want 2.0'
}
})
response.body.on('data', chunk => {
body.push(chunk)
})
await once(response.body, 'end')
t.equal(response.statusCode, 200)
t.equal(response.headers['content-type'], 'application/json; charset=utf-8')
t.equal(response.headers['x-custom-request-header'], 'want 2.0')
t.equal(response.headers['x-custom-response-header'], 'using 2.0')
t.equal(Buffer.concat(body).toString('utf8'), JSON.stringify({
alpnProtocol: 'h2',
httpVersion: '2.0'
}))
// make an https request for http/1 to confirm undici is using http/2
const httpsOptions = {
ca,
servername: 'agent1',
headers: {
'x-custom-request-header': 'want 1.1'
}
}
const httpsResponse = await new Promise((resolve, reject) => {
const httpsRequest = https.get(`https://localhost:${port}/`, httpsOptions, (res) => {
res.on('data', (chunk) => {
httpsBody.push(chunk)
})
res.on('end', () => {
resolve(res)
})
}).on('error', (err) => {
reject(err)
})
after(() => httpsRequest.destroy())
})
t.equal(httpsResponse.statusCode, 200)
t.equal(httpsResponse.headers['content-type'], 'application/json; charset=utf-8')
t.equal(httpsResponse.headers['x-custom-request-header'], 'want 1.1')
t.equal(httpsResponse.headers['x-custom-response-header'], 'using 1.1')
t.equal(Buffer.concat(httpsBody).toString('utf8'), JSON.stringify({
alpnProtocol: false,
httpVersion: '1.1'
}))
await t.completed
})
test('Should upgrade to HTTP/2 when HTTPS/1 is available for POST', async (t) => {
t = tspl(t, { plan: 15 })
const requestChunks = []
const responseBody = []
const httpsRequestChunks = []
const httpsResponseBody = []
const expectedBody = 'hello'
const buf = Buffer.from(expectedBody)
const body = new ArrayBuffer(buf.byteLength)
buf.copy(new Uint8Array(body))
// create the server and server stream handler
const server = createSecureServer(
{
key,
cert,
allowHTTP1: true
},
(req, res) => {
// use the stream handler for http2
if (req.httpVersion === '2.0') {
return
}
const { socket: { alpnProtocol } } = req
req.on('data', (chunk) => {
httpsRequestChunks.push(chunk)
})
req.on('end', () => {
// handle http/1 requests
res.writeHead(201, {
'content-type': 'text/plain; charset=utf-8',
'x-custom-request-header': req.headers['x-custom-request-header'] || '',
'x-custom-alpn-protocol': alpnProtocol
})
res.end('hello http/1!')
})
}
)
server.on('stream', (stream, headers) => {
t.equal(headers[':method'], 'POST')
t.equal(headers[':path'], '/')
t.equal(headers[':scheme'], 'https')
const { socket: { alpnProtocol } } = stream.session
stream.on('data', (chunk) => {
requestChunks.push(chunk)
})
stream.respond({
':status': 201,
'content-type': 'text/plain; charset=utf-8',
'x-custom-request-header': headers['x-custom-request-header'] || '',
'x-custom-alpn-protocol': alpnProtocol
})
stream.end('hello h2!')
})
// close the server on teardown
after(() => server.close())
await once(server.listen(0), 'listening')
// set the port
const port = server.address().port
// test undici against http/2
const client = new Client(`https://localhost:${port}`, {
connect: {
ca,
servername: 'agent1'
},
allowH2: true
})
// close the client on teardown
after(() => client.close())
// make an undici request using where it wants http/2
const response = await client.request({
path: '/',
method: 'POST',
headers: {
'x-custom-request-header': 'want 2.0'
},
body
})
response.body.on('data', (chunk) => {
responseBody.push(chunk)
})
await once(response.body, 'end')
t.equal(response.statusCode, 201)
t.equal(response.headers['content-type'], 'text/plain; charset=utf-8')
t.equal(response.headers['x-custom-request-header'], 'want 2.0')
t.equal(response.headers['x-custom-alpn-protocol'], 'h2')
t.equal(Buffer.concat(responseBody).toString('utf-8'), 'hello h2!')
t.equal(Buffer.concat(requestChunks).toString('utf-8'), expectedBody)
// make an https request for http/1 to confirm undici is using http/2
const httpsOptions = {
ca,
servername: 'agent1',
method: 'POST',
headers: {
'content-type': 'text/plain; charset=utf-8',
'content-length': Buffer.byteLength(body),
'x-custom-request-header': 'want 1.1'
}
}
const httpsResponse = await new Promise((resolve, reject) => {
const httpsRequest = https.request(`https://localhost:${port}/`, httpsOptions, (res) => {
res.on('data', (chunk) => {
httpsResponseBody.push(chunk)
})
res.on('end', () => {
resolve(res)
})
}).on('error', (err) => {
reject(err)
})
httpsRequest.on('error', (err) => {
reject(err)
})
httpsRequest.write(Buffer.from(body))
after(() => httpsRequest.destroy())
})
t.equal(httpsResponse.statusCode, 201)
t.equal(httpsResponse.headers['content-type'], 'text/plain; charset=utf-8')
t.equal(httpsResponse.headers['x-custom-request-header'], 'want 1.1')
t.equal(httpsResponse.headers['x-custom-alpn-protocol'], 'false')
t.equal(Buffer.concat(httpsResponseBody).toString('utf-8'), 'hello http/1!')
t.equal(Buffer.concat(httpsRequestChunks).toString('utf-8'), expectedBody)
await t.completed
})
================================================
FILE: test/http2-body.js
================================================
'use strict'
const { tspl } = require('@matteo.collina/tspl')
const { test, after } = require('node:test')
const { createSecureServer } = require('node:http2')
const { createReadStream, readFileSync } = require('node:fs')
const { once } = require('node:events')
const pem = require('@metcoder95/https-pem')
const { Client, FormData, Response } = require('..')
test('Should handle h2 request without body', async t => {
t = tspl(t, { plan: 9 })
const server = createSecureServer(await pem.generate({ opts: { keySize: 2048 } }))
const expectedBody = ''
const requestChunks = []
const responseBody = []
server.on('stream', async (stream, headers) => {
t.strictEqual(headers[':method'], 'POST')
t.strictEqual(headers[':path'], '/')
t.strictEqual(headers[':scheme'], 'https')
stream.respond({
'content-type': 'text/plain; charset=utf-8',
'x-custom-h2': headers['x-my-header'],
':status': 200
})
for await (const chunk of stream) {
requestChunks.push(chunk)
}
stream.end('hello h2!')
})
after(() => server.close())
await once(server.listen(0), 'listening')
const client = new Client(`https://localhost:${server.address().port}`, {
connect: {
rejectUnauthorized: false
},
allowH2: true
})
after(() => client.close())
const response = await client.request({
path: '/',
method: 'POST',
headers: {
'x-my-header': 'foo'
}
})
for await (const chunk of response.body) {
responseBody.push(chunk)
}
t.strictEqual(response.statusCode, 200)
t.strictEqual(response.headers['content-type'], 'text/plain; charset=utf-8')
t.strictEqual(response.headers['x-custom-h2'], 'foo')
t.strictEqual(Buffer.concat(responseBody).toString('utf-8'), 'hello h2!')
t.strictEqual(requestChunks.length, 0)
t.strictEqual(Buffer.concat(requestChunks).toString('utf-8'), expectedBody)
await t.completed
})
test('Should handle h2 request with body (string or buffer) - dispatch', async t => {
t = tspl(t, { plan: 9 })
const server = createSecureServer(await pem.generate({ opts: { keySize: 2048 } }))
const expectedBody = 'hello from client!'
const response = []
const requestBody = []
server.on('stream', (stream, headers) => {
stream.on('data', chunk => requestBody.push(chunk))
stream.respond({
'content-type': 'text/plain; charset=utf-8',
'x-custom-h2': headers['x-my-header'],
':status': 200
})
stream.end('hello h2!')
})
after(() => server.close())
await once(server.listen(0), 'listening')
const client = new Client(`https://localhost:${server.address().port}`, {
connect: {
rejectUnauthorized: false
},
allowH2: true
})
after(() => client.close())
client.dispatch(
{
path: '/',
method: 'POST',
headers: {
'x-my-header': 'foo',
'content-type': 'text/plain'
},
body: expectedBody
},
{
onConnect () {
t.ok(true, 'pass')
},
onError (err) {
t.ifError(err)
},
onHeaders (statusCode, headers) {
t.strictEqual(statusCode, 200)
t.strictEqual(headers[0].toString('utf-8'), 'content-type')
t.strictEqual(
headers[1].toString('utf-8'),
'text/plain; charset=utf-8'
)
t.strictEqual(headers[2].toString('utf-8'), 'x-custom-h2')
t.strictEqual(headers[3].toString('utf-8'), 'foo')
},
onData (chunk) {
response.push(chunk)
},
onBodySent (body) {
t.strictEqual(body.toString('utf-8'), expectedBody)
},
onComplete () {
t.strictEqual(Buffer.concat(response).toString('utf-8'), 'hello h2!')
t.strictEqual(
Buffer.concat(requestBody).toString('utf-8'),
'hello from client!'
)
}
}
)
await t.completed
})
test('Should handle h2 request with body (stream)', async t => {
t = tspl(t, { plan: 8 })
const server = createSecureServer(await pem.generate({ opts: { keySize: 2048 } }))
const expectedBody = readFileSync(__filename, 'utf-8')
const stream = createReadStream(__filename)
const requestChunks = []
const responseBody = []
server.on('stream', async (stream, headers) => {
t.strictEqual(headers[':method'], 'PUT')
t.strictEqual(headers[':path'], '/')
t.strictEqual(headers[':scheme'], 'https')
stream.respond({
'content-type': 'text/plain; charset=utf-8',
'x-custom-h2': headers['x-my-header'],
':status': 200
})
for await (const chunk of stream) {
requestChunks.push(chunk)
}
stream.end('hello h2!')
})
after(() => server.close())
await once(server.listen(0), 'listening')
const client = new Client(`https://localhost:${server.address().port}`, {
connect: {
rejectUnauthorized: false
},
allowH2: true
})
after(() => client.close())
const response = await client.request({
path: '/',
method: 'PUT',
headers: {
'x-my-header': 'foo'
},
body: stream
})
for await (const chunk of response.body) {
responseBody.push(chunk)
}
t.strictEqual(response.statusCode, 200)
t.strictEqual(response.headers['content-type'], 'text/plain; charset=utf-8')
t.strictEqual(response.headers['x-custom-h2'], 'foo')
t.strictEqual(Buffer.concat(responseBody).toString('utf-8'), 'hello h2!')
t.strictEqual(Buffer.concat(requestChunks).toString('utf-8'), expectedBody)
await t.completed
})
test('Should handle h2 request with body (iterable)', async t => {
t = tspl(t, { plan: 8 })
const server = createSecureServer(await pem.generate({ opts: { keySize: 2048 } }))
const expectedBody = 'hello'
const requestChunks = []
const responseBody = []
const iterableBody = {
[Symbol.iterator]: function * () {
const end = expectedBody.length - 1
for (let i = 0; i < end + 1; i++) {
yield expectedBody[i]
}
return expectedBody[end]
}
}
server.on('stream', (stream, headers) => {
t.strictEqual(headers[':method'], 'POST')
t.strictEqual(headers[':path'], '/')
t.strictEqual(headers[':scheme'], 'https')
stream.on('data', chunk => requestChunks.push(chunk))
stream.respond({
'content-type': 'text/plain; charset=utf-8',
'x-custom-h2': headers['x-my-header'],
':status': 200
})
stream.end('hello h2!')
})
after(() => server.close())
await once(server.listen(0), 'listening')
const client = new Client(`https://localhost:${server.address().port}`, {
connect: {
rejectUnauthorized: false
},
allowH2: true
})
after(() => client.close())
const response = await client.request({
path: '/',
method: 'POST',
headers: {
'x-my-header': 'foo'
},
body: iterableBody
})
response.body.on('data', chunk => {
responseBody.push(chunk)
})
await once(response.body, 'end')
t.strictEqual(response.statusCode, 200)
t.strictEqual(response.headers['content-type'], 'text/plain; charset=utf-8')
t.strictEqual(response.headers['x-custom-h2'], 'foo')
t.strictEqual(Buffer.concat(responseBody).toString('utf-8'), 'hello h2!')
t.strictEqual(Buffer.concat(requestChunks).toString('utf-8'), expectedBody)
await t.completed
})
test('Should handle h2 request with body (Blob)', async t => {
t = tspl(t, { plan: 8 })
const server = createSecureServer(await pem.generate({ opts: { keySize: 2048 } }))
const expectedBody = 'asd'
const requestChunks = []
const responseBody = []
const body = new Blob(['asd'], {
type: 'application/json'
})
server.on('stream', (stream, headers) => {
t.strictEqual(headers[':method'], 'POST')
t.strictEqual(headers[':path'], '/')
t.strictEqual(headers[':scheme'], 'https')
stream.on('data', chunk => requestChunks.push(chunk))
stream.respond({
'content-type': 'text/plain; charset=utf-8',
'x-custom-h2': headers['x-my-header'],
':status': 200
})
stream.end('hello h2!')
})
after(() => server.close())
await once(server.listen(0), 'listening')
const client = new Client(`https://localhost:${server.address().port}`, {
connect: {
rejectUnauthorized: false
},
allowH2: true
})
after(() => client.close())
const response = await client.request({
path: '/',
method: 'POST',
headers: {
'x-my-header': 'foo'
},
body
})
response.body.on('data', chunk => {
responseBody.push(chunk)
})
await once(response.body, 'end')
t.strictEqual(response.statusCode, 200)
t.strictEqual(response.headers['content-type'], 'text/plain; charset=utf-8')
t.strictEqual(response.headers['x-custom-h2'], 'foo')
t.strictEqual(Buffer.concat(responseBody).toString('utf-8'), 'hello h2!')
t.strictEqual(Buffer.concat(requestChunks).toString('utf-8'), expectedBody)
await t.completed
})
test('Should handle h2 request with body (Blob:ArrayBuffer)',
async t => {
t = tspl(t, { plan: 8 })
const server = createSecureServer(await pem.generate({ opts: { keySize: 2048 } }))
const expectedBody = 'hello'
const requestChunks = []
const responseBody = []
const buf = Buffer.from(expectedBody)
const body = new ArrayBuffer(buf.byteLength)
buf.copy(new Uint8Array(body))
server.on('stream', (stream, headers) => {
t.strictEqual(headers[':method'], 'POST')
t.strictEqual(headers[':path'], '/')
t.strictEqual(headers[':scheme'], 'https')
stream.on('data', chunk => requestChunks.push(chunk))
stream.respond({
'content-type': 'text/plain; charset=utf-8',
'x-custom-h2': headers['x-my-header'],
':status': 200
})
stream.end('hello h2!')
})
after(() => server.close())
await once(server.listen(0), 'listening')
const client = new Client(`https://localhost:${server.address().port}`, {
connect: {
rejectUnauthorized: false
},
allowH2: true
})
after(() => client.close())
const response = await client.request({
path: '/',
method: 'POST',
headers: {
'x-my-header': 'foo'
},
body
})
response.body.on('data', chunk => {
responseBody.push(chunk)
})
await once(response.body, 'end')
t.strictEqual(response.statusCode, 200)
t.strictEqual(response.headers['content-type'], 'text/plain; charset=utf-8')
t.strictEqual(response.headers['x-custom-h2'], 'foo')
t.strictEqual(Buffer.concat(responseBody).toString('utf-8'), 'hello h2!')
t.strictEqual(Buffer.concat(requestChunks).toString('utf-8'), expectedBody)
await t.completed
}
)
test('#3803 - sending FormData bodies works', async (t) => {
const assert = tspl(t, { plan: 4 })
const server = createSecureServer(await pem.generate({ opts: { keySize: 2048 } }))
server.on('stream', async (stream, headers) => {
const contentLength = Number(headers['content-length'])
assert.ok(!Number.isNaN(contentLength))
assert.ok(headers['content-type']?.startsWith('multipart/form-data; boundary='))
stream.respond({ ':status': 200 })
const fd = await new Response(stream, {
headers: {
'content-type': headers['content-type']
}
}).formData()
assert.deepEqual(fd.get('a'), 'b')
assert.deepEqual(fd.get('c').name, 'e.fgh')
stream.end()
})
await once(server.listen(0), 'listening')
const client = new Client(`https://localhost:${server.address().port}`, {
connect: {
rejectUnauthorized: false
},
allowH2: true
})
t.after(async () => {
server.close()
await client.close()
})
const fd = new FormData()
fd.set('a', 'b')
fd.set('c', new Blob(['d']), 'e.fgh')
await client.request({
path: '/',
method: 'POST',
body: fd
})
await assert.completed
})
================================================
FILE: test/http2-connection.js
================================================
'use strict'
const { tspl } = require('@matteo.collina/tspl')
const { test, after } = require('node:test')
const { createSecureServer } = require('node:http2')
const { once } = require('node:events')
const { Readable } = require('node:stream')
const pem = require('@metcoder95/https-pem')
const { Client } = require('..')
test('Should support H2 connection', async t => {
t = tspl(t, { plan: 9 })
const body = []
const server = createSecureServer(await pem.generate({ opts: { keySize: 2048 } }))
let authority = ''
server.on('stream', (stream, headers, _flags, rawHeaders) => {
t.strictEqual(headers['x-my-header'], 'foo')
t.strictEqual(headers[':method'], 'GET')
t.strictEqual(headers[':scheme'], 'https')
t.strictEqual(headers[':path'], '/')
t.strictEqual(headers[':authority'], authority)
stream.respond({
'content-type': 'text/plain; charset=utf-8',
'x-custom-h2': 'hello',
':status': 200
})
stream.end('hello h2!')
})
after(() => server.close())
await once(server.listen(0), 'listening')
authority = `localhost:${server.address().port}`
const client = new Client(`https://${authority}`, {
connect: {
rejectUnauthorized: false
},
allowH2: true
})
after(() => client.close())
const response = await client.request({
path: '/',
method: 'GET',
headers: {
'x-my-header': 'foo'
}
})
response.body.on('data', chunk => {
body.push(chunk)
})
await once(response.body, 'end')
t.strictEqual(response.statusCode, 200)
t.strictEqual(response.headers['content-type'], 'text/plain; charset=utf-8')
t.strictEqual(response.headers['x-custom-h2'], 'hello')
t.strictEqual(Buffer.concat(body).toString('utf8'), 'hello h2!')
await t.completed
})
test('Should support H2 connection(multiple requests)', async t => {
t = tspl(t, { plan: 21 })
const server = createSecureServer(await pem.generate({ opts: { keySize: 2048 } }))
server.on('stream', async (stream, headers, _flags, rawHeaders) => {
t.strictEqual(headers['x-my-header'], 'foo')
t.strictEqual(headers[':method'], 'POST')
const reqData = []
stream.on('data', chunk => reqData.push(chunk.toString()))
await once(stream, 'end')
const reqBody = reqData.join('')
t.strictEqual(reqBody.length > 0, true)
stream.respond({
'content-type': 'text/plain; charset=utf-8',
'x-custom-h2': 'hello',
':status': 200
})
stream.end(`hello h2! ${reqBody}`)
})
after(() => server.close())
await once(server.listen(0), 'listening')
const client = new Client(`https://localhost:${server.address().port}`, {
connect: {
rejectUnauthorized: false
},
allowH2: true
})
after(() => client.close())
for (let i = 0; i < 3; i++) {
const sendBody = `seq ${i}`
const body = []
const response = await client.request({
path: '/',
method: 'POST',
headers: {
'content-type': 'text/plain; charset=utf-8',
'x-my-header': 'foo'
},
body: Readable.from(sendBody)
})
response.body.on('data', chunk => {
body.push(chunk)
})
await once(response.body, 'end')
t.strictEqual(response.statusCode, 200)
t.strictEqual(response.headers['content-type'], 'text/plain; charset=utf-8')
t.strictEqual(response.headers['x-custom-h2'], 'hello')
t.strictEqual(Buffer.concat(body).toString('utf8'), `hello h2! ${sendBody}`)
}
await t.completed
})
test('Should support H2 connection (headers as array)', async t => {
t = tspl(t, { plan: 8 })
const body = []
const server = createSecureServer(await pem.generate({ opts: { keySize: 2048 } }))
server.on('stream', (stream, headers) => {
t.strictEqual(headers['x-my-header'], 'foo, bar')
t.strictEqual(headers['x-my-drink'], 'coffee, tea, water')
t.strictEqual(headers['x-other'], 'value')
t.strictEqual(headers[':method'], 'GET')
stream.respond({
'content-type': 'text/plain; charset=utf-8',
'x-custom-h2': 'hello',
':status': 200
})
stream.end('hello h2!')
})
after(() => server.close())
await once(server.listen(0), 'listening')
const client = new Client(`https://localhost:${server.address().port}`, {
connect: {
rejectUnauthorized: false
},
allowH2: true
})
after(() => client.close())
const response = await client.request({
path: '/',
method: 'GET',
headers: [
'x-my-header', 'foo',
'x-my-drink', ['coffee', 'tea'],
'x-my-drink', 'water',
'X-My-Header', 'bar',
'x-other', 'value'
]
})
response.body.on('data', chunk => {
body.push(chunk)
})
await once(response.body, 'end')
t.strictEqual(response.statusCode, 200)
t.strictEqual(response.headers['content-type'], 'text/plain; charset=utf-8')
t.strictEqual(response.headers['x-custom-h2'], 'hello')
t.strictEqual(Buffer.concat(body).toString('utf8'), 'hello h2!')
await t.completed
})
test('Should support multiple header values with semicolon separator', async t => {
t = tspl(t, { plan: 9 * 2 })
const body = []
const body2 = []
const expectedCookieHeaders = ['a=b', 'c=d', 'e=f']
const server = createSecureServer(await pem.generate({ opts: { keySize: 2048 } }))
server.on('stream', (stream, headers) => {
t.strictEqual(headers['x-my-header'], 'foo, bar')
t.strictEqual(headers['x-my-drink'], 'coffee, tea, water')
t.strictEqual(headers['x-other'], 'value')
t.strictEqual(headers['cookie'], expectedCookieHeaders.join('; '))
t.strictEqual(headers[':method'], 'GET')
stream.respond({
'content-type': 'text/plain; charset=utf-8',
'x-custom-h2': 'hello',
':status': 200
})
stream.end('hello h2!')
})
after(() => server.close())
await once(server.listen(0), 'listening')
const client = new Client(`https://localhost:${server.address().port}`, {
connect: {
rejectUnauthorized: false
},
allowH2: true
})
after(() => client.close())
const response = await client.request({
path: '/',
method: 'GET',
headers: [
'x-my-header', 'foo',
'x-my-drink', ['coffee', 'tea'],
'x-my-drink', 'water',
'X-My-Header', 'bar',
'x-other', 'value',
'cookie', expectedCookieHeaders
]
})
response.body.on('data', chunk => {
body.push(chunk)
})
await once(response.body, 'end')
t.strictEqual(response.statusCode, 200)
t.strictEqual(response.headers['content-type'], 'text/plain; charset=utf-8')
t.strictEqual(response.headers['x-custom-h2'], 'hello')
t.strictEqual(Buffer.concat(body).toString('utf8'), 'hello h2!')
const response2 = await client.request({
path: '/',
method: 'GET',
headers: [
'x-my-header', 'foo',
'x-my-drink', ['coffee', 'tea'],
'cookie', 'a=b',
'x-my-drink', 'water',
'X-My-Header', 'bar',
'cookie', 'c=d',
'x-other', 'value',
'cookie', 'e=f'
]
})
response2.body.on('data', chunk => {
body2.push(chunk)
})
await once(response2.body, 'end')
t.strictEqual(response2.statusCode, 200)
t.strictEqual(response2.headers['content-type'], 'text/plain; charset=utf-8')
t.strictEqual(response2.headers['x-custom-h2'], 'hello')
t.strictEqual(Buffer.concat(body).toString('utf8'), 'hello h2!')
await t.completed
})
test('Should support H2 connection(POST Buffer)', async t => {
t = tspl(t, { plan: 6 })
const server = createSecureServer({ ...await pem.generate({ opts: { keySize: 2048 } }), allowHTTP1: false })
server.on('stream', async (stream, headers, _flags, rawHeaders) => {
t.strictEqual(headers[':method'], 'POST')
const reqData = []
stream.on('data', chunk => reqData.push(chunk.toString()))
await once(stream, 'end')
t.strictEqual(reqData.join(''), 'hello!')
stream.respond({
'content-type': 'text/plain; charset=utf-8',
'x-custom-h2': 'hello',
':status': 200
})
stream.end('hello h2!')
})
after(() => server.close())
await once(server.listen(0), 'listening')
const client = new Client(`https://localhost:${server.address().port}`, {
connect: {
rejectUnauthorized: false
},
allowH2: true
})
after(() => client.close())
const sendBody = 'hello!'
const body = []
const response = await client.request({
path: '/',
method: 'POST',
body: sendBody
})
response.body.on('data', chunk => {
body.push(chunk)
})
await once(response.body, 'end')
t.strictEqual(response.statusCode, 200)
t.strictEqual(response.headers['content-type'], 'text/plain; charset=utf-8')
t.strictEqual(response.headers['x-custom-h2'], 'hello')
t.strictEqual(Buffer.concat(body).toString('utf8'), 'hello h2!')
await t.completed
})
================================================
FILE: test/http2-continue.js
================================================
'use strict'
const { tspl } = require('@matteo.collina/tspl')
const { test, after } = require('node:test')
const { createSecureServer } = require('node:http2')
const { once } = require('node:events')
const pem = require('@metcoder95/https-pem')
const { Client } = require('..')
test('Should handle h2 continue', async t => {
t = tspl(t, { plan: 7 })
const requestBody = []
const server = createSecureServer(await pem.generate({ opts: { keySize: 2048 } }), () => {})
const responseBody = []
server.on('checkContinue', (request, response) => {
t.strictEqual(request.headers.expect, '100-continue')
t.strictEqual(request.headers['x-my-header'], 'foo')
t.strictEqual(request.headers[':method'], 'POST')
response.writeContinue()
request.on('data', chunk => requestBody.push(chunk))
response.writeHead(200, {
'content-type': 'text/plain; charset=utf-8',
'x-custom-h2': 'foo'
})
response.end('hello h2!')
})
after(() => server.close())
await once(server.listen(0), 'listening')
const client = new Client(`https://localhost:${server.address().port}`, {
connect: {
rejectUnauthorized: false
},
expectContinue: true,
allowH2: true
})
after(() => client.close())
const response = await client.request({
path: '/',
method: 'POST',
headers: {
'x-my-header': 'foo'
},
expectContinue: true
})
response.body.on('data', chunk => {
responseBody.push(chunk)
})
await once(response.body, 'end')
t.strictEqual(response.statusCode, 200)
t.strictEqual(response.headers['content-type'], 'text/plain; charset=utf-8')
t.strictEqual(response.headers['x-custom-h2'], 'foo')
t.strictEqual(Buffer.concat(responseBody).toString('utf-8'), 'hello h2!')
await t.completed
})
================================================
FILE: test/http2-dispatcher.js
================================================
'use strict'
const { test, after } = require('node:test')
const { createSecureServer } = require('node:http2')
const { once } = require('node:events')
const { setTimeout: sleep } = require('node:timers/promises')
const { Writable, pipeline, PassThrough, Readable } = require('node:stream')
const { tspl } = require('@matteo.collina/tspl')
const pem = require('@metcoder95/https-pem')
const { Client } = require('..')
test('Dispatcher#Stream', async t => {
t = tspl(t, { plan: 4 })
const server = createSecureServer(await pem.generate({ opts: { keySize: 2048 } }))
const expectedBody = 'hello from client!'
const bufs = []
let requestBody = ''
server.on('stream', (stream, headers) => {
stream.setEncoding('utf-8')
stream.on('data', chunk => {
requestBody += chunk
})
stream.on('error', err => {
t.fail(err)
})
stream.respond({ ':status': 200, 'x-custom': 'custom-header' })
stream.end('hello h2!')
})
after(() => server.close())
await once(server.listen(0), 'listening')
const client = new Client(`https://localhost:${server.address().port}`, {
connect: {
rejectUnauthorized: false
},
allowH2: true
})
after(() => client.close())
await client.stream(
{ path: '/', opaque: { bufs }, method: 'POST', body: expectedBody },
({ statusCode, headers, opaque: { bufs } }) => {
t.strictEqual(statusCode, 200)
t.strictEqual(headers['x-custom'], 'custom-header')
return new Writable({
write (chunk, _encoding, cb) {
bufs.push(chunk)
cb()
}
})
}
)
t.strictEqual(Buffer.concat(bufs).toString('utf-8'), 'hello h2!')
t.strictEqual(requestBody, expectedBody)
await t.completed
})
test('Dispatcher#Pipeline', async t => {
t = tspl(t, { plan: 5 })
const server = createSecureServer(await pem.generate({ opts: { keySize: 2048 } }))
const expectedBody = 'hello from client!'
const bufs = []
let requestBody = ''
server.on('stream', (stream, headers) => {
stream.setEncoding('utf-8')
stream.on('data', chunk => {
requestBody += chunk
})
stream.on('error', err => {
t.fail(err)
})
stream.respond({ ':status': 200, 'x-custom': 'custom-header' })
stream.end('hello h2!')
})
after(() => server.close())
await once(server.listen(0), 'listening')
const client = new Client(`https://localhost:${server.address().port}`, {
connect: {
rejectUnauthorized: false
},
allowH2: true
})
after(() => client.close())
pipeline(
new Readable({
read () {
this.push(Buffer.from(expectedBody))
this.push(null)
}
}),
client.pipeline(
{ path: '/', method: 'POST', body: expectedBody },
({ statusCode, headers, body }) => {
t.strictEqual(statusCode, 200)
t.strictEqual(headers['x-custom'], 'custom-header')
return pipeline(body, new PassThrough(), () => {})
}
),
new Writable({
write (chunk, _, cb) {
bufs.push(chunk)
cb()
}
}),
err => {
t.ifError(err)
t.strictEqual(Buffer.concat(bufs).toString('utf-8'), 'hello h2!')
t.strictEqual(requestBody, expectedBody)
}
)
await t.completed
})
test('Dispatcher#Connect', async t => {
t = tspl(t, { plan: 5 })
const proxy = createSecureServer(await pem.generate({ opts: { keySize: 2048 } }))
const server = createSecureServer(await pem.generate({ opts: { keySize: 2048 } }))
const expectedBody = 'hello from client!'
let responseBody = ''
let requestBody = ''
proxy.on('stream', async (stream, headers) => {
if (headers[':method'] !== 'CONNECT') {
t.fail('Unexpected CONNECT method')
return
}
stream.on('error', err => {
t.fail(err)
})
const forward = new Client(`https://localhost:${server.address().port}`, {
connect: {
rejectUnauthorized: false
},
allowH2: true
})
after(() => forward.close())
try {
const response = await forward.request({
path: '/',
method: 'POST',
body: stream,
headers: {
'x-my-header': headers['x-my-header']
}
})
stream.respond({ ':status': 200, 'x-my-header': response.headers['x-my-header'] })
response.body.pipe(stream)
} catch (err) {
stream.destroy(err)
}
})
server.on('stream', (stream, headers) => {
stream.setEncoding('utf-8')
stream.on('data', chunk => {
requestBody += chunk
})
stream.once('end', () => {
t.strictEqual(requestBody, expectedBody)
})
stream.on('error', err => {
t.fail(err)
})
stream.respond({ ':status': 200, 'x-my-header': headers['x-my-header'] })
stream.end('helloworld')
})
await once(proxy.listen(0), 'listening')
await once(server.listen(0), 'listening')
const client = new Client(`https://localhost:${proxy.address().port}`, {
connect: {
rejectUnauthorized: false
},
allowH2: true
})
after(() => client.close())
after(() => proxy.close())
after(() => server.close())
const { statusCode, headers, socket } = await client.connect({ path: '/', headers: { 'x-my-header': 'foo' } })
t.strictEqual(statusCode, 200)
t.strictEqual(headers['x-my-header'], 'foo')
t.strictEqual(socket.closed, false)
socket.on('data', chunk => { responseBody += chunk })
socket.once('end', () => {
t.strictEqual(responseBody, 'helloworld')
})
socket.setEncoding('utf-8')
socket.write(expectedBody)
socket.end()
await t.completed
})
test('Dispatcher#Upgrade - Should throw on non-websocket upgrade', async t => {
const server = createSecureServer(await pem.generate({ opts: { keySize: 2048 } }))
server.on('stream', async (stream, headers) => {
stream.end()
})
t = tspl(t, { plan: 1 })
server.listen(0, async () => {
const client = new Client(`https://localhost:${server.address().port}`, {
connect: {
rejectUnauthorized: false
},
allowH2: true
})
after(() => server.close())
after(() => client.close())
try {
await client.upgrade({ path: '/', protocol: 'any' })
} catch (error) {
t.strictEqual(error.message, 'Custom upgrade "any" not supported over HTTP/2')
}
})
await t.completed
})
test('Dispatcher#Upgrade', async t => {
t = tspl(t, { plan: 3 })
const server = createSecureServer({ ...(await pem.generate({ opts: { keySize: 2048 } })), settings: { enableConnectProtocol: true } })
server.on('stream', (stream, headers) => {
stream.on('error', err => {
t.fail(err)
})
stream.respond({ ':status': 200 })
stream.resume()
stream.end()
})
await once(server.listen(0), 'listening')
const client = new Client(`https://localhost:${server.address().port}`, {
connect: {
rejectUnauthorized: false
},
allowH2: true
})
after(() => client.close().then(() => { server.close() }))
const { socket } = await client.upgrade({ path: '/', protocol: 'websocket' })
t.ok(socket.readable)
t.ok(socket.writable)
t.strictEqual(socket.closed, false)
after(() => socket.end())
await t.completed
})
test('Dispatcher#destroy', async t => {
t = tspl(t, { plan: 4 })
const promises = []
const server = createSecureServer(await pem.generate({ opts: { keySize: 2048 } }))
server.on('stream', (stream, headers) => {
stream.on('error', err => {
t.fail(err)
})
stream.resume()
setTimeout(stream.end.bind(stream), 1500)
})
after(() => server.close())
await once(server.listen(0), 'listening')
const client = new Client(`https://localhost:${server.address().port}`, {
connect: {
rejectUnauthorized: false
},
allowH2: true
})
// we don't want to close the client gracefully in an after hook
promises.push(
client.request({
path: '/',
method: 'GET',
headers: {
'x-my-header': 'foo'
}
})
)
promises.push(
client.request({
path: '/',
method: 'GET',
headers: {
'x-my-header': 'foo'
}
})
)
promises.push(
client.request({
path: '/',
method: 'GET',
headers: {
'x-my-header': 'foo'
}
})
)
promises.push(
client.request({
path: '/',
method: 'GET',
headers: {
'x-my-header': 'foo'
}
})
)
await client.destroy()
const results = await Promise.allSettled(promises)
t.strictEqual(results[0].status, 'rejected')
t.strictEqual(results[1].status, 'rejected')
t.strictEqual(results[2].status, 'rejected')
t.strictEqual(results[3].status, 'rejected')
await t.completed
})
test('Should handle h2 request without body', async t => {
t = tspl(t, { plan: 9 })
const server = createSecureServer(await pem.generate({ opts: { keySize: 2048 } }))
const expectedBody = ''
const requestChunks = []
const responseBody = []
server.on('stream', async (stream, headers) => {
t.strictEqual(headers[':method'], 'POST')
t.strictEqual(headers[':path'], '/')
t.strictEqual(headers[':scheme'], 'https')
stream.respond({
'content-type': 'text/plain; charset=utf-8',
'x-custom-h2': headers['x-my-header'],
':status': 200
})
stream.on('error', err => {
t.fail(err)
})
for await (const chunk of stream) {
requestChunks.push(chunk)
}
stream.end('hello h2!')
})
after(() => server.close())
await once(server.listen(0), 'listening')
const client = new Client(`https://localhost:${server.address().port}`, {
connect: {
rejectUnauthorized: false
},
allowH2: true
})
after(() => client.close())
const response = await client.request({
path: '/',
method: 'POST',
headers: {
'x-my-header': 'foo'
}
})
for await (const chunk of response.body) {
responseBody.push(chunk)
}
t.strictEqual(response.statusCode, 200)
t.strictEqual(response.headers['content-type'], 'text/plain; charset=utf-8')
t.strictEqual(response.headers['x-custom-h2'], 'foo')
t.strictEqual(Buffer.concat(responseBody).toString('utf-8'), 'hello h2!')
t.strictEqual(requestChunks.length, 0)
t.strictEqual(Buffer.concat(requestChunks).toString('utf-8'), expectedBody)
await t.completed
})
test('Should only accept valid ping interval values', async t => {
const planner = tspl(t, { plan: 3 })
planner.throws(() => new Client('https://localhost', {
connect: {
rejectUnauthorized: false
},
allowH2: true,
pingInterval: -1
}))
planner.throws(() => new Client('https://localhost', {
connect: {
rejectUnauthorized: false
},
allowH2: true,
pingInterval: 'foo'
}))
planner.throws(() => new Client('https://localhost', {
connect: {
rejectUnauthorized: false
},
allowH2: true,
pingInterval: 1.1
}))
await planner.completed
})
test('Should send http2 PING frames', async t => {
const server = createSecureServer(pem)
let session = null
let pingCounter = 0
server.on('stream', async (stream, headers) => {
stream.respond({
'content-type': 'text/plain; charset=utf-8',
'x-custom-h2': headers['x-my-header'],
':status': 200
},
{
waitForTrailers: true
})
stream.on('wantTrailers', () => {
stream.sendTrailers({
'x-trailer': 'hello'
})
})
stream.end('hello h2!')
})
server.on('session', (s) => {
session = s
session.on('ping', (payload) => {
pingCounter++
})
})
t = tspl(t, { plan: 2 })
server.listen(0, '127.0.0.1')
await once(server, 'listening')
const client = new Client(`https://${server.address().address}:${server.address().port}`, {
connect: {
rejectUnauthorized: false
},
allowH2: true,
pingInterval: 100
})
after(async () => {
server.close()
})
client.dispatch({
path: '/',
method: 'PUT',
body: 'hello'
}, {
onConnect () {
},
onHeaders () {
return true
},
onData () {
return true
},
onComplete (trailers) {
t.strictEqual(trailers['x-trailer'], 'hello')
},
onError (err) {
t.ifError(err)
}
})
await sleep(600)
await client.close()
t.equal(pingCounter, 5, 'Expected 5 PING frames to be sent')
await t.completed
})
test('Should not send http2 PING frames if interval === 0', async t => {
const server = createSecureServer(pem)
let session = null
let pingCounter = 0
server.on('stream', async (stream, headers) => {
stream.respond({
'content-type': 'text/plain; charset=utf-8',
'x-custom-h2': headers['x-my-header'],
':status': 200
},
{
waitForTrailers: true
})
stream.on('wantTrailers', () => {
stream.sendTrailers({
'x-trailer': 'hello'
})
})
stream.end('hello h2!')
})
server.on('session', (s) => {
session = s
session.on('ping', (payload) => {
pingCounter++
})
})
t = tspl(t, { plan: 2 })
server.listen(0, '127.0.0.1')
await once(server, 'listening')
const client = new Client(`https://${server.address().address}:${server.address().port}`, {
connect: {
rejectUnauthorized: false
},
allowH2: true,
pingInterval: 0
})
after(async () => {
server.close()
})
client.dispatch({
path: '/',
method: 'PUT',
body: 'hello'
}, {
onConnect () {
},
onHeaders () {
return true
},
onData () {
return true
},
onComplete (trailers) {
t.strictEqual(trailers['x-trailer'], 'hello')
},
onError (err) {
t.ifError(err)
}
})
await sleep(500)
await client.close()
t.equal(pingCounter, 0, 'Expected 0 PING frames to be sent')
await t.completed
})
test('Should not send http2 PING frames after connection is closed', async t => {
const server = createSecureServer(pem)
let session = null
let pingCounter = 0
server.on('stream', async (stream, headers) => {
stream.respond({
'content-type': 'text/plain; charset=utf-8',
'x-custom-h2': headers['x-my-header'],
':status': 200
},
{
waitForTrailers: true
})
stream.on('wantTrailers', () => {
stream.sendTrailers({
'x-trailer': 'hello'
})
})
stream.end('hello h2!')
})
server.on('session', (s) => {
session = s
session.on('ping', (payload) => {
pingCounter++
})
})
t = tspl(t, { plan: 2 })
server.listen(0, '127.0.0.1')
await once(server, 'listening')
const client = new Client(`https://${server.address().address}:${server.address().port}`, {
connect: {
rejectUnauthorized: false
},
allowH2: true,
pingInterval: 0
})
after(async () => {
session.close()
server.close()
})
client.dispatch({
path: '/',
method: 'PUT',
body: 'hello'
}, {
onConnect () {
},
onHeaders () {
return true
},
onData () {
return true
},
onComplete (trailers) {
t.strictEqual(trailers['x-trailer'], 'hello')
},
onError (err) {
t.ifError(err)
}
})
await client.close()
await sleep(500)
t.equal(pingCounter, 0, 'Expected 0 PING frames to be sent')
await t.completed
})
================================================
FILE: test/http2-goaway.js
================================================
'use strict'
const { tspl } = require('@matteo.collina/tspl')
const { test, after } = require('node:test')
const { createSecureServer } = require('node:http2')
const { once } = require('node:events')
const pem = require('@metcoder95/https-pem')
const { Client } = require('..')
test('#3046 - GOAWAY Frame', async t => {
t = tspl(t, { plan: 10 })
const server = createSecureServer(await pem.generate({ opts: { keySize: 2048 } }))
server.on('stream', (stream, headers) => {
setTimeout(() => {
if (stream.closed) return
stream.end('Hello World')
}, 100)
stream.respond({
'content-type': 'text/plain; charset=utf-8',
'x-custom-h2': 'hello',
':status': 200
})
})
server.on('session', session => {
setTimeout(() => {
session.goaway()
}, 50)
})
after(() => server.close())
await once(server.listen(0), 'listening')
const client = new Client(`https://localhost:${server.address().port}`, {
connect: {
rejectUnauthorized: false
},
allowH2: true
})
after(() => client.close())
client.on('disconnect', (url, disconnectClient, err) => {
t.ok(url instanceof URL)
t.deepStrictEqual(disconnectClient, [client])
t.strictEqual(err.message, 'HTTP/2: "GOAWAY" frame received with code 0')
})
client.on('connectionError', (url, disconnectClient, err) => {
t.ok(url instanceof URL)
t.deepStrictEqual(disconnectClient, [client])
t.strictEqual(err.message, 'HTTP/2: "GOAWAY" frame received with code 0')
})
const response = await client.request({
path: '/',
method: 'GET',
headers: {
'x-my-header': 'foo'
}
})
t.strictEqual(response.headers['content-type'], 'text/plain; charset=utf-8')
t.strictEqual(response.headers['x-custom-h2'], 'hello')
t.strictEqual(response.statusCode, 200)
await t.rejects(response.body.text(), {
message: 'HTTP/2: "GOAWAY" frame received with code 0',
code: 'UND_ERR_SOCKET'
})
await t.completed
})
test('#3753 - Handle GOAWAY Gracefully', async (t) => {
t = tspl(t, { plan: 30 })
const server = createSecureServer(await pem.generate({ opts: { keySize: 2048 } }))
let counter = 0
let session = null
server.on('session', s => {
session = s
})
server.on('stream', (stream) => {
counter++
// Due to the nature of the test, we need to ignore the error
// that is thrown when the session is destroyed and stream
// is in-flight
stream.on('error', () => {})
if (counter === 9 && session != null) {
session.goaway()
stream.end()
} else {
stream.respond({
'content-type': 'text/plain',
':status': 200
})
setTimeout(() => {
stream.end('hello world')
}, 150)
}
})
after(() => server.close())
await once(server.listen(0), 'listening')
const client = new Client(`https://localhost:${server.address().port}`, {
connect: {
rejectUnauthorized: false
},
pipelining: 2,
allowH2: true
})
after(() => client.close())
for (let i = 0; i < 15; i++) {
client.request({
path: '/',
method: 'GET',
headers: {
'x-my-header': 'foo'
}
}, (err, response) => {
if (err) {
t.strictEqual(err.message, 'HTTP/2: "GOAWAY" frame received with code 0')
t.strictEqual(err.code, 'UND_ERR_SOCKET')
} else {
t.strictEqual(response.statusCode, 200)
;(async function () {
let body
try {
body = await response.body.text()
} catch (err) {
t.strictEqual(err.code, 'UND_ERR_SOCKET')
return
}
t.strictEqual(body, 'hello world')
})()
}
})
}
await t.completed
})
================================================
FILE: test/http2-instantiation.js
================================================
'use strict'
const { tspl } = require('@matteo.collina/tspl')
const { test, after } = require('node:test')
const { createSecureServer } = require('node:http2')
const { once } = require('node:events')
const pem = require('@metcoder95/https-pem')
const { Client } = require('..')
test('Should throw if bad allowH2 has been passed', async t => {
t = tspl(t, { plan: 1 })
t.throws(() => {
// eslint-disable-next-line
new Client('https://localhost:1000', {
allowH2: 'true'
})
}, {
message: 'allowH2 must be a valid boolean value'
})
await t.completed
})
test('Should throw if bad maxConcurrentStreams has been passed', async t => {
t = tspl(t, { plan: 2 })
t.throws(() => {
// eslint-disable-next-line
new Client('https://localhost:1000', {
allowH2: true,
maxConcurrentStreams: {}
})
}, {
message: 'maxConcurrentStreams must be a positive integer, greater than 0'
})
t.throws(() => {
// eslint-disable-next-line
new Client('https://localhost:1000', {
allowH2: true,
maxConcurrentStreams: -1
})
}, {
message: 'maxConcurrentStreams must be a positive integer, greater than 0'
})
await t.completed
})
test(
'Request should fail if allowH2 is false and server advertises h1 only',
async t => {
t = tspl(t, { plan: 1 })
const server = createSecureServer(
{
...await pem.generate({ opts: { keySize: 2048 } }),
allowHTTP1: false,
ALPNProtocols: ['http/1.1']
},
(req, res) => {
t.fail('Should not create a valid h2 stream')
}
)
after(() => server.close())
await once(server.listen(0), 'listening')
const client = new Client(`https://localhost:${server.address().port}`, {
allowH2: false,
connect: {
rejectUnauthorized: false
}
})
after(() => client.close())
await t.rejects(client.request({
path: '/',
method: 'GET',
headers: {
'x-my-header': 'foo'
}
}))
await t.completed
})
================================================
FILE: test/http2-late-data.js
================================================
'use strict'
const { test, after } = require('node:test')
const { EventEmitter } = require('node:events')
const { tspl } = require('@matteo.collina/tspl')
const connectH2 = require('../lib/dispatcher/client-h2')
const Request = require('../lib/core/request')
const {
kUrl,
kSocket,
kMaxConcurrentStreams,
kHTTP2InitialWindowSize,
kHTTP2ConnectionWindowSize,
kBodyTimeout,
kStrictContentLength,
kQueue,
kRunningIdx,
kPendingIdx,
kOnError,
kResume,
kRunning,
kPingInterval
} = require('../lib/core/symbols')
test('Should ignore late http2 data after request completion', async (t) => {
t = tspl(t, { plan: 6 })
const http2 = require('node:http2')
const originalConnect = http2.connect
class FakeSocket extends EventEmitter {
constructor () {
super()
this.destroyed = false
}
destroy () {
this.destroyed = true
return this
}
ref () {}
unref () {}
}
class FakeStream extends EventEmitter {
setTimeout () {}
pause () {}
resume () {}
close () {}
write () { return true }
end () {}
cork () {}
uncork () {}
}
class FakeSession extends EventEmitter {
constructor (stream) {
super()
this.stream = stream
this.closed = false
this.destroyed = false
}
request () {
return this.stream
}
close () {
this.closed = true
}
destroy () {
this.destroyed = true
}
ref () {}
unref () {}
ping (_, cb) {
cb(null, 0)
}
}
const stream = new FakeStream()
const session = new FakeSession(stream)
http2.connect = function connectStub () {
return session
}
after(() => {
http2.connect = originalConnect
})
let resumeCalls = 0
let onDataCalls = 0
let onCompleteCalls = 0
const client = {
[kUrl]: new URL('https://localhost'),
[kSocket]: null,
[kMaxConcurrentStreams]: 100,
[kHTTP2InitialWindowSize]: null,
[kHTTP2ConnectionWindowSize]: null,
[kBodyTimeout]: 30_000,
[kStrictContentLength]: true,
[kQueue]: [],
[kRunningIdx]: 0,
[kPendingIdx]: 0,
[kRunning]: 1,
[kPingInterval]: 0,
[kOnError] (err) {
t.ifError(err)
},
[kResume] () {
resumeCalls++
},
emit () {},
destroyed: false
}
const context = connectH2(client, new FakeSocket())
const request = new Request('https://localhost', {
path: '/',
method: 'GET',
headers: {}
}, {
onConnect () {},
onHeaders () {
return true
},
onData () {
onDataCalls++
return true
},
onComplete (trailers) {
onCompleteCalls++
t.strictEqual(trailers['x-trailer'], 'hello')
},
onError (err) {
t.ifError(err)
}
})
client[kQueue].push(request)
t.ok(context.write(request))
stream.emit('response', { ':status': 200 })
stream.emit('trailers', { 'x-trailer': 'hello' })
t.doesNotThrow(() => {
stream.emit('data', Buffer.from('late-data'))
})
stream.emit('end')
t.strictEqual(onCompleteCalls, 1)
t.strictEqual(onDataCalls, 0)
t.ok(resumeCalls >= 1)
await t.completed
})
================================================
FILE: test/http2-pseudo-headers.js
================================================
'use strict'
const { tspl } = require('@matteo.collina/tspl')
const { test, after } = require('node:test')
const { createSecureServer } = require('node:http2')
const { once } = require('node:events')
const pem = require('@metcoder95/https-pem')
const { Client } = require('..')
test('Should provide pseudo-headers in proper order', async t => {
t = tspl(t, { plan: 2 })
const server = createSecureServer(await pem.generate({ opts: { keySize: 2048 } }))
server.on('stream', (stream, _headers, _flags, rawHeaders) => {
t.deepStrictEqual(rawHeaders, [
':authority',
`localhost:${server.address().port}`,
':method',
'GET',
':path',
'/',
':scheme',
'https'
])
stream.respond({
'content-type': 'text/plain; charset=utf-8',
':status': 200
})
stream.end()
})
after(() => server.close())
await once(server.listen(0), 'listening')
const client = new Client(`https://localhost:${server.address().port}`, {
connect: {
rejectUnauthorized: false
},
allowH2: true
})
after(() => client.close())
const response = await client.request({
path: '/',
method: 'GET'
})
t.strictEqual(response.statusCode, 200)
await response.body.dump()
await t.completed
})
test('The h2 pseudo-headers is not included in the headers', async t => {
t = tspl(t, { plan: 2 })
const server = createSecureServer(await pem.generate({ opts: { keySize: 2048 } }))
server.on('stream', (stream, headers) => {
stream.respond({
':status': 200
})
stream.end('hello h2!')
})
after(() => server.close())
await once(server.listen(0), 'listening')
const client = new Client(`https://localhost:${server.address().port}`, {
connect: {
rejectUnauthorized: false
},
allowH2: true
})
after(() => client.close())
const response = await client.request({
path: '/',
method: 'GET'
})
await response.body.text()
t.strictEqual(response.statusCode, 200)
t.strictEqual(response.headers[':status'], undefined)
await t.completed
})
================================================
FILE: test/http2-resume-null-request.js
================================================
'use strict'
// Regression test for:
// TypeError: Cannot read properties of null (reading 'servername')
// at _resume (lib/dispatcher/client.js)
//
// Race condition in H2: when a stream's 'end' event fires, client-h2.js does:
// client[kQueue][client[kRunningIdx]++] = null <- nulls the slot
// client[kResume]() <- _resume reads null slot
//
// If kPendingIdx was reset to kRunningIdx (e.g. by onHttp2SocketClose) between
// writeH2 dispatching the stream and the 'end' event firing, kPendingIdx now
// points at the null slot. _resume fetches kQueue[kPendingIdx] = null and
// crashes on null.servername.
//
// Fix: null guard in _resume after fetching the request from the queue.
const { test } = require('node:test')
const assert = require('node:assert')
const { Client } = require('..')
const {
kQueue,
kRunningIdx,
kPendingIdx,
kResume
} = require('../lib/core/symbols')
test('_resume should not crash when kQueue[kPendingIdx] is null', () => {
// Create a client against a non-existent server — we never connect,
// we only need the properly-initialized internal state.
const client = new Client('https://localhost:1', {
connect: { rejectUnauthorized: false },
allowH2: true
})
// Reproduce the exact queue state that triggers the bug:
//
// kQueue = [null] (slot was nulled by: kQueue[kRunningIdx++] = null)
// kRunningIdx = 0 (points at the null slot)
// kPendingIdx = 0 (reset to kRunningIdx by onHttp2SocketClose)
//
// kPending = kQueue.length - kPendingIdx = 1 - 0 = 1 (non-zero, passes the guard)
// kRunning = kPendingIdx - kRunningIdx = 0 - 0 = 0 (below pipelining limit)
// kQueue[kPendingIdx] = null (the crash point)
client[kQueue].push(null)
client[kRunningIdx] = 0
client[kPendingIdx] = 0
// Calling kResume() now replicates what client-h2.js does after nulling the slot.
// Without the fix: TypeError: Cannot read properties of null (reading 'servername')
// With the fix: returns early safely.
assert.doesNotThrow(
() => client[kResume](),
'Expected _resume to handle null queue slot without throwing'
)
// Restore a valid queue state before destroying so the client
// doesn't trip over the null slot we injected during cleanup.
client[kQueue].length = 0
client[kRunningIdx] = 0
client[kPendingIdx] = 0
client.destroy().catch(() => {})
})
================================================
FILE: test/http2-stream.js
================================================
'use strict'
const { tspl } = require('@matteo.collina/tspl')
const { test, after } = require('node:test')
const { createSecureServer } = require('node:http2')
const { once } = require('node:events')
const pem = require('@metcoder95/https-pem')
const { Client } = require('..')
test('Should throw informational error on half-closed streams (remote)', async t => {
t = tspl(t, { plan: 2 })
const server = createSecureServer(await pem.generate({ opts: { keySize: 2048 } }))
server.on('stream', (stream, headers) => {
stream.destroy()
})
after(() => server.close())
await once(server.listen(0), 'listening')
const client = new Client(`https://localhost:${server.address().port}`, {
connect: {
rejectUnauthorized: false
},
allowH2: true
})
after(() => client.close())
await t.rejects(client
.request({
path: '/',
method: 'GET'
}), {
message: 'HTTP/2: stream half-closed (remote)',
code: 'UND_ERR_INFO'
})
await t.rejects(client
.request({
path: '/',
method: 'GET'
}), {
message: 'HTTP/2: stream half-closed (remote)',
code: 'UND_ERR_INFO'
})
await t.completed
})
================================================
FILE: test/http2-timeout.js
================================================
'use strict'
const { tspl } = require('@matteo.collina/tspl')
const { test, after } = require('node:test')
const { createSecureServer } = require('node:http2')
const { createReadStream } = require('node:fs')
const { once } = require('node:events')
const pem = require('@metcoder95/https-pem')
const { Client } = require('..')
test('Should handle http2 stream timeout', async t => {
t = tspl(t, { plan: 1 })
const server = createSecureServer(await pem.generate({ opts: { keySize: 2048 } }))
const stream = createReadStream(__filename)
server.on('stream', (stream, headers) => {
stream.respond({
'content-type': 'text/plain; charset=utf-8',
'x-custom-h2': headers['x-my-header'],
':status': 200
})
setTimeout(() => {
stream.end('hello h2!')
}, 500)
})
after(() => server.close())
await once(server.listen(0), 'listening')
const client = new Client(`https://localhost:${server.address().port}`, {
connect: {
rejectUnauthorized: false
},
allowH2: true,
bodyTimeout: 50
})
after(() => client.close())
const res = await client.request({
path: '/',
method: 'PUT',
headers: {
'x-my-header': 'foo'
},
body: stream
})
await t.rejects(res.body.text(), {
message: 'HTTP/2: "stream timeout after 50"'
})
await t.completed
})
================================================
FILE: test/http2-trailers.js
================================================
'use strict'
const { tspl } = require('@matteo.collina/tspl')
const { test, after } = require('node:test')
const { createSecureServer } = require('node:http2')
const { once } = require('node:events')
const pem = require('@metcoder95/https-pem')
const { Client } = require('..')
test('Should handle http2 trailers', async t => {
t = tspl(t, { plan: 1 })
const server = createSecureServer(await pem.generate({ opts: { keySize: 2048 } }))
server.on('stream', (stream, headers) => {
stream.respond({
'content-type': 'text/plain; charset=utf-8',
'x-custom-h2': headers['x-my-header'],
':status': 200
},
{
waitForTrailers: true
})
stream.on('wantTrailers', () => {
stream.sendTrailers({
'x-trailer': 'hello'
})
})
stream.end('hello h2!')
})
after(() => server.close())
await once(server.listen(0, '127.0.0.1'), 'listening')
const client = new Client(`https://${server.address().address}:${server.address().port}`, {
connect: {
rejectUnauthorized: false
},
allowH2: true
})
after(() => client.close())
client.dispatch({
path: '/',
method: 'PUT',
body: 'hello'
}, {
onConnect () {
},
onHeaders () {
return true
},
onData () {
return true
},
onComplete (trailers) {
t.strictEqual(trailers['x-trailer'], 'hello')
},
onError (err) {
t.ifError(err)
}
})
await t.completed
})
================================================
FILE: test/http2-window-size.js
================================================
'use strict'
const { tspl } = require('@matteo.collina/tspl')
const { test, after } = require('node:test')
const { EventEmitter } = require('node:events')
const connectH2 = require('../lib/dispatcher/client-h2')
const {
kUrl,
kSocket,
kMaxConcurrentStreams,
kHTTP2Session,
kHTTP2InitialWindowSize,
kHTTP2ConnectionWindowSize
} = require('../lib/core/symbols')
test('Should plumb initialWindowSize and connectionWindowSize into the HTTP/2 session creation path', async (t) => {
t = tspl(t, { plan: 6 })
const http2 = require('node:http2')
const originalConnect = http2.connect
/** @type {any} */
let seenConnectOptions = null
/** @type {number[]} */
const setLocalWindowSizeCalls = []
class FakeSession extends EventEmitter {
unref () {}
ref () {}
close () {}
destroy () {}
request () {
throw new Error('not implemented')
}
setLocalWindowSize (size) {
setLocalWindowSizeCalls.push(size)
}
}
class FakeSocket extends EventEmitter {
constructor () {
super()
this.destroyed = false
}
unref () {}
ref () {}
destroy () {
this.destroyed = true
return this
}
}
const fakeSession = new FakeSession()
http2.connect = function connectStub (_authority, options) {
seenConnectOptions = options
return fakeSession
}
after(() => {
http2.connect = originalConnect
})
const initialWindowSize = 12345
const connectionWindowSize = 77777
const client = {
[kUrl]: new URL('https://localhost'),
[kMaxConcurrentStreams]: 100,
[kHTTP2InitialWindowSize]: initialWindowSize,
[kHTTP2ConnectionWindowSize]: connectionWindowSize,
[kSocket]: null,
[kHTTP2Session]: null
}
const socket = new FakeSocket()
connectH2(client, socket)
t.ok(seenConnectOptions && seenConnectOptions.settings)
t.strictEqual(seenConnectOptions.settings.enablePush, false)
t.strictEqual(
seenConnectOptions.settings.initialWindowSize,
initialWindowSize
)
t.strictEqual(client[kHTTP2Session], fakeSession)
// Emit 'connect' event
process.nextTick(() => {
fakeSession.emit('connect')
})
await new Promise((resolve) => process.nextTick(resolve))
t.strictEqual(setLocalWindowSizeCalls.length, 1)
t.strictEqual(setLocalWindowSizeCalls[0], connectionWindowSize)
await t.completed
})
================================================
FILE: test/https.js
================================================
'use strict'
const { tspl } = require('@matteo.collina/tspl')
const { test, after } = require('node:test')
const { Client } = require('..')
const { createServer } = require('node:https')
const pem = require('@metcoder95/https-pem')
test('https get with tls opts', async (t) => {
t = tspl(t, { plan: 6 })
const server = createServer({ ...pem, joinDuplicateHeaders: true }, (req, res) => {
t.strictEqual('/', req.url)
t.strictEqual('GET', req.method)
res.setHeader('content-type', 'text/plain')
res.end('hello')
})
after(() => server.close())
server.listen(0, () => {
const client = new Client(`https://localhost:${server.address().port}`, {
tls: {
rejectUnauthorized: false
}
})
after(() => client.close())
client.on('disconnect', () => {
if (!client.closed && !client.destroyed) {
t.fail('unexpected disconnect')
}
})
client.request({ path: '/', method: 'GET' }, (err, { statusCode, headers, body }) => {
t.ifError(err)
t.strictEqual(statusCode, 200)
t.strictEqual(headers['content-type'], 'text/plain')
const bufs = []
body.on('data', (buf) => {
bufs.push(buf)
})
body.on('end', () => {
t.strictEqual('hello', Buffer.concat(bufs).toString('utf8'))
})
})
})
await t.completed
})
test('https get with tls opts ip', async (t) => {
t = tspl(t, { plan: 6 })
const server = createServer({ ...pem, joinDuplicateHeaders: true }, (req, res) => {
t.strictEqual('/', req.url)
t.strictEqual('GET', req.method)
res.setHeader('content-type', 'text/plain')
res.end('hello')
})
after(() => server.close())
server.listen(0, () => {
const client = new Client(`https://127.0.0.1:${server.address().port}`, {
tls: {
rejectUnauthorized: false
}
})
after(() => client.close())
client.on('disconnect', () => {
if (!client.closed && !client.destroyed) {
t.fail('unexpected disconnect')
}
})
client.request({ path: '/', method: 'GET' }, (err, { statusCode, headers, body }) => {
t.ifError(err)
t.strictEqual(statusCode, 200)
t.strictEqual(headers['content-type'], 'text/plain')
const bufs = []
body.on('data', (buf) => {
bufs.push(buf)
})
body.on('end', () => {
t.strictEqual('hello', Buffer.concat(bufs).toString('utf8'))
})
})
})
await t.completed
})
================================================
FILE: test/imports/undici-import.ts
================================================
import { expectType } from 'tsd'
import { Dispatcher, interceptors, request } from '../../'
async function exampleCode () {
const retry = interceptors.retry()
const rd = interceptors.redirect()
const dump = interceptors.dump()
expectType(retry)
expectType(rd)
expectType(dump)
await request('http://localhost:3000/foo')
}
exampleCode()
================================================
FILE: test/inflight-and-close.js
================================================
'use strict'
const { tspl } = require('@matteo.collina/tspl')
const { test } = require('node:test')
const { request } = require('..')
const http = require('node:http')
test('inflight and close', async (t) => {
t = tspl(t, { plan: 3 })
const server = http.createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.writeHead(200)
res.end('Response body')
res.socket.end() // Close the connection immediately with every response
}).listen(0, '127.0.0.1', function () {
const url = `http://127.0.0.1:${this.address().port}`
request(url)
.then(({ statusCode, headers, body }) => {
t.ok(true, 'first response')
body.resume()
body.on('close', function () {
t.ok(true, 'first body closed')
})
return request(url)
.then(({ statusCode, headers, body }) => {
t.ok(true, 'second response')
body.resume()
body.on('close', function () {
server.close()
})
})
}).catch((err) => {
t.ifError(err)
})
})
await t.completed
})
================================================
FILE: test/infra/collect-a-sequence-of-code-points.js
================================================
'use strict'
const { test } = require('node:test')
const { collectASequenceOfCodePoints } = require('../../lib/web/infra')
test('https://infra.spec.whatwg.org/#collect-a-sequence-of-code-points', (t) => {
const input = 'text/plain;base64,'
const position = { position: 0 }
const result = collectASequenceOfCodePoints(
(char) => char !== ';',
input,
position
)
t.assert.strictEqual(result, 'text/plain')
t.assert.strictEqual(position.position, input.indexOf(';'))
})
================================================
FILE: test/install.js
================================================
'use strict'
const { test } = require('node:test')
const assert = require('node:assert')
const undici = require('../index')
const { installedExports } = require('../lib/global')
test('install() should overwrite only specified elements in globalThis', () => {
const exportsToCheck = new Set(installedExports)
// Use a Proxy to verify that only the expected globals are set
// and no other properties are added to globalThis by undici.install().
const proxyGlobalThis = new Proxy(globalThis, {
set (target, prop, value) {
if (exportsToCheck.has(prop)) {
target[prop] = value
exportsToCheck.delete(prop)
return true
}
throw new Error(`Unexpected global set: ${String(prop)}`)
}
})
// eslint-disable-next-line no-global-assign
globalThis = proxyGlobalThis
undici.install()
assert.strictEqual(exportsToCheck.size, 0, `Some expected globals were not set: ${[...exportsToCheck].join(', ')}`)
// Verify that the installed globals match the exports from undici
for (const name of installedExports) {
assert.strictEqual(globalThis[name], undici[name])
}
// Test that the installed classes are functional
const headers = new globalThis.Headers([['content-type', 'application/json']])
assert.strictEqual(headers.get('content-type'), 'application/json')
const request = new globalThis.Request('https://example.com')
assert.strictEqual(request.url, 'https://example.com/')
const response = new globalThis.Response('test body')
assert.strictEqual(response.status, 200)
const formData = new globalThis.FormData()
formData.append('key', 'value')
assert.strictEqual(formData.get('key'), 'value')
})
================================================
FILE: test/interceptors/cache-async-store.js
================================================
'use strict'
const { test, after, describe } = require('node:test')
const { strictEqual, notStrictEqual } = require('node:assert')
const { createServer } = require('node:http')
const { once } = require('node:events')
const { Readable } = require('node:stream')
const { request, Client, interceptors } = require('../../index')
const MemoryCacheStore = require('../../lib/cache/memory-cache-store')
const FakeTimers = require('@sinonjs/fake-timers')
const { setTimeout } = require('node:timers/promises')
/**
* Wraps a MemoryCacheStore to simulate an async remote store:
* - get() always returns a Promise
* - body is returned as a Readable stream instead of an array
*/
class AsyncCacheStore {
#inner
constructor () {
this.#inner = new MemoryCacheStore()
}
async get (key) {
const result = this.#inner.get(key)
if (!result) return undefined
const { body, ...rest } = result
const readable = new Readable({ read () {} })
if (body) {
for (const chunk of body) {
readable.push(chunk)
}
}
readable.push(null)
return { ...rest, body: readable }
}
createWriteStream (key, value) {
return this.#inner.createWriteStream(key, value)
}
delete (key) {
return this.#inner.delete(key)
}
}
describe('cache interceptor with async store', () => {
test('stale-while-revalidate 304 refreshes cache with async store', async () => {
const clock = FakeTimers.install({ now: 1 })
after(() => clock.uninstall())
let count200 = 0
let count304 = 0
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.sendDate = false
res.setHeader('Date', new Date(clock.now).toUTCString())
res.setHeader('Cache-Control', 'public, max-age=10, stale-while-revalidate=3600')
res.setHeader('ETag', '"test-etag"')
if (req.headers['if-none-match']) {
count304++
res.statusCode = 304
res.end()
} else {
res.end('hello world ' + count200++)
}
})
server.listen(0)
await once(server, 'listening')
const store = new AsyncCacheStore()
const dispatcher = new Client(`http://localhost:${server.address().port}`)
.compose(interceptors.cache({ store }))
after(async () => {
server.close()
await dispatcher.close()
})
const url = `http://localhost:${server.address().port}`
// First request, populates cache
{
const res = await request(url, { dispatcher })
strictEqual(await res.body.text(), 'hello world 0')
strictEqual(res.statusCode, 200)
strictEqual(res.headers.warning, undefined)
}
// Advance past max-age into stale-while-revalidate window
clock.tick(12000)
// Second request: stale, triggers background 304 revalidation
{
const res = await request(url, { dispatcher })
strictEqual(await res.body.text(), 'hello world 0')
strictEqual(res.statusCode, 200)
strictEqual(res.headers.warning, '110 - "response is stale"')
await setTimeout(100)
}
// Third request: should be fresh after 304 revalidation
{
clock.tick(10)
const res = await request(url, { dispatcher })
strictEqual(await res.body.text(), 'hello world 0')
strictEqual(res.statusCode, 200)
strictEqual(res.headers.warning, undefined)
}
strictEqual(count200, 1)
strictEqual(count304, 1)
})
test('stale-while-revalidate 200 refreshes cache with async store', async () => {
const clock = FakeTimers.install({ now: 1 })
after(() => clock.uninstall())
let requestCount = 0
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.sendDate = false
res.setHeader('Date', new Date(clock.now).toUTCString())
res.setHeader('Cache-Control', 'public, max-age=10, stale-while-revalidate=3600')
res.setHeader('ETag', `"etag-${requestCount}"`)
res.end('hello world ' + requestCount++)
})
server.listen(0)
await once(server, 'listening')
const store = new AsyncCacheStore()
const dispatcher = new Client(`http://localhost:${server.address().port}`)
.compose(interceptors.cache({ store }))
after(async () => {
server.close()
await dispatcher.close()
})
const url = `http://localhost:${server.address().port}`
// First request
{
const res = await request(url, { dispatcher })
strictEqual(await res.body.text(), 'hello world 0')
}
// Advance past max-age
clock.tick(12000)
// Stale response, triggers background 200 revalidation
{
const res = await request(url, { dispatcher })
strictEqual(await res.body.text(), 'hello world 0')
strictEqual(res.headers.warning, '110 - "response is stale"')
await setTimeout(100)
}
// Should be fresh with new content
{
clock.tick(10)
const res = await request(url, { dispatcher })
strictEqual(await res.body.text(), 'hello world 1')
strictEqual(res.headers.warning, undefined)
}
})
test('null vary values are not sent in revalidation headers', async () => {
const clock = FakeTimers.install({ now: 1 })
after(() => clock.uninstall())
let revalidationHeaders = null
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.sendDate = false
res.setHeader('Date', new Date(clock.now).toUTCString())
res.setHeader('Cache-Control', 'public, max-age=10, stale-while-revalidate=3600')
res.setHeader('ETag', '"test-etag"')
res.setHeader('Vary', 'X-Custom-Header, X-Another-Header')
if (req.headers['if-none-match']) {
revalidationHeaders = { ...req.headers }
res.statusCode = 304
res.end()
} else {
res.end('hello world')
}
})
server.listen(0)
await once(server, 'listening')
const store = new AsyncCacheStore()
const dispatcher = new Client(`http://localhost:${server.address().port}`)
.compose(interceptors.cache({ store }))
after(async () => {
server.close()
await dispatcher.close()
})
const url = `http://localhost:${server.address().port}`
// First request without X-Custom-Header or X-Another-Header
// These will be stored as null in the vary record
{
const res = await request(url, { dispatcher })
strictEqual(await res.body.text(), 'hello world')
}
// Advance past max-age
clock.tick(12000)
// Trigger stale-while-revalidate
{
const res = await request(url, { dispatcher })
strictEqual(res.headers.warning, '110 - "response is stale"')
await setTimeout(100)
}
// Verify the revalidation request did NOT include null vary headers
notStrictEqual(revalidationHeaders, null)
strictEqual(revalidationHeaders['x-custom-header'], undefined)
strictEqual(revalidationHeaders['x-another-header'], undefined)
strictEqual(revalidationHeaders['if-none-match'], '"test-etag"')
})
})
================================================
FILE: test/interceptors/cache-query-params.js
================================================
'use strict'
const { test, after } = require('node:test')
const { equal, notEqual } = require('node:assert')
const { createServer } = require('node:http')
const { once } = require('node:events')
const { Client, request, interceptors } = require('../../')
test('query parameters create separate cache entries', async () => {
let requestCount = 0
const server = createServer((req, res) => {
requestCount++
const url = new URL(req.url, 'http://localhost')
const param = url.searchParams.get('param') || 'default'
res.writeHead(200, {
'Content-Type': 'application/json',
'Cache-Control': 'public, max-age=100'
})
res.end(JSON.stringify({
message: `Response for param=${param}`,
requestNumber: requestCount
}))
})
server.listen(0)
await once(server, 'listening')
const client = new Client(`http://localhost:${server.address().port}`)
.compose(interceptors.cache())
after(async () => {
server.close()
await client.close()
})
const origin = `http://localhost:${server.address().port}`
// First request with param=value1
const response1 = await request(origin, {
dispatcher: client,
query: { param: 'value1' }
})
const body1 = await response1.body.text()
equal(requestCount, 1, 'First request should hit the server')
// Second request with same param - should be cached
const response2 = await request(origin, {
dispatcher: client,
query: { param: 'value1' }
})
const body2 = await response2.body.text()
equal(requestCount, 1, 'Second request with same query should be cached')
equal(body1, body2, 'Cached response should match original')
// Third request with different param - should NOT be cached
const response3 = await request(origin, {
dispatcher: client,
query: { param: 'value2' }
})
const body3 = await response3.body.text()
equal(requestCount, 2, 'Request with different query should hit the server')
notEqual(body1, body3, 'Different query parameters should create separate cache entries')
})
test('complex query parameters are handled correctly', async () => {
let requestCount = 0
const server = createServer((req, res) => {
requestCount++
res.writeHead(200, {
'Content-Type': 'application/json',
'Cache-Control': 'public, max-age=100'
})
res.end(JSON.stringify({
url: req.url,
requestNumber: requestCount
}))
})
server.listen(0)
await once(server, 'listening')
const client = new Client(`http://localhost:${server.address().port}`)
.compose(interceptors.cache())
after(async () => {
server.close()
await client.close()
})
const origin = `http://localhost:${server.address().port}`
// Complex query with arrays and multiple parameters
const complexQuery = {
search: 'hello world',
tags: ['javascript', 'nodejs'],
limit: 10,
active: true
}
// First request
const response1 = await request(origin, {
dispatcher: client,
query: complexQuery
})
const body1 = await response1.body.text()
equal(requestCount, 1, 'First complex query should hit the server')
// Same complex query - should be cached
const response2 = await request(origin, {
dispatcher: client,
query: complexQuery
})
const body2 = await response2.body.text()
equal(requestCount, 1, 'Same complex query should be cached')
equal(body1, body2, 'Complex query parameters should be cached correctly')
// Slightly different query - should NOT be cached
const response3 = await request(origin, {
dispatcher: client,
query: {
search: 'hello world',
tags: ['javascript', 'nodejs'],
limit: 20, // Different limit
active: true
}
})
const body3 = await response3.body.text()
equal(requestCount, 2, 'Different complex query should hit the server')
notEqual(body1, body3, 'Different query parameters should create separate cache entries')
})
test('query parameters vs path equivalence', async () => {
let requestCount = 0
const server = createServer((req, res) => {
requestCount++
res.writeHead(200, {
'Content-Type': 'application/json',
'Cache-Control': 'public, max-age=100'
})
res.end(JSON.stringify({
url: req.url,
requestNumber: requestCount
}))
})
server.listen(0)
await once(server, 'listening')
const client = new Client(`http://localhost:${server.address().port}`)
.compose(interceptors.cache())
after(async () => {
server.close()
await client.close()
})
const origin = `http://localhost:${server.address().port}`
// Request using query object
const response1 = await request(origin, {
dispatcher: client,
query: { foo: 'bar', baz: 'qux' }
})
const body1 = await response1.body.text()
equal(requestCount, 1, 'Query object request should hit the server')
// Request using path with query string - should be cached if URLs match
const response2 = await request(`${origin}/?foo=bar&baz=qux`, {
dispatcher: client
})
const body2 = await response2.body.text()
equal(requestCount, 1, 'Equivalent path query should be cached')
equal(body1, body2, 'Query object and path query should be equivalent')
})
test('empty and undefined query parameters', async () => {
let requestCount = 0
const server = createServer((req, res) => {
requestCount++
res.writeHead(200, {
'Content-Type': 'application/json',
'Cache-Control': 'public, max-age=100'
})
res.end(JSON.stringify({
url: req.url,
requestNumber: requestCount
}))
})
server.listen(0)
await once(server, 'listening')
const client = new Client(`http://localhost:${server.address().port}`)
.compose(interceptors.cache())
after(async () => {
server.close()
await client.close()
})
const origin = `http://localhost:${server.address().port}`
// Request with no query
const response1 = await request(origin, { dispatcher: client })
const body1 = await response1.body.text()
equal(requestCount, 1, 'No query request should hit the server')
// Request with empty query object - should be cached
const response2 = await request(origin, {
dispatcher: client,
query: {}
})
const body2 = await response2.body.text()
equal(requestCount, 1, 'Empty query object should be cached')
equal(body1, body2, 'No query and empty query should be equivalent')
// Request with undefined query - should be cached
const response3 = await request(origin, {
dispatcher: client,
query: undefined
})
const body3 = await response3.body.text()
equal(requestCount, 1, 'Undefined query should be cached')
equal(body1, body3, 'No query and undefined query should be equivalent')
})
================================================
FILE: test/interceptors/cache-revalidate-stale.js
================================================
'use strict'
const { test, after, describe } = require('node:test')
const { strictEqual } = require('node:assert')
const { createServer } = require('node:http')
const { once } = require('node:events')
const { request, Client, interceptors } = require('../../index')
const FakeTimers = require('@sinonjs/fake-timers')
const { setTimeout } = require('node:timers/promises')
test('revalidates the request when the response is stale', async () => {
const clock = FakeTimers.install({
now: 1
})
after(() => clock.uninstall())
let count = 0
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.sendDate = false
res.setHeader('Date', new Date(clock.now).toUTCString())
res.setHeader('Cache-Control', 'public, max-age=1')
res.end('hello world ' + count++)
})
server.listen(0)
await once(server, 'listening')
const dispatcher = new Client(`http://localhost:${server.address().port}`)
.compose(interceptors.cache())
after(async () => {
server.close()
await dispatcher.close()
})
const url = `http://localhost:${server.address().port}`
{
const res = await request(url, { dispatcher })
strictEqual(await res.body.text(), 'hello world 0')
}
clock.tick(999)
{
const res = await request(url, { dispatcher })
strictEqual(await res.body.text(), 'hello world 0')
}
clock.tick(1)
{
const res = await request(url, { dispatcher })
strictEqual(await res.body.text(), 'hello world 1')
}
clock.tick(999)
{
const res = await request(url, { dispatcher })
strictEqual(await res.body.text(), 'hello world 1')
}
clock.tick(1)
{
const res = await request(url, { dispatcher })
strictEqual(await res.body.text(), 'hello world 2')
}
})
describe('revalidates the request, handles 304s during stale-while-revalidate', async () => {
function isStale (res) {
return res.headers.warning === '110 - "response is stale"'
}
async function revalidateTest (useEtag = false) {
const clock = FakeTimers.install({
now: 1
})
after(() => clock.uninstall())
let count200 = 0
let count304 = 0
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.sendDate = false
res.setHeader('Date', new Date(clock.now).toUTCString())
res.setHeader('Cache-Control', 'public, max-age=10, stale-while-revalidate=3600')
if (useEtag) {
res.setHeader('ETag', '"xxx"')
}
// revalidation response.
if (req.headers['if-none-match'] || req.headers['if-modified-since']) {
count304++
res.statusCode = 304
res.end()
} else {
res.end('hello world ' + count200++)
}
})
server.listen(0)
await once(server, 'listening')
const dispatcher = new Client(`http://localhost:${server.address().port}`)
.compose(interceptors.cache())
after(async () => {
server.close()
await dispatcher.close()
})
const url = `http://localhost:${server.address().port}`
// initial request, cache the response
{
const res = await request(url, { dispatcher })
strictEqual(await res.body.text(), 'hello world 0')
strictEqual(isStale(res), false)
strictEqual(res.statusCode, 200)
}
// wait nearly a second, still fresh
{
clock.tick(900)
const res = await request(url, { dispatcher })
strictEqual(await res.body.text(), 'hello world 0')
strictEqual(isStale(res), false)
strictEqual(res.statusCode, 200)
}
// within stale-while-revalidate window, still return stale cached response, revalidate in background
{
clock.tick(12000)
const res = await request(url, { dispatcher })
strictEqual(await res.body.text(), 'hello world 0')
strictEqual(isStale(res), true)
strictEqual(res.statusCode, 200)
await setTimeout(100) // wait for revalidation to be complete.
}
// should get revalidated content, not stale.
{
clock.tick(10)
const res = await request(url, { dispatcher })
strictEqual(await res.body.text(), 'hello world 0')
strictEqual(isStale(res), false)
strictEqual(res.statusCode, 200)
}
strictEqual(count200, 1)
strictEqual(count304, 1)
}
test('using if-none-match', async () => await revalidateTest(true))
test('using if-modified-since', async () => await revalidateTest(false))
})
================================================
FILE: test/interceptors/cache.js
================================================
'use strict'
const { createServer } = require('node:http')
const { describe, test, after } = require('node:test')
const { once } = require('node:events')
const { equal, strictEqual, notEqual, fail } = require('node:assert')
const { setTimeout: sleep } = require('node:timers/promises')
const FakeTimers = require('@sinonjs/fake-timers')
const { Client, interceptors, cacheStores: { MemoryCacheStore } } = require('../../index')
const { makeCacheKey } = require('../../lib/util/cache.js')
describe('Cache Interceptor', () => {
test('caches request', async () => {
let requestsToOrigin = 0
const server = createServer({ joinDuplicateHeaders: true }, (_, res) => {
requestsToOrigin++
res.setHeader('cache-control', 's-maxage=10')
res.end('asd')
}).listen(0)
const client = new Client(`http://localhost:${server.address().port}`)
.compose(interceptors.cache())
after(async () => {
server.close()
await client.close()
})
await once(server, 'listening')
// Sanity check
equal(requestsToOrigin, 0)
/**
* @type {import('../../types/dispatcher').default.RequestOptions}
*/
const request = {
origin: 'localhost',
method: 'GET',
path: '/'
}
{
const res = await client.request(request)
equal(requestsToOrigin, 1)
strictEqual(await res.body.text(), 'asd')
}
{
const res = await client.request(request)
equal(requestsToOrigin, 1)
strictEqual(await res.body.text(), 'asd')
}
})
test('vary directives used to decide which response to use', async () => {
let requestsToOrigin = 0
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
requestsToOrigin++
res.setHeader('cache-control', 's-maxage=10')
res.setHeader('vary', 'a')
if (req.headers.a === 'asd123') {
res.end('asd')
} else {
res.end('dsa')
}
}).listen(0)
const client = new Client(`http://localhost:${server.address().port}`)
.compose(interceptors.cache())
after(async () => {
server.close()
await client.close()
})
await once(server, 'listening')
/**
* @type {import('../../types/dispatcher').default.RequestOptions}
*/
const requestA = {
origin: 'localhost',
method: 'GET',
path: '/',
headers: {
a: 'asd123'
}
}
/**
* @type {import('../../types/dispatcher').default.RequestOptions}
*/
const requestB = {
origin: 'localhost',
method: 'GET',
path: '/',
headers: {
a: 'dsa'
}
}
// Should reach origin
{
const res = await client.request(requestA)
equal(requestsToOrigin, 1)
strictEqual(await res.body.text(), 'asd')
}
// Should reach origin
{
const res = await client.request(requestB)
equal(requestsToOrigin, 2)
strictEqual(await res.body.text(), 'dsa')
}
// Should be cached
{
const res = await client.request(requestA)
equal(requestsToOrigin, 2)
strictEqual(await res.body.text(), 'asd')
}
// Should be cached
{
const res = await client.request(requestB)
equal(requestsToOrigin, 2)
strictEqual(await res.body.text(), 'dsa')
}
})
test('revalidates reponses with no-cache directive, regardless of cacheByDefault', async () => {
let requestCount = 0
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
++requestCount
res.setHeader('Vary', 'Accept-Encoding')
res.setHeader('cache-control', 'no-cache')
res.end(`Request count: ${requestCount}`)
}).listen(0)
after(async () => {
server.close()
await once(server, 'close')
})
await once(server, 'listening')
const client = new Client(`http://localhost:${server.address().port}`)
.compose(interceptors.cache({
cacheByDefault: 1000
}))
const request = {
origin: 'localhost',
method: 'GET',
path: '/'
}
const res1 = await client.request(request)
const body1 = await res1.body.text()
strictEqual(body1, 'Request count: 1')
strictEqual(requestCount, 1)
const res2 = await client.request(request)
const body2 = await res2.body.text()
strictEqual(body2, 'Request count: 2')
strictEqual(requestCount, 2)
})
test('expires caching', async () => {
const clock = FakeTimers.install({
shouldClearNativeTimers: true
})
let requestsToOrigin = 0
let serverError
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
const now = new Date()
now.setSeconds(now.getSeconds() + 1)
res.setHeader('date', 0)
res.setHeader('expires', now.toGMTString())
requestsToOrigin++
res.end('asd')
}).listen(0)
const client = new Client(`http://localhost:${server.address().port}`)
.compose(interceptors.cache())
after(async () => {
server.close()
await client.close()
clock.uninstall()
})
await once(server, 'listening')
strictEqual(requestsToOrigin, 0)
/**
* @type {import('../../types/dispatcher').default.RequestOptions}
*/
const request = {
origin: 'localhost',
method: 'GET',
path: '/'
}
// Send initial request. This should reach the origin
{
const res = await client.request(request)
if (serverError) {
throw serverError
}
equal(requestsToOrigin, 1)
strictEqual(await res.body.text(), 'asd')
}
// This is cached
{
const res = await client.request(request)
if (serverError) {
throw serverError
}
equal(requestsToOrigin, 1)
strictEqual(await res.body.text(), 'asd')
}
clock.tick(1500)
// Response is now stale, the origin should get a request
{
const res = await client.request(request)
equal(requestsToOrigin, 2)
strictEqual(await res.body.text(), 'asd')
}
// Response is now cached, the origin should not get a request
{
const res = await client.request(request)
equal(requestsToOrigin, 2)
strictEqual(await res.body.text(), 'asd')
}
})
test('expires caching with Etag', async () => {
const clock = FakeTimers.install({
shouldClearNativeTimers: true
})
let requestsToOrigin = 0
let serverError
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
const now = new Date()
now.setSeconds(now.getSeconds() + 1)
res.setHeader('date', 0)
res.setHeader('expires', now.toGMTString())
res.setHeader('etag', 'asd123')
requestsToOrigin++
res.end('asd')
}).listen(0)
const client = new Client(`http://localhost:${server.address().port}`)
.compose(interceptors.cache())
after(async () => {
server.close()
await client.close()
clock.uninstall()
})
await once(server, 'listening')
strictEqual(requestsToOrigin, 0)
/**
* @type {import('../../types/dispatcher').default.RequestOptions}
*/
const request = {
origin: 'localhost',
method: 'GET',
path: '/'
}
// Send initial request. This should reach the origin
{
const res = await client.request(request)
if (serverError) {
throw serverError
}
equal(requestsToOrigin, 1)
strictEqual(await res.body.text(), 'asd')
}
// This is cached
{
const res = await client.request(request)
if (serverError) {
throw serverError
}
equal(requestsToOrigin, 1)
strictEqual(await res.body.text(), 'asd')
}
clock.tick(1500)
// Response is now stale, the origin should get a request
{
const res = await client.request(request)
equal(requestsToOrigin, 2)
strictEqual(await res.body.text(), 'asd')
}
// Response is now cached, the origin should not get a request
{
const res = await client.request(request)
equal(requestsToOrigin, 2)
strictEqual(await res.body.text(), 'asd')
}
})
test('max-age caching', async () => {
const clock = FakeTimers.install({
shouldClearNativeTimers: true
})
let requestsToOrigin = 0
let serverError
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.setHeader('date', 0)
res.setHeader('cache-control', 's-maxage=1')
requestsToOrigin++
res.end('asd')
}).listen(0)
const client = new Client(`http://localhost:${server.address().port}`)
.compose(interceptors.cache())
after(async () => {
server.close()
await client.close()
clock.uninstall()
})
await once(server, 'listening')
strictEqual(requestsToOrigin, 0)
/**
* @type {import('../../types/dispatcher').default.RequestOptions}
*/
const request = {
origin: 'localhost',
method: 'GET',
path: '/'
}
// Send initial request. This should reach the origin
{
const res = await client.request(request)
if (serverError) {
throw serverError
}
equal(requestsToOrigin, 1)
strictEqual(await res.body.text(), 'asd')
}
clock.tick(1500)
// Response is now stale, the origin should get a request
{
const res = await client.request(request)
equal(requestsToOrigin, 2)
strictEqual(await res.body.text(), 'asd')
}
// Response is now cached, the origin should not get a request
{
const res = await client.request(request)
equal(requestsToOrigin, 2)
strictEqual(await res.body.text(), 'asd')
}
})
test('vary headers are present in revalidation request', async () => {
const clock = FakeTimers.install({
shouldClearNativeTimers: true
})
let requestsToOrigin = 0
let revalidationRequests = 0
let serverError
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.setHeader('date', 0)
res.setHeader('cache-control', 's-maxage=1, stale-while-revalidate=10')
try {
const ifNoneMatch = req.headers['if-none-match']
if (ifNoneMatch) {
revalidationRequests++
notEqual(req.headers.a, undefined)
notEqual(req.headers['b-mixed-case'], undefined)
res.statusCode = 304
res.end()
} else {
requestsToOrigin++
res.setHeader('vary', 'a, B-MIXED-CASe')
res.setHeader('etag', '"asd"')
res.end('asd')
}
} catch (err) {
serverError = err
res.end()
}
}).listen(0)
const client = new Client(`http://localhost:${server.address().port}`)
.compose(interceptors.cache())
after(async () => {
server.close()
await client.close()
clock.uninstall()
})
await once(server, 'listening')
strictEqual(requestsToOrigin, 0)
strictEqual(revalidationRequests, 0)
const request = {
origin: 'localhost',
path: '/',
method: 'GET'
}
{
const response = await client.request({
...request,
headers: {
a: 'asd',
'b-Mixed-case': 'asd'
}
})
if (serverError) {
throw serverError
}
strictEqual(requestsToOrigin, 1)
strictEqual(await response.body.text(), 'asd')
}
clock.tick(1500)
{
const response = await client.request({
...request,
headers: {
a: 'asd',
'B-mixed-CASE': 'asd'
}
})
if (serverError) {
throw serverError
}
strictEqual(requestsToOrigin, 1)
strictEqual(await response.body.text(), 'asd')
}
// Wait for background revalidation to complete
await sleep(100)
strictEqual(revalidationRequests, 1)
})
test('unsafe methods cause resource to be purged from cache', async () => {
const server = createServer({ joinDuplicateHeaders: true }, (_, res) => res.end('asd')).listen(0)
after(() => server.close())
await once(server, 'listening')
const store = new MemoryCacheStore()
let deleteCalled = false
const originalDelete = store.delete.bind(store)
store.delete = (key) => {
deleteCalled = true
originalDelete(key)
}
const client = new Client(`http://localhost:${server.address().port}`)
.compose(interceptors.cache({
store,
methods: ['GET'] // explicitly only cache GET methods
}))
/**
* @type {import('../../types/dispatcher').default.RequestOptions}
*/
const request = {
origin: 'localhost',
method: 'GET',
path: '/'
}
// Send initial request, will cache the response
await client.request(request)
// Sanity check
equal(deleteCalled, false)
// Make sure the common unsafe methods cause cache purges
for (const method of ['POST', 'PUT', 'PATCH', 'DELETE']) {
deleteCalled = false
await client.request({
...request,
method
})
equal(deleteCalled, true, method)
}
})
test('unsafe methods aren\'t cached', async () => {
const server = createServer({ joinDuplicateHeaders: true }, (_, res) => {
res.setHeader('cache-control', 'public, s-maxage=1')
res.end('')
}).listen(0)
after(() => server.close())
const client = new Client(`http://localhost:${server.address().port}`)
.compose(interceptors.cache({
store: {
get () {
return undefined
},
createWriteStream (key) {
fail(key.method)
},
delete () { }
}
}))
for (const method of ['POST', 'PUT', 'PATCH', 'DELETE']) {
await client.request({
origin: 'localhost',
method,
path: '/'
})
}
})
test('necessary headers are stripped', async () => {
const headers = [
// Headers defined in the spec that we need to strip
'connection',
'proxy-authenticate',
'proxy-authentication-info',
'proxy-authorization',
'proxy-connection',
'te',
'upgrade',
// Headers we need to specifiy to be stripped
'should-be-stripped'
]
let requestToOrigin = 0
const server = createServer({ joinDuplicateHeaders: true }, (_, res) => {
requestToOrigin++
res.setHeader('cache-control', 's-maxage=10, no-cache=should-be-stripped')
res.setHeader('should-not-be-stripped', 'asd')
for (const header of headers) {
res.setHeader(header, 'asd')
}
res.end()
}).listen(0)
const client = new Client(`http://localhost:${server.address().port}`)
.compose(interceptors.cache())
after(async () => {
server.close()
await client.close()
})
await once(server, 'listening')
/**
* @type {import('../../types/dispatcher').default.RequestOptions}
*/
const request = {
origin: 'localhost',
method: 'GET',
path: '/'
}
{
const res = await client.request(request)
equal(requestToOrigin, 1)
equal(res.headers['should-not-be-stripped'], 'asd')
for (const header of headers) {
equal(res.headers[header], 'asd')
}
}
{
const res = await client.request(request)
equal(requestToOrigin, 1)
equal(res.headers['should-not-be-stripped'], 'asd')
equal(res.headers['transfer-encoding'], undefined)
for (const header of headers) {
equal(res.headers[header], undefined)
}
}
})
test('cacheByDefault', async () => {
let requestsToOrigin = 0
const server = createServer({ joinDuplicateHeaders: true }, (_, res) => {
requestsToOrigin++
res.end('asd')
}).listen(0)
after(() => server.close())
const client = new Client(`http://localhost:${server.address().port}`)
.compose(interceptors.cache({
cacheByDefault: 3600
}))
equal(requestsToOrigin, 0)
// Should hit the origin
{
const res = await client.request({
origin: 'localhost',
path: '/',
method: 'GET'
})
equal(requestsToOrigin, 1)
equal(await res.body.text(), 'asd')
}
// Should hit the cache
{
const res = await client.request({
origin: 'localhost',
path: '/',
method: 'GET'
})
equal(requestsToOrigin, 1)
equal(await res.body.text(), 'asd')
}
})
test('stale-if-error (response)', async () => {
const clock = FakeTimers.install({
shouldClearNativeTimers: true
})
let requestsToOrigin = 0
const server = createServer({ joinDuplicateHeaders: true }, (_, res) => {
res.setHeader('date', 0)
requestsToOrigin++
if (requestsToOrigin === 1) {
// First request
res.setHeader('cache-control', 'public, s-maxage=10, stale-if-error=20')
res.end('asd')
} else {
res.statusCode = 500
res.end('')
}
}).listen(0)
const client = new Client(`http://localhost:${server.address().port}`)
.compose(interceptors.cache())
after(async () => {
clock.uninstall()
server.close()
await client.close()
})
await once(server, 'listening')
strictEqual(requestsToOrigin, 0)
/**
* @type {import('../../types/dispatcher').default.RequestOptions}
*/
const request = {
origin: 'localhost',
method: 'GET',
path: '/'
}
// Send first request. This will hit the origin and succeed
{
const response = await client.request(request)
equal(requestsToOrigin, 1)
equal(response.statusCode, 200)
equal(await response.body.text(), 'asd')
}
// Send second request. It isn't stale yet, so this should be from the
// cache and succeed
{
const response = await client.request(request)
equal(requestsToOrigin, 1)
equal(response.statusCode, 200)
equal(await response.body.text(), 'asd')
}
clock.tick(15000)
// Send third request. This is now stale, the revalidation request should
// fail but the response should still be served from cache.
{
const response = await client.request(request)
equal(requestsToOrigin, 2)
equal(response.statusCode, 200)
equal(await response.body.text(), 'asd')
}
clock.tick(25000)
// Send fourth request. We're now outside the stale-if-error threshold and
// should see the error.
{
const response = await client.request(request)
equal(requestsToOrigin, 3)
equal(response.statusCode, 500)
}
})
describe('Client-side directives', () => {
test('max-age', async () => {
const clock = FakeTimers.install({
shouldClearNativeTimers: true
})
let requestsToOrigin = 0
const server = createServer({ joinDuplicateHeaders: true }, (_, res) => {
requestsToOrigin++
res.setHeader('date', 0)
res.setHeader('cache-control', 'public, s-maxage=100')
res.end()
}).listen(0)
const client = new Client(`http://localhost:${server.address().port}`)
.compose(interceptors.cache())
after(async () => {
clock.uninstall()
server.close()
await client.close()
})
await once(server, 'listening')
strictEqual(requestsToOrigin, 0)
/**
* @type {import('../../types/dispatcher').default.RequestOptions}
*/
const request = {
origin: 'localhost',
method: 'GET',
path: '/'
}
// Send first request to cache the response
await client.request(request)
equal(requestsToOrigin, 1)
// Send second request, should be served by the cache since it's within
// the window
await client.request({
...request,
headers: {
'cache-control': 'max-age=5'
}
})
equal(requestsToOrigin, 1)
clock.tick(6000)
// Send third request, should reach the origin
await client.request({
...request,
headers: {
'cache-control': 'max-age=5'
}
})
equal(requestsToOrigin, 2)
})
test('max-stale', async () => {
const clock = FakeTimers.install({
shouldClearNativeTimers: true
})
let requestsToOrigin = 0
let revalidationRequests = 0
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.setHeader('date', 0)
res.setHeader('cache-control', 'public, s-maxage=1, stale-while-revalidate=10')
if (req.headers['if-modified-since']) {
revalidationRequests++
res.statusCode = 304
res.end()
} else {
requestsToOrigin++
res.end('asd')
}
}).listen(0)
const client = new Client(`http://localhost:${server.address().port}`)
.compose(interceptors.cache())
after(async () => {
server.close()
await client.close()
clock.uninstall()
})
await once(server, 'listening')
strictEqual(requestsToOrigin, 0)
/**
* @type {import('../../types/dispatcher').default.RequestOptions}
*/
const request = {
origin: 'localhost',
method: 'GET',
path: '/'
}
await client.request(request)
equal(requestsToOrigin, 1)
equal(revalidationRequests, 0)
clock.tick(1500)
// Send second request within the max-stale threshold
await client.request({
...request,
headers: {
'cache-control': 'max-stale=5'
}
})
equal(requestsToOrigin, 1)
equal(revalidationRequests, 0)
// Send third request outside the max-stale threshold
await client.request({
...request,
headers: {
'cache-control': 'max-stale=0'
}
})
equal(requestsToOrigin, 1)
// Wait for background revalidation to complete
await sleep(100)
equal(revalidationRequests, 1)
})
test('min-fresh', async () => {
const clock = FakeTimers.install({
shouldClearNativeTimers: true
})
let requestsToOrigin = 0
const server = createServer({ joinDuplicateHeaders: true }, (_, res) => {
requestsToOrigin++
res.setHeader('date', 0)
res.setHeader('cache-control', 'public, s-maxage=10')
res.end()
}).listen(0)
const client = new Client(`http://localhost:${server.address().port}`)
.compose(interceptors.cache())
after(async () => {
server.close()
await client.close()
clock.uninstall()
})
await once(server, 'listening')
strictEqual(requestsToOrigin, 0)
/**
* @type {import('../../types/dispatcher').default.RequestOptions}
*/
const request = {
origin: 'localhost',
method: 'GET',
path: '/'
}
await client.request(request)
equal(requestsToOrigin, 1)
// Fast forward to response having 8sec ttl
clock.tick(2000)
// Send request within the threshold
await client.request({
...request,
headers: {
'cache-control': 'min-fresh=5'
}
})
equal(requestsToOrigin, 1)
// Fast forward again, response has 2sec ttl
clock.tick(6000)
await client.request({
...request,
headers: {
'cache-control': 'min-fresh=5'
}
})
equal(requestsToOrigin, 2)
})
test('no-cache', async () => {
let requestsToOrigin = 0
let revalidationRequests = 0
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
if (req.headers['if-modified-since']) {
revalidationRequests++
res.statusCode = 304
res.end()
} else {
requestsToOrigin++
res.setHeader('cache-control', 'public, s-maxage=100')
res.end('asd')
}
}).listen(0)
const client = new Client(`http://localhost:${server.address().port}`)
.compose(interceptors.cache())
after(async () => {
server.close()
await client.close()
})
await once(server, 'listening')
strictEqual(requestsToOrigin, 0)
// Send initial request. This should reach the origin
await client.request({
origin: 'localhost',
method: 'GET',
path: '/',
headers: {
'cache-control': 'no-cache'
}
})
strictEqual(requestsToOrigin, 1)
strictEqual(revalidationRequests, 0)
// Send second request, a validation request should be sent
await client.request({
origin: 'localhost',
method: 'GET',
path: '/',
headers: {
'cache-control': 'no-cache'
}
})
strictEqual(requestsToOrigin, 1)
strictEqual(revalidationRequests, 1)
// Send third request w/o no-cache, this should be handled by the cache
await client.request({
origin: 'localhost',
method: 'GET',
path: '/'
})
strictEqual(requestsToOrigin, 1)
strictEqual(revalidationRequests, 1)
})
test('no-store', async () => {
const server = createServer({ joinDuplicateHeaders: true }, (_, res) => {
res.setHeader('cache-control', 'public, s-maxage=100')
res.end('asd')
}).listen(0)
const store = new MemoryCacheStore()
store.createWriteStream = () => {
fail('shouln\'t have reached this')
}
const client = new Client(`http://localhost:${server.address().port}`)
.compose(interceptors.cache({ store }))
after(async () => {
server.close()
await client.close()
})
await once(server, 'listening')
await client.request({
origin: 'localhost',
method: 'GET',
path: '/',
headers: {
'cache-control': 'no-store'
}
})
})
test('only-if-cached', async () => {
let requestsToOrigin = 0
const server = createServer({ joinDuplicateHeaders: true }, (_, res) => {
res.setHeader('cache-control', 'public, s-maxage=100')
res.end('asd')
requestsToOrigin++
}).listen(0)
const client = new Client(`http://localhost:${server.address().port}`)
.compose(interceptors.cache())
after(async () => {
server.close()
await client.close()
})
await once(server, 'listening')
// Send initial request. This should reach the origin
await client.request({
origin: 'localhost',
method: 'GET',
path: '/'
})
equal(requestsToOrigin, 1)
// Send second request, this shouldn't reach the origin
await client.request({
origin: 'localhost',
method: 'GET',
path: '/',
headers: {
'cache-control': 'only-if-cached'
}
})
equal(requestsToOrigin, 1)
// Send third request to an uncached resource, this should return a 504
{
const res = await client.request({
origin: 'localhost',
method: 'GET',
path: '/bla',
headers: {
'cache-control': 'only-if-cached'
}
})
equal(res.statusCode, 504)
}
// Send fourth request to an uncached resource w/ a , this should return a 504
{
const res = await client.request({
origin: 'localhost',
method: 'GET',
path: '/asd123',
headers: {
'cache-control': 'only-if-cached'
}
})
equal(res.statusCode, 504)
}
})
test('stale-if-error', async () => {
const clock = FakeTimers.install({
shouldClearNativeTimers: true
})
let requestsToOrigin = 0
const server = createServer({ joinDuplicateHeaders: true }, (_, res) => {
res.setHeader('date', 0)
requestsToOrigin++
if (requestsToOrigin === 1) {
// First request, send stale-while-revalidate to keep the value in the cache
res.setHeader('cache-control', 'public, s-maxage=10, stale-while-revalidate=20')
res.end('asd')
} else {
res.statusCode = 500
res.end('')
}
}).listen(0)
const client = new Client(`http://localhost:${server.address().port}`)
.compose(interceptors.cache())
after(async () => {
clock.uninstall()
server.close()
await client.close()
})
await once(server, 'listening')
strictEqual(requestsToOrigin, 0)
// Send first request. This will hit the origin and succeed
{
const response = await client.request({
origin: 'localhost',
path: '/',
method: 'GET'
})
equal(requestsToOrigin, 1)
equal(response.statusCode, 200)
equal(await response.body.text(), 'asd')
}
// Send second request. It isn't stale yet, so this should be from the
// cache and succeed
{
const response = await client.request({
origin: 'localhost',
path: '/',
method: 'GET'
})
equal(requestsToOrigin, 1)
equal(response.statusCode, 200)
equal(await response.body.text(), 'asd')
}
clock.tick(15000)
// Send third request. This is now stale but within stale-while-revalidate,
// should return stale immediately and trigger background revalidation
{
const response = await client.request({
origin: 'localhost',
path: '/',
method: 'GET',
headers: {
'cache-control': 'stale-if-error=20'
}
})
equal(response.statusCode, 200)
equal(await response.body.text(), 'asd')
}
// Wait for background revalidation to complete (which will fail with 500)
await sleep(100)
equal(requestsToOrigin, 2)
// Send a fourth request. Still within stale-while-revalidate but without stale-if-error,
// should return stale since previous revalidation failed and stale-if-error applies
{
const response = await client.request({
origin: 'localhost',
path: '/',
method: 'GET'
})
equal(response.statusCode, 200)
equal(await response.body.text(), 'asd')
}
// Wait for another background revalidation
await sleep(100)
equal(requestsToOrigin, 3)
clock.tick(25000)
// Send fifth request. We're now outside the stale-if-error threshold and
// should see the error.
{
const response = await client.request({
origin: 'localhost',
path: '/',
method: 'GET',
headers: {
'cache-control': 'stale-if-error=20'
}
})
equal(requestsToOrigin, 4)
equal(response.statusCode, 500)
}
})
})
// Partial list.
const cacheableStatusCodes = [
{ code: 204, body: '' },
{ code: 302, body: 'Found' },
{ code: 307, body: 'Temporary Redirect' },
{ code: 404, body: 'Not Found' },
{ code: 410, body: 'Gone' }
]
for (const { code, body } of cacheableStatusCodes) {
test(`caches ${code} response with cache headers`, async () => {
let requestsToOrigin = 0
const server = createServer({ joinDuplicateHeaders: true }, (_, res) => {
requestsToOrigin++
res.statusCode = code
res.setHeader('cache-control', 'public, max-age=60')
res.end(body)
}).listen(0)
const client = new Client(`http://localhost:${server.address().port}`)
.compose(interceptors.cache())
after(async () => {
server.close()
await client.close()
})
await once(server, 'listening')
equal(requestsToOrigin, 0)
const request = {
origin: 'localhost',
method: 'GET',
path: '/'
}
// First request should hit the origin
{
const res = await client.request(request)
equal(requestsToOrigin, 1)
equal(res.statusCode, code)
strictEqual(await res.body.text(), body)
}
// Second request should be served from cache
{
const res = await client.request(request)
equal(requestsToOrigin, 1) // Should still be 1 (cached)
equal(res.statusCode, code)
strictEqual(await res.body.text(), body)
}
})
}
// Partial list.
const nonHeuristicallyCacheableStatusCodes = [
{ code: 201, body: 'Created' },
{ code: 307, body: 'Temporary Redirect' },
{ code: 418, body: 'I am a teapot' }
]
for (const { code, body } of nonHeuristicallyCacheableStatusCodes) {
test(`does not cache non-heuristically cacheable status ${code} without explicit directive`, async () => {
let requestsToOrigin = 0
const server = createServer({ joinDuplicateHeaders: true }, (_, res) => {
requestsToOrigin++
res.statusCode = code
// By default the response may have a date and last-modified header set to 'now',
// causing the cache to compute a 0 heuristic expiry, causing the test to not ascertain
// it is really not cached.
res.setHeader('date', '')
res.end(body)
}).listen(0)
const client = new Client(`http://localhost:${server.address().port}`)
.compose(interceptors.cache({ cacheByDefault: 60 }))
after(async () => {
server.close()
await client.close()
})
await once(server, 'listening')
equal(requestsToOrigin, 0)
const request = {
origin: 'localhost',
method: 'GET',
path: '/'
}
// First request should hit the origin
{
const res = await client.request(request)
equal(requestsToOrigin, 1)
equal(res.statusCode, code)
strictEqual(await res.body.text(), body)
}
// Second request should also hit the origin (not cached)
{
const res = await client.request(request)
equal(requestsToOrigin, 2) // Should be 2 (not cached)
equal(res.statusCode, code)
strictEqual(await res.body.text(), body)
}
})
}
test('discriminates caching of range requests, or does not cache them', async () => {
let requestsToOrigin = 0
const body = 'Fake range request response'
const code = 206
const server = createServer({ joinDuplicateHeaders: true }, (_, res) => {
requestsToOrigin++
res.statusCode = code
res.setHeader('cache-control', 'public, max-age=60')
res.end(body)
}).listen(0)
const client = new Client(`http://localhost:${server.address().port}`)
.compose(interceptors.cache())
after(async () => {
server.close()
await client.close()
})
await once(server, 'listening')
equal(requestsToOrigin, 0)
const request = {
origin: 'localhost',
method: 'GET',
path: '/',
headers: {
range: 'bytes=10-'
}
}
// First request should hit the origin
{
const res = await client.request(request)
equal(requestsToOrigin, 1)
equal(res.statusCode, code)
strictEqual(await res.body.text(), body)
}
// Second request with different range should hit the origin too
request.headers.range = 'bytes=5-'
{
const res = await client.request(request)
equal(requestsToOrigin, 2)
equal(res.statusCode, code)
strictEqual(await res.body.text(), body)
}
})
test('discriminates caching of conditional requests (if-none-match), or does not cache them', async () => {
let requestsToOrigin = 0
const body = ''
const code = 304
const server = createServer({ joinDuplicateHeaders: true }, (_, res) => {
requestsToOrigin++
res.statusCode = code
res.setHeader('cache-control', 'public, max-age=60')
res.end(body)
}).listen(0)
const client = new Client(`http://localhost:${server.address().port}`)
.compose(interceptors.cache())
after(async () => {
server.close()
await client.close()
})
await once(server, 'listening')
equal(requestsToOrigin, 0)
const request = {
origin: 'localhost',
method: 'GET',
path: '/',
headers: {
'if-none-match': 'some-etag'
}
}
// First request should hit the origin
{
const res = await client.request(request)
equal(requestsToOrigin, 1)
equal(res.statusCode, code)
strictEqual(await res.body.text(), body)
}
// Second request with different etag should hit the origin too
request.headers['if-none-match'] = 'another-etag'
{
const res = await client.request(request)
equal(requestsToOrigin, 2)
equal(res.statusCode, code)
strictEqual(await res.body.text(), body)
}
})
test('discriminates caching of conditional requests (if-modified-since), or does not cache them', async () => {
let requestsToOrigin = 0
const body = ''
const code = 304
const server = createServer({ joinDuplicateHeaders: true }, (_, res) => {
requestsToOrigin++
res.statusCode = code
res.setHeader('cache-control', 'public, max-age=60')
res.end(body)
}).listen(0)
const client = new Client(`http://localhost:${server.address().port}`)
.compose(interceptors.cache())
after(async () => {
server.close()
await client.close()
})
await once(server, 'listening')
equal(requestsToOrigin, 0)
const request = {
origin: 'localhost',
method: 'GET',
path: '/',
headers: {
'if-modified-since': new Date().toUTCString()
}
}
// First request should hit the origin
{
const res = await client.request(request)
equal(requestsToOrigin, 1)
equal(res.statusCode, code)
strictEqual(await res.body.text(), body)
}
// Second request with different since should hit the origin too
request.headers['if-modified-since'] = new Date(0).toUTCString()
{
const res = await client.request(request)
equal(requestsToOrigin, 2)
equal(res.statusCode, code)
strictEqual(await res.body.text(), body)
}
})
test('stale-while-revalidate returns stale immediately and revalidates in background (RFC 5861)', async () => {
let requestsToOrigin = 0
let revalidationRequests = 0
let serverResponse = 'original-response'
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
const responseDate = new Date()
res.setHeader('date', responseDate.toUTCString())
res.setHeader('cache-control', 's-maxage=1, stale-while-revalidate=10')
if (req.headers['if-modified-since']) {
revalidationRequests++
// Return updated content on revalidation
serverResponse = 'revalidated-response'
res.end(serverResponse)
} else {
requestsToOrigin++
res.end(serverResponse)
}
}).listen(0)
const client = new Client(`http://localhost:${server.address().port}`)
.compose(interceptors.cache())
after(async () => {
server.close()
await client.close()
})
await once(server, 'listening')
const request = {
origin: 'localhost',
method: 'GET',
path: '/'
}
// Send initial request to cache the response
{
const res = await client.request(request)
equal(requestsToOrigin, 1)
strictEqual(await res.body.text(), 'original-response')
}
// Wait for response to become stale
await sleep(1100)
// Request stale content - should return immediately with stale content
const startTime = Date.now()
{
const res = await client.request(request)
const responseTime = Date.now() - startTime
// Should return stale content immediately (< 50ms)
equal(res.statusCode, 200)
strictEqual(await res.body.text(), 'original-response')
equal(requestsToOrigin, 1) // No additional origin requests yet
// Response should be immediate (RFC 5861 requirement)
if (responseTime > 100) {
fail(`stale-while-revalidate response took ${responseTime}ms, should be < 100ms`)
}
}
// Wait for background revalidation to complete
await sleep(500)
// Verify that revalidation occurred in background
equal(revalidationRequests, 1, 'Background revalidation should have occurred')
})
test('stale-while-revalidate updates cache after background revalidation (receiving new data)', async () => {
let requestsToOrigin = 0
let revalidationRequests = 0
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
const responseDate = new Date()
res.setHeader('date', responseDate.toUTCString())
res.setHeader('cache-control', 's-maxage=1, stale-while-revalidate=10')
if (req.headers['if-modified-since']) {
revalidationRequests++
// Return updated content
res.end('updated-response')
} else {
requestsToOrigin++
res.end('original-response')
}
}).listen(0)
const client = new Client(`http://localhost:${server.address().port}`)
.compose(interceptors.cache())
after(async () => {
server.close()
await client.close()
})
await once(server, 'listening')
const request = {
origin: 'localhost',
method: 'GET',
path: '/'
}
// Initial request
{
const res = await client.request(request)
equal(requestsToOrigin, 1)
strictEqual(await res.body.text(), 'original-response')
}
// Wait for staleness
await sleep(1100)
// First stale request - gets stale content immediately
{
const res = await client.request(request)
strictEqual(await res.body.text(), 'original-response')
}
// Wait for background revalidation
await sleep(500)
equal(revalidationRequests, 1)
// Second stale request - should get updated content from cache
// (still within stale-while-revalidate window)
{
const res = await client.request(request)
strictEqual(await res.body.text(), 'updated-response')
equal(requestsToOrigin, 1) // Still only one origin request
}
})
describe('origins option', () => {
test('caches request when origin matches string in whitelist', async () => {
let requestsToOrigin = 0
const server = createServer({ joinDuplicateHeaders: true }, (_, res) => {
requestsToOrigin++
res.setHeader('cache-control', 's-maxage=10')
res.end('cached')
}).listen(0)
after(() => server.close())
await once(server, 'listening')
const client = new Client(`http://localhost:${server.address().port}`)
.compose(interceptors.cache({
origins: ['localhost']
}))
after(() => client.close())
const request = {
origin: 'localhost',
method: 'GET',
path: '/'
}
// First request should hit origin
{
const res = await client.request(request)
equal(requestsToOrigin, 1)
strictEqual(await res.body.text(), 'cached')
}
// Second request should be served from cache
{
const res = await client.request(request)
equal(requestsToOrigin, 1)
strictEqual(await res.body.text(), 'cached')
}
})
test('skips caching when origin does not match string in whitelist', async () => {
let requestsToOrigin = 0
const server = createServer({ joinDuplicateHeaders: true }, (_, res) => {
requestsToOrigin++
res.setHeader('cache-control', 's-maxage=10')
res.end('not cached')
}).listen(0)
after(() => server.close())
await once(server, 'listening')
const client = new Client(`http://localhost:${server.address().port}`)
.compose(interceptors.cache({
origins: ['http://example.com']
}))
after(() => client.close())
const request = {
origin: 'localhost',
method: 'GET',
path: '/'
}
// First request should hit origin
{
const res = await client.request(request)
equal(requestsToOrigin, 1)
strictEqual(await res.body.text(), 'not cached')
}
// Second request should also hit origin (not cached)
{
const res = await client.request(request)
equal(requestsToOrigin, 2)
strictEqual(await res.body.text(), 'not cached')
}
})
test('caches request when origin matches RegExp in whitelist', async () => {
let requestsToOrigin = 0
const server = createServer({ joinDuplicateHeaders: true }, (_, res) => {
requestsToOrigin++
res.setHeader('cache-control', 's-maxage=10')
res.end('cached')
}).listen(0)
after(() => server.close())
await once(server, 'listening')
const client = new Client(`http://localhost:${server.address().port}`)
.compose(interceptors.cache({
origins: [/localhost/]
}))
after(() => client.close())
const request = {
origin: 'localhost',
method: 'GET',
path: '/'
}
// First request should hit origin
{
const res = await client.request(request)
equal(requestsToOrigin, 1)
strictEqual(await res.body.text(), 'cached')
}
// Second request should be served from cache
{
const res = await client.request(request)
equal(requestsToOrigin, 1)
strictEqual(await res.body.text(), 'cached')
}
})
test('skips caching when origin does not match RegExp in whitelist', async () => {
let requestsToOrigin = 0
const server = createServer({ joinDuplicateHeaders: true }, (_, res) => {
requestsToOrigin++
res.setHeader('cache-control', 's-maxage=10')
res.end('not cached')
}).listen(0)
after(() => server.close())
await once(server, 'listening')
const client = new Client(`http://localhost:${server.address().port}`)
.compose(interceptors.cache({
origins: [/example\.com/]
}))
after(() => client.close())
const request = {
origin: 'localhost',
method: 'GET',
path: '/'
}
// First request should hit origin
{
const res = await client.request(request)
equal(requestsToOrigin, 1)
strictEqual(await res.body.text(), 'not cached')
}
// Second request should also hit origin (not cached)
{
const res = await client.request(request)
equal(requestsToOrigin, 2)
strictEqual(await res.body.text(), 'not cached')
}
})
test('caches request when origin matches any entry in mixed array', async () => {
let requestsToOrigin = 0
const server = createServer({ joinDuplicateHeaders: true }, (_, res) => {
requestsToOrigin++
res.setHeader('cache-control', 's-maxage=10')
res.end('cached')
}).listen(0)
after(() => server.close())
await once(server, 'listening')
const client = new Client(`http://localhost:${server.address().port}`)
.compose(interceptors.cache({
origins: ['http://other.com', /localhost/]
}))
after(() => client.close())
const request = {
origin: 'localhost',
method: 'GET',
path: '/'
}
// First request should hit origin
{
const res = await client.request(request)
equal(requestsToOrigin, 1)
strictEqual(await res.body.text(), 'cached')
}
// Second request should be served from cache (matches RegExp)
{
const res = await client.request(request)
equal(requestsToOrigin, 1)
strictEqual(await res.body.text(), 'cached')
}
})
test('caches all origins when origins option is undefined (default behavior)', async () => {
let requestsToOrigin = 0
const server = createServer({ joinDuplicateHeaders: true }, (_, res) => {
requestsToOrigin++
res.setHeader('cache-control', 's-maxage=10')
res.end('cached')
}).listen(0)
after(() => server.close())
await once(server, 'listening')
const client = new Client(`http://localhost:${server.address().port}`)
.compose(interceptors.cache())
after(() => client.close())
const request = {
origin: 'localhost',
method: 'GET',
path: '/'
}
// First request should hit origin
{
const res = await client.request(request)
equal(requestsToOrigin, 1)
strictEqual(await res.body.text(), 'cached')
}
// Second request should be served from cache
{
const res = await client.request(request)
equal(requestsToOrigin, 1)
strictEqual(await res.body.text(), 'cached')
}
})
test('caches nothing when origins is empty array', async () => {
let requestsToOrigin = 0
const server = createServer({ joinDuplicateHeaders: true }, (_, res) => {
requestsToOrigin++
res.setHeader('cache-control', 's-maxage=10')
res.end('not cached')
}).listen(0)
after(() => server.close())
await once(server, 'listening')
const client = new Client(`http://localhost:${server.address().port}`)
.compose(interceptors.cache({
origins: []
}))
after(() => client.close())
const request = {
origin: 'localhost',
method: 'GET',
path: '/'
}
// First request should hit origin
{
const res = await client.request(request)
equal(requestsToOrigin, 1)
strictEqual(await res.body.text(), 'not cached')
}
// Second request should also hit origin (not cached)
{
const res = await client.request(request)
equal(requestsToOrigin, 2)
strictEqual(await res.body.text(), 'not cached')
}
})
test('throws TypeError when origins is not an array', async () => {
const { throws } = require('node:assert')
throws(
() => interceptors.cache({ origins: 'http://example.com' }),
{
name: 'TypeError',
message: /expected opts\.origins to be an array or undefined/i
}
)
})
test('throws TypeError when origins array contains invalid type', async () => {
const { throws } = require('node:assert')
throws(
() => interceptors.cache({ origins: [123] }),
{
name: 'TypeError',
message: /expected opts\.origins\[0\] to be a string or RegExp/i
}
)
})
test('string matching is case insensitive', async () => {
let requestsToOrigin = 0
const server = createServer({ joinDuplicateHeaders: true }, (_, res) => {
requestsToOrigin++
res.setHeader('cache-control', 's-maxage=10')
res.end('cached')
}).listen(0)
after(() => server.close())
await once(server, 'listening')
const client = new Client(`http://localhost:${server.address().port}`)
.compose(interceptors.cache({
origins: ['LOCALHOST']
}))
after(() => client.close())
const request = {
origin: 'localhost',
method: 'GET',
path: '/'
}
// First request should hit origin
{
const res = await client.request(request)
equal(requestsToOrigin, 1)
strictEqual(await res.body.text(), 'cached')
}
// Second request should be served from cache (case insensitive match)
{
const res = await client.request(request)
equal(requestsToOrigin, 1)
strictEqual(await res.body.text(), 'cached')
}
})
test('different hosts are treated as different origins', async () => {
let requestsToOrigin = 0
const server = createServer({ joinDuplicateHeaders: true }, (_, res) => {
requestsToOrigin++
res.setHeader('cache-control', 's-maxage=10')
res.end('not cached')
}).listen(0)
after(() => server.close())
await once(server, 'listening')
const client = new Client(`http://localhost:${server.address().port}`)
.compose(interceptors.cache({
origins: ['example.com']
}))
after(() => client.close())
const request = {
origin: 'localhost',
method: 'GET',
path: '/'
}
// First request should hit origin
{
const res = await client.request(request)
equal(requestsToOrigin, 1)
strictEqual(await res.body.text(), 'not cached')
}
// Second request should also hit origin (different host = different origin)
{
const res = await client.request(request)
equal(requestsToOrigin, 2)
strictEqual(await res.body.text(), 'not cached')
}
})
})
describe('determineDeleteAt', () => {
test('max-age response has deleteAt proportional to freshness lifetime, not 1 year', async () => {
const clock = FakeTimers.install({ now: 1000 })
after(() => clock.uninstall())
const store = new MemoryCacheStore()
const server = createServer({ joinDuplicateHeaders: true }, (_, res) => {
res.setHeader('cache-control', 'public, max-age=60')
res.setHeader('date', new Date(clock.now).toUTCString())
res.sendDate = false
res.end('short-lived')
}).listen(0)
after(async () => {
server.close()
await client.close()
})
await once(server, 'listening')
const origin = `http://localhost:${server.address().port}`
const client = new Client(origin)
.compose(interceptors.cache({ store }))
const res = await client.request({ origin, method: 'GET', path: '/delete-at-maxage' })
strictEqual(await res.body.text(), 'short-lived')
const cached = store.get(makeCacheKey({ origin, method: 'GET', path: '/delete-at-maxage', headers: {} }))
notEqual(cached, undefined)
// deleteAt should be approximately 2x max-age (staleAt + freshnessLifetime),
// not 1 year out
const maxExpected = clock.now + (60 * 1000 * 3) // generous upper bound
equal(cached.deleteAt < maxExpected, true, `deleteAt (${cached.deleteAt}) should be well under 3x max-age (${maxExpected})`)
equal(cached.deleteAt > cached.staleAt, true, 'deleteAt should be greater than staleAt to allow revalidation')
})
test('immutable response has deleteAt of ~1 year', async () => {
const clock = FakeTimers.install({ now: 1000 })
after(() => clock.uninstall())
const store = new MemoryCacheStore()
const server = createServer({ joinDuplicateHeaders: true }, (_, res) => {
res.setHeader('cache-control', 'public, immutable')
res.setHeader('date', new Date(clock.now).toUTCString())
res.sendDate = false
res.end('immutable-content')
}).listen(0)
after(async () => {
server.close()
await client.close()
})
await once(server, 'listening')
const origin = `http://localhost:${server.address().port}`
const client = new Client(origin)
.compose(interceptors.cache({ store }))
const res = await client.request({ origin, method: 'GET', path: '/delete-at-immutable' })
strictEqual(await res.body.text(), 'immutable-content')
const cached = store.get(makeCacheKey({ origin, method: 'GET', path: '/delete-at-immutable', headers: {} }))
notEqual(cached, undefined)
const oneYear = 31536000000
// deleteAt should be approximately 1 year out
equal(cached.deleteAt >= clock.now + oneYear - 1000, true, `deleteAt (${cached.deleteAt}) should be ~1 year out`)
})
test('stale-while-revalidate extends deleteAt beyond staleAt', async () => {
const clock = FakeTimers.install({ now: 1000 })
after(() => clock.uninstall())
const store = new MemoryCacheStore()
const server = createServer({ joinDuplicateHeaders: true }, (_, res) => {
res.setHeader('cache-control', 'public, max-age=60, stale-while-revalidate=300')
res.setHeader('date', new Date(clock.now).toUTCString())
res.sendDate = false
res.end('swr-content')
}).listen(0)
after(async () => {
server.close()
await client.close()
})
await once(server, 'listening')
const origin = `http://localhost:${server.address().port}`
const client = new Client(origin)
.compose(interceptors.cache({ store }))
const res = await client.request({ origin, method: 'GET', path: '/delete-at-swr' })
strictEqual(await res.body.text(), 'swr-content')
const cached = store.get(makeCacheKey({ origin, method: 'GET', path: '/delete-at-swr', headers: {} }))
notEqual(cached, undefined)
// deleteAt should be staleAt + stale-while-revalidate (300s)
const expectedDeleteAt = cached.staleAt + (300 * 1000)
equal(cached.deleteAt, expectedDeleteAt, `deleteAt (${cached.deleteAt}) should be staleAt + 300s (${expectedDeleteAt})`)
})
})
})
================================================
FILE: test/interceptors/decompress.js
================================================
'use strict'
const { test, after } = require('node:test')
const { createServer } = require('node:http')
const { once } = require('node:events')
const { createGzip, createDeflate, createBrotliCompress, createZstdCompress } = require('node:zlib')
const { tspl } = require('@matteo.collina/tspl')
const { Client, getGlobalDispatcher, setGlobalDispatcher, request } = require('../..')
const createDecompressInterceptor = require('../../lib/interceptor/decompress')
test('should decompress gzip response', async t => {
t = tspl(t, { plan: 3 })
const data = 'This is a test message for gzip compression validation.'
const server = createServer({ joinDuplicateHeaders: true }, async (req, res) => {
const gzip = createGzip()
res.writeHead(200, {
'Content-Type': 'text/plain',
'Content-Encoding': 'gzip'
})
gzip.pipe(res)
gzip.end(data)
})
server.listen(0)
await once(server, 'listening')
const client = new Client(
`http://localhost:${server.address().port}`
).compose(createDecompressInterceptor())
after(async () => {
await client.close()
server.close()
await once(server, 'close')
})
const response = await client.request({
method: 'GET',
path: '/'
})
const body = await response.body.text()
t.equal(response.statusCode, 200)
t.equal(response.headers['content-encoding'], undefined)
t.equal(body, data)
await t.completed
})
test('should decompress deflate response', async t => {
t = tspl(t, { plan: 3 })
const server = createServer({ joinDuplicateHeaders: true }, async (req, res) => {
const deflate = createDeflate()
res.writeHead(200, {
'Content-Type': 'text/plain',
'Content-Encoding': 'deflate'
})
const data = 'This message is compressed with deflate algorithm!'
deflate.pipe(res)
deflate.end(data)
})
server.listen(0)
await once(server, 'listening')
const client = new Client(
`http://localhost:${server.address().port}`
).compose(createDecompressInterceptor())
after(async () => {
await client.close()
server.close()
await once(server, 'close')
})
const response = await client.request({
method: 'GET',
path: '/'
})
const body = await response.body.text()
t.equal(response.statusCode, 200)
t.equal(response.headers['content-encoding'], undefined)
t.equal(body, 'This message is compressed with deflate algorithm!')
await t.completed
})
test('should decompress brotli response', async t => {
t = tspl(t, { plan: 3 })
const server = createServer({ joinDuplicateHeaders: true }, async (req, res) => {
const brotli = createBrotliCompress()
res.writeHead(200, {
'Content-Type': 'text/plain',
'Content-Encoding': 'br'
})
const data = 'This message is compressed with brotli compression!'
brotli.pipe(res)
brotli.end(data)
})
server.listen(0)
await once(server, 'listening')
const client = new Client(
`http://localhost:${server.address().port}`
).compose(createDecompressInterceptor())
after(async () => {
await client.close()
server.close()
await once(server, 'close')
})
const response = await client.request({
method: 'GET',
path: '/'
})
const body = await response.body.text()
t.equal(response.statusCode, 200)
t.equal(response.headers['content-encoding'], undefined)
t.equal(body, 'This message is compressed with brotli compression!')
await t.completed
})
test('should decompress zstd response', { skip: typeof createZstdCompress !== 'function' }, async t => {
t = tspl(t, { plan: 3 })
const server = createServer({ joinDuplicateHeaders: true }, async (req, res) => {
const zstd = createZstdCompress()
res.writeHead(200, {
'Content-Type': 'text/plain',
'Content-Encoding': 'zstd'
})
const data = 'This message is compressed with zstd compression!'
zstd.pipe(res)
zstd.end(data)
})
server.listen(0)
await once(server, 'listening')
const client = new Client(
`http://localhost:${server.address().port}`
).compose(createDecompressInterceptor())
after(async () => {
await client.close()
server.close()
await once(server, 'close')
})
const response = await client.request({
method: 'GET',
path: '/'
})
const body = await response.body.text()
t.equal(response.statusCode, 200)
t.equal(response.headers['content-encoding'], undefined)
t.equal(body, 'This message is compressed with zstd compression!')
await t.completed
})
test('should pass through uncompressed response', async t => {
t = tspl(t, { plan: 3 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.writeHead(200, {
'Content-Type': 'text/plain'
})
res.end('This is uncompressed data')
})
server.listen(0)
await once(server, 'listening')
const client = new Client(
`http://localhost:${server.address().port}`
).compose(createDecompressInterceptor())
after(async () => {
await client.close()
server.close()
await once(server, 'close')
})
const response = await client.request({
method: 'GET',
path: '/'
})
const body = await response.body.text()
t.equal(response.statusCode, 200)
t.equal(response.headers['content-type'], 'text/plain')
t.equal(body, 'This is uncompressed data')
await t.completed
})
test('should pass through unsupported encoding', async t => {
t = tspl(t, { plan: 3 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.writeHead(200, {
'Content-Type': 'text/plain',
'Content-Encoding': 'unsupported'
})
res.end('This has unsupported encoding')
})
server.listen(0)
await once(server, 'listening')
const client = new Client(
`http://localhost:${server.address().port}`
).compose(createDecompressInterceptor())
after(async () => {
await client.close()
server.close()
await once(server, 'close')
})
const response = await client.request({
method: 'GET',
path: '/'
})
const body = await response.body.text()
t.equal(response.statusCode, 200)
t.equal(response.headers['content-encoding'], 'unsupported')
t.equal(body, 'This has unsupported encoding')
await t.completed
})
test('should pass through error responses (4xx, 5xx)', async t => {
t = tspl(t, { plan: 3 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
const gzip = createGzip()
res.writeHead(404, {
'Content-Type': 'text/plain',
'Content-Encoding': 'gzip'
})
const data = 'Not found error message'
gzip.pipe(res)
gzip.end(data)
})
server.listen(0)
await once(server, 'listening')
const client = new Client(
`http://localhost:${server.address().port}`
).compose(createDecompressInterceptor())
after(async () => {
await client.close()
server.close()
await once(server, 'close')
})
const response = await client.request({
method: 'GET',
path: '/'
})
const body = await response.body.text()
t.equal(response.statusCode, 404)
t.equal(response.headers['content-encoding'], 'gzip')
t.notEqual(body, 'Not found error message')
await t.completed
})
test('should pass through 204 No Content responses', async t => {
t = tspl(t, { plan: 2 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.writeHead(204, {
'Content-Encoding': 'gzip'
})
res.end()
})
server.listen(0)
await once(server, 'listening')
const client = new Client(
`http://localhost:${server.address().port}`
).compose(createDecompressInterceptor())
after(async () => {
await client.close()
server.close()
await once(server, 'close')
})
const response = await client.request({
method: 'GET',
path: '/'
})
t.equal(response.statusCode, 204)
t.equal(response.headers['content-encoding'], 'gzip')
await t.completed
})
test('should pass through 304 Not Modified responses', async t => {
t = tspl(t, { plan: 2 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.writeHead(304, {
'Content-Encoding': 'gzip'
})
res.end()
})
server.listen(0)
await once(server, 'listening')
const client = new Client(
`http://localhost:${server.address().port}`
).compose(createDecompressInterceptor())
after(async () => {
await client.close()
server.close()
await once(server, 'close')
})
const response = await client.request({
method: 'GET',
path: '/'
})
t.equal(response.statusCode, 304)
t.equal(response.headers['content-encoding'], 'gzip')
await t.completed
})
test('should handle large compressed responses', async t => {
t = tspl(t, { plan: 3 })
const server = createServer({ joinDuplicateHeaders: true }, async (req, res) => {
const gzip = createGzip()
res.writeHead(200, {
'Content-Type': 'text/plain',
'Content-Encoding': 'gzip'
})
const largeData = 'A'.repeat(10000) + 'B'.repeat(10000) + 'C'.repeat(10000)
gzip.pipe(res)
gzip.end(largeData)
})
server.listen(0)
await once(server, 'listening')
const client = new Client(
`http://localhost:${server.address().port}`
).compose(createDecompressInterceptor())
after(async () => {
await client.close()
server.close()
await once(server, 'close')
})
const response = await client.request({
method: 'GET',
path: '/'
})
const body = await response.body.text()
t.equal(response.statusCode, 200)
t.equal(response.headers['content-encoding'], undefined)
t.equal(body.length, 30000)
await t.completed
})
test('should handle case-insensitive content-encoding', async t => {
t = tspl(t, { plan: 3 })
const server = createServer({ joinDuplicateHeaders: true }, async (req, res) => {
const gzip = createGzip()
res.writeHead(200, {
'Content-Type': 'text/plain',
'Content-Encoding': 'GZIP' // Uppercase
})
const data = 'Case insensitive test'
gzip.pipe(res)
gzip.end(data)
})
server.listen(0)
await once(server, 'listening')
const client = new Client(
`http://localhost:${server.address().port}`
).compose(createDecompressInterceptor())
after(async () => {
await client.close()
server.close()
await once(server, 'close')
})
const response = await client.request({
method: 'GET',
path: '/'
})
const body = await response.body.text()
t.equal(response.statusCode, 200)
t.equal(response.headers['content-encoding'], undefined)
t.equal(body, 'Case insensitive test')
await t.completed
})
test('should remove content-length header when decompressing', async t => {
t = tspl(t, { plan: 3 })
const server = createServer({ joinDuplicateHeaders: true }, async (req, res) => {
const gzip = createGzip()
res.writeHead(200, {
'Content-Type': 'text/plain',
'Content-Encoding': 'gzip'
})
const data = 'Test data'
gzip.pipe(res)
gzip.end(data)
})
server.listen(0)
await once(server, 'listening')
const client = new Client(
`http://localhost:${server.address().port}`
).compose(createDecompressInterceptor())
after(async () => {
await client.close()
server.close()
await once(server, 'close')
})
const response = await client.request({
method: 'GET',
path: '/'
})
const body = await response.body.text()
t.equal(response.statusCode, 200)
t.equal(response.headers['content-length'], undefined)
t.equal(body, 'Test data')
await t.completed
})
test('should allow decompressing 5xx responses when skipErrorResponses is false', async t => {
t = tspl(t, { plan: 3 })
const server = createServer({ joinDuplicateHeaders: true }, async (req, res) => {
const gzip = createGzip()
res.writeHead(500, {
'Content-Type': 'text/plain',
'Content-Encoding': 'gzip'
})
const data = 'Internal server error message'
gzip.pipe(res)
gzip.end(data)
})
server.listen(0)
await once(server, 'listening')
const client = new Client(
`http://localhost:${server.address().port}`
).compose(createDecompressInterceptor({ skipErrorResponses: false }))
after(async () => {
await client.close()
server.close()
await once(server, 'close')
})
const response = await client.request({
method: 'GET',
path: '/'
})
const body = await response.body.text()
t.equal(response.statusCode, 500)
t.equal(response.headers['content-encoding'], undefined) // Should be removed when decompressing
t.equal(body, 'Internal server error message') // Should be decompressed
await t.completed
})
test('should allow custom skipStatusCodes', async t => {
t = tspl(t, { plan: 3 })
const server = createServer({ joinDuplicateHeaders: true }, async (req, res) => {
const gzip = createGzip()
res.writeHead(201, {
'Content-Type': 'text/plain',
'Content-Encoding': 'gzip'
})
const data = 'Created response'
gzip.pipe(res)
gzip.end(data)
})
server.listen(0)
await once(server, 'listening')
// Skip decompression for 201 status codes
const client = new Client(
`http://localhost:${server.address().port}`
).compose(createDecompressInterceptor({ skipStatusCodes: [201, 204, 304] }))
after(async () => {
await client.close()
server.close()
await once(server, 'close')
})
const response = await client.request({
method: 'GET',
path: '/'
})
const body = await response.body.text()
t.equal(response.statusCode, 201)
t.equal(response.headers['content-encoding'], 'gzip') // Should be preserved when skipping
t.notEqual(body, 'Created response') // Should still be compressed
await t.completed
})
test('should decompress multiple encodings in correct order', async t => {
t = tspl(t, { plan: 3 })
const server = createServer({ joinDuplicateHeaders: true }, async (req, res) => {
// First compress with gzip, then with deflate (gzip, deflate)
const gzip = createGzip()
const deflate = createDeflate()
res.writeHead(200, {
'Content-Type': 'text/plain',
'Content-Encoding': 'gzip, deflate' // Applied in this order
})
const data = 'Multiple encoding test message'
// Pipe: data → gzip → deflate → response
gzip.pipe(deflate)
deflate.pipe(res)
gzip.end(data)
})
server.listen(0)
await once(server, 'listening')
const client = new Client(
`http://localhost:${server.address().port}`
).compose(createDecompressInterceptor())
after(async () => {
await client.close()
server.close()
await once(server, 'close')
})
const response = await client.request({
method: 'GET',
path: '/'
})
const body = await response.body.text()
t.equal(response.statusCode, 200)
t.equal(response.headers['content-encoding'], undefined) // Should be removed
t.equal(body, 'Multiple encoding test message') // Should be fully decompressed
await t.completed
})
test('should handle legacy encoding names (x-gzip)', async t => {
t = tspl(t, { plan: 3 })
const server = createServer({ joinDuplicateHeaders: true }, async (req, res) => {
const gzip = createGzip()
res.writeHead(200, {
'Content-Type': 'text/plain',
'Content-Encoding': 'x-gzip' // Legacy name
})
const data = 'Legacy encoding test'
gzip.pipe(res)
gzip.end(data)
})
server.listen(0)
await once(server, 'listening')
const client = new Client(
`http://localhost:${server.address().port}`
).compose(createDecompressInterceptor())
after(async () => {
await client.close()
server.close()
await once(server, 'close')
})
const response = await client.request({
method: 'GET',
path: '/'
})
const body = await response.body.text()
t.equal(response.statusCode, 200)
t.equal(response.headers['content-encoding'], undefined) // Should be removed
t.equal(body, 'Legacy encoding test')
await t.completed
})
test('should pass through responses with unsupported encoding in chain', async t => {
t = tspl(t, { plan: 3 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.writeHead(200, {
'Content-Type': 'text/plain',
'Content-Encoding': 'gzip, unsupported, deflate' // Contains unsupported encoding
})
res.end('This should pass through unchanged')
})
server.listen(0)
await once(server, 'listening')
const client = new Client(
`http://localhost:${server.address().port}`
).compose(createDecompressInterceptor())
after(async () => {
await client.close()
server.close()
await once(server, 'close')
})
const response = await client.request({
method: 'GET',
path: '/'
})
const body = await response.body.text()
t.equal(response.statusCode, 200)
t.equal(response.headers['content-encoding'], 'gzip, unsupported, deflate') // Should be preserved
t.equal(body, 'This should pass through unchanged')
await t.completed
})
test('should handle empty encoding values', async t => {
t = tspl(t, { plan: 3 })
const server = createServer({ joinDuplicateHeaders: true }, async (req, res) => {
const gzip = createGzip()
res.writeHead(200, {
'Content-Type': 'text/plain',
'Content-Encoding': 'gzip, ' // Contains empty value at end
})
const data = 'Empty encoding value test'
gzip.pipe(res)
gzip.end(data)
})
server.listen(0)
await once(server, 'listening')
const client = new Client(
`http://localhost:${server.address().port}`
).compose(createDecompressInterceptor())
after(async () => {
await client.close()
server.close()
await once(server, 'close')
})
const response = await client.request({
method: 'GET',
path: '/'
})
const body = await response.body.text()
t.equal(response.statusCode, 200)
t.equal(response.headers['content-encoding'], undefined)
t.equal(body, 'Empty encoding value test')
await t.completed
})
test('should handle multiple pause/resume cycles during decompression', async t => {
t = tspl(t, { plan: 3 })
const data = 'Large data chunk for testing multiple pause/resume cycles. '.repeat(1000)
const server = createServer({ joinDuplicateHeaders: true }, async (req, res) => {
const gzip = createGzip()
res.writeHead(200, {
'Content-Type': 'text/plain',
'Content-Encoding': 'gzip'
})
gzip.pipe(res)
gzip.end(data)
})
server.listen(0)
await once(server, 'listening')
const client = new Client(
`http://localhost:${server.address().port}`
).compose(createDecompressInterceptor())
after(async () => {
await client.close()
server.close()
await once(server, 'close')
})
let controller
let callCount = 0
let responseData = ''
const handler = {
onRequestStart (ctrl) {
controller = ctrl
},
onResponseStart (ctrl, statusCode, headers, statusMessage) {
t.equal(statusCode, 200)
for (let i = 0; i < 3; i++) {
callCount++
controller.pause()
controller.resume()
}
},
onResponseData (ctrl, chunk) {
responseData += chunk.toString()
},
onResponseEnd (ctrl, trailers) {
t.equal(callCount, 3, 'Should have called pause/resume 3 times')
t.equal(responseData, data, 'All data should be received')
},
onResponseError (ctrl, err) {
t.fail(err)
}
}
await client.dispatch({
method: 'GET',
path: '/'
}, handler)
await t.completed
})
test('should handle controller pause with chained decompression', async t => {
t = tspl(t, { plan: 3 })
const data = 'Test data for chained decompression pause/resume functionality'
const server = createServer({ joinDuplicateHeaders: true }, async (req, res) => {
const gzip = createGzip()
const deflate = createDeflate()
res.writeHead(200, {
'Content-Type': 'text/plain',
'Content-Encoding': 'gzip, deflate'
})
gzip.pipe(deflate)
deflate.pipe(res)
gzip.end(data)
})
server.listen(0)
await once(server, 'listening')
const client = new Client(
`http://localhost:${server.address().port}`
).compose(createDecompressInterceptor())
after(async () => {
await client.close()
server.close()
await once(server, 'close')
})
let controller
let pauseResumeWorked = false
let responseData = ''
const handler = {
onRequestStart (ctrl) {
controller = ctrl
},
onResponseStart (ctrl, statusCode, headers, statusMessage) {
t.equal(statusCode, 200)
try {
controller.pause()
controller.resume()
pauseResumeWorked = true
} catch (err) {
t.fail('Pause/resume should not throw error')
}
},
onResponseData (ctrl, chunk) {
responseData += chunk.toString()
},
onResponseEnd (ctrl, trailers) {
t.ok(pauseResumeWorked, 'Pause/resume should work with chained decompression')
t.equal(responseData, data, 'Data should be correctly decompressed from chained encodings')
},
onResponseError (ctrl, err) {
t.fail(err)
}
}
await client.dispatch({
method: 'GET',
path: '/'
}, handler)
await t.completed
})
test('should behave like fetch() for compressed responses', async t => {
t = tspl(t, { plan: 10 })
const testData = 'Test data that will be compressed and should be automatically decompressed by both fetch and request with decompress interceptor'
const server = createServer({ joinDuplicateHeaders: true }, async (req, res) => {
const gzip = createGzip()
res.writeHead(200, {
'Content-Type': 'text/plain',
'Content-Encoding': 'gzip'
})
gzip.pipe(res)
gzip.end(testData)
})
server.listen(0)
await once(server, 'listening')
const baseUrl = `http://localhost:${server.address().port}`
const { fetch } = require('../..')
const fetchResponse = await fetch(baseUrl)
const fetchBody = await fetchResponse.text()
const client = new Client(baseUrl)
const requestResponseWithoutDecompression = await client.request({
method: 'GET',
path: '/'
})
const requestBodyWithoutDecompression = await requestResponseWithoutDecompression.body.text()
const clientWithDecompression = client.compose(createDecompressInterceptor())
const requestResponseWithDecompression = await clientWithDecompression.request({
method: 'GET',
path: '/'
})
const requestBodyWithDecompression = await requestResponseWithDecompression.body.text()
after(async () => {
await client.close()
server.close()
await once(server, 'close')
})
t.equal(fetchResponse.status, 200)
t.equal(fetchBody, testData, 'fetch should automatically decompress')
t.equal(requestBodyWithDecompression, fetchBody, 'request with decompression interceptor should match fetch behavior')
t.notEqual(requestBodyWithoutDecompression, fetchBody, 'request without decompression interceptor should return compressed data')
t.equal(fetchResponse.headers.get('content-type'), 'text/plain', 'content-type header should be preserved with fetch')
t.equal(fetchResponse.headers.get('content-encoding'), 'gzip', 'content-encoding header should be preserved with fetch')
t.equal(requestResponseWithoutDecompression.headers['content-type'], 'text/plain', 'content-type header should be preserved without decompression')
t.equal(requestResponseWithoutDecompression.headers['content-encoding'], 'gzip', 'content-encoding header should be preserved without decompression')
t.equal(requestResponseWithDecompression.headers['content-type'], 'text/plain', 'content-type header should be preserved with decompression')
t.equal(requestResponseWithDecompression.headers['content-encoding'], undefined, 'content-encoding header should be removed with decompression')
await t.completed
})
// CVE fix: Limit the number of content-encodings to prevent resource exhaustion
// Similar to urllib3 (GHSA-gm62-xv2j-4w53) and curl (CVE-2022-32206)
const MAX_CONTENT_ENCODINGS = 5
test(`should allow exactly ${MAX_CONTENT_ENCODINGS} content-encodings`, async t => {
t = tspl(t, { plan: 3 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
// Use identity encodings (no actual compression) for simplicity
const encodings = Array(MAX_CONTENT_ENCODINGS).fill('identity').join(', ')
res.writeHead(200, {
'Content-Type': 'text/plain',
'Content-Encoding': encodings
})
res.end('test')
})
server.listen(0)
await once(server, 'listening')
const client = new Client(
`http://localhost:${server.address().port}`
).compose(createDecompressInterceptor())
after(async () => {
await client.close()
server.close()
await once(server, 'close')
})
const response = await client.request({
method: 'GET',
path: '/'
})
// With 5 identity encodings, the interceptor should pass through (identity is not in supportedEncodings)
t.equal(response.statusCode, 200)
t.ok(response.headers['content-encoding'], 'content-encoding header should be preserved for identity')
t.equal(await response.body.text(), 'test')
await t.completed
})
test(`should reject more than ${MAX_CONTENT_ENCODINGS} content-encodings`, async t => {
t = tspl(t, { plan: 1 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
const encodings = Array(MAX_CONTENT_ENCODINGS + 1).fill('gzip').join(', ')
res.writeHead(200, {
'Content-Type': 'text/plain',
'Content-Encoding': encodings
})
res.end('test')
})
server.listen(0)
await once(server, 'listening')
const client = new Client(
`http://localhost:${server.address().port}`
).compose(createDecompressInterceptor())
after(async () => {
await client.close()
server.close()
await once(server, 'close')
})
try {
const response = await client.request({
method: 'GET',
path: '/'
})
await response.body.text()
t.fail('Should have thrown an error')
} catch (err) {
t.ok(err.message.includes('content-encoding'), 'Error should mention content-encoding')
}
await t.completed
})
test('should reject excessive content-encoding chains', async t => {
t = tspl(t, { plan: 1 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
const encodings = Array(100).fill('gzip').join(', ')
res.writeHead(200, {
'Content-Type': 'text/plain',
'Content-Encoding': encodings
})
res.end('test')
})
server.listen(0)
await once(server, 'listening')
const client = new Client(
`http://localhost:${server.address().port}`
).compose(createDecompressInterceptor())
after(async () => {
await client.close()
server.close()
await once(server, 'close')
})
try {
const response = await client.request({
method: 'GET',
path: '/'
})
await response.body.text()
t.fail('Should have thrown an error')
} catch (err) {
t.ok(err.message.includes('content-encoding'), 'Error should mention content-encoding')
}
await t.completed
})
test('should work with global dispatcher for both fetch() and request()', async t => {
t = tspl(t, { plan: 8 })
const testData = 'Global dispatcher test data for decompression interceptor'
const server = createServer({ joinDuplicateHeaders: true }, async (req, res) => {
const gzip = createGzip()
const chunks = []
gzip.on('data', chunk => chunks.push(chunk))
gzip.on('end', () => {
const compressedData = Buffer.concat(chunks)
res.writeHead(200, {
'Content-Type': 'text/plain',
'Content-Encoding': 'gzip',
'Content-Length': compressedData.length
})
res.end(compressedData)
})
gzip.end(testData)
})
server.listen(0)
await once(server, 'listening')
const baseUrl = `http://localhost:${server.address().port}`
const originalDispatcher = getGlobalDispatcher()
setGlobalDispatcher(getGlobalDispatcher().compose(createDecompressInterceptor()))
after(async () => {
setGlobalDispatcher(originalDispatcher)
server.close()
await once(server, 'close')
})
const { fetch } = require('../..')
const fetchResponse = await fetch(baseUrl)
const fetchBody = await fetchResponse.text()
const requestResponse = await request(baseUrl, {
method: 'GET'
})
const requestBody = await requestResponse.body.text()
t.equal(fetchResponse.status, 200)
t.equal(fetchBody, testData, 'fetch should automatically decompress with global interceptor')
t.equal(requestResponse.statusCode, 200)
t.equal(requestBody, testData, 'request should automatically decompress with global interceptor')
t.equal(requestResponse.headers['content-encoding'], undefined, 'request content-encoding header should be removed with global interceptor')
t.equal(requestResponse.headers['content-length'], undefined, 'request content-length header should be removed with global interceptor')
t.equal(fetchResponse.headers.get('content-length'), undefined, 'content-length header should be removed with fetch due to global interceptor')
t.equal(fetchResponse.headers.get('content-encoding'), undefined, 'content-encoding header should be removed with fetch due to global interceptor')
await t.completed
})
================================================
FILE: test/interceptors/deduplicate.js
================================================
'use strict'
const { createServer } = require('node:http')
const { describe, test, after } = require('node:test')
const { once } = require('node:events')
const { strictEqual } = require('node:assert')
const { setTimeout: sleep } = require('node:timers/promises')
const diagnosticsChannel = require('node:diagnostics_channel')
const { Client, interceptors } = require('../../index')
describe('Deduplicate Interceptor', () => {
test('deduplicates concurrent requests for the same resource', async () => {
let requestsToOrigin = 0
const server = createServer({ joinDuplicateHeaders: true }, async (req, res) => {
requestsToOrigin++
// Simulate slow response to ensure requests overlap
await sleep(100)
res.end('response-body')
}).listen(0)
const client = new Client(`http://localhost:${server.address().port}`)
.compose(interceptors.deduplicate())
after(async () => {
server.close()
await client.close()
})
await once(server, 'listening')
const request = {
origin: 'localhost',
method: 'GET',
path: '/'
}
// Send multiple concurrent requests
const [res1, res2, res3] = await Promise.all([
client.request(request),
client.request(request),
client.request(request)
])
// Only one request should have reached the origin
strictEqual(requestsToOrigin, 1)
// All responses should have the same body
const [body1, body2, body3] = await Promise.all([
res1.body.text(),
res2.body.text(),
res3.body.text()
])
strictEqual(body1, 'response-body')
strictEqual(body2, 'response-body')
strictEqual(body3, 'response-body')
})
test('deduplicates concurrent requests with same headers', async () => {
let requestsToOrigin = 0
const server = createServer({ joinDuplicateHeaders: true }, async (req, res) => {
requestsToOrigin++
await sleep(100)
res.end(`response for ${req.headers['accept-encoding']}`)
}).listen(0)
const client = new Client(`http://localhost:${server.address().port}`)
.compose(interceptors.deduplicate())
after(async () => {
server.close()
await client.close()
})
await once(server, 'listening')
const request = {
origin: 'localhost',
method: 'GET',
path: '/',
headers: {
'accept-encoding': 'gzip'
}
}
// Send concurrent requests with same header values
const [res1, res2] = await Promise.all([
client.request(request),
client.request(request)
])
strictEqual(requestsToOrigin, 1)
const [body1, body2] = await Promise.all([
res1.body.text(),
res2.body.text()
])
strictEqual(body1, 'response for gzip')
strictEqual(body2, 'response for gzip')
})
test('does not deduplicate requests with different header values', async () => {
let requestsToOrigin = 0
const server = createServer({ joinDuplicateHeaders: true }, async (req, res) => {
requestsToOrigin++
await sleep(100)
res.end(`response for ${req.headers['accept-encoding']}`)
}).listen(0)
const client = new Client(`http://localhost:${server.address().port}`)
.compose(interceptors.deduplicate())
after(async () => {
server.close()
await client.close()
})
await once(server, 'listening')
const requestGzip = {
origin: 'localhost',
method: 'GET',
path: '/',
headers: {
'accept-encoding': 'gzip'
}
}
const requestBr = {
origin: 'localhost',
method: 'GET',
path: '/',
headers: {
'accept-encoding': 'br'
}
}
// Send concurrent requests with different header values
const [res1, res2] = await Promise.all([
client.request(requestGzip),
client.request(requestBr)
])
// Both should reach origin since they have different header values
strictEqual(requestsToOrigin, 2)
const [body1, body2] = await Promise.all([
res1.body.text(),
res2.body.text()
])
strictEqual(body1, 'response for gzip')
strictEqual(body2, 'response for br')
})
test('does not deduplicate requests with different paths', async () => {
let requestsToOrigin = 0
const server = createServer({ joinDuplicateHeaders: true }, async (req, res) => {
requestsToOrigin++
await sleep(100)
res.end(`response for ${req.url}`)
}).listen(0)
const client = new Client(`http://localhost:${server.address().port}`)
.compose(interceptors.deduplicate())
after(async () => {
server.close()
await client.close()
})
await once(server, 'listening')
// Send concurrent requests to different paths
const [res1, res2] = await Promise.all([
client.request({ origin: 'localhost', method: 'GET', path: '/a' }),
client.request({ origin: 'localhost', method: 'GET', path: '/b' })
])
// Both should reach origin
strictEqual(requestsToOrigin, 2)
const [body1, body2] = await Promise.all([
res1.body.text(),
res2.body.text()
])
strictEqual(body1, 'response for /a')
strictEqual(body2, 'response for /b')
})
test('propagates errors to all waiting handlers', async () => {
let requestsToOrigin = 0
const server = createServer({ joinDuplicateHeaders: true }, async (req, res) => {
requestsToOrigin++
await sleep(50)
// Destroy the connection to simulate an error
res.destroy()
}).listen(0)
const client = new Client(`http://localhost:${server.address().port}`)
.compose(interceptors.deduplicate())
after(async () => {
server.close()
await client.close()
})
await once(server, 'listening')
const request = {
origin: 'localhost',
method: 'GET',
path: '/'
}
// Send concurrent requests that will all fail
const results = await Promise.allSettled([
client.request(request),
client.request(request),
client.request(request)
])
// Only one request should have reached the origin
strictEqual(requestsToOrigin, 1)
// All should have failed
strictEqual(results[0].status, 'rejected')
strictEqual(results[1].status, 'rejected')
strictEqual(results[2].status, 'rejected')
})
test('works with cache interceptor for cacheable responses', async () => {
let requestsToOrigin = 0
const server = createServer({ joinDuplicateHeaders: true }, async (req, res) => {
requestsToOrigin++
await sleep(100)
res.setHeader('cache-control', 's-maxage=10')
res.end('cached-response')
}).listen(0)
const client = new Client(`http://localhost:${server.address().port}`)
.compose(interceptors.deduplicate())
.compose(interceptors.cache())
after(async () => {
server.close()
await client.close()
})
await once(server, 'listening')
const request = {
origin: 'localhost',
method: 'GET',
path: '/'
}
// First batch of concurrent requests
await Promise.all([
client.request(request),
client.request(request)
])
strictEqual(requestsToOrigin, 1)
// Subsequent request should be from cache
const res = await client.request(request)
strictEqual(requestsToOrigin, 1)
strictEqual(await res.body.text(), 'cached-response')
})
test('deduplication works with non-cacheable responses', async () => {
let requestsToOrigin = 0
const server = createServer({ joinDuplicateHeaders: true }, async (req, res) => {
requestsToOrigin++
await sleep(100)
res.setHeader('cache-control', 'no-store')
res.end('non-cached-response')
}).listen(0)
const client = new Client(`http://localhost:${server.address().port}`)
.compose(interceptors.deduplicate())
.compose(interceptors.cache())
after(async () => {
server.close()
await client.close()
})
await once(server, 'listening')
const request = {
origin: 'localhost',
method: 'GET',
path: '/'
}
// Concurrent requests for non-cacheable response should still be deduplicated
const [res1, res2] = await Promise.all([
client.request(request),
client.request(request)
])
strictEqual(requestsToOrigin, 1)
const [body1, body2] = await Promise.all([
res1.body.text(),
res2.body.text()
])
strictEqual(body1, 'non-cached-response')
strictEqual(body2, 'non-cached-response')
// But subsequent requests should NOT be cached
const res3 = await client.request(request)
strictEqual(requestsToOrigin, 2)
strictEqual(await res3.body.text(), 'non-cached-response')
})
test('deduplication cleans up pending requests after completion', async () => {
let requestsToOrigin = 0
const server = createServer({ joinDuplicateHeaders: true }, async (req, res) => {
requestsToOrigin++
await sleep(50)
res.end('response')
}).listen(0)
const client = new Client(`http://localhost:${server.address().port}`)
.compose(interceptors.deduplicate())
after(async () => {
server.close()
await client.close()
})
await once(server, 'listening')
const request = {
origin: 'localhost',
method: 'GET',
path: '/'
}
// First batch
await Promise.all([
client.request(request),
client.request(request)
])
strictEqual(requestsToOrigin, 1)
// Second batch - should create new request since first batch is complete
await Promise.all([
client.request(request),
client.request(request)
])
strictEqual(requestsToOrigin, 2)
})
test('deduplication works with chunked response bodies', async () => {
let requestsToOrigin = 0
const bodyPart = 'chunk-data-'
const server = createServer({ joinDuplicateHeaders: true }, async (req, res) => {
requestsToOrigin++
res.setHeader('transfer-encoding', 'chunked')
// Send multiple chunks
for (let i = 0; i < 5; i++) {
res.write(bodyPart + i)
await sleep(10)
}
res.end()
}).listen(0)
const client = new Client(`http://localhost:${server.address().port}`)
.compose(interceptors.deduplicate())
after(async () => {
server.close()
await client.close()
})
await once(server, 'listening')
const request = {
origin: 'localhost',
method: 'GET',
path: '/'
}
// Send concurrent requests
const [res1, res2] = await Promise.all([
client.request(request),
client.request(request)
])
strictEqual(requestsToOrigin, 1)
const expectedBody = 'chunk-data-0chunk-data-1chunk-data-2chunk-data-3chunk-data-4'
const [body1, body2] = await Promise.all([
res1.body.text(),
res2.body.text()
])
strictEqual(body1, expectedBody)
strictEqual(body2, expectedBody)
})
test('all response properties are available to deduplicated handlers', async () => {
let requestsToOrigin = 0
const server = createServer({ joinDuplicateHeaders: true }, async (req, res) => {
requestsToOrigin++
await sleep(100)
res.setHeader('x-custom-header', 'custom-value')
res.statusCode = 201
res.end('response')
}).listen(0)
const client = new Client(`http://localhost:${server.address().port}`)
.compose(interceptors.deduplicate())
after(async () => {
server.close()
await client.close()
})
await once(server, 'listening')
const request = {
origin: 'localhost',
method: 'GET',
path: '/'
}
const [res1, res2] = await Promise.all([
client.request(request),
client.request(request)
])
strictEqual(requestsToOrigin, 1)
// Both responses should have the same status code and headers
strictEqual(res1.statusCode, 201)
strictEqual(res2.statusCode, 201)
strictEqual(res1.headers['x-custom-header'], 'custom-value')
strictEqual(res2.headers['x-custom-header'], 'custom-value')
const [body1, body2] = await Promise.all([
res1.body.text(),
res2.body.text()
])
strictEqual(body1, 'response')
strictEqual(body2, 'response')
})
test('diagnostic channel tracks pending requests correctly', async () => {
const events = []
const channel = diagnosticsChannel.channel('undici:request:pending-requests')
const onMessage = (message) => {
events.push(message)
}
channel.subscribe(onMessage)
const server = createServer({ joinDuplicateHeaders: true }, async (req, res) => {
await sleep(100)
res.end('response')
}).listen(0)
const client = new Client(`http://localhost:${server.address().port}`)
.compose(interceptors.deduplicate())
after(async () => {
channel.unsubscribe(onMessage)
server.close()
await client.close()
})
await once(server, 'listening')
const request = {
origin: 'localhost',
method: 'GET',
path: '/'
}
// Send concurrent requests
await Promise.all([
client.request(request),
client.request(request)
])
// Should have seen: added (size=1), removed (size=0)
strictEqual(events.length, 2)
strictEqual(events[0].type, 'added')
strictEqual(events[0].size, 1)
strictEqual(events[1].type, 'removed')
strictEqual(events[1].size, 0)
})
test('diagnostic channel shows cleanup after error', async () => {
const events = []
const channel = diagnosticsChannel.channel('undici:request:pending-requests')
const onMessage = (message) => {
events.push(message)
}
channel.subscribe(onMessage)
const server = createServer({ joinDuplicateHeaders: true }, async (req, res) => {
await sleep(50)
res.destroy() // Simulate error
}).listen(0)
const client = new Client(`http://localhost:${server.address().port}`)
.compose(interceptors.deduplicate())
after(async () => {
channel.unsubscribe(onMessage)
server.close()
await client.close()
})
await once(server, 'listening')
const request = {
origin: 'localhost',
method: 'GET',
path: '/'
}
// Send concurrent requests that will error
await Promise.allSettled([
client.request(request),
client.request(request)
])
// Should have seen: added (size=1), removed (size=0)
strictEqual(events.length, 2)
strictEqual(events[0].type, 'added')
strictEqual(events[0].size, 1)
strictEqual(events[1].type, 'removed')
strictEqual(events[1].size, 0)
})
test('diagnostic channel tracks multiple pending requests separately', async () => {
const events = []
const channel = diagnosticsChannel.channel('undici:request:pending-requests')
const onMessage = (message) => {
events.push(message)
}
channel.subscribe(onMessage)
let requestsToOrigin = 0
const server = createServer({ joinDuplicateHeaders: true }, async (req, res) => {
requestsToOrigin++
await sleep(100)
res.end(`response for ${req.url}`)
}).listen(0)
const client = new Client(`http://localhost:${server.address().port}`)
.compose(interceptors.deduplicate())
after(async () => {
channel.unsubscribe(onMessage)
server.close()
await client.close()
})
await once(server, 'listening')
// Send requests to two different paths concurrently
await Promise.all([
client.request({ origin: 'localhost', method: 'GET', path: '/a' }),
client.request({ origin: 'localhost', method: 'GET', path: '/b' }),
client.request({ origin: 'localhost', method: 'GET', path: '/a' }), // Deduplicated with first
client.request({ origin: 'localhost', method: 'GET', path: '/b' }) // Deduplicated with second
])
// Should have 2 origin requests (one for /a, one for /b)
strictEqual(requestsToOrigin, 2)
// Should have 4 events: 2 added, 2 removed
strictEqual(events.length, 4)
// First two should be 'added' events with sizes 1 and 2
const addedEvents = events.filter(e => e.type === 'added')
const removedEvents = events.filter(e => e.type === 'removed')
strictEqual(addedEvents.length, 2)
strictEqual(removedEvents.length, 2)
strictEqual(addedEvents[0].size, 1)
strictEqual(addedEvents[1].size, 2)
strictEqual(removedEvents[removedEvents.length - 1].size, 0) // All cleaned up
})
test('does not deduplicate requests with different Authorization headers', async () => {
let requestsToOrigin = 0
const server = createServer({ joinDuplicateHeaders: true }, async (req, res) => {
requestsToOrigin++
await sleep(100)
res.end(`response for ${req.headers.authorization}`)
}).listen(0)
const client = new Client(`http://localhost:${server.address().port}`)
.compose(interceptors.deduplicate())
after(async () => {
server.close()
await client.close()
})
await once(server, 'listening')
// Send concurrent requests with different Authorization headers
const [res1, res2] = await Promise.all([
client.request({
origin: 'localhost',
method: 'GET',
path: '/',
headers: { authorization: 'Bearer token-user-1' }
}),
client.request({
origin: 'localhost',
method: 'GET',
path: '/',
headers: { authorization: 'Bearer token-user-2' }
})
])
// Both requests should reach origin since they have different Authorization headers
strictEqual(requestsToOrigin, 2)
const [body1, body2] = await Promise.all([
res1.body.text(),
res2.body.text()
])
strictEqual(body1, 'response for Bearer token-user-1')
strictEqual(body2, 'response for Bearer token-user-2')
})
test('does not deduplicate requests with different Cookie headers', async () => {
let requestsToOrigin = 0
const server = createServer({ joinDuplicateHeaders: true }, async (req, res) => {
requestsToOrigin++
await sleep(100)
res.end(`response for ${req.headers.cookie}`)
}).listen(0)
const client = new Client(`http://localhost:${server.address().port}`)
.compose(interceptors.deduplicate())
after(async () => {
server.close()
await client.close()
})
await once(server, 'listening')
// Send concurrent requests with different Cookie headers
const [res1, res2] = await Promise.all([
client.request({
origin: 'localhost',
method: 'GET',
path: '/',
headers: { cookie: 'session=user1-session-id' }
}),
client.request({
origin: 'localhost',
method: 'GET',
path: '/',
headers: { cookie: 'session=user2-session-id' }
})
])
// Both requests should reach origin since they have different Cookie headers
strictEqual(requestsToOrigin, 2)
const [body1, body2] = await Promise.all([
res1.body.text(),
res2.body.text()
])
strictEqual(body1, 'response for session=user1-session-id')
strictEqual(body2, 'response for session=user2-session-id')
})
test('deduplicates requests with same Authorization header', async () => {
let requestsToOrigin = 0
const server = createServer({ joinDuplicateHeaders: true }, async (req, res) => {
requestsToOrigin++
await sleep(100)
res.end(`response for ${req.headers.authorization}`)
}).listen(0)
const client = new Client(`http://localhost:${server.address().port}`)
.compose(interceptors.deduplicate())
after(async () => {
server.close()
await client.close()
})
await once(server, 'listening')
// Send concurrent requests with the same Authorization header
const [res1, res2] = await Promise.all([
client.request({
origin: 'localhost',
method: 'GET',
path: '/',
headers: { authorization: 'Bearer same-token' }
}),
client.request({
origin: 'localhost',
method: 'GET',
path: '/',
headers: { authorization: 'Bearer same-token' }
})
])
// Only one request should reach origin since they have the same Authorization header
strictEqual(requestsToOrigin, 1)
const [body1, body2] = await Promise.all([
res1.body.text(),
res2.body.text()
])
strictEqual(body1, 'response for Bearer same-token')
strictEqual(body2, 'response for Bearer same-token')
})
test('skipHeaderNames skips deduplication for requests with specified headers', async () => {
let requestsToOrigin = 0
const server = createServer({ joinDuplicateHeaders: true }, async (req, res) => {
requestsToOrigin++
await sleep(100)
res.end(`response ${requestsToOrigin}`)
}).listen(0)
const client = new Client(`http://localhost:${server.address().port}`)
.compose(interceptors.deduplicate({ skipHeaderNames: ['x-no-dedupe'] }))
after(async () => {
server.close()
await client.close()
})
await once(server, 'listening')
// Send concurrent requests with the skip header
const [res1, res2] = await Promise.all([
client.request({
origin: 'localhost',
method: 'GET',
path: '/',
headers: { 'x-no-dedupe': 'true' }
}),
client.request({
origin: 'localhost',
method: 'GET',
path: '/',
headers: { 'x-no-dedupe': 'true' }
})
])
// Both requests should reach origin since they have the skip header
strictEqual(requestsToOrigin, 2)
const [body1, body2] = await Promise.all([
res1.body.text(),
res2.body.text()
])
strictEqual(body1, 'response 1')
strictEqual(body2, 'response 2')
})
test('skipHeaderNames is case-insensitive', async () => {
let requestsToOrigin = 0
const server = createServer({ joinDuplicateHeaders: true }, async (req, res) => {
requestsToOrigin++
await sleep(100)
res.end(`response ${requestsToOrigin}`)
}).listen(0)
const client = new Client(`http://localhost:${server.address().port}`)
.compose(interceptors.deduplicate({ skipHeaderNames: ['X-No-Dedupe'] }))
after(async () => {
server.close()
await client.close()
})
await once(server, 'listening')
// Send concurrent requests with lowercase header (should still match)
const [res1, res2] = await Promise.all([
client.request({
origin: 'localhost',
method: 'GET',
path: '/',
headers: { 'x-no-dedupe': 'true' }
}),
client.request({
origin: 'localhost',
method: 'GET',
path: '/',
headers: { 'x-no-dedupe': 'true' }
})
])
// Both requests should reach origin since header matching is case-insensitive
strictEqual(requestsToOrigin, 2)
const [body1, body2] = await Promise.all([
res1.body.text(),
res2.body.text()
])
strictEqual(body1, 'response 1')
strictEqual(body2, 'response 2')
})
test('skipHeaderNames allows deduplication for requests without specified headers', async () => {
let requestsToOrigin = 0
const server = createServer({ joinDuplicateHeaders: true }, async (req, res) => {
requestsToOrigin++
await sleep(100)
res.end('response')
}).listen(0)
const client = new Client(`http://localhost:${server.address().port}`)
.compose(interceptors.deduplicate({ skipHeaderNames: ['x-no-dedupe'] }))
after(async () => {
server.close()
await client.close()
})
await once(server, 'listening')
// Send concurrent requests without the skip header
const [res1, res2] = await Promise.all([
client.request({
origin: 'localhost',
method: 'GET',
path: '/'
}),
client.request({
origin: 'localhost',
method: 'GET',
path: '/'
})
])
// Only one request should reach origin since they don't have the skip header
strictEqual(requestsToOrigin, 1)
const [body1, body2] = await Promise.all([
res1.body.text(),
res2.body.text()
])
strictEqual(body1, 'response')
strictEqual(body2, 'response')
})
test('skipHeaderNames with multiple headers', async () => {
let requestsToOrigin = 0
const server = createServer({ joinDuplicateHeaders: true }, async (req, res) => {
requestsToOrigin++
await sleep(100)
res.end(`response ${requestsToOrigin}`)
}).listen(0)
const client = new Client(`http://localhost:${server.address().port}`)
.compose(interceptors.deduplicate({ skipHeaderNames: ['x-skip-1', 'x-skip-2'] }))
after(async () => {
server.close()
await client.close()
})
await once(server, 'listening')
// Send concurrent requests with different skip headers
const [res1, res2, res3] = await Promise.all([
client.request({
origin: 'localhost',
method: 'GET',
path: '/',
headers: { 'x-skip-1': 'true' }
}),
client.request({
origin: 'localhost',
method: 'GET',
path: '/',
headers: { 'x-skip-2': 'true' }
}),
client.request({
origin: 'localhost',
method: 'GET',
path: '/'
})
])
// First two should not be deduplicated (they have skip headers)
// Third request starts a new pending request (no skip header)
// If third request arrives after first two start, it won't be deduplicated with them
// but could be deduplicated with other requests without skip headers
strictEqual(requestsToOrigin, 3)
const [body1, body2, body3] = await Promise.all([
res1.body.text(),
res2.body.text(),
res3.body.text()
])
strictEqual(body1, 'response 1')
strictEqual(body2, 'response 2')
strictEqual(body3, 'response 3')
})
test('throws TypeError if skipHeaderNames is not an array', () => {
const { throws } = require('node:assert')
throws(() => {
interceptors.deduplicate({ skipHeaderNames: 'not-an-array' })
}, {
name: 'TypeError',
message: 'expected opts.skipHeaderNames to be an array, got string'
})
})
test('excludeHeaderNames deduplicates requests with different excluded header values', async () => {
let requestsToOrigin = 0
const server = createServer({ joinDuplicateHeaders: true }, async (req, res) => {
requestsToOrigin++
await sleep(100)
res.end('response')
}).listen(0)
const client = new Client(`http://localhost:${server.address().port}`)
.compose(interceptors.deduplicate({ excludeHeaderNames: ['x-request-id'] }))
after(async () => {
server.close()
await client.close()
})
await once(server, 'listening')
// Send concurrent requests with different x-request-id values
const [res1, res2] = await Promise.all([
client.request({
origin: 'localhost',
method: 'GET',
path: '/',
headers: { 'x-request-id': 'req-1' }
}),
client.request({
origin: 'localhost',
method: 'GET',
path: '/',
headers: { 'x-request-id': 'req-2' }
})
])
// Only one request should reach origin since x-request-id is excluded from the key
strictEqual(requestsToOrigin, 1)
const [body1, body2] = await Promise.all([
res1.body.text(),
res2.body.text()
])
strictEqual(body1, 'response')
strictEqual(body2, 'response')
})
test('excludeHeaderNames is case-insensitive', async () => {
let requestsToOrigin = 0
const server = createServer({ joinDuplicateHeaders: true }, async (req, res) => {
requestsToOrigin++
await sleep(100)
res.end('response')
}).listen(0)
const client = new Client(`http://localhost:${server.address().port}`)
.compose(interceptors.deduplicate({ excludeHeaderNames: ['X-Request-ID'] }))
after(async () => {
server.close()
await client.close()
})
await once(server, 'listening')
// Send concurrent requests with lowercase header (should still be excluded)
const [res1, res2] = await Promise.all([
client.request({
origin: 'localhost',
method: 'GET',
path: '/',
headers: { 'x-request-id': 'req-1' }
}),
client.request({
origin: 'localhost',
method: 'GET',
path: '/',
headers: { 'x-request-id': 'req-2' }
})
])
// Should be deduplicated since header matching is case-insensitive
strictEqual(requestsToOrigin, 1)
const [body1, body2] = await Promise.all([
res1.body.text(),
res2.body.text()
])
strictEqual(body1, 'response')
strictEqual(body2, 'response')
})
test('excludeHeaderNames does not affect other headers in deduplication key', async () => {
let requestsToOrigin = 0
const server = createServer({ joinDuplicateHeaders: true }, async (req, res) => {
requestsToOrigin++
await sleep(100)
res.end(`response for ${req.headers['accept-encoding']}`)
}).listen(0)
const client = new Client(`http://localhost:${server.address().port}`)
.compose(interceptors.deduplicate({ excludeHeaderNames: ['x-request-id'] }))
after(async () => {
server.close()
await client.close()
})
await once(server, 'listening')
// Send concurrent requests with different accept-encoding (not excluded)
const [res1, res2] = await Promise.all([
client.request({
origin: 'localhost',
method: 'GET',
path: '/',
headers: { 'x-request-id': 'req-1', 'accept-encoding': 'gzip' }
}),
client.request({
origin: 'localhost',
method: 'GET',
path: '/',
headers: { 'x-request-id': 'req-2', 'accept-encoding': 'br' }
})
])
// Both should reach origin since accept-encoding differs and is not excluded
strictEqual(requestsToOrigin, 2)
const [body1, body2] = await Promise.all([
res1.body.text(),
res2.body.text()
])
strictEqual(body1, 'response for gzip')
strictEqual(body2, 'response for br')
})
test('excludeHeaderNames with multiple headers', async () => {
let requestsToOrigin = 0
const server = createServer({ joinDuplicateHeaders: true }, async (req, res) => {
requestsToOrigin++
await sleep(100)
res.end('response')
}).listen(0)
const client = new Client(`http://localhost:${server.address().port}`)
.compose(interceptors.deduplicate({ excludeHeaderNames: ['x-request-id', 'x-correlation-id'] }))
after(async () => {
server.close()
await client.close()
})
await once(server, 'listening')
// Send concurrent requests with different values for both excluded headers
const [res1, res2] = await Promise.all([
client.request({
origin: 'localhost',
method: 'GET',
path: '/',
headers: { 'x-request-id': 'req-1', 'x-correlation-id': 'corr-1' }
}),
client.request({
origin: 'localhost',
method: 'GET',
path: '/',
headers: { 'x-request-id': 'req-2', 'x-correlation-id': 'corr-2' }
})
])
// Should be deduplicated since both varying headers are excluded
strictEqual(requestsToOrigin, 1)
const [body1, body2] = await Promise.all([
res1.body.text(),
res2.body.text()
])
strictEqual(body1, 'response')
strictEqual(body2, 'response')
})
test('does not deduplicate non-safe methods when methods option uses defaults', async () => {
let requestsToOrigin = 0
const server = createServer({ joinDuplicateHeaders: true }, async (req, res) => {
requestsToOrigin++
await sleep(100)
res.end(`response ${requestsToOrigin}`)
}).listen(0)
const client = new Client(`http://localhost:${server.address().port}`)
.compose(interceptors.deduplicate())
after(async () => {
server.close()
await client.close()
})
await once(server, 'listening')
const [res1, res2] = await Promise.all([
client.request({
origin: 'localhost',
method: 'POST',
path: '/',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ token: 'a' })
}),
client.request({
origin: 'localhost',
method: 'POST',
path: '/',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ token: 'b' })
})
])
strictEqual(requestsToOrigin, 2)
const [body1, body2] = await Promise.all([
res1.body.text(),
res2.body.text()
])
strictEqual(body1, 'response 1')
strictEqual(body2, 'response 2')
})
test('does not deduplicate requests that arrive after body streaming starts', async () => {
let requestsToOrigin = 0
const server = createServer({ joinDuplicateHeaders: true }, async (req, res) => {
requestsToOrigin++
res.write('chunk-1')
await sleep(100)
res.end('chunk-2')
}).listen(0)
const client = new Client(`http://localhost:${server.address().port}`)
.compose(interceptors.deduplicate())
after(async () => {
server.close()
await client.close()
})
await once(server, 'listening')
const request = {
origin: 'localhost',
method: 'GET',
path: '/'
}
const firstResponsePromise = client.request(request)
// Wait until the first response starts streaming body data.
await sleep(20)
const [res1, res2] = await Promise.all([
firstResponsePromise,
client.request(request)
])
const [body1, body2] = await Promise.all([
res1.body.text(),
res2.body.text()
])
strictEqual(requestsToOrigin, 2)
strictEqual(body1, 'chunk-1chunk-2')
strictEqual(body2, 'chunk-1chunk-2')
})
test('errors paused waiting handlers when buffered data exceeds maxBufferSize', async () => {
let requestsToOrigin = 0
const chunk = Buffer.alloc(8 * 1024, 'a')
const totalChunks = 6
const server = createServer({ joinDuplicateHeaders: true }, async (req, res) => {
requestsToOrigin++
for (let i = 0; i < totalChunks; i++) {
res.write(chunk)
await sleep(10)
}
res.end()
}).listen(0)
const client = new Client(`http://localhost:${server.address().port}`)
.compose(interceptors.deduplicate({ maxBufferSize: 16 * 1024 }))
after(async () => {
server.close()
await client.close()
})
await once(server, 'listening')
const request = {
origin: 'localhost',
method: 'GET',
path: '/'
}
const primaryResponsePromise = client.request(request)
const slowWaitingHandlerErrorPromise = new Promise((resolve, reject) => {
client.dispatch(request, {
onConnect () {},
onHeaders () {},
onData () {
// Pause the waiting handler immediately and never resume it.
return false
},
onComplete () {
reject(new Error('Expected paused waiting handler to fail'))
},
onError (err) {
resolve(err)
}
})
})
const primaryResponse = await primaryResponsePromise
const primaryBody = await primaryResponse.body.arrayBuffer()
const waitingHandlerErr = await slowWaitingHandlerErrorPromise
strictEqual(requestsToOrigin, 1)
strictEqual(primaryBody.byteLength, chunk.length * totalChunks)
strictEqual(waitingHandlerErr.code, 'UND_ERR_ABORTED')
})
test('throws TypeError if maxBufferSize is not a positive finite number', () => {
const { throws } = require('node:assert')
throws(() => {
interceptors.deduplicate({ maxBufferSize: 0 })
}, {
name: 'TypeError',
message: 'expected opts.maxBufferSize to be a positive finite number, got 0'
})
})
test('throws TypeError if excludeHeaderNames is not an array', () => {
const { throws } = require('node:assert')
throws(() => {
interceptors.deduplicate({ excludeHeaderNames: 'not-an-array' })
}, {
name: 'TypeError',
message: 'expected opts.excludeHeaderNames to be an array, got string'
})
})
})
================================================
FILE: test/interceptors/dns.js
================================================
'use strict'
const FakeTimers = require('@sinonjs/fake-timers')
const { test, after } = require('node:test')
const { isIP } = require('node:net')
const { lookup } = require('node:dns')
const { createServer } = require('node:http')
const { createServer: createSecureServer } = require('node:https')
const { once } = require('node:events')
const { tspl } = require('@matteo.collina/tspl')
const pem = require('@metcoder95/https-pem')
const { interceptors, Agent, request } = require('../..')
const { dns } = interceptors
test('Should validate options', t => {
t = tspl(t, { plan: 11 })
t.throws(() => dns({ dualStack: 'true' }), { code: 'UND_ERR_INVALID_ARG' })
t.throws(() => dns({ dualStack: 0 }), { code: 'UND_ERR_INVALID_ARG' })
t.throws(() => dns({ affinity: '4' }), { code: 'UND_ERR_INVALID_ARG' })
t.throws(() => dns({ affinity: 7 }), { code: 'UND_ERR_INVALID_ARG' })
t.throws(() => dns({ maxTTL: -1 }), { code: 'UND_ERR_INVALID_ARG' })
t.throws(() => dns({ maxTTL: '0' }), { code: 'UND_ERR_INVALID_ARG' })
t.throws(() => dns({ maxItems: '1' }), { code: 'UND_ERR_INVALID_ARG' })
t.throws(() => dns({ maxItems: -1 }), { code: 'UND_ERR_INVALID_ARG' })
t.throws(() => dns({ lookup: {} }), { code: 'UND_ERR_INVALID_ARG' })
t.throws(() => dns({ pick: [] }), { code: 'UND_ERR_INVALID_ARG' })
t.throws(() => dns({ storage: new Map() }), { code: 'UND_ERR_INVALID_ARG' })
})
test('Should automatically resolve IPs (dual stack)', async t => {
t = tspl(t, { plan: 8 })
const hostsnames = []
const server = createServer({ joinDuplicateHeaders: true })
const requestOptions = {
method: 'GET',
path: '/',
headers: {
'content-type': 'application/json'
}
}
server.on('request', (req, res) => {
res.writeHead(200, { 'content-type': 'text/plain' })
res.end('hello world!')
})
server.listen(0)
await once(server, 'listening')
const client = new Agent().compose([
dispatch => {
return (opts, handler) => {
const url = new URL(opts.origin)
t.equal(hostsnames.includes(url.hostname), false)
if (url.hostname[0] === '[') {
// [::1] -> ::1
t.equal(isIP(url.hostname.slice(1, 4)), 6)
} else {
t.equal(isIP(url.hostname), 4)
}
hostsnames.push(url.hostname)
return dispatch(opts, handler)
}
},
dns({
lookup: (_origin, _opts, cb) => {
cb(null, [
{
address: '::1',
family: 6
},
{
address: '127.0.0.1',
family: 4
}
])
}
})
])
after(async () => {
await client.close()
server.close()
await once(server, 'close')
})
const response = await client.request({
...requestOptions,
origin: `http://localhost:${server.address().port}`
})
t.equal(response.statusCode, 200)
t.equal(await response.body.text(), 'hello world!')
const response2 = await client.request({
...requestOptions,
origin: `http://localhost:${server.address().port}`
})
t.equal(response2.statusCode, 200)
t.equal(await response2.body.text(), 'hello world!')
})
test('Should respect DNS origin hostname for SNI on TLS', async t => {
t = tspl(t, { plan: 12 })
const hostsnames = []
const server = createSecureServer(pem)
const requestOptions = {
method: 'GET',
path: '/',
headers: {
'content-type': 'application/json'
}
}
server.on('request', (req, res) => {
t.equal(req.headers.host, `localhost:${server.address().port}`)
res.writeHead(200, { 'content-type': 'text/plain' })
res.end('hello world!')
})
server.listen(0)
await once(server, 'listening')
const client = new Agent({
connect: {
rejectUnauthorized: false
}
}).compose([
dispatch => {
return (opts, handler) => {
const url = new URL(opts.origin)
t.equal(hostsnames.includes(url.hostname), false)
t.equal(opts.servername, 'localhost')
if (url.hostname[0] === '[') {
// [::1] -> ::1
t.equal(isIP(url.hostname.slice(1, 4)), 6)
} else {
t.equal(isIP(url.hostname), 4)
}
hostsnames.push(url.hostname)
return dispatch(opts, handler)
}
},
dns({
lookup: (_origin, _opts, cb) => {
cb(null, [
{
address: '::1',
family: 6
},
{
address: '127.0.0.1',
family: 4
}
])
}
})
])
after(async () => {
await client.close()
server.close()
await once(server, 'close')
})
const response = await client.request({
...requestOptions,
origin: `https://localhost:${server.address().port}`
})
t.equal(response.statusCode, 200)
t.equal(await response.body.text(), 'hello world!')
const response2 = await client.request({
...requestOptions,
origin: `https://localhost:${server.address().port}`
})
t.equal(response2.statusCode, 200)
t.equal(await response2.body.text(), 'hello world!')
})
test('Should recover on network errors (dual stack - 4)', async t => {
t = tspl(t, { plan: 7 })
let counter = 0
const server = createServer({ joinDuplicateHeaders: true })
const requestOptions = {
method: 'GET',
path: '/',
headers: {
'content-type': 'application/json'
}
}
server.on('request', (req, res) => {
res.writeHead(200, { 'content-type': 'text/plain' })
res.end('hello world!')
})
server.listen(0, '::1')
await once(server, 'listening')
const client = new Agent().compose([
dispatch => {
return (opts, handler) => {
++counter
const url = new URL(opts.origin)
switch (counter) {
case 1:
t.equal(isIP(url.hostname), 4)
break
case 2:
// [::1] -> ::1
t.equal(isIP(url.hostname.slice(1, 4)), 6)
break
case 3:
// [::1] -> ::1
t.equal(isIP(url.hostname.slice(1, 4)), 6)
break
default:
t.fail('should not reach this point')
}
return dispatch(opts, handler)
}
},
dns({
lookup: (_origin, _opts, cb) => {
cb(null, [
{
address: '::1',
family: 6
},
{
address: '127.0.0.1',
family: 4
}
])
}
})
])
after(async () => {
await client.close()
server.close()
await once(server, 'close')
})
const response = await client.request({
...requestOptions,
origin: `http://localhost:${server.address().port}`
})
t.equal(response.statusCode, 200)
t.equal(await response.body.text(), 'hello world!')
const response2 = await client.request({
...requestOptions,
origin: `http://localhost:${server.address().port}`
})
t.equal(response2.statusCode, 200)
t.equal(await response2.body.text(), 'hello world!')
})
test('Should recover on network errors (dual stack - 6)', async t => {
t = tspl(t, { plan: 7 })
let counter = 0
const server = createServer({ joinDuplicateHeaders: true })
const requestOptions = {
method: 'GET',
path: '/',
headers: {
'content-type': 'application/json'
}
}
server.on('request', (req, res) => {
res.writeHead(200, { 'content-type': 'text/plain' })
res.end('hello world!')
})
server.listen(0, '127.0.0.1')
await once(server, 'listening')
const client = new Agent().compose([
dispatch => {
return (opts, handler) => {
++counter
const url = new URL(opts.origin)
switch (counter) {
case 1:
t.equal(isIP(url.hostname), 4)
break
case 2:
// [::1] -> ::1
t.equal(isIP(url.hostname.slice(1, 4)), 6)
break
case 3:
// [::1] -> ::1
t.equal(isIP(url.hostname), 4)
break
default:
t.fail('should not reach this point')
}
return dispatch(opts, handler)
}
},
dns({
lookup: (_origin, _opts, cb) => {
cb(null, [
{
address: '::1',
family: 6
},
{
address: '127.0.0.1',
family: 4
}
])
}
})
])
after(async () => {
await client.close()
server.close()
await once(server, 'close')
})
const response = await client.request({
...requestOptions,
origin: `http://localhost:${server.address().port}`
})
t.equal(response.statusCode, 200)
t.equal(await response.body.text(), 'hello world!')
const response2 = await client.request({
...requestOptions,
origin: `http://localhost:${server.address().port}`
})
t.equal(response2.statusCode, 200)
t.equal(await response2.body.text(), 'hello world!')
})
test('Should throw when on dual-stack disabled (4)', async t => {
t = tspl(t, { plan: 2 })
let counter = 0
const requestOptions = {
method: 'GET',
path: '/',
headers: {
'content-type': 'application/json'
}
}
const client = new Agent().compose([
dispatch => {
return (opts, handler) => {
++counter
const url = new URL(opts.origin)
switch (counter) {
case 1:
t.equal(isIP(url.hostname), 4)
break
default:
t.fail('should not reach this point')
}
return dispatch(opts, handler)
}
},
dns({ dualStack: false, affinity: 4 })
])
const promise = client.request({
...requestOptions,
origin: 'http://localhost:1234'
})
await t.rejects(promise, 'ECONNREFUSED')
await t.completed
})
test('Should throw when on dual-stack disabled (6)', async t => {
t = tspl(t, { plan: 2 })
let counter = 0
const requestOptions = {
method: 'GET',
path: '/',
headers: {
'content-type': 'application/json'
}
}
const client = new Agent().compose([
dispatch => {
return (opts, handler) => {
++counter
const url = new URL(opts.origin)
switch (counter) {
case 1:
// [::1] -> ::1
t.equal(isIP(url.hostname.slice(1, 4)), 6)
break
default:
t.fail('should not reach this point')
}
return dispatch(opts, handler)
}
},
dns({ dualStack: false, affinity: 6 })
])
const promise = client.request({
...requestOptions,
origin: 'http://localhost:9999'
})
await t.rejects(promise, 'ECONNREFUSED')
await t.completed
})
test('Should automatically resolve IPs (dual stack disabled - 4)', async t => {
t = tspl(t, { plan: 6 })
let counter = 0
const server = createServer({ joinDuplicateHeaders: true })
const requestOptions = {
method: 'GET',
path: '/',
headers: {
'content-type': 'application/json'
}
}
server.on('request', (req, res) => {
res.writeHead(200, { 'content-type': 'text/plain' })
res.end('hello world!')
})
server.listen(0)
await once(server, 'listening')
const client = new Agent().compose([
dispatch => {
return (opts, handler) => {
++counter
const url = new URL(opts.origin)
switch (counter) {
case 1:
t.equal(isIP(url.hostname), 4)
break
case 2:
// [::1] -> ::1
t.equal(isIP(url.hostname), 4)
break
default:
t.fail('should not reach this point')
}
return dispatch(opts, handler)
}
},
dns({ dualStack: false })
])
after(async () => {
await client.close()
server.close()
await once(server, 'close')
})
const response = await client.request({
...requestOptions,
origin: `http://localhost:${server.address().port}`
})
t.equal(response.statusCode, 200)
t.equal(await response.body.text(), 'hello world!')
const response2 = await client.request({
...requestOptions,
origin: `http://localhost:${server.address().port}`
})
t.equal(response2.statusCode, 200)
t.equal(await response2.body.text(), 'hello world!')
})
test('Should automatically resolve IPs (dual stack disabled - 6)', async t => {
t = tspl(t, { plan: 6 })
let counter = 0
const server = createServer({ joinDuplicateHeaders: true })
const requestOptions = {
method: 'GET',
path: '/',
headers: {
'content-type': 'application/json'
}
}
server.on('request', (req, res) => {
res.writeHead(200, { 'content-type': 'text/plain' })
res.end('hello world!')
})
server.listen(0)
await once(server, 'listening')
const client = new Agent().compose([
dispatch => {
return (opts, handler) => {
++counter
const url = new URL(opts.origin)
switch (counter) {
case 1:
// [::1] -> ::1
t.equal(isIP(url.hostname.slice(1, 4)), 6)
break
case 2:
// [::1] -> ::1
t.equal(isIP(url.hostname.slice(1, 4)), 6)
break
default:
t.fail('should not reach this point')
}
return dispatch(opts, handler)
}
},
dns({ dualStack: false, affinity: 6 })
])
after(async () => {
await client.close()
server.close()
await once(server, 'close')
})
const response = await client.request({
...requestOptions,
origin: `http://localhost:${server.address().port}`
})
t.equal(response.statusCode, 200)
t.equal(await response.body.text(), 'hello world!')
const response2 = await client.request({
...requestOptions,
origin: `http://localhost:${server.address().port}`
})
t.equal(response2.statusCode, 200)
t.equal(await response2.body.text(), 'hello world!')
})
test('Should we handle TTL (4)', async t => {
t = tspl(t, { plan: 10 })
const clock = FakeTimers.install()
let counter = 0
let lookupCounter = 0
const server = createServer({ joinDuplicateHeaders: true })
const requestOptions = {
method: 'GET',
path: '/',
headers: {
'content-type': 'application/json'
}
}
server.on('request', (req, res) => {
res.writeHead(200, { 'content-type': 'text/plain' })
res.end('hello world!')
})
server.listen(0, '127.0.0.1')
await once(server, 'listening')
const client = new Agent().compose([
dispatch => {
return (opts, handler) => {
++counter
const url = new URL(opts.origin)
switch (counter) {
case 1:
t.equal(isIP(url.hostname), 4)
break
case 2:
t.equal(isIP(url.hostname), 4)
break
case 3:
t.equal(isIP(url.hostname), 4)
break
default:
t.fail('should not reach this point')
}
return dispatch(opts, handler)
}
},
dns({
dualStack: false,
affinity: 4,
maxTTL: 400,
lookup: (origin, opts, cb) => {
++lookupCounter
lookup(
origin.hostname,
{ all: true, family: opts.affinity },
cb
)
}
})
])
after(async () => {
clock.uninstall()
await client.close()
server.close()
await once(server, 'close')
})
const response = await client.request({
...requestOptions,
origin: `http://localhost:${server.address().port}`
})
t.equal(response.statusCode, 200)
t.equal(await response.body.text(), 'hello world!')
clock.tick(200)
const response2 = await client.request({
...requestOptions,
origin: `http://localhost:${server.address().port}`
})
t.equal(response2.statusCode, 200)
t.equal(await response2.body.text(), 'hello world!')
clock.tick(300)
const response3 = await client.request({
...requestOptions,
origin: `http://localhost:${server.address().port}`
})
t.equal(response3.statusCode, 200)
t.equal(await response3.body.text(), 'hello world!')
t.equal(lookupCounter, 2)
})
test('Should we handle TTL (6)', async t => {
t = tspl(t, { plan: 10 })
const clock = FakeTimers.install()
let counter = 0
let lookupCounter = 0
const server = createServer({ joinDuplicateHeaders: true })
const requestOptions = {
method: 'GET',
path: '/',
headers: {
'content-type': 'application/json'
}
}
server.on('request', (req, res) => {
res.writeHead(200, { 'content-type': 'text/plain' })
res.end('hello world!')
})
server.listen(0, '::1')
await once(server, 'listening')
const client = new Agent().compose([
dispatch => {
return (opts, handler) => {
++counter
const url = new URL(opts.origin)
switch (counter) {
case 1:
// [::1] -> ::1
t.equal(isIP(url.hostname.slice(1, 4)), 6)
break
case 2:
// [::1] -> ::1
t.equal(isIP(url.hostname.slice(1, 4)), 6)
break
case 3:
// [::1] -> ::1
t.equal(isIP(url.hostname.slice(1, 4)), 6)
break
default:
t.fail('should not reach this point')
}
return dispatch(opts, handler)
}
},
dns({
dualStack: false,
affinity: 6,
maxTTL: 400,
lookup: (origin, opts, cb) => {
++lookupCounter
lookup(
origin.hostname,
{ all: true, family: opts.affinity },
cb
)
}
})
])
after(async () => {
clock.uninstall()
await client.close()
server.close()
await once(server, 'close')
})
const response = await client.request({
...requestOptions,
origin: `http://localhost:${server.address().port}`
})
t.equal(response.statusCode, 200)
t.equal(await response.body.text(), 'hello world!')
clock.tick(200)
const response2 = await client.request({
...requestOptions,
origin: `http://localhost:${server.address().port}`
})
t.equal(response2.statusCode, 200)
t.equal(await response2.body.text(), 'hello world!')
clock.tick(300)
const response3 = await client.request({
...requestOptions,
origin: `http://localhost:${server.address().port}`
})
t.equal(response3.statusCode, 200)
t.equal(await response3.body.text(), 'hello world!')
t.equal(lookupCounter, 2)
})
test('Should set lowest TTL between resolved and option maxTTL', async t => {
t = tspl(t, { plan: 9 })
const clock = FakeTimers.install()
let lookupCounter = 0
const server = createServer({ joinDuplicateHeaders: true })
const requestOptions = {
method: 'GET',
path: '/',
headers: {
'content-type': 'application/json'
}
}
server.on('request', (req, res) => {
res.writeHead(200, { 'content-type': 'text/plain' })
res.end('hello world!')
})
server.listen(0, '127.0.0.1')
await once(server, 'listening')
const client = new Agent().compose(
dns({
dualStack: false,
affinity: 4,
maxTTL: 200,
lookup: (origin, opts, cb) => {
++lookupCounter
cb(null, [
{
address: '127.0.0.1',
family: 4,
ttl: lookupCounter === 1 ? 50 : 500
}
])
}
})
)
after(async () => {
clock.uninstall()
await client.close()
server.close()
await once(server, 'close')
})
const response = await client.request({
...requestOptions,
origin: `http://localhost:${server.address().port}`
})
t.equal(response.statusCode, 200)
t.equal(await response.body.text(), 'hello world!')
clock.tick(100)
// 100ms: lookup since ttl = Math.min(50, maxTTL: 200)
const response2 = await client.request({
...requestOptions,
origin: `http://localhost:${server.address().port}`
})
t.equal(response2.statusCode, 200)
t.equal(await response2.body.text(), 'hello world!')
clock.tick(100)
// 100ms: cached since ttl = Math.min(500, maxTTL: 200)
const response3 = await client.request({
...requestOptions,
origin: `http://localhost:${server.address().port}`
})
t.equal(response3.statusCode, 200)
t.equal(await response3.body.text(), 'hello world!')
clock.tick(150)
// 250ms: lookup since ttl = Math.min(500, maxTTL: 200)
const response4 = await client.request({
...requestOptions,
origin: `http://localhost:${server.address().port}`
})
t.equal(response4.statusCode, 200)
t.equal(await response4.body.text(), 'hello world!')
t.equal(lookupCounter, 3)
})
test('Should use all dns entries (dual stack)', async t => {
t = tspl(t, { plan: 16 })
let counter = 0
let lookupCounter = 0
const server = createServer({ joinDuplicateHeaders: true })
const requestOptions = {
method: 'GET',
path: '/',
headers: {
'content-type': 'application/json'
}
}
server.on('request', (req, res) => {
res.writeHead(200, { 'content-type': 'text/plain' })
res.end('hello world!')
})
server.listen(0)
await once(server, 'listening')
const client = new Agent().compose([
dispatch => {
return (opts, handler) => {
++counter
const url = new URL(opts.origin)
switch (counter) {
case 1:
t.equal(url.hostname, '1.1.1.1')
break
case 2:
t.equal(url.hostname, '[::1]')
break
case 3:
t.equal(url.hostname, '2.2.2.2')
break
case 4:
t.equal(url.hostname, '[::2]')
break
case 5:
t.equal(url.hostname, '1.1.1.1')
break
default:
t.fail('should not reach this point')
}
url.hostname = '127.0.0.1'
opts.origin = url.toString()
return dispatch(opts, handler)
}
},
dns({
lookup (origin, opts, cb) {
lookupCounter++
cb(null, [
{ address: '::1', family: 6 },
{ address: '::2', family: 6 },
{ address: '1.1.1.1', family: 4 },
{ address: '2.2.2.2', family: 4 }
])
}
})
])
after(async () => {
await client.close()
server.close()
await once(server, 'close')
})
for (let i = 0; i < 5; i++) {
const response = await client.request({
...requestOptions,
origin: `http://localhost:${server.address().port}`
})
t.equal(response.statusCode, 200)
t.equal(await response.body.text(), 'hello world!')
}
t.equal(lookupCounter, 1)
})
test('Should use all dns entries (dual stack disabled - 4)', async t => {
t = tspl(t, { plan: 10 })
let counter = 0
let lookupCounter = 0
const server = createServer({ joinDuplicateHeaders: true })
const requestOptions = {
method: 'GET',
path: '/',
headers: {
'content-type': 'application/json'
}
}
server.on('request', (req, res) => {
res.writeHead(200, { 'content-type': 'text/plain' })
res.end('hello world!')
})
server.listen(0)
await once(server, 'listening')
const client = new Agent().compose([
dispatch => {
return (opts, handler) => {
++counter
const url = new URL(opts.origin)
switch (counter) {
case 1:
t.equal(url.hostname, '1.1.1.1')
break
case 2:
t.equal(url.hostname, '2.2.2.2')
break
case 3:
t.equal(url.hostname, '1.1.1.1')
break
default:
t.fail('should not reach this point')
}
url.hostname = '127.0.0.1'
opts.origin = url.toString()
return dispatch(opts, handler)
}
},
dns({
dualStack: false,
lookup (origin, opts, cb) {
lookupCounter++
cb(null, [
{ address: '1.1.1.1', family: 4 },
{ address: '2.2.2.2', family: 4 }
])
}
})
])
after(async () => {
await client.close()
server.close()
await once(server, 'close')
})
const response1 = await client.request({
...requestOptions,
origin: `http://localhost:${server.address().port}`
})
t.equal(response1.statusCode, 200)
t.equal(await response1.body.text(), 'hello world!')
const response2 = await client.request({
...requestOptions,
origin: `http://localhost:${server.address().port}`
})
t.equal(response2.statusCode, 200)
t.equal(await response2.body.text(), 'hello world!')
const response3 = await client.request({
...requestOptions,
origin: `http://localhost:${server.address().port}`
})
t.equal(response3.statusCode, 200)
t.equal(await response3.body.text(), 'hello world!')
t.equal(lookupCounter, 1)
})
test('Should use all dns entries (dual stack disabled - 6)', async t => {
t = tspl(t, { plan: 10 })
let counter = 0
let lookupCounter = 0
const server = createServer({ joinDuplicateHeaders: true })
const requestOptions = {
method: 'GET',
path: '/',
headers: {
'content-type': 'application/json'
}
}
server.on('request', (req, res) => {
res.writeHead(200, { 'content-type': 'text/plain' })
res.end('hello world!')
})
server.listen(0)
await once(server, 'listening')
const client = new Agent().compose([
dispatch => {
return (opts, handler) => {
++counter
const url = new URL(opts.origin)
switch (counter) {
case 1:
t.equal(url.hostname, '[::1]')
break
case 2:
t.equal(url.hostname, '[::2]')
break
case 3:
t.equal(url.hostname, '[::1]')
break
default:
t.fail('should not reach this point')
}
url.hostname = '127.0.0.1'
opts.origin = url.toString()
return dispatch(opts, handler)
}
},
dns({
dualStack: false,
affinity: 6,
lookup (origin, opts, cb) {
lookupCounter++
cb(null, [
{ address: '::1', family: 6 },
{ address: '::2', family: 6 }
])
}
})
])
after(async () => {
await client.close()
server.close()
await once(server, 'close')
})
const response1 = await client.request({
...requestOptions,
origin: `http://localhost:${server.address().port}`
})
t.equal(response1.statusCode, 200)
t.equal(await response1.body.text(), 'hello world!')
const response2 = await client.request({
...requestOptions,
origin: `http://localhost:${server.address().port}`
})
t.equal(response2.statusCode, 200)
t.equal(await response2.body.text(), 'hello world!')
const response3 = await client.request({
...requestOptions,
origin: `http://localhost:${server.address().port}`
})
t.equal(response3.statusCode, 200)
t.equal(await response3.body.text(), 'hello world!')
t.equal(lookupCounter, 1)
})
test('Should handle single family resolved (dual stack)', async t => {
t = tspl(t, { plan: 7 })
const clock = FakeTimers.install()
let counter = 0
let lookupCounter = 0
const server = createServer({ joinDuplicateHeaders: true })
const requestOptions = {
method: 'GET',
path: '/',
headers: {
'content-type': 'application/json'
}
}
server.on('request', (req, res) => {
res.writeHead(200, { 'content-type': 'text/plain' })
res.end('hello world!')
})
server.listen(0)
await once(server, 'listening')
const client = new Agent().compose([
dispatch => {
return (opts, handler) => {
++counter
const url = new URL(opts.origin)
switch (counter) {
case 1:
t.equal(isIP(url.hostname), 4)
break
case 2:
// [::1] -> ::1
t.equal(isIP(url.hostname.slice(1, 4)), 6)
break
default:
t.fail('should not reach this point')
}
return dispatch(opts, handler)
}
},
dns({
lookup (origin, opts, cb) {
lookupCounter++
if (lookupCounter === 1) {
cb(null, [
{ address: '127.0.0.1', family: 4, ttl: 50 }
])
} else {
cb(null, [
{ address: '::1', family: 6, ttl: 50 }
])
}
}
})
])
after(async () => {
clock.uninstall()
await client.close()
server.close()
await once(server, 'close')
})
const response = await client.request({
...requestOptions,
origin: `http://localhost:${server.address().port}`
})
t.equal(response.statusCode, 200)
t.equal(await response.body.text(), 'hello world!')
clock.tick(100)
const response2 = await client.request({
...requestOptions,
origin: `http://localhost:${server.address().port}`
})
t.equal(response2.statusCode, 200)
t.equal(await response2.body.text(), 'hello world!')
t.equal(lookupCounter, 2)
})
test('Should prefer affinity (dual stack - 4)', async t => {
t = tspl(t, { plan: 10 })
const clock = FakeTimers.install()
let counter = 0
let lookupCounter = 0
const server = createServer({ joinDuplicateHeaders: true })
const requestOptions = {
method: 'GET',
path: '/',
headers: {
'content-type': 'application/json'
}
}
server.on('request', (req, res) => {
res.writeHead(200, { 'content-type': 'text/plain' })
res.end('hello world!')
})
server.listen(0)
await once(server, 'listening')
const client = new Agent().compose([
dispatch => {
return (opts, handler) => {
++counter
const url = new URL(opts.origin)
switch (counter) {
case 1:
t.equal(url.hostname, '1.1.1.1')
break
case 2:
t.equal(url.hostname, '2.2.2.2')
break
case 3:
t.equal(url.hostname, '1.1.1.1')
break
default:
t.fail('should not reach this point')
}
url.hostname = '127.0.0.1'
opts.origin = url.toString()
return dispatch(opts, handler)
}
},
dns({
affinity: 4,
lookup (origin, opts, cb) {
lookupCounter++
cb(null, [
{ address: '1.1.1.1', family: 4 },
{ address: '2.2.2.2', family: 4 },
{ address: '::1', family: 6 },
{ address: '::2', family: 6 }
])
}
})
])
after(async () => {
clock.uninstall()
await client.close()
server.close()
await once(server, 'close')
})
const response = await client.request({
...requestOptions,
origin: `http://localhost:${server.address().port}`
})
t.equal(response.statusCode, 200)
t.equal(await response.body.text(), 'hello world!')
clock.tick(100)
const response2 = await client.request({
...requestOptions,
origin: `http://localhost:${server.address().port}`
})
t.equal(response2.statusCode, 200)
t.equal(await response2.body.text(), 'hello world!')
const response3 = await client.request({
...requestOptions,
origin: `http://localhost:${server.address().port}`
})
t.equal(response3.statusCode, 200)
t.equal(await response3.body.text(), 'hello world!')
t.equal(lookupCounter, 1)
})
test('Should prefer affinity (dual stack - 6)', async t => {
t = tspl(t, { plan: 10 })
const clock = FakeTimers.install()
let counter = 0
let lookupCounter = 0
const server = createServer({ joinDuplicateHeaders: true })
const requestOptions = {
method: 'GET',
path: '/',
headers: {
'content-type': 'application/json'
}
}
server.on('request', (req, res) => {
res.writeHead(200, { 'content-type': 'text/plain' })
res.end('hello world!')
})
server.listen(0)
await once(server, 'listening')
const client = new Agent().compose([
dispatch => {
return (opts, handler) => {
++counter
const url = new URL(opts.origin)
switch (counter) {
case 1:
t.equal(url.hostname, '[::1]')
break
case 2:
t.equal(url.hostname, '[::2]')
break
case 3:
t.equal(url.hostname, '[::1]')
break
default:
t.fail('should not reach this point')
}
url.hostname = '127.0.0.1'
opts.origin = url.toString()
return dispatch(opts, handler)
}
},
dns({
affinity: 6,
lookup (origin, opts, cb) {
lookupCounter++
cb(null, [
{ address: '1.1.1.1', family: 4 },
{ address: '2.2.2.2', family: 4 },
{ address: '::1', family: 6 },
{ address: '::2', family: 6 }
])
}
})
])
after(async () => {
clock.uninstall()
await client.close()
server.close()
await once(server, 'close')
})
const response = await client.request({
...requestOptions,
origin: `http://localhost:${server.address().port}`
})
t.equal(response.statusCode, 200)
t.equal(await response.body.text(), 'hello world!')
clock.tick(100)
const response2 = await client.request({
...requestOptions,
origin: `http://localhost:${server.address().port}`
})
t.equal(response2.statusCode, 200)
t.equal(await response2.body.text(), 'hello world!')
const response3 = await client.request({
...requestOptions,
origin: `http://localhost:${server.address().port}`
})
t.equal(response3.statusCode, 200)
t.equal(await response3.body.text(), 'hello world!')
t.equal(lookupCounter, 1)
})
test('Should use resolved ports (4)', async t => {
t = tspl(t, { plan: 5 })
let lookupCounter = 0
const server1 = createServer({ joinDuplicateHeaders: true })
const server2 = createServer({ joinDuplicateHeaders: true })
const requestOptions = {
method: 'GET',
path: '/',
headers: {
'content-type': 'application/json'
}
}
server1.on('request', (req, res) => {
res.writeHead(200, { 'content-type': 'text/plain' })
res.end('hello world!')
})
server1.listen(0)
server2.on('request', (req, res) => {
res.writeHead(200, { 'content-type': 'text/plain' })
res.end('hello world! (x2)')
})
server2.listen(0)
await Promise.all([once(server1, 'listening'), once(server2, 'listening')])
const client = new Agent().compose([
dns({
lookup (origin, opts, cb) {
lookupCounter++
cb(null, [
{ address: '127.0.0.1', family: 4, port: server1.address().port },
{ address: '127.0.0.1', family: 4, port: server2.address().port }
])
}
})
])
after(async () => {
await client.close()
server1.close()
server2.close()
await Promise.all([once(server1, 'close'), once(server2, 'close')])
})
const response = await client.request({
...requestOptions,
origin: 'http://localhost'
})
t.equal(response.statusCode, 200)
t.equal(await response.body.text(), 'hello world!')
const response2 = await client.request({
...requestOptions,
origin: 'http://localhost'
})
t.equal(response2.statusCode, 200)
t.equal(await response2.body.text(), 'hello world! (x2)')
t.equal(lookupCounter, 1)
})
test('Should use resolved ports (6)', async t => {
t = tspl(t, { plan: 5 })
let lookupCounter = 0
const server1 = createServer({ joinDuplicateHeaders: true })
const server2 = createServer({ joinDuplicateHeaders: true })
const requestOptions = {
method: 'GET',
path: '/',
headers: {
'content-type': 'application/json'
}
}
server1.on('request', (req, res) => {
res.writeHead(200, { 'content-type': 'text/plain' })
res.end('hello world!')
})
server1.listen(0, '::1')
server2.on('request', (req, res) => {
res.writeHead(200, { 'content-type': 'text/plain' })
res.end('hello world! (x2)')
})
server2.listen(0, '::1')
await Promise.all([once(server1, 'listening'), once(server2, 'listening')])
const client = new Agent().compose([
dns({
lookup (origin, opts, cb) {
lookupCounter++
cb(null, [
{ address: '::1', family: 6, port: server1.address().port },
{ address: '::1', family: 6, port: server2.address().port }
])
}
})
])
after(async () => {
await client.close()
server1.close()
server2.close()
await Promise.all([once(server1, 'close'), once(server2, 'close')])
})
const response = await client.request({
...requestOptions,
origin: 'http://localhost'
})
t.equal(response.statusCode, 200)
t.equal(await response.body.text(), 'hello world!')
const response2 = await client.request({
...requestOptions,
origin: 'http://localhost'
})
t.equal(response2.statusCode, 200)
t.equal(await response2.body.text(), 'hello world! (x2)')
t.equal(lookupCounter, 1)
})
test('Should handle max cached items', async t => {
t = tspl(t, { plan: 9 })
let counter = 0
const server1 = createServer({ joinDuplicateHeaders: true })
const server2 = createServer({ joinDuplicateHeaders: true })
const requestOptions = {
method: 'GET',
path: '/',
headers: {
'content-type': 'application/json'
}
}
server1.on('request', (req, res) => {
res.writeHead(200, { 'content-type': 'text/plain' })
res.end('hello world!')
})
server1.listen(0)
server2.on('request', (req, res) => {
res.writeHead(200, { 'content-type': 'text/plain' })
res.end('hello world! (x2)')
})
server2.listen(0)
await Promise.all([once(server1, 'listening'), once(server2, 'listening')])
const client = new Agent().compose([
dispatch => {
return (opts, handler) => {
++counter
const url = new URL(opts.origin)
switch (counter) {
case 1:
t.equal(isIP(url.hostname), 4)
break
case 2:
// [::1] -> ::1
t.equal(isIP(url.hostname.slice(1, 4)), 6)
break
case 3:
t.equal(url.hostname, 'developer.mozilla.org')
// Rewrite origin to avoid reaching internet
opts.origin = `http://127.0.0.1:${server2.address().port}`
break
default:
t.fails('should not reach this point')
}
return dispatch(opts, handler)
}
},
dns({
maxItems: 1,
lookup: (_origin, _opts, cb) => {
cb(null, [
{
address: '::1',
family: 6
},
{
address: '127.0.0.1',
family: 4
}
])
}
})
])
after(async () => {
await client.close()
server1.close()
server2.close()
await Promise.all([once(server1, 'close'), once(server2, 'close')])
})
const response = await client.request({
...requestOptions,
origin: `http://localhost:${server1.address().port}`
})
t.equal(response.statusCode, 200)
t.equal(await response.body.text(), 'hello world!')
const response2 = await client.request({
...requestOptions,
origin: `http://localhost:${server1.address().port}`
})
t.equal(response2.statusCode, 200)
t.equal(await response2.body.text(), 'hello world!')
const response3 = await client.request({
...requestOptions,
origin: 'https://developer.mozilla.org'
})
t.equal(response3.statusCode, 200)
t.equal(await response3.body.text(), 'hello world! (x2)')
})
test('Should support external storage', async t => {
t = tspl(t, { plan: 9 })
let counter = 0
const server1 = createServer({ joinDuplicateHeaders: true })
const server2 = createServer({ joinDuplicateHeaders: true })
const requestOptions = {
method: 'GET',
path: '/',
headers: {
'content-type': 'application/json'
}
}
server1.on('request', (req, res) => {
res.writeHead(200, { 'content-type': 'text/plain' })
res.end('hello world!')
})
server1.listen(0)
server2.on('request', (req, res) => {
res.writeHead(200, { 'content-type': 'text/plain' })
res.end('hello world! (x2)')
})
server2.listen(0)
await Promise.all([once(server1, 'listening'), once(server2, 'listening')])
const cache = new Map()
const storage = {
get (origin) {
return cache.get(origin)
},
set (origin, records) {
cache.set(origin, records)
},
delete (origin) {
cache.delete(origin)
},
// simulate internal DNSStorage behaviour with `maxItems: 1` parameter
full () {
return cache.size === 1
}
}
const client = new Agent().compose([
dispatch => {
return (opts, handler) => {
++counter
const url = new URL(opts.origin)
switch (counter) {
case 1:
t.equal(isIP(url.hostname), 4)
break
case 2:
// [::1] -> ::1
t.equal(isIP(url.hostname.slice(1, 4)), 6)
break
case 3:
t.equal(url.hostname, 'developer.mozilla.org')
// Rewrite origin to avoid reaching internet
opts.origin = `http://127.0.0.1:${server2.address().port}`
break
default:
t.fails('should not reach this point')
}
return dispatch(opts, handler)
}
},
dns({
storage,
lookup: (_origin, _opts, cb) => {
cb(null, [
{
address: '::1',
family: 6
},
{
address: '127.0.0.1',
family: 4
}
])
}
})
])
after(async () => {
await client.close()
server1.close()
server2.close()
await Promise.all([once(server1, 'close'), once(server2, 'close')])
})
const response = await client.request({
...requestOptions,
origin: `http://localhost:${server1.address().port}`
})
t.equal(response.statusCode, 200)
t.equal(await response.body.text(), 'hello world!')
const response2 = await client.request({
...requestOptions,
origin: `http://localhost:${server1.address().port}`
})
t.equal(response2.statusCode, 200)
t.equal(await response2.body.text(), 'hello world!')
const response3 = await client.request({
...requestOptions,
origin: 'https://developer.mozilla.org'
})
t.equal(response3.statusCode, 200)
t.equal(await response3.body.text(), 'hello world! (x2)')
})
test('retry once with dual-stack', async t => {
t = tspl(t, { plan: 2 })
const requestOptions = {
method: 'GET',
path: '/',
headers: {
'content-type': 'application/json'
}
}
let counter = 0
const client = new Agent().compose([
dispatch => {
return (opts, handler) => {
counter++
return dispatch(opts, handler)
}
},
dns({
lookup: (_origin, _opts, cb) => {
cb(null, [
{
address: '127.0.0.1',
port: 3669,
family: 4,
ttl: 1000
},
{
address: '::1',
port: 3669,
family: 6,
ttl: 1000
}
])
}
})
])
after(async () => {
await client.close()
})
await t.rejects(client.request({
...requestOptions,
origin: 'http://localhost'
}), 'ECONNREFUSED')
t.equal(counter, 2)
})
test('Should handle ENOTFOUND response error', async t => {
t = tspl(t, { plan: 3 })
let lookupCounter = 0
const requestOptions = {
method: 'GET',
path: '/',
origin: 'http://localhost'
}
const client = new Agent().compose([
dns({
lookup (origin, opts, cb) {
lookupCounter++
if (lookupCounter === 1) {
const err = new Error('test error')
err.code = 'ENOTFOUND'
cb(err)
} else {
// Causes InformationalError
cb(null, [])
}
}
})
])
after(async () => {
await client.close()
})
let error1
try {
await client.request(requestOptions)
} catch (err) {
error1 = err
}
t.equal(error1.code, 'ENOTFOUND')
// Test that the records in the dns interceptor were deleted after the
// previous request
let error2
try {
await client.request(requestOptions)
} catch (err) {
error2 = err
}
t.equal(error2.name, 'InformationalError')
t.equal(lookupCounter, 2)
})
test('#3937 - Handle host correctly', async t => {
t = tspl(t, { plan: 10 })
const hostsnames = []
const server = createServer({ joinDuplicateHeaders: true })
const requestOptions = {
method: 'GET',
path: '/',
headers: {
'content-type': 'application/json'
}
}
server.on('request', (req, res) => {
t.equal(req.headers.host, `localhost:${server.address().port}`)
res.writeHead(200, { 'content-type': 'text/plain' })
res.end('hello world!')
})
server.listen(0)
await once(server, 'listening')
const client = new Agent().compose([
dispatch => {
return (opts, handler) => {
const url = new URL(opts.origin)
t.equal(hostsnames.includes(url.hostname), false)
if (url.hostname[0] === '[') {
// [::1] -> ::1
t.equal(isIP(url.hostname.slice(1, 4)), 6)
} else {
t.equal(isIP(url.hostname), 4)
}
hostsnames.push(url.hostname)
return dispatch(opts, handler)
}
},
dns({
lookup: (_origin, _opts, cb) => {
cb(null, [
{
address: '::1',
family: 6
},
{
address: '127.0.0.1',
family: 4
}
])
}
})
])
after(async () => {
await client.close()
server.close()
await once(server, 'close')
})
const response = await client.request({
...requestOptions,
origin: `http://localhost:${server.address().port}`
})
t.equal(response.statusCode, 200)
t.equal(await response.body.text(), 'hello world!')
const response2 = await client.request({
...requestOptions,
origin: `http://localhost:${server.address().port}`
})
t.equal(response2.statusCode, 200)
t.equal(await response2.body.text(), 'hello world!')
})
test('#4444 - Should preserve tuple-style headers', async t => {
t = tspl(t, { plan: 5 })
const server = createServer({ joinDuplicateHeaders: true })
server.on('request', (req, res) => {
t.equal(req.headers.host, `localhost:${server.address().port}`)
t.equal(req.headers.foo, 'bar')
t.equal(req.headers['0'], undefined)
res.writeHead(200, { 'content-type': 'text/plain' })
res.end('hello world!')
})
server.listen(0)
await once(server, 'listening')
const client = new Agent().compose([
dns({
lookup: (_origin, _opts, cb) => {
cb(null, [
{
address: '::1',
family: 6
},
{
address: '127.0.0.1',
family: 4
}
])
}
})
])
after(async () => {
await client.close()
server.close()
await once(server, 'close')
})
const response = await request(`http://localhost:${server.address().port}`, {
dispatcher: client,
headers: [['foo', 'bar']]
})
t.equal(response.statusCode, 200)
t.equal(await response.body.text(), 'hello world!')
})
test('#4444 - Should preserve iterable headers', async t => {
t = tspl(t, { plan: 5 })
const server = createServer({ joinDuplicateHeaders: true })
server.on('request', (req, res) => {
t.equal(req.headers.host, `localhost:${server.address().port}`)
t.equal(req.headers.foo, 'bar')
t.equal(req.headers['0'], undefined)
res.writeHead(200, { 'content-type': 'text/plain' })
res.end('hello world!')
})
server.listen(0)
await once(server, 'listening')
const client = new Agent().compose([
dns({
lookup: (_origin, _opts, cb) => {
cb(null, [
{
address: '::1',
family: 6
},
{
address: '127.0.0.1',
family: 4
}
])
}
})
])
after(async () => {
await client.close()
server.close()
await once(server, 'close')
})
const response = await request(`http://localhost:${server.address().port}`, {
dispatcher: client,
headers: new Map([['foo', 'bar']])
})
t.equal(response.statusCode, 200)
t.equal(await response.body.text(), 'hello world!')
})
test('#3951 - Should handle lookup errors correctly', async t => {
const suite = tspl(t, { plan: 1 })
const requestOptions = {
method: 'GET',
path: '/',
headers: {
'content-type': 'application/json'
}
}
const client = new Agent().compose([
dns({
lookup: (_origin, _opts, cb) => {
cb(new Error('lookup error'))
}
})
])
suite.rejects(client.request({
...requestOptions,
origin: 'http://localhost'
}), new Error('lookup error'))
})
================================================
FILE: test/interceptors/dump-interceptor.js
================================================
'use strict'
const { platform } = require('node:os')
const { test, after } = require('node:test')
const { createServer } = require('node:http')
const { once } = require('node:events')
const { tspl } = require('@matteo.collina/tspl')
const { Client, Agent, interceptors } = require('../..')
const { dump } = interceptors
// TODO: Fix tests on windows
const skip = platform() === 'win32'
test('Should handle preemptive network error', { skip }, async t => {
t = tspl(t, { plan: 4 })
let offset = 0
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
const max = 1024 * 1024
const buffer = Buffer.alloc(max)
res.writeHead(200, {
'Content-Length': buffer.length,
'Content-Type': 'application/octet-stream'
})
const interval = setInterval(() => {
offset += 256
const chunk = buffer.subarray(offset - 256, offset)
if (offset === max) {
clearInterval(interval)
res.end(chunk)
return
}
res.write(chunk)
}, 0)
})
const requestOptions = {
method: 'GET',
path: '/'
}
const client = new Agent().compose(dump({ maxSize: 1024 * 1024 }))
after(async () => {
await client.close()
server.close()
await once(server, 'close')
})
try {
await client.request({
origin: 'http://localhost',
...requestOptions
})
} catch (error) {
t.equal(error.code, 'ECONNREFUSED')
}
server.listen(0)
await once(server, 'listening')
const response = await client.request({
origin: `http://localhost:${server.address().port}`,
...requestOptions
})
const body = await response.body.text()
t.equal(response.headers['content-length'], `${1024 * 1024}`)
t.equal(response.statusCode, 200)
t.equal(body, '')
await t.completed
})
test('Should dump on abort', { skip }, async t => {
t = tspl(t, { plan: 2 })
let offset = 0
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
const max = 1024 * 1024
const buffer = Buffer.alloc(max)
res.writeHead(200, {
'Content-Type': 'application/octet-stream'
})
const interval = setInterval(() => {
offset += 256
const chunk = buffer.subarray(offset - 256, offset)
if (offset === max) {
clearInterval(interval)
res.end(chunk)
return
}
res.write(chunk)
}, 0)
})
const abc = new AbortController()
const requestOptions = {
method: 'GET',
path: '/',
signal: abc.signal
}
server.listen(0)
await once(server, 'listening')
const client = new Client(
`http://localhost:${server.address().port}`
).compose(dump({ maxSize: 512 }))
after(async () => {
await client.close()
server.close()
await once(server, 'close')
})
const response = await client.request(requestOptions)
abc.abort()
try {
await response.body.text()
} catch (error) {
t.equal(response.statusCode, 200)
t.equal(error.name, 'AbortError')
}
await t.completed
})
test('Should dump on already aborted request', { skip }, async t => {
t = tspl(t, { plan: 3 })
let offset = 0
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
const max = 1024
const buffer = Buffer.alloc(max)
res.writeHead(200, {
'Content-Type': 'application/octet-stream'
})
res.once('close', () => {
t.equal(offset, 1024)
})
const interval = setInterval(() => {
offset += 256
const chunk = buffer.subarray(offset - 256, offset)
if (offset === max) {
clearInterval(interval)
res.end(chunk)
return
}
res.write(chunk)
}, 0)
})
const abc = new AbortController()
const requestOptions = {
method: 'GET',
path: '/',
signal: abc.signal
}
server.listen(0)
await once(server, 'listening')
const client = new Client(
`http://localhost:${server.address().port}`
).compose(dump({ maxSize: 512 }))
after(async () => {
await client.close()
server.close()
await once(server, 'close')
})
abc.abort()
client.request(requestOptions).catch(err => {
t.equal(err.name, 'AbortError')
t.equal(err.message, 'This operation was aborted')
})
await t.completed
})
test('Should dump response body up to limit (default)', { skip }, async t => {
t = tspl(t, { plan: 3 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
const buffer = Buffer.alloc(1024 * 1024)
res.writeHead(200, {
'Content-Length': buffer.length,
'Content-Type': 'application/octet-stream'
})
res.end(buffer)
})
const requestOptions = {
method: 'GET',
path: '/'
}
server.listen(0)
await once(server, 'listening')
const client = new Client(
`http://localhost:${server.address().port}`
).compose(dump())
after(async () => {
await client.close()
server.close()
await once(server, 'close')
})
const response = await client.request(requestOptions)
const body = await response.body.text()
t.equal(response.statusCode, 200)
t.equal(response.headers['content-length'], `${1024 * 1024}`)
t.equal(body, '')
await t.completed
})
test('Should dump response body up to limit and ignore trailers', { skip }, async t => {
t = tspl(t, { plan: 3 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.writeHead(200, {
'Content-Type': 'text/plain',
'Transfer-Encoding': 'chunked',
Trailer: 'X-Foo'
})
res.write(Buffer.alloc(1024 * 1024).toString('utf-8'))
res.addTrailers({ 'X-Foo': 'bar' })
res.end()
})
const requestOptions = {
method: 'GET',
path: '/'
}
server.listen(0)
await once(server, 'listening')
const client = new Client(
`http://localhost:${server.address().port}`
).compose(dump())
after(async () => {
await client.close()
server.close()
await once(server, 'close')
})
const response = await client.request(requestOptions)
const body = await response.body.text()
t.equal(response.statusCode, 200)
t.equal(body, '')
t.equal(response.trailers['x-foo'], undefined)
await t.completed
})
test('Should forward common error', { skip }, async t => {
t = tspl(t, { plan: 1 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.destroy()
})
const requestOptions = {
method: 'GET',
path: '/'
}
server.listen(0)
await once(server, 'listening')
const client = new Client(
`http://localhost:${server.address().port}`
).compose(dump())
after(async () => {
await client.close()
server.close()
await once(server, 'close')
})
await t.rejects(client.request.bind(client, requestOptions), {
name: 'SocketError',
code: 'UND_ERR_SOCKET',
message: 'other side closed'
})
await t.completed
})
test('Should throw on bad opts', { skip }, async t => {
t = tspl(t, { plan: 6 })
t.throws(
() => {
new Client('http://localhost').compose(dump({ maxSize: {} })).dispatch(
{
method: 'GET',
path: '/'
},
{}
)
},
{
name: 'InvalidArgumentError',
message: 'maxSize must be a number greater than 0'
}
)
t.throws(
() => {
new Client('http://localhost').compose(dump({ maxSize: '0' })).dispatch(
{
method: 'GET',
path: '/'
},
{}
)
},
{
name: 'InvalidArgumentError',
message: 'maxSize must be a number greater than 0'
}
)
t.throws(
() => {
new Client('http://localhost').compose(dump({ maxSize: -1 })).dispatch(
{
method: 'GET',
path: '/'
},
{}
)
},
{
name: 'InvalidArgumentError',
message: 'maxSize must be a number greater than 0'
}
)
t.throws(
() => {
new Client('http://localhost').compose(dump()).dispatch(
{
method: 'GET',
path: '/',
dumpMaxSize: {}
},
{}
)
},
{
name: 'InvalidArgumentError',
message: 'maxSize must be a number greater than 0'
}
)
t.throws(
() => {
new Client('http://localhost').compose(dump()).dispatch(
{
method: 'GET',
path: '/',
dumpMaxSize: '0'
},
{}
)
},
{
name: 'InvalidArgumentError',
message: 'maxSize must be a number greater than 0'
}
)
t.throws(
() => {
new Client('http://localhost').compose(dump()).dispatch(
{
method: 'GET',
path: '/',
dumpMaxSize: -1
},
{}
)
},
{
name: 'InvalidArgumentError',
message: 'maxSize must be a number greater than 0'
}
)
})
test('Should dump response body up to limit (opts)', { skip }, async t => {
t = tspl(t, { plan: 3 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
const buffer = Buffer.alloc(1 * 1024)
res.writeHead(200, {
'Content-Length': buffer.length,
'Content-Type': 'application/octet-stream'
})
res.end(buffer)
})
const requestOptions = {
method: 'GET',
path: '/'
}
server.listen(0)
await once(server, 'listening')
const client = new Client(
`http://localhost:${server.address().port}`
).compose(dump({ maxSize: 1 * 1024 }))
after(async () => {
await client.close()
server.close()
await once(server, 'close')
})
const response = await client.request(requestOptions)
const body = await response.body.text()
t.equal(response.statusCode, 200)
t.equal(response.headers['content-length'], `${1 * 1024}`)
t.equal(body, '')
await t.completed
})
test('Should abort if content length grater than max size', { skip }, async t => {
t = tspl(t, { plan: 1 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
const buffer = Buffer.alloc(2 * 1024)
res.writeHead(200, {
'Content-Length': buffer.length,
'Content-Type': 'application/octet-stream'
})
res.end(buffer)
})
const requestOptions = {
method: 'GET',
path: '/'
}
server.listen(0)
await once(server, 'listening')
const client = new Client(
`http://localhost:${server.address().port}`
).compose(dump({ maxSize: 1 * 1024, abortOnDumped: false }))
after(async () => {
await client.close()
server.close()
await once(server, 'close')
})
t.rejects(client.request(requestOptions), {
name: 'AbortError',
message: 'Response size (2048) larger than maxSize (1024)'
})
await t.completed
})
test('Should dump response body up to limit (dispatch opts)', { skip }, async t => {
t = tspl(t, { plan: 3 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
const buffer = Buffer.alloc(1 * 1024)
res.writeHead(200, {
'Content-Length': buffer.length,
'Content-Type': 'application/octet-stream'
})
res.end(buffer)
})
const requestOptions = {
method: 'GET',
path: '/',
dumpMaxSize: 1 * 1024,
abortOnDumped: false
}
server.listen(0)
await once(server, 'listening')
const client = new Client(
`http://localhost:${server.address().port}`
).compose(dump())
after(async () => {
await client.close()
server.close()
await once(server, 'close')
})
const response = await client.request(requestOptions)
const body = await response.body.text()
t.equal(response.statusCode, 200)
t.equal(response.headers['content-length'], `${1 * 1024}`)
t.equal(body, '')
await t.completed
})
test('Should abort if content length grater than max size (dispatch opts)', { skip }, async t => {
t = tspl(t, { plan: 1 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
const buffer = Buffer.alloc(2 * 1024)
res.writeHead(200, {
'Content-Length': buffer.length,
'Content-Type': 'application/octet-stream'
})
res.end(buffer)
})
const requestOptions = {
method: 'GET',
path: '/',
dumpMaxSize: 100
}
server.listen(0)
await once(server, 'listening')
const client = new Client(
`http://localhost:${server.address().port}`
).compose(dump())
after(async () => {
await client.close()
server.close()
await once(server, 'close')
})
await t.rejects(
async () => {
return await client.request(requestOptions).then(res => res.body.text())
},
{
name: 'AbortError',
message: 'Response size (2048) larger than maxSize (100)'
}
)
await t.completed
})
================================================
FILE: test/interceptors/redirect-cross-origin-fix.js
================================================
'use strict'
const { test, after } = require('node:test')
const { createServer } = require('node:http')
const { once } = require('node:events')
const { tspl } = require('@matteo.collina/tspl')
const undici = require('../..')
const {
interceptors: { redirect }
} = undici
test('Client should throw redirect loop error for cross-origin redirect', async (t) => {
t = tspl(t, { plan: 2 })
const serverA = createServer((req, res) => {
res.writeHead(301, {
Location: 'http://localhost:9999/target' // Different port = cross-origin
})
res.end()
})
serverA.listen(0)
after(() => serverA.close())
await once(serverA, 'listening')
const client = new undici.Client(`http://localhost:${serverA.address().port}`).compose(
redirect({ maxRedirections: 2 }) // Keep low to avoid long waits
)
after(() => client.close())
try {
await client.request({
method: 'GET',
path: '/test'
})
t.fail('Expected error but request succeeded')
} catch (error) {
t.ok(error.message.includes('Redirect loop detected'), 'Error message indicates redirect loop')
t.ok(error.message.includes('Client or Pool'), 'Error message mentions Client or Pool')
}
await t.completed
})
test('Pool should throw redirect loop error for cross-origin redirect', async (t) => {
t = tspl(t, { plan: 2 })
const serverA = createServer((req, res) => {
res.writeHead(301, {
Location: 'http://localhost:9998/target' // Different port = cross-origin
})
res.end()
})
serverA.listen(0)
after(() => serverA.close())
await once(serverA, 'listening')
const pool = new undici.Pool(`http://localhost:${serverA.address().port}`).compose(
redirect({ maxRedirections: 2 }) // Keep low to avoid long waits
)
after(() => pool.close())
try {
await pool.request({
method: 'GET',
path: '/test'
})
t.fail('Expected error but request succeeded')
} catch (error) {
t.ok(error.message.includes('Redirect loop detected'), 'Error message indicates redirect loop')
t.ok(error.message.includes('Client or Pool'), 'Error message mentions Client or Pool')
}
await t.completed
})
test('Agent should successfully follow cross-origin redirect', async (t) => {
t = tspl(t, { plan: 2 })
const serverB = createServer((req, res) => {
res.writeHead(200)
res.end('Cross-origin redirect success')
})
const serverA = createServer((req, res) => {
res.writeHead(301, {
Location: `http://localhost:${serverB.address().port}/success`
})
res.end()
})
serverA.listen(0)
serverB.listen(0)
after(() => {
serverA.close()
serverB.close()
})
await Promise.all([
once(serverA, 'listening'),
once(serverB, 'listening')
])
const agent = new undici.Agent().compose(
redirect({ maxRedirections: 2 })
)
after(() => agent.close())
const response = await agent.request({
origin: `http://localhost:${serverA.address().port}`,
method: 'GET',
path: '/test'
})
const body = await response.body.text()
t.strictEqual(response.statusCode, 200, 'Response has 200 status code')
t.ok(body.includes('Cross-origin redirect success'), 'Response body indicates successful cross-origin redirect')
await t.completed
})
================================================
FILE: test/interceptors/redirect-issue-3803.js
================================================
'use strict'
const { FormData, request, Agent, interceptors } = require('../..')
const { test } = require('node:test')
const { createServer } = require('node:http')
const { once } = require('node:events')
const { tspl } = require('@matteo.collina/tspl')
test('redirecting works with a FormData body', async (t) => {
const plan = tspl(t, { plan: 1 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
if (req.url === '/1') {
res.writeHead(302, undefined, { location: '/2' })
res.end()
} else {
res.end('OK')
}
}).listen(0)
t.after(() => server.close())
await once(server, 'listening')
const agent = new Agent().compose(interceptors.redirect({ maxRedirections: 1 }))
const body = new FormData()
body.append('hello', 'world')
const { context } = await request(`http://localhost:${server.address().port}/1`, {
body,
method: 'POST',
dispatcher: agent
})
plan.deepStrictEqual(context.history, [
new URL(`http://localhost:${server.address().port}/1`),
new URL(`http://localhost:${server.address().port}/2`)
])
})
================================================
FILE: test/interceptors/redirect.js
================================================
'use strict'
const { test, after } = require('node:test')
const { createServer } = require('node:http')
const { once } = require('node:events')
const { tspl } = require('@matteo.collina/tspl')
const undici = require('../..')
const {
startRedirectingServer,
startRedirectingWithBodyServer,
startRedirectingChainServers,
startRedirectingWithoutLocationServer,
startRedirectingWithAuthorization,
startRedirectingWithCookie,
startRedirectingWithQueryParams,
startServer,
startRedirectingWithRelativePath
} = require('../utils/redirecting-servers')
const { createReadable, createReadableStream } = require('../utils/stream')
const {
interceptors: { redirect }
} = undici
for (const factory of [
(server, opts) =>
new undici.Agent(opts).compose(
redirect({ maxRedirections: opts?.maxRedirections })
),
(server, opts) =>
new undici.Pool(`http://${server}`, opts).compose(
redirect({ maxRedirections: opts?.maxRedirections })
),
(server, opts) =>
new undici.Client(`http://${server}`, opts).compose(
redirect({ maxRedirections: opts?.maxRedirections })
)
]) {
const request = (t, server, opts, ...args) => {
const dispatcher = factory(server, opts)
after(() => dispatcher.close())
return undici.request(args[0], { ...args[1], dispatcher }, args[2])
}
test('should always have a history with the final URL even if no redirections were followed', async t => {
t = tspl(t, { plan: 4 })
const server = await startRedirectingServer()
const {
statusCode,
headers,
body: bodyStream,
context: { history }
} = await request(t, server, undefined, `http://${server}/200?key=value`, {
maxRedirections: 10
})
const body = await bodyStream.text()
t.strictEqual(statusCode, 200)
t.ok(!headers.location)
t.deepStrictEqual(
history.map(x => x.toString()),
[`http://${server}/200?key=value`]
)
t.strictEqual(
body,
`GET /5 key=value :: host@${server} connection@keep-alive`
)
await t.completed
})
test('should not follow redirection by default if not using RedirectAgent', async t => {
t = tspl(t, { plan: 3 })
const server = await startRedirectingServer()
const {
statusCode,
headers,
body: bodyStream
} = await request(t, server, undefined, `http://${server}`)
const body = await bodyStream.text()
t.strictEqual(statusCode, 302)
t.strictEqual(headers.location, `http://${server}/302/1`)
t.strictEqual(body.length, 0)
await t.completed
})
test('should follow redirection after a HTTP 300', async t => {
t = tspl(t, { plan: 4 })
const server = await startRedirectingServer()
const {
statusCode,
headers,
body: bodyStream,
context: { history }
} = await request(t, server, undefined, `http://${server}/300?key=value`, {
maxRedirections: 10
})
const body = await bodyStream.text()
t.strictEqual(statusCode, 200)
t.ok(!headers.location)
t.deepStrictEqual(
history.map(x => x.toString()),
[
`http://${server}/300?key=value`,
`http://${server}/300/1?key=value`,
`http://${server}/300/2?key=value`,
`http://${server}/300/3?key=value`,
`http://${server}/300/4?key=value`,
`http://${server}/300/5?key=value`
]
)
t.strictEqual(
body,
`GET /5 key=value :: host@${server} connection@keep-alive`
)
await t.completed
})
test('should follow redirection after a HTTP 300 default', async t => {
t = tspl(t, { plan: 4 })
const server = await startRedirectingServer()
const {
statusCode,
headers,
body: bodyStream,
context: { history }
} = await request(
t,
server,
{ maxRedirections: 10 },
`http://${server}/300?key=value`
)
const body = await bodyStream.text()
t.strictEqual(statusCode, 200)
t.ok(!headers.location)
t.deepStrictEqual(
history.map(x => x.toString()),
[
`http://${server}/300?key=value`,
`http://${server}/300/1?key=value`,
`http://${server}/300/2?key=value`,
`http://${server}/300/3?key=value`,
`http://${server}/300/4?key=value`,
`http://${server}/300/5?key=value`
]
)
t.strictEqual(
body,
`GET /5 key=value :: host@${server} connection@keep-alive`
)
await t.completed
})
test('should follow redirection after a HTTP 301 changing method to GET', async t => {
t = tspl(t, { plan: 3 })
const server = await startRedirectingServer()
const {
statusCode,
headers,
body: bodyStream
} = await request(t, server, undefined, `http://${server}/301`, {
method: 'POST',
body: 'REQUEST',
maxRedirections: 10
})
const body = await bodyStream.text()
t.strictEqual(statusCode, 200)
t.ok(!headers.location)
t.strictEqual(
body,
`GET /5 :: host@${server} connection@keep-alive`
)
})
test('should follow redirection after a HTTP 302', async t => {
t = tspl(t, { plan: 3 })
const server = await startRedirectingServer()
const {
statusCode,
headers,
body: bodyStream
} = await request(t, server, undefined, `http://${server}/302`, {
method: 'PUT',
body: Buffer.from('REQUEST'),
maxRedirections: 10
})
const body = await bodyStream.text()
t.strictEqual(statusCode, 200)
t.ok(!headers.location)
t.strictEqual(
body,
`PUT /5 :: host@${server} connection@keep-alive content-length@7 :: REQUEST`
)
})
test('should follow redirection after a HTTP 303 changing method to GET', async t => {
t = tspl(t, { plan: 3 })
const server = await startRedirectingServer()
const {
statusCode,
headers,
body: bodyStream
} = await request(t, server, undefined, `http://${server}/303`, {
method: 'PATCH',
body: 'REQUEST',
maxRedirections: 10
})
const body = await bodyStream.text()
t.strictEqual(statusCode, 200)
t.ok(!headers.location)
t.strictEqual(body, `GET /5 :: host@${server} connection@keep-alive`)
await t.completed
})
test('should remove Host and request body related headers when following HTTP 303 (array)', async t => {
t = tspl(t, { plan: 3 })
const server = await startRedirectingServer()
const {
statusCode,
headers,
body: bodyStream
} = await request(t, server, undefined, `http://${server}/303`, {
method: 'PATCH',
headers: [
'Content-Encoding',
'gzip',
'X-Foo1',
'1',
'X-Foo2',
'2',
'Content-Type',
'application/json',
'X-Foo3',
'3',
'Host',
'localhost',
'X-Bar',
'4'
],
maxRedirections: 10
})
const body = await bodyStream.text()
t.strictEqual(statusCode, 200)
t.ok(!headers.location)
t.strictEqual(
body,
`GET /5 :: host@${server} connection@keep-alive x-foo1@1 x-foo2@2 x-foo3@3 x-bar@4`
)
await t.completed
})
test('should remove Host and request body related headers when following HTTP 303 (object)', async t => {
t = tspl(t, { plan: 3 })
const server = await startRedirectingServer()
const {
statusCode,
headers,
body: bodyStream
} = await request(t, server, undefined, `http://${server}/303`, {
method: 'PATCH',
headers: {
'Content-Encoding': 'gzip',
'X-Foo1': '1',
'X-Foo2': '2',
'Content-Type': 'application/json',
'X-Foo3': '3',
Host: 'localhost',
'X-Bar': '4'
},
maxRedirections: 10
})
const body = await bodyStream.text()
t.strictEqual(statusCode, 200)
t.ok(!headers.location)
t.strictEqual(
body,
`GET /5 :: host@${server} connection@keep-alive x-foo1@1 x-foo2@2 x-foo3@3 x-bar@4`
)
await t.completed
})
test('should follow redirection after a HTTP 307', async t => {
t = tspl(t, { plan: 3 })
const server = await startRedirectingServer()
const {
statusCode,
headers,
body: bodyStream
} = await request(t, server, undefined, `http://${server}/307`, {
method: 'DELETE',
maxRedirections: 10
})
const body = await bodyStream.text()
t.strictEqual(statusCode, 200)
t.ok(!headers.location)
t.strictEqual(body, `DELETE /5 :: host@${server} connection@keep-alive`)
await t.completed
})
test('should follow redirection after a HTTP 308', async t => {
t = tspl(t, { plan: 3 })
const server = await startRedirectingServer()
const {
statusCode,
headers,
body: bodyStream
} = await request(t, server, undefined, `http://${server}/308`, {
method: 'OPTIONS',
maxRedirections: 10
})
const body = await bodyStream.text()
t.strictEqual(statusCode, 200)
t.ok(!headers.location)
t.strictEqual(body, `OPTIONS /5 :: host@${server} connection@keep-alive`)
await t.completed
})
test('should ignore HTTP 3xx response bodies', async t => {
t = tspl(t, { plan: 4 })
const server = await startRedirectingWithBodyServer()
const {
statusCode,
headers,
body: bodyStream,
context: { history }
} = await request(t, server, undefined, `http://${server}/`, {
maxRedirections: 10
})
const body = await bodyStream.text()
t.strictEqual(statusCode, 200)
t.ok(!headers.location)
t.deepStrictEqual(
history.map(x => x.toString()),
[`http://${server}/`, `http://${server}/end`]
)
t.strictEqual(body, 'FINAL')
await t.completed
})
test('should ignore query after redirection', async t => {
t = tspl(t, { plan: 3 })
const server = await startRedirectingWithQueryParams()
const {
statusCode,
headers,
context: { history }
} = await request(t, server, undefined, `http://${server}/`, {
maxRedirections: 10,
query: { param1: 'first' }
})
t.strictEqual(statusCode, 200)
t.ok(!headers.location)
t.deepStrictEqual(
history.map(x => x.toString()),
[`http://${server}/`, `http://${server}/?param2=second`]
)
await t.completed
})
test('should follow a redirect chain up to the allowed number of times', async t => {
t = tspl(t, { plan: 4 })
const server = await startRedirectingServer()
const {
statusCode,
headers,
body: bodyStream,
context: { history }
} = await request(t, server, undefined, `http://${server}/300`, {
maxRedirections: 2
})
const body = await bodyStream.text()
t.strictEqual(statusCode, 300)
t.strictEqual(headers.location, `http://${server}/300/3`)
t.deepStrictEqual(
history.map(x => x.toString()),
[
`http://${server}/300`,
`http://${server}/300/1`,
`http://${server}/300/2`
]
)
t.strictEqual(body.length, 0)
await t.completed
})
test('should follow a redirect chain up to the allowed number of times for redirectionLimitReached', async t => {
t = tspl(t, { plan: 1 })
const server = await startRedirectingServer()
try {
await request(t, server, undefined, `http://${server}/300`, {
maxRedirections: 2,
throwOnMaxRedirect: true
})
} catch (error) {
if (error.message.startsWith('max redirects')) {
t.ok(true, 'Max redirects handled correctly')
} else {
t.fail(`Unexpected error: ${error.message}`)
}
}
await t.completed
})
test('when a Location response header is NOT present', async t => {
t = tspl(t, { plan: 6 * 3 })
const redirectCodes = [300, 301, 302, 303, 307, 308]
const server = await startRedirectingWithoutLocationServer()
for (const code of redirectCodes) {
const {
statusCode,
headers,
body: bodyStream
} = await request(t, server, undefined, `http://${server}/${code}`, {
maxRedirections: 10
})
const body = await bodyStream.text()
t.strictEqual(statusCode, code)
t.ok(!headers.location)
t.strictEqual(body.length, 0)
}
await t.completed
})
test('should not allow invalid maxRedirections arguments', async t => {
t = tspl(t, { plan: 1 })
try {
await request(t, 'localhost', undefined, 'http://localhost', {
method: 'GET',
maxRedirections: 'INVALID'
})
t.fail('Did not throw')
} catch (err) {
t.strictEqual(err.message, 'maxRedirections must be a positive number')
}
await t.completed
})
test('should not allow invalid maxRedirections arguments default', async t => {
t = tspl(t, { plan: 1 })
try {
await request(
t,
'localhost',
{
maxRedirections: 'INVALID'
},
'http://localhost',
{
method: 'GET'
}
)
t.fail('Did not throw')
} catch (err) {
t.strictEqual(err.message, 'maxRedirections must be a positive number')
}
await t.completed
})
test('should not follow redirects when using ReadableStream request bodies', async t => {
t = tspl(t, { plan: 3 })
const server = await startRedirectingServer()
const {
statusCode,
headers,
body: bodyStream
} = await request(t, server, undefined, `http://${server}/301`, {
method: 'PUT',
body: createReadableStream('REQUEST'),
maxRedirections: 10
})
const body = await bodyStream.text()
t.strictEqual(statusCode, 301)
t.strictEqual(headers.location, `http://${server}/301/2`)
t.strictEqual(body.length, 0)
await t.completed
})
test('should not follow redirects when using Readable request bodies', async t => {
t = tspl(t, { plan: 3 })
const server = await startRedirectingServer()
const {
statusCode,
headers,
body: bodyStream
} = await request(t, server, undefined, `http://${server}/301`, {
method: 'PUT',
body: createReadable('REQUEST'),
maxRedirections: 10
})
const body = await bodyStream.text()
t.strictEqual(statusCode, 301)
t.strictEqual(headers.location, `http://${server}/301/1`)
t.strictEqual(body.length, 0)
await t.completed
})
test('should follow redirects when using Readable request bodies w/ POST 101', async t => {
t = tspl(t, { plan: 1 })
const server = await startRedirectingServer()
const {
statusCode,
body: bodyStream
} = await request(t, server, undefined, `http://${server}/301`, {
method: 'POST',
body: createReadable('REQUEST'),
maxRedirections: 10
})
await bodyStream.text()
t.strictEqual(statusCode, 200)
await t.completed
})
}
test('should follow redirections when going cross origin', async t => {
t = tspl(t, { plan: 4 })
const [server1, server2, server3] = await startRedirectingChainServers()
const {
statusCode,
headers,
body: bodyStream,
context: { history }
} = await undici.request(`http://${server1}`, {
method: 'POST',
dispatcher: new undici.Agent({}).compose(redirect({ maxRedirections: 10 }))
})
const body = await bodyStream.text()
t.strictEqual(statusCode, 200)
t.ok(!headers.location)
t.deepStrictEqual(
history.map(x => x.toString()),
[
`http://${server1}/`,
`http://${server2}/`,
`http://${server3}/`,
`http://${server2}/end`,
`http://${server3}/end`,
`http://${server1}/end`
]
)
t.strictEqual(body, 'GET')
await t.completed
})
test('should handle errors (callback)', async t => {
t = tspl(t, { plan: 1 })
undici.request(
'http://localhost:0',
{
dispatcher: new undici.Agent({}).compose(
redirect({ maxRedirections: 10 })
)
},
error => {
t.match(error.code, /EADDRNOTAVAIL|ECONNREFUSED/)
}
)
await t.completed
})
test('should handle errors (promise)', async t => {
t = tspl(t, { plan: 1 })
try {
await undici.request('http://localhost:0', {
dispatcher: new undici.Agent({}).compose(
redirect({ maxRedirections: 10 })
)
})
t.fail('Did not throw')
} catch (error) {
t.match(error.code, /EADDRNOTAVAIL|ECONNREFUSED/)
}
await t.completed
})
test('removes authorization header on third party origin', async t => {
t = tspl(t, { plan: 1 })
const [server1] = await startRedirectingWithAuthorization('secret')
const { body: bodyStream } = await undici.request(`http://${server1}`, {
dispatcher: new undici.Agent({}).compose(redirect({ maxRedirections: 10 })),
headers: {
authorization: 'secret'
}
})
const body = await bodyStream.text()
t.strictEqual(body, '')
await t.completed
})
test('removes cookie header on third party origin', async t => {
t = tspl(t, { plan: 1 })
const [server1] = await startRedirectingWithCookie('a=b')
const { body: bodyStream } = await undici.request(`http://${server1}`, {
dispatcher: new undici.Agent({}).compose(redirect({ maxRedirections: 10 })),
headers: {
cookie: 'a=b'
}
})
const body = await bodyStream.text()
t.strictEqual(body, '')
await t.completed
})
test('should upgrade the connection when no redirects are present', async t => {
t = tspl(t, { plan: 2 })
const server = await startServer((req, res) => {
if (req.url === '/') {
res.statusCode = 301
res.setHeader('Location', `http://${server}/end`)
res.end('REDIRECT')
return
}
res.statusCode = 101
res.setHeader('Connection', 'upgrade')
res.setHeader('Upgrade', req.headers.upgrade)
res.end('')
})
const { headers, socket } = await undici.upgrade(`http://${server}/`, {
method: 'GET',
protocol: 'foo/1',
dispatcher: new undici.Client(`http://${server}/`).compose(redirect({ maxRedirections: 10 }))
})
socket.end()
t.strictEqual(headers.connection, 'upgrade')
t.strictEqual(headers.upgrade, 'foo/1')
await t.completed
})
test('should redirect to relative URL according to RFC 7231', async t => {
t = tspl(t, { plan: 2 })
const server = await startRedirectingWithRelativePath()
const { statusCode, body } = await undici.request(`http://${server}`, {
dispatcher: new undici.Client(`http://${server}/`).compose(redirect({ maxRedirections: 3 }))
})
const finalPath = await body.text()
t.strictEqual(statusCode, 200)
t.strictEqual(finalPath, '/absolute/b')
})
test('same-origin redirect preserves plain object headers with polluted Object.prototype[Symbol.iterator]', async (t) => {
const { strictEqual } = tspl(t, { plan: 2 })
const server = createServer((req, res) => {
if (req.url === '/redirect') {
res.writeHead(302, {
Location: '/final'
})
res.end()
return
}
strictEqual(req.headers['x-custom'], 'ok')
res.end('redirected')
}).listen(0)
const originalIterator = Object.prototype[Symbol.iterator]
// eslint-disable-next-line no-extend-native
Object.prototype[Symbol.iterator] = function * () {}
try {
await once(server, 'listening')
const res = await undici.request(`http://localhost:${server.address().port}/redirect`, {
dispatcher: new undici.Agent({}).compose(redirect({ maxRedirections: 1 })),
headers: {
'X-Custom': 'ok'
}
})
const text = await res.body.text()
strictEqual(text, 'redirected')
} finally {
if (originalIterator === undefined) {
delete Object.prototype[Symbol.iterator]
} else {
// eslint-disable-next-line no-extend-native
Object.prototype[Symbol.iterator] = originalIterator
}
server.close()
}
})
test('Cross-origin redirects clear forbidden headers', async (t) => {
const { strictEqual } = tspl(t, { plan: 6 })
const server1 = createServer({ joinDuplicateHeaders: true }, (req, res) => {
strictEqual(req.headers.cookie, undefined)
strictEqual(req.headers.authorization, undefined)
strictEqual(req.headers['proxy-authorization'], undefined)
res.end('redirected')
}).listen(0)
const server2 = createServer({ joinDuplicateHeaders: true }, (req, res) => {
strictEqual(req.headers.authorization, 'test')
strictEqual(req.headers.cookie, 'ddd=dddd')
res.writeHead(302, {
...req.headers,
Location: `http://localhost:${server1.address().port}`
})
res.end()
}).listen(0)
t.after(() => {
server1.close()
server2.close()
})
await Promise.all([
once(server1, 'listening'),
once(server2, 'listening')
])
const res = await undici.request(`http://localhost:${server2.address().port}`, {
dispatcher: new undici.Agent({}).compose(redirect({ maxRedirections: 1 })),
headers: {
Authorization: 'test',
Cookie: 'ddd=dddd',
'Proxy-Authorization': 'test'
}
})
const text = await res.body.text()
strictEqual(text, 'redirected')
})
================================================
FILE: test/interceptors/response-error.js
================================================
'use strict'
const assert = require('node:assert')
const { once } = require('node:events')
const { createServer } = require('node:http')
const { test, after } = require('node:test')
const { interceptors, Client } = require('../..')
const { responseError } = interceptors
test('should throw error for error response', async () => {
const server = createServer({ joinDuplicateHeaders: true })
server.on('request', (req, res) => {
res.writeHead(400, { 'content-type': 'text/plain' })
res.end('Bad Request')
})
server.listen(0)
await once(server, 'listening')
const client = new Client(
`http://localhost:${server.address().port}`
).compose(responseError())
after(async () => {
await client.close()
server.close()
await once(server, 'close')
})
let error
try {
await client.request({
method: 'GET',
path: '/',
headers: {
'content-type': 'text/plain'
}
})
} catch (err) {
error = err
}
assert.equal(error.statusCode, 400)
assert.equal(error.message, 'Response Error')
assert.equal(error.body, 'Bad Request')
})
test('should not throw error for ok response', async () => {
const server = createServer({ joinDuplicateHeaders: true })
server.on('request', (req, res) => {
res.writeHead(200, { 'content-type': 'text/plain' })
res.end('hello')
})
server.listen(0)
await once(server, 'listening')
const client = new Client(
`http://localhost:${server.address().port}`
).compose(responseError())
after(async () => {
await client.close()
server.close()
await once(server, 'close')
})
const response = await client.request({
method: 'GET',
path: '/',
headers: {
'content-type': 'text/plain'
}
})
assert.equal(response.statusCode, 200)
assert.equal(await response.body.text(), 'hello')
})
test('should throw error for error response, parsing JSON', async () => {
const server = createServer({ joinDuplicateHeaders: true })
server.on('request', (req, res) => {
res.writeHead(400, { 'content-type': 'application/json; charset=utf-8' })
res.end(JSON.stringify({ message: 'Bad Request' }))
})
server.listen(0)
await once(server, 'listening')
const client = new Client(
`http://localhost:${server.address().port}`
).compose(responseError())
after(async () => {
await client.close()
server.close()
await once(server, 'close')
})
let error
try {
await client.request({
method: 'GET',
path: '/',
headers: {
'content-type': 'text/plain'
}
})
} catch (err) {
error = err
}
assert.equal(error.statusCode, 400)
assert.equal(error.message, 'Response Error')
assert.deepStrictEqual(error.body, {
message: 'Bad Request'
})
})
test('should throw error for error response, parsing JSON without charset', async () => {
const server = createServer({ joinDuplicateHeaders: true })
server.on('request', (req, res) => {
res.writeHead(400, { 'content-type': 'application/json' })
res.end(JSON.stringify({ message: 'Bad Request' }))
})
server.listen(0)
await once(server, 'listening')
const client = new Client(
`http://localhost:${server.address().port}`
).compose(responseError())
after(async () => {
await client.close()
server.close()
await once(server, 'close')
})
let error
try {
await client.request({
method: 'GET',
path: '/',
headers: {
'content-type': 'text/plain'
}
})
} catch (err) {
error = err
}
assert.equal(error.statusCode, 400)
assert.equal(error.message, 'Response Error')
assert.deepStrictEqual(error.body, {
message: 'Bad Request'
})
})
test('should throw error for networking errors response', async () => {
const client = new Client(
'http://localhost:12345'
).compose(responseError())
after(async () => {
await client.close()
})
let error
try {
await client.request({
method: 'GET',
path: '/',
headers: {
'content-type': 'text/plain'
}
})
} catch (err) {
error = err
}
assert.equal(error.code, 'ECONNREFUSED')
})
test('should throw error for error response without content type', async () => {
const server = createServer({ joinDuplicateHeaders: true })
server.on('request', (req, res) => {
res.writeHead(400, {})
res.end()
})
server.listen(0)
await once(server, 'listening')
const client = new Client(
`http://localhost:${server.address().port}`
).compose(responseError())
after(async () => {
await client.close()
server.close()
await once(server, 'close')
})
let error
try {
await client.request({
method: 'GET',
path: '/',
headers: {
'content-type': 'text/plain'
}
})
} catch (err) {
error = err
}
assert.equal(error.statusCode, 400)
assert.equal(error.message, 'Response Error')
assert.deepStrictEqual(error.body, '')
})
================================================
FILE: test/interceptors/retry.js
================================================
'use strict'
const { tspl } = require('@matteo.collina/tspl')
const { test, after } = require('node:test')
const { createServer } = require('node:http')
const { once } = require('node:events')
const { spawnSync } = require('node:child_process')
const { Client, interceptors } = require('../..')
const { retry, redirect, dns } = interceptors
test('Should retry status code', async t => {
t = tspl(t, { plan: 4 })
let counter = 0
const server = createServer({ joinDuplicateHeaders: true })
const retryOptions = {
retry: (err, { state, opts }, done) => {
counter++
if (err.statusCode === 500 || err.message.includes('other side closed')) {
setTimeout(done, 500)
return
}
return done(err)
}
}
const requestOptions = {
method: 'GET',
path: '/',
headers: {
'content-type': 'application/json'
}
}
server.on('request', (req, res) => {
switch (counter) {
case 0:
req.destroy()
t.ok(true, 'pass')
return
case 1:
res.writeHead(500)
res.end('failed')
t.ok(true, 'pass')
return
case 2:
res.writeHead(200)
res.end('hello world!')
return
default:
t.fail()
}
})
server.listen(0)
await once(server, 'listening')
const client = new Client(
`http://localhost:${server.address().port}`
).compose(retry(retryOptions))
after(async () => {
await client.close()
server.close()
await once(server, 'close')
})
const response = await client.request(requestOptions)
t.equal(response.statusCode, 200)
t.equal(await response.body.text(), 'hello world!')
})
test('Should retry on error code', async t => {
t = tspl(t, { plan: 2 })
let counter = 0
const retryOptions = {
retry: (err, _state, done) => {
if (counter < 5) {
counter++
setTimeout(done, 500)
} else {
done(err)
}
},
maxRetries: 5
}
const requestOptions = {
origin: 'http://localhost:123',
method: 'GET',
path: '/',
headers: {
'content-type': 'application/json'
}
}
const client = new Client(
'http://localhost:123'
).compose(dns({
lookup: (_h, _o, cb) => {
const error = new Error('ENOTFOUND')
error.code = 'ENOTFOUND'
cb(error)
}
}), retry(retryOptions))
after(async () => {
await client.close()
})
await t.rejects(client.request(requestOptions), { code: 'ENOTFOUND' })
t.equal(counter, 5)
})
test('Should use retry-after header for retries', async t => {
t = tspl(t, { plan: 3 })
let counter = 0
const server = createServer({ joinDuplicateHeaders: true })
let checkpoint
const dispatchOptions = {
method: 'PUT',
path: '/',
headers: {
'content-type': 'application/json'
}
}
server.on('request', (req, res) => {
switch (counter) {
case 0:
res.writeHead(429, {
'retry-after': 1
})
res.end('rate limit')
checkpoint = Date.now()
counter++
return
case 1:
res.writeHead(200)
res.end('hello world!')
t.ok(Date.now() - checkpoint >= 500)
counter++
return
default:
t.fail('unexpected request')
}
})
server.listen(0)
await once(server, 'listening')
const client = new Client(
`http://localhost:${server.address().port}`
).compose(retry())
after(async () => {
await client.close()
server.close()
await once(server, 'close')
})
const response = await client.request(dispatchOptions)
t.equal(response.statusCode, 200)
t.equal(await response.body.text(), 'hello world!')
})
test('Should use retry-after header for retries (date)', async t => {
t = tspl(t, { plan: 3 })
let counter = 0
const server = createServer({ joinDuplicateHeaders: true })
let checkpoint
const requestOptions = {
method: 'PUT',
path: '/',
headers: {
'content-type': 'application/json'
}
}
server.on('request', (req, res) => {
switch (counter) {
case 0:
checkpoint = Date.now()
res.writeHead(429, {
'retry-after': new Date(
checkpoint + 2000
).toUTCString()
})
res.end('rate limit')
counter++
return
case 1:
res.writeHead(200)
res.end('hello world!')
t.ok(Date.now() - checkpoint >= 1000)
counter++
return
default:
t.fail('unexpected request')
}
})
server.listen(0)
await once(server, 'listening')
const client = new Client(
`http://localhost:${server.address().port}`
).compose(retry())
after(async () => {
await client.close()
server.close()
await once(server, 'close')
})
const response = await client.request(requestOptions)
t.equal(response.statusCode, 200)
t.equal(await response.body.text(), 'hello world!')
})
test('Should retry with defaults', async t => {
t = tspl(t, { plan: 2 })
let counter = 0
const server = createServer({ joinDuplicateHeaders: true })
const requestOptions = {
method: 'GET',
path: '/',
headers: {
'content-type': 'application/json'
}
}
server.on('request', (req, res) => {
switch (counter) {
case 0:
req.destroy()
counter++
return
case 1:
res.writeHead(500)
res.end('failed')
counter++
return
case 2:
res.writeHead(200)
res.end('hello world!')
counter++
return
default:
t.fail()
}
})
server.listen(0)
await once(server, 'listening')
const client = new Client(
`http://localhost:${server.address().port}`
).compose(retry())
after(async () => {
await client.close()
server.close()
await once(server, 'close')
})
const response = await client.request(requestOptions)
t.equal(response.statusCode, 200)
t.equal(await response.body.text(), 'hello world!')
})
test('Should pass context from other interceptors', async t => {
t = tspl(t, { plan: 2 })
const server = createServer({ joinDuplicateHeaders: true })
const requestOptions = {
method: 'GET',
path: '/'
}
server.on('request', (req, res) => {
res.writeHead(200)
res.end('hello world!')
})
server.listen(0)
await once(server, 'listening')
const client = new Client(
`http://localhost:${server.address().port}`
).compose(redirect({ maxRedirections: 1 }), retry())
after(async () => {
await client.close()
server.close()
await once(server, 'close')
})
const response = await client.request(requestOptions)
t.equal(response.statusCode, 200)
t.deepStrictEqual(response.context, { history: [] })
})
test('Should handle 206 partial content', async t => {
t = tspl(t, { plan: 5 })
let counter = 0
// Took from: https://github.com/nxtedition/nxt-lib/blob/4b001ebc2f22cf735a398f35ff800dd553fe5933/test/undici/retry.js#L47
let x = 0
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
if (x === 0) {
t.ok(true, 'pass')
res.setHeader('content-length', '6')
res.setHeader('etag', 'asd')
res.write('abc')
setTimeout(() => {
res.destroy()
}, 1e2)
} else if (x === 1) {
t.deepStrictEqual(req.headers.range, 'bytes=3-5')
res.setHeader('content-range', 'bytes 3-5/6')
res.setHeader('etag', 'asd')
res.statusCode = 206
res.end('def')
}
x++
})
const retryOptions = {
retry: function (err, _, done) {
counter++
if (err.code && err.code === 'UND_ERR_DESTROYED') {
return done(false)
}
if (err.statusCode === 206) return done(err)
setTimeout(done, 800)
}
}
const requestOptions = {
method: 'GET',
path: '/',
headers: {
'content-type': 'application/json'
},
retryOptions
}
server.listen(0)
await once(server, 'listening')
const client = new Client(
`http://localhost:${server.address().port}`
).compose(retry())
after(async () => {
await client.close()
server.close()
await once(server, 'close')
})
const response = await client.request(requestOptions)
t.equal(response.statusCode, 200)
t.strictEqual(await response.body.text(), 'abcdef')
t.strictEqual(counter, 1)
})
test('Should handle 206 partial content - bad-etag', async t => {
t = tspl(t, { plan: 5 })
// Took from: https://github.com/nxtedition/nxt-lib/blob/4b001ebc2f22cf735a398f35ff800dd553fe5933/test/undici/retry.js#L47
let x = 0
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
if (x === 0) {
t.ok(true, 'pass')
res.setHeader('etag', 'asd')
res.write('abc')
setTimeout(() => {
res.destroy()
}, 1e2)
} else if (x === 1) {
t.deepStrictEqual(req.headers.range, 'bytes=3-')
res.setHeader('content-range', 'bytes 3-6/6')
res.setHeader('etag', 'erwsd')
res.statusCode = 206
res.end('def')
}
x++
})
const requestOptions = {
method: 'GET',
path: '/',
headers: {
'content-type': 'application/json'
},
retryOptions: {
retry: (err, { state, opts }, done) => {
if (err.message.includes('other side closed')) {
setTimeout(done, 100)
return
}
return done(err)
}
}
}
server.listen(0)
await once(server, 'listening')
const client = new Client(
`http://localhost:${server.address().port}`
).compose(retry())
after(async () => {
await client.close()
server.close()
await once(server, 'close')
})
try {
const response = await client.request(requestOptions)
await response.body.text()
} catch (error) {
t.strictEqual(error.name, 'RequestRetryError')
t.strictEqual(error.code, 'UND_ERR_REQ_RETRY')
t.strictEqual(error.message, 'ETag mismatch')
}
})
test('retrying a request with a body', async t => {
t = tspl(t, { plan: 2 })
let counter = 0
const server = createServer({ joinDuplicateHeaders: true })
const requestOptions = {
method: 'POST',
path: '/',
headers: {
'content-type': 'application/json'
},
body: JSON.stringify({ hello: 'world' }),
retryOptions: {
retry: (err, { state, opts }, done) => {
counter++
if (
err.statusCode === 500 ||
err.message.includes('other side closed')
) {
setTimeout(done, 500)
return
}
return done(err)
}
}
}
server.on('request', (req, res) => {
switch (counter) {
case 0:
req.destroy()
return
case 1:
res.writeHead(500)
res.end('failed')
return
case 2:
res.writeHead(200)
res.end('hello world!')
return
default:
t.fail()
}
})
server.listen(0)
await once(server, 'listening')
const client = new Client(
`http://localhost:${server.address().port}`
).compose(retry())
after(async () => {
await client.close()
server.close()
await once(server, 'close')
})
const response = await client.request(requestOptions)
t.equal(response.statusCode, 200)
t.equal(await response.body.text(), 'hello world!')
})
test('should not error if request is not meant to be retried', async t => {
t = tspl(t, { plan: 2 })
const server = createServer({ joinDuplicateHeaders: true })
server.on('request', (req, res) => {
res.writeHead(400)
res.end('Bad request')
})
server.listen(0)
await once(server, 'listening')
const client = new Client(
`http://localhost:${server.address().port}`
).compose(retry())
after(async () => {
await client.close()
server.close()
await once(server, 'close')
})
const response = await client.request({
method: 'GET',
path: '/',
headers: {
'content-type': 'application/json'
}
})
t.equal(response.statusCode, 400)
t.equal(await response.body.text(), 'Bad request')
})
test('#3975 - keep event loop ticking', async t => {
const suite = tspl(t, { plan: 2 })
const res = spawnSync('node', ['./test/fixtures/interceptors/retry-event-loop.js'], {
stdio: 'pipe'
})
const output = res.stderr.toString()
suite.ok(output.includes('UND_ERR_REQ_RETRY'))
suite.ok(output.includes('RequestRetryError: Request failed'))
})
================================================
FILE: test/invalid-headers.js
================================================
'use strict'
const { tspl } = require('@matteo.collina/tspl')
const { test, after } = require('node:test')
const { Client, errors } = require('..')
test('invalid headers', (t) => {
t = tspl(t, { plan: 10 })
const client = new Client('http://localhost:3000')
after(() => client.close())
client.request({
path: '/',
method: 'GET',
headers: {
'content-length': 'asd'
}
}, (err, data) => {
t.ok(err instanceof errors.InvalidArgumentError)
})
client.request({
path: '/',
method: 'GET',
headers: 1
}, (err, data) => {
t.ok(err instanceof errors.InvalidArgumentError)
})
client.request({
path: '/',
method: 'GET',
headers: {
'transfer-encoding': 'chunked'
}
}, (err, data) => {
t.ok(err instanceof errors.InvalidArgumentError)
})
client.request({
path: '/',
method: 'GET',
headers: {
upgrade: 'asd'
}
}, (err, data) => {
t.ok(err instanceof errors.InvalidArgumentError)
})
client.request({
path: '/',
method: 'GET',
headers: {
connection: 'asd'
}
}, (err, data) => {
t.ok(err instanceof errors.InvalidArgumentError)
})
client.request({
path: '/',
method: 'GET',
headers: {
'keep-alive': 'timeout=5'
}
}, (err, data) => {
t.ok(err instanceof errors.InvalidArgumentError)
})
client.request({
path: '/',
method: 'GET',
headers: {
foo: {}
}
}, (err, data) => {
t.ok(err instanceof errors.InvalidArgumentError)
})
client.request({
path: '/',
method: 'GET',
headers: {
expect: '100-continue'
}
}, (err, data) => {
t.ok(err instanceof errors.NotSupportedError)
})
client.request({
path: '/',
method: 'GET',
headers: {
Expect: '100-continue'
}
}, (err, data) => {
t.ok(err instanceof errors.NotSupportedError)
})
client.request({
path: '/',
method: 'GET',
headers: {
expect: 'asd'
}
}, (err, data) => {
t.ok(err instanceof errors.NotSupportedError)
})
})
================================================
FILE: test/ip-prioritization.js
================================================
'use strict'
const { test } = require('node:test')
const { Client } = require('..')
const { createServer } = require('node:http')
const { once } = require('node:events')
test('HTTP/1.1 Request Prioritization', async (t) => {
let priority = null
const server = createServer((req, res) => {
res.end('ok')
})
server.listen(0)
await once(server, 'listening')
const client = new Client(`http://localhost:${server.address().port}`, {
connect: (opts, cb) => {
const socket = require('node:net').connect({
...opts,
host: opts.hostname,
port: opts.port
}, () => {
cb(null, socket)
})
socket.setTypeOfService = (p) => {
priority = p
}
return socket
}
})
try {
await client.request({
path: '/',
method: 'GET',
typeOfService: 42
})
// Check if priority was set
if (priority !== 42) {
throw new Error(`Expected priority 42, got ${priority}`)
}
} finally {
await client.close()
server.close()
}
})
test('HTTP/2 Connection Prioritization', async (t) => {
const net = require('node:net')
const buildConnector = require('../lib/core/connect')
let receivedHints = null
// Mock net.connect
t.mock.method(net, 'connect', (options) => {
receivedHints = options.typeOfService
const socket = new (require('node:events').EventEmitter)()
socket.cork = () => { }
socket.uncork = () => { }
socket.destroy = () => { }
socket.ref = () => { }
socket.unref = () => { }
socket.setKeepAlive = () => socket
socket.setNoDelay = () => socket
// Simulate connection to allow callback to fire
process.nextTick(() => {
socket.emit('connect')
})
return socket
})
// Test buildConnector directly to ensure options passing
const connector = buildConnector({ typeOfService: 123, allowH2: true })
await new Promise((resolve, reject) => {
connector({ hostname: 'localhost', host: 'localhost', protocol: 'http:', port: 3000 }, (err, socket) => {
if (err) reject(err)
else resolve(socket)
})
})
if (receivedHints !== 123) {
throw new Error(`Expected typeOfService 123, got ${receivedHints}`)
}
})
================================================
FILE: test/issue-1757.js
================================================
'use strict'
const { deepStrictEqual, strictEqual } = require('node:assert')
const { test } = require('node:test')
const { Dispatcher, setGlobalDispatcher, fetch, MockAgent } = require('..')
class MiniflareDispatcher extends Dispatcher {
constructor (inner, options) {
super(options)
this.inner = inner
}
dispatch (options, handler) {
return this.inner.dispatch(options, handler)
}
close (...args) {
return this.inner.close(...args)
}
destroy (...args) {
return this.inner.destroy(...args)
}
}
test('https://github.com/nodejs/undici/issues/1757', async () => {
const mockAgent = new MockAgent()
const mockClient = mockAgent.get('http://localhost:3000')
mockAgent.disableNetConnect()
setGlobalDispatcher(new MiniflareDispatcher(mockAgent))
mockClient.intercept({
path: () => true,
method: () => true
}).reply(200, async (opts) => {
if (opts.body?.[Symbol.asyncIterator]) {
const chunks = []
for await (const chunk of opts.body) {
chunks.push(chunk)
}
return Buffer.concat(chunks)
}
return opts.body
})
const response = await fetch('http://localhost:3000', {
method: 'POST',
body: JSON.stringify({ foo: 'bar' })
})
deepStrictEqual(await response.json(), { foo: 'bar' })
strictEqual(response.status, 200)
})
================================================
FILE: test/issue-2065.js
================================================
'use strict'
const { tspl } = require('@matteo.collina/tspl')
const { test, after } = require('node:test')
const { createServer } = require('node:http')
const { once } = require('node:events')
const { FormData, request } = require('..')
test('undici.request with a FormData body should set content-length header', async (t) => {
t = tspl(t, { plan: 1 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
t.ok(req.headers['content-length'])
res.end()
}).listen(0)
after(() => server.close())
await once(server, 'listening')
const body = new FormData()
body.set('file', new File(['abc'], 'abc.txt'))
await request(`http://localhost:${server.address().port}`, {
method: 'POST',
body
})
})
================================================
FILE: test/issue-2078.js
================================================
'use strict'
const { tspl } = require('@matteo.collina/tspl')
const { test, after } = require('node:test')
const { MockAgent, getGlobalDispatcher, setGlobalDispatcher, fetch } = require('..')
test('MockPool.reply headers are an object, not an array - issue #2078', async (t) => {
t = tspl(t, { plan: 1 })
const global = getGlobalDispatcher()
const mockAgent = new MockAgent()
const mockPool = mockAgent.get('http://localhost')
after(() => setGlobalDispatcher(global))
setGlobalDispatcher(mockAgent)
mockPool.intercept({
path: '/foo',
method: 'GET'
}).reply((options) => {
t.strictEqual(Array.isArray(options.headers), false)
return { statusCode: 200 }
})
await fetch('http://localhost/foo')
await t.completed
})
================================================
FILE: test/issue-2283.js
================================================
'use strict'
const { describe, test } = require('node:test')
const assert = require('node:assert')
const { FormData, Response } = require('..')
describe('https://github.com/nodejs/undici/issues/2283', () => {
test('preserve full type when parsing multipart/form-data', async (t) => {
const testBlob = new Blob(['123'], { type: 'text/plain;charset=utf-8' })
const fd = new FormData()
fd.set('x', testBlob)
const res = new Response(fd)
const body = await res.clone().text()
// Just making sure that it contains ;charset=utf-8
assert.ok(body.includes('text/plain;charset=utf-8'))
const formData = await new Response(fd).formData()
// returns just 'text/plain'
assert.ok(formData.get('x').type === 'text/plain;charset=utf-8')
})
})
================================================
FILE: test/issue-2349.js
================================================
'use strict'
const { test } = require('node:test')
const { rejects } = require('node:assert')
const { Writable } = require('node:stream')
const { MockAgent, stream } = require('..')
test('stream() does not fail after request has been aborted', () => {
const mockAgent = new MockAgent()
mockAgent.disableNetConnect()
mockAgent
.get('http://localhost:3333')
.intercept({
path: '/'
})
.reply(200, 'ok')
.delay(10)
const parts = []
const ac = new AbortController()
setTimeout(() => ac.abort(), 5)
rejects(
stream(
'http://localhost:3333/',
{
opaque: { parts },
signal: ac.signal,
dispatcher: mockAgent
},
({ opaque: { parts } }) => {
return new Writable({
write (chunk, _encoding, callback) {
parts.push(chunk)
callback()
}
})
}
),
new DOMException('This operation was aborted', 'AbortError')
)
})
================================================
FILE: test/issue-2590.js
================================================
'use strict'
const { tspl } = require('@matteo.collina/tspl')
const { test, after } = require('node:test')
const { request } = require('..')
const { createServer } = require('node:http')
const { once } = require('node:events')
test('aborting request with custom reason', async (t) => {
t = tspl(t, { plan: 3 })
const server = createServer({ joinDuplicateHeaders: true }, () => {}).listen(0)
after(() => server.close())
await once(server, 'listening')
const timeout = AbortSignal.timeout(0)
const ac = new AbortController()
ac.abort(new Error('aborted'))
const ac2 = new AbortController()
ac2.abort() // no reason
await t.rejects(
request(`http://localhost:${server.address().port}`, { signal: timeout }),
timeout.reason
)
await t.rejects(
request(`http://localhost:${server.address().port}`, { signal: ac.signal }),
/Error: aborted/
)
await t.rejects(
request(`http://localhost:${server.address().port}`, { signal: ac2.signal }),
{ name: 'AbortError' }
)
await t.completed
})
================================================
FILE: test/issue-3356.js
================================================
'use strict'
const { tspl } = require('@matteo.collina/tspl')
const { test, after } = require('node:test')
const { setTimeout: sleep } = require('node:timers/promises')
const { createServer } = require('node:http')
const { once } = require('node:events')
const { tick: fastTimersTick } = require('../lib/util/timers')
const { fetch, Agent, RetryAgent } = require('..')
test('https://github.com/nodejs/undici/issues/3356', { skip: process.env.CITGM }, async (t) => {
t = tspl(t, { plan: 3 })
let shouldRetry = true
const server = createServer({ joinDuplicateHeaders: true })
server.on('request', (req, res) => {
res.writeHead(200, { 'content-type': 'text/plain' })
if (shouldRetry) {
shouldRetry = false
res.flushHeaders()
res.write('h')
setTimeout(() => { res.end('ello world!') }, 100)
} else {
res.end('hello world!')
}
})
server.listen(0)
await once(server, 'listening')
after(async () => {
server.close()
await once(server, 'close')
})
const agent = new RetryAgent(new Agent({ bodyTimeout: 50 }), {
errorCodes: ['UND_ERR_BODY_TIMEOUT']
})
const response = await fetch(`http://localhost:${server.address().port}`, {
dispatcher: agent
})
fastTimersTick()
await sleep(500)
try {
t.equal(response.status, 200)
// consume response
await response.text()
} catch (err) {
t.equal(err.name, 'TypeError')
t.equal(err.cause.code, 'UND_ERR_REQ_RETRY')
}
await t.completed
})
================================================
FILE: test/issue-3410.js
================================================
'use strict'
const { tspl } = require('@matteo.collina/tspl')
const { fork } = require('node:child_process')
const { resolve: pathResolve } = require('node:path')
const { describe, test } = require('node:test')
const { Agent, fetch, setGlobalDispatcher } = require('..')
const { eventLoopBlocker } = require('./utils/event-loop-blocker')
describe('https://github.com/nodejs/undici/issues/3410', () => {
test('FastTimers', async (t) => {
t = tspl(t, { plan: 1 })
// Spawn a server in a new process to avoid effects from the blocking event loop
const {
serverProcess,
address
} = await new Promise((resolve, reject) => {
const childProcess = fork(
pathResolve(__dirname, './utils/hello-world-server.js'),
[],
{ windowsHide: true }
)
childProcess.on('message', (address) => {
resolve({
serverProcess: childProcess,
address
})
})
childProcess.on('error', err => {
reject(err)
})
})
const connectTimeout = 2000
setGlobalDispatcher(new Agent({ connectTimeout }))
const fetchPromise = fetch(address)
eventLoopBlocker(3000)
const response = await fetchPromise
t.equal(await response.text(), 'Hello World')
serverProcess.kill('SIGKILL')
})
test('native Timers', async (t) => {
t = tspl(t, { plan: 1 })
// Spawn a server in a new process to avoid effects from the blocking event loop
const {
serverProcess,
address
} = await new Promise((resolve, reject) => {
const childProcess = fork(
pathResolve(__dirname, './utils/hello-world-server.js'),
[],
{ windowsHide: true }
)
childProcess.on('message', (address) => {
resolve({
serverProcess: childProcess,
address
})
})
childProcess.on('error', err => {
reject(err)
})
})
const connectTimeout = 900
setGlobalDispatcher(new Agent({ connectTimeout }))
const fetchPromise = fetch(address)
eventLoopBlocker(1500)
const response = await fetchPromise
t.equal(await response.text(), 'Hello World')
serverProcess.kill('SIGKILL')
})
})
================================================
FILE: test/issue-3904.js
================================================
const { describe, test, after } = require('node:test')
const assert = require('node:assert')
const { createServer } = require('node:http')
const { once } = require('node:events')
const MemoryCacheStore = require('../lib/cache/memory-cache-store.js')
const { Agent, interceptors, request, setGlobalDispatcher } = require('..')
describe('Cache with cache-control: no-store request header', () => {
[
'CACHE-CONTROL',
'cache-control',
'Cache-Control'
].forEach(headerName => {
test(`should not cache response for request with header: "${headerName}: no-store`, async () => {
const store = new MemoryCacheStore()
let requestCount = 0
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
++requestCount
res.setHeader('Vary', 'Accept-Encoding')
res.setHeader('Cache-Control', 'max-age=60')
res.end(`Request count: ${requestCount}`)
})
after(async () => {
server.close()
await once(server, 'close')
})
await new Promise(resolve => server.listen(0, resolve))
const { port } = server.address()
const url = `http://localhost:${port}`
const agent = new Agent()
setGlobalDispatcher(
agent.compose(
interceptors.cache({
store,
cacheByDefault: 1000,
methods: ['GET']
})
)
)
const res1 = await request(url, { headers: { [headerName]: 'no-store' } })
const body1 = await res1.body.text()
assert.strictEqual(body1, 'Request count: 1')
assert.strictEqual(requestCount, 1)
const res2 = await request(url)
const body2 = await res2.body.text()
assert.strictEqual(body2, 'Request count: 2')
assert.strictEqual(requestCount, 2)
await new Promise(resolve => server.close(resolve))
})
})
})
================================================
FILE: test/issue-3934.js
================================================
'use strict'
const { test } = require('node:test')
const { createServer } = require('node:http')
const { once } = require('node:events')
const assert = require('node:assert')
const { Agent, RetryAgent, request } = require('..')
// https://github.com/nodejs/undici/issues/3934
test('WrapHandler works with multiple header values', async (t) => {
const server = createServer({ joinDuplicateHeaders: true }, async (_req, res) => {
const headers = [
['set-cookie', 'a'],
['set-cookie', 'b'],
['set-cookie', 'c']
]
res.writeHead(200, headers)
res.end()
}).listen(0)
await once(server, 'listening')
t.after(() => server.close())
const agent = new Agent()
const retryAgent = new RetryAgent(agent)
const {
headers
} = await request(`http://localhost:${server.address().port}`, { dispatcher: retryAgent })
assert.deepStrictEqual(headers['set-cookie'], ['a', 'b', 'c'])
})
// https://github.com/nodejs/undici/issues/4797
test('WrapHandler preserves latin1 header bytes', async (t) => {
const expected = Buffer.from([0xE2, 0x80, 0xA6]).toString('latin1')
const server = createServer({ joinDuplicateHeaders: true }, async (_req, res) => {
res.writeHead(200, {
'x-test': expected
})
res.end()
}).listen(0)
await once(server, 'listening')
t.after(() => server.close())
const agent = new Agent()
const retryAgent = new RetryAgent(agent)
const {
headers
} = await request(`http://localhost:${server.address().port}`, { dispatcher: retryAgent })
assert.strictEqual(headers['x-test'], expected)
})
================================================
FILE: test/issue-3959.js
================================================
const { describe, test, after } = require('node:test')
const assert = require('node:assert')
const { createServer } = require('node:http')
const MemoryCacheStore = require('../lib/cache/memory-cache-store.js')
const { request, Agent, setGlobalDispatcher } = require('..')
const { interceptors } = require('..')
const { runtimeFeatures } = require('../lib/util/runtime-features.js')
describe('Cache with Vary headers', () => {
async function runCacheTest (store) {
let requestCount = 0
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
requestCount++
res.setHeader('Vary', 'Accept-Encoding')
res.setHeader('Cache-Control', 'max-age=60')
res.end(`Request count: ${requestCount}`)
})
await new Promise(resolve => server.listen(0, resolve))
const port = server.address().port
const url = `http://localhost:${port}`
const agent = new Agent()
setGlobalDispatcher(
agent.compose(
interceptors.cache({
store,
cacheByDefault: 1000,
methods: ['GET']
})
)
)
const res1 = await request(url)
const body1 = await res1.body.text()
assert.strictEqual(body1, 'Request count: 1')
assert.strictEqual(requestCount, 1)
const res2 = await request(url)
const body2 = await res2.body.text()
assert.strictEqual(body2, 'Request count: 1')
assert.strictEqual(requestCount, 1)
const res3 = await request(url, {
headers: {
'Accept-Encoding': 'gzip'
}
})
const body3 = await res3.body.text()
assert.strictEqual(body3, 'Request count: 2')
assert.strictEqual(requestCount, 2)
await new Promise(resolve => server.close(resolve))
}
test('should cache response with MemoryCacheStore when Vary header exists but request header is missing', async () => {
await runCacheTest(new MemoryCacheStore())
})
test('should cache response with SqliteCacheStore when Vary header exists but request header is missing', { skip: runtimeFeatures.has('sqlite') === false }, async () => {
const SqliteCacheStore = require('../lib/cache/sqlite-cache-store.js')
const sqliteStore = new SqliteCacheStore()
await runCacheTest(sqliteStore)
after(() => sqliteStore.close())
})
})
================================================
FILE: test/issue-4244.js
================================================
const { test, describe } = require('node:test')
const assert = require('node:assert')
const { createServer } = require('node:http')
const { request, Agent, Pool } = require('..')
// https://github.com/nodejs/undici/issues/4424
describe('Agent should close inactive clients', () => {
test('without active connections', async (t) => {
const server = createServer({ keepAliveTimeout: 0 }, async (_req, res) => {
res.setHeader('connection', 'close')
res.writeHead(200)
res.end('ok')
}).listen(0)
t.after(() => {
server.closeAllConnections?.()
server.close()
})
let p
const agent = new Agent({
factory: (origin, opts) => {
const pool = new Pool(origin, opts)
let _resolve, _reject
p = new Promise((resolve, reject) => {
_resolve = resolve
_reject = reject
})
pool.on('disconnect', () => {
setImmediate(() => pool.destroyed ? _resolve() : _reject(new Error('client not destroyed')))
})
return pool
}
})
const { statusCode } = await request(`http://localhost:${server.address().port}`, { dispatcher: agent })
assert.equal(statusCode, 200)
await p
})
test('in case of connection error', async (t) => {
let p
const agent = new Agent({
factory: (origin, opts) => {
const pool = new Pool(origin, opts)
let _resolve, _reject
p = new Promise((resolve, reject) => {
_resolve = resolve
_reject = reject
})
pool.on('connectionError', () => {
setImmediate(() => pool.destroyed ? _resolve() : _reject(new Error('client not destroyed')))
})
return pool
}
})
try {
await request('http://localhost:0', { dispatcher: agent })
} catch (_) {
// ignore
}
await p
})
})
================================================
FILE: test/issue-4691.js
================================================
'use strict'
const { tspl } = require('@matteo.collina/tspl')
const { test, after } = require('node:test')
const { createServer } = require('node:http')
const { once } = require('node:events')
const { RetryAgent, Client, FormData } = require('..')
// https://github.com/nodejs/undici/issues/4691
test('Should retry status code with FormData body', async t => {
t = tspl(t, { plan: 2 })
let counter = 0
const server = createServer({ joinDuplicateHeaders: true })
const opts = {
maxRetries: 5,
timeout: 1,
timeoutFactor: 1
}
server.on('request', (req, res) => {
switch (counter++) {
case 0:
req.destroy()
return
case 1:
res.writeHead(500)
res.end('failed')
return
case 2:
res.writeHead(200)
res.end('hello world!')
return
default:
t.fail()
}
})
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
const agent = new RetryAgent(client, opts)
after(async () => {
await agent.close()
server.close()
await once(server, 'close')
})
const formData = new FormData()
formData.append('field', 'test string')
agent.request({
method: 'GET',
path: '/',
headers: {
'content-type': 'application/json'
},
body: formData
}).then((res) => {
t.equal(res.statusCode, 200)
res.body.setEncoding('utf8')
let chunks = ''
res.body.on('data', chunk => { chunks += chunk })
res.body.on('end', () => {
t.equal(chunks, 'hello world!')
})
})
})
await t.completed
})
================================================
FILE: test/issue-4806.js
================================================
'use strict'
const { test, after } = require('node:test')
const { createServer } = require('node:http')
const { once } = require('node:events')
const { tspl } = require('@matteo.collina/tspl')
const { Agent, request } = require('..')
// https://github.com/nodejs/undici/issues/4806
test('Agent clientTtl cleanup does not trigger unhandled rejections', async (t) => {
t = tspl(t, { plan: 2 })
const server = createServer((req, res) => {
res.end('ok')
})
after(() => server.close())
server.listen(0, async () => {
const agent = new Agent({ clientTtl: 10 })
after(async () => agent.close())
const onUnhandled = (err) => t.fail(err)
process.once('unhandledRejection', onUnhandled)
after(() => process.removeListener('unhandledRejection', onUnhandled))
const origin = `http://localhost:${server.address().port}`
const res1 = await request(origin, { dispatcher: agent })
t.strictEqual(res1.statusCode, 200)
await new Promise(resolve => setTimeout(resolve, 20))
const res2 = await request(origin, { dispatcher: agent })
t.strictEqual(res2.statusCode, 200)
res2.body.resume()
await once(res2.body, 'end')
await new Promise(resolve => setTimeout(resolve, 20))
})
await t.completed
})
================================================
FILE: test/issue-4880.js
================================================
'use strict'
// Regression test for: "Cannot read properties of null (reading 'push')"
//
// Stack trace:
// TypeError: Cannot read properties of null (reading 'push')
// at RequestHandler.onData (lib/api/api-request.js:148:21)
// at ClientHttp2Stream. (lib/dispatcher/client-h2.js:706:17)
//
// Root cause:
// The 'data' listener was registered unconditionally on the stream, outside
// the 'response' handler. This meant 'data' events could fire before 'response',
// i.e. before onHeaders() ran and set RequestHandler.res. With res still null
// (its initial value), onData() -> this.res.push(chunk) crashed.
//
// Fix: register the 'data' listener only inside the 'response' handler, after
// onHeaders() has run and res is guaranteed to be set. A closure-local
// dataHandlerActive flag additionally guards against already-queued 'data'
// events that arrive after abort() tears down the stream.
//
// Reproduced by: TLS H2 server with slow responses (2.5s), 20 connections x 100
// concurrent streams = 2000 max concurrent, 10k total requests queued. Under this
// backpressure undici dispatches data events before response headers are processed.
const { tspl } = require('@matteo.collina/tspl')
const { test, after } = require('node:test')
const { Worker, isMainThread, parentPort } = require('node:worker_threads')
const { Agent, interceptors } = require('..')
// ── Server (runs in worker thread) ──────────────────────────────────────────
if (!isMainThread) {
const http2 = require('node:http2')
const pem = require('@metcoder95/https-pem')
pem.generate({ opts: { keySize: 2048 } }).then((cert) => {
const body = JSON.stringify({ ok: true })
const server = http2.createSecureServer({ key: cert.key, cert: cert.cert, allowHTTP1: false })
server.on('stream', (stream, headers) => {
const path = headers[':path'] ?? ''
if (path.startsWith('/slow')) {
const url = new URL(path, 'https://localhost')
const delayMs = parseInt(url.searchParams.get('delayMs') ?? '2500')
setTimeout(() => {
stream.respond({ ':status': 200, 'content-type': 'application/json' })
stream.end(body)
}, delayMs)
} else {
stream.respond({ ':status': 200, 'content-type': 'application/json' })
stream.end(body)
}
})
server.listen(0, '127.0.0.1', () => {
parentPort.postMessage({ port: server.address().port })
})
})
}
// ── Client / tests (runs in main thread) ────────────────────────────────────
function startServer () {
return new Promise((resolve) => {
const worker = new Worker(__filename)
worker.once('message', ({ port }) => resolve({ worker, port }))
})
}
function makeDispatcher (connections, maxConcurrentStreams) {
return new Agent({
keepAliveTimeout: 20_000,
keepAliveMaxTimeout: 60_000,
bodyTimeout: 20_000,
headersTimeout: 20_000,
allowH2: true,
connections,
pipelining: maxConcurrentStreams,
maxConcurrentStreams,
connect: { rejectUnauthorized: false }
}).compose(interceptors.responseError())
}
// Integration test: TLS H2 server in a worker thread, slow responses (2.5s),
// 20 connections x 100 streams = 2000 concurrent max, 10k total requests queued, responseError interceptor active.
test('h2: no crash when data arrives after stream abort under high concurrency', async (t) => {
t = tspl(t, { plan: 2 })
const { worker, port } = await startServer()
after(() => worker.terminate())
const connections = 20
const maxConcurrentStreams = 100
const dispatcher = makeDispatcher(connections, maxConcurrentStreams)
after(() => dispatcher.close())
const origin = `https://127.0.0.1:${port}`
const count = 10_000
const requests = Array.from({ length: count }, () =>
dispatcher.request({
origin,
path: '/slow?delayMs=2500',
method: 'GET'
}).then((res) => res.body.dump())
)
const results = await Promise.allSettled(requests)
const errors = results.filter((r) => r.status === 'rejected')
const successCount = results.length - errors.length
if (errors.length > 0) {
const groups = new Map()
for (const e of errors) {
const msg = e.reason?.message ?? String(e.reason)
groups.set(msg, (groups.get(msg) ?? 0) + 1)
}
console.log('Error breakdown:', Object.fromEntries(groups))
console.log('First error stack:\n', errors[0].reason?.stack)
}
// If the bug is present, some requests crash with:
// "Cannot read properties of null (reading 'push')"
t.ok(successCount + errors.length === count, `all ${count} requests settled`)
t.ok(successCount === count, `all ${count} requests succeeded (got ${errors.length} errors)`)
await t.completed
})
================================================
FILE: test/issue-803.js
================================================
'use strict'
const { tspl } = require('@matteo.collina/tspl')
const { test, after } = require('node:test')
const { once } = require('node:events')
const { Client } = require('..')
const { createServer } = require('node:http')
test('https://github.com/nodejs/undici/issues/803', { timeout: 60000 }, async (t) => {
t = tspl(t, { plan: 2 })
const SIZE = 5900373096
const server = createServer({ joinDuplicateHeaders: true }, async (req, res) => {
const chunkSize = res.writableHighWaterMark << 5
const parts = (SIZE / chunkSize) | 0
const lastPartSize = SIZE % chunkSize
const chunk = Buffer.allocUnsafe(chunkSize)
res.shouldKeepAlive = false
res.setHeader('content-length', SIZE)
let i = 0
while (i++ < parts) {
if (res.write(chunk) === false) {
await once(res, 'drain')
}
}
if (res.write(chunk.subarray(0, lastPartSize)) === false) {
await once(res, 'drain')
}
res.end()
})
after(() => server.close())
server.listen(0)
await once(server, 'listening')
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.close())
client.on('disconnect', () => {
if (!client.closed && !client.destroyed) {
t.fail('unexpected disconnect')
}
})
client.request({
path: '/',
method: 'GET'
}, (err, data) => {
t.ifError(err)
let pos = 0
data.body.on('data', (buf) => {
pos += buf.length
})
data.body.on('end', () => {
t.strictEqual(pos, SIZE)
})
})
await t.completed
})
================================================
FILE: test/issue-810.js
================================================
'use strict'
const { tspl } = require('@matteo.collina/tspl')
const { test, after } = require('node:test')
const { once } = require('node:events')
const { Client, errors } = require('..')
const net = require('node:net')
test('https://github.com/mcollina/undici/issues/810', async (t) => {
t = tspl(t, { plan: 3 })
let x = 0
const server = net.createServer(socket => {
if (x++ === 0) {
socket.write('HTTP/1.1 200 OK\r\n')
socket.write('Content-Length: 1\r\n\r\n')
socket.write('11111\r\n')
} else {
socket.write('HTTP/1.1 200 OK\r\n')
socket.write('Content-Length: 0\r\n\r\n')
socket.write('\r\n')
}
})
after(() => server.close())
server.listen(0)
await once(server, 'listening')
const client = new Client(`http://localhost:${server.address().port}`, { pipelining: 2 })
after(() => client.close())
client.request({
path: '/',
method: 'GET'
}, (err, data) => {
t.ifError(err)
data.body.resume().on('end', () => {
// t.fail() FIX: Should fail.
t.ok(true, 'pass')
}).on('error', err => (
t.ok(err instanceof errors.HTTPParserError)
))
})
client.request({
path: '/',
method: 'GET'
}, (err, data) => {
t.ok(err instanceof errors.HTTPParserError)
})
await t.completed
})
test('https://github.com/mcollina/undici/issues/810 no pipelining', async (t) => {
t = tspl(t, { plan: 2 })
const server = net.createServer(socket => {
socket.write('HTTP/1.1 200 OK\r\n')
socket.write('Content-Length: 1\r\n\r\n')
socket.write('11111\r\n')
})
after(() => server.close())
server.listen(0)
await once(server, 'listening')
const client = new Client(`http://localhost:${server.address().port}`)
client.request({
path: '/',
method: 'GET'
}, (err, data) => {
t.ifError(err)
data.body.resume().on('end', () => {
// t.fail() FIX: Should fail.
t.ok(true, 'pass')
})
})
await t.completed
})
test('https://github.com/mcollina/undici/issues/810 pipelining', async (t) => {
t = tspl(t, { plan: 2 })
const server = net.createServer(socket => {
socket.write('HTTP/1.1 200 OK\r\n')
socket.write('Content-Length: 1\r\n\r\n')
socket.write('11111\r\n')
})
after(() => server.close())
server.listen(0)
await once(server, 'listening')
const client = new Client(`http://localhost:${server.address().port}`, { pipelining: true })
after(() => client.close())
client.request({
path: '/',
method: 'GET'
}, (err, data) => {
t.ifError(err)
data.body.resume().on('end', () => {
// t.fail() FIX: Should fail.
t.ok(true, 'pass')
})
})
await t.completed
})
test('https://github.com/mcollina/undici/issues/810 pipelining 2', async (t) => {
t = tspl(t, { plan: 3 })
const server = net.createServer(socket => {
socket.write('HTTP/1.1 200 OK\r\n')
socket.write('Content-Length: 1\r\n\r\n')
socket.write('11111\r\n')
})
after(() => server.close())
server.listen(0)
await once(server, 'listening')
const client = new Client(`http://localhost:${server.address().port}`, { pipelining: true })
after(() => client.close())
client.request({
path: '/',
method: 'GET'
}, (err, data) => {
t.ifError(err)
data.body.resume().on('end', () => {
// t.fail() FIX: Should fail.
t.ok(true, 'pass')
})
})
client.request({
path: '/',
method: 'GET'
}, (err, data) => {
t.ok(err instanceof errors.HTTPParserError)
})
await t.completed
})
================================================
FILE: test/jest/instanceof-error.test.js
================================================
'use strict'
const { createServer } = require('node:http')
const { once } = require('node:events')
/* global expect, it, jest, AbortController */
// https://github.com/facebook/jest/issues/11607#issuecomment-899068995
jest.useRealTimers()
it('isErrorLike sanity check', () => {
const { isErrorLike } = require('../../lib/web/fetch/util')
const error = new DOMException('')
// https://github.com/facebook/jest/issues/2549
expect(error instanceof Error).toBeFalsy()
expect(isErrorLike(error)).toBeTruthy()
})
it('Real use-case', async () => {
const { fetch } = require('../..')
const ac = new AbortController()
ac.abort()
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.end()
}).listen(0)
await once(server, 'listening')
const promise = fetch(`https://localhost:${server.address().port}`, {
signal: ac.signal
})
await expect(async () => await promise).rejects.toThrow(/^Th(e|is) operation was aborted\.?$/)
server.close()
await once(server, 'close')
})
================================================
FILE: test/jest/issue-1757.test.js
================================================
'use strict'
const { Dispatcher, setGlobalDispatcher, MockAgent } = require('../..')
/* global expect, it */
class MiniflareDispatcher extends Dispatcher {
constructor (inner, options) {
super(options)
this.inner = inner
}
dispatch (options, handler) {
return this.inner.dispatch(options, handler)
}
close (...args) {
return this.inner.close(...args)
}
destroy (...args) {
return this.inner.destroy(...args)
}
}
it('https://github.com/nodejs/undici/issues/1757', async () => {
// fetch isn't exported in <16.8
const { fetch } = require('../..')
const mockAgent = new MockAgent()
const mockClient = mockAgent.get('http://localhost:3000')
mockAgent.disableNetConnect()
setGlobalDispatcher(new MiniflareDispatcher(mockAgent))
mockClient.intercept({
path: () => true,
method: () => true
}).reply(200, async (opts) => {
if (opts.body?.[Symbol.asyncIterator]) {
const chunks = []
for await (const chunk of opts.body) {
chunks.push(chunk)
}
return Buffer.concat(chunks)
}
return opts.body
})
const response = await fetch('http://localhost:3000', {
method: 'POST',
body: JSON.stringify({ foo: 'bar' })
})
expect(response.json()).resolves.toMatchObject({ foo: 'bar' })
expect(response.status).toBe(200)
})
================================================
FILE: test/jest/mock-agent.test.js
================================================
'use strict'
const { request, setGlobalDispatcher, MockAgent } = require('../..')
const { getResponse } = require('../../lib/mock/mock-utils')
/* global describe, it, afterEach, expect */
describe('MockAgent', () => {
let mockAgent
afterEach(() => {
mockAgent.close()
})
it('should work in jest', async () => {
expect.assertions(4)
const baseUrl = 'http://localhost:9999'
mockAgent = new MockAgent()
setGlobalDispatcher(mockAgent)
const mockClient = mockAgent.get(baseUrl)
mockClient.intercept({
path: '/foo?hello=there&see=ya',
method: 'POST',
body: 'form1=data1&form2=data2'
}).reply(200, { foo: 'bar' }, {
headers: {
'content-type': 'application/json'
},
trailers: { 'Content-MD5': 'test' }
})
const { statusCode, headers, trailers, body } = await request(`${baseUrl}/foo?hello=there&see=ya`, {
method: 'POST',
body: 'form1=data1&form2=data2'
})
expect(statusCode).toBe(200)
expect(headers).toEqual({ 'content-type': 'application/json' })
expect(trailers).toEqual({ 'content-md5': 'test' })
const jsonResponse = JSON.parse(await getResponse(body))
expect(jsonResponse).toEqual({ foo: 'bar' })
})
})
describe('MockAgent with ignoreTrailingSlash option', () => {
const trailingSlashUrl = 'http://localhost:9999/'
const noTrailingSlashUrl = 'http://localhost:9999'
it('should not remove trailing slash from origin if the option is not enable', async () => {
const mockClient = new MockAgent()
const dispatcherOne = mockClient.get(trailingSlashUrl)
const dispatcherTwo = mockClient.get(noTrailingSlashUrl)
expect(dispatcherOne).not.toBe(dispatcherTwo)
})
it('should remove trailing slash from origin if enabled the option', async () => {
const mockClient = new MockAgent({ ignoreTrailingSlash: true })
const dispatcherOne = mockClient.get(trailingSlashUrl)
const dispatcherTwo = mockClient.get(noTrailingSlashUrl)
expect(dispatcherOne).toBe(dispatcherTwo)
})
})
================================================
FILE: test/jest/mock-scope.test.js
================================================
'use strict'
const { MockAgent, setGlobalDispatcher, request } = require('../../index')
/* global afterAll, expect, it, AbortController */
const mockAgent = new MockAgent()
afterAll(async () => {
await mockAgent.close()
})
it('Jest works with MockScope.delay - issue #1327', async () => {
mockAgent.disableNetConnect()
setGlobalDispatcher(mockAgent)
const mockPool = mockAgent.get('http://localhost:3333')
mockPool.intercept({
path: '/jest-bugs',
method: 'GET'
}).reply(200, 'Hello').delay(100)
const ac = new AbortController()
setTimeout(() => ac.abort(), 5)
const promise = request('http://localhost:3333/jest-bugs', {
signal: ac.signal
})
await expect(async () => await promise).rejects.toThrow('This operation was aborted')
}, 1000)
================================================
FILE: test/jest/parser-timeout.test.js
================================================
'use strict'
/* global jest, describe, it, beforeEach, afterEach, expect */
// test/jest/parser-timeout.test.js
const EventEmitter = require('node:events')
const connectH1 = require('../../lib/dispatcher/client-h1')
const { kParser } = require('../../lib/core/symbols')
// DummySocket extends EventEmitter to support .on/.off/.read()
class DummySocket extends EventEmitter {
constructor () {
super()
this.destroyed = false
this.errored = null
}
read () {
return null
}
}
const dummyClient = {
[Symbol.for('kMaxHeadersSize')]: 1024,
[Symbol.for('kMaxResponseSize')]: 1024,
[Symbol.for('kQueue')]: [],
[Symbol.for('kRunningIdx')]: 0,
headersTimeout: 100,
bodyTimeout: 100
}
describe('Parser#setTimeout under fake timers', () => {
beforeEach(() => jest.useFakeTimers('modern'))
afterEach(() => jest.useRealTimers())
it('does not throw when calling setTimeout under fake timers', async () => {
const socket = new DummySocket()
await connectH1(dummyClient, socket) // connectH1 creates Parser -> socket[kParser]
const parser = socket[kParser]
expect(() => parser.setTimeout(200, 0)).not.toThrow()
})
})
================================================
FILE: test/jest/test.js
================================================
'use strict'
const { Client } = require('../..')
const { createServer } = require('node:http')
/* global test, expect */
test('should work in jest', async () => {
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
expect(req.url).toBe('/')
expect(req.method).toBe('POST')
expect(req.headers.host).toBe(`localhost:${server.address().port}`)
res.setHeader('Content-Type', 'text/plain')
res.end('hello')
})
await expect(new Promise((resolve, reject) => {
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
client.request({
path: '/',
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: '{}'
}, (err, result) => {
server.close()
client.close()
if (err) {
reject(err)
} else {
resolve(result.body.text())
}
})
})
})).resolves.toBe('hello')
})
================================================
FILE: test/jest/util-timers.test.js
================================================
'use strict'
/* global jest, describe, it, beforeEach, afterEach, expect */
// test/jest/util-timers.test.js
const timers = require('../../lib/util/timers')
describe('util/timers under fake timers', () => {
beforeEach(() => jest.useFakeTimers('modern'))
afterEach(() => {
jest.useRealTimers()
try {
timers.reset()
} catch {}
})
it('setFastTimeout + clearFastTimeout does not throw', () => {
const fast = timers.setFastTimeout(() => {}, 2000)
expect(typeof fast.refresh).toBe('function')
expect(() => timers.clearFastTimeout(fast)).not.toThrow()
})
it('short setTimeout + clearTimeout does not throw', () => {
const t = timers.setTimeout(() => {}, 10)
expect(() => timers.clearTimeout(t)).not.toThrow()
})
})
================================================
FILE: test/max-headers.js
================================================
'use strict'
const { tspl } = require('@matteo.collina/tspl')
const { test, after } = require('node:test')
const { Client } = require('..')
const { createServer } = require('node:http')
const { once } = require('node:events')
test('handle a lot of headers', async (t) => {
t = tspl(t, { plan: 3 })
const headers = {}
for (let n = 0; n < 64; ++n) {
headers[n] = String(n)
}
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.writeHead(200, headers)
res.end()
})
after(() => server.close())
server.listen(0)
await once(server, 'listening')
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.close())
client.on('disconnect', () => {
if (!client.closed && !client.destroyed) {
t.fail('unexpected disconnect')
}
})
client.request({
path: '/',
method: 'GET'
}, (err, data) => {
t.ifError(err)
const headers2 = {}
for (let n = 0; n < 64; ++n) {
headers2[n] = data.headers[n]
}
t.deepStrictEqual(headers2, headers)
data.body
.resume()
.on('end', () => {
t.ok(true, 'pass')
})
})
await t.completed
})
================================================
FILE: test/max-response-size.js
================================================
'use strict'
const { tspl } = require('@matteo.collina/tspl')
const { test, after, describe } = require('node:test')
const { Client, errors } = require('..')
const { createServer } = require('node:http')
describe('max response size', async (t) => {
test('default max default size should allow all responses', async (t) => {
t = tspl(t, { plan: 3 })
const server = createServer({ joinDuplicateHeaders: true })
after(() => {
server.closeAllConnections?.()
server.close()
})
server.on('request', (req, res) => {
res.end('hello')
})
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`, { maxResponseSize: -1 })
after(() => client.close())
client.on('disconnect', () => {
if (!client.closed && !client.destroyed) {
t.fail('unexpected disconnect')
}
})
client.request({ path: '/', method: 'GET' }, (err, { statusCode, body }) => {
t.ifError(err)
t.strictEqual(statusCode, 200)
const bufs = []
body.on('data', (buf) => {
bufs.push(buf)
})
body.on('end', () => {
t.strictEqual('hello', Buffer.concat(bufs).toString('utf8'))
})
})
})
await t.completed
})
test('max response size set to zero should allow only empty responses', async (t) => {
t = tspl(t, { plan: 3 })
const server = createServer({ joinDuplicateHeaders: true })
after(() => {
server.closeAllConnections?.()
server.close()
})
server.on('request', (req, res) => {
res.end()
})
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`, { maxResponseSize: 0 })
after(() => client.close())
client.on('disconnect', () => {
if (!client.closed && !client.destroyed) {
t.fail('unexpected disconnect')
}
})
client.request({ path: '/', method: 'GET' }, (err, { statusCode, body }) => {
t.ifError(err)
t.strictEqual(statusCode, 200)
const bufs = []
body.on('data', (buf) => {
bufs.push(buf)
})
body.on('end', () => {
t.strictEqual('', Buffer.concat(bufs).toString('utf8'))
})
})
})
await t.completed
})
test('should throw an error if the response is too big', async (t) => {
t = tspl(t, { plan: 3 })
const server = createServer({ joinDuplicateHeaders: true })
after(() => {
server.closeAllConnections?.()
server.close()
})
server.on('request', (req, res) => {
res.end('hello')
})
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`, {
maxResponseSize: 1
})
after(() => client.close())
client.request({ path: '/', method: 'GET' }, (err, { body }) => {
t.ifError(err)
body.on('error', (err) => {
t.ok(err)
t.ok(err instanceof errors.ResponseExceededMaxSizeError)
})
})
})
await t.completed
})
test('invalid max response size should throw an error', async (t) => {
t = tspl(t, { plan: 2 })
t.throws(() => {
// eslint-disable-next-line no-new
new Client('http://localhost:3000', { maxResponseSize: 'hello' })
}, 'maxResponseSize must be a number')
t.throws(() => {
// eslint-disable-next-line no-new
new Client('http://localhost:3000', { maxResponseSize: -2 })
}, 'maxResponseSize must be greater than or equal to -1')
})
await t.completed
})
================================================
FILE: test/mock-agent.js
================================================
'use strict'
const { test, after, describe } = require('node:test')
const { createServer } = require('node:http')
const { once } = require('node:events')
const { request, setGlobalDispatcher, MockAgent, Agent } = require('..')
const { getResponse } = require('../lib/mock/mock-utils')
const { kClients, kConnected } = require('../lib/core/symbols')
const { InvalidArgumentError, ClientDestroyedError } = require('../lib/core/errors')
const MockClient = require('../lib/mock/mock-client')
const MockPool = require('../lib/mock/mock-pool')
const { kAgent, kMockAgentIsCallHistoryEnabled } = require('../lib/mock/mock-symbols')
const Dispatcher = require('../lib/dispatcher/dispatcher')
const { MockNotMatchedError } = require('../lib/mock/mock-errors')
const { fetch } = require('..')
const { MockCallHistory } = require('../lib/mock/mock-call-history')
describe('MockAgent - constructor', () => {
test('sets up mock agent', t => {
t.plan(1)
t.assert.doesNotThrow(() => new MockAgent())
})
test('should implement the Dispatcher API', t => {
t.plan(1)
const mockAgent = new MockAgent()
t.assert.ok(mockAgent instanceof Dispatcher)
})
test('sets up mock agent with single connection', t => {
t.plan(1)
t.assert.doesNotThrow(() => new MockAgent({ connections: 1 }))
})
test('should error passed agent is not valid', t => {
t.plan(2)
t.assert.throws(() => new MockAgent({ agent: {} }), new InvalidArgumentError('Argument opts.agent must implement Agent'))
t.assert.throws(() => new MockAgent({ agent: { dispatch: '' } }), new InvalidArgumentError('Argument opts.agent must implement Agent'))
})
test('should be able to specify the agent to mock', t => {
t.plan(1)
const agent = new Agent()
after(() => agent.close())
const mockAgent = new MockAgent({ agent })
t.assert.strictEqual(mockAgent[kAgent], agent)
})
test('should disable call history by default', t => {
t.plan(2)
const mockAgent = new MockAgent()
after(() => mockAgent.close())
t.assert.strictEqual(mockAgent[kMockAgentIsCallHistoryEnabled], false)
t.assert.strictEqual(mockAgent.getCallHistory(), undefined)
})
test('should enable call history if option is true', t => {
t.plan(2)
const mockAgent = new MockAgent({ enableCallHistory: true })
after(() => mockAgent.close())
t.assert.strictEqual(mockAgent[kMockAgentIsCallHistoryEnabled], true)
t.assert.ok(mockAgent.getCallHistory() instanceof MockCallHistory)
})
test('should disable call history if option is false', t => {
t.plan(2)
after(() => mockAgent.close())
const mockAgent = new MockAgent({ enableCallHistory: false })
t.assert.strictEqual(mockAgent[kMockAgentIsCallHistoryEnabled], false)
t.assert.strictEqual(mockAgent.getCallHistory(), undefined)
})
test('should throw if enableCallHistory option is not a boolean', t => {
t.plan(1)
t.assert.throws(() => new MockAgent({ enableCallHistory: 'hello' }), new InvalidArgumentError('options.enableCallHistory must to be a boolean'))
})
})
describe('MockAgent - enableCallHistory', () => {
test('should enable call history and add call history log', async (t) => {
t.plan(2)
const mockAgent = new MockAgent()
setGlobalDispatcher(mockAgent)
after(() => mockAgent.close())
const mockClient = mockAgent.get('http://localhost:9999')
mockClient.intercept({
path: '/foo',
method: 'GET'
}).reply(200, 'foo').persist()
await fetch('http://localhost:9999/foo')
t.assert.strictEqual(mockAgent.getCallHistory()?.calls()?.length, undefined)
mockAgent.enableCallHistory()
await request('http://localhost:9999/foo')
t.assert.strictEqual(mockAgent.getCallHistory()?.calls()?.length, 1)
})
})
describe('MockAgent - disableCallHistory', () => {
test('should disable call history and not add call history log', async (t) => {
t.plan(2)
const mockAgent = new MockAgent({ enableCallHistory: true })
setGlobalDispatcher(mockAgent)
after(() => mockAgent.close())
const mockClient = mockAgent.get('http://localhost:9999')
mockClient.intercept({
path: '/foo',
method: 'GET'
}).reply(200, 'foo').persist()
await request('http://localhost:9999/foo')
t.assert.strictEqual(mockAgent.getCallHistory()?.calls()?.length, 1)
mockAgent.disableCallHistory()
await request('http://localhost:9999/foo')
t.assert.strictEqual(mockAgent.getCallHistory()?.calls()?.length, 1)
})
})
describe('MockAgent - get', () => {
test('should return MockClient', (t) => {
t.plan(1)
const baseUrl = 'http://localhost:9999'
const mockAgent = new MockAgent({ connections: 1 })
after(() => mockAgent.close())
const mockClient = mockAgent.get(baseUrl)
t.assert.ok(mockClient instanceof MockClient)
})
test('should return MockPool', (t) => {
t.plan(1)
const baseUrl = 'http://localhost:9999'
const mockAgent = new MockAgent()
after(() => mockAgent.close())
const mockPool = mockAgent.get(baseUrl)
t.assert.ok(mockPool instanceof MockPool)
})
test('should return the same instance if already created', (t) => {
t.plan(1)
const baseUrl = 'http://localhost:9999'
const mockAgent = new MockAgent()
after(() => mockAgent.close())
const mockPool1 = mockAgent.get(baseUrl)
const mockPool2 = mockAgent.get(baseUrl)
t.assert.strictEqual(mockPool1, mockPool2)
})
})
describe('MockAgent - dispatch', () => {
test('should call the dispatch method of the MockPool', (t) => {
t.plan(1)
const baseUrl = 'http://localhost:9999'
const mockAgent = new MockAgent()
after(() => mockAgent.close())
const mockPool = mockAgent.get(baseUrl)
mockPool.intercept({
path: '/foo',
method: 'GET'
}).reply(200, 'hello')
t.assert.doesNotThrow(() => mockAgent.dispatch({
origin: baseUrl,
path: '/foo',
method: 'GET'
}, {
onHeaders: (_statusCode, _headers, resume) => resume(),
onData: () => {},
onComplete: () => {},
onError: () => {}
}))
})
test('should call the dispatch method of the MockClient', (t) => {
t.plan(1)
const baseUrl = 'http://localhost:9999'
const mockAgent = new MockAgent({ connections: 1 })
after(() => mockAgent.close())
const mockClient = mockAgent.get(baseUrl)
mockClient.intercept({
path: '/foo',
method: 'GET'
}).reply(200, 'hello')
t.assert.doesNotThrow(() => mockAgent.dispatch({
origin: baseUrl,
path: '/foo',
method: 'GET'
}, {
onHeaders: (_statusCode, _headers, resume) => resume(),
onData: () => {},
onComplete: () => {},
onError: () => {}
}))
})
})
test('MockAgent - .close should clean up registered pools', async (t) => {
t.plan(5)
const baseUrl = 'http://localhost:9999'
const mockAgent = new MockAgent()
// Register a pool
const mockPool = mockAgent.get(baseUrl)
t.assert.ok(mockPool instanceof MockPool)
t.assert.strictEqual(mockPool[kConnected], 1)
t.assert.strictEqual(mockAgent[kClients].size, 1)
await mockAgent.close()
t.assert.strictEqual(mockPool[kConnected], 0)
t.assert.strictEqual(mockAgent[kClients].size, 0)
})
test('MockAgent - .close should clean up registered clients', async (t) => {
t.plan(5)
const baseUrl = 'http://localhost:9999'
const mockAgent = new MockAgent({ connections: 1 })
// Register a pool
const mockClient = mockAgent.get(baseUrl)
t.assert.ok(mockClient instanceof MockClient)
t.assert.strictEqual(mockClient[kConnected], 1)
t.assert.strictEqual(mockAgent[kClients].size, 1)
await mockAgent.close()
t.assert.strictEqual(mockClient[kConnected], 0)
t.assert.strictEqual(mockAgent[kClients].size, 0)
})
test('MockAgent - [kClients] should match encapsulated agent', async (t) => {
t.plan(1)
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.setHeader('content-type', 'text/plain')
res.end('should not be called')
t.assert.fail('should not be called')
})
after(() => {
server.closeAllConnections?.()
server.close()
})
await once(server.listen(0), 'listening')
const baseUrl = `http://localhost:${server.address().port}`
const agent = new Agent()
after(() => agent.close())
const mockAgent = new MockAgent({ agent })
const mockPool = mockAgent.get(baseUrl)
mockPool.intercept({
path: '/foo',
method: 'GET'
}).reply(200, 'hello')
// The MockAgent should encapsulate the input agent clients
t.assert.strictEqual(mockAgent[kClients].size, agent[kClients].size)
})
test('MockAgent - basic intercept with MockAgent.request', async (t) => {
t.plan(4)
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.setHeader('content-type', 'text/plain')
res.end('should not be called')
t.assert.fail('should not be called')
})
after(() => {
server.closeAllConnections?.()
server.close()
})
await once(server.listen(0), 'listening')
const baseUrl = `http://localhost:${server.address().port}`
const mockAgent = new MockAgent()
after(() => mockAgent.close())
const mockPool = mockAgent.get(baseUrl)
mockPool.intercept({
path: '/foo?hello=there&see=ya',
method: 'POST',
body: 'form1=data1&form2=data2'
}).reply(200, { foo: 'bar' }, {
headers: { 'content-type': 'application/json' },
trailers: { 'Content-MD5': 'test' }
})
const { statusCode, headers, trailers, body } = await mockAgent.request({
origin: baseUrl,
path: '/foo?hello=there&see=ya',
method: 'POST',
body: 'form1=data1&form2=data2'
})
t.assert.strictEqual(statusCode, 200)
t.assert.strictEqual(headers['content-type'], 'application/json')
t.assert.deepStrictEqual(trailers, { 'content-md5': 'test' })
const jsonResponse = JSON.parse(await getResponse(body))
t.assert.deepStrictEqual(jsonResponse, {
foo: 'bar'
})
})
test('MockAgent - basic intercept with request', async (t) => {
t.plan(4)
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.setHeader('content-type', 'text/plain')
res.end('should not be called')
t.assert.fail('should not be called')
})
after(() => {
server.closeAllConnections?.()
server.close()
})
await once(server.listen(0), 'listening')
const baseUrl = `http://localhost:${server.address().port}`
const mockAgent = new MockAgent()
setGlobalDispatcher(mockAgent)
after(() => mockAgent.close())
const mockPool = mockAgent.get(baseUrl)
mockPool.intercept({
path: '/foo?hello=there&see=ya',
method: 'POST',
body: 'form1=data1&form2=data2'
}).reply(200, { foo: 'bar' }, {
headers: { 'content-type': 'application/json' },
trailers: { 'Content-MD5': 'test' }
})
const { statusCode, headers, trailers, body } = await request(`${baseUrl}/foo?hello=there&see=ya`, {
method: 'POST',
body: 'form1=data1&form2=data2'
})
t.assert.strictEqual(statusCode, 200)
t.assert.strictEqual(headers['content-type'], 'application/json')
t.assert.deepStrictEqual(trailers, { 'content-md5': 'test' })
const jsonResponse = JSON.parse(await getResponse(body))
t.assert.deepStrictEqual(jsonResponse, {
foo: 'bar'
})
})
test('MockAgent - should support local agents', async (t) => {
t.plan(4)
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.setHeader('content-type', 'text/plain')
res.end('should not be called')
t.assert.fail('should not be called')
})
after(() => {
server.closeAllConnections?.()
server.close()
})
await once(server.listen(0), 'listening')
const baseUrl = `http://localhost:${server.address().port}`
const mockAgent = new MockAgent()
after(() => mockAgent.close())
const mockPool = mockAgent.get(baseUrl)
mockPool.intercept({
path: '/foo?hello=there&see=ya',
method: 'POST',
body: 'form1=data1&form2=data2'
}).reply(200, { foo: 'bar' }, {
headers: {
'content-type': 'application/json'
},
trailers: { 'Content-MD5': 'test' }
})
const { statusCode, headers, trailers, body } = await request(`${baseUrl}/foo?hello=there&see=ya`, {
method: 'POST',
body: 'form1=data1&form2=data2',
dispatcher: mockAgent
})
t.assert.strictEqual(statusCode, 200)
t.assert.strictEqual(headers['content-type'], 'application/json')
t.assert.deepStrictEqual(trailers, { 'content-md5': 'test' })
const jsonResponse = JSON.parse(await getResponse(body))
t.assert.deepStrictEqual(jsonResponse, {
foo: 'bar'
})
})
test('MockAgent - should support specifying custom agents to mock', async (t) => {
t.plan(4)
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.setHeader('content-type', 'text/plain')
res.end('should not be called')
t.assert.fail('should not be called')
})
after(() => {
server.closeAllConnections?.()
server.close()
})
await once(server.listen(0), 'listening')
const baseUrl = `http://localhost:${server.address().port}`
const agent = new Agent()
after(() => agent.close())
const mockAgent = new MockAgent({ agent })
setGlobalDispatcher(mockAgent)
const mockPool = mockAgent.get(baseUrl)
mockPool.intercept({
path: '/foo?hello=there&see=ya',
method: 'POST',
body: 'form1=data1&form2=data2'
}).reply(200, { foo: 'bar' }, {
headers: {
'content-type': 'application/json'
},
trailers: { 'Content-MD5': 'test' }
})
const { statusCode, headers, trailers, body } = await request(`${baseUrl}/foo?hello=there&see=ya`, {
method: 'POST',
body: 'form1=data1&form2=data2'
})
t.assert.strictEqual(statusCode, 200)
t.assert.strictEqual(headers['content-type'], 'application/json')
t.assert.deepStrictEqual(trailers, { 'content-md5': 'test' })
const jsonResponse = JSON.parse(await getResponse(body))
t.assert.deepStrictEqual(jsonResponse, {
foo: 'bar'
})
})
test('MockAgent - basic Client intercept with request', async (t) => {
t.plan(4)
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.setHeader('content-type', 'text/plain')
res.end('should not be called')
t.assert.fail('should not be called')
})
after(() => {
server.closeAllConnections?.()
server.close()
})
await once(server.listen(0), 'listening')
const baseUrl = `http://localhost:${server.address().port}`
const mockAgent = new MockAgent({ connections: 1 })
setGlobalDispatcher(mockAgent)
after(() => mockAgent.close())
const mockClient = mockAgent.get(baseUrl)
mockClient.intercept({
path: '/foo?hello=there&see=ya',
method: 'POST',
body: 'form1=data1&form2=data2'
}).reply(200, { foo: 'bar' }, {
headers: {
'content-type': 'application/json'
},
trailers: { 'Content-MD5': 'test' }
})
const { statusCode, headers, trailers, body } = await request(`${baseUrl}/foo?hello=there&see=ya`, {
method: 'POST',
body: 'form1=data1&form2=data2'
})
t.assert.strictEqual(statusCode, 200)
t.assert.strictEqual(headers['content-type'], 'application/json')
t.assert.deepStrictEqual(trailers, { 'content-md5': 'test' })
const jsonResponse = JSON.parse(await getResponse(body))
t.assert.deepStrictEqual(jsonResponse, {
foo: 'bar'
})
})
test('MockAgent - basic intercept with multiple pools', async (t) => {
t.plan(4)
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.setHeader('content-type', 'text/plain')
res.end('should not be called')
t.assert.fail('should not be called')
})
after(() => {
server.closeAllConnections?.()
server.close()
})
await once(server.listen(0), 'listening')
const baseUrl = `http://localhost:${server.address().port}`
const mockAgent = new MockAgent()
setGlobalDispatcher(mockAgent)
after(() => mockAgent.close())
const mockPool1 = mockAgent.get(baseUrl)
const mockPool2 = mockAgent.get('http://localhost:9999')
mockPool1.intercept({
path: '/foo?hello=there&see=ya',
method: 'POST',
body: 'form1=data1&form2=data2'
}).reply(200, { foo: 'bar-1' }, {
headers: {
'content-type': 'application/json'
},
trailers: { 'Content-MD5': 'test' }
})
mockPool2.intercept({
path: '/foo?hello=there&see=ya',
method: 'GET',
body: 'form1=data1&form2=data2'
}).reply(200, { foo: 'bar-2' })
const { statusCode, headers, trailers, body } = await request(`${baseUrl}/foo?hello=there&see=ya`, {
method: 'POST',
body: 'form1=data1&form2=data2'
})
t.assert.strictEqual(statusCode, 200)
t.assert.strictEqual(headers['content-type'], 'application/json')
t.assert.deepStrictEqual(trailers, { 'content-md5': 'test' })
const jsonResponse = JSON.parse(await getResponse(body))
t.assert.deepStrictEqual(jsonResponse, {
foo: 'bar-1'
})
})
test('MockAgent - should handle multiple responses for an interceptor', async (t) => {
t.plan(6)
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.setHeader('content-type', 'text/plain')
res.end('should not be called')
t.assert.fail('should not be called')
})
after(() => {
server.closeAllConnections?.()
server.close()
})
await once(server.listen(0), 'listening')
const baseUrl = `http://localhost:${server.address().port}`
const mockAgent = new MockAgent()
setGlobalDispatcher(mockAgent)
after(() => mockAgent.close())
const mockPool = mockAgent.get(baseUrl)
const interceptor = mockPool.intercept({
path: '/foo',
method: 'POST'
})
interceptor.reply(200, { foo: 'bar' }, {
headers: {
'content-type': 'application/json'
}
})
interceptor.reply(200, { hello: 'there' }, {
headers: {
'content-type': 'application/json'
}
})
{
const { statusCode, headers, body } = await request(`${baseUrl}/foo`, {
method: 'POST'
})
t.assert.strictEqual(statusCode, 200)
t.assert.strictEqual(headers['content-type'], 'application/json')
const jsonResponse = JSON.parse(await getResponse(body))
t.assert.deepStrictEqual(jsonResponse, {
foo: 'bar'
})
}
{
const { statusCode, headers, body } = await request(`${baseUrl}/foo`, {
method: 'POST'
})
t.assert.strictEqual(statusCode, 200)
t.assert.strictEqual(headers['content-type'], 'application/json')
const jsonResponse = JSON.parse(await getResponse(body))
t.assert.deepStrictEqual(jsonResponse, {
hello: 'there'
})
}
})
test('MockAgent - should call original Pool dispatch if request not found', async (t) => {
t.plan(5)
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
t.assert.strictEqual(req.url, '/foo')
t.assert.strictEqual(req.method, 'GET')
res.setHeader('content-type', 'text/plain')
res.end('hello')
})
after(() => {
server.closeAllConnections?.()
server.close()
})
await once(server.listen(0), 'listening')
const baseUrl = `http://localhost:${server.address().port}`
const mockAgent = new MockAgent()
setGlobalDispatcher(mockAgent)
after(() => mockAgent.close())
const { statusCode, headers, body } = await request(`${baseUrl}/foo`, {
method: 'GET'
})
t.assert.strictEqual(statusCode, 200)
t.assert.strictEqual(headers['content-type'], 'text/plain')
const response = await getResponse(body)
t.assert.strictEqual(response, 'hello')
})
test('MockAgent - should call original Client dispatch if request not found', async (t) => {
t.plan(5)
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
t.assert.strictEqual(req.url, '/foo')
t.assert.strictEqual(req.method, 'GET')
res.setHeader('content-type', 'text/plain')
res.end('hello')
})
after(() => {
server.closeAllConnections?.()
server.close()
})
await once(server.listen(0), 'listening')
const baseUrl = `http://localhost:${server.address().port}`
const mockAgent = new MockAgent({ connections: 1 })
setGlobalDispatcher(mockAgent)
after(() => mockAgent.close())
const { statusCode, headers, body } = await request(`${baseUrl}/foo`, {
method: 'GET'
})
t.assert.strictEqual(statusCode, 200)
t.assert.strictEqual(headers['content-type'], 'text/plain')
const response = await getResponse(body)
t.assert.strictEqual(response, 'hello')
})
test('MockAgent - should handle string responses', async (t) => {
t.plan(2)
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.setHeader('content-type', 'text/plain')
res.end('should not be called')
t.assert.fail('should not be called')
})
after(() => {
server.closeAllConnections?.()
server.close()
})
await once(server.listen(0), 'listening')
const baseUrl = `http://localhost:${server.address().port}`
const mockAgent = new MockAgent()
setGlobalDispatcher(mockAgent)
after(() => mockAgent.close())
const mockPool = mockAgent.get(baseUrl)
mockPool.intercept({
path: '/foo',
method: 'POST'
}).reply(200, 'hello')
const { statusCode, body } = await request(`${baseUrl}/foo`, {
method: 'POST'
})
t.assert.strictEqual(statusCode, 200)
const response = await getResponse(body)
t.assert.strictEqual(response, 'hello')
})
test('MockAgent - should handle basic concurrency for requests', { jobs: 5 }, async (t) => {
const mockAgent = new MockAgent()
setGlobalDispatcher(mockAgent)
after(() => mockAgent.close())
await Promise.all([...Array(5).keys()].map(idx =>
test(`concurrent job (${idx})`, async (t) => {
t.plan(2)
const baseUrl = 'http://localhost:9999'
const mockPool = mockAgent.get(baseUrl)
mockPool.intercept({
path: '/foo',
method: 'POST'
}).reply(200, { foo: `bar ${idx}` })
const { statusCode, body } = await request(`${baseUrl}/foo`, {
method: 'POST'
})
t.assert.strictEqual(statusCode, 200)
const jsonResponse = JSON.parse(await getResponse(body))
t.assert.deepStrictEqual(jsonResponse, {
foo: `bar ${idx}`
})
})
))
})
test('MockAgent - handle delays to simulate work', async (t) => {
t.plan(3)
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.setHeader('content-type', 'text/plain')
res.end('should not be called')
t.assert.fail('should not be called')
})
after(() => {
server.closeAllConnections?.()
server.close()
})
await once(server.listen(0), 'listening')
const baseUrl = `http://localhost:${server.address().port}`
const mockAgent = new MockAgent()
setGlobalDispatcher(mockAgent)
after(() => mockAgent.close())
const mockPool = mockAgent.get(baseUrl)
mockPool.intercept({
path: '/foo',
method: 'POST'
}).reply(200, 'hello').delay(50)
const start = process.hrtime()
const { statusCode, body } = await request(`${baseUrl}/foo`, {
method: 'POST'
})
t.assert.strictEqual(statusCode, 200)
const response = await getResponse(body)
t.assert.strictEqual(response, 'hello')
const elapsedInMs = Math.ceil(process.hrtime(start)[1] / 1e6)
t.assert.ok(elapsedInMs >= 50, `Elapsed time is not greater than 50ms: ${elapsedInMs}`)
})
test('MockAgent - should persist requests', async (t) => {
t.plan(8)
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.setHeader('content-type', 'text/plain')
res.end('should not be called')
t.assert.fail('should not be called')
})
after(() => {
server.closeAllConnections?.()
server.close()
})
await once(server.listen(0), 'listening')
const baseUrl = `http://localhost:${server.address().port}`
const mockAgent = new MockAgent()
setGlobalDispatcher(mockAgent)
after(() => mockAgent.close())
const mockPool = mockAgent.get(baseUrl)
mockPool.intercept({
path: '/foo?hello=there&see=ya',
method: 'POST',
body: 'form1=data1&form2=data2'
}).reply(200, { foo: 'bar' }, {
headers: {
'content-type': 'application/json'
},
trailers: { 'Content-MD5': 'test' }
}).persist()
{
const { statusCode, headers, trailers, body } = await request(`${baseUrl}/foo?hello=there&see=ya`, {
method: 'POST',
body: 'form1=data1&form2=data2'
})
t.assert.strictEqual(statusCode, 200)
t.assert.strictEqual(headers['content-type'], 'application/json')
t.assert.deepStrictEqual(trailers, { 'content-md5': 'test' })
const jsonResponse = JSON.parse(await getResponse(body))
t.assert.deepStrictEqual(jsonResponse, {
foo: 'bar'
})
}
{
const { statusCode, headers, trailers, body } = await request(`${baseUrl}/foo?hello=there&see=ya`, {
method: 'POST',
body: 'form1=data1&form2=data2'
})
t.assert.strictEqual(statusCode, 200)
t.assert.strictEqual(headers['content-type'], 'application/json')
t.assert.deepStrictEqual(trailers, { 'content-md5': 'test' })
const jsonResponse = JSON.parse(await getResponse(body))
t.assert.deepStrictEqual(jsonResponse, {
foo: 'bar'
})
}
})
test('MockAgent - getCallHistory with no name parameter should return the agent call history', async (t) => {
t.plan(1)
const mockAgent = new MockAgent({ enableCallHistory: true })
setGlobalDispatcher(mockAgent)
after(() => mockAgent.close())
const mockClient = mockAgent.get('http://localhost:9999')
mockClient.intercept({
path: '/foo',
method: 'GET'
}).reply(200, 'foo')
t.assert.ok(mockAgent.getCallHistory() instanceof MockCallHistory)
})
test('MockAgent - getCallHistory with request should return the call history instance with history log', async (t) => {
t.plan(9)
const mockAgent = new MockAgent({ enableCallHistory: true })
setGlobalDispatcher(mockAgent)
after(() => mockAgent.close())
const baseUrl = 'http://localhost:9999'
const mockClient = mockAgent.get(baseUrl)
mockClient.intercept({
path: /^\/foo/,
method: 'POST'
}).reply(200, 'foo')
t.assert.ok(mockAgent.getCallHistory()?.calls().length === 0)
const path = '/foo'
const url = new URL(path, baseUrl)
const method = 'POST'
const body = { data: 'value' }
const query = { a: 1 }
const headers = { 'content-type': 'application/json' }
await request(url, { method, query, body: JSON.stringify(body), headers })
t.assert.ok(mockAgent.getCallHistory()?.calls().length === 1)
t.assert.strictEqual(mockAgent.getCallHistory()?.lastCall()?.body, JSON.stringify(body))
t.assert.strictEqual(mockAgent.getCallHistory()?.lastCall()?.headers, headers)
t.assert.strictEqual(mockAgent.getCallHistory()?.lastCall()?.method, method)
t.assert.strictEqual(mockAgent.getCallHistory()?.lastCall()?.origin, baseUrl)
t.assert.strictEqual(mockAgent.getCallHistory()?.lastCall()?.path, path)
t.assert.strictEqual(mockAgent.getCallHistory()?.lastCall()?.fullUrl, `${url.toString()}?${new URLSearchParams(query).toString()}`)
t.assert.deepStrictEqual(mockAgent.getCallHistory()?.lastCall()?.searchParams, { a: '1' })
})
test('MockAgent - getCallHistory with fetch should return the call history instance with history log', async (t) => {
t.plan(9)
const mockAgent = new MockAgent({ enableCallHistory: true })
setGlobalDispatcher(mockAgent)
after(() => mockAgent.close())
const baseUrl = 'http://localhost:9999'
const mockClient = mockAgent.get(baseUrl)
mockClient.intercept({
path: /^\/foo/,
method: 'POST'
}).reply(200, 'foo')
t.assert.ok(mockAgent.getCallHistory()?.calls().length === 0)
const path = '/foo'
const url = new URL(path, baseUrl)
const method = 'POST'
const body = { data: 'value' }
const query = { a: 1 }
url.search = new URLSearchParams(query)
const headers = { authorization: 'token', 'content-type': 'application/json' }
await fetch(url, { method, query, body: JSON.stringify(body), headers })
t.assert.ok(mockAgent.getCallHistory()?.calls().length === 1)
t.assert.strictEqual(mockAgent.getCallHistory()?.lastCall()?.body, JSON.stringify(body))
t.assert.deepStrictEqual(mockAgent.getCallHistory()?.lastCall()?.headers, {
...headers,
'accept-encoding': 'gzip, deflate',
'content-length': '16',
'content-type': 'application/json',
'accept-language': '*',
'sec-fetch-mode': 'cors',
'user-agent': 'undici',
accept: '*/*'
})
t.assert.strictEqual(mockAgent.getCallHistory()?.lastCall()?.method, method)
t.assert.strictEqual(mockAgent.getCallHistory()?.lastCall()?.origin, baseUrl)
t.assert.strictEqual(mockAgent.getCallHistory()?.lastCall()?.path, url.pathname)
t.assert.strictEqual(mockAgent.getCallHistory()?.lastCall()?.fullUrl, url.toString())
t.assert.deepStrictEqual(mockAgent.getCallHistory()?.lastCall()?.searchParams, { a: '1' })
})
test('MockAgent - getCallHistory with fetch with a minimal configuration should register call history log', async (t) => {
t.plan(11)
const mockAgent = new MockAgent({ enableCallHistory: true })
setGlobalDispatcher(mockAgent)
after(() => mockAgent.close())
const baseUrl = 'http://localhost:9999'
const mockClient = mockAgent.get(baseUrl)
mockClient.intercept({
path: '/'
}).reply(200, 'foo')
const path = '/'
const url = new URL(path, baseUrl)
await fetch(url)
t.assert.ok(mockAgent.getCallHistory()?.calls().length === 1)
t.assert.strictEqual(mockAgent.getCallHistory()?.lastCall()?.body, null)
t.assert.deepStrictEqual(mockAgent.getCallHistory()?.lastCall()?.headers, {
'accept-encoding': 'gzip, deflate',
'accept-language': '*',
'sec-fetch-mode': 'cors',
'user-agent': 'undici',
accept: '*/*'
})
t.assert.strictEqual(mockAgent.getCallHistory()?.lastCall()?.method, 'GET')
t.assert.strictEqual(mockAgent.getCallHistory()?.lastCall()?.origin, baseUrl)
t.assert.strictEqual(mockAgent.getCallHistory()?.lastCall()?.path, path)
t.assert.strictEqual(mockAgent.getCallHistory()?.lastCall()?.fullUrl, baseUrl + path)
t.assert.deepStrictEqual(mockAgent.getCallHistory()?.lastCall()?.searchParams, {})
t.assert.strictEqual(mockAgent.getCallHistory()?.lastCall()?.host, 'localhost:9999')
t.assert.strictEqual(mockAgent.getCallHistory()?.lastCall()?.port, '9999')
t.assert.strictEqual(mockAgent.getCallHistory()?.lastCall()?.protocol, 'http:')
})
test('MockAgent - getCallHistory with request with a minimal configuration should register call history log', async (t) => {
t.plan(11)
const mockAgent = new MockAgent({ enableCallHistory: true })
setGlobalDispatcher(mockAgent)
after(() => mockAgent.close())
const baseUrl = 'http://localhost:9999'
const mockClient = mockAgent.get(baseUrl)
mockClient.intercept({
path: '/'
}).reply(200, 'foo')
const path = '/'
const url = new URL(path, baseUrl)
await request(url)
t.assert.ok(mockAgent.getCallHistory()?.calls().length === 1)
t.assert.strictEqual(mockAgent.getCallHistory()?.lastCall()?.body, undefined)
t.assert.strictEqual(mockAgent.getCallHistory()?.lastCall()?.headers, undefined)
t.assert.strictEqual(mockAgent.getCallHistory()?.lastCall()?.method, 'GET')
t.assert.strictEqual(mockAgent.getCallHistory()?.lastCall()?.origin, baseUrl)
t.assert.strictEqual(mockAgent.getCallHistory()?.lastCall()?.path, path)
t.assert.strictEqual(mockAgent.getCallHistory()?.lastCall()?.fullUrl, baseUrl + path)
t.assert.deepStrictEqual(mockAgent.getCallHistory()?.lastCall()?.searchParams, {})
t.assert.strictEqual(mockAgent.getCallHistory()?.lastCall()?.host, 'localhost:9999')
t.assert.strictEqual(mockAgent.getCallHistory()?.lastCall()?.port, '9999')
t.assert.strictEqual(mockAgent.getCallHistory()?.lastCall()?.protocol, 'http:')
})
test('MockAgent - clearCallHistory should clear call history logs', async (t) => {
t.plan(3)
const mockAgent = new MockAgent({ enableCallHistory: true })
setGlobalDispatcher(mockAgent)
after(() => mockAgent.close())
const baseUrl = 'http://localhost:9999'
const mockClient = mockAgent.get(baseUrl)
mockClient.intercept({
path: /^\/foo/,
method: 'POST'
}).reply(200, 'foo').persist()
t.assert.ok(mockAgent.getCallHistory()?.calls().length === 0)
const path = '/foo'
const url = new URL(path, baseUrl)
const method = 'POST'
const body = { data: 'value' }
const query = { a: 1 }
const headers = { 'content-type': 'application/json' }
await request(url, { method, query, body: JSON.stringify(body), headers })
await request(url, { method, query, body: JSON.stringify(body), headers })
await request(url, { method, query, body: JSON.stringify(body), headers })
await request(url, { method, query, body: JSON.stringify(body), headers })
t.assert.ok(mockAgent.getCallHistory()?.calls().length === 4)
mockAgent.clearCallHistory()
t.assert.ok(mockAgent.getCallHistory()?.calls().length === 0)
})
test('MockAgent - handle persists with delayed requests', async (t) => {
t.plan(4)
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.setHeader('content-type', 'text/plain')
res.end('should not be called')
t.assert.fail('should not be called')
})
after(() => {
server.closeAllConnections?.()
server.close()
})
await once(server.listen(0), 'listening')
const baseUrl = `http://localhost:${server.address().port}`
const mockAgent = new MockAgent()
setGlobalDispatcher(mockAgent)
after(() => mockAgent.close())
const mockPool = mockAgent.get(baseUrl)
mockPool.intercept({
path: '/foo',
method: 'POST'
}).reply(200, 'hello').delay(1).persist()
{
const { statusCode, body } = await request(`${baseUrl}/foo`, {
method: 'POST'
})
t.assert.strictEqual(statusCode, 200)
const response = await getResponse(body)
t.assert.strictEqual(response, 'hello')
}
{
const { statusCode, body } = await request(`${baseUrl}/foo`, {
method: 'POST'
})
t.assert.strictEqual(statusCode, 200)
const response = await getResponse(body)
t.assert.strictEqual(response, 'hello')
}
})
test('MockAgent - calling close on a mock pool should not affect other mock pools', async (t) => {
t.plan(4)
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.setHeader('content-type', 'text/plain')
res.end('should not be called')
t.assert.fail('should not be called')
})
after(() => {
server.closeAllConnections?.()
server.close()
})
await once(server.listen(0), 'listening')
const baseUrl = `http://localhost:${server.address().port}`
const mockAgent = new MockAgent()
setGlobalDispatcher(mockAgent)
after(() => mockAgent.close())
const mockPoolToClose = mockAgent.get('http://localhost:9999')
mockPoolToClose.intercept({
path: '/foo',
method: 'GET'
}).reply(200, 'should-not-be-returned')
const mockPool = mockAgent.get(baseUrl)
mockPool.intercept({
path: '/foo',
method: 'GET'
}).reply(200, 'foo')
mockPool.intercept({
path: '/bar',
method: 'POST'
}).reply(200, 'bar')
await mockPoolToClose.close()
{
const { statusCode, body } = await request(`${baseUrl}/foo`, {
method: 'GET'
})
t.assert.strictEqual(statusCode, 200)
const response = await getResponse(body)
t.assert.strictEqual(response, 'foo')
}
{
const { statusCode, body } = await request(`${baseUrl}/bar`, {
method: 'POST'
})
t.assert.strictEqual(statusCode, 200)
const response = await getResponse(body)
t.assert.strictEqual(response, 'bar')
}
})
test('MockAgent - close removes all registered mock clients', async (t) => {
t.plan(2)
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.setHeader('content-type', 'text/plain')
res.end('should not be called')
t.assert.fail('should not be called')
})
after(() => {
server.closeAllConnections?.()
server.close()
})
await once(server.listen(0), 'listening')
const baseUrl = `http://localhost:${server.address().port}`
const mockAgent = new MockAgent({ connections: 1 })
setGlobalDispatcher(mockAgent)
const mockClient = mockAgent.get(baseUrl)
mockClient.intercept({
path: '/foo',
method: 'GET'
}).reply(200, 'foo')
await mockAgent.close()
t.assert.strictEqual(mockAgent[kClients].size, 0)
try {
await request(`${baseUrl}/foo`, { method: 'GET' })
} catch (err) {
t.assert.ok(err instanceof ClientDestroyedError)
}
})
test('MockAgent - close clear all registered mock call history logs', async (t) => {
t.plan(2)
const mockAgent = new MockAgent({ enableCallHistory: true })
setGlobalDispatcher(mockAgent)
const mockClient = mockAgent.get('http://localhost:9999')
mockClient.intercept({
path: '/foo',
method: 'GET'
}).reply(200, 'foo')
await request('http://localhost:9999/foo')
t.assert.strictEqual(mockAgent.getCallHistory().calls().length, 1)
await mockAgent.close()
t.assert.strictEqual(mockAgent.getCallHistory().calls().length, 0)
})
test('MockAgent - close removes all registered mock pools', async (t) => {
t.plan(2)
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.setHeader('content-type', 'text/plain')
res.end('should not be called')
t.assert.fail('should not be called')
})
after(() => {
server.closeAllConnections?.()
server.close()
})
await once(server.listen(0), 'listening')
const baseUrl = `http://localhost:${server.address().port}`
const mockAgent = new MockAgent()
setGlobalDispatcher(mockAgent)
const mockPool = mockAgent.get(baseUrl)
mockPool.intercept({
path: '/foo',
method: 'GET'
}).reply(200, 'foo')
await mockAgent.close()
t.assert.strictEqual(mockAgent[kClients].size, 0)
try {
await request(`${baseUrl}/foo`, { method: 'GET' })
} catch (err) {
t.assert.ok(err instanceof ClientDestroyedError)
}
})
test('MockAgent - should handle replyWithError', async (t) => {
t.plan(1)
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.setHeader('content-type', 'text/plain')
res.end('should not be called')
t.assert.fail('should not be called')
})
after(() => {
server.closeAllConnections?.()
server.close()
})
await once(server.listen(0), 'listening')
const baseUrl = `http://localhost:${server.address().port}`
const mockAgent = new MockAgent()
setGlobalDispatcher(mockAgent)
after(() => mockAgent.close())
const mockPool = mockAgent.get(baseUrl)
mockPool.intercept({
path: '/foo',
method: 'GET'
}).replyWithError(new Error('kaboom'))
await t.assert.rejects(request(`${baseUrl}/foo`, { method: 'GET' }), new Error('kaboom'))
})
test('MockAgent - should support setting a reply to respond a set amount of times', async (t) => {
t.plan(9)
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
t.assert.strictEqual(req.url, '/foo')
t.assert.strictEqual(req.method, 'GET')
res.setHeader('content-type', 'text/plain')
res.end('hello')
})
after(() => {
server.closeAllConnections?.()
server.close()
})
await once(server.listen(0), 'listening')
const baseUrl = `http://localhost:${server.address().port}`
const mockAgent = new MockAgent()
setGlobalDispatcher(mockAgent)
after(() => mockAgent.close())
const mockPool = mockAgent.get(baseUrl)
mockPool.intercept({
path: '/foo',
method: 'GET'
}).reply(200, 'foo').times(2)
{
const { statusCode, body } = await request(`${baseUrl}/foo`)
t.assert.strictEqual(statusCode, 200)
const response = await getResponse(body)
t.assert.strictEqual(response, 'foo')
}
{
const { statusCode, body } = await request(`${baseUrl}/foo`)
t.assert.strictEqual(statusCode, 200)
const response = await getResponse(body)
t.assert.strictEqual(response, 'foo')
}
{
const { statusCode, headers, body } = await request(`${baseUrl}/foo`)
t.assert.strictEqual(statusCode, 200)
t.assert.strictEqual(headers['content-type'], 'text/plain')
const response = await getResponse(body)
t.assert.strictEqual(response, 'hello')
}
})
test('MockAgent - persist overrides times', async (t) => {
t.plan(6)
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.setHeader('content-type', 'text/plain')
res.end('should not be called')
t.assert.fail('should not be called')
})
after(() => {
server.closeAllConnections?.()
server.close()
})
await once(server.listen(0), 'listening')
const baseUrl = `http://localhost:${server.address().port}`
const mockAgent = new MockAgent()
setGlobalDispatcher(mockAgent)
after(() => mockAgent.close())
const mockPool = mockAgent.get(baseUrl)
mockPool.intercept({
path: '/foo',
method: 'GET'
}).reply(200, 'foo').times(2).persist()
{
const { statusCode, body } = await request(`${baseUrl}/foo`, {
method: 'GET'
})
t.assert.strictEqual(statusCode, 200)
const response = await getResponse(body)
t.assert.strictEqual(response, 'foo')
}
{
const { statusCode, body } = await request(`${baseUrl}/foo`, {
method: 'GET'
})
t.assert.strictEqual(statusCode, 200)
const response = await getResponse(body)
t.assert.strictEqual(response, 'foo')
}
{
const { statusCode, body } = await request(`${baseUrl}/foo`, {
method: 'GET'
})
t.assert.strictEqual(statusCode, 200)
const response = await getResponse(body)
t.assert.strictEqual(response, 'foo')
}
})
test('MockAgent - matcher should not find mock dispatch if path is of unsupported type', async (t) => {
t.plan(4)
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
t.assert.strictEqual(req.url, '/foo')
t.assert.strictEqual(req.method, 'GET')
res.end('hello')
})
after(() => {
server.closeAllConnections?.()
server.close()
})
await once(server.listen(0), 'listening')
const baseUrl = `http://localhost:${server.address().port}`
const mockAgent = new MockAgent()
setGlobalDispatcher(mockAgent)
after(() => mockAgent.close())
const mockPool = mockAgent.get(baseUrl)
mockPool.intercept({
path: {},
method: 'GET'
}).reply(200, 'foo')
const { statusCode, body } = await request(`${baseUrl}/foo`, {
method: 'GET'
})
t.assert.strictEqual(statusCode, 200)
const response = await getResponse(body)
t.assert.strictEqual(response, 'hello')
})
test('MockAgent - should match path with regex', async (t) => {
t.plan(4)
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.setHeader('content-type', 'text/plain')
res.end('should not be called')
t.assert.fail('should not be called')
})
after(() => {
server.closeAllConnections?.()
server.close()
})
await once(server.listen(0), 'listening')
const baseUrl = `http://localhost:${server.address().port}`
const mockAgent = new MockAgent()
setGlobalDispatcher(mockAgent)
after(() => mockAgent.close())
const mockPool = mockAgent.get(baseUrl)
mockPool.intercept({
path: /foo/,
method: 'GET'
}).reply(200, 'foo').persist()
{
const { statusCode, body } = await request(`${baseUrl}/foo`, {
method: 'GET'
})
t.assert.strictEqual(statusCode, 200)
const response = await getResponse(body)
t.assert.strictEqual(response, 'foo')
}
{
const { statusCode, body } = await request(`${baseUrl}/hello/foobar`, {
method: 'GET'
})
t.assert.strictEqual(statusCode, 200)
const response = await getResponse(body)
t.assert.strictEqual(response, 'foo')
}
})
test('MockAgent - should match path with function', async (t) => {
t.plan(2)
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.setHeader('content-type', 'text/plain')
res.end('should not be called')
t.assert.fail('should not be called')
})
after(() => {
server.closeAllConnections?.()
server.close()
})
await once(server.listen(0), 'listening')
const baseUrl = `http://localhost:${server.address().port}`
const mockAgent = new MockAgent()
setGlobalDispatcher(mockAgent)
after(() => mockAgent.close())
const mockPool = mockAgent.get(baseUrl)
mockPool.intercept({
path: (value) => value === '/foo',
method: 'GET'
}).reply(200, 'foo')
const { statusCode, body } = await request(`${baseUrl}/foo`, {
method: 'GET'
})
t.assert.strictEqual(statusCode, 200)
const response = await getResponse(body)
t.assert.strictEqual(response, 'foo')
})
test('MockAgent - should match method with regex', async (t) => {
t.plan(2)
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.setHeader('content-type', 'text/plain')
res.end('should not be called')
t.assert.fail('should not be called')
})
after(() => {
server.closeAllConnections?.()
server.close()
})
await once(server.listen(0), 'listening')
const baseUrl = `http://localhost:${server.address().port}`
const mockAgent = new MockAgent()
setGlobalDispatcher(mockAgent)
after(() => mockAgent.close())
const mockPool = mockAgent.get(baseUrl)
mockPool.intercept({
path: '/foo',
method: /^GET$/
}).reply(200, 'foo')
const { statusCode, body } = await request(`${baseUrl}/foo`, {
method: 'GET'
})
t.assert.strictEqual(statusCode, 200)
const response = await getResponse(body)
t.assert.strictEqual(response, 'foo')
})
test('MockAgent - should match method with function', async (t) => {
t.plan(2)
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.setHeader('content-type', 'text/plain')
res.end('should not be called')
t.assert.fail('should not be called')
})
after(() => {
server.closeAllConnections?.()
server.close()
})
await once(server.listen(0), 'listening')
const baseUrl = `http://localhost:${server.address().port}`
const mockAgent = new MockAgent()
setGlobalDispatcher(mockAgent)
after(() => mockAgent.close())
const mockPool = mockAgent.get(baseUrl)
mockPool.intercept({
path: '/foo',
method: (value) => value === 'GET'
}).reply(200, 'foo')
const { statusCode, body } = await request(`${baseUrl}/foo`, {
method: 'GET'
})
t.assert.strictEqual(statusCode, 200)
const response = await getResponse(body)
t.assert.strictEqual(response, 'foo')
})
test('MockAgent - should match body with regex', async (t) => {
t.plan(2)
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.setHeader('content-type', 'text/plain')
res.end('should not be called')
t.assert.fail('should not be called')
})
after(() => {
server.closeAllConnections?.()
server.close()
})
await once(server.listen(0), 'listening')
const baseUrl = `http://localhost:${server.address().port}`
const mockAgent = new MockAgent()
setGlobalDispatcher(mockAgent)
after(() => mockAgent.close())
const mockPool = mockAgent.get(baseUrl)
mockPool.intercept({
path: '/foo',
method: 'GET',
body: /hello/
}).reply(200, 'foo')
const { statusCode, body } = await request(`${baseUrl}/foo`, {
method: 'GET',
body: 'hello=there'
})
t.assert.strictEqual(statusCode, 200)
const response = await getResponse(body)
t.assert.strictEqual(response, 'foo')
})
test('MockAgent - should match body with function', async (t) => {
t.plan(2)
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.setHeader('content-type', 'text/plain')
res.end('should not be called')
t.assert.fail('should not be called')
})
after(() => {
server.closeAllConnections?.()
server.close()
})
await once(server.listen(0), 'listening')
const baseUrl = `http://localhost:${server.address().port}`
const mockAgent = new MockAgent()
setGlobalDispatcher(mockAgent)
after(() => mockAgent.close())
const mockPool = mockAgent.get(baseUrl)
mockPool.intercept({
path: '/foo',
method: 'GET',
body: (value) => value.startsWith('hello')
}).reply(200, 'foo')
const { statusCode, body } = await request(`${baseUrl}/foo`, {
method: 'GET',
body: 'hello=there'
})
t.assert.strictEqual(statusCode, 200)
const response = await getResponse(body)
t.assert.strictEqual(response, 'foo')
})
test('MockAgent - should match headers with string', async (t) => {
t.plan(6)
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.end('should not be called')
t.assert.fail('should not be called')
})
after(() => {
server.closeAllConnections?.()
server.close()
})
await once(server.listen(0), 'listening')
const baseUrl = `http://localhost:${server.address().port}`
const mockAgent = new MockAgent()
setGlobalDispatcher(mockAgent)
after(() => mockAgent.close())
const mockPool = mockAgent.get(baseUrl)
mockPool.intercept({
path: '/foo',
method: 'GET',
headers: {
'User-Agent': 'undici',
Host: 'example.com'
}
}).reply(200, 'foo')
// Disable net connect so we can make sure it matches properly
mockAgent.disableNetConnect()
await t.assert.rejects(request(`${baseUrl}/foo`, {
method: 'GET'
}), MockNotMatchedError, 'should reject with MockNotMatchedError')
await t.assert.rejects(request(`${baseUrl}/foo`, {
method: 'GET',
headers: {
foo: 'bar'
}
}), MockNotMatchedError, 'should reject with MockNotMatchedError')
await t.assert.rejects(request(`${baseUrl}/foo`, {
method: 'GET',
headers: {
foo: 'bar',
'User-Agent': 'undici'
}
}), MockNotMatchedError, 'should reject with MockNotMatchedError')
await t.assert.rejects(request(`${baseUrl}/foo`, {
method: 'GET',
headers: {
foo: 'bar',
'User-Agent': 'undici',
Host: 'wrong'
}
}), MockNotMatchedError, 'should reject with MockNotMatchedError')
const { statusCode, body } = await request(`${baseUrl}/foo`, {
method: 'GET',
headers: {
foo: 'bar',
'User-Agent': 'undici',
Host: 'example.com'
}
})
t.assert.strictEqual(statusCode, 200)
const response = await getResponse(body)
t.assert.strictEqual(response, 'foo')
})
test('MockAgent - should match headers with regex', async (t) => {
t.plan(6)
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.end('should not be called')
t.assert.fail('should not be called')
})
after(() => {
server.closeAllConnections?.()
server.close()
})
await once(server.listen(0), 'listening')
const baseUrl = `http://localhost:${server.address().port}`
const mockAgent = new MockAgent()
setGlobalDispatcher(mockAgent)
after(() => mockAgent.close())
const mockPool = mockAgent.get(baseUrl)
mockPool.intercept({
path: '/foo',
method: 'GET',
headers: {
'User-Agent': /^undici$/,
Host: /^example.com$/
}
}).reply(200, 'foo')
// Disable net connect so we can make sure it matches properly
mockAgent.disableNetConnect()
await t.assert.rejects(request(`${baseUrl}/foo`, {
method: 'GET'
}), MockNotMatchedError, 'should reject with MockNotMatchedError')
await t.assert.rejects(request(`${baseUrl}/foo`, {
method: 'GET',
headers: {
foo: 'bar'
}
}), MockNotMatchedError, 'should reject with MockNotMatchedError')
await t.assert.rejects(request(`${baseUrl}/foo`, {
method: 'GET',
headers: {
foo: 'bar',
'User-Agent': 'undici'
}
}), MockNotMatchedError, 'should reject with MockNotMatchedError')
await t.assert.rejects(request(`${baseUrl}/foo`, {
method: 'GET',
headers: {
foo: 'bar',
'User-Agent': 'undici',
Host: 'wrong'
}
}), MockNotMatchedError, 'should reject with MockNotMatchedError')
const { statusCode, body } = await request(`${baseUrl}/foo`, {
method: 'GET',
headers: {
foo: 'bar',
'User-Agent': 'undici',
Host: 'example.com'
}
})
t.assert.strictEqual(statusCode, 200)
const response = await getResponse(body)
t.assert.strictEqual(response, 'foo')
})
test('MockAgent - should match headers with function', async (t) => {
t.plan(6)
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.end('should not be called')
t.assert.fail('should not be called')
})
after(() => {
server.closeAllConnections?.()
server.close()
})
await once(server.listen(0), 'listening')
const baseUrl = `http://localhost:${server.address().port}`
const mockAgent = new MockAgent()
setGlobalDispatcher(mockAgent)
after(() => mockAgent.close())
const mockPool = mockAgent.get(baseUrl)
mockPool.intercept({
path: '/foo',
method: 'GET',
headers: {
'User-Agent': (value) => value === 'undici',
Host: (value) => value === 'example.com'
}
}).reply(200, 'foo')
// Disable net connect so we can make sure it matches properly
mockAgent.disableNetConnect()
await t.assert.rejects(request(`${baseUrl}/foo`, {
method: 'GET'
}), MockNotMatchedError, 'should reject with MockNotMatchedError')
await t.assert.rejects(request(`${baseUrl}/foo`, {
method: 'GET',
headers: {
foo: 'bar'
}
}), MockNotMatchedError, 'should reject with MockNotMatchedError')
await t.assert.rejects(request(`${baseUrl}/foo`, {
method: 'GET',
headers: {
foo: 'bar',
'User-Agent': 'undici'
}
}), MockNotMatchedError, 'should reject with MockNotMatchedError')
await t.assert.rejects(request(`${baseUrl}/foo`, {
method: 'GET',
headers: {
foo: 'bar',
'User-Agent': 'undici',
Host: 'wrong'
}
}), MockNotMatchedError, 'should reject with MockNotMatchedError')
const { statusCode, body } = await request(`${baseUrl}/foo`, {
method: 'GET',
headers: {
foo: 'bar',
'User-Agent': 'undici',
Host: 'example.com'
}
})
t.assert.strictEqual(statusCode, 200)
const response = await getResponse(body)
t.assert.strictEqual(response, 'foo')
})
test('MockAgent - should match url with regex', async (t) => {
t.plan(2)
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.setHeader('content-type', 'text/plain')
res.end('should not be called')
t.assert.fail('should not be called')
})
after(() => {
server.closeAllConnections?.()
server.close()
})
await once(server.listen(0), 'listening')
const baseUrl = `http://localhost:${server.address().port}`
const mockAgent = new MockAgent()
setGlobalDispatcher(mockAgent)
after(() => mockAgent.close())
const mockPool = mockAgent.get(new RegExp(baseUrl))
mockPool.intercept({
path: '/foo',
method: 'GET'
}).reply(200, 'foo')
const { statusCode, body } = await request(`${baseUrl}/foo`, {
method: 'GET'
})
t.assert.strictEqual(statusCode, 200)
const response = await getResponse(body)
t.assert.strictEqual(response, 'foo')
})
test('MockAgent - should match url with function', async (t) => {
t.plan(2)
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.setHeader('content-type', 'text/plain')
res.end('should not be called')
t.assert.fail('should not be called')
})
after(() => {
server.closeAllConnections?.()
server.close()
})
await once(server.listen(0), 'listening')
const baseUrl = `http://localhost:${server.address().port}`
const mockAgent = new MockAgent()
setGlobalDispatcher(mockAgent)
after(() => mockAgent.close())
const mockPool = mockAgent.get((value) => baseUrl === value)
mockPool.intercept({
path: '/foo',
method: 'GET'
}).reply(200, 'foo')
const { statusCode, body } = await request(`${baseUrl}/foo`, {
method: 'GET'
})
t.assert.strictEqual(statusCode, 200)
const response = await getResponse(body)
t.assert.strictEqual(response, 'foo')
})
test('MockAgent - handle default reply headers', async (t) => {
t.plan(3)
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.setHeader('content-type', 'text/plain')
res.end('should not be called')
t.assert.fail('should not be called')
})
after(() => {
server.closeAllConnections?.()
server.close()
})
await once(server.listen(0), 'listening')
const baseUrl = `http://localhost:${server.address().port}`
const mockAgent = new MockAgent()
setGlobalDispatcher(mockAgent)
after(() => mockAgent.close())
const mockPool = mockAgent.get(baseUrl)
mockPool.intercept({
path: '/foo',
method: 'GET'
}).defaultReplyHeaders({ foo: 'bar' }).reply(200, 'foo', { headers: { hello: 'there' } })
const { statusCode, headers, body } = await request(`${baseUrl}/foo`, {
method: 'GET'
})
t.assert.strictEqual(statusCode, 200)
t.assert.deepStrictEqual(headers, {
foo: 'bar',
hello: 'there'
})
const response = await getResponse(body)
t.assert.strictEqual(response, 'foo')
})
test('MockAgent - handle default reply trailers', async (t) => {
t.plan(3)
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.setHeader('content-type', 'text/plain')
res.end('should not be called')
t.assert.fail('should not be called')
})
after(() => {
server.closeAllConnections?.()
server.close()
})
await once(server.listen(0), 'listening')
const baseUrl = `http://localhost:${server.address().port}`
const mockAgent = new MockAgent()
setGlobalDispatcher(mockAgent)
after(() => mockAgent.close())
const mockPool = mockAgent.get(baseUrl)
mockPool.intercept({
path: '/foo',
method: 'GET'
}).defaultReplyTrailers({ foo: 'bar' }).reply(200, 'foo', { trailers: { hello: 'there' } })
const { statusCode, trailers, body } = await request(`${baseUrl}/foo`, {
method: 'GET'
})
t.assert.strictEqual(statusCode, 200)
t.assert.deepStrictEqual(trailers, {
foo: 'bar',
hello: 'there'
})
const response = await getResponse(body)
t.assert.strictEqual(response, 'foo')
})
test('MockAgent - return calculated content-length if specified', async (t) => {
t.plan(3)
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.setHeader('content-type', 'text/plain')
res.end('should not be called')
t.assert.fail('should not be called')
})
after(() => {
server.closeAllConnections?.()
server.close()
})
await once(server.listen(0), 'listening')
const baseUrl = `http://localhost:${server.address().port}`
const mockAgent = new MockAgent()
setGlobalDispatcher(mockAgent)
after(() => mockAgent.close())
const mockPool = mockAgent.get(baseUrl)
mockPool.intercept({
path: '/foo',
method: 'GET'
}).replyContentLength().reply(200, 'foo', { headers: { hello: 'there' } })
const { statusCode, headers, body } = await request(`${baseUrl}/foo`, {
method: 'GET'
})
t.assert.strictEqual(statusCode, 200)
t.assert.deepStrictEqual(headers, {
hello: 'there',
'content-length': '3'
})
const response = await getResponse(body)
t.assert.strictEqual(response, 'foo')
})
test('MockAgent - return calculated content-length for object response if specified', async (t) => {
t.plan(3)
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.setHeader('content-type', 'text/plain')
res.end('should not be called')
t.assert.fail('should not be called')
})
after(() => {
server.closeAllConnections?.()
server.close()
})
await once(server.listen(0), 'listening')
const baseUrl = `http://localhost:${server.address().port}`
const mockAgent = new MockAgent()
setGlobalDispatcher(mockAgent)
after(() => mockAgent.close())
const mockPool = mockAgent.get(baseUrl)
mockPool.intercept({
path: '/foo',
method: 'GET'
}).replyContentLength().reply(200, { foo: 'bar' }, { headers: { hello: 'there' } })
const { statusCode, headers, body } = await request(`${baseUrl}/foo`, {
method: 'GET'
})
t.assert.strictEqual(statusCode, 200)
t.assert.deepStrictEqual(headers, {
hello: 'there',
'content-length': '13'
})
const jsonResponse = JSON.parse(await getResponse(body))
t.assert.deepStrictEqual(jsonResponse, { foo: 'bar' })
})
test('MockAgent - should activate and deactivate mock clients', async (t) => {
t.plan(9)
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
t.assert.strictEqual(req.url, '/foo')
t.assert.strictEqual(req.method, 'GET')
res.setHeader('content-type', 'text/plain')
res.end('hello')
})
after(() => {
server.closeAllConnections?.()
server.close()
})
await once(server.listen(0), 'listening')
const baseUrl = `http://localhost:${server.address().port}`
const mockAgent = new MockAgent()
setGlobalDispatcher(mockAgent)
after(() => mockAgent.close())
const mockPool = mockAgent.get(baseUrl)
mockPool.intercept({
path: '/foo',
method: 'GET'
}).reply(200, 'foo').persist()
{
const { statusCode, body } = await request(`${baseUrl}/foo`, {
method: 'GET'
})
t.assert.strictEqual(statusCode, 200)
const response = await getResponse(body)
t.assert.strictEqual(response, 'foo')
}
mockAgent.deactivate()
{
const { statusCode, headers, body } = await request(`${baseUrl}/foo`, {
method: 'GET'
})
t.assert.strictEqual(statusCode, 200)
t.assert.strictEqual(headers['content-type'], 'text/plain')
const response = await getResponse(body)
t.assert.strictEqual(response, 'hello')
}
mockAgent.activate()
{
const { statusCode, body } = await request(`${baseUrl}/foo`, {
method: 'GET'
})
t.assert.strictEqual(statusCode, 200)
const response = await getResponse(body)
t.assert.strictEqual(response, 'foo')
}
})
test('MockAgent - enableNetConnect should allow all original dispatches to be called if dispatch not found', async (t) => {
t.plan(5)
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
t.assert.strictEqual(req.url, '/foo')
t.assert.strictEqual(req.method, 'GET')
res.setHeader('content-type', 'text/plain')
res.end('hello')
})
after(() => {
server.closeAllConnections?.()
server.close()
})
await once(server.listen(0), 'listening')
const baseUrl = `http://localhost:${server.address().port}`
const mockAgent = new MockAgent()
setGlobalDispatcher(mockAgent)
after(() => mockAgent.close())
const mockPool = mockAgent.get(baseUrl)
mockPool.intercept({
path: '/wrong',
method: 'GET'
}).reply(200, 'foo')
mockAgent.enableNetConnect()
const { statusCode, headers, body } = await request(`${baseUrl}/foo`, {
method: 'GET'
})
t.assert.strictEqual(statusCode, 200)
t.assert.strictEqual(headers['content-type'], 'text/plain')
const response = await getResponse(body)
t.assert.strictEqual(response, 'hello')
})
test('MockAgent - enableNetConnect with a host string should allow all original dispatches to be called if mockDispatch not found', async (t) => {
t.plan(5)
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
t.assert.strictEqual(req.url, '/foo')
t.assert.strictEqual(req.method, 'GET')
res.setHeader('content-type', 'text/plain')
res.end('hello')
})
after(() => {
server.closeAllConnections?.()
server.close()
})
await once(server.listen(0), 'listening')
const baseUrl = `http://localhost:${server.address().port}`
const mockAgent = new MockAgent()
setGlobalDispatcher(mockAgent)
after(() => mockAgent.close())
const mockPool = mockAgent.get(baseUrl)
mockPool.intercept({
path: '/wrong',
method: 'GET'
}).reply(200, 'foo')
mockAgent.enableNetConnect(`localhost:${server.address().port}`)
const { statusCode, headers, body } = await request(`${baseUrl}/foo`, {
method: 'GET'
})
t.assert.strictEqual(statusCode, 200)
t.assert.strictEqual(headers['content-type'], 'text/plain')
const response = await getResponse(body)
t.assert.strictEqual(response, 'hello')
})
test('MockAgent - enableNetConnect when called with host string multiple times should allow all original dispatches to be called if mockDispatch not found', async (t) => {
t.plan(5)
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
t.assert.strictEqual(req.url, '/foo')
t.assert.strictEqual(req.method, 'GET')
res.setHeader('content-type', 'text/plain')
res.end('hello')
})
after(() => {
server.closeAllConnections?.()
server.close()
})
await once(server.listen(0), 'listening')
const baseUrl = `http://localhost:${server.address().port}`
const mockAgent = new MockAgent()
setGlobalDispatcher(mockAgent)
after(() => mockAgent.close())
const mockPool = mockAgent.get(baseUrl)
mockPool.intercept({
path: '/wrong',
method: 'GET'
}).reply(200, 'foo')
mockAgent.enableNetConnect('example.com:9999')
mockAgent.enableNetConnect(`localhost:${server.address().port}`)
const { statusCode, headers, body } = await request(`${baseUrl}/foo`, {
method: 'GET'
})
t.assert.strictEqual(statusCode, 200)
t.assert.strictEqual(headers['content-type'], 'text/plain')
const response = await getResponse(body)
t.assert.strictEqual(response, 'hello')
})
test('MockAgent - enableNetConnect with a host regex should allow all original dispatches to be called if mockDispatch not found', async (t) => {
t.plan(5)
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
t.assert.strictEqual(req.url, '/foo')
t.assert.strictEqual(req.method, 'GET')
res.setHeader('content-type', 'text/plain')
res.end('hello')
})
after(() => {
server.closeAllConnections?.()
server.close()
})
await once(server.listen(0), 'listening')
const baseUrl = `http://localhost:${server.address().port}`
const mockAgent = new MockAgent()
setGlobalDispatcher(mockAgent)
after(() => mockAgent.close())
const mockPool = mockAgent.get(baseUrl)
mockPool.intercept({
path: '/wrong',
method: 'GET'
}).reply(200, 'foo')
mockAgent.enableNetConnect(new RegExp(`localhost:${server.address().port}`))
const { statusCode, headers, body } = await request(`${baseUrl}/foo`, {
method: 'GET'
})
t.assert.strictEqual(statusCode, 200)
t.assert.strictEqual(headers['content-type'], 'text/plain')
const response = await getResponse(body)
t.assert.strictEqual(response, 'hello')
})
test('MockAgent - enableNetConnect with a function should allow all original dispatches to be called if mockDispatch not found', async (t) => {
t.plan(5)
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
t.assert.strictEqual(req.url, '/foo')
t.assert.strictEqual(req.method, 'GET')
res.setHeader('content-type', 'text/plain')
res.end('hello')
})
after(() => {
server.closeAllConnections?.()
server.close()
})
await once(server.listen(0), 'listening')
const baseUrl = `http://localhost:${server.address().port}`
const mockAgent = new MockAgent()
setGlobalDispatcher(mockAgent)
after(() => mockAgent.close())
const mockPool = mockAgent.get(baseUrl)
mockPool.intercept({
path: '/wrong',
method: 'GET'
}).reply(200, 'foo')
mockAgent.enableNetConnect((value) => value === `localhost:${server.address().port}`)
const { statusCode, headers, body } = await request(`${baseUrl}/foo`, {
method: 'GET'
})
t.assert.strictEqual(statusCode, 200)
t.assert.strictEqual(headers['content-type'], 'text/plain')
const response = await getResponse(body)
t.assert.strictEqual(response, 'hello')
})
test('MockAgent - enableNetConnect with an unknown input should throw', async (t) => {
t.plan(1)
const mockAgent = new MockAgent()
setGlobalDispatcher(mockAgent)
after(() => mockAgent.close())
const mockPool = mockAgent.get('http://localhost:9999')
mockPool.intercept({
path: '/wrong',
method: 'GET'
}).reply(200, 'foo')
t.assert.throws(() => mockAgent.enableNetConnect({}), new InvalidArgumentError('Unsupported matcher. Must be one of String|Function|RegExp.'))
})
test('MockAgent - enableNetConnect should throw if dispatch not matched for path and the origin was not allowed by net connect', async (t) => {
t.plan(1)
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
t.assert.fail('should not be called')
res.end('should not be called')
})
after(() => {
server.closeAllConnections?.()
server.close()
})
await once(server.listen(0), 'listening')
const baseUrl = `http://localhost:${server.address().port}`
const mockAgent = new MockAgent()
setGlobalDispatcher(mockAgent)
after(() => mockAgent.close())
const mockPool = mockAgent.get(baseUrl)
mockPool.intercept({
path: '/foo',
method: 'GET'
}).reply(200, 'foo')
mockAgent.enableNetConnect('example.com:9999')
await t.assert.rejects(request(`${baseUrl}/wrong`, {
method: 'GET'
}), new MockNotMatchedError(`Mock dispatch not matched for path '/wrong': subsequent request to origin ${baseUrl} was not allowed (net.connect is not enabled for this origin)`))
})
test('MockAgent - enableNetConnect should throw if dispatch not matched for method and the origin was not allowed by net connect', async (t) => {
t.plan(1)
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
t.assert.fail('should not be called')
res.end('should not be called')
})
after(() => {
server.closeAllConnections?.()
server.close()
})
await once(server.listen(0), 'listening')
const baseUrl = `http://localhost:${server.address().port}`
const mockAgent = new MockAgent()
setGlobalDispatcher(mockAgent)
after(() => mockAgent.close())
const mockPool = mockAgent.get(baseUrl)
mockPool.intercept({
path: '/foo',
method: 'GET'
}).reply(200, 'foo')
mockAgent.enableNetConnect('example.com:9999')
await t.assert.rejects(request(`${baseUrl}/foo`, {
method: 'WRONG'
}), new MockNotMatchedError(`Mock dispatch not matched for method 'WRONG' on path '/foo': subsequent request to origin ${baseUrl} was not allowed (net.connect is not enabled for this origin)`))
})
test('MockAgent - enableNetConnect should throw if dispatch not matched for body and the origin was not allowed by net connect', async (t) => {
t.plan(1)
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
t.assert.fail('should not be called')
res.end('should not be called')
})
after(() => {
server.closeAllConnections?.()
server.close()
})
await once(server.listen(0), 'listening')
const baseUrl = `http://localhost:${server.address().port}`
const mockAgent = new MockAgent()
setGlobalDispatcher(mockAgent)
after(() => mockAgent.close())
const mockPool = mockAgent.get(baseUrl)
mockPool.intercept({
path: '/foo',
method: 'GET',
body: 'hello'
}).reply(200, 'foo')
mockAgent.enableNetConnect('example.com:9999')
await t.assert.rejects(request(`${baseUrl}/foo`, {
method: 'GET',
body: 'wrong'
}), new MockNotMatchedError(`Mock dispatch not matched for body 'wrong' on path '/foo': subsequent request to origin ${baseUrl} was not allowed (net.connect is not enabled for this origin)`))
})
test('MockAgent - enableNetConnect should throw if dispatch not matched for headers and the origin was not allowed by net connect', async (t) => {
t.plan(1)
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
t.assert.fail('should not be called')
res.end('should not be called')
})
after(() => {
server.closeAllConnections?.()
server.close()
})
await once(server.listen(0), 'listening')
const baseUrl = `http://localhost:${server.address().port}`
const mockAgent = new MockAgent()
setGlobalDispatcher(mockAgent)
after(() => mockAgent.close())
const mockPool = mockAgent.get(baseUrl)
mockPool.intercept({
path: '/foo',
method: 'GET',
headers: {
'User-Agent': 'undici'
}
}).reply(200, 'foo')
mockAgent.enableNetConnect('example.com:9999')
await t.assert.rejects(request(`${baseUrl}/foo`, {
method: 'GET',
headers: {
'User-Agent': 'wrong'
}
}), new MockNotMatchedError(`Mock dispatch not matched for headers '{"User-Agent":"wrong"}' on path '/foo': subsequent request to origin ${baseUrl} was not allowed (net.connect is not enabled for this origin)`))
})
test('MockAgent - disableNetConnect should throw if dispatch not found by net connect', async (t) => {
t.plan(1)
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
t.assert.strictEqual(req.url, '/foo')
t.assert.strictEqual(req.method, 'GET')
res.setHeader('content-type', 'text/plain')
res.end('hello')
})
after(() => {
server.closeAllConnections?.()
server.close()
})
await once(server.listen(0), 'listening')
const baseUrl = `http://localhost:${server.address().port}`
const mockAgent = new MockAgent()
setGlobalDispatcher(mockAgent)
after(() => mockAgent.close())
const mockPool = mockAgent.get(baseUrl)
mockPool.intercept({
path: '/wrong',
method: 'GET'
}).reply(200, 'foo')
mockAgent.disableNetConnect()
await t.assert.rejects(request(`${baseUrl}/foo`, {
method: 'GET'
}), new MockNotMatchedError(`Mock dispatch not matched for path '/foo': subsequent request to origin ${baseUrl} was not allowed (net.connect disabled)`))
})
test('MockAgent - headers function interceptor', async (t) => {
t.plan(8)
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
t.assert.fail('should not be called')
res.end('should not be called')
})
after(() => {
server.closeAllConnections?.()
server.close()
})
await once(server.listen(0), 'listening')
const baseUrl = `http://localhost:${server.address().port}`
const mockAgent = new MockAgent()
setGlobalDispatcher(mockAgent)
after(() => mockAgent.close())
const mockPool = mockAgent.get(baseUrl)
// Disable net connect so we can make sure it matches properly
mockAgent.disableNetConnect()
mockPool.intercept({
path: '/foo',
method: 'GET',
headers (headers) {
t.assert.strictEqual(typeof headers, 'object')
return !Object.keys(headers).includes('authorization')
}
}).reply(200, 'foo').times(3)
await t.assert.rejects(request(`${baseUrl}/foo`, {
method: 'GET',
headers: {
Authorization: 'Bearer foo'
}
}), new MockNotMatchedError(`Mock dispatch not matched for headers '{"Authorization":"Bearer foo"}' on path '/foo': subsequent request to origin ${baseUrl} was not allowed (net.connect disabled)`))
await t.assert.rejects(request(`${baseUrl}/foo`, {
method: 'GET',
headers: ['Authorization', 'Bearer foo']
}), new MockNotMatchedError(`Mock dispatch not matched for headers '["Authorization","Bearer foo"]' on path '/foo': subsequent request to origin ${baseUrl} was not allowed (net.connect disabled)`))
{
const { statusCode } = await request(`${baseUrl}/foo`, {
method: 'GET',
headers: {
foo: 'bar'
}
})
t.assert.strictEqual(statusCode, 200)
}
{
const { statusCode } = await request(`${baseUrl}/foo`, {
method: 'GET'
})
t.assert.strictEqual(statusCode, 200)
}
})
test('MockAgent - clients are not garbage collected', async (t) => {
const samples = 250
t.plan(2)
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
t.assert.fail('should not be called')
res.end('should not be called')
})
after(() => {
server.closeAllConnections?.()
server.close()
})
await once(server.listen(0), 'listening')
const baseUrl = `http://localhost:${server.address().port}`
// Create the dispatcher and disable net connect so we can make sure it matches properly
const dispatcher = new MockAgent()
dispatcher.disableNetConnect()
// When Node 16 is the minimum supported, this can be replaced by simply requiring setTimeout from timers/promises
function sleep (delay) {
return new Promise(resolve => {
setTimeout(resolve, delay)
})
}
// Purposely create the pool inside a function so that the reference is lost
function intercept () {
// Create the pool and add a lot of intercepts
const pool = dispatcher.get(baseUrl)
for (let i = 0; i < samples; i++) {
pool.intercept({
path: `/foo/${i}`,
method: 'GET'
}).reply(200, Buffer.alloc(1024 * 1024))
}
}
intercept()
const results = new Set()
for (let i = 0; i < samples; i++) {
// Let's make some time pass to allow garbage collection to happen
await sleep(10)
const { statusCode } = await request(`${baseUrl}/foo/${i}`, { method: 'GET', dispatcher })
results.add(statusCode)
}
t.assert.strictEqual(results.size, 1)
t.assert.ok(results.has(200))
})
// https://github.com/nodejs/undici/issues/1321
test('MockAgent - using fetch yields correct statusText', async (t) => {
t.plan(4)
const mockAgent = new MockAgent()
mockAgent.disableNetConnect()
setGlobalDispatcher(mockAgent)
after(() => mockAgent.close())
const mockPool = mockAgent.get('http://localhost:3000')
mockPool.intercept({
path: '/statusText',
method: 'GET'
}).reply(200, 'Body')
const { status, statusText } = await fetch('http://localhost:3000/statusText')
t.assert.strictEqual(status, 200)
t.assert.strictEqual(statusText, 'OK')
mockPool.intercept({
path: '/unknownStatusText',
method: 'GET'
}).reply(420, 'Everyday')
const unknownStatusCodeRes = await fetch('http://localhost:3000/unknownStatusText')
t.assert.strictEqual(unknownStatusCodeRes.status, 420)
t.assert.strictEqual(unknownStatusCodeRes.statusText, 'unknown')
})
// https://github.com/nodejs/undici/issues/1556
test('MockAgent - using fetch yields a headers object in the reply callback', async (t) => {
t.plan(1)
const mockAgent = new MockAgent()
mockAgent.disableNetConnect()
after(() => mockAgent.close())
const mockPool = mockAgent.get('http://localhost:3000')
mockPool.intercept({
path: '/headers',
method: 'GET'
}).reply(200, (opts) => {
t.assert.deepStrictEqual(opts.headers, {
accept: '*/*',
'accept-language': '*',
'sec-fetch-mode': 'cors',
'user-agent': 'undici',
'accept-encoding': 'gzip, deflate'
})
return {}
})
await fetch('http://localhost:3000/headers', {
dispatcher: mockAgent
})
})
// https://github.com/nodejs/undici/issues/1579
test('MockAgent - headers in mock dispatcher intercept should be case-insensitive', async (t) => {
t.plan(1)
const mockAgent = new MockAgent()
mockAgent.disableNetConnect()
setGlobalDispatcher(mockAgent)
after(() => mockAgent.close())
const mockPool = mockAgent.get('https://example.com')
mockPool
.intercept({
path: '/',
headers: {
authorization: 'Bearer 12345',
'USER-agent': 'undici'
}
})
.reply(200)
await fetch('https://example.com', {
headers: {
Authorization: 'Bearer 12345',
'user-AGENT': 'undici'
}
})
t.assert.ok(true, 'end')
})
// https://github.com/nodejs/undici/issues/1757
test('MockAgent - reply callback can be asynchronous', async (t) => {
t.plan(2)
class MiniflareDispatcher extends Dispatcher {
constructor (inner, options) {
super(options)
this.inner = inner
}
dispatch (options, handler) {
return this.inner.dispatch(options, handler)
}
close (...args) {
return this.inner.close(...args)
}
destroy (...args) {
return this.inner.destroy(...args)
}
}
const mockAgent = new MockAgent()
const mockClient = mockAgent.get('http://localhost:3000')
mockAgent.disableNetConnect()
setGlobalDispatcher(new MiniflareDispatcher(mockAgent))
after(() => mockAgent.close())
mockClient.intercept({
path: () => true,
method: () => true
}).reply(200, async (opts) => {
if (opts.body && opts.body[Symbol.asyncIterator]) {
const chunks = []
for await (const chunk of opts.body) {
chunks.push(chunk)
}
return Buffer.concat(chunks)
}
return opts.body
}).persist()
{
const response = await fetch('http://localhost:3000', {
method: 'POST',
body: JSON.stringify({ foo: 'bar' })
})
t.assert.deepStrictEqual(await response.json(), { foo: 'bar' })
}
{
const response = await fetch('http://localhost:3000', {
method: 'POST',
body: new ReadableStream({
start (controller) {
controller.enqueue(new TextEncoder().encode('{"foo":'))
setTimeout(() => {
controller.enqueue(new TextEncoder().encode('"bar"}'))
controller.close()
}, 100)
}
}),
duplex: 'half'
})
t.assert.deepStrictEqual(await response.json(), { foo: 'bar' })
}
})
test('MockAgent - headers should be array of strings', async (t) => {
t.plan(1)
const mockAgent = new MockAgent()
mockAgent.disableNetConnect()
setGlobalDispatcher(mockAgent)
const mockPool = mockAgent.get('http://localhost:3000')
mockPool.intercept({
path: '/foo',
method: 'GET'
}).reply(200, 'foo', {
headers: {
'set-cookie': [
'foo=bar',
'bar=baz',
'baz=qux'
]
}
})
const { headers } = await request('http://localhost:3000/foo', {
method: 'GET'
})
t.assert.deepStrictEqual(headers['set-cookie'], [
'foo=bar',
'bar=baz',
'baz=qux'
])
})
// https://github.com/nodejs/undici/issues/2418
test('MockAgent - Sending ReadableStream body', async (t) => {
t.plan(1)
const mockAgent = new MockAgent()
setGlobalDispatcher(mockAgent)
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.setHeader('content-type', 'text/plain')
req.pipe(res)
})
after(() => mockAgent.close())
after(() => {
server.closeAllConnections?.()
server.close()
})
await once(server.listen(0), 'listening')
const url = `http://localhost:${server.address().port}`
const response = await fetch(url, {
method: 'POST',
body: new ReadableStream({
start (controller) {
controller.enqueue('test')
controller.close()
}
}),
duplex: 'half'
})
t.assert.deepStrictEqual(await response.text(), 'test')
})
// https://github.com/nodejs/undici/issues/2616
test('MockAgent - headers should be array of strings (fetch)', async (t) => {
t.plan(1)
const mockAgent = new MockAgent()
mockAgent.disableNetConnect()
setGlobalDispatcher(mockAgent)
after(() => mockAgent.close())
const mockPool = mockAgent.get('http://localhost:3000')
mockPool
.intercept({
path: '/foo',
method: 'GET'
})
.reply(200, 'foo', {
headers: {
'set-cookie': ['foo=bar', 'bar=baz', 'baz=qux']
}
})
const response = await fetch('http://localhost:3000/foo', {
method: 'GET'
})
t.assert.deepStrictEqual(response.headers.getSetCookie(), ['foo=bar', 'bar=baz', 'baz=qux'])
})
// https://github.com/nodejs/undici/issues/4146
;[
'/foo?array=item1&array=item2',
'/foo?array[]=item1&array[]=item2',
'/foo?array=item1,item2'
].forEach(path => {
test(`MockAgent - should accept non-standard multi value search parameters when acceptNonStandardSearchParameters is true "${path}"`, async (t) => {
t.plan(4)
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.setHeader('content-type', 'text/plain')
res.end('should not be called')
t.assert.fail('should not be called')
})
after(() => {
server.closeAllConnections?.()
server.close()
})
await once(server.listen(0), 'listening')
const baseUrl = `http://localhost:${server.address().port}`
const mockAgent = new MockAgent({ acceptNonStandardSearchParameters: true })
after(() => mockAgent.close())
const mockPool = mockAgent.get(baseUrl)
mockPool.intercept({
path: '/foo',
method: 'GET',
query: {
array: ['item1', 'item2']
}
}).reply(200, { foo: 'bar' }, {
headers: { 'content-type': 'application/json' },
trailers: { 'Content-MD5': 'test' }
})
const { statusCode, headers, trailers, body } = await mockAgent.request({
origin: baseUrl,
path,
method: 'GET'
})
t.assert.strictEqual(statusCode, 200)
t.assert.strictEqual(headers['content-type'], 'application/json')
t.assert.deepStrictEqual(trailers, { 'content-md5': 'test' })
const jsonResponse = JSON.parse(await getResponse(body))
t.assert.deepStrictEqual(jsonResponse, {
foo: 'bar'
})
})
})
test('MockAgent - should not accept non-standard search parameters when acceptNonStandardSearchParameters is false (default)', async (t) => {
t.plan(2)
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.setHeader('content-type', 'text/plain')
res.end('(non-intercepted) response from server')
})
after(() => {
server.closeAllConnections?.()
server.close()
})
await once(server.listen(0), 'listening')
const baseUrl = `http://localhost:${server.address().port}`
const mockAgent = new MockAgent()
after(() => mockAgent.close())
const mockPool = mockAgent.get(baseUrl)
mockPool.intercept({
path: '/foo',
method: 'GET',
query: {
array: ['item1', 'item2']
}
}).reply(200, { foo: 'bar' }, {
headers: { 'content-type': 'application/json' },
trailers: { 'Content-MD5': 'test' }
})
const { statusCode, body } =
await mockAgent.request({
origin: baseUrl,
path: '/foo?array[]=item1&array[]=item2',
method: 'GET'
})
t.assert.strictEqual(statusCode, 200)
const textResponse = await getResponse(body)
t.assert.strictEqual(textResponse, '(non-intercepted) response from server')
})
// https://github.com/nodejs/undici/issues/4703
describe('MockAgent - case-insensitive origin matching', () => {
test('should match origins with different hostname case', async (t) => {
t.plan(2)
const mockAgent = new MockAgent()
after(() => mockAgent.close())
const url1 = 'http://myEndpoint'
const url2 = 'http://myendpoint' // Different case
const mockPool = mockAgent.get(url1)
mockPool
.intercept({
path: '/test',
method: 'GET'
})
.reply(200, { success: true }, {
headers: { 'content-type': 'application/json' }
})
const { statusCode, body } = await mockAgent.request({
origin: url2, // Different case should still match
method: 'GET',
path: '/test'
})
t.assert.strictEqual(statusCode, 200)
const jsonResponse = JSON.parse(await getResponse(body))
t.assert.deepStrictEqual(jsonResponse, { success: true })
})
test('should match URL object origin with string origin', async (t) => {
t.plan(2)
const mockAgent = new MockAgent()
after(() => mockAgent.close())
const url = 'http://myEndpoint'
const mockPool = mockAgent.get(url)
mockPool
.intercept({
path: '/path',
method: 'GET'
})
.reply(200, { key: 'value' }, {
headers: { 'content-type': 'application/json' }
})
const { statusCode, body } = await mockAgent.request({
origin: new URL(url), // URL object should match string origin
method: 'GET',
path: '/path'
})
t.assert.strictEqual(statusCode, 200)
const jsonResponse = JSON.parse(await getResponse(body))
t.assert.deepStrictEqual(jsonResponse, { key: 'value' })
})
test('should match URL object with different hostname case', async (t) => {
t.plan(2)
const mockAgent = new MockAgent()
after(() => mockAgent.close())
const url1 = 'http://Example.com'
const url2 = new URL('http://example.com') // Different case
const mockPool = mockAgent.get(url1)
mockPool
.intercept({
path: '/test',
method: 'GET'
})
.reply(200, { success: true }, {
headers: { 'content-type': 'application/json' }
})
const { statusCode, body } = await mockAgent.request({
origin: url2, // URL object with different case should match
method: 'GET',
path: '/test'
})
t.assert.strictEqual(statusCode, 200)
const jsonResponse = JSON.parse(await getResponse(body))
t.assert.deepStrictEqual(jsonResponse, { success: true })
})
test('should handle mixed case scenarios correctly', async (t) => {
t.plan(2)
const mockAgent = new MockAgent()
after(() => mockAgent.close())
const url1 = 'http://MyEndpoint.com'
const url2 = 'http://myendpoint.com' // All lowercase
const mockPool = mockAgent.get(url1)
mockPool
.intercept({
path: '/api',
method: 'GET'
})
.reply(200, { data: 'test' }, {
headers: { 'content-type': 'application/json' }
})
const { statusCode, body } = await mockAgent.request({
origin: url2,
method: 'GET',
path: '/api'
})
t.assert.strictEqual(statusCode, 200)
const jsonResponse = JSON.parse(await getResponse(body))
t.assert.deepStrictEqual(jsonResponse, { data: 'test' })
})
test('should preserve port numbers when normalizing', async (t) => {
t.plan(2)
const mockAgent = new MockAgent()
after(() => mockAgent.close())
const url1 = 'http://Example.com:8080'
const url2 = 'http://example.com:8080' // Different case, same port
const mockPool = mockAgent.get(url1)
mockPool
.intercept({
path: '/test',
method: 'GET'
})
.reply(200, { port: 8080 }, {
headers: { 'content-type': 'application/json' }
})
const { statusCode, body } = await mockAgent.request({
origin: url2,
method: 'GET',
path: '/test'
})
t.assert.strictEqual(statusCode, 200)
const jsonResponse = JSON.parse(await getResponse(body))
t.assert.deepStrictEqual(jsonResponse, { port: 8080 })
})
test('should handle https origins with case differences', async (t) => {
t.plan(2)
const mockAgent = new MockAgent()
after(() => mockAgent.close())
const url1 = 'https://Api.Example.com'
const url2 = new URL('https://api.example.com') // Different case
const mockPool = mockAgent.get(url1)
mockPool
.intercept({
path: '/data',
method: 'GET'
})
.reply(200, { secure: true }, {
headers: { 'content-type': 'application/json' }
})
const { statusCode, body } = await mockAgent.request({
origin: url2,
method: 'GET',
path: '/data'
})
t.assert.strictEqual(statusCode, 200)
const jsonResponse = JSON.parse(await getResponse(body))
t.assert.deepStrictEqual(jsonResponse, { secure: true })
})
})
================================================
FILE: test/mock-call-history-log.js
================================================
'use strict'
const { test, describe } = require('node:test')
const { MockCallHistoryLog } = require('../lib/mock/mock-call-history')
const { InvalidArgumentError } = require('../lib/core/errors')
describe('MockCallHistoryLog - constructor', () => {
function assertConsistent (t, mockCallHistoryLog) {
t.assert.strictEqual(mockCallHistoryLog.body, null)
t.assert.strictEqual(mockCallHistoryLog.headers, undefined)
t.assert.deepStrictEqual(mockCallHistoryLog.searchParams, { query: 'value' })
t.assert.strictEqual(mockCallHistoryLog.method, 'PUT')
t.assert.strictEqual(mockCallHistoryLog.origin, 'https://localhost:4000')
t.assert.strictEqual(mockCallHistoryLog.path, '/endpoint')
t.assert.strictEqual(mockCallHistoryLog.hash, '#here')
t.assert.strictEqual(mockCallHistoryLog.protocol, 'https:')
t.assert.strictEqual(mockCallHistoryLog.host, 'localhost:4000')
t.assert.strictEqual(mockCallHistoryLog.port, '4000')
}
test('should populate class properties with query in path', t => {
t.plan(10)
const mockCallHistoryLog = new MockCallHistoryLog({
body: null,
headers: undefined,
method: 'PUT',
origin: 'https://localhost:4000',
path: '/endpoint?query=value#here'
})
assertConsistent(t, mockCallHistoryLog)
})
test('should populate class properties with query in argument', t => {
t.plan(10)
const mockCallHistoryLog = new MockCallHistoryLog({
body: null,
headers: undefined,
method: 'PUT',
origin: 'https://localhost:4000',
path: '/endpoint#here',
query: { query: 'value' }
})
assertConsistent(t, mockCallHistoryLog)
})
test('should throw when url computing failed', t => {
t.plan(1)
t.assert.throws(() => new MockCallHistoryLog({}), new InvalidArgumentError('An error occurred when computing MockCallHistoryLog.url'))
})
})
describe('MockCallHistoryLog - toMap', () => {
test('should return a Map of eleven element', t => {
t.plan(1)
const mockCallHistoryLog = new MockCallHistoryLog({
body: '"{}"',
headers: { 'content-type': 'application/json' },
method: 'PUT',
origin: 'https://localhost:4000',
path: '/endpoint?query=value#here'
})
t.assert.strictEqual(mockCallHistoryLog.toMap().size, 11)
})
})
describe('MockCallHistoryLog - toString', () => {
test('should return a string with all property', t => {
t.plan(1)
const mockCallHistoryLog = new MockCallHistoryLog({
body: '"{ "data": "hello" }"',
headers: { 'content-type': 'application/json' },
method: 'PUT',
origin: 'https://localhost:4000',
path: '/endpoint?query=value#here'
})
t.assert.strictEqual(mockCallHistoryLog.toString(), 'protocol->https:|host->localhost:4000|port->4000|origin->https://localhost:4000|path->/endpoint|hash->#here|searchParams->{"query":"value"}|fullUrl->https://localhost:4000/endpoint?query=value#here|method->PUT|body->"{ "data": "hello" }"|headers->{"content-type":"application/json"}')
})
test('should return a string when headers is an Array of string Array', t => {
t.plan(1)
const mockCallHistoryLog = new MockCallHistoryLog({
body: '"{ "data": "hello" }"',
headers: ['content-type', ['application/json', 'application/xml']],
method: 'PUT',
origin: 'https://localhost:4000',
path: '/endpoint?query=value#here'
})
t.assert.strictEqual(mockCallHistoryLog.toString(), 'protocol->https:|host->localhost:4000|port->4000|origin->https://localhost:4000|path->/endpoint|hash->#here|searchParams->{"query":"value"}|fullUrl->https://localhost:4000/endpoint?query=value#here|method->PUT|body->"{ "data": "hello" }"|headers->["content-type",["application/json","application/xml"]]')
})
})
================================================
FILE: test/mock-call-history.js
================================================
'use strict'
const { test, describe } = require('node:test')
const { MockCallHistory, MockCallHistoryLog } = require('../lib/mock/mock-call-history')
const { kMockCallHistoryAddLog } = require('../lib/mock/mock-symbols')
const { InvalidArgumentError } = require('../lib/core/errors')
describe('MockCallHistory - constructor', () => {
test('should returns a MockCallHistory', t => {
t.plan(1)
const mockCallHistory = new MockCallHistory()
t.assert.ok(mockCallHistory instanceof MockCallHistory)
})
})
describe('MockCallHistory - add log', () => {
test('should add a log', t => {
t.plan(2)
const mockCallHistoryHello = new MockCallHistory()
t.assert.strictEqual(mockCallHistoryHello.calls().length, 0)
mockCallHistoryHello[kMockCallHistoryAddLog]({ path: '/', origin: 'https://localhost:4000' })
t.assert.strictEqual(mockCallHistoryHello.calls().length, 1)
})
})
describe('MockCallHistory - calls', () => {
test('should returns every logs', t => {
t.plan(1)
const mockCallHistoryHello = new MockCallHistory('hello')
mockCallHistoryHello[kMockCallHistoryAddLog]({ path: '/', origin: 'https://localhost:4000' })
mockCallHistoryHello[kMockCallHistoryAddLog]({ path: '/', origin: 'https://localhost:4000' })
t.assert.strictEqual(mockCallHistoryHello.calls().length, 2)
})
test('should returns empty array when no logs', t => {
t.plan(1)
const mockCallHistoryHello = new MockCallHistory('hello')
t.assert.ok(mockCallHistoryHello.calls() instanceof Array)
})
})
describe('MockCallHistory - firstCall', () => {
test('should returns the first log registered', t => {
t.plan(1)
const mockCallHistoryHello = new MockCallHistory('hello')
mockCallHistoryHello[kMockCallHistoryAddLog]({ path: '/', origin: 'http://localhost:4000' })
mockCallHistoryHello[kMockCallHistoryAddLog]({ path: '/noop', origin: 'http://localhost:4000' })
t.assert.strictEqual(mockCallHistoryHello.firstCall()?.path, '/')
})
test('should returns undefined when no logs', t => {
t.plan(1)
const mockCallHistoryHello = new MockCallHistory('hello')
t.assert.strictEqual(mockCallHistoryHello.firstCall(), undefined)
})
})
describe('MockCallHistory - lastCall', () => {
test('should returns the first log registered', t => {
t.plan(1)
const mockCallHistoryHello = new MockCallHistory('hello')
mockCallHistoryHello[kMockCallHistoryAddLog]({ path: '/', origin: 'http://localhost:4000' })
mockCallHistoryHello[kMockCallHistoryAddLog]({ path: '/noop', origin: 'http://localhost:4000' })
t.assert.strictEqual(mockCallHistoryHello.lastCall()?.path, '/noop')
})
test('should returns undefined when no logs', t => {
t.plan(1)
const mockCallHistoryHello = new MockCallHistory('hello')
t.assert.strictEqual(mockCallHistoryHello.lastCall(), undefined)
})
})
describe('MockCallHistory - nthCall', () => {
test('should returns the nth log registered', t => {
t.plan(2)
const mockCallHistoryHello = new MockCallHistory('hello')
mockCallHistoryHello[kMockCallHistoryAddLog]({ path: '/', origin: 'http://localhost:4000' })
mockCallHistoryHello[kMockCallHistoryAddLog]({ path: '/noop', origin: 'http://localhost:4000' })
t.assert.strictEqual(mockCallHistoryHello.nthCall(1)?.path, '/')
t.assert.strictEqual(mockCallHistoryHello.nthCall(2)?.path, '/noop')
})
test('should returns undefined when no logs', t => {
t.plan(1)
const mockCallHistoryHello = new MockCallHistory('hello')
t.assert.strictEqual(mockCallHistoryHello.nthCall(3), undefined)
})
test('should throw if index is not a number', t => {
t.plan(1)
const mockCallHistoryHello = new MockCallHistory('hello')
t.assert.throws(() => mockCallHistoryHello.nthCall('noop'), new InvalidArgumentError('nthCall must be called with a number'))
})
test('should throw if index is not an integer', t => {
t.plan(1)
const mockCallHistoryHello = new MockCallHistory('hello')
t.assert.throws(() => mockCallHistoryHello.nthCall(1.3), new InvalidArgumentError('nthCall must be called with an integer'))
})
test('should throw if index is equal to zero', t => {
t.plan(1)
const mockCallHistoryHello = new MockCallHistory('hello')
t.assert.throws(() => mockCallHistoryHello.nthCall(0), new InvalidArgumentError('nthCall must be called with a positive value. use firstCall or lastCall instead'))
})
test('should throw if index is negative', t => {
t.plan(1)
const mockCallHistoryHello = new MockCallHistory('hello')
t.assert.throws(() => mockCallHistoryHello.nthCall(-1), new InvalidArgumentError('nthCall must be called with a positive value. use firstCall or lastCall instead'))
})
})
describe('MockCallHistory - iterator', () => {
test('should permit to iterate over logs with for..of', t => {
t.plan(4)
const mockCallHistoryHello = new MockCallHistory('hello')
mockCallHistoryHello[kMockCallHistoryAddLog]({ path: '/', origin: 'http://localhost:4000' })
mockCallHistoryHello[kMockCallHistoryAddLog]({ path: '/noop', origin: 'http://localhost:4000' })
for (const log of mockCallHistoryHello) {
t.assert.ok(log instanceof MockCallHistoryLog)
t.assert.ok(typeof log.path === 'string')
}
})
test('should permit to iterate over logs with spread operator', t => {
t.plan(2)
const mockCallHistoryHello = new MockCallHistory('hello')
mockCallHistoryHello[kMockCallHistoryAddLog]({ path: '/', origin: 'http://localhost:4000' })
mockCallHistoryHello[kMockCallHistoryAddLog]({ path: '/noop', origin: 'http://localhost:4000' })
const logs = [...mockCallHistoryHello]
t.assert.ok(logs.every((log) => log instanceof MockCallHistoryLog))
t.assert.strictEqual(logs.length, 2)
})
})
describe('MockCallHistory - filterCalls without options', () => {
test('should filter logs with a function', t => {
t.plan(2)
const mockCallHistoryHello = new MockCallHistory('hello')
mockCallHistoryHello[kMockCallHistoryAddLog]({ path: '/', origin: 'http://localhost:4000' })
mockCallHistoryHello[kMockCallHistoryAddLog]({ path: '/noop', origin: 'http://localhost:4000' })
const filtered = mockCallHistoryHello.filterCalls((log) => log.path === '/noop')
t.assert.strictEqual(filtered?.[0]?.path, '/noop')
t.assert.strictEqual(filtered.length, 1)
})
test('should filter logs with a regexp', t => {
t.plan(2)
const mockCallHistoryHello = new MockCallHistory('hello')
mockCallHistoryHello[kMockCallHistoryAddLog]({ path: '/', origin: 'http://localhost:4000' })
mockCallHistoryHello[kMockCallHistoryAddLog]({ path: '/noop', origin: 'https://localhost:4000' })
const filtered = mockCallHistoryHello.filterCalls(/https:\/\//)
t.assert.strictEqual(filtered?.[0]?.path, '/noop')
t.assert.strictEqual(filtered.length, 1)
})
test('should filter logs with an object', t => {
t.plan(2)
const mockCallHistoryHello = new MockCallHistory('hello')
mockCallHistoryHello[kMockCallHistoryAddLog]({ path: '/', origin: 'http://localhost:4000' })
mockCallHistoryHello[kMockCallHistoryAddLog]({ path: '/yes', origin: 'http://localhost:4000' })
mockCallHistoryHello[kMockCallHistoryAddLog]({ path: '/noop', origin: 'https://localhost:4000' })
const filtered = mockCallHistoryHello.filterCalls({ protocol: 'https:' })
t.assert.strictEqual(filtered?.[0]?.path, '/noop')
t.assert.strictEqual(filtered.length, 1)
})
test('should returns every logs with an empty object', t => {
t.plan(1)
const mockCallHistoryHello = new MockCallHistory('hello')
mockCallHistoryHello[kMockCallHistoryAddLog]({ path: '/', origin: 'http://localhost:4000' })
mockCallHistoryHello[kMockCallHistoryAddLog]({ path: '/yes', origin: 'http://localhost:4000' })
mockCallHistoryHello[kMockCallHistoryAddLog]({ path: '/noop', origin: 'https://localhost:4000' })
const filtered = mockCallHistoryHello.filterCalls({})
t.assert.strictEqual(filtered.length, 3)
})
test('should filter logs with an object with host property', t => {
t.plan(1)
const mockCallHistoryHello = new MockCallHistory('hello')
mockCallHistoryHello[kMockCallHistoryAddLog]({ path: '/', origin: 'http://localhost:4000' })
mockCallHistoryHello[kMockCallHistoryAddLog]({ path: '/yes', origin: 'http://localhost:4000' })
mockCallHistoryHello[kMockCallHistoryAddLog]({ path: '/noop', origin: 'https://127.0.0.1:4000' })
const filtered = mockCallHistoryHello.filterCalls({ host: /localhost/ })
t.assert.strictEqual(filtered.length, 2)
})
test('should filter logs with an object with port property', t => {
t.plan(1)
const mockCallHistoryHello = new MockCallHistory('hello')
mockCallHistoryHello[kMockCallHistoryAddLog]({ path: '/', origin: 'http://localhost:1000' })
mockCallHistoryHello[kMockCallHistoryAddLog]({ path: '/yes', origin: 'http://localhost:4000' })
mockCallHistoryHello[kMockCallHistoryAddLog]({ path: '/noop', origin: 'https://127.0.0.1:4000' })
const filtered = mockCallHistoryHello.filterCalls({ port: '1000' })
t.assert.strictEqual(filtered.length, 1)
})
test('should filter logs with an object with hash property', t => {
t.plan(1)
const mockCallHistoryHello = new MockCallHistory('hello')
mockCallHistoryHello[kMockCallHistoryAddLog]({ path: '/#hello', origin: 'http://localhost:1000' })
mockCallHistoryHello[kMockCallHistoryAddLog]({ path: '/yes', origin: 'http://localhost:4000' })
mockCallHistoryHello[kMockCallHistoryAddLog]({ path: '/noop', origin: 'https://127.0.0.1:4000' })
const filtered = mockCallHistoryHello.filterCalls({ hash: '#hello' })
t.assert.strictEqual(filtered.length, 1)
})
test('should filter logs with an object with fullUrl property', t => {
t.plan(1)
const mockCallHistoryHello = new MockCallHistory('hello')
mockCallHistoryHello[kMockCallHistoryAddLog]({ path: '#hello', origin: 'http://localhost:1000' })
mockCallHistoryHello[kMockCallHistoryAddLog]({ path: '/yes', origin: 'http://localhost:4000' })
mockCallHistoryHello[kMockCallHistoryAddLog]({ path: '/noop', origin: 'https://127.0.0.1:4000' })
const filtered = mockCallHistoryHello.filterCalls({ fullUrl: 'http://localhost:1000/#hello' })
t.assert.strictEqual(filtered.length, 1)
})
test('should filter logs with an object with method property', t => {
t.plan(1)
const mockCallHistoryHello = new MockCallHistory('hello')
mockCallHistoryHello[kMockCallHistoryAddLog]({ path: '/', origin: 'http://localhost:1000', method: 'POST' })
mockCallHistoryHello[kMockCallHistoryAddLog]({ path: '/yes', origin: 'http://localhost:4000', method: 'GET' })
mockCallHistoryHello[kMockCallHistoryAddLog]({ path: '/noop', origin: 'https://127.0.0.1:4000', method: 'PUT' })
const filtered = mockCallHistoryHello.filterCalls({ method: /(PUT|GET)/ })
t.assert.strictEqual(filtered.length, 2)
})
test('should use "OR" operator', t => {
t.plan(1)
const mockCallHistoryHello = new MockCallHistory('hello')
mockCallHistoryHello[kMockCallHistoryAddLog]({ path: '/', origin: 'http://localhost:4000' })
mockCallHistoryHello[kMockCallHistoryAddLog]({ path: '/yes', origin: 'http://localhost:4000' })
mockCallHistoryHello[kMockCallHistoryAddLog]({ path: '/noop', origin: 'https://localhost:4000' })
const filtered = mockCallHistoryHello.filterCalls({ protocol: 'https:', path: /^\/$/ })
t.assert.strictEqual(filtered.length, 2)
})
test('should returns no duplicated logs', t => {
t.plan(1)
const mockCallHistoryHello = new MockCallHistory('hello')
mockCallHistoryHello[kMockCallHistoryAddLog]({ path: '/', origin: 'http://localhost:4000' })
mockCallHistoryHello[kMockCallHistoryAddLog]({ path: '/yes', origin: 'http://localhost:4000' })
mockCallHistoryHello[kMockCallHistoryAddLog]({ path: '/noop', origin: 'https://localhost:4000' })
const filtered = mockCallHistoryHello.filterCalls({ protocol: 'https:', origin: /localhost/ })
t.assert.strictEqual(filtered.length, 3)
})
test('should throw if criteria is typeof number', t => {
t.plan(1)
const mockCallHistoryHello = new MockCallHistory('hello')
mockCallHistoryHello[kMockCallHistoryAddLog]({ path: '/', origin: 'http://localhost:4000' })
t.assert.throws(() => mockCallHistoryHello.filterCalls({ path: 3 }), new InvalidArgumentError('path parameter should be one of string, regexp, undefined or null'))
})
test('should throw if criteria is not a function, regexp, nor object', t => {
t.plan(1)
const mockCallHistoryHello = new MockCallHistory('hello')
mockCallHistoryHello[kMockCallHistoryAddLog]({ path: '/', origin: 'http://localhost:4000' })
t.assert.throws(() => mockCallHistoryHello.filterCalls(3), new InvalidArgumentError('criteria parameter should be one of function, regexp, or object'))
})
})
describe('MockCallHistory - filterCalls with options', () => {
test('should throw if options.operator is not a valid string', t => {
t.plan(1)
const mockCallHistoryHello = new MockCallHistory('hello')
mockCallHistoryHello[kMockCallHistoryAddLog]({ path: '/', origin: 'http://localhost:4000' })
t.assert.throws(() => mockCallHistoryHello.filterCalls({ path: '/' }, { operator: 'wrong' }), new InvalidArgumentError('options.operator must to be a case insensitive string equal to \'OR\' or \'AND\''))
})
test('should not throw if options.operator is "or"', t => {
t.plan(1)
const mockCallHistoryHello = new MockCallHistory('hello')
mockCallHistoryHello[kMockCallHistoryAddLog]({ path: '/', origin: 'http://localhost:4000' })
t.assert.doesNotThrow(() => mockCallHistoryHello.filterCalls({ path: '/' }, { operator: 'or' }))
})
test('should not throw if options.operator is "and"', t => {
t.plan(1)
const mockCallHistoryHello = new MockCallHistory('hello')
mockCallHistoryHello[kMockCallHistoryAddLog]({ path: '/', origin: 'http://localhost:4000' })
t.assert.doesNotThrow(() => mockCallHistoryHello.filterCalls({ path: '/' }, { operator: 'and' }))
})
test('should use "OR" operator if options is an empty object', t => {
t.plan(1)
const mockCallHistoryHello = new MockCallHistory('hello')
mockCallHistoryHello[kMockCallHistoryAddLog]({ path: '/', origin: 'http://localhost:4000' })
mockCallHistoryHello[kMockCallHistoryAddLog]({ path: '/foo', origin: 'http://localhost:4000' })
const filtered = mockCallHistoryHello.filterCalls({ path: '/' }, {})
t.assert.strictEqual(filtered.length, 1)
})
test('should use "AND" operator correctly', t => {
t.plan(1)
const mockCallHistoryHello = new MockCallHistory('hello')
mockCallHistoryHello[kMockCallHistoryAddLog]({ path: '/', origin: 'http://localhost:4000' })
mockCallHistoryHello[kMockCallHistoryAddLog]({ path: '/', origin: 'http://localhost:5000' })
mockCallHistoryHello[kMockCallHistoryAddLog]({ path: '/foo', origin: 'http://localhost:4000' })
mockCallHistoryHello[kMockCallHistoryAddLog]({ path: '/foo', origin: 'http://localhost:5000' })
const filtered = mockCallHistoryHello.filterCalls({ path: '/', port: '4000' }, { operator: 'AND' })
t.assert.strictEqual(filtered.length, 2)
})
test('should use "AND" operator with a lot of filters', t => {
t.plan(1)
const mockCallHistoryHello = new MockCallHistory('hello')
mockCallHistoryHello[kMockCallHistoryAddLog]({ path: '/#hello', origin: 'http://localhost:1000', method: 'GET' })
mockCallHistoryHello[kMockCallHistoryAddLog]({ path: '/#hello', origin: 'http://localhost:1000', method: 'GET' })
mockCallHistoryHello[kMockCallHistoryAddLog]({ path: '/#hello', origin: 'http://localhost:1000', method: 'DELETE' })
mockCallHistoryHello[kMockCallHistoryAddLog]({ path: '/#hello', origin: 'http://localhost:1000', method: 'POST' })
mockCallHistoryHello[kMockCallHistoryAddLog]({ path: '/#hello', origin: 'http://localhost:1000', method: 'PUT' })
const filtered = mockCallHistoryHello.filterCalls({ path: '/', port: '1000', host: /localhost/, method: /(POST|PUT)/ }, { operator: 'AND' })
t.assert.strictEqual(filtered.length, 2)
})
})
================================================
FILE: test/mock-client.js
================================================
'use strict'
const { test, after, describe } = require('node:test')
const { createServer } = require('node:http')
const { promisify } = require('node:util')
const { MockAgent, MockClient, setGlobalDispatcher, request } = require('..')
const { kUrl } = require('../lib/core/symbols')
const { kDispatches } = require('../lib/mock/mock-symbols')
const { InvalidArgumentError } = require('../lib/core/errors')
const { MockInterceptor } = require('../lib/mock/mock-interceptor')
const { getResponse } = require('../lib/mock/mock-utils')
const Dispatcher = require('../lib/dispatcher/dispatcher')
describe('MockClient - constructor', () => {
test('fails if opts.agent does not implement `get` method', t => {
t.plan(1)
t.assert.throws(() => new MockClient('http://localhost:9999', { agent: { get: 'not a function' } }), InvalidArgumentError)
})
test('sets agent', t => {
t.plan(1)
t.assert.doesNotThrow(() => new MockClient('http://localhost:9999', { agent: new MockAgent({ connections: 1 }) }))
})
test('should implement the Dispatcher API', t => {
t.plan(1)
const mockClient = new MockClient('http://localhost:9999', { agent: new MockAgent({ connections: 1 }) })
t.assert.ok(mockClient instanceof Dispatcher)
})
})
describe('MockClient - dispatch', () => {
test('should handle a single interceptor', (t) => {
t.plan(1)
const baseUrl = 'http://localhost:9999'
const mockAgent = new MockAgent({ connections: 1 })
after(() => mockAgent.close())
const mockClient = mockAgent.get(baseUrl)
this[kUrl] = new URL('http://localhost:9999')
mockClient[kDispatches] = [
{
path: '/foo',
method: 'GET',
data: {
statusCode: 200,
data: 'hello',
headers: {},
trailers: {},
error: null
}
}
]
t.assert.doesNotThrow(() => mockClient.dispatch({
path: '/foo',
method: 'GET'
}, {
onHeaders: (_statusCode, _headers, resume) => resume(),
onData: () => {},
onComplete: () => {}
}))
})
test('should directly throw error from mockDispatch function if error is not a MockNotMatchedError', (t) => {
t.plan(1)
const baseUrl = 'http://localhost:9999'
const mockAgent = new MockAgent({ connections: 1 })
after(() => mockAgent.close())
const mockClient = mockAgent.get(baseUrl)
this[kUrl] = new URL('http://localhost:9999')
mockClient[kDispatches] = [
{
path: '/foo',
method: 'GET',
data: {
statusCode: 200,
data: 'hello',
headers: {},
trailers: {},
error: null
}
}
]
t.assert.throws(() => mockClient.dispatch({
path: '/foo',
method: 'GET'
}, {
onHeaders: (_statusCode, _headers, resume) => { throw new Error('kaboom') },
onData: () => {},
onComplete: () => {}
}), new Error('kaboom'))
})
})
test('MockClient - intercept should return a MockInterceptor', (t) => {
t.plan(1)
const baseUrl = 'http://localhost:9999'
const mockAgent = new MockAgent({ connections: 1 })
after(() => mockAgent.close())
const mockClient = mockAgent.get(baseUrl)
const interceptor = mockClient.intercept({
path: '/foo',
method: 'GET'
})
t.assert.ok(interceptor instanceof MockInterceptor)
})
describe('MockClient - intercept validation', () => {
test('it should error if no options specified in the intercept', t => {
t.plan(1)
const mockAgent = new MockAgent({ connections: 1 })
after(() => mockAgent.close())
const mockClient = mockAgent.get('http://localhost:9999')
t.assert.throws(() => mockClient.intercept(), new InvalidArgumentError('opts must be an object'))
})
test('it should error if no path specified in the intercept', t => {
t.plan(1)
const mockAgent = new MockAgent({ connections: 1 })
after(() => mockAgent.close())
const mockClient = mockAgent.get('http://localhost:9999')
t.assert.throws(() => mockClient.intercept({}), new InvalidArgumentError('opts.path must be defined'))
})
test('it should default to GET if no method specified in the intercept', t => {
t.plan(1)
const mockAgent = new MockAgent({ connections: 1 })
after(() => mockAgent.close())
const mockClient = mockAgent.get('http://localhost:9999')
t.assert.doesNotThrow(() => mockClient.intercept({ path: '/foo' }))
})
test('it should uppercase the method - https://github.com/nodejs/undici/issues/1320', t => {
t.plan(1)
const mockAgent = new MockAgent()
const mockClient = mockAgent.get('http://localhost:3000')
after(() => mockAgent.close())
mockClient.intercept({
path: '/test',
method: 'patch'
}).reply(200, 'Hello!')
t.assert.strictEqual(mockClient[kDispatches][0].method, 'PATCH')
})
})
test('MockClient - close should run without error', async (t) => {
t.plan(1)
const baseUrl = 'http://localhost:9999'
const mockAgent = new MockAgent({ connections: 1 })
after(() => mockAgent.close())
const mockClient = mockAgent.get(baseUrl)
mockClient[kDispatches] = [
{
path: '/foo',
method: 'GET',
data: {
statusCode: 200,
data: 'hello',
headers: {},
trailers: {},
error: null
}
}
]
await mockClient.close()
t.assert.ok(true, 'pass')
})
test('MockClient - should be able to set as globalDispatcher', async (t) => {
t.plan(3)
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.setHeader('content-type', 'text/plain')
res.end('should not be called')
t.assert.fail('should not be called')
})
after(() => {
server.closeAllConnections?.()
server.close()
})
await promisify(server.listen.bind(server))(0)
const baseUrl = `http://localhost:${server.address().port}`
const mockAgent = new MockAgent({ connections: 1 })
after(() => mockAgent.close())
const mockClient = mockAgent.get(baseUrl)
t.assert.ok(mockClient instanceof MockClient)
setGlobalDispatcher(mockClient)
mockClient.intercept({
path: '/foo',
method: 'GET'
}).reply(200, 'hello')
const { statusCode, body } = await request(`${baseUrl}/foo`, {
method: 'GET'
})
t.assert.strictEqual(statusCode, 200)
const response = await getResponse(body)
t.assert.deepStrictEqual(response, 'hello')
})
test('MockClient - should support query params', async (t) => {
t.plan(3)
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.setHeader('content-type', 'text/plain')
res.end('should not be called')
t.assert.fail('should not be called')
})
after(() => {
server.closeAllConnections?.()
server.close()
})
await promisify(server.listen.bind(server))(0)
const baseUrl = `http://localhost:${server.address().port}`
const mockAgent = new MockAgent({ connections: 1 })
after(() => mockAgent.close())
const mockClient = mockAgent.get(baseUrl)
t.assert.ok(mockClient instanceof MockClient)
setGlobalDispatcher(mockClient)
const query = {
pageNum: 1
}
mockClient.intercept({
path: '/foo',
query,
method: 'GET'
}).reply(200, 'hello')
const { statusCode, body } = await request(`${baseUrl}/foo`, {
method: 'GET',
query
})
t.assert.strictEqual(statusCode, 200)
const response = await getResponse(body)
t.assert.deepStrictEqual(response, 'hello')
})
test('MockClient - should intercept query params with hardcoded path', async (t) => {
t.plan(3)
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.setHeader('content-type', 'text/plain')
res.end('should not be called')
t.assert.fail('should not be called')
})
after(() => {
server.closeAllConnections?.()
server.close()
})
await promisify(server.listen.bind(server))(0)
const baseUrl = `http://localhost:${server.address().port}`
const mockAgent = new MockAgent({ connections: 1 })
after(() => mockAgent.close())
const mockClient = mockAgent.get(baseUrl)
t.assert.ok(mockClient instanceof MockClient)
setGlobalDispatcher(mockClient)
const query = {
pageNum: 1
}
mockClient.intercept({
path: '/foo?pageNum=1',
method: 'GET'
}).reply(200, 'hello')
const { statusCode, body } = await request(`${baseUrl}/foo`, {
method: 'GET',
query
})
t.assert.strictEqual(statusCode, 200)
const response = await getResponse(body)
t.assert.deepStrictEqual(response, 'hello')
})
test('MockClient - should intercept query params regardless of key ordering', async (t) => {
t.plan(3)
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.setHeader('content-type', 'text/plain')
res.end('should not be called')
t.assert.fail('should not be called')
})
after(() => {
server.closeAllConnections?.()
server.close()
})
await promisify(server.listen.bind(server))(0)
const baseUrl = `http://localhost:${server.address().port}`
const mockAgent = new MockAgent({ connections: 1 })
after(() => mockAgent.close())
const mockClient = mockAgent.get(baseUrl)
t.assert.ok(mockClient instanceof MockClient)
setGlobalDispatcher(mockClient)
const query = {
pageNum: 1,
limit: 100,
ordering: [false, true]
}
mockClient.intercept({
path: '/foo',
query: {
ordering: query.ordering,
pageNum: query.pageNum,
limit: query.limit
},
method: 'GET'
}).reply(200, 'hello')
const { statusCode, body } = await request(`${baseUrl}/foo`, {
method: 'GET',
query
})
t.assert.strictEqual(statusCode, 200)
const response = await getResponse(body)
t.assert.deepStrictEqual(response, 'hello')
})
test('MockClient - should be able to use as a local dispatcher', async (t) => {
t.plan(3)
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.setHeader('content-type', 'text/plain')
res.end('should not be called')
t.assert.fail('should not be called')
})
after(() => {
server.closeAllConnections?.()
server.close()
})
await promisify(server.listen.bind(server))(0)
const baseUrl = `http://localhost:${server.address().port}`
const mockAgent = new MockAgent({ connections: 1 })
after(() => mockAgent.close())
const mockClient = mockAgent.get(baseUrl)
t.assert.ok(mockClient instanceof MockClient)
mockClient.intercept({
path: '/foo',
method: 'GET'
}).reply(200, 'hello')
const { statusCode, body } = await request(`${baseUrl}/foo`, {
method: 'GET',
dispatcher: mockClient
})
t.assert.strictEqual(statusCode, 200)
const response = await getResponse(body)
t.assert.deepStrictEqual(response, 'hello')
})
test('MockClient - basic intercept with MockClient.request', async (t) => {
t.plan(5)
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.setHeader('content-type', 'text/plain')
res.end('should not be called')
t.assert.fail('should not be called')
})
after(() => {
server.closeAllConnections?.()
server.close()
})
await promisify(server.listen.bind(server))(0)
const baseUrl = `http://localhost:${server.address().port}`
const mockAgent = new MockAgent({ connections: 1 })
after(() => mockAgent.close())
const mockClient = mockAgent.get(baseUrl)
t.assert.ok(mockClient instanceof MockClient)
mockClient.intercept({
path: '/foo?hello=there&see=ya',
method: 'POST',
body: 'form1=data1&form2=data2'
}).reply(200, { foo: 'bar' }, {
headers: { 'content-type': 'application/json' },
trailers: { 'Content-MD5': 'test' }
})
const { statusCode, headers, trailers, body } = await mockClient.request({
origin: baseUrl,
path: '/foo?hello=there&see=ya',
method: 'POST',
body: 'form1=data1&form2=data2'
})
t.assert.strictEqual(statusCode, 200)
t.assert.strictEqual(headers['content-type'], 'application/json')
t.assert.deepStrictEqual(trailers, { 'content-md5': 'test' })
const jsonResponse = JSON.parse(await getResponse(body))
t.assert.deepStrictEqual(jsonResponse, {
foo: 'bar'
})
})
test('MockClient - cleans mocks', async (t) => {
t.plan(4)
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.setHeader('content-type', 'text/plain')
res.end('hello')
})
after(() => {
server.closeAllConnections?.()
server.close()
})
await promisify(server.listen.bind(server))(0)
const baseUrl = `http://localhost:${server.address().port}`
const mockAgent = new MockAgent({ connections: 1 })
after(() => mockAgent.close())
const mockClient = mockAgent.get(baseUrl)
t.assert.ok(mockClient instanceof MockClient)
setGlobalDispatcher(mockClient)
mockClient.intercept({
path: '/foo',
method: 'GET'
}).reply(500, () => {
t.assert.fail('should not be called')
})
mockClient.cleanMocks()
t.assert.strictEqual(mockClient[kDispatches].length, 0)
const { statusCode, body } = await request(`${baseUrl}/foo`, {
method: 'GET'
})
t.assert.strictEqual(statusCode, 200)
const response = await getResponse(body)
t.assert.deepStrictEqual(response, 'hello')
})
================================================
FILE: test/mock-delayed-abort.js
================================================
'use strict'
const { test } = require('node:test')
const { MockAgent, interceptors } = require('..')
const DecoratorHandler = require('../lib/handler/decorator-handler')
const { tspl } = require('@matteo.collina/tspl')
test('MockAgent with delayed response and AbortSignal should not cause uncaught errors', async (t) => {
const p = tspl(t, { plan: 2 })
const agent = new MockAgent()
t.after(() => agent.close())
const mockPool = agent.get('https://example.com')
mockPool.intercept({ path: '/test', method: 'GET' })
.reply(200, { success: true }, { headers: { 'content-type': 'application/json' } })
.delay(100)
const ac = new AbortController()
// Abort the request after 10ms
setTimeout(() => {
ac.abort(new Error('Request aborted'))
}, 10)
try {
await agent.request({
origin: 'https://example.com',
path: '/test',
method: 'GET',
signal: ac.signal
})
p.fail('Should have thrown an error')
} catch (err) {
p.ok(err.message === 'Request aborted' || err.name === 'AbortError', 'Error should be related to abort')
}
// Wait for the delayed response to fire - should not cause any uncaught errors
await new Promise(resolve => setTimeout(resolve, 150))
p.ok(true, 'No uncaught errors after delayed response')
})
test('MockAgent with delayed response and composed interceptor (decompress) should not cause uncaught errors', async (t) => {
const p = tspl(t, { plan: 2 })
// The decompress interceptor has assertions that fail if onResponseStart is called after onError
const agent = new MockAgent().compose(interceptors.decompress())
t.after(() => agent.close())
const mockPool = agent.get('https://example.com')
mockPool.intercept({ path: '/test', method: 'GET' })
.reply(200, { success: true }, { headers: { 'content-type': 'application/json' } })
.delay(100)
const ac = new AbortController()
// Abort the request after 10ms
setTimeout(() => {
ac.abort(new Error('Request aborted'))
}, 10)
try {
await agent.request({
origin: 'https://example.com',
path: '/test',
method: 'GET',
signal: ac.signal
})
p.fail('Should have thrown an error')
} catch (err) {
p.ok(err.message === 'Request aborted' || err.name === 'AbortError', 'Error should be related to abort')
}
// Wait for the delayed response to fire - should not cause any uncaught errors
await new Promise(resolve => setTimeout(resolve, 150))
p.ok(true, 'No uncaught errors after delayed response')
})
test('MockAgent with delayed response and DecoratorHandler should not call onResponseStart after onError', async (t) => {
const p = tspl(t, { plan: 2 })
class TestDecoratorHandler extends DecoratorHandler {
#onErrorCalled = false
onResponseStart (controller, statusCode, headers, statusMessage) {
if (this.#onErrorCalled) {
p.fail('onResponseStart should not be called after onError')
}
return super.onResponseStart(controller, statusCode, headers, statusMessage)
}
onResponseError (controller, err) {
this.#onErrorCalled = true
return super.onResponseError(controller, err)
}
}
const agent = new MockAgent()
t.after(() => agent.close())
const mockPool = agent.get('https://example.com')
mockPool.intercept({ path: '/test', method: 'GET' })
.reply(200, { success: true }, { headers: { 'content-type': 'application/json' } })
.delay(100)
const ac = new AbortController()
// Abort the request after 10ms
setTimeout(() => {
ac.abort(new Error('Request aborted'))
}, 10)
const originalDispatch = agent.dispatch.bind(agent)
agent.dispatch = (opts, handler) => {
const decoratedHandler = new TestDecoratorHandler(handler)
return originalDispatch(opts, decoratedHandler)
}
try {
await agent.request({
origin: 'https://example.com',
path: '/test',
method: 'GET',
signal: ac.signal
})
p.fail('Should have thrown an error')
} catch (err) {
p.ok(err.message === 'Request aborted' || err.name === 'AbortError', 'Error should be related to abort')
}
// Wait for the delayed response to fire
await new Promise(resolve => setTimeout(resolve, 150))
p.ok(true, 'Decorator handler invariants maintained')
})
================================================
FILE: test/mock-errors.js
================================================
'use strict'
const { describe, test } = require('node:test')
const { mockErrors, errors } = require('..')
describe('MockNotMatchedError', () => {
test('should implement an UndiciError', t => {
t.plan(4)
const mockError = new mockErrors.MockNotMatchedError()
t.assert.ok(mockError instanceof errors.UndiciError)
t.assert.deepStrictEqual(mockError.name, 'MockNotMatchedError')
t.assert.deepStrictEqual(mockError.code, 'UND_MOCK_ERR_MOCK_NOT_MATCHED')
t.assert.deepStrictEqual(mockError.message, 'The request does not match any registered mock dispatches')
})
test('should set a custom message', t => {
t.plan(4)
const mockError = new mockErrors.MockNotMatchedError('custom message')
t.assert.ok(mockError instanceof errors.UndiciError)
t.assert.deepStrictEqual(mockError.name, 'MockNotMatchedError')
t.assert.deepStrictEqual(mockError.code, 'UND_MOCK_ERR_MOCK_NOT_MATCHED')
t.assert.deepStrictEqual(mockError.message, 'custom message')
})
})
================================================
FILE: test/mock-interceptor-unused-assertions.js
================================================
'use strict'
const { test, beforeEach, afterEach } = require('node:test')
const { MockAgent, setGlobalDispatcher } = require('..')
const PendingInterceptorsFormatter = require('../lib/mock/pending-interceptors-formatter')
const util = require('../lib/core/util')
// Since Node.js v21 `console.table` rows are aligned to the left
// https://github.com/nodejs/node/pull/50135
const tableRowsAlignedToLeft = util.nodeMajor >= 21 || (util.nodeMajor === 20 && util.nodeMinor >= 11)
// `console.table` treats emoji as two character widths for cell width determination
const Y = process.versions.icu ? '✅' : 'Y '
const N = process.versions.icu ? '❌' : 'N '
// Avoid colors in the output for inline snapshots.
const pendingInterceptorsFormatter = new PendingInterceptorsFormatter({ disableColors: true })
let originalGlobalDispatcher
const origin = 'https://localhost:9999'
beforeEach(() => {
// Disallow all network activity by default by using a mock agent as the global dispatcher
const globalDispatcher = new MockAgent()
globalDispatcher.disableNetConnect()
setGlobalDispatcher(globalDispatcher)
originalGlobalDispatcher = globalDispatcher
})
afterEach(() => {
setGlobalDispatcher(originalGlobalDispatcher)
})
function mockAgentWithOneInterceptor () {
const agent = new MockAgent()
agent.disableNetConnect()
agent
.get('https://example.com')
.intercept({ method: 'GET', path: '/' })
.reply(200, '')
return agent
}
test('1 pending interceptor', t => {
t.plan(1)
try {
mockAgentWithOneInterceptor().assertNoPendingInterceptors({ pendingInterceptorsFormatter })
t.assert.fail('Should have thrown')
} catch (err) {
t.assert.deepStrictEqual(err.message, tableRowsAlignedToLeft
? `
1 interceptor is pending:
┌─────────┬────────┬───────────────────────┬──────┬─────────────┬────────────┬─────────────┬───────────┐
│ (index) │ Method │ Origin │ Path │ Status code │ Persistent │ Invocations │ Remaining │
├─────────┼────────┼───────────────────────┼──────┼─────────────┼────────────┼─────────────┼───────────┤
│ 0 │ 'GET' │ 'https://example.com' │ '/' │ 200 │ '${N}' │ 0 │ 1 │
└─────────┴────────┴───────────────────────┴──────┴─────────────┴────────────┴─────────────┴───────────┘
`.trim()
: `
1 interceptor is pending:
┌─────────┬────────┬───────────────────────┬──────┬─────────────┬────────────┬─────────────┬───────────┐
│ (index) │ Method │ Origin │ Path │ Status code │ Persistent │ Invocations │ Remaining │
├─────────┼────────┼───────────────────────┼──────┼─────────────┼────────────┼─────────────┼───────────┤
│ 0 │ 'GET' │ 'https://example.com' │ '/' │ 200 │ '${N}' │ 0 │ 1 │
└─────────┴────────┴───────────────────────┴──────┴─────────────┴────────────┴─────────────┴───────────┘
`.trim())
}
})
test('2 pending interceptors', t => {
t.plan(1)
const withTwoInterceptors = mockAgentWithOneInterceptor()
withTwoInterceptors
.get(origin)
.intercept({ method: 'get', path: '/some/path' })
.reply(204, 'OK')
try {
withTwoInterceptors.assertNoPendingInterceptors({ pendingInterceptorsFormatter })
} catch (err) {
t.assert.deepStrictEqual(err.message, tableRowsAlignedToLeft
? `
2 interceptors are pending:
┌─────────┬────────┬──────────────────────────┬──────────────┬─────────────┬────────────┬─────────────┬───────────┐
│ (index) │ Method │ Origin │ Path │ Status code │ Persistent │ Invocations │ Remaining │
├─────────┼────────┼──────────────────────────┼──────────────┼─────────────┼────────────┼─────────────┼───────────┤
│ 0 │ 'GET' │ 'https://example.com' │ '/' │ 200 │ '${N}' │ 0 │ 1 │
│ 1 │ 'GET' │ 'https://localhost:9999' │ '/some/path' │ 204 │ '${N}' │ 0 │ 1 │
└─────────┴────────┴──────────────────────────┴──────────────┴─────────────┴────────────┴─────────────┴───────────┘
`.trim()
: `
2 interceptors are pending:
┌─────────┬────────┬──────────────────────────┬──────────────┬─────────────┬────────────┬─────────────┬───────────┐
│ (index) │ Method │ Origin │ Path │ Status code │ Persistent │ Invocations │ Remaining │
├─────────┼────────┼──────────────────────────┼──────────────┼─────────────┼────────────┼─────────────┼───────────┤
│ 0 │ 'GET' │ 'https://example.com' │ '/' │ 200 │ '${N}' │ 0 │ 1 │
│ 1 │ 'GET' │ 'https://localhost:9999' │ '/some/path' │ 204 │ '${N}' │ 0 │ 1 │
└─────────┴────────┴──────────────────────────┴──────────────┴─────────────┴────────────┴─────────────┴───────────┘
`.trim())
}
})
test('Variations of persist(), times(), and pending status', async t => {
t.plan(6)
// Agent with unused interceptor
const agent = mockAgentWithOneInterceptor()
// Unused with persist()
agent
.get(origin)
.intercept({ method: 'get', path: '/persistent/unused' })
.reply(200, 'OK')
.persist()
// Used with persist()
agent
.get(origin)
.intercept({ method: 'GET', path: '/persistent/used' })
.reply(200, 'OK')
.persist()
t.assert.deepStrictEqual((await agent.request({ origin, method: 'GET', path: '/persistent/used' })).statusCode, 200)
// Consumed without persist()
agent.get(origin)
.intercept({ method: 'post', path: '/transient/pending' })
.reply(201, 'Created')
t.assert.deepStrictEqual((await agent.request({ origin, method: 'POST', path: '/transient/pending' })).statusCode, 201)
// Partially pending with times()
agent.get(origin)
.intercept({ method: 'get', path: '/times/partial' })
.reply(200, 'OK')
.times(5)
t.assert.deepStrictEqual((await agent.request({ origin, method: 'GET', path: '/times/partial' })).statusCode, 200)
// Unused with times()
agent.get(origin)
.intercept({ method: 'get', path: '/times/unused' })
.reply(200, 'OK')
.times(2)
// Fully pending with times()
agent.get(origin)
.intercept({ method: 'get', path: '/times/pending' })
.reply(200, 'OK')
.times(2)
t.assert.deepStrictEqual((await agent.request({ origin, method: 'GET', path: '/times/pending' })).statusCode, 200)
t.assert.deepStrictEqual((await agent.request({ origin, method: 'GET', path: '/times/pending' })).statusCode, 200)
try {
agent.assertNoPendingInterceptors({ pendingInterceptorsFormatter })
t.assert.fail('Should have thrown')
} catch (err) {
t.assert.deepStrictEqual(err.message, tableRowsAlignedToLeft
? `
4 interceptors are pending:
┌─────────┬────────┬──────────────────────────┬──────────────────────┬─────────────┬────────────┬─────────────┬───────────┐
│ (index) │ Method │ Origin │ Path │ Status code │ Persistent │ Invocations │ Remaining │
├─────────┼────────┼──────────────────────────┼──────────────────────┼─────────────┼────────────┼─────────────┼───────────┤
│ 0 │ 'GET' │ 'https://example.com' │ '/' │ 200 │ '${N}' │ 0 │ 1 │
│ 1 │ 'GET' │ 'https://localhost:9999' │ '/persistent/unused' │ 200 │ '${Y}' │ 0 │ Infinity │
│ 2 │ 'GET' │ 'https://localhost:9999' │ '/times/partial' │ 200 │ '${N}' │ 1 │ 4 │
│ 3 │ 'GET' │ 'https://localhost:9999' │ '/times/unused' │ 200 │ '${N}' │ 0 │ 2 │
└─────────┴────────┴──────────────────────────┴──────────────────────┴─────────────┴────────────┴─────────────┴───────────┘
`.trim()
: `
4 interceptors are pending:
┌─────────┬────────┬──────────────────────────┬──────────────────────┬─────────────┬────────────┬─────────────┬───────────┐
│ (index) │ Method │ Origin │ Path │ Status code │ Persistent │ Invocations │ Remaining │
├─────────┼────────┼──────────────────────────┼──────────────────────┼─────────────┼────────────┼─────────────┼───────────┤
│ 0 │ 'GET' │ 'https://example.com' │ '/' │ 200 │ '${N}' │ 0 │ 1 │
│ 1 │ 'GET' │ 'https://localhost:9999' │ '/persistent/unused' │ 200 │ '${Y}' │ 0 │ Infinity │
│ 2 │ 'GET' │ 'https://localhost:9999' │ '/times/partial' │ 200 │ '${N}' │ 1 │ 4 │
│ 3 │ 'GET' │ 'https://localhost:9999' │ '/times/unused' │ 200 │ '${N}' │ 0 │ 2 │
└─────────┴────────┴──────────────────────────┴──────────────────────┴─────────────┴────────────┴─────────────┴───────────┘
`.trim())
}
})
test('works when no interceptors are registered', t => {
t.plan(2)
const agent = new MockAgent()
agent.disableNetConnect()
t.assert.deepStrictEqual(agent.pendingInterceptors(), [])
t.assert.doesNotThrow(() => agent.assertNoPendingInterceptors())
})
test('works when all interceptors are pending', async t => {
t.plan(4)
const agent = new MockAgent()
agent.disableNetConnect()
agent.get(origin).intercept({ method: 'get', path: '/' }).reply(200, 'OK')
t.assert.deepStrictEqual((await agent.request({ origin, method: 'GET', path: '/' })).statusCode, 200)
agent.get(origin).intercept({ method: 'get', path: '/persistent' }).reply(200, 'OK')
t.assert.deepStrictEqual((await agent.request({ origin, method: 'GET', path: '/persistent' })).statusCode, 200)
t.assert.deepStrictEqual(agent.pendingInterceptors(), [])
t.assert.doesNotThrow(() => agent.assertNoPendingInterceptors())
})
test('defaults to rendering output with terminal color when process.env.CI is unset', t => {
t.plan(1)
// This ensures that the test works in an environment where the CI env var is set.
const oldCiEnvVar = process.env.CI
delete process.env.CI
try {
mockAgentWithOneInterceptor().assertNoPendingInterceptors()
t.assert.fail('Should have thrown')
} catch (err) {
t.assert.deepStrictEqual(err.message, tableRowsAlignedToLeft
? `
1 interceptor is pending:
┌─────────┬────────┬───────────────────────┬──────┬─────────────┬────────────┬─────────────┬───────────┐
│ (index) │ Method │ Origin │ Path │ Status code │ Persistent │ Invocations │ Remaining │
├─────────┼────────┼───────────────────────┼──────┼─────────────┼────────────┼─────────────┼───────────┤
│ 0 │ \u001b[32m'GET'\u001b[39m │ \u001b[32m'https://example.com'\u001b[39m │ \u001b[32m'/'\u001b[39m │ \u001b[33m200\u001b[39m │ \u001b[32m'${N}'\u001b[39m │ \u001b[33m0\u001b[39m │ \u001b[33m1\u001b[39m │
└─────────┴────────┴───────────────────────┴──────┴─────────────┴────────────┴─────────────┴───────────┘
`.trim()
: `
1 interceptor is pending:
┌─────────┬────────┬───────────────────────┬──────┬─────────────┬────────────┬─────────────┬───────────┐
│ (index) │ Method │ Origin │ Path │ Status code │ Persistent │ Invocations │ Remaining │
├─────────┼────────┼───────────────────────┼──────┼─────────────┼────────────┼─────────────┼───────────┤
│ 0 │ \u001b[32m'GET'\u001b[39m │ \u001b[32m'https://example.com'\u001b[39m │ \u001b[32m'/'\u001b[39m │ \u001b[33m200\u001b[39m │ \u001b[32m'${N}'\u001b[39m │ \u001b[33m0\u001b[39m │ \u001b[33m1\u001b[39m │
└─────────┴────────┴───────────────────────┴──────┴─────────────┴────────────┴─────────────┴───────────┘
`.trim())
// Re-set the CI env var if it were set.
// Assigning `undefined` does not work,
// because reading the env var afterwards yields the string 'undefined',
// so we need to re-set it conditionally.
if (oldCiEnvVar != null) {
process.env.CI = oldCiEnvVar
}
}
})
test('returns unused interceptors', t => {
t.plan(1)
t.assert.deepStrictEqual(mockAgentWithOneInterceptor().pendingInterceptors(), [
{
timesInvoked: 0,
times: 1,
persist: false,
consumed: false,
pending: true,
ignoreTrailingSlash: false,
path: '/',
method: 'GET',
body: undefined,
query: undefined,
headers: undefined,
data: {
error: null,
statusCode: 200,
data: '',
headers: {},
trailers: {}
},
origin: 'https://example.com'
}
])
})
================================================
FILE: test/mock-interceptor.js
================================================
'use strict'
const { describe, test, after } = require('node:test')
const { MockInterceptor, MockScope } = require('../lib/mock/mock-interceptor')
const MockAgent = require('../lib/mock/mock-agent')
const { kDispatchKey } = require('../lib/mock/mock-symbols')
const { InvalidArgumentError } = require('../lib/core/errors')
const { fetch } = require('../lib/web/fetch/index')
describe('MockInterceptor - path', () => {
test('should remove hash fragment from paths', t => {
t.plan(1)
const mockInterceptor = new MockInterceptor({
path: '#foobar',
method: ''
}, [])
t.assert.strictEqual(mockInterceptor[kDispatchKey].path, '')
})
})
describe('MockInterceptor - reply', () => {
test('should return MockScope', t => {
t.plan(1)
const mockInterceptor = new MockInterceptor({
path: '',
method: ''
}, [])
const result = mockInterceptor.reply(200, 'hello')
t.assert.ok(result instanceof MockScope)
})
test('should error if passed options invalid', t => {
t.plan(2)
const mockInterceptor = new MockInterceptor({
path: '',
method: ''
}, [])
t.assert.throws(() => mockInterceptor.reply(), new InvalidArgumentError('statusCode must be defined'))
t.assert.throws(() => mockInterceptor.reply(200, '', 'hello'), new InvalidArgumentError('responseOptions must be an object'))
})
})
describe('MockInterceptor - reply callback', () => {
test('should return MockScope', t => {
t.plan(1)
const mockInterceptor = new MockInterceptor({
path: '',
method: ''
}, [])
const result = mockInterceptor.reply(200, () => 'hello')
t.assert.ok(result instanceof MockScope)
})
test('should error if passed options invalid', t => {
t.plan(3)
const mockInterceptor = new MockInterceptor({
path: '',
method: ''
}, [])
t.assert.throws(() => mockInterceptor.reply(), new InvalidArgumentError('statusCode must be defined'))
t.assert.throws(() => mockInterceptor.reply(200, () => { }, 'hello'), new InvalidArgumentError('responseOptions must be an object'))
t.assert.throws(() => mockInterceptor.reply(200, () => { }, null), new InvalidArgumentError('responseOptions must be an object'))
})
})
describe('MockInterceptor - reply options callback', () => {
test('should return MockScope', t => {
t.plan(2)
const mockInterceptor = new MockInterceptor({
path: '',
method: ''
}, [])
const result = mockInterceptor.reply((options) => ({
statusCode: 200,
data: 'hello'
}))
t.assert.ok(result instanceof MockScope)
// Test parameters
const baseUrl = 'http://localhost:9999'
const mockAgent = new MockAgent()
after(() => mockAgent.close())
const mockPool = mockAgent.get(baseUrl)
mockPool.intercept({
path: '/test',
method: 'GET'
}).reply((options) => {
t.assert.deepStrictEqual(options, { path: '/test', method: 'GET', headers: { foo: 'bar' } })
return { statusCode: 200, data: 'hello' }
})
mockPool.dispatch({
path: '/test',
method: 'GET',
headers: { foo: 'bar' }
}, {
onHeaders: () => { },
onData: () => { },
onComplete: () => { }
})
})
test('should handle undefined data', t => {
t.plan(2)
const mockInterceptor = new MockInterceptor({
path: '',
method: ''
}, [])
const result = mockInterceptor.reply((options) => ({
statusCode: 200,
data: undefined
}))
t.assert.ok(result instanceof MockScope)
// Test parameters
const baseUrl = 'http://localhost:9999'
const mockAgent = new MockAgent()
after(() => mockAgent.close())
const mockPool = mockAgent.get(baseUrl)
mockPool.intercept({
path: '/test',
method: 'GET'
}).reply((options) => {
t.assert.deepStrictEqual(options, { path: '/test', method: 'GET', headers: { foo: 'bar' } })
return { statusCode: 200, data: 'hello' }
})
mockPool.dispatch({
path: '/test',
method: 'GET',
headers: { foo: 'bar' }
}, {
onHeaders: () => { },
onData: () => { },
onComplete: () => { }
})
})
test('should error if passed options invalid', async (t) => {
t.plan(4)
const baseUrl = 'http://localhost:9999'
const mockAgent = new MockAgent()
after(() => mockAgent.close())
const mockPool = mockAgent.get(baseUrl)
mockPool.intercept({
path: '/test-return-undefined',
method: 'GET'
}).reply(() => { })
mockPool.intercept({
path: '/test-return-null',
method: 'GET'
}).reply(() => { return null })
mockPool.intercept({
path: '/test3',
method: 'GET'
}).reply(() => ({
statusCode: 200,
data: 'hello',
responseOptions: 42
}))
mockPool.intercept({
path: '/test4',
method: 'GET'
}).reply(() => ({
data: 'hello',
responseOptions: 42
}))
t.assert.throws(() => mockPool.dispatch({
path: '/test-return-undefined',
method: 'GET'
}, {
onHeaders: () => { },
onData: () => { },
onComplete: () => { }
}), new InvalidArgumentError('reply options callback must return an object'))
t.assert.throws(() => mockPool.dispatch({
path: '/test-return-null',
method: 'GET'
}, {
onHeaders: () => { },
onData: () => { },
onComplete: () => { }
}), new InvalidArgumentError('reply options callback must return an object'))
t.assert.throws(() => mockPool.dispatch({
path: '/test3',
method: 'GET'
}, {
onHeaders: () => { },
onData: () => { },
onComplete: () => { }
}), new InvalidArgumentError('responseOptions must be an object'))
t.assert.throws(() => mockPool.dispatch({
path: '/test4',
method: 'GET'
}, {
onHeaders: () => { },
onData: () => { },
onComplete: () => { }
}), new InvalidArgumentError('statusCode must be defined'))
})
})
describe('MockInterceptor - replyWithError', () => {
test('should return MockScope', t => {
t.plan(1)
const mockInterceptor = new MockInterceptor({
path: '',
method: ''
}, [])
const result = mockInterceptor.replyWithError(new Error('kaboom'))
t.assert.ok(result instanceof MockScope)
})
test('should error if passed options invalid', t => {
t.plan(1)
const mockInterceptor = new MockInterceptor({
path: '',
method: ''
}, [])
t.assert.throws(() => mockInterceptor.replyWithError(), new InvalidArgumentError('error must be defined'))
})
})
describe('MockInterceptor - defaultReplyHeaders', () => {
test('should return MockInterceptor', t => {
t.plan(1)
const mockInterceptor = new MockInterceptor({
path: '',
method: ''
}, [])
const result = mockInterceptor.defaultReplyHeaders({})
t.assert.ok(result instanceof MockInterceptor)
})
test('should error if passed options invalid', t => {
t.plan(1)
const mockInterceptor = new MockInterceptor({
path: '',
method: ''
}, [])
t.assert.throws(() => mockInterceptor.defaultReplyHeaders(), new InvalidArgumentError('headers must be defined'))
})
})
describe('MockInterceptor - defaultReplyTrailers', () => {
test('should return MockInterceptor', t => {
t.plan(1)
const mockInterceptor = new MockInterceptor({
path: '',
method: ''
}, [])
const result = mockInterceptor.defaultReplyTrailers({})
t.assert.ok(result instanceof MockInterceptor)
})
test('should error if passed options invalid', t => {
t.plan(1)
const mockInterceptor = new MockInterceptor({
path: '',
method: ''
}, [])
t.assert.throws(() => mockInterceptor.defaultReplyTrailers(), new InvalidArgumentError('trailers must be defined'))
})
})
describe('MockInterceptor - replyContentLength', () => {
test('should return MockInterceptor', t => {
t.plan(1)
const mockInterceptor = new MockInterceptor({
path: '',
method: ''
}, [])
const result = mockInterceptor.defaultReplyTrailers({})
t.assert.ok(result instanceof MockInterceptor)
})
})
describe('https://github.com/nodejs/undici/issues/3649', () => {
[
['/api/some-path', '/api/some-path'],
['/api/some-path/', '/api/some-path'],
['/api/some-path', '/api/some-path/'],
['/api/some-path/', '/api/some-path/'],
['/api/some-path////', '/api/some-path//'],
['', ''],
['/', ''],
['', '/'],
['/', '/']
].forEach(([interceptPath, fetchedPath], index) => {
test(`MockAgent should match with or without trailing slash by setting ignoreTrailingSlash as MockAgent option /${index}`, async (t) => {
t.plan(1)
const mockAgent = new MockAgent({ ignoreTrailingSlash: true })
mockAgent.disableNetConnect()
mockAgent
.get('https://localhost')
.intercept({ path: interceptPath }).reply(200, { ok: true })
const res = await fetch(new URL(fetchedPath, 'https://localhost'), { dispatcher: mockAgent })
t.assert.deepStrictEqual(await res.json(), { ok: true })
})
test(`MockAgent should match with or without trailing slash by setting ignoreTrailingSlash as intercept option /${index}`, async (t) => {
t.plan(1)
const mockAgent = new MockAgent()
mockAgent.disableNetConnect()
mockAgent
.get('https://localhost')
.intercept({ path: interceptPath, ignoreTrailingSlash: true }).reply(200, { ok: true })
const res = await fetch(new URL(fetchedPath, 'https://localhost'), { dispatcher: mockAgent })
t.assert.deepStrictEqual(await res.json(), { ok: true })
})
if (
(interceptPath === fetchedPath && (interceptPath !== '' && fetchedPath !== '')) ||
(interceptPath === '/' && fetchedPath === '')
) {
test(`MockAgent should should match on strict equal cases of paths when ignoreTrailingSlash is not set /${index}`, async (t) => {
t.plan(1)
const mockAgent = new MockAgent()
mockAgent.disableNetConnect()
mockAgent
.get('https://localhost')
.intercept({ path: interceptPath }).reply(200, { ok: true })
const res = await fetch(new URL(fetchedPath, 'https://localhost'), { dispatcher: mockAgent })
t.assert.deepStrictEqual(await res.json(), { ok: true })
})
} else {
test(`MockAgent should should reject on not strict equal cases of paths when ignoreTrailingSlash is not set /${index}`, async (t) => {
t.plan(1)
const mockAgent = new MockAgent()
mockAgent.disableNetConnect()
mockAgent
.get('https://localhost')
.intercept({ path: interceptPath }).reply(200, { ok: true })
await t.assert.rejects(fetch(new URL(fetchedPath, 'https://localhost'), { dispatcher: mockAgent }))
})
}
})
})
describe('MockInterceptor - different payloads', () => {
[
// Buffer
['arrayBuffer', 'ArrayBuffer', 'ArrayBuffer', new TextEncoder().encode('{"test":true}').buffer, new TextEncoder().encode('{"test":true}').buffer],
['json', 'ArrayBuffer', 'Object', new TextEncoder().encode('{"test":true}').buffer, { test: true }],
['bytes', 'ArrayBuffer', 'Uint8Array', new TextEncoder().encode('{"test":true}').buffer, new TextEncoder().encode('{"test":true}')],
['text', 'ArrayBuffer', 'string', new TextEncoder().encode('{"test":true}').buffer, '{"test":true}'],
// Buffer
['arrayBuffer', 'Buffer', 'ArrayBuffer', Buffer.from('{"test":true}'), new TextEncoder().encode('{"test":true}').buffer],
['json', 'Buffer', 'Object', Buffer.from('{"test":true}'), { test: true }],
['bytes', 'Buffer', 'Uint8Array', Buffer.from('{"test":true}'), new TextEncoder().encode('{"test":true}')],
['text', 'Buffer', 'string', Buffer.from('{"test":true}'), '{"test":true}'],
// Uint8Array
['arrayBuffer', 'Uint8Array', 'ArrayBuffer', new TextEncoder().encode('{"test":true}'), new TextEncoder().encode('{"test":true}').buffer],
['json', 'Uint8Array', 'Object', new TextEncoder().encode('{"test":true}'), { test: true }],
['bytes', 'Uint8Array', 'Uint8Array', new TextEncoder().encode('{"test":true}'), new TextEncoder().encode('{"test":true}')],
['text', 'Uint8Array', 'string', new TextEncoder().encode('{"test":true}'), '{"test":true}'],
// string
['arrayBuffer', 'string', 'ArrayBuffer', '{"test":true}', new TextEncoder().encode('{"test":true}').buffer],
['json', 'string', 'Object', '{"test":true}', { test: true }],
['bytes', 'string', 'Uint8Array', '{"test":true}', new TextEncoder().encode('{"test":true}')],
['text', 'string', 'string', '{"test":true}', '{"test":true}'],
// object
['arrayBuffer', 'Object', 'ArrayBuffer', { test: true }, new TextEncoder().encode('{"test":true}').buffer],
['json', 'Object', 'Object', { test: true }, { test: true }],
['bytes', 'Object', 'Uint8Array', { test: true }, new TextEncoder().encode('{"test":true}')],
['text', 'Object', 'string', { test: true }, '{"test":true}']
].forEach(([method, inputType, outputType, input, output]) => {
test(`${inputType} will be returned as ${outputType} via ${method}()`, async (t) => {
t.plan(1)
const mockAgent = new MockAgent()
mockAgent.disableNetConnect()
mockAgent
.get('https://localhost')
.intercept({ path: '/' }).reply(200, input)
const response = await fetch('https://localhost', { dispatcher: mockAgent })
t.assert.deepStrictEqual(await response[method](), output)
})
})
})
================================================
FILE: test/mock-pool.js
================================================
'use strict'
const { test, after, describe } = require('node:test')
const { createServer } = require('node:http')
const { promisify } = require('node:util')
const { MockAgent, MockPool, getGlobalDispatcher, setGlobalDispatcher, request } = require('..')
const { kUrl } = require('../lib/core/symbols')
const { kDispatches } = require('../lib/mock/mock-symbols')
const { InvalidArgumentError } = require('../lib/core/errors')
const { MockInterceptor } = require('../lib/mock/mock-interceptor')
const { getResponse } = require('../lib/mock/mock-utils')
const Dispatcher = require('../lib/dispatcher/dispatcher')
const { fetch } = require('..')
describe('MockPool - constructor', () => {
test('fails if opts.agent does not implement `get` method', t => {
t.plan(1)
t.assert.throws(() => new MockPool('http://localhost:9999', { agent: { get: 'not a function' } }), InvalidArgumentError)
})
test('sets agent', t => {
t.plan(1)
t.assert.doesNotThrow(() => new MockPool('http://localhost:9999', { agent: new MockAgent() }))
})
test('should implement the Dispatcher API', t => {
t.plan(1)
const mockPool = new MockPool('http://localhost:9999', { agent: new MockAgent() })
t.assert.ok(mockPool instanceof Dispatcher)
})
})
describe('MockPool - dispatch', () => {
test('should handle a single interceptor', (t) => {
t.plan(1)
const baseUrl = 'http://localhost:9999'
const mockAgent = new MockAgent()
after(() => mockAgent.close())
const mockPool = mockAgent.get(baseUrl)
this[kUrl] = new URL('http://localhost:9999')
mockPool[kDispatches] = [
{
path: '/foo',
method: 'GET',
data: {
statusCode: 200,
data: 'hello',
headers: {},
trailers: {},
error: null
}
}
]
t.assert.doesNotThrow(() => mockPool.dispatch({
path: '/foo',
method: 'GET'
}, {
onHeaders: (_statusCode, _headers, resume) => resume(),
onData: () => { },
onComplete: () => { }
}))
})
test('should directly throw error from mockDispatch function if error is not a MockNotMatchedError', (t) => {
t.plan(1)
const baseUrl = 'http://localhost:9999'
const mockAgent = new MockAgent()
after(() => mockAgent.close())
const mockPool = mockAgent.get(baseUrl)
this[kUrl] = new URL('http://localhost:9999')
mockPool[kDispatches] = [
{
path: '/foo',
method: 'GET',
data: {
statusCode: 200,
data: 'hello',
headers: {},
trailers: {},
error: null
}
}
]
t.assert.throws(() => mockPool.dispatch({
path: '/foo',
method: 'GET'
}, {
onHeaders: (_statusCode, _headers, resume) => { throw new Error('kaboom') },
onData: () => { },
onComplete: () => { }
}), new Error('kaboom'))
})
})
test('MockPool - intercept should return a MockInterceptor', (t) => {
t.plan(1)
const baseUrl = 'http://localhost:9999'
const mockAgent = new MockAgent()
after(() => mockAgent.close())
const mockPool = mockAgent.get(baseUrl)
const interceptor = mockPool.intercept({
path: '/foo',
method: 'GET'
})
t.assert.ok(interceptor instanceof MockInterceptor)
})
describe('MockPool - intercept validation', () => {
test('it should error if no options specified in the intercept', t => {
t.plan(1)
const mockAgent = new MockAgent()
after(() => mockAgent.close())
const mockPool = mockAgent.get('http://localhost:9999')
t.assert.throws(() => mockPool.intercept(), new InvalidArgumentError('opts must be an object'))
})
test('it should error if no path specified in the intercept', t => {
t.plan(1)
const mockAgent = new MockAgent()
after(() => mockAgent.close())
const mockPool = mockAgent.get('http://localhost:9999')
t.assert.throws(() => mockPool.intercept({}), new InvalidArgumentError('opts.path must be defined'))
})
test('it should default to GET if no method specified in the intercept', t => {
t.plan(1)
const mockAgent = new MockAgent()
after(() => mockAgent.close())
const mockPool = mockAgent.get('http://localhost:9999')
t.assert.doesNotThrow(() => mockPool.intercept({ path: '/foo' }))
})
})
test('MockPool - close should run without error', async (t) => {
t.plan(1)
const baseUrl = 'http://localhost:9999'
const mockAgent = new MockAgent()
after(() => mockAgent.close())
const mockPool = mockAgent.get(baseUrl)
mockPool[kDispatches] = [
{
path: '/foo',
method: 'GET',
data: {
statusCode: 200,
data: 'hello',
headers: {},
trailers: {},
error: null
}
}
]
await mockPool.close()
t.assert.ok(true, 'pass')
})
test('MockPool - should be able to set as globalDispatcher', async (t) => {
t.plan(3)
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.setHeader('content-type', 'text/plain')
res.end('should not be called')
t.assert.fail('should not be called')
})
after(() => {
server.closeAllConnections?.()
server.close()
})
await promisify(server.listen.bind(server))(0)
const baseUrl = `http://localhost:${server.address().port}`
const mockAgent = new MockAgent()
after(() => mockAgent.close())
const mockPool = mockAgent.get(baseUrl)
t.assert.ok(mockPool instanceof MockPool)
setGlobalDispatcher(mockPool)
mockPool.intercept({
path: '/foo',
method: 'GET'
}).reply(200, 'hello')
const { statusCode, body } = await request(`${baseUrl}/foo`, {
method: 'GET'
})
t.assert.strictEqual(statusCode, 200)
const response = await getResponse(body)
t.assert.deepStrictEqual(response, 'hello')
})
test('MockPool - should be able to use as a local dispatcher', async (t) => {
t.plan(3)
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.setHeader('content-type', 'text/plain')
res.end('should not be called')
t.assert.fail('should not be called')
})
after(() => {
server.closeAllConnections?.()
server.close()
})
await promisify(server.listen.bind(server))(0)
const baseUrl = `http://localhost:${server.address().port}`
const mockAgent = new MockAgent()
after(() => mockAgent.close())
const mockPool = mockAgent.get(baseUrl)
t.assert.ok(mockPool instanceof MockPool)
mockPool.intercept({
path: '/foo',
method: 'GET'
}).reply(200, 'hello')
const { statusCode, body } = await request(`${baseUrl}/foo`, {
method: 'GET',
dispatcher: mockPool
})
t.assert.strictEqual(statusCode, 200)
const response = await getResponse(body)
t.assert.deepStrictEqual(response, 'hello')
})
test('MockPool - basic intercept with MockPool.request', async (t) => {
t.plan(5)
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.setHeader('content-type', 'text/plain')
res.end('should not be called')
t.assert.fail('should not be called')
})
after(() => {
server.closeAllConnections?.()
server.close()
})
await promisify(server.listen.bind(server))(0)
const baseUrl = `http://localhost:${server.address().port}`
const mockAgent = new MockAgent()
after(() => mockAgent.close())
const mockPool = mockAgent.get(baseUrl)
t.assert.ok(mockPool instanceof MockPool)
mockPool.intercept({
path: '/foo?hello=there&see=ya',
method: 'POST',
body: 'form1=data1&form2=data2'
}).reply(200, { foo: 'bar' }, {
headers: { 'content-type': 'application/json' },
trailers: { 'Content-MD5': 'test' }
})
const { statusCode, headers, trailers, body } = await mockPool.request({
origin: baseUrl,
path: '/foo?hello=there&see=ya',
method: 'POST',
body: 'form1=data1&form2=data2'
})
t.assert.strictEqual(statusCode, 200)
t.assert.strictEqual(headers['content-type'], 'application/json')
t.assert.deepStrictEqual(trailers, { 'content-md5': 'test' })
const jsonResponse = JSON.parse(await getResponse(body))
t.assert.deepStrictEqual(jsonResponse, {
foo: 'bar'
})
})
// https://github.com/nodejs/undici/issues/1546
test('MockPool - correct errors when consuming invalid JSON body', async (t) => {
t.plan(1)
const oldDispatcher = getGlobalDispatcher()
const mockAgent = new MockAgent()
mockAgent.disableNetConnect()
setGlobalDispatcher(mockAgent)
after(() => setGlobalDispatcher(oldDispatcher))
const mockPool = mockAgent.get('https://google.com')
mockPool.intercept({
path: 'https://google.com'
}).reply(200, 'it\'s just a text')
const { body } = await request('https://google.com')
await t.assert.rejects(body.json(), SyntaxError)
})
test('MockPool - allows matching headers in fetch', async (t) => {
t.plan(2)
const oldDispatcher = getGlobalDispatcher()
const baseUrl = 'http://localhost:9999'
const mockAgent = new MockAgent()
mockAgent.disableNetConnect()
setGlobalDispatcher(mockAgent)
after(async () => {
await mockAgent.close()
setGlobalDispatcher(oldDispatcher)
})
const pool = mockAgent.get(baseUrl)
pool.intercept({
path: '/foo',
method: 'GET',
headers: {
accept: 'application/json'
}
}).reply(200, { ok: 1 }).times(3)
await fetch(`${baseUrl}/foo`, {
headers: {
accept: 'application/json'
}
})
// no 'accept: application/json' header sent, not matched
await t.assert.rejects(fetch(`${baseUrl}/foo`))
// not 'accept: application/json', not matched
await t.assert.rejects(fetch(`${baseUrl}/foo`, {
headers: {
accept: 'text/plain'
}
}), new TypeError('fetch failed'))
})
test('MockPool - cleans mocks', async (t) => {
t.plan(4)
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.setHeader('content-type', 'text/plain')
res.end('hello')
})
after(() => {
server.closeAllConnections?.()
server.close()
})
await promisify(server.listen.bind(server))(0)
const baseUrl = `http://localhost:${server.address().port}`
const mockAgent = new MockAgent()
after(() => mockAgent.close())
const mockPool = mockAgent.get(baseUrl)
t.assert.ok(mockPool instanceof MockPool)
setGlobalDispatcher(mockPool)
mockPool.intercept({
path: '/foo',
method: 'GET'
}).reply(500, () => {
t.assert.fail('should not be called')
})
mockPool.cleanMocks()
t.assert.strictEqual(mockPool[kDispatches].length, 0)
const { statusCode, body } = await request(`${baseUrl}/foo`, {
method: 'GET'
})
t.assert.strictEqual(statusCode, 200)
const response = await getResponse(body)
t.assert.deepStrictEqual(response, 'hello')
})
================================================
FILE: test/mock-scope.js
================================================
'use strict'
const { test, describe } = require('node:test')
const { MockScope } = require('../lib/mock/mock-interceptor')
const { InvalidArgumentError } = require('../lib/core/errors')
describe('MockScope - delay', () => {
test('should return MockScope', t => {
t.plan(1)
const mockScope = new MockScope({
path: '',
method: ''
}, [])
const result = mockScope.delay(200)
t.assert.ok(result instanceof MockScope)
})
test('should error if passed options invalid', t => {
t.plan(4)
const mockScope = new MockScope({
path: '',
method: ''
}, [])
t.assert.throws(() => mockScope.delay(), new InvalidArgumentError('waitInMs must be a valid integer > 0'))
t.assert.throws(() => mockScope.delay(200.1), new InvalidArgumentError('waitInMs must be a valid integer > 0'))
t.assert.throws(() => mockScope.delay(0), new InvalidArgumentError('waitInMs must be a valid integer > 0'))
t.assert.throws(() => mockScope.delay(-1), new InvalidArgumentError('waitInMs must be a valid integer > 0'))
})
})
describe('MockScope - persist', () => {
test('should return MockScope', t => {
t.plan(1)
const mockScope = new MockScope({
path: '',
method: ''
}, [])
const result = mockScope.persist()
t.assert.ok(result instanceof MockScope)
})
})
describe('MockScope - times', t => {
test('should return MockScope', t => {
t.plan(1)
const mockScope = new MockScope({
path: '',
method: ''
}, [])
const result = mockScope.times(200)
t.assert.ok(result instanceof MockScope)
})
test('should error if passed options invalid', t => {
t.plan(4)
const mockScope = new MockScope({
path: '',
method: ''
}, [])
t.assert.throws(() => mockScope.times(), new InvalidArgumentError('repeatTimes must be a valid integer > 0'))
t.assert.throws(() => mockScope.times(200.1), new InvalidArgumentError('repeatTimes must be a valid integer > 0'))
t.assert.throws(() => mockScope.times(0), new InvalidArgumentError('repeatTimes must be a valid integer > 0'))
t.assert.throws(() => mockScope.times(-1), new InvalidArgumentError('repeatTimes must be a valid integer > 0'))
})
})
================================================
FILE: test/mock-utils.js
================================================
'use strict'
const { test, describe } = require('node:test')
const { MockNotMatchedError } = require('../lib/mock/mock-errors')
const {
deleteMockDispatch,
getMockDispatch,
getResponseData,
getStatusText,
getHeaderByName,
buildHeadersFromArray,
normalizeSearchParams,
normalizeOrigin
} = require('../lib/mock/mock-utils')
test('deleteMockDispatch - should do nothing if not able to find mock dispatch', (t) => {
t.plan(1)
const key = {
url: 'url',
path: 'path',
method: 'method',
body: 'body'
}
t.assert.doesNotThrow(() => deleteMockDispatch([], key))
})
describe('getMockDispatch', () => {
test('it should find a mock dispatch', (t) => {
t.plan(1)
const dispatches = [
{
path: 'path',
method: 'method',
consumed: false
}
]
const result = getMockDispatch(dispatches, {
path: 'path',
method: 'method'
})
t.assert.deepStrictEqual(result, {
path: 'path',
method: 'method',
consumed: false
})
})
test('it should skip consumed dispatches', (t) => {
t.plan(1)
const dispatches = [
{
path: 'path',
method: 'method',
consumed: true
},
{
path: 'path',
method: 'method',
consumed: false
}
]
const result = getMockDispatch(dispatches, {
path: 'path',
method: 'method'
})
t.assert.deepStrictEqual(result, {
path: 'path',
method: 'method',
consumed: false
})
})
test('it should throw if dispatch not found', (t) => {
t.plan(1)
const dispatches = [
{
path: 'path',
method: 'method',
consumed: false
}
]
t.assert.throws(() => getMockDispatch(dispatches, {
path: 'wrong',
method: 'wrong'
}), new MockNotMatchedError('Mock dispatch not matched for path \'wrong\''))
})
test('it should throw if no dispatch matches method', (t) => {
t.plan(1)
const dispatches = [
{
path: 'path',
method: 'method',
consumed: false
}
]
t.assert.throws(() => getMockDispatch(dispatches, {
path: 'path',
method: 'wrong'
}), new MockNotMatchedError('Mock dispatch not matched for method \'wrong\' on path \'path\''))
})
test('it should throw if no dispatch matches body', (t) => {
t.plan(1)
const dispatches = [
{
path: 'path',
method: 'method',
body: 'body',
consumed: false
}
]
t.assert.throws(() => getMockDispatch(dispatches, {
path: 'path',
method: 'method',
body: 'wrong'
}), new MockNotMatchedError('Mock dispatch not matched for body \'wrong\' on path \'path\''))
})
test('it should throw if no dispatch matches headers', (t) => {
t.plan(1)
const dispatches = [
{
path: 'path',
method: 'method',
body: 'body',
headers: { key: 'value' },
consumed: false
}
]
t.assert.throws(() => getMockDispatch(dispatches, {
path: 'path',
method: 'method',
body: 'body',
headers: { key: 'wrong' }
}), new MockNotMatchedError('Mock dispatch not matched for headers \'{"key":"wrong"}\' on path \'path\''))
})
})
describe('getResponseData', () => {
test('it should stringify objects', (t) => {
t.plan(1)
const responseData = getResponseData({ str: 'string', num: 42 })
t.assert.strictEqual(responseData, '{"str":"string","num":42}')
})
test('it should return strings untouched', (t) => {
t.plan(1)
const responseData = getResponseData('test')
t.assert.strictEqual(responseData, 'test')
})
test('it should return buffers untouched', (t) => {
t.plan(1)
const responseData = getResponseData(Buffer.from('test'))
t.assert.ok(Buffer.isBuffer(responseData))
})
test('it should return Uint8Array untouched', (t) => {
t.plan(1)
const responseData = getResponseData(new TextEncoder().encode('{"test":true}'))
t.assert.ok(responseData instanceof Uint8Array)
})
test('it should return ArrayBuffers untouched', (t) => {
t.plan(1)
const responseData = getResponseData(new TextEncoder().encode('{"test":true}').buffer)
t.assert.ok(responseData instanceof ArrayBuffer)
})
test('it should handle undefined', (t) => {
t.plan(1)
const responseData = getResponseData(undefined)
t.assert.strictEqual(responseData, '')
})
})
test('getStatusText', (t) => {
t.plan(64)
for (const statusCode of [
100, 101, 102, 103, 200, 201, 202, 203,
204, 205, 206, 207, 208, 226, 300, 301,
302, 303, 304, 305, 306, 307, 308, 400,
401, 402, 403, 404, 405, 406, 407, 408,
409, 410, 411, 412, 413, 414, 415, 416,
417, 418, 421, 422, 423, 424, 425, 426,
428, 429, 431, 451, 500, 501, 502, 503,
504, 505, 506, 507, 508, 510, 511
]) {
t.assert.ok(getStatusText(statusCode))
}
t.assert.strictEqual(getStatusText(420), 'unknown')
})
test('getHeaderByName', (t) => {
t.plan(6)
const headersRecord = {
key: 'value'
}
t.assert.strictEqual(getHeaderByName(headersRecord, 'key'), 'value')
t.assert.strictEqual(getHeaderByName(headersRecord, 'anotherKey'), undefined)
const headersArray = ['key', 'value']
t.assert.strictEqual(getHeaderByName(headersArray, 'key'), 'value')
t.assert.strictEqual(getHeaderByName(headersArray, 'anotherKey'), undefined)
const { Headers } = require('../index')
const headers = new Headers([
['key', 'value']
])
t.assert.strictEqual(getHeaderByName(headers, 'key'), 'value')
t.assert.strictEqual(getHeaderByName(headers, 'anotherKey'), null)
})
describe('buildHeadersFromArray', () => {
test('it should build headers from array', (t) => {
t.plan(2)
const headers = buildHeadersFromArray([
'key', 'value'
])
t.assert.deepStrictEqual(Object.keys(headers).length, 1)
t.assert.strictEqual(headers.key, 'value')
})
})
describe('normalizeQueryParams', () => {
test('it should handle basic cases', (t) => {
t.plan(4)
t.assert.deepStrictEqual(normalizeSearchParams('').toString(), '')
t.assert.deepStrictEqual(normalizeSearchParams('a').toString(), 'a=')
t.assert.deepStrictEqual(normalizeSearchParams('b=2&c=3&a=1').toString(), 'b=2&c=3&a=1')
t.assert.deepStrictEqual(normalizeSearchParams('lang=en_EN&id=123').toString(), 'lang=en_EN&id=123')
})
// https://github.com/nodejs/undici/issues/4146
test('it should handle multiple values set using different syntaxes', (t) => {
t.plan(3)
t.assert.deepStrictEqual(normalizeSearchParams('a=1&a=2&a=3').toString(), 'a=1&a=2&a=3')
t.assert.deepStrictEqual(normalizeSearchParams('a[]=1&a[]=2&a[]=3').toString(), 'a=1&a=2&a=3')
t.assert.deepStrictEqual(normalizeSearchParams('a=1,2,3').toString(), 'a=1&a=2&a=3')
})
test('should handle edge case scenarios', (t) => {
t.plan(4)
t.assert.deepStrictEqual(normalizeSearchParams('a="b[]"').toString(), `a=${encodeURIComponent('"b[]"')}`)
t.assert.deepStrictEqual(normalizeSearchParams('a="1,2,3"').toString(), `a=${encodeURIComponent('"1,2,3"')}`)
const encodedSingleQuote = '%27'
t.assert.deepStrictEqual(normalizeSearchParams("a='b[]'").toString(), `a=${encodedSingleQuote}${encodeURIComponent('b[]')}${encodedSingleQuote}`)
t.assert.deepStrictEqual(normalizeSearchParams("a='1,2,3'").toString(), `a=${encodedSingleQuote}${encodeURIComponent('1,2,3')}${encodedSingleQuote}`)
})
})
describe('normalizeOrigin', () => {
test('should normalize hostname to lowercase for string origins', (t) => {
t.plan(4)
t.assert.strictEqual(normalizeOrigin('http://Example.com'), 'http://example.com')
t.assert.strictEqual(normalizeOrigin('http://EXAMPLE.COM'), 'http://example.com')
t.assert.strictEqual(normalizeOrigin('https://Api.Example.com'), 'https://api.example.com')
t.assert.strictEqual(normalizeOrigin('http://MyEndpoint'), 'http://myendpoint')
})
test('should normalize hostname to lowercase for URL objects', (t) => {
t.plan(4)
t.assert.strictEqual(normalizeOrigin(new URL('http://Example.com')), 'http://example.com')
t.assert.strictEqual(normalizeOrigin(new URL('http://EXAMPLE.COM')), 'http://example.com')
t.assert.strictEqual(normalizeOrigin(new URL('https://Api.Example.com')), 'https://api.example.com')
t.assert.strictEqual(normalizeOrigin(new URL('http://MyEndpoint')), 'http://myendpoint')
})
test('should preserve port numbers', (t) => {
t.plan(3)
t.assert.strictEqual(normalizeOrigin('http://Example.com:8080'), 'http://example.com:8080')
t.assert.strictEqual(normalizeOrigin(new URL('http://Example.com:3000')), 'http://example.com:3000')
t.assert.strictEqual(normalizeOrigin(new URL('https://Test.com:8443')), 'https://test.com:8443')
})
test('should return RegExp matchers as-is', (t) => {
t.plan(1)
const regex = /http:\/\/example\.com/
t.assert.strictEqual(normalizeOrigin(regex), regex)
})
test('should return function matchers as-is', (t) => {
t.plan(1)
const fn = (origin) => origin === 'http://example.com'
t.assert.strictEqual(normalizeOrigin(fn), fn)
})
test('should return other non-string, non-URL types as-is', (t) => {
t.plan(4)
const obj = { origin: 'http://example.com' }
const num = 123
const bool = true
const nullValue = null
t.assert.strictEqual(normalizeOrigin(obj), obj)
t.assert.strictEqual(normalizeOrigin(num), num)
t.assert.strictEqual(normalizeOrigin(bool), bool)
t.assert.strictEqual(normalizeOrigin(nullValue), nullValue)
})
test('should handle invalid URLs gracefully', (t) => {
t.plan(2)
// Invalid URL strings should be returned as-is
t.assert.strictEqual(normalizeOrigin('not-a-url'), 'not-a-url')
t.assert.strictEqual(normalizeOrigin('://invalid'), '://invalid')
})
test('should handle IPv4 addresses', (t) => {
t.plan(2)
t.assert.strictEqual(normalizeOrigin('http://192.168.1.1'), 'http://192.168.1.1')
t.assert.strictEqual(normalizeOrigin('http://127.0.0.1:3000'), 'http://127.0.0.1:3000')
})
test('should handle IPv6 addresses', (t) => {
t.plan(2)
t.assert.strictEqual(normalizeOrigin('http://[::1]'), 'http://[::1]')
t.assert.strictEqual(normalizeOrigin('http://[2001:db8::1]:8080'), 'http://[2001:db8::1]:8080')
})
test('should handle localhost with different cases', (t) => {
t.plan(3)
t.assert.strictEqual(normalizeOrigin('http://LocalHost'), 'http://localhost')
t.assert.strictEqual(normalizeOrigin('http://LOCALHOST:3000'), 'http://localhost:3000')
t.assert.strictEqual(normalizeOrigin(new URL('http://LocalHost')), 'http://localhost')
})
test('should handle subdomains with mixed case', (t) => {
t.plan(3)
t.assert.strictEqual(normalizeOrigin('http://Api.Example.Com'), 'http://api.example.com')
t.assert.strictEqual(normalizeOrigin('https://WWW.Example.COM'), 'https://www.example.com')
t.assert.strictEqual(normalizeOrigin(new URL('http://Sub.Domain.Example.Com')), 'http://sub.domain.example.com')
})
test('should handle paths in URL objects (should only normalize origin part)', (t) => {
t.plan(2)
// URL objects with paths should still only return the origin
const url1 = new URL('http://Example.com/path/to/resource')
t.assert.strictEqual(normalizeOrigin(url1), 'http://example.com')
const url2 = new URL('https://Api.Example.com:8080/api/v1')
t.assert.strictEqual(normalizeOrigin(url2), 'https://api.example.com:8080')
})
})
================================================
FILE: test/no-strict-content-length.js
================================================
'use strict'
const { tspl } = require('@matteo.collina/tspl')
const { ok } = require('node:assert')
const { test, after, describe } = require('node:test')
const { Client } = require('..')
const { createServer } = require('node:http')
const { Readable } = require('node:stream')
const { wrapWithAsyncIterable } = require('./utils/async-iterators')
describe('strictContentLength: false', () => {
const emitWarningOriginal = process.emitWarning
let emitWarningCalled = false
process.emitWarning = function () {
emitWarningCalled = true
}
function assertEmitWarningCalledAndReset () {
ok(emitWarningCalled)
emitWarningCalled = false
}
after(() => {
process.emitWarning = emitWarningOriginal
})
test('request invalid content-length', async (t) => {
t = tspl(t, { plan: 8 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.end()
})
after(() => {
server.closeAllConnections?.()
server.close()
})
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`, {
strictContentLength: false
})
after(() => client.close())
client.request({
path: '/',
method: 'PUT',
headers: {
'content-length': 10
},
body: 'asd'
}, (err, data) => {
assertEmitWarningCalledAndReset()
t.ifError(err)
})
client.request({
path: '/',
method: 'PUT',
headers: {
'content-length': 10
},
body: 'asdasdasdasdasdasda'
}, (err, data) => {
assertEmitWarningCalledAndReset()
t.ifError(err)
})
client.request({
path: '/',
method: 'PUT',
headers: {
'content-length': 10
},
body: Buffer.alloc(9)
}, (err, data) => {
assertEmitWarningCalledAndReset()
t.ifError(err)
})
client.request({
path: '/',
method: 'PUT',
headers: {
'content-length': 10
},
body: Buffer.alloc(11)
}, (err, data) => {
assertEmitWarningCalledAndReset()
t.ifError(err)
})
client.request({
path: '/',
method: 'HEAD',
headers: {
'content-length': 10
}
}, (err, data) => {
t.ifError(err)
})
client.request({
path: '/',
method: 'GET',
headers: {
'content-length': 0
}
}, (err, data) => {
t.ifError(err)
})
client.request({
path: '/',
method: 'GET',
headers: {
'content-length': 4
},
body: new Readable({
read () {
this.push('asd')
this.push(null)
}
})
}, (err, data) => {
t.ifError(err)
})
client.request({
path: '/',
method: 'GET',
headers: {
'content-length': 4
},
body: new Readable({
read () {
this.push('asasdasdasdd')
this.push(null)
}
})
}, (err, data) => {
t.ifError(err)
})
})
await t.completed
})
test('request streaming content-length less than body size', async (t) => {
t = tspl(t, { plan: 1 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.end()
})
after(() => {
server.closeAllConnections?.()
server.close()
})
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`, {
strictContentLength: false
})
after(() => client.close())
client.on('disconnect', () => {
if (!client.closed && !client.destroyed) {
t.fail('unexpected disconnect')
}
})
client.request({
path: '/',
method: 'PUT',
headers: {
'content-length': 2
},
body: new Readable({
read () {
setImmediate(() => {
this.push('abcd')
this.push(null)
})
}
})
}, (err) => {
assertEmitWarningCalledAndReset()
t.ifError(err)
})
})
await t.completed
})
test('request streaming content-length greater than body size', async (t) => {
t = tspl(t, { plan: 1 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.end()
})
after(() => {
server.closeAllConnections?.()
server.close()
})
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`, {
strictContentLength: false
})
after(() => client.close())
client.on('disconnect', () => {
if (!client.closed && !client.destroyed) {
t.fail('unexpected disconnect')
}
})
client.request({
path: '/',
method: 'PUT',
headers: {
'content-length': 10
},
body: new Readable({
read () {
setImmediate(() => {
this.push('abcd')
this.push(null)
})
}
})
}, (err) => {
assertEmitWarningCalledAndReset()
t.ifError(err)
})
})
await t.completed
})
test('request streaming data when content-length=0', async (t) => {
t = tspl(t, { plan: 1 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.end()
})
after(() => {
server.closeAllConnections?.()
server.close()
})
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`, {
strictContentLength: false
})
after(() => client.close())
client.on('disconnect', () => {
if (!client.closed && !client.destroyed) {
t.fail('unexpected disconnect')
}
})
client.request({
path: '/',
method: 'PUT',
headers: {
'content-length': 0
},
body: new Readable({
read () {
setImmediate(() => {
this.push('asdasdasdkajsdnasdkjasnd')
this.push(null)
})
}
})
}, (err) => {
assertEmitWarningCalledAndReset()
t.ifError(err)
})
})
await t.completed
})
test('request async iterating content-length less than body size', async (t) => {
t = tspl(t, { plan: 1 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.end()
})
after(() => {
server.closeAllConnections?.()
server.close()
})
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`, {
strictContentLength: false
})
after(() => client.close())
client.on('disconnect', () => {
if (!client.closed && !client.destroyed) {
t.fail('unexpected disconnect')
}
})
client.request({
path: '/',
method: 'PUT',
headers: {
'content-length': 2
},
body: wrapWithAsyncIterable(new Readable({
read () {
setImmediate(() => {
this.push('abcd')
this.push(null)
})
}
}))
}, (err) => {
assertEmitWarningCalledAndReset()
t.ifError(err)
})
})
await t.completed
})
test('request async iterator content-length greater than body size', async (t) => {
t = tspl(t, { plan: 1 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.end()
})
after(() => {
server.closeAllConnections?.()
server.close()
})
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`, {
strictContentLength: false
})
after(() => client.close())
client.on('disconnect', () => {
if (!client.closed && !client.destroyed) {
t.fail('unexpected disconnect')
}
})
client.request({
path: '/',
method: 'PUT',
headers: {
'content-length': 10
},
body: wrapWithAsyncIterable(new Readable({
read () {
setImmediate(() => {
this.push('abcd')
this.push(null)
})
}
}))
}, (err) => {
assertEmitWarningCalledAndReset()
t.ifError(err)
})
})
await t.completed
})
test('request async iterator data when content-length=0', async (t) => {
t = tspl(t, { plan: 1 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.end()
})
after(() => {
server.closeAllConnections?.()
server.close()
})
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`, {
strictContentLength: false
})
after(() => client.close())
client.on('disconnect', () => {
if (!client.closed && !client.destroyed) {
t.fail('unexpected disconnect')
}
})
client.request({
path: '/',
method: 'PUT',
headers: {
'content-length': 0
},
body: wrapWithAsyncIterable(new Readable({
read () {
setImmediate(() => {
this.push('asdasdasdkajsdnasdkjasnd')
this.push(null)
})
}
}))
}, (err) => {
assertEmitWarningCalledAndReset()
t.ifError(err)
})
})
await t.completed
})
})
================================================
FILE: test/node-fetch/LICENSE
================================================
The MIT License (MIT)
Copyright (c) 2016 - 2020 Node Fetch Team
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: test/node-fetch/headers.js
================================================
'use strict'
const assert = require('node:assert')
const { describe, it } = require('node:test')
const { format } = require('node:util')
const { Headers } = require('../../index.js')
describe('Headers', () => {
it('should have attributes conforming to Web IDL', () => {
const headers = new Headers()
assert.strictEqual(Object.getOwnPropertyNames(headers).length, 0)
const enumerableProperties = []
for (const property in headers) {
enumerableProperties.push(property)
}
for (const toCheck of [
'append',
'delete',
'entries',
'forEach',
'get',
'has',
'keys',
'set',
'values'
]) {
assert.strictEqual(enumerableProperties.includes(toCheck), true)
}
})
it('should allow iterating through all headers with forEach', () => {
const headers = new Headers([
['b', '2'],
['c', '4'],
['b', '3'],
['a', '1']
])
assert.strictEqual(typeof headers.forEach, 'function')
const result = []
for (const [key, value] of headers.entries()) {
result.push([key, value])
}
assert.deepStrictEqual(result, [
['a', '1'],
['b', '2, 3'],
['c', '4']
])
})
it('should be iterable with forEach', () => {
const headers = new Headers()
headers.append('Accept', 'application/json')
headers.append('Accept', 'text/plain')
headers.append('Content-Type', 'text/html')
const results = []
headers.forEach((value, key, object) => {
results.push({ value, key, object })
})
assert.strictEqual(results.length, 2)
assert.deepStrictEqual(results[0], { key: 'accept', value: 'application/json, text/plain', object: headers })
assert.deepStrictEqual(results[1], { key: 'content-type', value: 'text/html', object: headers })
})
it.skip('should set "this" to undefined by default on forEach', () => {
const headers = new Headers({ Accept: 'application/json' })
headers.forEach(function () {
assert.strictEqual(this, undefined)
})
})
it('should accept thisArg as a second argument for forEach', () => {
const headers = new Headers({ Accept: 'application/json' })
const thisArg = {}
headers.forEach(function () {
assert.strictEqual(this, thisArg)
}, thisArg)
})
it('should allow iterating through all headers with for-of loop', () => {
const headers = new Headers([
['b', '2'],
['c', '4'],
['a', '1']
])
headers.append('b', '3')
assert.strictEqual(typeof headers[Symbol.iterator], 'function')
const result = []
for (const pair of headers) {
result.push(pair)
}
assert.deepStrictEqual(result, [
['a', '1'],
['b', '2, 3'],
['c', '4']
])
})
it('should allow iterating through all headers with entries()', () => {
const headers = new Headers([
['b', '2'],
['c', '4'],
['a', '1']
])
headers.append('b', '3')
assert.strictEqual(typeof headers.entries, 'function')
assert.strictEqual(typeof headers.entries()[Symbol.iterator], 'function')
const entries = headers.entries()
assert.strictEqual(typeof entries.next, 'function')
assert.deepStrictEqual(entries.next().value, ['a', '1'])
assert.strictEqual(typeof entries.next, 'function')
assert.deepStrictEqual(entries.next().value, ['b', '2, 3'])
assert.strictEqual(typeof entries.next, 'function')
assert.deepStrictEqual(entries.next().value, ['c', '4'])
assert.deepStrictEqual([...headers.entries()], [
['a', '1'],
['b', '2, 3'],
['c', '4']
])
})
it('should allow iterating through all headers with keys()', () => {
const headers = new Headers([
['b', '2'],
['c', '4'],
['a', '1']
])
headers.append('b', '3')
assert.strictEqual(typeof headers.keys, 'function')
assert.strictEqual(typeof headers.keys()[Symbol.iterator], 'function')
const keys = headers.keys()
assert.strictEqual(typeof keys.next, 'function')
assert.strictEqual(keys.next().value, 'a')
assert.strictEqual(typeof keys.next, 'function')
assert.strictEqual(keys.next().value, 'b')
assert.strictEqual(typeof keys.next, 'function')
assert.strictEqual(keys.next().value, 'c')
assert.deepStrictEqual([...headers.keys()], ['a', 'b', 'c'])
})
it('should allow iterating through all headers with values()', () => {
const headers = new Headers([
['b', '2'],
['c', '4'],
['a', '1']
])
headers.append('b', '3')
assert.strictEqual(typeof headers.values, 'function')
assert.strictEqual(typeof headers.values()[Symbol.iterator], 'function')
const values = headers.values()
assert.strictEqual(typeof values.next, 'function')
assert.strictEqual(values.next().value, '1')
assert.strictEqual(typeof values.next, 'function')
assert.strictEqual(values.next().value, '2, 3')
assert.strictEqual(typeof values.next, 'function')
assert.strictEqual(values.next().value, '4')
assert.deepStrictEqual([...headers.values()], ['1', '2, 3', '4'])
})
it('should reject illegal header', () => {
const headers = new Headers()
assert.throws(() => new Headers({ 'He y': 'ok' }), TypeError)
assert.throws(() => new Headers({ 'Hé-y': 'ok' }), TypeError)
assert.throws(() => new Headers({ 'He-y': 'ăk' }), TypeError)
assert.throws(() => headers.append('Hé-y', 'ok'), TypeError)
assert.throws(() => headers.delete('Hé-y'), TypeError)
assert.throws(() => headers.get('Hé-y'), TypeError)
assert.throws(() => headers.has('Hé-y'), TypeError)
assert.throws(() => headers.set('Hé-y', 'ok'), TypeError)
// Should reject empty header
assert.throws(() => headers.append('', 'ok'), TypeError)
})
it.skip('should ignore unsupported attributes while reading headers', () => {
const FakeHeader = function () { }
// Prototypes are currently ignored
// This might change in the future: #181
FakeHeader.prototype.z = 'fake'
const res = new FakeHeader()
res.a = 'string'
res.b = ['1', '2']
res.c = ''
res.d = []
res.e = 1
res.f = [1, 2]
res.g = { a: 1 }
res.h = undefined
res.i = null
res.j = Number.NaN
res.k = true
res.l = false
res.m = Buffer.from('test')
const h1 = new Headers(res)
h1.set('n', [1, 2])
h1.append('n', ['3', 4])
const h1Raw = h1.raw()
assert.strictEqual(h1Raw.a.includes('string'), true)
assert.strictEqual(h1Raw.b.includes('1,2'), true)
assert.strictEqual(h1Raw.c.includes(''), true)
assert.strictEqual(h1Raw.d.includes(''), true)
assert.strictEqual(h1Raw.e.includes('1'), true)
assert.strictEqual(h1Raw.f.includes('1,2'), true)
assert.strictEqual(h1Raw.g.includes('[object Object]'), true)
assert.strictEqual(h1Raw.h.includes('undefined'), true)
assert.strictEqual(h1Raw.i.includes('null'), true)
assert.strictEqual(h1Raw.j.includes('NaN'), true)
assert.strictEqual(h1Raw.k.includes('true'), true)
assert.strictEqual(h1Raw.l.includes('false'), true)
assert.strictEqual(h1Raw.m.includes('test'), true)
assert.strictEqual(h1Raw.n.includes('1,2'), true)
assert.strictEqual(h1Raw.n.includes('3,4'), true)
assert.strictEqual(h1Raw.z, undefined)
})
it.skip('should wrap headers', () => {
const h1 = new Headers({
a: '1'
})
const h1Raw = h1.raw()
const h2 = new Headers(h1)
h2.set('b', '1')
const h2Raw = h2.raw()
const h3 = new Headers(h2)
h3.append('a', '2')
const h3Raw = h3.raw()
assert.strictEqual(h1Raw.a.includes('1'), true)
assert.strictEqual(h1Raw.a.includes('2'), false)
assert.strictEqual(h2Raw.a.includes('1'), true)
assert.strictEqual(h2Raw.a.includes('2'), false)
assert.strictEqual(h2Raw.b.includes('1'), true)
assert.strictEqual(h3Raw.a.includes('1'), true)
assert.strictEqual(h3Raw.a.includes('2'), true)
assert.strictEqual(h3Raw.b.includes('1'), true)
})
it('should accept headers as an iterable of tuples', () => {
let headers
headers = new Headers([
['a', '1'],
['b', '2'],
['a', '3']
])
assert.strictEqual(headers.get('a'), '1, 3')
assert.strictEqual(headers.get('b'), '2')
headers = new Headers([
new Set(['a', '1']),
['b', '2'],
new Map([['a', null], ['3', null]]).keys()
])
assert.strictEqual(headers.get('a'), '1, 3')
assert.strictEqual(headers.get('b'), '2')
headers = new Headers(new Map([
['a', '1'],
['b', '2']
]))
assert.strictEqual(headers.get('a'), '1')
assert.strictEqual(headers.get('b'), '2')
})
it('should throw a TypeError if non-tuple exists in a headers initializer', () => {
assert.throws(() => new Headers([['b', '2', 'huh?']]), TypeError)
assert.throws(() => new Headers(['b2']), TypeError)
assert.throws(() => new Headers('b2'), TypeError)
assert.throws(() => new Headers({ [Symbol.iterator]: 42 }), TypeError)
})
it.skip('should use a custom inspect function', () => {
const headers = new Headers([
['Host', 'thehost'],
['Host', 'notthehost'],
['a', '1'],
['b', '2'],
['a', '3']
])
assert.strictEqual(format(headers), "{ a: [ '1', '3' ], b: '2', host: 'thehost' }")
})
})
================================================
FILE: test/node-fetch/main.js
================================================
'use strict'
// Test tools
const assert = require('node:assert')
const { describe, it, before, beforeEach, after } = require('node:test')
const { setTimeout: delay } = require('node:timers/promises')
const zlib = require('node:zlib')
const stream = require('node:stream')
const vm = require('node:vm')
const crypto = require('node:crypto')
const {
fetch,
Headers,
Request,
FormData,
Response,
setGlobalDispatcher,
Agent
} = require('../../index.js')
const HeadersOrig = require('../../lib/web/fetch/headers.js').Headers
const ResponseOrig = require('../../lib/web/fetch/response.js').Response
const RequestOrig = require('../../lib/web/fetch/request.js').Request
const TestServer = require('./utils/server.js')
const { createServer } = require('node:http')
const { default: tspl } = require('@matteo.collina/tspl')
const {
Uint8Array: VMUint8Array
} = vm.runInNewContext('this')
describe('node-fetch', () => {
const local = new TestServer()
let base
before(async () => {
await local.start()
setGlobalDispatcher(new Agent({
connect: {
rejectUnauthorized: false
}
}))
base = `http://${local.hostname}:${local.port}/`
})
after(async () => {
return local.stop()
})
it('should return a promise', () => {
const url = `${base}hello`
const p = fetch(url)
assert.ok(p instanceof Promise)
assert.strictEqual(typeof p.then, 'function')
})
it('should expose Headers, Response and Request constructors', () => {
assert.strictEqual(Headers, HeadersOrig)
assert.strictEqual(Response, ResponseOrig)
assert.strictEqual(Request, RequestOrig)
})
it('should support proper toString output for Headers, Response and Request objects', () => {
assert.strictEqual(new Headers().toString(), '[object Headers]')
assert.strictEqual(new Response().toString(), '[object Response]')
assert.strictEqual(new Request(base).toString(), '[object Request]')
})
// TODO Should we reflect the input?
it('should reject with error if url is protocol relative', () => {
const url = '//example.com/'
return assert.rejects(fetch(url), new TypeError('Failed to parse URL from //example.com/'))
})
it('should reject with error if url is relative path', () => {
const url = '/some/path'
return assert.rejects(fetch(url), new TypeError('Failed to parse URL from /some/path'))
})
// TODO: This seems odd
it('should reject with error if protocol is unsupported', () => {
const url = 'ftp://example.com/'
return assert.rejects(fetch(url), new TypeError('fetch failed'))
})
it('should reject with error on network failure', { timeout: 5000 }, function () {
const url = 'http://localhost:50000/'
return assert.rejects(fetch(url), new TypeError('fetch failed'))
})
it('should resolve into response', () => {
const url = `${base}hello`
return fetch(url).then(res => {
assert.ok(res instanceof Response)
assert.ok(res.headers instanceof Headers)
assert.ok(res.body instanceof ReadableStream)
assert.strictEqual(res.bodyUsed, false)
assert.strictEqual(res.url, url)
assert.strictEqual(res.ok, true)
assert.strictEqual(res.status, 200)
assert.strictEqual(res.statusText, 'OK')
})
})
it('Response.redirect should resolve into response', () => {
const res = Response.redirect('http://localhost')
assert.ok(res instanceof Response)
assert.ok(res.headers instanceof Headers)
assert.strictEqual(res.headers.get('location'), 'http://localhost/')
assert.strictEqual(res.status, 302)
})
it('Response.redirect /w invalid url should fail', () => {
assert.throws(() => {
Response.redirect('localhost')
})
})
it('Response.redirect /w invalid status should fail', () => {
assert.throws(() => {
Response.redirect('http://localhost', 200)
})
})
it('should accept plain text response', () => {
const url = `${base}plain`
return fetch(url).then(res => {
assert.strictEqual(res.headers.get('content-type'), 'text/plain')
return res.text().then(result => {
assert.strictEqual(res.bodyUsed, true)
assert.strictEqual(typeof result, 'string')
assert.strictEqual(result, 'text')
})
})
})
it('should accept html response (like plain text)', () => {
const url = `${base}html`
return fetch(url).then(res => {
assert.strictEqual(res.headers.get('content-type'), 'text/html')
return res.text().then(result => {
assert.strictEqual(res.bodyUsed, true)
assert.strictEqual(typeof result, 'string')
assert.strictEqual(result, '')
})
})
})
it('should accept json response', () => {
const url = `${base}json`
return fetch(url).then(res => {
assert.strictEqual(res.headers.get('content-type'), 'application/json')
return res.json().then(result => {
assert.strictEqual(res.bodyUsed, true)
assert.strictEqual(typeof result, 'object')
assert.deepStrictEqual(result, { name: 'value' })
})
})
})
it('should send request with custom headers', () => {
const url = `${base}inspect`
const options = {
headers: { 'x-custom-header': 'abc' }
}
return fetch(url, options).then(res => {
return res.json()
}).then(res => {
assert.strictEqual(res.headers['x-custom-header'], 'abc')
})
})
it('should send request with custom headers array', () => {
const url = `${base}inspect`
const options = {
headers: { 'x-custom-header': ['abc'] }
}
return fetch(url, options).then(res => {
return res.json()
}).then(res => {
assert.strictEqual(res.headers['x-custom-header'], 'abc')
})
})
it('should send request with multi-valued headers', () => {
const url = `${base}inspect`
const options = {
headers: { 'x-custom-header': ['abc', '123'] }
}
return fetch(url, options).then(res => {
return res.json()
}).then(res => {
assert.strictEqual(res.headers['x-custom-header'], 'abc,123')
})
})
it('should accept headers instance', () => {
const url = `${base}inspect`
const options = {
headers: new Headers({ 'x-custom-header': 'abc' })
}
return fetch(url, options).then(res => {
return res.json()
}).then(res => {
assert.strictEqual(res.headers['x-custom-header'], 'abc')
})
})
it('should follow redirect code 301', () => {
const url = `${base}redirect/301`
return fetch(url).then(res => {
assert.strictEqual(res.url, `${base}inspect`)
assert.strictEqual(res.status, 200)
assert.strictEqual(res.ok, true)
})
})
it('should follow redirect code 302', () => {
const url = `${base}redirect/302`
return fetch(url).then(res => {
assert.strictEqual(res.url, `${base}inspect`)
assert.strictEqual(res.status, 200)
})
})
it('should follow redirect code 303', () => {
const url = `${base}redirect/303`
return fetch(url).then(res => {
assert.strictEqual(res.url, `${base}inspect`)
assert.strictEqual(res.status, 200)
})
})
it('should follow redirect code 307', () => {
const url = `${base}redirect/307`
return fetch(url).then(res => {
assert.strictEqual(res.url, `${base}inspect`)
assert.strictEqual(res.status, 200)
})
})
it('should follow redirect code 308', () => {
const url = `${base}redirect/308`
return fetch(url).then(res => {
assert.strictEqual(res.url, `${base}inspect`)
assert.strictEqual(res.status, 200)
})
})
it('should follow redirect chain', () => {
const url = `${base}redirect/chain`
return fetch(url).then(res => {
assert.strictEqual(res.url, `${base}inspect`)
assert.strictEqual(res.status, 200)
})
})
it('should follow POST request redirect code 301 with GET', () => {
const url = `${base}redirect/301`
const options = {
method: 'POST',
body: 'a=1'
}
return fetch(url, options).then(res => {
assert.strictEqual(res.url, `${base}inspect`)
assert.strictEqual(res.status, 200)
return res.json().then(result => {
assert.strictEqual(result.method, 'GET')
assert.strictEqual(result.body, '')
})
})
})
it('should follow PATCH request redirect code 301 with PATCH', () => {
const url = `${base}redirect/301`
const options = {
method: 'PATCH',
body: 'a=1'
}
return fetch(url, options).then(res => {
assert.strictEqual(res.url, `${base}inspect`)
assert.strictEqual(res.status, 200)
return res.json().then(res => {
assert.strictEqual(res.method, 'PATCH')
assert.strictEqual(res.body, 'a=1')
})
})
})
it('should follow POST request redirect code 302 with GET', () => {
const url = `${base}redirect/302`
const options = {
method: 'POST',
body: 'a=1'
}
return fetch(url, options).then(res => {
assert.strictEqual(res.url, `${base}inspect`)
assert.strictEqual(res.status, 200)
return res.json().then(result => {
assert.strictEqual(result.method, 'GET')
assert.strictEqual(result.body, '')
})
})
})
it('should follow PATCH request redirect code 302 with PATCH', () => {
const url = `${base}redirect/302`
const options = {
method: 'PATCH',
body: 'a=1'
}
return fetch(url, options).then(res => {
assert.strictEqual(res.url, `${base}inspect`)
assert.strictEqual(res.status, 200)
return res.json().then(res => {
assert.strictEqual(res.method, 'PATCH')
assert.strictEqual(res.body, 'a=1')
})
})
})
it('should follow redirect code 303 with GET', () => {
const url = `${base}redirect/303`
const options = {
method: 'PUT',
body: 'a=1'
}
return fetch(url, options).then(res => {
assert.strictEqual(res.url, `${base}inspect`)
assert.strictEqual(res.status, 200)
return res.json().then(result => {
assert.strictEqual(result.method, 'GET')
assert.strictEqual(result.body, '')
})
})
})
it('should follow PATCH request redirect code 307 with PATCH', () => {
const url = `${base}redirect/307`
const options = {
method: 'PATCH',
body: 'a=1'
}
return fetch(url, options).then(res => {
assert.strictEqual(res.url, `${base}inspect`)
assert.strictEqual(res.status, 200)
return res.json().then(result => {
assert.strictEqual(result.method, 'PATCH')
assert.strictEqual(result.body, 'a=1')
})
})
})
it('should not follow non-GET redirect if body is a readable stream', () => {
const url = `${base}redirect/307`
const options = {
method: 'PATCH',
body: stream.Readable.from('tada')
}
return assert.rejects(fetch(url, options), new TypeError('RequestInit: duplex option is required when sending a body.'))
})
it('should obey maximum redirect, reject case', () => {
const url = `${base}redirect/chain/20`
return assert.rejects(fetch(url), new TypeError('fetch failed'))
})
it('should obey redirect chain, resolve case', () => {
const url = `${base}redirect/chain/19`
return fetch(url).then(res => {
assert.strictEqual(res.url, `${base}inspect`)
assert.strictEqual(res.status, 200)
})
})
it('should support redirect mode, error flag', () => {
const url = `${base}redirect/301`
const options = {
redirect: 'error'
}
return assert.rejects(fetch(url, options), new TypeError('fetch failed'))
})
it('should support redirect mode, manual flag when there is no redirect', () => {
const url = `${base}hello`
const options = {
redirect: 'manual'
}
return fetch(url, options).then(res => {
assert.strictEqual(res.url, url)
assert.strictEqual(res.status, 200)
assert.strictEqual(res.headers.get('location'), null)
})
})
it('should follow redirect code 301 and keep existing headers', () => {
const url = `${base}redirect/301`
const options = {
headers: new Headers({ 'x-custom-header': 'abc' })
}
return fetch(url, options).then(res => {
assert.strictEqual(res.url, `${base}inspect`)
return res.json()
}).then(res => {
assert.strictEqual(res.headers['x-custom-header'], 'abc')
})
})
it('should treat broken redirect as ordinary response (follow)', () => {
const url = `${base}redirect/no-location`
return fetch(url).then(res => {
assert.strictEqual(res.url, url)
assert.strictEqual(res.status, 301)
assert.strictEqual(res.headers.get('location'), null)
})
})
it('should treat broken redirect as ordinary response (manual)', () => {
const url = `${base}redirect/no-location`
const options = {
redirect: 'manual'
}
return fetch(url, options).then(res => {
assert.strictEqual(res.url, url)
assert.strictEqual(res.status, 301)
assert.strictEqual(res.headers.get('location'), null)
})
})
it('should throw a TypeError on an invalid redirect option', () => {
const url = `${base}redirect/301`
const options = {
redirect: 'foobar'
}
return fetch(url, options).then(() => {
assert.fail()
}, error => {
assert.ok(error instanceof TypeError)
})
})
it('should set redirected property on response when redirect', () => {
const url = `${base}redirect/301`
return fetch(url).then(res => {
assert.strictEqual(res.redirected, true)
})
})
it('should not set redirected property on response without redirect', () => {
const url = `${base}hello`
return fetch(url).then(res => {
assert.strictEqual(res.redirected, false)
})
})
it('should handle client-error response', () => {
const url = `${base}error/400`
return fetch(url).then(res => {
assert.strictEqual(res.headers.get('content-type'), 'text/plain')
assert.strictEqual(res.status, 400)
assert.strictEqual(res.statusText, 'Bad Request')
assert.strictEqual(res.ok, false)
return res.text().then(result => {
assert.strictEqual(res.bodyUsed, true)
assert.strictEqual(typeof result, 'string')
assert.strictEqual(result, 'client error')
})
})
})
it('should handle server-error response', () => {
const url = `${base}error/500`
return fetch(url).then(res => {
assert.strictEqual(res.headers.get('content-type'), 'text/plain')
assert.strictEqual(res.status, 500)
assert.strictEqual(res.statusText, 'Internal Server Error')
assert.strictEqual(res.ok, false)
return res.text().then(result => {
assert.strictEqual(res.bodyUsed, true)
assert.strictEqual(typeof result, 'string')
assert.strictEqual(result, 'server error')
})
})
})
it('should handle network-error response', () => {
const url = `${base}error/reset`
return assert.rejects(fetch(url), new TypeError('fetch failed'))
})
it('should handle network-error partial response', () => {
const url = `${base}error/premature`
return fetch(url).then(res => {
assert.strictEqual(res.status, 200)
assert.strictEqual(res.ok, true)
return assert.rejects(() => res.text(), new TypeError('terminated'))
})
})
it('should handle network-error in chunked response async iterator', () => {
const url = `${base}error/premature/chunked`
return fetch(url).then(res => {
assert.strictEqual(res.status, 200)
assert.strictEqual(res.ok, true)
const read = async body => {
const chunks = []
for await (const chunk of body) {
chunks.push(chunk)
}
return chunks
}
return assert.rejects(read(res.body), new TypeError('terminated'))
})
})
it('should handle network-error in chunked response in consumeBody', () => {
const url = `${base}error/premature/chunked`
return fetch(url).then(res => {
assert.strictEqual(res.status, 200)
assert.strictEqual(res.ok, true)
return assert.rejects(res.text(), new TypeError('terminated'))
})
})
it('should handle DNS-error response', () => {
const url = 'http://domain.invalid'
return assert.rejects(fetch(url), new TypeError('fetch failed'))
})
// TODO: Should we pass through the error message?
it('should reject invalid json response', () => {
const url = `${base}error/json`
return fetch(url).then(res => {
assert.strictEqual(res.headers.get('content-type'), 'application/json')
return assert.rejects(res.json(), SyntaxError)
})
})
it('should handle response with no status text', () => {
const url = `${base}no-status-text`
return fetch(url).then(res => {
assert.strictEqual(res.statusText, '')
})
})
it('should handle no content response', () => {
const url = `${base}no-content`
return fetch(url).then(res => {
assert.strictEqual(res.status, 204)
assert.strictEqual(res.statusText, 'No Content')
assert.strictEqual(res.ok, true)
return res.text().then(result => {
assert.strictEqual(typeof result, 'string')
assert.strictEqual(result, '')
})
})
})
// TODO: Should we pass through the error message?
it('should reject when trying to parse no content response as json', () => {
const url = `${base}no-content`
return fetch(url).then(res => {
assert.strictEqual(res.status, 204)
assert.strictEqual(res.statusText, 'No Content')
assert.strictEqual(res.ok, true)
return assert.rejects(res.json(), new SyntaxError('Unexpected end of JSON input'))
})
})
it('should handle no content response with gzip encoding', () => {
const url = `${base}no-content/gzip`
return fetch(url).then(res => {
assert.strictEqual(res.status, 204)
assert.strictEqual(res.statusText, 'No Content')
assert.strictEqual(res.headers.get('content-encoding'), 'gzip')
assert.strictEqual(res.ok, true)
return res.text().then(result => {
assert.strictEqual(typeof result, 'string')
assert.strictEqual(result, '')
})
})
})
it('should handle not modified response', () => {
const url = `${base}not-modified`
return fetch(url).then(res => {
assert.strictEqual(res.status, 304)
assert.strictEqual(res.statusText, 'Not Modified')
assert.strictEqual(res.ok, false)
return res.text().then(result => {
assert.strictEqual(typeof result, 'string')
assert.strictEqual(result, '')
})
})
})
it('should handle not modified response with gzip encoding', () => {
const url = `${base}not-modified/gzip`
return fetch(url).then(res => {
assert.strictEqual(res.status, 304)
assert.strictEqual(res.statusText, 'Not Modified')
assert.strictEqual(res.headers.get('content-encoding'), 'gzip')
assert.strictEqual(res.ok, false)
return res.text().then(result => {
assert.strictEqual(typeof result, 'string')
assert.strictEqual(result, '')
})
})
})
it('should decompress gzip response', () => {
const url = `${base}gzip`
return fetch(url).then(res => {
assert.strictEqual(res.headers.get('content-type'), 'text/plain')
return res.text().then(result => {
assert.strictEqual(typeof result, 'string')
assert.strictEqual(result, 'hello world')
})
})
})
it('should decompress slightly invalid gzip response', async () => {
const url = `${base}gzip-truncated`
const res = await fetch(url)
assert.strictEqual(res.headers.get('content-type'), 'text/plain')
const result = await res.text()
assert.strictEqual(typeof result, 'string')
assert.strictEqual(result, 'hello world')
})
it('should decompress deflate response', () => {
const url = `${base}deflate`
return fetch(url).then(res => {
assert.strictEqual(res.headers.get('content-type'), 'text/plain')
return res.text().then(result => {
assert.strictEqual(typeof result, 'string')
assert.strictEqual(result, 'hello world')
})
})
})
it('should decompress deflate raw response from old apache server', () => {
const url = `${base}deflate-raw`
return fetch(url).then(res => {
assert.strictEqual(res.headers.get('content-type'), 'text/plain')
return res.text().then(result => {
assert.strictEqual(typeof result, 'string')
assert.strictEqual(result, 'hello world')
})
})
})
it('should decompress brotli response', function () {
if (typeof zlib.createBrotliDecompress !== 'function') {
this.skip()
}
const url = `${base}brotli`
return fetch(url).then(res => {
assert.strictEqual(res.headers.get('content-type'), 'text/plain')
return res.text().then(result => {
assert.strictEqual(typeof result, 'string')
assert.strictEqual(result, 'hello world')
})
})
})
it('should handle no content response with brotli encoding', function () {
if (typeof zlib.createBrotliDecompress !== 'function') {
this.skip()
}
const url = `${base}no-content/brotli`
return fetch(url).then(res => {
assert.strictEqual(res.status, 204)
assert.strictEqual(res.statusText, 'No Content')
assert.strictEqual(res.headers.get('content-encoding'), 'br')
assert.strictEqual(res.ok, true)
return res.text().then(result => {
assert.strictEqual(typeof result, 'string')
assert.strictEqual(result, '')
})
})
})
it('should skip decompression if unsupported', () => {
const url = `${base}sdch`
return fetch(url).then(res => {
assert.strictEqual(res.headers.get('content-type'), 'text/plain')
return res.text().then(result => {
assert.strictEqual(typeof result, 'string')
assert.strictEqual(result, 'fake sdch string')
})
})
})
it('should skip decompression if unsupported codings', () => {
const url = `${base}multiunsupported`
return fetch(url).then(res => {
assert.strictEqual(res.headers.get('content-type'), 'text/plain')
return res.text().then(result => {
assert.strictEqual(typeof result, 'string')
assert.strictEqual(result, 'multiunsupported')
})
})
})
it('should decompress multiple coding', () => {
const url = `${base}multisupported`
return fetch(url).then(res => {
assert.strictEqual(res.headers.get('content-type'), 'text/plain')
return res.text().then(result => {
assert.strictEqual(typeof result, 'string')
assert.strictEqual(result, 'hello world')
})
})
})
it('should reject if response compression is invalid', () => {
const url = `${base}invalid-content-encoding`
return fetch(url).then(res => {
assert.strictEqual(res.headers.get('content-type'), 'text/plain')
return assert.rejects(res.text(), new TypeError('terminated'))
})
})
it('should handle errors on the body stream even if it is not used', done => {
const url = `${base}invalid-content-encoding`
fetch(url)
.then(res => {
assert.strictEqual(res.status, 200)
})
.catch(() => { })
.then(new Promise((resolve) => {
// Wait a few ms to see if an uncaught error occurs
setTimeout(() => {
resolve()
}, 20)
}))
})
it('should collect handled errors on the body stream to reject if the body is used later', () => {
const url = `${base}invalid-content-encoding`
return fetch(url).then(delay(20)).then(res => {
assert.strictEqual(res.headers.get('content-type'), 'text/plain')
return assert.rejects(res.text(), new TypeError('terminated'))
})
})
it('should not overwrite existing accept-encoding header when auto decompression is true', () => {
const url = `${base}inspect`
const options = {
compress: true,
headers: {
'Accept-Encoding': 'gzip'
}
}
return fetch(url, options).then(res => res.json()).then(res => {
assert.strictEqual(res.headers['accept-encoding'], 'gzip')
})
})
describe('AbortController', () => {
let controller
beforeEach(() => {
controller = new AbortController()
})
it('should support request cancellation with signal', () => {
const fetches = [
fetch(
`${base}timeout`,
{
method: 'POST',
signal: controller.signal,
headers: {
'Content-Type': 'application/json',
body: JSON.stringify({ hello: 'world' })
}
}
)
]
controller.abort()
return Promise.all(fetches.map(async fetched => {
try {
await fetched
assert.fail('should have thrown')
} catch (error) {
assert.ok(error instanceof Error)
assert.strictEqual(error.name, 'AbortError')
}
}))
})
it('should support multiple request cancellation with signal', () => {
const fetches = [
fetch(`${base}timeout`, { signal: controller.signal }),
fetch(
`${base}timeout`,
{
method: 'POST',
signal: controller.signal,
headers: {
'Content-Type': 'application/json',
body: JSON.stringify({ hello: 'world' })
}
}
)
]
controller.abort()
return Promise.all(fetches.map(async fetched => {
try {
await fetched
assert.fail('should have thrown')
} catch (error) {
assert.ok(error instanceof Error)
assert.strictEqual(error.name, 'AbortError')
}
}))
})
it('should reject immediately if signal has already been aborted', async () => {
const url = `${base}timeout`
const options = {
signal: controller.signal
}
controller.abort()
const fetched = fetch(url, options)
try {
await fetched
assert.fail('should have thrown')
} catch (error) {
assert.ok(error instanceof Error)
assert.strictEqual(error.name, 'AbortError')
}
})
it('should allow redirects to be aborted', async () => {
const request = new Request(`${base}redirect/slow`, {
signal: controller.signal
})
setTimeout(() => {
controller.abort()
}, 20)
try {
await fetch(request)
assert.fail('should have thrown')
} catch (error) {
assert.ok(error instanceof Error)
assert.strictEqual(error.name, 'AbortError')
}
})
it('should allow redirected response body to be aborted', async () => {
const request = new Request(`${base}redirect/slow-stream`, {
signal: controller.signal
})
const fetched = fetch(request).then(res => {
assert.strictEqual(res.headers.get('content-type'), 'text/plain')
const result = res.text()
controller.abort()
return result
})
try {
await fetched
assert.fail('should have thrown')
} catch (error) {
assert.ok(error instanceof Error)
assert.strictEqual(error.name, 'AbortError')
}
})
it('should reject response body with AbortError when aborted before stream has been read completely', async () => {
const response = await fetch(
`${base}slow`,
{ signal: controller.signal }
)
const promise = response.text()
controller.abort()
try {
await promise
assert.fail('should have thrown')
} catch (error) {
assert.ok(error instanceof Error)
assert.strictEqual(error.name, 'AbortError')
}
})
it('should reject response body methods immediately with AbortError when aborted before stream is disturbed', async () => {
const response = await fetch(
`${base}slow`,
{ signal: controller.signal }
)
controller.abort()
const promise = response.text()
try {
await promise
assert.fail('should have thrown')
} catch (error) {
assert.ok(error instanceof Error)
assert.strictEqual(error.name, 'AbortError')
}
})
})
it('should throw a TypeError if a signal is not of type AbortSignal or EventTarget', () => {
return Promise.all([
assert.rejects(fetch(`${base}inspect`, { signal: {} }), new TypeError('RequestInit: Expected signal ("{}") to be an instance of AbortSignal.')),
assert.rejects(fetch(`${base}inspect`, { signal: '' }), new TypeError('RequestInit: Expected signal ("""") to be an instance of AbortSignal.')),
assert.rejects(fetch(`${base}inspect`, { signal: Object.create(null) }), new TypeError('RequestInit: Expected signal ("[Object: null prototype] {}") to be an instance of AbortSignal.'))
])
})
it('should gracefully handle a null signal', () => {
return fetch(`${base}hello`, { signal: null }).then(res => {
return assert.strictEqual(res.ok, true)
})
})
it('should allow setting User-Agent', () => {
const url = `${base}inspect`
const options = {
headers: {
'user-agent': 'faked'
}
}
return fetch(url, options).then(res => res.json()).then(res => {
assert.strictEqual(res.headers['user-agent'], 'faked')
})
})
it('should set default Accept header', () => {
const url = `${base}inspect`
fetch(url).then(res => res.json()).then(res => {
assert.strictEqual(res.headers.accept, '*/*')
})
})
it('should allow setting Accept header', () => {
const url = `${base}inspect`
const options = {
headers: {
accept: 'application/json'
}
}
return fetch(url, options).then(res => res.json()).then(res => {
assert.strictEqual(res.headers.accept, 'application/json')
})
})
it('should allow POST request', () => {
const url = `${base}inspect`
const options = {
method: 'POST'
}
return fetch(url, options).then(res => {
return res.json()
}).then(res => {
assert.strictEqual(res.method, 'POST')
assert.strictEqual(res.headers['transfer-encoding'], undefined)
assert.strictEqual(res.headers['content-type'], undefined)
assert.strictEqual(res.headers['content-length'], '0')
})
})
it('should allow POST request with string body', () => {
const url = `${base}inspect`
const options = {
method: 'POST',
body: 'a=1'
}
return fetch(url, options).then(res => {
return res.json()
}).then(res => {
assert.strictEqual(res.method, 'POST')
assert.strictEqual(res.body, 'a=1')
assert.strictEqual(res.headers['transfer-encoding'], undefined)
assert.strictEqual(res.headers['content-type'], 'text/plain;charset=UTF-8')
assert.strictEqual(res.headers['content-length'], '3')
})
})
it('should allow POST request with buffer body', () => {
const url = `${base}inspect`
const options = {
method: 'POST',
body: Buffer.from('a=1', 'utf-8')
}
return fetch(url, options).then(res => {
return res.json()
}).then(res => {
assert.strictEqual(res.method, 'POST')
assert.strictEqual(res.body, 'a=1')
assert.strictEqual(res.headers['transfer-encoding'], undefined)
assert.strictEqual(res.headers['content-type'], undefined)
assert.strictEqual(res.headers['content-length'], '3')
})
})
it('should allow POST request with ArrayBuffer body', () => {
const encoder = new TextEncoder()
const url = `${base}inspect`
const options = {
method: 'POST',
body: encoder.encode('Hello, world!\n').buffer
}
return fetch(url, options).then(res => res.json()).then(res => {
assert.strictEqual(res.method, 'POST')
assert.strictEqual(res.body, 'Hello, world!\n')
assert.strictEqual(res.headers['transfer-encoding'], undefined)
assert.strictEqual(res.headers['content-type'], undefined)
assert.strictEqual(res.headers['content-length'], '14')
})
})
it('should allow POST request with ArrayBuffer body from a VM context', () => {
const url = `${base}inspect`
const options = {
method: 'POST',
body: new VMUint8Array(Buffer.from('Hello, world!\n')).buffer
}
return fetch(url, options).then(res => res.json()).then(res => {
assert.strictEqual(res.method, 'POST')
assert.strictEqual(res.body, 'Hello, world!\n')
assert.strictEqual(res.headers['transfer-encoding'], undefined)
assert.strictEqual(res.headers['content-type'], undefined)
assert.strictEqual(res.headers['content-length'], '14')
})
})
it('should allow POST request with ArrayBufferView (Uint8Array) body', () => {
const encoder = new TextEncoder()
const url = `${base}inspect`
const options = {
method: 'POST',
body: encoder.encode('Hello, world!\n')
}
return fetch(url, options).then(res => res.json()).then(res => {
assert.strictEqual(res.method, 'POST')
assert.strictEqual(res.body, 'Hello, world!\n')
assert.strictEqual(res.headers['transfer-encoding'], undefined)
assert.strictEqual(res.headers['content-type'], undefined)
assert.strictEqual(res.headers['content-length'], '14')
})
})
it('should allow POST request with ArrayBufferView (BigUint64Array) body', () => {
const encoder = new TextEncoder()
const url = `${base}inspect`
const options = {
method: 'POST',
body: new BigUint64Array(encoder.encode('0123456789abcdef').buffer)
}
return fetch(url, options).then(res => res.json()).then(res => {
assert.strictEqual(res.method, 'POST')
assert.strictEqual(res.body, '0123456789abcdef')
assert.strictEqual(res.headers['transfer-encoding'], undefined)
assert.strictEqual(res.headers['content-type'], undefined)
assert.strictEqual(res.headers['content-length'], '16')
})
})
it('should allow POST request with ArrayBufferView (DataView) body', () => {
const encoder = new TextEncoder()
const url = `${base}inspect`
const options = {
method: 'POST',
body: new DataView(encoder.encode('Hello, world!\n').buffer)
}
return fetch(url, options).then(res => res.json()).then(res => {
assert.strictEqual(res.method, 'POST')
assert.strictEqual(res.body, 'Hello, world!\n')
assert.strictEqual(res.headers['transfer-encoding'], undefined)
assert.strictEqual(res.headers['content-type'], undefined)
assert.strictEqual(res.headers['content-length'], '14')
})
})
it('should allow POST request with ArrayBufferView (Uint8Array) body from a VM context', () => {
const url = `${base}inspect`
const options = {
method: 'POST',
body: new VMUint8Array(Buffer.from('Hello, world!\n'))
}
return fetch(url, options).then(res => res.json()).then(res => {
assert.strictEqual(res.method, 'POST')
assert.strictEqual(res.body, 'Hello, world!\n')
assert.strictEqual(res.headers['transfer-encoding'], undefined)
assert.strictEqual(res.headers['content-type'], undefined)
assert.strictEqual(res.headers['content-length'], '14')
})
})
it('should allow POST request with ArrayBufferView (Uint8Array, offset, length) body', () => {
const encoder = new TextEncoder()
const url = `${base}inspect`
const options = {
method: 'POST',
body: encoder.encode('Hello, world!\n').subarray(7, 13)
}
return fetch(url, options).then(res => res.json()).then(res => {
assert.strictEqual(res.method, 'POST')
assert.strictEqual(res.body, 'world!')
assert.strictEqual(res.headers['transfer-encoding'], undefined)
assert.strictEqual(res.headers['content-type'], undefined)
assert.strictEqual(res.headers['content-length'], '6')
})
})
it('should allow POST request with blob body without type', () => {
const url = `${base}inspect`
const options = {
method: 'POST',
body: new Blob(['a=1'])
}
return fetch(url, options).then(res => {
return res.json()
}).then(res => {
assert.strictEqual(res.method, 'POST')
assert.strictEqual(res.body, 'a=1')
assert.strictEqual(res.headers['transfer-encoding'], undefined)
// assert.strictEqual(res.headers['content-type'], undefined)
assert.strictEqual(res.headers['content-length'], '3')
})
})
it('should allow POST request with blob body with type', () => {
const url = `${base}inspect`
const options = {
method: 'POST',
body: new Blob(['a=1'], {
type: 'text/plain;charset=UTF-8'
})
}
return fetch(url, options).then(res => {
return res.json()
}).then(res => {
assert.strictEqual(res.method, 'POST')
assert.strictEqual(res.body, 'a=1')
assert.strictEqual(res.headers['transfer-encoding'], undefined)
assert.strictEqual(res.headers['content-type'], 'text/plain;charset=utf-8')
assert.strictEqual(res.headers['content-length'], '3')
})
})
it('should allow POST request with readable stream as body', () => {
const url = `${base}inspect`
const options = {
method: 'POST',
body: stream.Readable.from('a=1'),
duplex: 'half'
}
return fetch(url, options).then(res => {
return res.json()
}).then(res => {
assert.strictEqual(res.method, 'POST')
assert.strictEqual(res.body, 'a=1')
assert.strictEqual(res.headers['transfer-encoding'], 'chunked')
assert.strictEqual(res.headers['content-type'], undefined)
assert.strictEqual(res.headers['content-length'], undefined)
})
})
it('should allow POST request with object body', () => {
const url = `${base}inspect`
// Note that fetch simply calls tostring on an object
const options = {
method: 'POST',
body: { a: 1 }
}
return fetch(url, options).then(res => {
return res.json()
}).then(res => {
assert.strictEqual(res.method, 'POST')
assert.strictEqual(res.body, '[object Object]')
assert.strictEqual(res.headers['content-type'], 'text/plain;charset=UTF-8')
assert.strictEqual(res.headers['content-length'], '15')
})
})
it('should allow POST request with form-data as body', () => {
const form = new FormData()
form.append('a', '1')
const url = `${base}multipart`
const options = {
method: 'POST',
body: form
}
return fetch(url, options).then(res => {
return res.json()
}).then(res => {
assert.strictEqual(res.method, 'POST')
assert.ok(res.headers['content-type'].startsWith('multipart/form-data; boundary='))
assert.strictEqual(res.body, 'a=1')
})
})
it('constructing a Response with URLSearchParams as body should have a Content-Type', () => {
const parameters = new URLSearchParams()
const res = new Response(parameters)
res.headers.get('Content-Type')
assert.strictEqual(res.headers.get('Content-Type'), 'application/x-www-form-urlencoded;charset=UTF-8')
})
it('constructing a Request with URLSearchParams as body should have a Content-Type', () => {
const parameters = new URLSearchParams()
const request = new Request(base, { method: 'POST', body: parameters })
assert.strictEqual(request.headers.get('Content-Type'), 'application/x-www-form-urlencoded;charset=UTF-8')
})
it('Reading a body with URLSearchParams should echo back the result', () => {
const parameters = new URLSearchParams()
parameters.append('a', '1')
return new Response(parameters).text().then(text => {
assert.strictEqual(text, 'a=1')
})
})
// Body should be cloned...
it('constructing a Request/Response with URLSearchParams and mutating it should not affected body', () => {
const parameters = new URLSearchParams()
const request = new Request(`${base}inspect`, { method: 'POST', body: parameters })
parameters.append('a', '1')
return request.text().then(text => {
assert.strictEqual(text, '')
})
})
it('should allow POST request with URLSearchParams as body', () => {
const parameters = new URLSearchParams()
parameters.append('a', '1')
const url = `${base}inspect`
const options = {
method: 'POST',
body: parameters
}
return fetch(url, options).then(res => {
return res.json()
}).then(res => {
assert.strictEqual(res.method, 'POST')
assert.strictEqual(res.headers['content-type'], 'application/x-www-form-urlencoded;charset=UTF-8')
assert.strictEqual(res.headers['content-length'], '3')
assert.strictEqual(res.body, 'a=1')
})
})
it('should still recognize URLSearchParams when extended', () => {
class CustomSearchParameters extends URLSearchParams { }
const parameters = new CustomSearchParameters()
parameters.append('a', '1')
const url = `${base}inspect`
const options = {
method: 'POST',
body: parameters
}
return fetch(url, options).then(res => {
return res.json()
}).then(res => {
assert.strictEqual(res.method, 'POST')
assert.strictEqual(res.headers['content-type'], 'application/x-www-form-urlencoded;charset=UTF-8')
assert.strictEqual(res.headers['content-length'], '3')
assert.strictEqual(res.body, 'a=1')
})
})
it('should allow PUT request', () => {
const url = `${base}inspect`
const options = {
method: 'PUT',
body: 'a=1'
}
return fetch(url, options).then(res => {
return res.json()
}).then(res => {
assert.strictEqual(res.method, 'PUT')
assert.strictEqual(res.body, 'a=1')
})
})
it('should allow DELETE request', () => {
const url = `${base}inspect`
const options = {
method: 'DELETE'
}
return fetch(url, options).then(res => {
return res.json()
}).then(res => {
assert.strictEqual(res.method, 'DELETE')
})
})
it('should allow DELETE request with string body', () => {
const url = `${base}inspect`
const options = {
method: 'DELETE',
body: 'a=1'
}
return fetch(url, options).then(res => {
return res.json()
}).then(res => {
assert.strictEqual(res.method, 'DELETE')
assert.strictEqual(res.body, 'a=1')
assert.strictEqual(res.headers['transfer-encoding'], undefined)
assert.strictEqual(res.headers['content-length'], '3')
})
})
it('should allow PATCH request', () => {
const url = `${base}inspect`
const options = {
method: 'PATCH',
body: 'a=1'
}
return fetch(url, options).then(res => {
return res.json()
}).then(res => {
assert.strictEqual(res.method, 'PATCH')
assert.strictEqual(res.body, 'a=1')
})
})
it('should allow HEAD request', () => {
const url = `${base}hello`
const options = {
method: 'HEAD'
}
return fetch(url, options).then(res => {
assert.strictEqual(res.status, 200)
assert.strictEqual(res.statusText, 'OK')
assert.strictEqual(res.headers.get('content-type'), 'text/plain')
// assert.ok(res.body instanceof stream.Transform)
return res.text()
}).then(text => {
assert.strictEqual(text, '')
})
})
it('should allow HEAD request with content-encoding header', () => {
const url = `${base}error/404`
const options = {
method: 'HEAD'
}
return fetch(url, options).then(res => {
assert.strictEqual(res.status, 404)
assert.strictEqual(res.headers.get('content-encoding'), 'gzip')
return res.text()
}).then(text => {
assert.strictEqual(text, '')
})
})
it('should allow OPTIONS request', () => {
const url = `${base}options`
const options = {
method: 'OPTIONS'
}
return fetch(url, options).then(res => {
assert.strictEqual(res.status, 200)
assert.strictEqual(res.statusText, 'OK')
assert.strictEqual(res.headers.get('allow'), 'GET, HEAD, OPTIONS')
// assert.ok(res.body instanceof stream.Transform)
})
})
it('should reject decoding body twice', () => {
const url = `${base}plain`
return fetch(url).then(res => {
assert.strictEqual(res.headers.get('content-type'), 'text/plain')
return res.text().then(() => {
assert.strictEqual(res.bodyUsed, true)
return assert.rejects(res.text(), new TypeError('Body is unusable: Body has already been read'))
})
})
})
it('should allow cloning a json response and log it as text response', () => {
const url = `${base}json`
return fetch(url).then(res => {
const r1 = res.clone()
return Promise.all([res.json(), r1.text()]).then(results => {
assert.deepStrictEqual(results[0], { name: 'value' })
assert.strictEqual(results[1], '{"name":"value"}')
})
})
})
it('should allow cloning a json response, and then log it as text response', () => {
const url = `${base}json`
return fetch(url).then(res => {
const r1 = res.clone()
return res.json().then(result => {
assert.deepStrictEqual(result, { name: 'value' })
return r1.text().then(result => {
assert.strictEqual(result, '{"name":"value"}')
})
})
})
})
it('should allow cloning a json response, first log as text response, then return json object', () => {
const url = `${base}json`
return fetch(url).then(res => {
const r1 = res.clone()
return r1.text().then(result => {
assert.strictEqual(result, '{"name":"value"}')
return res.json().then(result => {
assert.deepStrictEqual(result, { name: 'value' })
})
})
})
})
it('should not allow cloning a response after its been used', () => {
const url = `${base}hello`
return fetch(url).then(res =>
res.text().then(() => {
assert.throws(() => {
res.clone()
}, new TypeError('Response.clone: Body has already been consumed.'))
})
)
})
/* global expect */
// TODO: fix test.
it.skip('should timeout on cloning response without consuming one of the streams when the second packet size is equal default highWaterMark', { timeout: 300 }, function () {
const url = local.mockState(res => {
// Observed behavior of TCP packets splitting:
// - response body size <= 65438 → single packet sent
// - response body size > 65438 → multiple packets sent
// Max TCP packet size is 64kB (http://stackoverflow.com/a/2614188/5763764),
// but first packet probably transfers more than the response body.
const firstPacketMaxSize = 65438
const secondPacketSize = 16 * 1024 // = defaultHighWaterMark
res.end(crypto.randomBytes(firstPacketMaxSize + secondPacketSize))
})
return expect(
fetch(url).then(res => res.clone().buffer())
).to.timeout
})
// TODO: fix test.
it.skip('should timeout on cloning response without consuming one of the streams when the second packet size is equal custom highWaterMark', { timeout: 300 }, function () {
const url = local.mockState(res => {
const firstPacketMaxSize = 65438
const secondPacketSize = 10
res.end(crypto.randomBytes(firstPacketMaxSize + secondPacketSize))
})
return expect(
fetch(url, { highWaterMark: 10 }).then(res => res.clone().buffer())
).to.timeout
})
// TODO: fix test.
it.skip('should not timeout on cloning response without consuming one of the streams when the second packet size is less than default highWaterMark', { timeout: 300 }, async function () {
const url = local.mockState(res => {
const firstPacketMaxSize = 65438
const secondPacketSize = 16 * 1024 // = defaultHighWaterMark
res.end(crypto.randomBytes(firstPacketMaxSize + secondPacketSize - 1))
})
return expect(
fetch(url).then(res => res.clone().buffer())
).not.to.timeout
})
// TODO: fix test.
it.skip('should not timeout on cloning response without consuming one of the streams when the second packet size is less than custom highWaterMark', { timeout: 300 }, function () {
const url = local.mockState(res => {
const firstPacketMaxSize = 65438
const secondPacketSize = 10
res.end(crypto.randomBytes(firstPacketMaxSize + secondPacketSize - 1))
})
return expect(
fetch(url, { highWaterMark: 10 }).then(res => res.clone().buffer())
).not.to.timeout
})
// TODO: fix test.
it.skip('should not timeout on cloning response without consuming one of the streams when the response size is double the custom large highWaterMark - 1', { timeout: 300 }, function () {
const url = local.mockState(res => {
res.end(crypto.randomBytes((2 * 512 * 1024) - 1))
})
return expect(
fetch(url, { highWaterMark: 512 * 1024 }).then(res => res.clone().buffer())
).not.to.timeout
})
// TODO: fix test.
it.skip('should allow get all responses of a header', () => {
const url = `${base}cookie`
return fetch(url).then(res => {
const expected = 'a=1, b=1'
assert.strictEqual(res.headers.get('set-cookie'), expected)
assert.strictEqual(res.headers.get('Set-Cookie'), expected)
})
})
it('should support fetch with Request instance', () => {
const url = `${base}hello`
const request = new Request(url)
return fetch(request).then(res => {
assert.strictEqual(res.url, url)
assert.strictEqual(res.ok, true)
assert.strictEqual(res.status, 200)
})
})
it('should support fetch with Node.js URL object', () => {
const url = `${base}hello`
const urlObject = new URL(url)
const request = new Request(urlObject)
return fetch(request).then(res => {
assert.strictEqual(res.url, url)
assert.strictEqual(res.ok, true)
assert.strictEqual(res.status, 200)
})
})
it('should support fetch with WHATWG URL object', () => {
const url = `${base}hello`
const urlObject = new URL(url)
const request = new Request(urlObject)
return fetch(request).then(res => {
assert.strictEqual(res.url, url)
assert.strictEqual(res.ok, true)
assert.strictEqual(res.status, 200)
})
})
it('if params are given, do not modify anything', () => {
const url = `${base}question?a=1`
const urlObject = new URL(url)
const request = new Request(urlObject)
return fetch(request).then(res => {
assert.strictEqual(res.url, url)
assert.strictEqual(res.ok, true)
assert.strictEqual(res.status, 200)
})
})
it('should support reading blob as text', () => {
return new Response('hello')
.blob()
.then(blob => blob.text())
.then(body => {
assert.strictEqual(body, 'hello')
})
})
it('should support reading blob as arrayBuffer', () => {
return new Response('hello')
.blob()
.then(blob => blob.arrayBuffer())
.then(ab => {
const string = String.fromCharCode.apply(null, new Uint8Array(ab))
assert.strictEqual(string, 'hello')
})
})
it('should support blob round-trip', () => {
const url = `${base}hello`
let length
let type
return fetch(url).then(res => res.blob()).then(async blob => {
const url = `${base}inspect`
length = blob.size
type = blob.type
return fetch(url, {
method: 'POST',
body: blob
})
}).then(res => res.json()).then(({ body, headers }) => {
assert.strictEqual(body, 'world')
assert.strictEqual(headers['content-type'], type)
assert.strictEqual(headers['content-length'], String(length))
})
})
it('should support overwrite Request instance', () => {
const url = `${base}inspect`
const request = new Request(url, {
method: 'POST',
headers: {
a: '1'
}
})
return fetch(request, {
method: 'GET',
headers: {
a: '2'
}
}).then(res => {
return res.json()
}).then(body => {
assert.strictEqual(body.method, 'GET')
assert.strictEqual(body.headers.a, '2')
})
})
it('should support http request', { timeout: 5000 }, async function (t) {
t = tspl(t, { plan: 2 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.end()
})
after(() => server.close())
server.listen(0, () => {
const url = `http://localhost:${server.address().port}`
const options = {
method: 'HEAD'
}
fetch(url, options).then(res => {
t.strictEqual(res.status, 200)
t.strictEqual(res.ok, true)
})
})
await t.completed
})
it('should encode URLs as UTF-8', async () => {
const url = `${base}möbius`
const res = await fetch(url)
assert.strictEqual(res.url, `${base}m%C3%B6bius`)
})
it('should allow manual redirect handling', { timeout: 5000 }, function () {
const url = `${base}redirect/302`
const options = {
redirect: 'manual'
}
return fetch(url, options).then(res => {
assert.strictEqual(res.status, 302)
assert.strictEqual(res.url, url)
assert.strictEqual(res.type, 'basic')
assert.strictEqual(res.headers.get('Location'), '/inspect')
assert.strictEqual(res.ok, false)
})
})
})
================================================
FILE: test/node-fetch/mock.js
================================================
'use strict'
// Test tools
const assert = require('node:assert')
const { describe, it } = require('node:test')
const {
fetch,
MockAgent,
setGlobalDispatcher,
Headers
} = require('../../index.js')
describe('node-fetch with MockAgent', () => {
it('should match the url', async () => {
const mockAgent = new MockAgent()
setGlobalDispatcher(mockAgent)
const mockPool = mockAgent.get('http://localhost:3000')
mockPool
.intercept({
path: '/test',
method: 'GET'
})
.reply(200, { success: true })
.persist()
const res = await fetch('http://localhost:3000/test', {
method: 'GET'
})
assert.strictEqual(res.status, 200)
assert.deepStrictEqual(await res.json(), { success: true })
})
it('should match the body', async () => {
const mockAgent = new MockAgent()
setGlobalDispatcher(mockAgent)
const mockPool = mockAgent.get('http://localhost:3000')
mockPool
.intercept({
path: '/test',
method: 'POST',
body: (value) => {
return value === 'request body'
}
})
.reply(200, { success: true })
.persist()
const res = await fetch('http://localhost:3000/test', {
method: 'POST',
body: 'request body'
})
assert.strictEqual(res.status, 200)
assert.deepStrictEqual(await res.json(), { success: true })
})
it('should match the headers', async () => {
const mockAgent = new MockAgent()
setGlobalDispatcher(mockAgent)
const mockPool = mockAgent.get('http://localhost:3000')
mockPool
.intercept({
path: '/test',
method: 'GET',
headers: (h) => {
return h['user-agent'] === 'undici'
}
})
.reply(200, { success: true })
.persist()
const res = await fetch('http://localhost:3000/test', {
method: 'GET',
headers: new Headers({ 'User-Agent': 'undici' })
})
assert.strictEqual(res.status, 200)
assert.deepStrictEqual(await res.json(), { success: true })
})
it('should match the headers with a matching function', async () => {
const mockAgent = new MockAgent()
setGlobalDispatcher(mockAgent)
const mockPool = mockAgent.get('http://localhost:3000')
mockPool
.intercept({
path: '/test',
method: 'GET',
headers (headers) {
assert.strictEqual(typeof headers, 'object')
assert.strictEqual(headers['user-agent'], 'undici')
return true
}
})
.reply(200, { success: true })
.persist()
const res = await fetch('http://localhost:3000/test', {
method: 'GET',
headers: new Headers({ 'User-Agent': 'undici' })
})
assert.strictEqual(res.status, 200)
assert.deepStrictEqual(await res.json(), { success: true })
})
})
================================================
FILE: test/node-fetch/request.js
================================================
'use strict'
const assert = require('node:assert')
const { describe, it, before, after } = require('node:test')
const stream = require('node:stream')
const http = require('node:http')
const { Request, FormData } = require('../../index.js')
const TestServer = require('./utils/server.js')
describe('Request', () => {
const local = new TestServer()
let base
before(async () => {
await local.start()
base = `http://${local.hostname}:${local.port}/`
})
after(async () => {
return local.stop()
})
it('should have attributes conforming to Web IDL', () => {
const request = new Request('http://github.com/')
const enumerableProperties = []
for (const property in request) {
enumerableProperties.push(property)
}
for (const toCheck of [
'body',
'bodyUsed',
'arrayBuffer',
'blob',
'json',
'text',
'method',
'url',
'headers',
'redirect',
'clone',
'signal'
]) {
assert.ok(enumerableProperties.includes(toCheck))
}
for (const toCheck of [
'body', 'bodyUsed', 'method', 'url', 'headers', 'redirect', 'signal'
]) {
assert.throws(() => {
request[toCheck] = 'abc'
}, new TypeError(`Cannot set property ${toCheck} of # which has only a getter`))
}
})
it.skip('should support wrapping Request instance', () => {
const url = `${base}hello`
const form = new FormData()
form.append('a', '1')
const { signal } = new AbortController()
const r1 = new Request(url, {
method: 'POST',
follow: 1,
body: form,
signal
})
const r2 = new Request(r1, {
follow: 2
})
assert.strictEqual(r2.url, url)
assert.strictEqual(r2.method, 'POST')
assert.strictEqual(r2.signal[Symbol.toStringTag], 'AbortSignal')
// Note that we didn't clone the body
assert.strictEqual(r2.body, form)
assert.strictEqual(r1.follow, 1)
assert.strictEqual(r2.follow, 2)
assert.strictEqual(r1.counter, 0)
assert.strictEqual(r2.counter, 0)
})
it.skip('should override signal on derived Request instances', () => {
const parentAbortController = new AbortController()
const derivedAbortController = new AbortController()
const parentRequest = new Request(`${base}hello`, {
signal: parentAbortController.signal
})
const derivedRequest = new Request(parentRequest, {
signal: derivedAbortController.signal
})
assert.strictEqual(parentRequest.signal, parentAbortController.signal)
assert.strictEqual(derivedRequest.signal, derivedAbortController.signal)
})
it.skip('should allow removing signal on derived Request instances', () => {
const parentAbortController = new AbortController()
const parentRequest = new Request(`${base}hello`, {
signal: parentAbortController.signal
})
const derivedRequest = new Request(parentRequest, {
signal: null
})
assert.strictEqual(parentRequest.signal, parentAbortController.signal)
assert.strictEqual(derivedRequest.signal, null)
})
it('should throw error with GET/HEAD requests with body', () => {
assert.throws(() => new Request(base, { body: '' }), new TypeError('Request with GET/HEAD method cannot have body.'))
assert.throws(() => new Request(base, { body: 'a' }), new TypeError('Request with GET/HEAD method cannot have body.'))
assert.throws(() => new Request(base, { body: '', method: 'HEAD' }), new TypeError('Request with GET/HEAD method cannot have body.'))
assert.throws(() => new Request(base, { body: 'a', method: 'HEAD' }), new TypeError('Request with GET/HEAD method cannot have body.'))
assert.throws(() => new Request(base, { body: '', method: 'get' }), new TypeError('Request with GET/HEAD method cannot have body.'))
assert.throws(() => new Request(base, { body: 'a', method: 'get' }), new TypeError('Request with GET/HEAD method cannot have body.'))
assert.throws(() => new Request(base, { body: '', method: 'head' }), new TypeError('Request with GET/HEAD method cannot have body.'))
assert.throws(() => new Request(base, { body: 'a', method: 'head' }), new TypeError('Request with GET/HEAD method cannot have body.'))
})
it('should default to null as body', () => {
const request = new Request(base)
assert.strictEqual(request.body, null)
return request.text().then(result => assert.strictEqual(result, ''))
})
it('should support parsing headers', () => {
const url = base
const request = new Request(url, {
headers: {
a: '1'
}
})
assert.strictEqual(request.url, url)
assert.strictEqual(request.headers.get('a'), '1')
})
it('should support arrayBuffer() method', () => {
const url = base
const request = new Request(url, {
method: 'POST',
body: 'a=1'
})
assert.strictEqual(request.url, url)
return request.arrayBuffer().then(result => {
assert.ok(result instanceof ArrayBuffer)
const string = String.fromCharCode.apply(null, new Uint8Array(result))
assert.strictEqual(string, 'a=1')
})
})
it('should support text() method', () => {
const url = base
const request = new Request(url, {
method: 'POST',
body: 'a=1'
})
assert.strictEqual(request.url, url)
return request.text().then(result => {
assert.strictEqual(result, 'a=1')
})
})
it('should support json() method', () => {
const url = base
const request = new Request(url, {
method: 'POST',
body: '{"a":1}'
})
assert.strictEqual(request.url, url)
return request.json().then(result => {
assert.strictEqual(result.a, 1)
})
})
it('should support blob() method', () => {
const url = base
const request = new Request(url, {
method: 'POST',
body: Buffer.from('a=1')
})
assert.strictEqual(request.url, url)
return request.blob().then(result => {
assert.ok(result instanceof Blob)
assert.strictEqual(result.size, 3)
assert.strictEqual(result.type, '')
})
})
it('should support clone() method', () => {
const url = base
const body = stream.Readable.from('a=1')
const agent = new http.Agent()
const { signal } = new AbortController()
const request = new Request(url, {
body,
method: 'POST',
redirect: 'manual',
headers: {
b: '2'
},
follow: 3,
compress: false,
agent,
signal,
duplex: 'half'
})
const cl = request.clone()
assert.strictEqual(cl.url, url)
assert.strictEqual(cl.method, 'POST')
assert.strictEqual(cl.redirect, 'manual')
assert.strictEqual(cl.headers.get('b'), '2')
assert.strictEqual(cl.method, 'POST')
// Clone body shouldn't be the same body
assert.notDeepEqual(cl.body, body)
return Promise.all([cl.text(), request.text()]).then(results => {
assert.strictEqual(results[0], 'a=1')
assert.strictEqual(results[1], 'a=1')
})
})
it('should support ArrayBuffer as body', () => {
const encoder = new TextEncoder()
const body = encoder.encode('a=12345678901234').buffer
const request = new Request(base, {
method: 'POST',
body
})
new Uint8Array(body)[0] = 0
return request.text().then(result => {
assert.strictEqual(result, 'a=12345678901234')
})
})
it('should support Uint8Array as body', () => {
const encoder = new TextEncoder()
const fullbuffer = encoder.encode('a=12345678901234').buffer
const body = new Uint8Array(fullbuffer, 2, 9)
const request = new Request(base, {
method: 'POST',
body
})
body[0] = 0
return request.text().then(result => {
assert.strictEqual(result, '123456789')
})
})
it('should support BigUint64Array as body', () => {
const encoder = new TextEncoder()
const fullbuffer = encoder.encode('a=12345678901234').buffer
const body = new BigUint64Array(fullbuffer, 8, 1)
const request = new Request(base, {
method: 'POST',
body
})
body[0] = 0n
return request.text().then(result => {
assert.strictEqual(result, '78901234')
})
})
it('should support DataView as body', () => {
const encoder = new TextEncoder()
const fullbuffer = encoder.encode('a=12345678901234').buffer
const body = new Uint8Array(fullbuffer, 2, 9)
const request = new Request(base, {
method: 'POST',
body
})
body[0] = 0
return request.text().then(result => {
assert.strictEqual(result, '123456789')
})
})
})
================================================
FILE: test/node-fetch/response.js
================================================
'use strict'
const assert = require('node:assert')
const { describe, it, before, after } = require('node:test')
const stream = require('node:stream')
const { Response } = require('../../index.js')
const TestServer = require('./utils/server.js')
describe('Response', () => {
const local = new TestServer()
before(async () => {
await local.start()
})
after(async () => {
return local.stop()
})
it('should have attributes conforming to Web IDL', () => {
const res = new Response()
const enumerableProperties = []
for (const property in res) {
enumerableProperties.push(property)
}
for (const toCheck of [
'body',
'bodyUsed',
'arrayBuffer',
'blob',
'json',
'text',
'type',
'url',
'status',
'ok',
'redirected',
'statusText',
'headers',
'clone'
]) {
assert.ok(enumerableProperties.includes(toCheck))
}
for (const toCheck of [
'body',
'bodyUsed',
'type',
'url',
'status',
'ok',
'redirected',
'statusText',
'headers'
]) {
assert.throws(() => {
res[toCheck] = 'abc'
}, new TypeError(`Cannot set property ${toCheck} of # which has only a getter`))
}
})
it('should support empty options', async () => {
const res = new Response(stream.Readable.from('a=1'))
const result = await res.text()
assert.strictEqual(result, 'a=1')
})
it('should support parsing headers', () => {
const res = new Response(null, {
headers: {
a: '1'
}
})
assert.strictEqual(res.headers.get('a'), '1')
})
it('should support text() method', async () => {
const res = new Response('a=1')
const result = await res.text()
assert.strictEqual(result, 'a=1')
})
it('should support json() method', async () => {
const res = new Response('{"a":1}')
const result = await res.json()
assert.deepStrictEqual(result, { a: 1 })
})
if (Blob) {
it('should support blob() method', async () => {
const res = new Response('a=1', {
method: 'POST',
headers: {
'Content-Type': 'text/plain'
}
})
const result = await res.blob()
assert.ok(result instanceof Blob)
assert.strictEqual(result.size, 3)
assert.strictEqual(result.type, 'text/plain')
})
}
it('should support clone() method', () => {
const body = stream.Readable.from('a=1')
const res = new Response(body, {
headers: {
a: '1'
},
status: 346,
statusText: 'production'
})
const cl = res.clone()
assert.strictEqual(cl.headers.get('a'), '1')
assert.strictEqual(cl.type, 'default')
assert.strictEqual(cl.status, 346)
assert.strictEqual(cl.statusText, 'production')
assert.strictEqual(cl.ok, false)
// Clone body shouldn't be the same body
assert.notStrictEqual(cl.body, body)
return Promise.all([cl.text(), res.text()]).then(results => {
assert.strictEqual(results[0], 'a=1')
assert.strictEqual(results[1], 'a=1')
})
})
it('should support stream as body', async () => {
const body = stream.Readable.from('a=1')
const res = new Response(body)
const result = await res.text()
assert.strictEqual(result, 'a=1')
})
it('should support string as body', async () => {
const res = new Response('a=1')
const result = await res.text()
assert.strictEqual(result, 'a=1')
})
it('should support buffer as body', async () => {
const res = new Response(Buffer.from('a=1'))
const result = await res.text()
assert.strictEqual(result, 'a=1')
})
it('should support ArrayBuffer as body', async () => {
const encoder = new TextEncoder()
const fullbuffer = encoder.encode('a=12345678901234').buffer
const res = new Response(fullbuffer)
new Uint8Array(fullbuffer)[0] = 0
const result = await res.text()
assert.strictEqual(result, 'a=12345678901234')
})
it('should support blob as body', async () => {
const res = new Response(new Blob(['a=1']))
const result = await res.text()
assert.strictEqual(result, 'a=1')
})
it('should support Uint8Array as body', async () => {
const encoder = new TextEncoder()
const fullbuffer = encoder.encode('a=12345678901234').buffer
const body = new Uint8Array(fullbuffer, 2, 9)
const res = new Response(body)
body[0] = 0
const result = await res.text()
assert.strictEqual(result, '123456789')
})
it('should support BigUint64Array as body', async () => {
const encoder = new TextEncoder()
const fullbuffer = encoder.encode('a=12345678901234').buffer
const body = new BigUint64Array(fullbuffer, 8, 1)
const res = new Response(body)
body[0] = 0n
const result = await res.text()
assert.strictEqual(result, '78901234')
})
it('should support DataView as body', async () => {
const encoder = new TextEncoder()
const fullbuffer = encoder.encode('a=12345678901234').buffer
const body = new Uint8Array(fullbuffer, 2, 9)
const res = new Response(body)
body[0] = 0
const result = await res.text()
assert.strictEqual(result, '123456789')
})
it('should default to null as body', () => {
const res = new Response()
assert.strictEqual(res.body, null)
return res.text().then(result => assert.strictEqual(result, ''))
})
it('should default to 200 as status code', () => {
const res = new Response(null)
assert.strictEqual(res.status, 200)
})
it('should default to empty string as url', () => {
const res = new Response()
assert.strictEqual(res.url, '')
})
it('should support error() static method', () => {
const res = Response.error()
assert.ok(res instanceof Response)
assert.strictEqual(res.status, 0)
assert.strictEqual(res.statusText, '')
assert.strictEqual(res.type, 'error')
})
it('should support undefined status', () => {
const res = new Response(null, { status: undefined })
assert.strictEqual(res.status, 200)
})
it('should support undefined statusText', () => {
const res = new Response(null, { statusText: undefined })
assert.strictEqual(res.statusText, '')
})
it('should not set bodyUsed to undefined', () => {
const res = new Response()
assert.strictEqual(res.bodyUsed, false)
})
})
================================================
FILE: test/node-fetch/utils/dummy.txt
================================================
i am a dummy
================================================
FILE: test/node-fetch/utils/read-stream.js
================================================
module.exports = async function readStream (stream) {
const chunks = []
for await (const chunk of stream) {
chunks.push(chunk instanceof Buffer ? chunk : Buffer.from(chunk))
}
return Buffer.concat(chunks)
}
================================================
FILE: test/node-fetch/utils/server.js
================================================
'use strict'
const http = require('node:http')
const zlib = require('node:zlib')
const { once } = require('node:events')
const Busboy = require('@fastify/busboy')
module.exports = class TestServer {
constructor () {
this.server = http.createServer({ joinDuplicateHeaders: true }, this.router)
// Node 8 default keepalive timeout is 5000ms
// make it shorter here as we want to close server quickly at the end of tests
this.server.keepAliveTimeout = 1000
this.server.on('error', err => {
console.log(err.stack)
})
this.server.on('connection', socket => {
socket.setTimeout(1500)
})
}
async start () {
this.server.listen(0, 'localhost')
return once(this.server, 'listening')
}
async stop () {
this.server.close()
return once(this.server, 'close')
}
get port () {
return this.server.address().port
}
get hostname () {
return 'localhost'
}
mockState (responseHandler) {
this.server.nextResponseHandler = responseHandler
return `http://${this.hostname}:${this.port}/mocked`
}
router (request, res) {
const p = request.url
if (p === '/mocked') {
if (this.nextResponseHandler) {
this.nextResponseHandler(res)
this.nextResponseHandler = undefined
} else {
throw new Error('No mocked response. Use ’TestServer.mockState()’.')
}
}
if (p === '/hello') {
res.statusCode = 200
res.setHeader('Content-Type', 'text/plain')
res.end('world')
}
if (p.includes('question')) {
res.statusCode = 200
res.setHeader('Content-Type', 'text/plain')
res.end('ok')
}
if (p === '/plain') {
res.statusCode = 200
res.setHeader('Content-Type', 'text/plain')
res.end('text')
}
if (p === '/no-status-text') {
res.writeHead(200, '', {}).end()
}
if (p === '/options') {
res.statusCode = 200
res.setHeader('Allow', 'GET, HEAD, OPTIONS')
res.end('hello world')
}
if (p === '/html') {
res.statusCode = 200
res.setHeader('Content-Type', 'text/html')
res.end('')
}
if (p === '/json') {
res.statusCode = 200
res.setHeader('Content-Type', 'application/json')
res.end(JSON.stringify({
name: 'value'
}))
}
if (p === '/gzip') {
res.statusCode = 200
res.setHeader('Content-Type', 'text/plain')
res.setHeader('Content-Encoding', 'gzip')
zlib.gzip('hello world', (err, buffer) => {
if (err) {
throw err
}
res.end(buffer)
})
}
if (p === '/gzip-truncated') {
res.statusCode = 200
res.setHeader('Content-Type', 'text/plain')
res.setHeader('Content-Encoding', 'gzip')
zlib.gzip('hello world', (err, buffer) => {
if (err) {
throw err
}
// Truncate the CRC checksum and size check at the end of the stream
res.end(buffer.slice(0, -8))
})
}
if (p === '/gzip-capital') {
res.statusCode = 200
res.setHeader('Content-Type', 'text/plain')
res.setHeader('Content-Encoding', 'GZip')
zlib.gzip('hello world', (err, buffer) => {
if (err) {
throw err
}
res.end(buffer)
})
}
if (p === '/deflate') {
res.statusCode = 200
res.setHeader('Content-Type', 'text/plain')
res.setHeader('Content-Encoding', 'deflate')
zlib.deflate('hello world', (err, buffer) => {
if (err) {
throw err
}
res.end(buffer)
})
}
if (p === '/brotli') {
res.statusCode = 200
res.setHeader('Content-Type', 'text/plain')
if (typeof zlib.createBrotliDecompress === 'function') {
res.setHeader('Content-Encoding', 'br')
zlib.brotliCompress('hello world', (err, buffer) => {
if (err) {
throw err
}
res.end(buffer)
})
}
}
if (p === '/multiunsupported') {
res.statusCode = 200
res.setHeader('Content-Type', 'text/plain')
if (typeof zlib.createBrotliDecompress === 'function') {
res.setHeader('Content-Encoding', 'br,asd,br')
res.end('multiunsupported')
}
}
if (p === '/multisupported') {
res.statusCode = 200
res.setHeader('Content-Type', 'text/plain')
if (typeof zlib.createBrotliDecompress === 'function') {
res.setHeader('Content-Encoding', 'br,br')
zlib.brotliCompress('hello world', (err, buffer) => {
if (err) {
throw err
}
zlib.brotliCompress(buffer, (err, buffer) => {
if (err) {
throw err
}
res.end(buffer)
})
})
}
}
if (p === '/deflate-raw') {
res.statusCode = 200
res.setHeader('Content-Type', 'text/plain')
res.setHeader('Content-Encoding', 'deflate')
zlib.deflateRaw('hello world', (err, buffer) => {
if (err) {
throw err
}
res.end(buffer)
})
}
if (p === '/sdch') {
res.statusCode = 200
res.setHeader('Content-Type', 'text/plain')
res.setHeader('Content-Encoding', 'sdch')
res.end('fake sdch string')
}
if (p === '/invalid-content-encoding') {
res.statusCode = 200
res.setHeader('Content-Type', 'text/plain')
res.setHeader('Content-Encoding', 'gzip')
res.end('fake gzip string')
}
if (p === '/timeout') {
setTimeout(() => {
res.statusCode = 200
res.setHeader('Content-Type', 'text/plain')
res.end('text')
}, 1000)
}
if (p === '/slow') {
res.statusCode = 200
res.setHeader('Content-Type', 'text/plain')
res.write('test')
setTimeout(() => {
res.end('test')
}, 1000)
}
if (p === '/cookie') {
res.statusCode = 200
res.setHeader('Set-Cookie', ['a=1', 'b=1'])
res.end('cookie')
}
if (p === '/size/chunk') {
res.statusCode = 200
res.setHeader('Content-Type', 'text/plain')
setTimeout(() => {
res.write('test')
}, 10)
setTimeout(() => {
res.end('test')
}, 20)
}
if (p === '/size/long') {
res.statusCode = 200
res.setHeader('Content-Type', 'text/plain')
res.end('testtest')
}
if (p === '/redirect/301') {
res.statusCode = 301
res.setHeader('Location', '/inspect')
res.end()
}
if (p === '/redirect/302') {
res.statusCode = 302
res.setHeader('Location', '/inspect')
res.end()
}
if (p === '/redirect/303') {
res.statusCode = 303
res.setHeader('Location', '/inspect')
res.end()
}
if (p === '/redirect/307') {
res.statusCode = 307
res.setHeader('Location', '/inspect')
res.end()
}
if (p === '/redirect/308') {
res.statusCode = 308
res.setHeader('Location', '/inspect')
res.end()
}
if (p === '/redirect/chain') {
res.statusCode = 301
res.setHeader('Location', '/redirect/301')
res.end()
}
if (p.startsWith('/redirect/chain/')) {
const count = parseInt(p.split('/').pop()) - 1
res.statusCode = 301
res.setHeader('Location', count ? `/redirect/chain/${count}` : '/redirect/301')
res.end()
}
if (p === '/redirect/no-location') {
res.statusCode = 301
res.end()
}
if (p === '/redirect/slow') {
res.statusCode = 301
res.setHeader('Location', '/redirect/301')
setTimeout(() => {
res.end()
}, 1000)
}
if (p === '/redirect/slow-chain') {
res.statusCode = 301
res.setHeader('Location', '/redirect/slow')
setTimeout(() => {
res.end()
}, 10)
}
if (p === '/redirect/slow-stream') {
res.statusCode = 301
res.setHeader('Location', '/slow')
res.end()
}
if (p === '/redirect/bad-location') {
res.socket.write('HTTP/1.1 301\r\nLocation: ☃\r\nContent-Length: 0\r\n')
res.socket.end('\r\n')
}
if (p === '/error/400') {
res.statusCode = 400
res.setHeader('Content-Type', 'text/plain')
res.end('client error')
}
if (p === '/error/404') {
res.statusCode = 404
res.setHeader('Content-Encoding', 'gzip')
res.end()
}
if (p === '/error/500') {
res.statusCode = 500
res.setHeader('Content-Type', 'text/plain')
res.end('server error')
}
if (p === '/error/reset') {
res.destroy()
}
if (p === '/error/premature') {
res.writeHead(200, { 'content-length': 50 })
res.write('foo')
setTimeout(() => {
res.destroy()
}, 100)
}
if (p === '/error/premature/chunked') {
res.writeHead(200, {
'Content-Type': 'application/json',
'Transfer-Encoding': 'chunked'
})
res.write(`${JSON.stringify({ data: 'hi' })}\n`)
setTimeout(() => {
res.write(`${JSON.stringify({ data: 'bye' })}\n`)
}, 200)
setTimeout(() => {
res.destroy()
}, 400)
}
if (p === '/error/json') {
res.statusCode = 200
res.setHeader('Content-Type', 'application/json')
res.end('invalid json')
}
if (p === '/no-content') {
res.statusCode = 204
res.end()
}
if (p === '/no-content/gzip') {
res.statusCode = 204
res.setHeader('Content-Encoding', 'gzip')
res.end()
}
if (p === '/no-content/brotli') {
res.statusCode = 204
res.setHeader('Content-Encoding', 'br')
res.end()
}
if (p === '/not-modified') {
res.statusCode = 304
res.end()
}
if (p === '/not-modified/gzip') {
res.statusCode = 304
res.setHeader('Content-Encoding', 'gzip')
res.end()
}
if (p === '/inspect') {
res.statusCode = 200
res.setHeader('Content-Type', 'application/json')
let body = ''
request.on('data', c => {
body += c
})
request.on('end', () => {
res.end(JSON.stringify({
method: request.method,
url: request.url,
headers: request.headers,
body
}))
})
}
if (p === '/multipart') {
res.statusCode = 200
res.setHeader('Content-Type', 'application/json')
const busboy = new Busboy({ headers: request.headers })
let body = ''
busboy.on('file', async (fieldName, file, fileName) => {
body += `${fieldName}=${fileName}`
// consume file data
// eslint-disable-next-line no-empty, no-unused-vars
for await (const c of file) {}
})
busboy.on('field', (fieldName, value) => {
body += `${fieldName}=${value}`
})
busboy.on('finish', () => {
res.end(JSON.stringify({
method: request.method,
url: request.url,
headers: request.headers,
body
}))
})
request.pipe(busboy)
}
if (p === '/m%C3%B6bius') {
res.statusCode = 200
res.setHeader('Content-Type', 'text/plain')
res.end('ok')
}
}
}
================================================
FILE: test/node-platform-objects.js
================================================
'use strict'
const { tspl } = require('@matteo.collina/tspl')
const { test } = require('node:test')
const { markAsUncloneable } = require('node:worker_threads')
const { Response, Request, FormData, Headers, ErrorEvent, MessageEvent, CloseEvent, EventSource, WebSocket } = require('..')
const { CacheStorage } = require('../lib/web/cache/cachestorage')
const { Cache } = require('../lib/web/cache/cache')
const { kConstruct } = require('../lib/core/symbols')
test('unserializable web instances should be uncloneable if node exposes the api', (t) => {
if (markAsUncloneable !== undefined) {
t = tspl(t, { plan: 11 })
const uncloneables = [
{ Uncloneable: Response, brand: 'Response' },
{ Uncloneable: Request, value: 'http://localhost', brand: 'Request' },
{ Uncloneable: FormData, brand: 'FormData' },
{ Uncloneable: MessageEvent, value: 'dummy event', brand: 'MessageEvent' },
{ Uncloneable: CloseEvent, value: 'dummy event', brand: 'CloseEvent' },
{ Uncloneable: ErrorEvent, value: 'dummy event', brand: 'ErrorEvent' },
{ Uncloneable: EventSource, value: 'http://localhost', brand: 'EventSource', doneCb: (entity) => entity.close() },
{ Uncloneable: Headers, brand: 'Headers' },
{ Uncloneable: WebSocket, value: 'http://localhost', brand: 'WebSocket' },
{ Uncloneable: Cache, value: kConstruct, brand: 'Cache' },
{ Uncloneable: CacheStorage, value: kConstruct, brand: 'CacheStorage' }
]
uncloneables.forEach((platformEntity) => {
const entity = new platformEntity.Uncloneable(platformEntity.value)
t.throws(() => structuredClone(entity),
DOMException,
`Cloning ${platformEntity.brand} should throw DOMException`)
platformEntity.doneCb?.(entity)
})
}
})
================================================
FILE: test/node-test/abort-controller.js
================================================
'use strict'
const { test } = require('node:test')
const { AbortController: NPMAbortController } = require('abort-controller')
const { Client, errors } = require('../..')
const { createServer } = require('node:http')
const { createReadStream } = require('node:fs')
const { wrapWithAsyncIterable } = require('../utils/async-iterators')
const { tspl } = require('@matteo.collina/tspl')
const { closeServerAsPromise } = require('../utils/node-http')
const controllers = [{
AbortControllerImpl: NPMAbortController,
controllerName: 'npm-abortcontroller-shim'
}]
if (global.AbortController) {
controllers.push({
AbortControllerImpl: global.AbortController,
controllerName: 'native-abortcontroller'
})
}
for (const { AbortControllerImpl, controllerName } of controllers) {
test(`Abort ${controllerName} before creating request`, async (t) => {
const p = tspl(t, { plan: 1 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
p.fail()
})
t.after(closeServerAsPromise(server))
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
const abortController = new AbortControllerImpl()
t.after(client.destroy.bind(client))
abortController.abort()
client.request({ path: '/', method: 'GET', signal: abortController.signal }, (err, response) => {
p.ok(err instanceof errors.RequestAbortedError || err instanceof DOMException)
})
})
await p.completed
})
test(`Abort ${controllerName} before sending request (no body)`, async (t) => {
const p = tspl(t, { plan: 3 })
let count = 0
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
if (count === 1) {
p.fail('The second request should never be executed')
}
count += 1
res.end('hello')
})
t.after(closeServerAsPromise(server))
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
const abortController = new AbortControllerImpl()
t.after(client.destroy.bind(client))
client.request({ path: '/', method: 'GET' }, (err, response) => {
p.ifError(err)
const bufs = []
response.body.on('data', (buf) => {
bufs.push(buf)
})
response.body.on('end', () => {
p.strictEqual('hello', Buffer.concat(bufs).toString('utf8'))
})
})
client.request({ path: '/', method: 'GET', signal: abortController.signal }, (err, response) => {
p.ok(err instanceof errors.RequestAbortedError || err instanceof DOMException)
})
abortController.abort()
})
await p.completed
})
test(`Abort ${controllerName} while waiting response (no body)`, async (t) => {
const p = tspl(t, { plan: 1 })
const abortController = new AbortControllerImpl()
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
abortController.abort()
res.setHeader('content-type', 'text/plain')
res.end('hello world')
})
t.after(closeServerAsPromise(server))
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
t.after(client.destroy.bind(client))
client.request({ path: '/', method: 'GET', signal: abortController.signal }, (err, response) => {
p.ok(err instanceof errors.RequestAbortedError || err instanceof DOMException)
})
})
await p.completed
})
test(`Abort ${controllerName} while waiting response (write headers started) (no body)`, async (t) => {
const p = tspl(t, { plan: 1 })
const abortController = new AbortControllerImpl()
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.writeHead(200, { 'content-type': 'text/plain' })
res.flushHeaders()
abortController.abort()
res.end('hello world')
})
t.after(closeServerAsPromise(server))
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
t.after(client.destroy.bind(client))
client.request({ path: '/', method: 'GET', signal: abortController.signal }, (err, response) => {
p.ok(err instanceof errors.RequestAbortedError || err instanceof DOMException)
})
})
await p.completed
})
test(`Abort ${controllerName} while waiting response (write headers and write body started) (no body)`, async (t) => {
const p = tspl(t, { plan: 2 })
const abortController = new AbortControllerImpl()
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.writeHead(200, { 'content-type': 'text/plain' })
res.write('hello')
})
t.after(closeServerAsPromise(server))
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
t.after(client.destroy.bind(client))
client.request({ path: '/', method: 'GET', signal: abortController.signal }, (err, response) => {
p.ifError(err)
response.body.on('data', () => {
abortController.abort()
})
response.body.on('error', err => {
p.ok(err instanceof errors.RequestAbortedError || err instanceof DOMException)
})
})
})
await p.completed
})
function waitingWithBody (body, type) {
test(`Abort ${controllerName} while waiting response (with body ${type})`, async (t) => {
const p = tspl(t, { plan: 1 })
const abortController = new AbortControllerImpl()
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
abortController.abort()
res.setHeader('content-type', 'text/plain')
res.end('hello world')
})
t.after(closeServerAsPromise(server))
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
t.after(client.destroy.bind(client))
client.request({ path: '/', method: 'POST', body, signal: abortController.signal }, (err, response) => {
p.ok(err instanceof errors.RequestAbortedError || err instanceof DOMException)
})
})
await p.completed
})
}
waitingWithBody('hello', 'string')
waitingWithBody(createReadStream(__filename), 'stream')
waitingWithBody(new Uint8Array([42]), 'Uint8Array')
waitingWithBody(wrapWithAsyncIterable(createReadStream(__filename)), 'async-iterator')
function writeHeadersStartedWithBody (body, type) {
test(`Abort ${controllerName} while waiting response (write headers started) (with body ${type})`, async (t) => {
const p = tspl(t, { plan: 1 })
const abortController = new AbortControllerImpl()
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.writeHead(200, { 'content-type': 'text/plain' })
res.flushHeaders()
abortController.abort()
res.end('hello world')
})
t.after(closeServerAsPromise(server))
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
t.after(client.destroy.bind(client))
client.request({ path: '/', method: 'POST', body, signal: abortController.signal }, (err, response) => {
p.ok(err instanceof errors.RequestAbortedError || err instanceof DOMException)
})
})
await p.completed
})
}
writeHeadersStartedWithBody('hello', 'string')
writeHeadersStartedWithBody(createReadStream(__filename), 'stream')
writeHeadersStartedWithBody(new Uint8Array([42]), 'Uint8Array')
writeHeadersStartedWithBody(wrapWithAsyncIterable(createReadStream(__filename)), 'async-iterator')
function writeBodyStartedWithBody (body, type) {
test(`Abort ${controllerName} while waiting response (write headers and write body started) (with body ${type})`, async (t) => {
const p = tspl(t, { plan: 2 })
const abortController = new AbortControllerImpl()
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.writeHead(200, { 'content-type': 'text/plain' })
res.write('hello')
})
t.after(closeServerAsPromise(server))
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
t.after(client.destroy.bind(client))
client.request({ path: '/', method: 'POST', body, signal: abortController.signal }, (err, response) => {
p.ifError(err)
response.body.on('data', () => {
abortController.abort()
})
response.body.on('error', err => {
p.ok(err instanceof errors.RequestAbortedError || err instanceof DOMException)
})
})
})
await p.completed
})
}
writeBodyStartedWithBody('hello', 'string')
writeBodyStartedWithBody(createReadStream(__filename), 'stream')
writeBodyStartedWithBody(new Uint8Array([42]), 'Uint8Array')
writeBodyStartedWithBody(wrapWithAsyncIterable(createReadStream(__filename), 'async-iterator'))
}
================================================
FILE: test/node-test/abort-event-emitter.js
================================================
'use strict'
const { test } = require('node:test')
const EventEmitter = require('node:events')
const { Client, errors } = require('../..')
const { createServer } = require('node:http')
const { createReadStream } = require('node:fs')
const { Readable } = require('node:stream')
const { tspl } = require('@matteo.collina/tspl')
const { wrapWithAsyncIterable } = require('../utils/async-iterators')
const { closeServerAsPromise } = require('../utils/node-http')
test('Abort before sending request (no body)', async (t) => {
const p = tspl(t, { plan: 4 })
let count = 0
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
if (count === 1) {
p.fail('The second request should never be executed')
}
count += 1
res.end('hello')
})
t.after(closeServerAsPromise(server))
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
const ee = new EventEmitter()
t.after(client.destroy.bind(client))
client.request({ path: '/', method: 'GET' }, (err, response) => {
p.ifError(err)
const bufs = []
response.body.on('data', (buf) => {
bufs.push(buf)
})
response.body.on('end', () => {
p.strictEqual('hello', Buffer.concat(bufs).toString('utf8'))
})
})
const body = new Readable({ read () { } })
body.on('error', (err) => {
p.ok(err instanceof errors.RequestAbortedError)
})
client.request({
path: '/',
method: 'GET',
signal: ee,
body
}, (err, response) => {
p.ok(err instanceof errors.RequestAbortedError)
})
ee.emit('abort')
})
await p.completed
})
test('Abort before sending request (no body) async iterator', async (t) => {
const p = tspl(t, { plan: 3 })
let count = 0
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
if (count === 1) {
t.fail('The second request should never be executed')
}
count += 1
res.end('hello')
})
t.after(closeServerAsPromise(server))
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
const ee = new EventEmitter()
t.after(client.destroy.bind(client))
client.request({ path: '/', method: 'GET' }, (err, response) => {
p.ifError(err)
const bufs = []
response.body.on('data', (buf) => {
bufs.push(buf)
})
response.body.on('end', () => {
p.strictEqual('hello', Buffer.concat(bufs).toString('utf8'))
})
})
const body = wrapWithAsyncIterable(new Readable({ read () { } }))
client.request({
path: '/',
method: 'GET',
signal: ee,
body
}, (err, response) => {
p.ok(err instanceof errors.RequestAbortedError)
})
ee.emit('abort')
})
await p.completed
})
test('Abort while waiting response (no body)', async (t) => {
const p = tspl(t, { plan: 1 })
const ee = new EventEmitter()
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
ee.emit('abort')
res.setHeader('content-type', 'text/plain')
res.end('hello world')
})
t.after(closeServerAsPromise(server))
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
t.after(client.destroy.bind(client))
client.request({ path: '/', method: 'GET', signal: ee }, (err, response) => {
p.ok(err instanceof errors.RequestAbortedError)
})
})
await p.completed
})
test('Abort while waiting response (write headers started) (no body)', async (t) => {
const p = tspl(t, { plan: 1 })
const ee = new EventEmitter()
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.writeHead(200, { 'content-type': 'text/plain' })
res.flushHeaders()
ee.emit('abort')
res.end('hello world')
})
t.after(closeServerAsPromise(server))
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
t.after(client.destroy.bind(client))
client.request({ path: '/', method: 'GET', signal: ee }, (err, response) => {
p.ok(err instanceof errors.RequestAbortedError)
})
})
await p.completed
})
test('Abort while waiting response (write headers and write body started) (no body)', async (t) => {
const p = tspl(t, { plan: 2 })
const ee = new EventEmitter()
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.writeHead(200, { 'content-type': 'text/plain' })
res.write('hello')
})
t.after(closeServerAsPromise(server))
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
t.after(client.destroy.bind(client))
client.request({ path: '/', method: 'GET', signal: ee }, (err, response) => {
p.ifError(err)
response.body.on('data', () => {
ee.emit('abort')
})
response.body.on('error', err => {
p.ok(err instanceof errors.RequestAbortedError)
})
})
})
await p.completed
})
function waitingWithBody (body, type) {
test(`Abort while waiting response (with body ${type})`, async (t) => {
const p = tspl(t, { plan: 1 })
const ee = new EventEmitter()
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
ee.emit('abort')
res.setHeader('content-type', 'text/plain')
res.end('hello world')
})
t.after(closeServerAsPromise(server))
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
t.after(client.destroy.bind(client))
client.request({ path: '/', method: 'POST', body, signal: ee }, (err, response) => {
p.ok(err instanceof errors.RequestAbortedError)
})
})
await p.completed
})
}
waitingWithBody('hello', 'string')
waitingWithBody(createReadStream(__filename), 'stream')
waitingWithBody(new Uint8Array([42]), 'Uint8Array')
waitingWithBody(wrapWithAsyncIterable(createReadStream(__filename)), 'async-iterator')
function writeHeadersStartedWithBody (body, type) {
test(`Abort while waiting response (write headers started) (with body ${type})`, async (t) => {
const p = tspl(t, { plan: 1 })
const ee = new EventEmitter()
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.writeHead(200, { 'content-type': 'text/plain' })
res.flushHeaders()
ee.emit('abort')
res.end('hello world')
})
t.after(closeServerAsPromise(server))
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
t.after(client.destroy.bind(client))
client.request({ path: '/', method: 'POST', body, signal: ee }, (err, response) => {
p.ok(err instanceof errors.RequestAbortedError)
})
})
await p.completed
})
}
writeHeadersStartedWithBody('hello', 'string')
writeHeadersStartedWithBody(createReadStream(__filename), 'stream')
writeHeadersStartedWithBody(new Uint8Array([42]), 'Uint8Array')
writeHeadersStartedWithBody(wrapWithAsyncIterable(createReadStream(__filename)), 'async-iterator')
function writeBodyStartedWithBody (body, type) {
test(`Abort while waiting response (write headers and write body started) (with body ${type})`, async (t) => {
const p = tspl(t, { plan: 2 })
const ee = new EventEmitter()
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.writeHead(200, { 'content-type': 'text/plain' })
res.write('hello')
})
t.after(closeServerAsPromise(server))
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
t.after(client.destroy.bind(client))
client.request({ path: '/', method: 'POST', body, signal: ee }, (err, response) => {
p.ifError(err)
response.body.on('data', () => {
ee.emit('abort')
})
response.body.on('error', err => {
p.ok(err instanceof errors.RequestAbortedError)
})
})
})
await p.completed
})
}
writeBodyStartedWithBody('hello', 'string')
writeBodyStartedWithBody(createReadStream(__filename), 'stream')
writeBodyStartedWithBody(new Uint8Array([42]), 'Uint8Array')
writeBodyStartedWithBody(wrapWithAsyncIterable(createReadStream(__filename)), 'async-iterator')
================================================
FILE: test/node-test/agent.js
================================================
'use strict'
const { describe, test, after } = require('node:test')
const assert = require('node:assert/strict')
const { once } = require('node:events')
const http = require('node:http')
const { PassThrough } = require('node:stream')
const { kRunning } = require('../../lib/core/symbols')
const {
Agent,
errors,
request,
stream,
pipeline,
Pool,
setGlobalDispatcher,
getGlobalDispatcher
} = require('../..')
const { tspl } = require('@matteo.collina/tspl')
const { closeServerAsPromise } = require('../utils/node-http')
describe('setGlobalDispatcher', () => {
after(() => {
// reset globalAgent to a fresh Agent instance for later tests
setGlobalDispatcher(new Agent())
})
test('fails if agent does not implement `get` method', t => {
const p = tspl(t, { plan: 1 })
p.throws(() => setGlobalDispatcher({ dispatch: 'not a function' }), errors.InvalidArgumentError)
})
test('sets global agent', async t => {
const p = tspl(t, { plan: 2 })
p.doesNotThrow(() => setGlobalDispatcher(new Agent()))
p.doesNotThrow(() => setGlobalDispatcher({ dispatch: () => {} }))
})
})
test('Agent', t => {
const p = tspl(t, { plan: 1 })
p.doesNotThrow(() => new Agent())
})
test('Agent enforces maxOrigins', async (t) => {
const p = tspl(t, { plan: 1 })
const dispatcher = new Agent({
maxOrigins: 1,
keepAliveMaxTimeout: 100,
keepAliveTimeout: 100
})
t.after(() => dispatcher.close())
const handler = (_req, res) => {
setTimeout(() => res.end('ok'), 50)
}
const server1 = http.createServer({ joinDuplicateHeaders: true }, handler)
server1.listen(0)
await once(server1, 'listening')
t.after(closeServerAsPromise(server1))
const server2 = http.createServer({ joinDuplicateHeaders: true }, handler)
server2.listen(0)
await once(server2, 'listening')
t.after(closeServerAsPromise(server2))
try {
await Promise.all([
request(`http://localhost:${server1.address().port}`, { dispatcher }),
request(`http://localhost:${server2.address().port}`, { dispatcher })
])
} catch (err) {
p.ok(err instanceof errors.MaxOriginsReachedError)
}
await p.completed
})
test('agent should call callback after closing internal pools', async (t) => {
const p = tspl(t, { plan: 2 })
const wanted = 'payload'
const server = http.createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.setHeader('Content-Type', 'text/plain')
res.end(wanted)
})
t.after(closeServerAsPromise(server))
server.listen(0, () => {
const dispatcher = new Agent()
const origin = `http://localhost:${server.address().port}`
request(origin, { dispatcher })
.then(() => {
// first request should resolve
p.ok(1)
})
.catch(err => {
p.fail(err)
})
dispatcher.once('connect', () => {
dispatcher.close(() => {
request(origin, { dispatcher })
.then(() => {
p.fail('second request should not resolve')
})
.catch(err => {
p.ok(err instanceof errors.ClientDestroyedError)
})
})
})
})
await p.completed
})
test('agent close throws when callback is not a function', t => {
const p = tspl(t, { plan: 1 })
const dispatcher = new Agent()
try {
dispatcher.close({})
} catch (err) {
p.ok(err instanceof errors.InvalidArgumentError)
}
})
test('agent should close internal pools', async (t) => {
const p = tspl(t, { plan: 2 })
const wanted = 'payload'
const server = http.createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.setHeader('Content-Type', 'text/plain')
res.end(wanted)
})
t.after(closeServerAsPromise(server))
server.listen(0, () => {
const dispatcher = new Agent()
const origin = `http://localhost:${server.address().port}`
request(origin, { dispatcher })
.then(() => {
// first request should resolve
p.ok(1)
})
.catch(err => {
p.fail(err)
})
dispatcher.once('connect', () => {
dispatcher.close()
.then(() => request(origin, { dispatcher }))
.then(() => {
p.fail('second request should not resolve')
})
.catch(err => {
p.ok(err instanceof errors.ClientDestroyedError)
})
})
})
await p.completed
})
test('agent should destroy internal pools and call callback', async (t) => {
const p = tspl(t, { plan: 2 })
const wanted = 'payload'
const server = http.createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.setHeader('Content-Type', 'text/plain')
res.end(wanted)
})
t.after(closeServerAsPromise(server))
server.listen(0, () => {
const dispatcher = new Agent()
const origin = `http://localhost:${server.address().port}`
request(origin, { dispatcher })
.then(() => {
p.fail()
})
.catch(err => {
p.ok(err instanceof errors.ClientDestroyedError)
})
dispatcher.once('connect', () => {
dispatcher.destroy(() => {
request(origin, { dispatcher })
.then(() => {
p.fail()
})
.catch(err => {
p.ok(err instanceof errors.ClientDestroyedError)
})
})
})
})
await p.completed
})
test('agent destroy throws when callback is not a function', t => {
const p = tspl(t, { plan: 1 })
const dispatcher = new Agent()
try {
dispatcher.destroy(new Error('mock error'), {})
} catch (err) {
p.ok(err instanceof errors.InvalidArgumentError)
}
})
test('agent close/destroy callback with error', t => {
const p = tspl(t, { plan: 4 })
const dispatcher = new Agent()
p.strictEqual(dispatcher.closed, false)
dispatcher.close()
p.strictEqual(dispatcher.closed, true)
p.strictEqual(dispatcher.destroyed, false)
dispatcher.destroy(new Error('mock error'))
p.strictEqual(dispatcher.destroyed, true)
})
test('agent should destroy internal pools', async t => {
const p = tspl(t, { plan: 2 })
const wanted = 'payload'
const server = http.createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.setHeader('Content-Type', 'text/plain')
res.end(wanted)
})
t.after(closeServerAsPromise(server))
server.listen(0, () => {
const dispatcher = new Agent()
const origin = `http://localhost:${server.address().port}`
request(origin, { dispatcher })
.then(() => {
p.fail()
})
.catch(err => {
p.ok(err instanceof errors.ClientDestroyedError)
})
dispatcher.once('connect', () => {
dispatcher.destroy()
.then(() => request(origin, { dispatcher }))
.then(() => {
p.fail()
})
.catch(err => {
p.ok(err instanceof errors.ClientDestroyedError)
})
})
})
await p.completed
})
test('multiple connections', async t => {
const connections = 3
const p = tspl(t, { plan: 6 * connections })
const server = http.createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.writeHead(200, {
Connection: 'keep-alive',
'Keep-Alive': 'timeout=1s'
})
res.end('ok')
})
t.after(closeServerAsPromise(server))
server.listen(0, async () => {
const origin = `http://localhost:${server.address().port}`
const dispatcher = new Agent({ connections })
t.after(() => { dispatcher.close.bind(dispatcher)() })
dispatcher.on('connect', (origin, [dispatcher]) => {
p.ok(dispatcher)
})
dispatcher.on('disconnect', (origin, [dispatcher], error) => {
p.ok(dispatcher)
p.ok(error instanceof errors.InformationalError)
p.strictEqual(error.code, 'UND_ERR_INFO')
p.strictEqual(error.message, 'reset')
})
for (let i = 0; i < connections; i++) {
try {
await request(origin, { dispatcher })
p.ok(1)
} catch (err) {
p.fail(err)
}
}
})
await p.completed
})
test('agent factory supports URL parameter', async (t) => {
const p = tspl(t, { plan: 2 })
const noopHandler = {
onConnect () {},
onHeaders () {},
onData () {},
onComplete () {
server.close()
},
onError (err) {
throw err
}
}
const dispatcher = new Agent({
factory: (origin, opts) => {
p.ok(origin instanceof URL)
return new Pool(origin, opts)
}
})
const server = http.createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.setHeader('Content-Type', 'text/plain')
res.end('asd')
})
server.listen(0, () => {
p.doesNotThrow(() => dispatcher.dispatch({
origin: new URL(`http://localhost:${server.address().port}`),
path: '/',
method: 'GET'
}, noopHandler))
})
await p.completed
})
test('agent factory supports string parameter', async (t) => {
const p = tspl(t, { plan: 2 })
const noopHandler = {
onConnect () {},
onHeaders () {},
onData () {},
onComplete () {
server.close()
},
onError (err) {
throw err
}
}
const dispatcher = new Agent({
factory: (origin, opts) => {
p.ok(typeof origin === 'string')
return new Pool(origin, opts)
}
})
const server = http.createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.setHeader('Content-Type', 'text/plain')
res.end('asd')
})
server.listen(0, () => {
p.doesNotThrow(() => dispatcher.dispatch({
origin: `http://localhost:${server.address().port}`,
path: '/',
method: 'GET'
}, noopHandler))
})
await p.completed
})
test('with globalAgent', async t => {
const p = tspl(t, { plan: 6 })
const wanted = 'payload'
const server = http.createServer({ joinDuplicateHeaders: true }, (req, res) => {
p.strictEqual('/', req.url)
p.strictEqual('GET', req.method)
p.strictEqual(`localhost:${server.address().port}`, req.headers.host)
res.setHeader('Content-Type', 'text/plain')
res.end(wanted)
})
t.after(closeServerAsPromise(server))
server.listen(0, () => {
request(`http://localhost:${server.address().port}`)
.then(({ statusCode, headers, body }) => {
p.strictEqual(statusCode, 200)
p.strictEqual(headers['content-type'], 'text/plain')
const bufs = []
body.on('data', (buf) => {
bufs.push(buf)
})
body.on('end', () => {
p.strictEqual(wanted, Buffer.concat(bufs).toString('utf8'))
})
})
.catch(err => {
p.fail(err)
})
})
await p.completed
})
test('with local agent', async t => {
const p = tspl(t, { plan: 6 })
const wanted = 'payload'
const server = http.createServer({ joinDuplicateHeaders: true }, (req, res) => {
p.strictEqual('/', req.url)
p.strictEqual('GET', req.method)
p.strictEqual(`localhost:${server.address().port}`, req.headers.host)
res.setHeader('Content-Type', 'text/plain')
res.end(wanted)
})
t.after(closeServerAsPromise(server))
const dispatcher = new Agent({
connect: {
servername: 'agent1'
}
})
server.listen(0, () => {
request(`http://localhost:${server.address().port}`, { dispatcher })
.then(({ statusCode, headers, body }) => {
p.strictEqual(statusCode, 200)
p.strictEqual(headers['content-type'], 'text/plain')
const bufs = []
body.on('data', (buf) => {
bufs.push(buf)
})
body.on('end', () => {
p.strictEqual(wanted, Buffer.concat(bufs).toString('utf8'))
})
})
.catch(err => {
p.fail(err)
})
})
await p.completed
})
test('fails with invalid args', t => {
assert.throws(() => request(), errors.InvalidArgumentError, 'throws on missing url argument')
assert.throws(() => request(''), errors.InvalidArgumentError, 'throws on invalid url')
assert.throws(() => request({}), errors.InvalidArgumentError, 'throws on missing url.origin argument')
assert.throws(() => request({ origin: '' }), errors.InvalidArgumentError, 'throws on invalid url.origin argument')
assert.throws(() => request('https://example.com', { path: 0 }), errors.InvalidArgumentError, 'throws on opts.path argument')
assert.throws(() => request('https://example.com', { agent: new Agent() }), errors.InvalidArgumentError, 'throws on opts.path argument')
assert.throws(() => request('https://example.com', 'asd'), errors.InvalidArgumentError, 'throws on non object opts argument')
})
test('with globalAgent', async t => {
const p = tspl(t, { plan: 6 })
const wanted = 'payload'
const server = http.createServer({ joinDuplicateHeaders: true }, (req, res) => {
p.strictEqual('/', req.url)
p.strictEqual('GET', req.method)
p.strictEqual(`localhost:${server.address().port}`, req.headers.host)
res.setHeader('Content-Type', 'text/plain')
res.end(wanted)
})
t.after(closeServerAsPromise(server))
server.listen(0, () => {
stream(
`http://localhost:${server.address().port}`,
{
opaque: new PassThrough()
},
({ statusCode, headers, opaque: pt }) => {
p.strictEqual(statusCode, 200)
p.strictEqual(headers['content-type'], 'text/plain')
const bufs = []
pt.on('data', (buf) => {
bufs.push(buf)
})
pt.on('end', () => {
p.strictEqual(wanted, Buffer.concat(bufs).toString('utf8'))
})
pt.on('error', () => {
p.fail()
})
return pt
}
)
})
await p.completed
})
test('with a local agent', async t => {
const p = tspl(t, { plan: 6 })
const wanted = 'payload'
const server = http.createServer({ joinDuplicateHeaders: true }, (req, res) => {
p.strictEqual('/', req.url)
p.strictEqual('GET', req.method)
p.strictEqual(`localhost:${server.address().port}`, req.headers.host)
res.setHeader('Content-Type', 'text/plain')
res.end(wanted)
})
t.after(closeServerAsPromise(server))
const dispatcher = new Agent()
dispatcher.on('connect', (origin, [dispatcher]) => {
p.ok(dispatcher)
p.strictEqual(dispatcher[kRunning], 0)
process.nextTick(() => {
p.strictEqual(dispatcher[kRunning], 1)
})
})
server.listen(0, () => {
stream(
`http://localhost:${server.address().port}`,
{
dispatcher,
opaque: new PassThrough()
},
({ statusCode, headers, opaque: pt }) => {
p.strictEqual(statusCode, 200)
p.strictEqual(headers['content-type'], 'text/plain')
const bufs = []
pt.on('data', (buf) => {
bufs.push(buf)
})
pt.on('end', () => {
p.strictEqual(wanted, Buffer.concat(bufs).toString('utf8'))
})
pt.on('error', (err) => {
p.fail(err)
})
return pt
}
)
})
await p.completed
})
test('stream: fails with invalid URL', t => {
const p = tspl(t, { plan: 4 })
p.throws(() => stream(), errors.InvalidArgumentError, 'throws on missing url argument')
p.throws(() => stream(''), errors.InvalidArgumentError, 'throws on invalid url')
p.throws(() => stream({}), errors.InvalidArgumentError, 'throws on missing url.origin argument')
p.throws(() => stream({ origin: '' }), errors.InvalidArgumentError, 'throws on invalid url.origin argument')
})
test('with globalAgent', async t => {
const p = tspl(t, { plan: 6 })
const wanted = 'payload'
const server = http.createServer({ joinDuplicateHeaders: true }, (req, res) => {
p.strictEqual('/', req.url)
p.strictEqual('GET', req.method)
p.strictEqual(`localhost:${server.address().port}`, req.headers.host)
res.setHeader('Content-Type', 'text/plain')
res.end(wanted)
})
t.after(closeServerAsPromise(server))
server.listen(0, () => {
const bufs = []
pipeline(
`http://localhost:${server.address().port}`,
{},
({ statusCode, headers, body }) => {
p.strictEqual(statusCode, 200)
p.strictEqual(headers['content-type'], 'text/plain')
return body
}
)
.end()
.on('data', buf => {
bufs.push(buf)
})
.on('end', () => {
p.strictEqual(wanted, Buffer.concat(bufs).toString('utf8'))
})
.on('error', (err) => {
p.fail(err)
})
})
await p.completed
})
test('with a local agent', async t => {
const p = tspl(t, { plan: 6 })
const wanted = 'payload'
const server = http.createServer({ joinDuplicateHeaders: true }, (req, res) => {
p.strictEqual('/', req.url)
p.strictEqual('GET', req.method)
p.strictEqual(`localhost:${server.address().port}`, req.headers.host)
res.setHeader('Content-Type', 'text/plain')
res.end(wanted)
})
t.after(closeServerAsPromise(server))
const dispatcher = new Agent()
server.listen(0, () => {
const bufs = []
pipeline(
`http://localhost:${server.address().port}`,
{ dispatcher },
({ statusCode, headers, body }) => {
p.strictEqual(statusCode, 200)
p.strictEqual(headers['content-type'], 'text/plain')
return body
}
)
.end()
.on('data', buf => {
bufs.push(buf)
})
.on('end', () => {
p.strictEqual(wanted, Buffer.concat(bufs).toString('utf8'))
})
.on('error', () => {
p.fail()
})
})
await p.completed
})
test('pipeline: fails with invalid URL', t => {
const p = tspl(t, { plan: 4 })
p.throws(() => pipeline(), errors.InvalidArgumentError, 'throws on missing url argument')
p.throws(() => pipeline(''), errors.InvalidArgumentError, 'throws on invalid url')
p.throws(() => pipeline({}), errors.InvalidArgumentError, 'throws on missing url.origin argument')
p.throws(() => pipeline({ origin: '' }), errors.InvalidArgumentError, 'throws on invalid url.origin argument')
})
test('pipeline: fails with invalid onInfo', async (t) => {
const p = tspl(t, { plan: 2 })
pipeline({ origin: 'http://localhost', path: '/', onInfo: 'foo' }, () => {}).on('error', (err) => {
p.ok(err instanceof errors.InvalidArgumentError)
p.equal(err.message, 'invalid onInfo callback')
})
await p.completed
})
test('request: fails with invalid onInfo', async (t) => {
try {
await request({ origin: 'http://localhost', path: '/', onInfo: 'foo' })
assert.fail('should throw')
} catch (e) {
assert.ok(e)
assert.strictEqual(e.message, 'invalid onInfo callback')
}
})
test('stream: fails with invalid onInfo', async (t) => {
try {
await stream({ origin: 'http://localhost', path: '/', onInfo: 'foo' }, () => new PassThrough())
assert.fail('should throw')
} catch (e) {
assert.ok(e)
assert.strictEqual(e.message, 'invalid onInfo callback')
}
})
test('constructor validations', t => {
const p = tspl(t, { plan: 3 })
p.throws(() => new Agent({ factory: 'ASD' }), errors.InvalidArgumentError, 'throws on invalid opts argument')
p.throws(() => new Agent({ maxOrigins: -1 }), errors.InvalidArgumentError, 'maxOrigins must be a number greater than 0')
p.throws(() => new Agent({ maxOrigins: 'foo' }), errors.InvalidArgumentError, 'maxOrigins must be a number greater than 0')
})
test('dispatch validations', async t => {
const dispatcher = new Agent()
const noopHandler = {
onConnect () {},
onHeaders () {},
onData () {},
onComplete () {
server.close()
},
onError (err) {
throw err
}
}
const server = http.createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.setHeader('Content-Type', 'text/plain')
res.end('asd')
})
const p = tspl(t, { plan: 6 })
p.throws(() => dispatcher.dispatch('ASD'), errors.InvalidArgumentError, 'throws on missing handler')
p.throws(() => dispatcher.dispatch('ASD', noopHandler), errors.InvalidArgumentError, 'throws on invalid opts argument type')
p.throws(() => dispatcher.dispatch({}, noopHandler), errors.InvalidArgumentError, 'throws on invalid opts.origin argument')
p.throws(() => dispatcher.dispatch({ origin: '' }, noopHandler), errors.InvalidArgumentError, 'throws on invalid opts.origin argument')
p.throws(() => dispatcher.dispatch({}, {}), errors.InvalidArgumentError, 'throws on invalid handler.onError')
server.listen(0, () => {
p.doesNotThrow(() => dispatcher.dispatch({
origin: new URL(`http://localhost:${server.address().port}`),
path: '/',
method: 'GET'
}, noopHandler))
})
await p.completed
})
test('drain', async t => {
const p = tspl(t, { plan: 2 })
const dispatcher = new Agent({
connections: 1,
pipelining: 1
})
dispatcher.on('drain', () => {
p.ok(1)
})
class Handler {
onConnect () {}
onHeaders () {}
onData () {}
onComplete () {}
onError () {
p.fail()
}
}
const server = http.createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.setHeader('Content-Type', 'text/plain')
res.end('asd')
})
t.after(closeServerAsPromise(server))
server.listen(0, () => {
p.strictEqual(dispatcher.dispatch({
origin: `http://localhost:${server.address().port}`,
method: 'GET',
path: '/'
}, new Handler()), false)
})
await p.completed
})
test('global api', async t => {
const p = tspl(t, { plan: 6 * 2 })
const server = http.createServer({ joinDuplicateHeaders: true }, (req, res) => {
if (req.url === '/bar') {
p.strictEqual(req.method, 'PUT')
p.strictEqual(req.url, '/bar')
} else {
p.strictEqual(req.method, 'GET')
p.strictEqual(req.url, '/foo')
}
req.pipe(res)
})
t.after(closeServerAsPromise(server))
server.listen(0, async () => {
const origin = `http://localhost:${server.address().port}`
await request(origin, { path: '/foo' })
await request(`${origin}/foo`)
await request({ origin, path: '/foo' })
await stream({ origin, path: '/foo' }, () => new PassThrough())
await request({ protocol: 'http:', hostname: 'localhost', port: server.address().port, path: '/foo' })
await request(`${origin}/bar`, { body: 'asd' })
})
await p.completed
})
test('global api throws', t => {
const origin = 'http://asd'
assert.throws(() => request(`${origin}/foo`, { path: '/foo' }), errors.InvalidArgumentError)
assert.throws(() => request({ origin, path: 0 }, { path: '/foo' }), errors.InvalidArgumentError)
assert.throws(() => request({ origin, pathname: 0 }, { path: '/foo' }), errors.InvalidArgumentError)
assert.throws(() => request({ origin: 0 }, { path: '/foo' }), errors.InvalidArgumentError)
assert.throws(() => request(0), errors.InvalidArgumentError)
assert.throws(() => request(1), errors.InvalidArgumentError)
})
test('unreachable request rejects and can be caught', async t => {
const p = tspl(t, { plan: 1 })
request('https://thisis.not/avalid/url').catch(() => {
p.ok(1)
})
await p.completed
})
test('connect is not valid', t => {
const p = tspl(t, { plan: 1 })
p.throws(() => new Agent({ connect: false }), errors.InvalidArgumentError, 'connect must be a function or an object')
})
test('the dispatcher is truly global', t => {
const agent = getGlobalDispatcher()
assert.ok(require.resolve('../../index.js') in require.cache)
delete require.cache[require.resolve('../../index.js')]
assert.strictEqual(require.resolve('../../index.js') in require.cache, false)
const undiciFresh = require('../../index.js')
assert.ok(require.resolve('../../index.js') in require.cache)
assert.strictEqual(agent, undiciFresh.getGlobalDispatcher())
})
test('stats', async t => {
const p = tspl(t, { plan: 7 })
const wanted = 'payload'
const server = http.createServer({ joinDuplicateHeaders: true }, (req, res) => {
p.strictEqual('/', req.url)
p.strictEqual('GET', req.method)
res.end(wanted)
})
t.after(closeServerAsPromise(server))
const dispatcher = new Agent({
connect: {
servername: 'agent1'
}
})
server.listen(0, () => {
request(`http://localhost:${server.address().port}`, { dispatcher })
.then(({ statusCode, headers, body }) => {
p.strictEqual(statusCode, 200)
const originForStats = `http://localhost:${server.address().port}`
const agentStats = dispatcher.stats[originForStats]
p.strictEqual(agentStats.connected, 1)
p.strictEqual(agentStats.pending, 0)
p.strictEqual(agentStats.running, 0)
p.strictEqual(agentStats.size, 0)
})
.catch(err => {
p.fail(err)
})
})
await p.completed
})
================================================
FILE: test/node-test/async_hooks.js
================================================
'use strict'
const { test } = require('node:test')
const { Client } = require('../..')
const { createServer } = require('node:http')
const { createHook, executionAsyncId } = require('node:async_hooks')
const { readFile } = require('node:fs')
const { PassThrough } = require('node:stream')
const { tspl } = require('@matteo.collina/tspl')
const { closeServerAsPromise } = require('../utils/node-http')
const transactions = new Map()
function getCurrentTransaction () {
const asyncId = executionAsyncId()
return transactions.has(asyncId) ? transactions.get(asyncId) : null
}
function setCurrentTransaction (trans) {
const asyncId = executionAsyncId()
transactions.set(asyncId, trans)
}
const hook = createHook({
init (asyncId, type, triggerAsyncId, resource) {
if (type === 'TIMERWRAP') return
// process._rawDebug(type + ' ' + asyncId)
transactions.set(asyncId, getCurrentTransaction())
},
destroy (asyncId) {
transactions.delete(asyncId)
}
})
hook.enable()
test('async hooks', async (t) => {
const p = tspl(t, { plan: 31 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.setHeader('content-type', 'text/plain')
readFile(__filename, (err, buf) => {
p.ifError(err)
const buf1 = buf.slice(0, buf.length / 2)
const buf2 = buf.slice(buf.length / 2)
// we split the file so that it's received in 2 chunks
// and it should restore the state on the second
res.write(buf1)
setTimeout(() => {
res.end(buf2)
}, 10)
})
})
t.after(() => server.close.bind(server)())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
t.after(() => client.destroy.bind(client)())
client.request({ path: '/', method: 'GET' }, (err, { statusCode, headers, body }) => {
p.ifError(err)
body.resume()
p.deepStrictEqual(getCurrentTransaction(), null)
setCurrentTransaction({ hello: 'world2' })
client.request({ path: '/', method: 'GET' }, (err, { statusCode, headers, body }) => {
p.ifError(err)
p.deepStrictEqual(getCurrentTransaction(), { hello: 'world2' })
body.once('data', () => {
p.ok(1, 1)
body.resume()
})
body.on('end', () => {
p.ok(1, 1)
})
})
})
client.request({ path: '/', method: 'GET' }, (err, { statusCode, headers, body }) => {
p.ifError(err)
body.resume()
p.deepStrictEqual(getCurrentTransaction(), null)
setCurrentTransaction({ hello: 'world' })
client.request({ path: '/', method: 'GET' }, (err, { statusCode, headers, body }) => {
p.ifError(err)
p.deepStrictEqual(getCurrentTransaction(), { hello: 'world' })
body.once('data', () => {
p.ok(1)
body.resume()
})
body.on('end', () => {
p.ok(1)
})
})
})
client.request({ path: '/', method: 'HEAD' }, (err, { statusCode, headers, body }) => {
p.ifError(err)
body.resume()
p.deepStrictEqual(getCurrentTransaction(), null)
setCurrentTransaction({ hello: 'world' })
client.request({ path: '/', method: 'HEAD' }, (err, { statusCode, headers, body }) => {
p.ifError(err)
p.deepStrictEqual(getCurrentTransaction(), { hello: 'world' })
body.once('data', () => {
p.ok(1)
body.resume()
})
body.on('end', () => {
p.ok(1)
})
})
})
client.stream({ path: '/', method: 'GET' }, () => {
p.strictEqual(getCurrentTransaction(), null)
return new PassThrough().resume()
}, (err) => {
p.ifError(err)
p.deepStrictEqual(getCurrentTransaction(), null)
setCurrentTransaction({ hello: 'world' })
client.stream({ path: '/', method: 'GET' }, () => {
p.deepStrictEqual(getCurrentTransaction(), { hello: 'world' })
return new PassThrough().resume()
}, (err) => {
p.ifError(err)
p.deepStrictEqual(getCurrentTransaction(), { hello: 'world' })
})
})
})
await p.completed
})
test('async hooks client is destroyed', async (t) => {
const p = tspl(t, { plan: 7 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.setHeader('content-type', 'text/plain')
readFile(__filename, (err, buf) => {
p.ifError(err)
res.write('asd')
})
})
t.after(closeServerAsPromise(server))
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
t.after(client.destroy.bind(client))
client.request({ path: '/', method: 'GET' }, (err, { body }) => {
p.ifError(err)
body.resume()
body.on('error', (err) => {
p.ok(err)
})
p.deepStrictEqual(getCurrentTransaction(), null)
setCurrentTransaction({ hello: 'world2' })
client.request({ path: '/', method: 'GET' }, (err) => {
p.strictEqual(err.message, 'The client is destroyed')
p.deepStrictEqual(getCurrentTransaction(), { hello: 'world2' })
})
client.destroy((err) => {
p.ifError(err)
})
})
})
await p.completed
})
test('async hooks pipeline handler', async (t) => {
const p = tspl(t, { plan: 2 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.end('hello')
})
t.after(closeServerAsPromise(server))
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
t.after(() => { return client.close() })
setCurrentTransaction({ hello: 'world2' })
client
.pipeline({ path: '/', method: 'GET' }, ({ body }) => {
p.deepStrictEqual(getCurrentTransaction(), { hello: 'world2' })
return body
})
.on('close', () => {
p.ok(1)
})
.resume()
.end()
})
await p.completed
})
================================================
FILE: test/node-test/autoselectfamily.js
================================================
'use strict'
const { test } = require('node:test')
const dgram = require('node:dgram')
const { Resolver } = require('node:dns')
const dnsPacket = require('dns-packet')
const { createServer } = require('node:http')
const { Client, Agent, request } = require('../..')
const { tspl } = require('@matteo.collina/tspl')
/*
* IMPORTANT
*
* As only some version of Node have autoSelectFamily enabled by default (>= 20), make sure the option is always
* explicitly passed in tests in this file to avoid compatibility problems across release lines.
*
*/
const skip = false
function _lookup (resolver, hostname, options, cb) {
resolver.resolve(hostname, 'ANY', (err, replies) => {
if (err) {
return cb(err)
}
const hosts = replies
.map((r) => ({ address: r.address, family: r.type === 'AAAA' ? 6 : 4 }))
.sort((a, b) => b.family - a.family)
if (options.all === true) {
return cb(null, hosts)
}
return cb(null, hosts[0].address, hosts[0].family)
})
}
function createDnsServer (ipv6Addr, ipv4Addr, cb) {
// Create a DNS server which replies with an AAAA and an A record for the same host
const socket = dgram.createSocket('udp4')
socket.on('message', (msg, { address, port }) => {
const parsed = dnsPacket.decode(msg)
const response = dnsPacket.encode({
type: 'answer',
id: parsed.id,
questions: parsed.questions,
answers: [
{ type: 'AAAA', class: 'IN', name: 'example.org', data: '::1', ttl: 123 },
{ type: 'A', class: 'IN', name: 'example.org', data: '127.0.0.1', ttl: 123 }
]
})
socket.send(response, port, address)
})
socket.bind(0, () => {
const resolver = new Resolver()
resolver.setServers([`127.0.0.1:${socket.address().port}`])
cb(null, { dnsServer: socket, lookup: _lookup.bind(null, resolver) })
})
}
test('with autoSelectFamily enable the request succeeds when using request', { skip }, async (t) => {
const p = tspl(t, { plan: 3 })
createDnsServer('::1', '127.0.0.1', function (_, { dnsServer, lookup }) {
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.end('hello')
})
t.after(() => {
server.close()
dnsServer.close()
})
server.listen(0, '127.0.0.1', () => {
const agent = new Agent({ connect: { lookup }, autoSelectFamily: true })
request(
`http://example.org:${server.address().port}/`, {
method: 'GET',
dispatcher: agent
}, (err, { statusCode, body }) => {
p.ifError(err)
let response = Buffer.alloc(0)
body.on('data', chunk => {
response = Buffer.concat([response, chunk])
})
body.on('end', () => {
p.strictEqual(statusCode, 200)
p.strictEqual(response.toString('utf-8'), 'hello')
})
})
})
})
await p.completed
})
test('with autoSelectFamily enable the request succeeds when using a client', { skip }, async (t) => {
const p = tspl(t, { plan: 3 })
createDnsServer('::1', '127.0.0.1', function (_, { dnsServer, lookup }) {
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.end('hello')
})
t.after(() => {
server.close()
dnsServer.close()
})
server.listen(0, '127.0.0.1', () => {
const client = new Client(`http://example.org:${server.address().port}`, { connect: { lookup }, autoSelectFamily: true })
t.after(client.destroy.bind(client))
client.request({
path: '/',
method: 'GET'
}, (err, { statusCode, body }) => {
p.ifError(err)
let response = Buffer.alloc(0)
body.on('data', chunk => {
response = Buffer.concat([response, chunk])
})
body.on('end', () => {
p.strictEqual(statusCode, 200)
p.strictEqual(response.toString('utf-8'), 'hello')
})
})
})
})
await p.completed
})
test('with autoSelectFamily disabled the request fails when using request', { skip }, async (t) => {
const p = tspl(t, { plan: 1 })
createDnsServer('::1', '127.0.0.1', function (_, { dnsServer, lookup }) {
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.end('hello')
})
t.after(() => {
server.close()
dnsServer.close()
})
server.listen(0, '127.0.0.1', () => {
const agent = new Agent({ connect: { lookup, autoSelectFamily: false } })
request(`http://example.org:${server.address().port}`, {
method: 'GET',
dispatcher: agent
}, (err, { statusCode, body }) => {
p.ok(['ECONNREFUSED', 'EAFNOSUPPORT'].includes(err.code))
})
})
})
await p.completed
})
test('with autoSelectFamily disabled via Agent.Options["autoSelectFamily"] the request fails when using request', { skip }, async (t) => {
const p = tspl(t, { plan: 1 })
createDnsServer('::1', '127.0.0.1', function (_, { dnsServer, lookup }) {
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.end('hello')
})
t.after(() => {
server.close()
dnsServer.close()
})
server.listen(0, '127.0.0.1', () => {
const agent = new Agent({ autoSelectFamily: false, connect: { lookup } })
request(`http://example.org:${server.address().port}`, {
method: 'GET',
dispatcher: agent
}, (err, { statusCode, body }) => {
p.ok(['ECONNREFUSED', 'EAFNOSUPPORT'].includes(err.code))
})
})
})
await p.completed
})
test('with autoSelectFamily disabled the request fails when using a client', { skip }, async (t) => {
const p = tspl(t, { plan: 1 })
createDnsServer('::1', '127.0.0.1', function (_, { dnsServer, lookup }) {
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.end('hello')
})
t.after(() => {
server.close()
dnsServer.close()
})
server.listen(0, '127.0.0.1', () => {
const client = new Client(`http://example.org:${server.address().port}`, { connect: { lookup, autoSelectFamily: false } })
t.after(client.destroy.bind(client))
client.request({
path: '/',
method: 'GET'
}, (err, { statusCode, body }) => {
p.ok(['ECONNREFUSED', 'EAFNOSUPPORT'].includes(err.code))
})
})
})
await p.completed
})
test('with autoSelectFamily disabled via Client.Options["autoSelectFamily"] the request fails when using a client', { skip }, async (t) => {
const p = tspl(t, { plan: 1 })
createDnsServer('::1', '127.0.0.1', function (_, { dnsServer, lookup }) {
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.end('hello')
})
t.after(() => {
server.close()
dnsServer.close()
})
server.listen(0, '127.0.0.1', () => {
const client = new Client(`http://example.org:${server.address().port}`, { autoSelectFamily: false, connect: { lookup } })
t.after(client.destroy.bind(client))
client.request({
path: '/',
method: 'GET'
}, (err, { statusCode, body }) => {
p.ok(['ECONNREFUSED', 'EAFNOSUPPORT'].includes(err.code))
})
})
})
await p.completed
})
================================================
FILE: test/node-test/balanced-pool.js
================================================
'use strict'
const { describe, test } = require('node:test')
const assert = require('node:assert/strict')
const { BalancedPool, Pool, Client, errors } = require('../..')
const { createServer } = require('node:http')
const { promisify } = require('node:util')
const { tspl } = require('@matteo.collina/tspl')
test('throws when factory is not a function', (t) => {
const p = tspl(t, { plan: 2 })
try {
new BalancedPool(null, { factory: '' }) // eslint-disable-line
} catch (err) {
p.ok(err instanceof errors.InvalidArgumentError)
p.strictEqual(err.message, 'factory must be a function.')
}
})
test('add/remove upstreams', (t) => {
const p = tspl(t, { plan: 7 })
const upstream01 = 'http://localhost:1'
const upstream02 = 'http://localhost:2'
const pool = new BalancedPool()
p.deepStrictEqual(pool.upstreams, [])
// try to remove non-existent upstream
pool.removeUpstream(upstream01)
p.deepStrictEqual(pool.upstreams, [])
pool.addUpstream(upstream01)
p.deepStrictEqual(pool.upstreams, [upstream01])
// try to add the same upstream
pool.addUpstream(upstream01)
p.deepStrictEqual(pool.upstreams, [upstream01])
pool.addUpstream(upstream02)
p.deepStrictEqual(pool.upstreams, [upstream01, upstream02])
pool.removeUpstream(upstream02)
p.deepStrictEqual(pool.upstreams, [upstream01])
pool.removeUpstream(upstream01)
p.deepStrictEqual(pool.upstreams, [])
})
test('basic get', async (t) => {
const p = tspl(t, { plan: 16 })
let server1Called = 0
const server1 = createServer({ joinDuplicateHeaders: true }, (req, res) => {
server1Called++
p.strictEqual('/', req.url)
p.strictEqual('GET', req.method)
res.setHeader('content-type', 'text/plain')
res.end('hello')
})
t.after(server1.close.bind(server1))
await promisify(server1.listen).call(server1, 0)
let server2Called = 0
const server2 = createServer({ joinDuplicateHeaders: true }, (req, res) => {
server2Called++
p.strictEqual('/', req.url)
p.strictEqual('GET', req.method)
res.setHeader('content-type', 'text/plain')
res.end('hello')
})
t.after(server2.close.bind(server2))
await promisify(server2.listen).call(server2, 0)
const client = new BalancedPool()
client.addUpstream(`http://localhost:${server1.address().port}`)
client.addUpstream(`http://localhost:${server2.address().port}`)
t.after(client.destroy.bind(client))
{
const { statusCode, headers, body } = await client.request({ path: '/', method: 'GET' })
p.strictEqual(statusCode, 200)
p.strictEqual(headers['content-type'], 'text/plain')
p.strictEqual('hello', await body.text())
}
{
const { statusCode, headers, body } = await client.request({ path: '/', method: 'GET' })
p.strictEqual(statusCode, 200)
p.strictEqual(headers['content-type'], 'text/plain')
p.strictEqual('hello', await body.text())
}
p.strictEqual(server1Called, 1)
p.strictEqual(server2Called, 1)
p.strictEqual(client.destroyed, false)
p.strictEqual(client.closed, false)
await client.close()
p.strictEqual(client.destroyed, true)
p.strictEqual(client.closed, true)
})
test('connect/disconnect event(s)', async (t) => {
const clients = 2
const p = tspl(t, { plan: clients * 5 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.writeHead(200, {
Connection: 'keep-alive',
'Keep-Alive': 'timeout=1s'
})
res.end('ok')
})
t.after(server.close.bind(server))
server.listen(0, () => {
const pool = new BalancedPool(`http://localhost:${server.address().port}`, {
connections: clients,
keepAliveTimeoutThreshold: 100
})
t.after(() => pool.close.bind(pool)())
pool.on('connect', (origin, [pool, pool2, client]) => {
p.ok(client instanceof Client)
})
pool.on('disconnect', (origin, [pool, pool2, client], error) => {
p.ok(client instanceof Client)
p.ok(error instanceof errors.InformationalError)
p.strictEqual(error.code, 'UND_ERR_INFO')
})
for (let i = 0; i < clients; i++) {
pool.request({
path: '/',
method: 'GET'
}, (err, { headers, body }) => {
p.ifError(err)
body.resume()
})
}
})
await p.completed
})
test('busy', async (t) => {
const p = tspl(t, { plan: 8 * 6 + 2 + 1 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
p.strictEqual('/', req.url)
p.strictEqual('GET', req.method)
res.setHeader('content-type', 'text/plain')
res.end('hello')
})
t.after(server.close.bind(server))
server.listen(0, async () => {
const client = new BalancedPool(`http://localhost:${server.address().port}`, {
connections: 2,
pipelining: 2
})
client.on('drain', () => {
p.ok(1)
})
client.on('connect', () => {
p.ok(1)
})
t.after(client.destroy.bind(client))
for (let n = 1; n <= 8; ++n) {
client.request({ path: '/', method: 'GET' }, (err, { statusCode, headers, body }) => {
p.ifError(err)
p.strictEqual(statusCode, 200)
p.strictEqual(headers['content-type'], 'text/plain')
const bufs = []
body.on('data', (buf) => {
bufs.push(buf)
})
body.on('end', () => {
p.strictEqual('hello', Buffer.concat(bufs).toString('utf8'))
})
})
}
})
await p.completed
})
test('factory option with basic get request', async (t) => {
const p = tspl(t, { plan: 12 })
let factoryCalled = 0
const opts = {
factory: (origin, opts) => {
factoryCalled++
return new Pool(origin, opts)
}
}
const client = new BalancedPool([], opts)
let serverCalled = 0
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
serverCalled++
p.strictEqual('/', req.url)
p.strictEqual('GET', req.method)
res.setHeader('content-type', 'text/plain')
res.end('hello')
})
t.after(server.close.bind(server))
await promisify(server.listen).call(server, 0)
client.addUpstream(`http://localhost:${server.address().port}`)
p.deepStrictEqual(client.upstreams, [`http://localhost:${server.address().port}`])
t.after(client.destroy.bind(client))
{
const { statusCode, headers, body } = await client.request({ path: '/', method: 'GET' })
p.strictEqual(statusCode, 200)
p.strictEqual(headers['content-type'], 'text/plain')
p.strictEqual('hello', await body.text())
}
p.strictEqual(serverCalled, 1)
p.strictEqual(factoryCalled, 1)
p.strictEqual(client.destroyed, false)
p.strictEqual(client.closed, false)
await client.close()
p.strictEqual(client.destroyed, true)
p.strictEqual(client.closed, true)
})
test('throws when upstream is missing', async (t) => {
const p = tspl(t, { plan: 2 })
const pool = new BalancedPool()
try {
await pool.request({ path: '/', method: 'GET' })
} catch (e) {
p.ok(e instanceof errors.BalancedPoolMissingUpstreamError)
p.strictEqual(e.message, 'No upstream has been added to the BalancedPool')
}
})
test('getUpstream returns the correct Pool for given upstream', (t) => {
const p = tspl(t, { plan: 4 })
const upstream1 = 'http://localhost:3001'
const upstream2 = 'http://localhost:3002'
const pool = new BalancedPool()
pool.addUpstream(upstream1)
pool.addUpstream(upstream2)
const pool1 = pool.getUpstream(upstream1)
const pool2 = pool.getUpstream(upstream2)
p.ok(pool1 instanceof Pool)
p.ok(pool2 instanceof Pool)
const { kUrl } = require('../../lib/core/symbols')
p.strictEqual(pool1[kUrl].origin, upstream1)
p.strictEqual(pool2[kUrl].origin, upstream2)
})
test('getUpstream returns undefined for non-existent upstream', (t) => {
const p = tspl(t, { plan: 1 })
const pool = new BalancedPool()
pool.addUpstream('http://localhost:3001')
const result = pool.getUpstream('http://localhost:9999')
p.strictEqual(result, undefined)
})
test('getUpstream returns undefined for closed/destroyed upstream', (t) => {
const p = tspl(t, { plan: 2 })
const upstream = 'http://localhost:3001'
const pool = new BalancedPool()
pool.addUpstream(upstream)
const upstreamPool = pool.getUpstream(upstream)
p.ok(upstreamPool instanceof Pool)
upstreamPool.destroy()
const result = pool.getUpstream(upstream)
p.strictEqual(result, undefined)
})
class TestServer {
constructor ({ config: { server, socketHangup, downOnRequests, socketHangupOnRequests }, onRequest }) {
this.config = {
downOnRequests: downOnRequests || [],
socketHangupOnRequests: socketHangupOnRequests || [],
socketHangup
}
this.name = server
// start a server listening to any port available on the host
this.port = 0
this.iteration = 0
this.requestsCount = 0
this.onRequest = onRequest
this.server = null
}
_shouldHangupOnClient () {
if (this.config.socketHangup) {
return true
}
if (this.config.socketHangupOnRequests.includes(this.requestsCount)) {
return true
}
return false
}
_shouldStopServer () {
if (this.config.upstreamDown === true || this.config.downOnRequests.includes(this.requestsCount)) {
return true
}
return false
}
async prepareForIteration (iteration) {
// set current iteration
this.iteration = iteration
if (this._shouldStopServer()) {
await this.stop()
} else if (!this.isRunning()) {
await this.start()
}
}
start () {
this.server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
if (this._shouldHangupOnClient()) {
req.destroy(new Error('(ツ)'))
return
}
this.requestsCount++
res.end('server is running!')
this.onRequest(this)
}).listen(this.port)
this.server.keepAliveTimeout = 2000
return new Promise((resolve) => {
this.server.on('listening', () => {
// store the used port to use it again if the server was stopped as part of test and then started again
this.port = this.server.address().port
return resolve()
})
})
}
isRunning () {
return !!this.server.address()
}
stop () {
if (!this.isRunning()) {
return
}
return new Promise(resolve => {
this.server.close(() => resolve())
})
}
}
const cases = [
// 0
{
iterations: 100,
maxWeightPerServer: 100,
errorPenalty: 7,
config: [{ server: 'A' }, { server: 'B' }, { server: 'C' }],
expected: ['A', 'B', 'C', 'A', 'B', 'C', 'A', 'B', 'C', 'A', 'B', 'C'],
expectedConnectionRefusedErrors: 0,
expectedSocketErrors: 0,
expectedRatios: [0.34, 0.33, 0.33]
},
// 1
{
iterations: 100,
maxWeightPerServer: 100,
errorPenalty: 15,
config: [{ server: 'A', downOnRequests: [0] }, { server: 'B' }, { server: 'C' }],
expected: ['A/connectionRefused', 'B', 'C', 'B', 'C', 'B', 'C', 'A', 'B', 'C', 'A'],
expectedConnectionRefusedErrors: 1,
expectedSocketErrors: 0,
expectedRatios: [0.32, 0.34, 0.34]
},
// 2
{
iterations: 100,
maxWeightPerServer: 100,
errorPenalty: 15,
config: [{ server: 'A' }, { server: 'B', downOnRequests: [0] }, { server: 'C' }],
expected: ['A', 'B/connectionRefused', 'C', 'A', 'C', 'A', 'C', 'A', 'B', 'C'],
expectedConnectionRefusedErrors: 1,
expectedSocketErrors: 0,
expectedRatios: [0.34, 0.32, 0.34]
},
// 3
{
iterations: 100,
maxWeightPerServer: 100,
errorPenalty: 15,
config: [{ server: 'A' }, { server: 'B', downOnRequests: [0] }, { server: 'C', downOnRequests: [0] }],
expected: ['A', 'B/connectionRefused', 'C/connectionRefused', 'A', 'A', 'A', 'B', 'C'],
expectedConnectionRefusedErrors: 2,
expectedSocketErrors: 0,
expectedRatios: [0.35, 0.33, 0.32]
},
// 4
{
iterations: 100,
maxWeightPerServer: 100,
errorPenalty: 15,
config: [{ server: 'A', downOnRequests: [0] }, { server: 'B', downOnRequests: [0] }, { server: 'C', downOnRequests: [0] }],
expected: ['A/connectionRefused', 'B/connectionRefused', 'C/connectionRefused', 'A', 'B', 'C', 'A', 'B', 'C'],
expectedConnectionRefusedErrors: 3,
expectedSocketErrors: 0,
expectedRatios: [0.34, 0.33, 0.33]
},
// 5
{
iterations: 100,
maxWeightPerServer: 100,
errorPenalty: 15,
config: [{ server: 'A', downOnRequests: [0, 1, 2] }, { server: 'B', downOnRequests: [0, 1, 2] }, { server: 'C', downOnRequests: [0, 1, 2] }],
expected: ['A/connectionRefused', 'B/connectionRefused', 'C/connectionRefused', 'A/connectionRefused', 'B/connectionRefused', 'C/connectionRefused', 'A/connectionRefused', 'B/connectionRefused', 'C/connectionRefused', 'A', 'B', 'C', 'A', 'B', 'C'],
expectedConnectionRefusedErrors: 9,
expectedSocketErrors: 0,
expectedRatios: [0.34, 0.33, 0.33]
},
// 6
{
iterations: 100,
maxWeightPerServer: 100,
errorPenalty: 15,
config: [{ server: 'A', downOnRequests: [0] }, { server: 'B', downOnRequests: [0, 1] }, { server: 'C', downOnRequests: [0] }],
expected: ['A/connectionRefused', 'B/connectionRefused', 'C/connectionRefused', 'A', 'B/connectionRefused', 'C', 'A', 'B', 'C', 'A', 'B', 'C', 'A', 'C', 'A', 'C', 'A', 'C', 'A', 'B'],
expectedConnectionRefusedErrors: 4,
expectedSocketErrors: 0,
expectedRatios: [0.36, 0.29, 0.35]
},
// 7
{
iterations: 100,
maxWeightPerServer: 100,
errorPenalty: 15,
config: [{ server: 'A', socketHangupOnRequests: [1] }, { server: 'B' }, { server: 'C' }],
expected: ['A', 'B', 'C', 'A/socketError', 'B', 'C', 'B', 'C', 'B', 'C', 'A'],
expectedConnectionRefusedErrors: 0,
expectedSocketErrors: 1,
expectedRatios: [0.32, 0.34, 0.34]
},
// 8
{
iterations: 100,
maxWeightPerServer: 100,
errorPenalty: 7,
config: [{ server: 'A' }, { server: 'B' }, { server: 'C' }, { server: 'D' }, { server: 'E' }],
expected: ['A', 'B', 'C', 'D', 'E', 'A', 'B', 'C', 'D', 'E'],
expectedConnectionRefusedErrors: 0,
expectedSocketErrors: 0,
expectedRatios: [0.2, 0.2, 0.2, 0.2, 0.2]
},
// 9
{
iterations: 100,
maxWeightPerServer: 100,
errorPenalty: 15,
config: [{ server: 'A', downOnRequests: [0, 1, 2, 3] }, { server: 'B' }, { server: 'C' }],
expected: ['A/connectionRefused', 'B', 'C', 'B', 'C', 'B', 'C', 'A/connectionRefused', 'B', 'C', 'B', 'C', 'A/connectionRefused', 'B', 'C', 'B', 'C', 'A/connectionRefused', 'B', 'C', 'A', 'B', 'C', 'A', 'B', 'C'],
expectedConnectionRefusedErrors: 4,
expectedSocketErrors: 0,
expectedRatios: [0.18, 0.41, 0.41]
}
]
describe('weighted round robin', () => {
for (const [index, { config, expected, expectedRatios, iterations = 9, expectedConnectionRefusedErrors = 0, expectedSocketErrors = 0, maxWeightPerServer, errorPenalty = 10, skip = false }] of cases.entries()) {
test(`case ${index}`, { skip }, async (t) => {
// create an array to store successful requests
const requestLog = []
// create instances of the test servers according to the config
const servers = config.map((serverConfig) => new TestServer({
config: serverConfig,
onRequest: (server) => {
requestLog.push(server.name)
}
}))
t.after(() => servers.map(server => server.stop()))
// start all servers to get a port so that we can build the upstream urls to supply them to undici
await Promise.all(servers.map(server => server.start()))
// build upstream urls
const urls = servers.map(server => `http://localhost:${server.port}`)
// add upstreams
const client = new BalancedPool(urls[0], { maxWeightPerServer, errorPenalty, keepAliveTimeoutThreshold: 100 })
urls.slice(1).map(url => client.addUpstream(url))
let connectionRefusedErrors = 0
let socketErrors = 0
for (let i = 0; i < iterations; i++) {
// setup test servers for the next iteration
await Promise.all(servers.map(server => server.prepareForIteration(i)))
// send a request using undici
try {
await client.request({ path: '/', method: 'GET' })
} catch (e) {
const serverWithError =
servers.find(server => server.port === e.port) ||
servers.find(server => {
if (typeof AggregateError === 'function' && e instanceof AggregateError) {
return e.errors.some(e => server.port === (e.socket?.remotePort ?? e.port))
}
return server.port === e.socket.remotePort
})
serverWithError.requestsCount++
if (e.code === 'ECONNREFUSED') {
requestLog.push(`${serverWithError.name}/connectionRefused`)
connectionRefusedErrors++
}
if (e.code === 'UND_ERR_SOCKET') {
requestLog.push(`${serverWithError.name}/socketError`)
socketErrors++
}
}
}
const totalRequests = servers.reduce((acc, server) => {
return acc + server.requestsCount
}, 0)
assert.strictEqual(totalRequests, iterations)
assert.strictEqual(connectionRefusedErrors, expectedConnectionRefusedErrors)
assert.strictEqual(socketErrors, expectedSocketErrors)
if (expectedRatios) {
const ratios = servers.reduce((acc, el) => {
acc[el.name] = 0
return acc
}, {})
requestLog.map(el => ratios[el[0]]++)
assert.deepStrictEqual(Object.keys(ratios).map(k => ratios[k] / iterations), expectedRatios)
}
if (expected) {
assert.deepStrictEqual(requestLog.slice(0, expected.length), expected)
}
await client.close()
})
}
})
test('should not be vulnerable to __proto__ pollution via options', async (t) => {
const { EventEmitter } = require('node:events')
let capturedOpts
// Simulate attacker-controlled options with __proto__ property
const attackerOptions = JSON.parse(`
{
"__proto__": {
"polluted": "YES",
"connections": 1
}
}
`)
attackerOptions.factory = (origin, opts) => {
capturedOpts = opts
const stub = new EventEmitter()
stub.dispatch = () => true
stub.close = () => Promise.resolve()
stub.destroy = () => Promise.resolve()
stub.destroyed = false
stub.closed = false
return stub
}
new BalancedPool(['http://localhost/'], attackerOptions) // eslint-disable-line no-new
// Verify that the captured options do not have polluted prototype
assert.strictEqual(capturedOpts.polluted, undefined, 'polluted property should not exist on options')
assert.strictEqual(Object.getPrototypeOf(capturedOpts).polluted, undefined, 'prototype should not be polluted')
assert.strictEqual({}.polluted, undefined, 'global Object.prototype should not be polluted')
})
================================================
FILE: test/node-test/ca-fingerprint.js
================================================
'use strict'
const crypto = require('node:crypto')
const https = require('node:https')
const { test } = require('node:test')
const { Client, buildConnector } = require('../..')
const pem = require('@metcoder95/https-pem')
const { tspl } = require('@matteo.collina/tspl')
const caFingerprint = getFingerprint(pem.cert.toString()
.split('\n')
.slice(1, -1)
.map(line => line.trim())
.join('')
)
test('Validate CA fingerprint with a custom connector', async t => {
const p = tspl(t, { plan: 2 })
const server = https.createServer({ ...pem, joinDuplicateHeaders: true }, (req, res) => {
res.setHeader('Content-Type', 'text/plain')
res.end('hello')
})
server.listen(0, function () {
const connector = buildConnector({ rejectUnauthorized: false })
const client = new Client(`https://localhost:${server.address().port}`, {
connect (opts, cb) {
connector(opts, (err, socket) => {
if (err) {
cb(err)
} else if (getIssuerCertificate(socket).fingerprint256 !== caFingerprint) {
socket.destroy()
cb(new Error('Fingerprint does not match'))
} else {
cb(null, socket)
}
})
}
})
t.after(() => {
client.close()
server.close()
})
client.request({
path: '/',
method: 'GET'
}, (err, data) => {
p.ifError(err)
data.body
.resume()
.on('end', () => {
p.ok(1)
})
})
})
await p.completed
})
test('Bad CA fingerprint with a custom connector', async t => {
const p = tspl(t, { plan: 2 })
const server = https.createServer({ ...pem, joinDuplicateHeaders: true }, (req, res) => {
res.setHeader('Content-Type', 'text/plain')
res.end('hello')
})
server.listen(0, function () {
const connector = buildConnector({ rejectUnauthorized: false })
const client = new Client(`https://localhost:${server.address().port}`, {
connect (opts, cb) {
connector(opts, (err, socket) => {
if (err) {
cb(err)
} else if (getIssuerCertificate(socket).fingerprint256 !== 'FO:OB:AR') {
socket.destroy()
cb(new Error('Fingerprint does not match'))
} else {
cb(null, socket)
}
})
}
})
t.after(() => {
client.close()
server.close()
})
client.request({
path: '/',
method: 'GET'
}, (err, data) => {
p.strictEqual(err.message, 'Fingerprint does not match')
p.strictEqual(data.body, undefined)
})
})
await p.completed
})
function getIssuerCertificate (socket) {
let certificate = socket.getPeerCertificate(true)
while (certificate && Object.keys(certificate).length > 0) {
// invalid certificate
if (certificate.issuerCertificate == null) {
return null
}
// We have reached the root certificate.
// In case of self-signed certificates, `issuerCertificate` may be a circular reference.
if (certificate.fingerprint256 === certificate.issuerCertificate.fingerprint256) {
break
}
// continue the loop
certificate = certificate.issuerCertificate
}
return certificate
}
function getFingerprint (content, inputEncoding = 'base64', outputEncoding = 'hex') {
const shasum = crypto.createHash('sha256')
shasum.update(content, inputEncoding)
const res = shasum.digest(outputEncoding)
return res.toUpperCase().match(/.{1,2}/g).join(':')
}
================================================
FILE: test/node-test/client-abort.js
================================================
'use strict'
const { test } = require('node:test')
const { Client, errors } = require('../..')
const { createServer } = require('node:http')
const { Readable } = require('node:stream')
const { tspl } = require('@matteo.collina/tspl')
class OnAbortError extends Error {}
test('aborted response errors', async (t) => {
const p = tspl(t, { plan: 3 })
const server = createServer({ joinDuplicateHeaders: true })
server.once('request', (req, res) => {
// TODO: res.write will cause body to emit 'error' twice
// due to bug in readable-stream.
res.end('asd')
})
t.after(server.close.bind(server))
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
t.after(client.destroy.bind(client))
client.request({ path: '/', method: 'GET' }, (err, { statusCode, headers, body }) => {
p.ifError(err)
body.destroy()
body
.on('error', err => {
p.ok(err instanceof errors.RequestAbortedError)
})
.on('close', () => {
p.ok(1)
})
})
})
await p.completed
})
test('aborted req', async (t) => {
const p = tspl(t, { plan: 1 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.end(Buffer.alloc(4 + 1, 'a'))
})
t.after(server.close.bind(server))
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
t.after(client.destroy.bind(client))
client.request({
method: 'POST',
path: '/',
body: new Readable({
read () {
setImmediate(() => {
this.destroy()
})
}
})
}, (err) => {
p.ok(err instanceof errors.RequestAbortedError)
})
})
await p.completed
})
test('abort', async (t) => {
const p = tspl(t, { plan: 2 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.end()
})
t.after(server.close.bind(server))
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
t.after(client.destroy.bind(client))
client.dispatch({
method: 'GET',
path: '/'
}, {
onConnect (abort) {
setImmediate(abort)
},
onHeaders () {
p.ok(0)
},
onData () {
p.ok(0)
},
onComplete () {
p.ok(0)
},
onError (err) {
p.ok(err instanceof errors.RequestAbortedError)
}
})
client.on('disconnect', () => {
p.ok(1)
})
})
await p.completed
})
test('abort pipelined', async (t) => {
const p = tspl(t, { plan: 6 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
})
t.after(server.close.bind(server))
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`, {
pipelining: 2
})
t.after(client.destroy.bind(client))
let counter = 0
client.dispatch({
method: 'GET',
path: '/',
blocking: false
}, {
onConnect (abort) {
// This request will be retried
if (counter++ === 1) {
abort()
}
p.ok(1)
},
onHeaders () {
p.ok(0)
},
onData () {
p.ok(0)
},
onComplete () {
p.ok(0)
},
onError (err) {
p.ok(err instanceof errors.RequestAbortedError)
}
})
client.dispatch({
method: 'GET',
path: '/',
blocking: false
}, {
onConnect (abort) {
abort()
},
onHeaders () {
p.ok(0)
},
onData () {
p.ok(0)
},
onComplete () {
p.ok(0)
},
onError (err) {
p.ok(err instanceof errors.RequestAbortedError)
}
})
client.on('disconnect', () => {
p.ok(1)
})
})
await p.completed
})
test('propagate unallowed throws in request.onError', async (t) => {
const p = tspl(t, { plan: 2 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.end()
})
t.after(server.close.bind(server))
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
t.after(client.destroy.bind(client))
client.dispatch({
method: 'GET',
path: '/'
}, {
onConnect (abort) {
setImmediate(abort)
},
onHeaders () {
p.ok(0)
},
onData () {
p.ok(0)
},
onComplete () {
p.ok(0)
},
onError () {
throw new OnAbortError('error')
}
})
client.on('error', (err) => {
p.ok(err instanceof OnAbortError)
})
client.on('disconnect', () => {
p.ok(1)
})
})
await p.completed
})
================================================
FILE: test/node-test/client-connect.js
================================================
'use strict'
const { test } = require('node:test')
const { Client, errors } = require('../..')
const http = require('node:http')
const EE = require('node:events')
const { kBusy } = require('../../lib/core/symbols')
const { tspl } = require('@matteo.collina/tspl')
const { closeServerAsPromise } = require('../utils/node-http')
test('basic connect', async (t) => {
const p = tspl(t, { plan: 3 })
const server = http.createServer({ joinDuplicateHeaders: true }, (c) => {
p.ok(0)
})
server.on('connect', (req, socket, firstBodyChunk) => {
socket.write('HTTP/1.1 200 Connection established\r\n\r\n')
let data = firstBodyChunk.toString()
socket.on('data', (buf) => {
data += buf.toString()
})
socket.on('end', () => {
socket.end(data)
})
})
t.after(closeServerAsPromise(server))
server.listen(0, async () => {
const client = new Client(`http://localhost:${server.address().port}`)
t.after(() => { return client.close() })
const signal = new EE()
const promise = client.connect({
signal,
path: '/'
})
p.strictEqual(signal.listenerCount('abort'), 1)
const { socket } = await promise
p.strictEqual(signal.listenerCount('abort'), 0)
let recvData = ''
socket.on('data', (d) => {
recvData += d
})
socket.on('end', () => {
p.strictEqual(recvData.toString(), 'Body')
})
socket.write('Body')
socket.end()
})
await p.completed
})
test('connect error', async (t) => {
const p = tspl(t, { plan: 1 })
const server = http.createServer({ joinDuplicateHeaders: true }, (c) => {
p.ok(0)
})
server.on('connect', (req, socket, firstBodyChunk) => {
socket.destroy()
})
t.after(closeServerAsPromise(server))
server.listen(0, async () => {
const client = new Client(`http://localhost:${server.address().port}`)
t.after(() => { return client.close() })
try {
await client.connect({
path: '/'
})
} catch (err) {
p.ok(err)
}
})
await p.completed
})
test('connect invalid opts', (t) => {
const p = tspl(t, { plan: 6 })
const client = new Client('http://localhost:5432')
client.connect(null, err => {
p.ok(err instanceof errors.InvalidArgumentError)
p.strictEqual(err.message, 'invalid opts')
})
try {
client.connect(null, null)
p.ok(0)
} catch (err) {
p.ok(err instanceof errors.InvalidArgumentError)
p.strictEqual(err.message, 'invalid opts')
}
try {
client.connect({ path: '/' }, null)
p.ok(0)
} catch (err) {
p.ok(err instanceof errors.InvalidArgumentError)
p.strictEqual(err.message, 'invalid callback')
}
})
test('connect wait for empty pipeline', async (t) => {
const p = tspl(t, { plan: 7 })
let canConnect = false
const server = http.createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.end()
canConnect = true
})
server.on('connect', (req, socket, firstBodyChunk) => {
p.strictEqual(canConnect, true)
socket.write('HTTP/1.1 200 Connection established\r\n\r\n')
let data = firstBodyChunk.toString()
socket.on('data', (buf) => {
data += buf.toString()
})
socket.on('end', () => {
socket.end(data)
})
})
t.after(closeServerAsPromise(server))
server.listen(0, async () => {
const client = new Client(`http://localhost:${server.address().port}`, {
pipelining: 3
})
t.after(() => { return client.close() })
client.request({
path: '/',
method: 'GET',
blocking: false
}, (err) => {
p.ifError(err)
})
client.once('connect', () => {
process.nextTick(() => {
p.strictEqual(client[kBusy], false)
client.connect({
path: '/'
}, (err, { socket }) => {
p.ifError(err)
let recvData = ''
socket.on('data', (d) => {
recvData += d
})
socket.on('end', () => {
p.strictEqual(recvData.toString(), 'Body')
})
socket.write('Body')
socket.end()
})
p.strictEqual(client[kBusy], true)
client.request({
path: '/',
method: 'GET'
}, (err) => {
p.ifError(err)
})
})
})
})
await p.completed
})
test('connect aborted', async (t) => {
const p = tspl(t, { plan: 6 })
const server = http.createServer({ joinDuplicateHeaders: true }, (req, res) => {
p.ok(0)
})
server.on('connect', (req, c, firstBodyChunk) => {
p.ok(0)
})
t.after(closeServerAsPromise(server))
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`, {
pipelining: 3
})
t.after(() => {
client.destroy()
})
const signal = new EE()
client.connect({
path: '/',
signal,
opaque: 'asd'
}, (err, { opaque }) => {
p.strictEqual(opaque, 'asd')
p.strictEqual(signal.listenerCount('abort'), 0)
p.ok(err instanceof errors.RequestAbortedError)
})
p.strictEqual(client[kBusy], true)
p.strictEqual(signal.listenerCount('abort'), 1)
signal.emit('abort')
client.close(() => {
p.ok(1)
})
})
await p.completed
})
test('basic connect error', async (t) => {
const p = tspl(t, { plan: 2 })
const server = http.createServer({ joinDuplicateHeaders: true }, (c) => {
p.ok(0)
})
server.on('connect', (req, socket, firstBodyChunk) => {
socket.write('HTTP/1.1 200 Connection established\r\n\r\n')
let data = firstBodyChunk.toString()
socket.on('data', (buf) => {
data += buf.toString()
})
socket.on('end', () => {
socket.end(data)
})
})
t.after(closeServerAsPromise(server))
server.listen(0, async () => {
const client = new Client(`http://localhost:${server.address().port}`)
t.after(() => { return client.close() })
const _err = new Error()
client.connect({
path: '/'
}, (err, { socket }) => {
p.ifError(err)
socket.on('error', (err) => {
p.strictEqual(err, _err)
})
throw _err
})
})
await p.completed
})
test('connect invalid signal', async (t) => {
const p = tspl(t, { plan: 2 })
const server = http.createServer({ joinDuplicateHeaders: true }, (req, res) => {
p.ok(0)
})
server.on('connect', (req, c, firstBodyChunk) => {
p.ok(0)
})
t.after(closeServerAsPromise(server))
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
t.after(client.destroy.bind(client))
client.on('disconnect', () => {
p.ok(0)
})
client.connect({
path: '/',
signal: 'error',
opaque: 'asd'
}, (err, { opaque }) => {
p.strictEqual(opaque, 'asd')
p.ok(err instanceof errors.InvalidArgumentError)
})
})
await p.completed
})
================================================
FILE: test/node-test/client-dispatch.js
================================================
'use strict'
const { test } = require('node:test')
const assert = require('node:assert/strict')
const http = require('node:http')
const https = require('node:https')
const { Client, Pool, errors } = require('../..')
const stream = require('node:stream')
const { createSecureServer } = require('node:http2')
const pem = require('@metcoder95/https-pem')
const { tspl } = require('@matteo.collina/tspl')
const { closeServerAsPromise, closeClientAndServerAsPromise } = require('../utils/node-http')
test('dispatch invalid opts', (t) => {
const p = tspl(t, { plan: 14 })
const client = new Client('http://localhost:5000')
try {
client.dispatch({
path: '/',
method: 'GET',
upgrade: 1
}, null)
} catch (err) {
p.ok(err instanceof errors.InvalidArgumentError)
p.strictEqual(err.message, 'handler must be an object')
}
try {
client.dispatch({
path: '/',
method: 'GET',
upgrade: 1
}, 'asd')
} catch (err) {
p.ok(err instanceof errors.InvalidArgumentError)
p.strictEqual(err.message, 'handler must be an object')
}
client.dispatch({
path: '/',
method: 'GET',
upgrade: 1
}, {
onError (err) {
p.ok(err instanceof errors.InvalidArgumentError)
p.strictEqual(err.message, 'upgrade must be a string')
}
})
client.dispatch({
path: '/',
method: 'GET',
headersTimeout: 'asd'
}, {
onError (err) {
p.ok(err instanceof errors.InvalidArgumentError)
p.strictEqual(err.message, 'invalid headersTimeout')
}
})
client.dispatch({
path: '/',
method: 'GET',
bodyTimeout: 'asd'
}, {
onError (err) {
p.ok(err instanceof errors.InvalidArgumentError)
p.strictEqual(err.message, 'invalid bodyTimeout')
}
})
client.dispatch({
origin: 'another',
path: '/',
method: 'GET',
bodyTimeout: 'asd'
}, {
onError (err) {
p.ok(err instanceof errors.InvalidArgumentError)
p.strictEqual(err.message, 'invalid bodyTimeout')
}
})
client.dispatch(null, {
onError (err) {
p.ok(err instanceof errors.InvalidArgumentError)
p.strictEqual(err.message, 'opts must be an object.')
}
})
})
test('basic dispatch get', async (t) => {
const p = tspl(t, { plan: 11 })
const server = http.createServer({ joinDuplicateHeaders: true }, (req, res) => {
p.strictEqual('/', req.url)
p.strictEqual('GET', req.method)
p.strictEqual(`localhost:${server.address().port}`, req.headers.host)
p.strictEqual(undefined, req.headers.foo)
p.strictEqual('bar', req.headers.bar)
p.strictEqual('', req.headers.baz)
p.strictEqual(undefined, req.headers['content-length'])
res.end('hello')
})
t.after(closeServerAsPromise(server))
const reqHeaders = {
foo: undefined,
bar: 'bar',
baz: null
}
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
t.after(() => { return client.close() })
const bufs = []
client.dispatch({
path: '/',
method: 'GET',
headers: reqHeaders
}, {
onConnect () {
},
onHeaders (statusCode, headers) {
p.strictEqual(statusCode, 200)
p.strictEqual(Array.isArray(headers), true)
},
onData (buf) {
bufs.push(buf)
},
onComplete (trailers) {
p.deepStrictEqual(trailers, [])
p.strictEqual('hello', Buffer.concat(bufs).toString('utf8'))
},
onError () {
p.ok(0)
}
})
})
await p.completed
})
test('trailers dispatch get', async (t) => {
const p = tspl(t, { plan: 12 })
const server = http.createServer({ joinDuplicateHeaders: true }, (req, res) => {
p.strictEqual('/', req.url)
p.strictEqual('GET', req.method)
p.strictEqual(`localhost:${server.address().port}`, req.headers.host)
p.strictEqual(undefined, req.headers.foo)
p.strictEqual('bar', req.headers.bar)
p.strictEqual(undefined, req.headers['content-length'])
res.addTrailers({ 'Content-MD5': 'test' })
res.setHeader('Content-Type', 'text/plain')
res.setHeader('Trailer', 'Content-MD5')
res.end('hello')
})
t.after(closeServerAsPromise(server))
const reqHeaders = {
foo: undefined,
bar: 'bar'
}
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
t.after(() => { return client.close() })
const bufs = []
client.dispatch({
path: '/',
method: 'GET',
headers: reqHeaders
}, {
onConnect () {
},
onHeaders (statusCode, headers) {
p.strictEqual(statusCode, 200)
p.strictEqual(Array.isArray(headers), true)
{
const contentTypeIdx = headers.findIndex(x => x.toString() === 'Content-Type')
p.strictEqual(headers[contentTypeIdx + 1].toString(), 'text/plain')
}
},
onData (buf) {
bufs.push(buf)
},
onComplete (trailers) {
p.strictEqual(Array.isArray(trailers), true)
{
const contentMD5Idx = trailers.findIndex(x => x.toString() === 'Content-MD5')
p.strictEqual(trailers[contentMD5Idx + 1].toString(), 'test')
}
p.strictEqual('hello', Buffer.concat(bufs).toString('utf8'))
},
onError () {
p.ok(0)
}
})
})
await p.completed
})
test('dispatch onHeaders error', async (t) => {
const p = tspl(t, { plan: 1 })
const server = http.createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.end()
})
t.after(closeServerAsPromise(server))
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
t.after(() => { return client.close() })
const _err = new Error()
client.dispatch({
path: '/',
method: 'GET'
}, {
onConnect () {
},
onHeaders (statusCode, headers) {
throw _err
},
onData (buf) {
p.ok(0)
},
onComplete (trailers) {
p.ok(0)
},
onError (err) {
p.strictEqual(err, _err)
}
})
})
await p.completed
})
test('dispatch onComplete error', async (t) => {
const p = tspl(t, { plan: 2 })
const server = http.createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.end()
})
t.after(closeServerAsPromise(server))
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
t.after(() => { return client.close() })
const _err = new Error()
client.dispatch({
path: '/',
method: 'GET'
}, {
onConnect () {
},
onHeaders (statusCode, headers) {
p.ok(1)
},
onData (buf) {
p.ok(0)
},
onComplete (trailers) {
throw _err
},
onError (err) {
p.strictEqual(err, _err)
}
})
})
await p.completed
})
test('dispatch onData error', async (t) => {
const p = tspl(t, { plan: 2 })
const server = http.createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.end('ad')
})
t.after(closeServerAsPromise(server))
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
t.after(() => { return client.close() })
const _err = new Error()
client.dispatch({
path: '/',
method: 'GET'
}, {
onConnect () {
},
onHeaders (statusCode, headers) {
p.ok(1)
},
onData (buf) {
throw _err
},
onComplete (trailers) {
p.ok(0)
},
onError (err) {
p.strictEqual(err, _err)
}
})
})
await p.completed
})
test('dispatch onConnect error', async (t) => {
const p = tspl(t, { plan: 1 })
const server = http.createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.end('ad')
})
t.after(closeServerAsPromise(server))
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
t.after(() => { return client.close() })
const _err = new Error()
client.dispatch({
path: '/',
method: 'GET'
}, {
onConnect () {
throw _err
},
onHeaders (statusCode, headers) {
p.ok(0)
},
onData (buf) {
p.ok(0)
},
onComplete (trailers) {
p.ok(0)
},
onError (err) {
p.strictEqual(err, _err)
}
})
})
await p.completed
})
test('connect call onUpgrade once', async (t) => {
const p = tspl(t, { plan: 2 })
const server = http.createServer({ joinDuplicateHeaders: true }, (c) => {
p.ok(0)
})
server.on('connect', (req, socket, firstBodyChunk) => {
socket.write('HTTP/1.1 200 Connection established\r\n\r\n')
let data = firstBodyChunk.toString()
socket.on('data', (buf) => {
data += buf.toString()
})
socket.on('end', () => {
socket.end(data)
})
})
t.after(closeServerAsPromise(server))
server.listen(0, async () => {
const client = new Client(`http://localhost:${server.address().port}`)
t.after(() => { return client.close() })
let recvData = ''
let count = 0
client.dispatch({
method: 'CONNECT',
path: '/'
}, {
onConnect () {
},
onHeaders (statusCode, headers) {
t.ok(true, 'should not throw')
},
onUpgrade (statusCode, headers, socket) {
p.strictEqual(count++, 0)
socket.on('data', (d) => {
recvData += d
})
socket.on('end', () => {
p.strictEqual(recvData.toString(), 'Body')
})
socket.write('Body')
socket.end()
},
onData (buf) {
p.ok(0)
},
onComplete (trailers) {
p.ok(0)
},
onError () {
p.ok(0)
}
})
})
await p.completed
})
test('dispatch onHeaders missing', async (t) => {
const p = tspl(t, { plan: 1 })
const server = http.createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.end('ad')
})
t.after(closeServerAsPromise(server))
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
t.after(() => { return client.close() })
client.dispatch({
path: '/',
method: 'GET'
}, {
onConnect () {
},
onData (buf) {
p.ok(0, 'should not throw')
},
onComplete (trailers) {
p.ok(0, 'should not throw')
},
onError (err) {
p.strictEqual(err.code, 'UND_ERR_INVALID_ARG')
}
})
})
await p.completed
})
test('dispatch onData missing', async (t) => {
const p = tspl(t, { plan: 1 })
const server = http.createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.end('ad')
})
t.after(closeServerAsPromise(server))
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
t.after(() => { return client.close() })
client.dispatch({
path: '/',
method: 'GET'
}, {
onConnect () {
},
onHeaders (statusCode, headers) {
p.ok(0, 'should not throw')
},
onComplete (trailers) {
p.ok(0, 'should not throw')
},
onError (err) {
p.strictEqual(err.code, 'UND_ERR_INVALID_ARG')
}
})
})
await p.completed
})
test('dispatch onComplete missing', async (t) => {
const p = tspl(t, { plan: 1 })
const server = http.createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.end('ad')
})
t.after(closeServerAsPromise(server))
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
t.after(() => { return client.close() })
client.dispatch({
path: '/',
method: 'GET'
}, {
onConnect () {
},
onHeaders (statusCode, headers) {
p.ok(0)
},
onData (buf) {
p.ok(0)
},
onError (err) {
p.strictEqual(err.code, 'UND_ERR_INVALID_ARG')
}
})
})
await p.completed
})
test('dispatch onError missing', async (t) => {
const p = tspl(t, { plan: 1 })
const server = http.createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.end('ad')
})
t.after(closeServerAsPromise(server))
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
t.after(() => { return client.close() })
try {
client.dispatch({
path: '/',
method: 'GET'
}, {
onConnect () {
},
onHeaders (statusCode, headers) {
p.ok(0)
},
onData (buf) {
p.ok(0)
},
onComplete (trailers) {
p.ok(0)
}
})
} catch (err) {
p.strictEqual(err.code, 'UND_ERR_INVALID_ARG')
}
})
await p.completed
})
test('dispatch CONNECT onUpgrade missing', async (t) => {
const p = tspl(t, { plan: 2 })
const server = http.createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.end('ad')
})
t.after(closeServerAsPromise(server))
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
t.after(() => client.destroy.bind(client)())
client.dispatch({
path: '/',
method: 'GET',
upgrade: 'Websocket'
}, {
onConnect () {
},
onHeaders (statusCode, headers) {
},
onError (err) {
p.strictEqual(err.code, 'UND_ERR_INVALID_ARG')
p.strictEqual(err.message, 'invalid onUpgrade method')
}
})
})
await p.completed
})
test('dispatch upgrade onUpgrade missing', async (t) => {
const p = tspl(t, { plan: 2 })
const server = http.createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.end('ad')
})
t.after(closeServerAsPromise(server))
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
t.after(() => { return client.close() })
client.dispatch({
path: '/',
method: 'GET',
upgrade: 'Websocket'
}, {
onConnect () {
},
onHeaders (statusCode, headers) {
},
onError (err) {
p.strictEqual(err.code, 'UND_ERR_INVALID_ARG')
p.strictEqual(err.message, 'invalid onUpgrade method')
}
})
})
await p.completed
})
test('dispatch pool onError missing', async (t) => {
const p = tspl(t, { plan: 2 })
const server = http.createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.end('ad')
})
t.after(closeServerAsPromise(server))
server.listen(0, () => {
const client = new Pool(`http://localhost:${server.address().port}`)
t.after(() => { return client.close() })
try {
client.dispatch({
path: '/',
method: 'GET',
upgrade: 1
}, {
})
} catch (err) {
p.strictEqual(err.code, 'UND_ERR_INVALID_ARG')
p.strictEqual(err.message, 'upgrade must be a string')
}
})
await p.completed
})
test('dispatch onBodySent not a function', async (t) => {
const p = tspl(t, { plan: 2 })
const server = http.createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.end('ad')
})
t.after(closeServerAsPromise(server))
server.listen(0, () => {
const client = new Pool(`http://localhost:${server.address().port}`)
t.after(() => { return client.close() })
client.dispatch({
path: '/',
method: 'GET'
}, {
onBodySent: '42',
onConnect () {},
onHeaders () {},
onData () {},
onError (err) {
p.strictEqual(err.code, 'UND_ERR_INVALID_ARG')
p.strictEqual(err.message, 'invalid onBodySent method')
}
})
})
await p.completed
})
test('dispatch onBodySent buffer', async (t) => {
const p = tspl(t, { plan: 3 })
const server = http.createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.end('ad')
})
t.after(closeServerAsPromise(server))
server.listen(0, () => {
const client = new Pool(`http://localhost:${server.address().port}`)
t.after(() => { return client.close() })
const body = 'hello 🚀'
client.dispatch({
path: '/',
method: 'POST',
body
}, {
onBodySent (chunk) {
p.strictEqual(chunk.toString(), body)
},
onRequestSent () {
p.ok(1)
},
onError (err) {
throw err
},
onConnect () {},
onHeaders () {},
onData () {},
onComplete () {
p.ok(1)
}
})
})
await p.completed
})
test('dispatch onBodySent stream', async (t) => {
const p = tspl(t, { plan: 8 })
const server = http.createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.end('ad')
})
t.after(closeServerAsPromise(server))
const chunks = ['he', 'llo', 'world', '🚀']
const toSendBytes = chunks.reduce((a, b) => a + Buffer.byteLength(b), 0)
const body = stream.Readable.from(chunks)
server.listen(0, () => {
const client = new Pool(`http://localhost:${server.address().port}`)
t.after(() => { return client.close() })
let sentBytes = 0
let currentChunk = 0
client.dispatch({
path: '/',
method: 'POST',
body
}, {
onBodySent (chunk) {
p.strictEqual(chunks[currentChunk++], chunk)
sentBytes += Buffer.byteLength(chunk)
},
onRequestSent () {
p.ok(1)
},
onError (err) {
throw err
},
onConnect () {},
onHeaders () {},
onData () {},
onComplete () {
p.strictEqual(currentChunk, chunks.length)
p.strictEqual(sentBytes, toSendBytes)
p.ok(1)
}
})
})
await p.completed
})
test('dispatch onBodySent async-iterable', (t, done) => {
const server = http.createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.end('ad')
})
t.after(closeServerAsPromise(server))
const chunks = ['he', 'llo', 'world', '🚀']
const toSendBytes = chunks.reduce((a, b) => a + Buffer.byteLength(b), 0)
server.listen(0, () => {
const client = new Pool(`http://localhost:${server.address().port}`)
t.after(() => { return client.close() })
let sentBytes = 0
let currentChunk = 0
client.dispatch({
path: '/',
method: 'POST',
body: chunks
}, {
onBodySent (chunk) {
assert.strictEqual(chunks[currentChunk++], chunk)
sentBytes += Buffer.byteLength(chunk)
},
onError (err) {
throw err
},
onConnect () {},
onHeaders () {},
onData () {},
onComplete () {
assert.strictEqual(currentChunk, chunks.length)
assert.strictEqual(sentBytes, toSendBytes)
done()
}
})
})
})
test('dispatch onBodySent throws error', (t, done) => {
const server = http.createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.end('ended')
})
t.after(closeServerAsPromise(server))
server.listen(0, () => {
const client = new Pool(`http://localhost:${server.address().port}`)
t.after(() => { return client.close() })
const body = 'hello'
client.dispatch({
path: '/',
method: 'POST',
body
}, {
onBodySent (chunk) {
throw new Error('fail')
},
onError (err) {
assert.ok(err instanceof Error)
assert.strictEqual(err.message, 'fail')
done()
},
onConnect () {},
onHeaders () {},
onData () {},
onComplete () {}
})
})
})
test('dispatches in expected order', async (t) => {
const server = http.createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.end('ended')
})
t.after(closeServerAsPromise(server))
const p = tspl(t, { plan: 1 })
server.listen(0, () => {
const client = new Pool(`http://localhost:${server.address().port}`)
t.after(() => { return client.close() })
const dispatches = []
client.dispatch({
path: '/',
method: 'POST',
body: 'body'
}, {
onConnect () {
dispatches.push('onConnect')
},
onBodySent () {
dispatches.push('onBodySent')
},
onResponseStarted () {
dispatches.push('onResponseStarted')
},
onHeaders () {
dispatches.push('onHeaders')
},
onData () {
dispatches.push('onData')
},
onComplete () {
dispatches.push('onComplete')
p.deepStrictEqual(dispatches, ['onConnect', 'onBodySent', 'onResponseStarted', 'onHeaders', 'onData', 'onComplete'])
},
onError (err) {
p.ifError(err)
}
})
})
await p.completed
})
test('onResponseStarted is called with interceptor', async (t) => {
const server = http.createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.end('ended')
})
t.after(closeServerAsPromise(server))
const p = tspl(t, { plan: 2 })
server.listen(0, () => {
const pool = new Pool(`http://localhost:${server.address().port}`)
const client = pool.compose((dispatch) => (opts, handler) => dispatch(opts, handler))
t.after(() => { return pool.close() })
let responseStartedCalled = false
client.dispatch({
path: '/',
method: 'GET'
}, {
onConnect () {},
onResponseStarted () {
responseStartedCalled = true
},
onHeaders () {},
onData () {},
onComplete () {
p.strictEqual(responseStartedCalled, true)
p.ok(true)
},
onError (err) {
p.ifError(err)
}
})
})
await p.completed
})
test('dispatches in expected order for http2', async (t) => {
const server = createSecureServer(pem)
server.on('stream', (stream) => {
stream.respond({
'content-type': 'text/plain; charset=utf-8',
':status': 200
})
stream.end('ended')
})
const p = tspl(t, { plan: 1 })
server.listen(0, () => {
const client = new Pool(`https://localhost:${server.address().port}`, {
connect: {
rejectUnauthorized: false
},
allowH2: true
})
t.after(closeClientAndServerAsPromise(client, server))
const dispatches = []
client.dispatch({
path: '/',
method: 'POST',
body: 'body'
}, {
onConnect () {
dispatches.push('onConnect')
},
onBodySent () {
dispatches.push('onBodySent')
},
onResponseStarted () {
dispatches.push('onResponseStarted')
},
onHeaders () {
dispatches.push('onHeaders')
},
onData () {
dispatches.push('onData')
},
onComplete () {
dispatches.push('onComplete')
p.deepStrictEqual(dispatches, ['onConnect', 'onBodySent', 'onResponseStarted', 'onHeaders', 'onData', 'onComplete'])
},
onError (err) {
p.ifError(err)
}
})
})
await p.completed
})
test('Issue#3065 - fix bad destroy handling', async (t) => {
const p = tspl(t, { plan: 4 })
const server = https.createServer({ ...pem, joinDuplicateHeaders: true }, (req, res) => {
res.writeHead(200, { 'content-type': 'text/plain' })
res.end('ended')
})
server.listen(0, () => {
const client = new Client(`https://localhost:${server.address().port}`, {
connect: {
rejectUnauthorized: false
}
})
t.after(closeClientAndServerAsPromise(client, server))
const dispatches = []
const dispatches2 = []
client.once('disconnect', (...args) => {
const [,, err] = args
p.strictEqual(err.code, 'UND_ERR_INFO')
p.strictEqual(err.message, 'servername changed')
})
client.dispatch({
path: '/',
method: 'POST',
body: 'body'
}, {
onConnect () {
dispatches.push('onConnect')
},
onBodySent () {
dispatches.push('onBodySent')
},
onResponseStarted () {
dispatches.push('onResponseStarted')
},
onHeaders () {
dispatches.push('onHeaders')
},
onData () {
dispatches.push('onData')
},
onComplete () {
dispatches.push('onComplete')
p.deepStrictEqual(dispatches, ['onConnect', 'onBodySent', 'onResponseStarted', 'onHeaders', 'onData', 'onComplete'])
},
onError (err) {
p.ifError(err)
}
})
client.dispatch({
servername: 'google.com',
path: '/',
method: 'POST',
body: 'body'
}, {
onConnect () {
dispatches2.push('onConnect')
},
onBodySent () {
dispatches2.push('onBodySent')
},
onResponseStarted () {
dispatches2.push('onResponseStarted')
},
onHeaders () {
dispatches2.push('onHeaders')
},
onData () {
dispatches2.push('onData')
},
onComplete () {
dispatches2.push('onComplete')
p.deepStrictEqual(dispatches2, ['onConnect', 'onBodySent', 'onResponseStarted', 'onHeaders', 'onData', 'onComplete'])
},
onError (err) {
p.ifError(err)
}
})
})
await p.completed
})
test('Issue#3065 - fix bad destroy handling (h2)', async (t) => {
// Due to we handle the session, the request for h2 will fail on servername change
const p = tspl(t, { plan: 4 })
const server = createSecureServer(pem)
server.on('stream', (stream) => {
stream.respond({
'content-type': 'text/plain; charset=utf-8',
':status': 200
})
stream.end('ended')
})
server.listen(0, () => {
const client = new Client(`https://localhost:${server.address().port}`, {
connect: {
rejectUnauthorized: false
},
allowH2: true
})
t.after(closeClientAndServerAsPromise(client, server))
const dispatches = []
const dispatches2 = []
client.once('disconnect', (...args) => {
const [,, err] = args
p.strictEqual(err.code, 'UND_ERR_INFO')
p.strictEqual(err.message, 'servername changed')
})
client.dispatch({
path: '/',
method: 'POST',
body: 'body'
}, {
onConnect () {
dispatches.push('onConnect')
},
onBodySent () {
dispatches.push('onBodySent')
},
onResponseStarted () {
dispatches.push('onResponseStarted')
},
onHeaders () {
dispatches.push('onHeaders1')
},
onData () {
dispatches.push('onData')
},
onComplete () {
dispatches.push('onComplete')
p.deepStrictEqual(dispatches, ['onConnect', 'onBodySent', 'onResponseStarted', 'onHeaders1', 'onData', 'onComplete'])
},
onError (err) {
p.ifError(err)
}
})
client.dispatch({
servername: 'google.com',
path: '/',
method: 'POST',
body: 'body'
}, {
onConnect () {
dispatches2.push('onConnect')
},
onBodySent () {
dispatches2.push('onBodySent')
},
onResponseStarted () {
dispatches2.push('onResponseStarted')
},
onHeaders () {
dispatches2.push('onHeaders2')
},
onData () {
dispatches2.push('onData')
},
onComplete () {
dispatches2.push('onComplete')
p.deepStrictEqual(dispatches2, ['onConnect', 'onBodySent', 'onResponseStarted', 'onHeaders2', 'onData', 'onComplete'])
},
onError (err) {
p.ifError(err)
}
})
})
await p.completed
})
================================================
FILE: test/node-test/client-errors.js
================================================
'use strict'
const assert = require('node:assert')
const https = require('node:https')
const net = require('node:net')
const { Readable } = require('node:stream')
const { test, after } = require('node:test')
const { Client, Pool, errors } = require('../..')
const { createServer } = require('node:http')
const pem = require('@metcoder95/https-pem')
const { tspl } = require('@matteo.collina/tspl')
const { kSocket } = require('../../lib/core/symbols')
const { wrapWithAsyncIterable, maybeWrapStream, consts } = require('../utils/async-iterators')
const { closeServerAsPromise } = require('../utils/node-http')
class IteratorError extends Error {}
test('GET errors and reconnect with pipelining 1', async (t) => {
const p = tspl(t, { plan: 9 })
const server = createServer({ joinDuplicateHeaders: true })
server.once('request', (req, res) => {
// first request received, destroying
p.ok(1)
res.socket.destroy()
server.once('request', (req, res) => {
p.strictEqual('/', req.url)
p.strictEqual('GET', req.method)
res.setHeader('content-type', 'text/plain')
res.end('hello')
})
})
t.after(closeServerAsPromise(server))
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`, {
pipelining: 1
})
t.after(client.destroy.bind(client))
client.request({ path: '/', method: 'GET', idempotent: false, opaque: 'asd' }, (err, data) => {
p.ok(err instanceof Error) // we are expecting an error
p.strictEqual(data.opaque, 'asd')
})
client.request({ path: '/', method: 'GET' }, (err, { statusCode, headers, body }) => {
p.ifError(err)
p.strictEqual(statusCode, 200)
p.strictEqual(headers['content-type'], 'text/plain')
const bufs = []
body.on('data', (buf) => {
bufs.push(buf)
})
body.on('end', () => {
p.strictEqual('hello', Buffer.concat(bufs).toString('utf8'))
})
})
})
await p.completed
})
test('GET errors and reconnect with pipelining 3', async (t) => {
const server = createServer({ joinDuplicateHeaders: true })
const requestsThatWillError = 3
let requests = 0
const p = tspl(t, { plan: 6 + requestsThatWillError * 3 })
server.on('request', (req, res) => {
if (requests++ < requestsThatWillError) {
// request received, destroying
p.ok(1)
// socket might not be there if it was destroyed by another
// pipelined request
if (res.socket) {
res.socket.destroy()
}
} else {
p.strictEqual('/', req.url)
p.strictEqual('GET', req.method)
res.setHeader('content-type', 'text/plain')
res.end('hello')
}
})
t.after(closeServerAsPromise(server))
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`, {
pipelining: 3
})
t.after(client.destroy.bind(client))
// all of these will error
for (let i = 0; i < 3; i++) {
client.request({ path: '/', method: 'GET', idempotent: false, opaque: 'asd' }, (err, data) => {
p.ok(err instanceof Error) // we are expecting an error
p.strictEqual(data.opaque, 'asd')
})
}
// this will be queued up
client.request({ path: '/', method: 'GET', idempotent: false }, (err, { statusCode, headers, body }) => {
p.ifError(err)
p.strictEqual(statusCode, 200)
p.strictEqual(headers['content-type'], 'text/plain')
const bufs = []
body.on('data', (buf) => {
bufs.push(buf)
})
body.on('end', () => {
p.strictEqual('hello', Buffer.concat(bufs).toString('utf8'))
})
})
})
await p.completed
})
function errorAndPipelining (type) {
test(`POST with a ${type} that errors and pipelining 1 should reconnect`, async (t) => {
const p = tspl(t, { plan: 12 })
const server = createServer({ joinDuplicateHeaders: true })
server.once('request', (req, res) => {
p.strictEqual('/', req.url)
p.strictEqual('POST', req.method)
p.strictEqual('42', req.headers['content-length'])
const bufs = []
req.on('data', (buf) => {
bufs.push(buf)
})
req.on('aborted', () => {
// we will abruptly close the connection here
// but this will still end
p.strictEqual('a string', Buffer.concat(bufs).toString('utf8'))
})
server.once('request', (req, res) => {
p.strictEqual('/', req.url)
p.strictEqual('GET', req.method)
res.setHeader('content-type', 'text/plain')
res.end('hello')
})
})
t.after(closeServerAsPromise(server))
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
t.after(client.destroy.bind(client))
client.request({
path: '/',
method: 'POST',
headers: {
// higher than the length of the string
'content-length': 42
},
opaque: 'asd',
body: maybeWrapStream(new Readable({
read () {
this.push('a string')
this.destroy(new Error('kaboom'))
}
}), type)
}, (err, data) => {
p.strictEqual(err.message, 'kaboom')
p.strictEqual(data.opaque, 'asd')
})
// this will be queued up
client.request({ path: '/', method: 'GET', idempotent: false }, (err, { statusCode, headers, body }) => {
p.ifError(err)
p.strictEqual(statusCode, 200)
p.strictEqual(headers['content-type'], 'text/plain')
const bufs = []
body.on('data', (buf) => {
bufs.push(buf)
})
body.on('end', () => {
p.strictEqual('hello', Buffer.concat(bufs).toString('utf8'))
})
})
})
await p.completed
})
}
errorAndPipelining(consts.STREAM)
errorAndPipelining(consts.ASYNC_ITERATOR)
function errorAndChunkedEncodingPipelining (type) {
test(`POST with chunked encoding, ${type} body that errors and pipelining 1 should reconnect`, async (t) => {
const p = tspl(t, { plan: 12 })
const server = createServer({ joinDuplicateHeaders: true })
server.once('request', (req, res) => {
p.strictEqual('/', req.url)
p.strictEqual('POST', req.method)
p.strictEqual(req.headers['content-length'], undefined)
const bufs = []
req.on('data', (buf) => {
bufs.push(buf)
})
req.on('aborted', () => {
// we will abruptly close the connection here
// but this will still end
p.strictEqual('a string', Buffer.concat(bufs).toString('utf8'))
})
server.once('request', (req, res) => {
p.strictEqual('/', req.url)
p.strictEqual('GET', req.method)
res.setHeader('content-type', 'text/plain')
res.end('hello')
})
})
t.after(closeServerAsPromise(server))
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
t.after(client.destroy.bind(client))
client.request({
path: '/',
method: 'POST',
opaque: 'asd',
body: maybeWrapStream(new Readable({
read () {
this.push('a string')
this.destroy(new Error('kaboom'))
}
}), type)
}, (err, data) => {
p.strictEqual(err.message, 'kaboom')
p.strictEqual(data.opaque, 'asd')
})
// this will be queued up
client.request({ path: '/', method: 'GET' }, (err, { statusCode, headers, body }) => {
p.ifError(err)
p.strictEqual(statusCode, 200)
p.strictEqual(headers['content-type'], 'text/plain')
const bufs = []
body.on('data', (buf) => {
bufs.push(buf)
})
body.on('end', () => {
p.strictEqual('hello', Buffer.concat(bufs).toString('utf8'))
})
})
})
await p.completed
})
}
errorAndChunkedEncodingPipelining(consts.STREAM)
errorAndChunkedEncodingPipelining(consts.ASYNC_ITERATOR)
test('invalid options throws', (t, done) => {
try {
new Client({ port: 'foobar', protocol: 'https:' }) // eslint-disable-line
assert.ok(0)
} catch (err) {
assert.ok(err instanceof errors.InvalidArgumentError)
assert.strictEqual(err.message, 'Invalid URL: port must be a valid integer or a string representation of an integer.')
}
try {
new Client(new URL('http://asd:200/somepath')) // eslint-disable-line
assert.ok(0)
} catch (err) {
assert.ok(err instanceof errors.InvalidArgumentError)
assert.strictEqual(err.message, 'invalid url')
}
try {
new Client(new URL('http://asd:200?q=asd')) // eslint-disable-line
assert.ok(0)
} catch (err) {
assert.ok(err instanceof errors.InvalidArgumentError)
assert.strictEqual(err.message, 'invalid url')
}
try {
new Client(new URL('http://asd:200#asd')) // eslint-disable-line
assert.ok(0)
} catch (err) {
assert.ok(err instanceof errors.InvalidArgumentError)
assert.strictEqual(err.message, 'invalid url')
}
try {
new Client(new URL('http://localhost:200'), { // eslint-disable-line
socketPath: 1
})
assert.ok(0)
} catch (err) {
assert.ok(err instanceof errors.InvalidArgumentError)
assert.strictEqual(err.message, 'invalid socketPath')
}
try {
new Client(new URL('http://localhost:200'), { // eslint-disable-line
keepAliveTimeout: 'asd'
})
assert.ok(0)
} catch (err) {
assert.ok(err instanceof errors.InvalidArgumentError)
assert.strictEqual(err.message, 'invalid keepAliveTimeout')
}
try {
new Client(new URL('http://localhost:200'), { // eslint-disable-line
localAddress: 123
})
assert.ok(0)
} catch (err) {
assert.ok(err instanceof errors.InvalidArgumentError)
assert.strictEqual(err.message, 'localAddress must be valid string IP address')
}
try {
new Client(new URL('http://localhost:200'), { // eslint-disable-line
localAddress: 'abcd123'
})
assert.ok(0)
} catch (err) {
assert.ok(err instanceof errors.InvalidArgumentError)
assert.strictEqual(err.message, 'localAddress must be valid string IP address')
}
try {
new Client(new URL('http://localhost:200'), { // eslint-disable-line
keepAliveMaxTimeout: 'asd'
})
assert.ok(0)
} catch (err) {
assert.ok(err instanceof errors.InvalidArgumentError)
assert.strictEqual(err.message, 'invalid keepAliveMaxTimeout')
}
try {
new Client(new URL('http://localhost:200'), { // eslint-disable-line
keepAliveMaxTimeout: 0
})
assert.ok(0)
} catch (err) {
assert.ok(err instanceof errors.InvalidArgumentError)
assert.strictEqual(err.message, 'invalid keepAliveMaxTimeout')
}
try {
new Client(new URL('http://localhost:200'), { // eslint-disable-line
keepAliveTimeoutThreshold: 'asd'
})
assert.ok(0)
} catch (err) {
assert.ok(err instanceof errors.InvalidArgumentError)
assert.strictEqual(err.message, 'invalid keepAliveTimeoutThreshold')
}
try {
new Client({ // eslint-disable-line
protocol: 'asd'
})
assert.ok(0)
} catch (err) {
assert.ok(err instanceof errors.InvalidArgumentError)
assert.strictEqual(err.message, 'Invalid URL protocol: the URL must start with `http:` or `https:`.')
}
try {
new Client({ // eslint-disable-line
hostname: 1
})
assert.ok(0)
} catch (err) {
assert.ok(err instanceof errors.InvalidArgumentError)
assert.strictEqual(err.message, 'Invalid URL hostname: the hostname must be a string or null/undefined.')
}
try {
new Client(new URL('http://localhost:200'), { // eslint-disable-line
maxHeaderSize: 'asd'
})
assert.ok(0)
} catch (err) {
assert.ok(err instanceof errors.InvalidArgumentError)
assert.strictEqual(err.message, 'invalid maxHeaderSize')
}
try {
new Client(new URL('http://localhost:200'), { // eslint-disable-line
maxHeaderSize: 0
})
assert.ok(0)
} catch (err) {
assert.ok(err instanceof errors.InvalidArgumentError)
assert.strictEqual(err.message, 'invalid maxHeaderSize')
}
try {
new Client(new URL('http://localhost:200'), { // eslint-disable-line
maxHeaderSize: 0
})
assert.ok(0)
} catch (err) {
assert.ok(err instanceof errors.InvalidArgumentError)
assert.strictEqual(err.message, 'invalid maxHeaderSize')
}
try {
new Client(new URL('http://localhost:200'), { // eslint-disable-line
maxHeaderSize: -10
})
assert.ok(0)
} catch (err) {
assert.ok(err instanceof errors.InvalidArgumentError)
assert.strictEqual(err.message, 'invalid maxHeaderSize')
}
try {
new Client(new URL('http://localhost:200'), { // eslint-disable-line
maxHeaderSize: 1.5
})
assert.ok(0)
} catch (err) {
assert.ok(err instanceof errors.InvalidArgumentError)
assert.strictEqual(err.message, 'invalid maxHeaderSize')
}
try {
new Client(1) // eslint-disable-line
assert.ok(0)
} catch (err) {
assert.ok(err instanceof errors.InvalidArgumentError)
assert.strictEqual(err.message, 'Invalid URL: The URL argument must be a non-null object.')
}
try {
const client = new Client(new URL('http://localhost:200'))
client.destroy(null, null)
assert.ok(0)
} catch (err) {
assert.ok(err instanceof errors.InvalidArgumentError)
assert.strictEqual(err.message, 'invalid callback')
}
try {
const client = new Client(new URL('http://localhost:200'))
client.close(null, null)
assert.ok(0)
} catch (err) {
assert.ok(err instanceof errors.InvalidArgumentError)
assert.strictEqual(err.message, 'invalid callback')
}
try {
new Client(new URL('http://localhost:200'), { maxKeepAliveTimeout: 1e3 }) // eslint-disable-line
assert.ok(0)
} catch (err) {
assert.ok(err instanceof errors.InvalidArgumentError)
assert.strictEqual(err.message, 'unsupported maxKeepAliveTimeout, use keepAliveMaxTimeout instead')
}
try {
new Client(new URL('http://localhost:200'), { keepAlive: false }) // eslint-disable-line
assert.ok(0)
} catch (err) {
assert.ok(err instanceof errors.InvalidArgumentError)
assert.strictEqual(err.message, 'unsupported keepAlive, use pipelining=0 instead')
}
try {
new Client(new URL('http://localhost:200'), { idleTimeout: 30e3 }) // eslint-disable-line
assert.ok(0)
} catch (err) {
assert.ok(err instanceof errors.InvalidArgumentError)
assert.strictEqual(err.message, 'unsupported idleTimeout, use keepAliveTimeout instead')
}
try {
new Client(new URL('http://localhost:200'), { socketTimeout: 30e3 }) // eslint-disable-line
assert.ok(0)
} catch (err) {
assert.ok(err instanceof errors.InvalidArgumentError)
assert.strictEqual(err.message, 'unsupported socketTimeout, use headersTimeout & bodyTimeout instead')
}
try {
new Client(new URL('http://localhost:200'), { requestTimeout: 30e3 }) // eslint-disable-line
assert.ok(0)
} catch (err) {
assert.ok(err instanceof errors.InvalidArgumentError)
assert.strictEqual(err.message, 'unsupported requestTimeout, use headersTimeout & bodyTimeout instead')
}
try {
new Client(new URL('http://localhost:200'), { connectTimeout: -1 }) // eslint-disable-line
assert.ok(0)
} catch (err) {
assert.ok(err instanceof errors.InvalidArgumentError)
assert.strictEqual(err.message, 'invalid connectTimeout')
}
try {
new Client(new URL('http://localhost:200'), { connectTimeout: Infinity }) // eslint-disable-line
assert.ok(0)
} catch (err) {
assert.ok(err instanceof errors.InvalidArgumentError)
assert.strictEqual(err.message, 'invalid connectTimeout')
}
try {
new Client(new URL('http://localhost:200'), { connectTimeout: 'asd' }) // eslint-disable-line
assert.ok(0)
} catch (err) {
assert.ok(err instanceof errors.InvalidArgumentError)
assert.strictEqual(err.message, 'invalid connectTimeout')
}
try {
new Client(new URL('http://localhost:200'), { connect: 'asd' }) // eslint-disable-line
assert.ok(0)
} catch (err) {
assert.ok(err instanceof errors.InvalidArgumentError)
assert.strictEqual(err.message, 'connect must be a function or an object')
}
try {
new Client(new URL('http://localhost:200'), { connect: -1 }) // eslint-disable-line
assert.ok(0)
} catch (err) {
assert.ok(err instanceof errors.InvalidArgumentError)
assert.strictEqual(err.message, 'connect must be a function or an object')
}
try {
new Pool(new URL('http://localhost:200'), { connect: 'asd' }) // eslint-disable-line
assert.ok(0)
} catch (err) {
assert.ok(err instanceof errors.InvalidArgumentError)
assert.strictEqual(err.message, 'connect must be a function or an object')
}
try {
new Pool(new URL('http://localhost:200'), { connect: -1 }) // eslint-disable-line
assert.ok(0)
} catch (err) {
assert.ok(err instanceof errors.InvalidArgumentError)
assert.strictEqual(err.message, 'connect must be a function or an object')
}
try {
new Client(new URL('http://localhost:200'), { maxCachedSessions: -10 }) // eslint-disable-line
assert.ok(0)
} catch (err) {
assert.ok(err instanceof errors.InvalidArgumentError)
assert.strictEqual(err.message, 'maxCachedSessions must be a positive integer or zero')
}
try {
new Client(new URL('http://localhost:200'), { maxCachedSessions: 'foo' }) // eslint-disable-line
assert.ok(0)
} catch (err) {
assert.ok(err instanceof errors.InvalidArgumentError)
assert.strictEqual(err.message, 'maxCachedSessions must be a positive integer or zero')
}
try {
new Client(new URL('http://localhost:200'), { maxRequestsPerClient: 'foo' }) // eslint-disable-line
assert.ok(0)
} catch (err) {
assert.ok(err instanceof errors.InvalidArgumentError)
assert.strictEqual(err.message, 'maxRequestsPerClient must be a positive number')
}
try {
new Client(new URL('http://localhost:200'), { autoSelectFamilyAttemptTimeout: 'foo' }) // eslint-disable-line
assert.ok(0)
} catch (err) {
assert.ok(err instanceof errors.InvalidArgumentError)
assert.strictEqual(err.message, 'autoSelectFamilyAttemptTimeout must be a positive number')
}
try {
new Client(new URL('http://localhost:200'), { initialWindowSize: 'foo' }) // eslint-disable-line
assert.ok(0)
} catch (err) {
assert.ok(err instanceof errors.InvalidArgumentError)
assert.strictEqual(err.message, 'initialWindowSize must be a positive integer, greater than 0')
}
try {
new Client(new URL('http://localhost:200'), { initialWindowSize: 0 }) // eslint-disable-line
assert.ok(0)
} catch (err) {
assert.ok(err instanceof errors.InvalidArgumentError)
assert.strictEqual(err.message, 'initialWindowSize must be a positive integer, greater than 0')
}
try {
new Client(new URL('http://localhost:200'), { initialWindowSize: -1 }) // eslint-disable-line
assert.ok(0)
} catch (err) {
assert.ok(err instanceof errors.InvalidArgumentError)
assert.strictEqual(err.message, 'initialWindowSize must be a positive integer, greater than 0')
}
try {
new Client(new URL('http://localhost:200'), { initialWindowSize: 1.5 }) // eslint-disable-line
assert.ok(0)
} catch (err) {
assert.ok(err instanceof errors.InvalidArgumentError)
assert.strictEqual(err.message, 'initialWindowSize must be a positive integer, greater than 0')
}
try {
new Client(new URL('http://localhost:200'), { connectionWindowSize: 'foo' }) // eslint-disable-line
assert.ok(0)
} catch (err) {
assert.ok(err instanceof errors.InvalidArgumentError)
assert.strictEqual(err.message, 'connectionWindowSize must be a positive integer, greater than 0')
}
try {
new Client(new URL('http://localhost:200'), { connectionWindowSize: 0 }) // eslint-disable-line
assert.ok(0)
} catch (err) {
assert.ok(err instanceof errors.InvalidArgumentError)
assert.strictEqual(err.message, 'connectionWindowSize must be a positive integer, greater than 0')
}
try {
new Client(new URL('http://localhost:200'), { connectionWindowSize: -1 }) // eslint-disable-line
assert.ok(0)
} catch (err) {
assert.ok(err instanceof errors.InvalidArgumentError)
assert.strictEqual(err.message, 'connectionWindowSize must be a positive integer, greater than 0')
}
try {
new Client(new URL('http://localhost:200'), { connectionWindowSize: 1.5 }) // eslint-disable-line
assert.ok(0)
} catch (err) {
assert.ok(err instanceof errors.InvalidArgumentError)
assert.strictEqual(err.message, 'connectionWindowSize must be a positive integer, greater than 0')
}
done()
})
test('POST which fails should error response', async (t) => {
const p = tspl(t, { plan: 6 })
const server = createServer({ joinDuplicateHeaders: true })
server.on('request', (req, res) => {
req.once('data', () => {
res.destroy()
})
})
t.after(closeServerAsPromise(server))
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
t.after(client.destroy.bind(client))
function checkError (err) {
// Different platforms error with different codes...
p.ok(
err.code === 'EPIPE' ||
err.code === 'ECONNRESET' ||
err.code === 'UND_ERR_SOCKET' ||
err.message === 'other side closed'
)
}
{
const body = new Readable({ read () {} })
body.push('asd')
body.on('error', (err) => {
checkError(err)
})
client.request({
path: '/',
method: 'POST',
body
}, (err) => {
checkError(err)
})
}
{
const body = new Readable({ read () {} })
body.push('asd')
body.on('error', (err) => {
checkError(err)
})
client.request({
path: '/',
method: 'POST',
headers: {
'content-length': 100
},
body
}, (err) => {
checkError(err)
})
}
{
const body = wrapWithAsyncIterable(['asd'], true)
client.request({
path: '/',
method: 'POST',
body
}, (err) => {
checkError(err)
})
}
{
const body = wrapWithAsyncIterable(['asd'], true)
client.request({
path: '/',
method: 'POST',
headers: {
'content-length': 100
},
body
}, (err) => {
checkError(err)
})
}
})
await p.completed
})
test('client destroy cleanup', async (t) => {
const p = tspl(t, { plan: 3 })
const _err = new Error('kaboom')
let client
const server = createServer({ joinDuplicateHeaders: true })
server.once('request', (req, res) => {
req.once('data', () => {
client.destroy(_err, (err) => {
p.ifError(err)
})
})
})
t.after(closeServerAsPromise(server))
server.listen(0, () => {
client = new Client(`http://localhost:${server.address().port}`)
t.after(client.destroy.bind(client))
const body = new Readable({ read () {} })
body.push('asd')
body.on('error', (err) => {
p.strictEqual(err, _err)
})
client.request({
path: '/',
method: 'POST',
body
}, (err, data) => {
p.strictEqual(err, _err)
})
})
await p.completed
})
test('throwing async-iterator causes error', async (t) => {
const p = tspl(t, { plan: 1 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.end(Buffer.alloc(4 + 1, 'a'))
})
t.after(closeServerAsPromise(server))
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
t.after(client.destroy.bind(client))
client.request({
method: 'POST',
path: '/',
body: (async function * () {
yield 'hello'
throw new IteratorError('bad iterator')
})()
}, (err) => {
p.ok(err instanceof IteratorError)
})
})
await p.completed
})
test('client async-iterator destroy cleanup', async (t) => {
const p = tspl(t, { plan: 2 })
const _err = new Error('kaboom')
let client
const server = createServer({ joinDuplicateHeaders: true })
server.once('request', (req, res) => {
req.once('data', () => {
client.destroy(_err, (err) => {
p.ifError(err)
})
})
})
t.after(closeServerAsPromise(server))
server.listen(0, () => {
client = new Client(`http://localhost:${server.address().port}`)
t.after(client.destroy.bind(client))
const body = wrapWithAsyncIterable(['asd'], true)
client.request({
path: '/',
method: 'POST',
body
}, (err, data) => {
p.strictEqual(err, _err)
})
})
await p.completed
})
test('GET errors body', async (t) => {
const p = tspl(t, { plan: 2 })
const server = createServer({ joinDuplicateHeaders: true })
server.once('request', (req, res) => {
res.write('asd')
setTimeout(() => {
res.destroy()
}, 19)
})
t.after(closeServerAsPromise(server))
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
t.after(client.destroy.bind(client))
client.request({ path: '/', method: 'GET' }, (err, { statusCode, headers, body }) => {
p.ifError(err)
body.resume()
body.on('error', err => (
p.ok(err)
))
})
})
await p.completed
})
test('validate request body', async (t) => {
const p = tspl(t, { plan: 6 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.end('asd')
})
t.after(closeServerAsPromise(server))
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
t.after(() => { return client.close() })
client.request({
path: '/',
method: 'POST',
body: /asdasd/
}, (err, data) => {
p.ok(err instanceof errors.InvalidArgumentError)
})
client.request({
path: '/',
method: 'POST',
body: 0
}, (err, data) => {
p.ok(err instanceof errors.InvalidArgumentError)
})
client.request({
path: '/',
method: 'POST',
body: false
}, (err, data) => {
p.ok(err instanceof errors.InvalidArgumentError)
})
client.request({
path: '/',
method: 'POST',
body: ''
}, (err, data) => {
p.ifError(err)
data.body.resume()
})
client.request({
path: '/',
method: 'POST',
body: new Uint8Array()
}, (err, data) => {
p.ifError(err)
data.body.resume()
})
client.request({
path: '/',
method: 'POST',
body: Buffer.alloc(10)
}, (err, data) => {
p.ifError(err)
data.body.resume()
})
})
await p.completed
})
function socketFailWrite (type) {
test(`socket fail while writing ${type} request body`, async (t) => {
const p = tspl(t, { plan: 2 })
const server = createServer({ joinDuplicateHeaders: true })
server.once('request', (req, res) => {
})
t.after(closeServerAsPromise(server))
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
t.after(client.destroy.bind(client))
const preBody = new Readable({ read () {} })
preBody.push('asd')
const body = maybeWrapStream(preBody, type)
client.on('connect', () => {
process.nextTick(() => {
client[kSocket].destroy('kaboom')
})
})
client.request({
path: '/',
method: 'POST',
body
}, (err) => {
p.ok(err)
})
client.close((err) => {
p.ifError(err)
})
})
await p.completed
})
}
socketFailWrite(consts.STREAM)
socketFailWrite(consts.ASYNC_ITERATOR)
function socketFailEndWrite (type) {
test(`socket fail while ending ${type} request body`, async (t) => {
const p = tspl(t, { plan: 3 })
const server = createServer({ joinDuplicateHeaders: true })
server.once('request', (req, res) => {
res.end()
})
t.after(closeServerAsPromise(server))
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`, {
pipelining: 2
})
t.after(client.destroy.bind(client))
const _err = new Error('kaboom')
client.on('connect', () => {
process.nextTick(() => {
client[kSocket].destroy(_err)
})
})
const preBody = new Readable({ read () {} })
preBody.push(null)
const body = maybeWrapStream(preBody, type)
client.request({
path: '/',
method: 'POST',
body
}, (err) => {
p.strictEqual(err, _err)
})
client.close((err) => {
p.ifError(err)
client.close((err) => {
p.ok(err instanceof errors.ClientDestroyedError)
})
})
})
await p.completed
})
}
socketFailEndWrite(consts.STREAM)
socketFailEndWrite(consts.ASYNC_ITERATOR)
test('queued request should not fail on socket destroy', async (t) => {
const p = tspl(t, { plan: 4 })
const server = createServer({ joinDuplicateHeaders: true })
server.on('request', (req, res) => {
res.end()
})
t.after(closeServerAsPromise(server))
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`, {
pipelining: 1
})
t.after(client.destroy.bind(client))
client.request({
path: '/',
method: 'GET'
}, (err, data) => {
p.ifError(err)
data.body.resume().on('error', () => {
p.ok(1)
})
client[kSocket].destroy()
client.request({
path: '/',
method: 'GET'
}, (err, data) => {
p.ifError(err)
data.body.resume().on('end', () => {
p.ok(1)
})
})
})
})
await p.completed
})
test('queued request should fail on client destroy', async (t) => {
const p = tspl(t, { plan: 6 })
const server = createServer({ joinDuplicateHeaders: true })
server.on('request', (req, res) => {
res.end()
})
t.after(closeServerAsPromise(server))
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`, {
pipelining: 1
})
t.after(client.destroy.bind(client))
let requestErrored = false
client.request({
path: '/',
method: 'GET'
}, (err, data) => {
p.ifError(err)
data.body.resume()
.on('error', () => {
p.ok(1)
})
client.destroy((err) => {
p.ifError(err)
p.strictEqual(requestErrored, true)
})
})
client.request({
path: '/',
method: 'GET',
opaque: 'asd'
}, (err, data) => {
requestErrored = true
p.ok(err)
p.strictEqual(data.opaque, 'asd')
})
})
await p.completed
})
test('retry idempotent inflight', async (t) => {
const p = tspl(t, { plan: 3 })
const server = createServer({ joinDuplicateHeaders: true })
server.on('request', (req, res) => {
res.end()
})
t.after(closeServerAsPromise(server))
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`, {
pipelining: 3
})
t.after(() => { return client.close() })
client.request({
path: '/',
method: 'POST',
body: new Readable({
read () {
this.destroy(new Error('kaboom'))
}
})
}, (err) => {
p.ok(err)
})
client.request({
path: '/',
method: 'GET'
}, (err, data) => {
p.ifError(err)
data.body.resume()
})
client.request({
path: '/',
method: 'GET'
}, (err, data) => {
p.ifError(err)
data.body.resume()
})
})
await p.completed
})
test('invalid opts', async (t) => {
const p = tspl(t, { plan: 5 })
const client = new Client('http://localhost:5000')
client.request(null, (err) => {
p.ok(err instanceof errors.InvalidArgumentError)
})
client.pipeline(null).on('error', (err) => {
p.ok(err instanceof errors.InvalidArgumentError)
})
client.request({
path: '/',
method: 'GET',
highWaterMark: '1000'
}, (err) => {
p.ok(err instanceof errors.InvalidArgumentError)
p.strictEqual(err.message, 'invalid highWaterMark')
})
client.request({
path: '/',
method: 'GET',
highWaterMark: -1
}, (err) => {
p.ok(err instanceof errors.InvalidArgumentError)
p.strictEqual(err.message, 'invalid highWaterMark')
})
await p.completed
})
test('default port for http and https', async (t) => {
const p = tspl(t, { plan: 4 })
try {
new Client(new URL('http://localhost:80')) // eslint-disable-line
p.ok('Should not throw')
} catch (err) {
p.fail(err)
}
try {
new Client(new URL('http://localhost')) // eslint-disable-line
p.ok('Should not throw')
} catch (err) {
p.fail(err)
}
try {
new Client(new URL('https://localhost:443')) // eslint-disable-line
p.ok('Should not throw')
} catch (err) {
p.fail(err)
}
try {
new Client(new URL('https://localhost')) // eslint-disable-line
p.ok('Should not throw')
} catch (err) {
p.fail(err)
}
})
test('CONNECT throws in next tick', async (t) => {
const p = tspl(t, { plan: 3 })
const server = createServer({ joinDuplicateHeaders: true })
server.on('request', (req, res) => {
res.end()
})
t.after(closeServerAsPromise(server))
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
t.after(client.destroy.bind(client))
client.request({
path: '/',
method: 'GET'
}, (err, data) => {
p.ifError(err)
data.body
.on('end', () => {
let ticked = false
client.request({
path: '/',
method: 'CONNECT'
}, (err) => {
p.ok(err)
p.strictEqual(ticked, true)
})
ticked = true
})
.resume()
})
})
await p.completed
})
test('invalid signal', async (t) => {
const p = tspl(t, { plan: 8 })
const client = new Client('http://localhost:3333')
t.after(client.destroy.bind(client))
let ticked = false
client.request({ path: '/', method: 'GET', signal: {}, opaque: 'asd' }, (err, { opaque }) => {
p.strictEqual(ticked, true)
p.strictEqual(opaque, 'asd')
p.ok(err instanceof errors.InvalidArgumentError)
})
client.pipeline({ path: '/', method: 'GET', signal: {} }, () => {})
.on('error', (err) => {
p.strictEqual(ticked, true)
p.ok(err instanceof errors.InvalidArgumentError)
})
client.stream({ path: '/', method: 'GET', signal: {}, opaque: 'asd' }, () => {}, (err, { opaque }) => {
p.strictEqual(ticked, true)
p.strictEqual(opaque, 'asd')
p.ok(err instanceof errors.InvalidArgumentError)
})
ticked = true
await p.completed
})
test('invalid body chunk does not crash', async (t) => {
const p = tspl(t, { plan: 1 })
const server = createServer({ joinDuplicateHeaders: true })
server.on('request', (req, res) => {
res.end()
})
t.after(closeServerAsPromise(server))
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
t.after(client.destroy.bind(client))
client.request({
path: '/',
body: new Readable({
objectMode: true,
read () {
this.push({})
}
}),
method: 'GET'
}, (err) => {
p.strictEqual(err.code, 'ERR_INVALID_ARG_TYPE')
})
})
await p.completed
})
test('socket errors', async (t) => {
const p = tspl(t, { plan: 2 })
const client = new Client('http://localhost:5554')
t.after(client.destroy.bind(client))
client.request({ path: '/', method: 'GET' }, (err, data) => {
p.ok(err)
// TODO: Why UND_ERR_SOCKET?
p.ok(err.code === 'ECONNREFUSED' || err.code === 'UND_ERR_SOCKET', err.code)
p.end()
})
await p.completed
})
test('headers overflow', (t, done) => {
const p = tspl(t, { plan: 2 })
const server = createServer({ joinDuplicateHeaders: true })
server.on('request', (req, res) => {
res.writeHead(200, {
'x-test-1': '1',
'x-test-2': '2'
})
res.end()
})
t.after(closeServerAsPromise(server))
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`, {
maxHeaderSize: 10
})
t.after(client.destroy.bind(client))
client.request({ path: '/', method: 'GET' }, (err, data) => {
p.ok(err)
p.strictEqual(err.code, 'UND_ERR_HEADERS_OVERFLOW')
done()
})
})
})
test('SocketError should expose socket details (net)', async (t) => {
const p = tspl(t, { plan: 8 })
const server = createServer({ joinDuplicateHeaders: true })
server.once('request', (req, res) => {
res.destroy()
})
t.after(closeServerAsPromise(server))
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
t.after(client.destroy.bind(client))
client.request({ path: '/', method: 'GET' }, (err, data) => {
p.ok(err instanceof errors.SocketError)
if (err.socket.remoteFamily === 'IPv4') {
p.strictEqual(err.socket.remoteFamily, 'IPv4')
p.strictEqual(err.socket.localAddress, '127.0.0.1')
p.strictEqual(err.socket.remoteAddress, '127.0.0.1')
} else {
p.strictEqual(err.socket.remoteFamily, 'IPv6')
p.strictEqual(err.socket.localAddress, '::1')
p.strictEqual(err.socket.remoteAddress, '::1')
}
p.ok(typeof err.socket.localPort === 'number')
p.ok(typeof err.socket.remotePort === 'number')
p.ok(typeof err.socket.bytesWritten === 'number')
p.ok(typeof err.socket.bytesRead === 'number')
})
})
await p.completed
})
test('SocketError should expose socket details (tls)', async (t) => {
const p = tspl(t, { plan: 8 })
const server = https.createServer({ ...pem, joinDuplicateHeaders: true })
server.once('request', (req, res) => {
res.destroy()
})
t.after(closeServerAsPromise(server))
server.listen(0, () => {
const client = new Client(`https://localhost:${server.address().port}`, {
tls: {
rejectUnauthorized: false
}
})
t.after(client.destroy.bind(client))
client.request({ path: '/', method: 'GET' }, (err, data) => {
p.ok(err instanceof errors.SocketError)
if (err.socket.remoteFamily === 'IPv4') {
p.strictEqual(err.socket.remoteFamily, 'IPv4')
p.strictEqual(err.socket.localAddress, '127.0.0.1')
p.strictEqual(err.socket.remoteAddress, '127.0.0.1')
} else {
p.strictEqual(err.socket.remoteFamily, 'IPv6')
p.strictEqual(err.socket.localAddress, '::1')
p.strictEqual(err.socket.remoteAddress, '::1')
}
p.ok(typeof err.socket.localPort === 'number')
p.ok(typeof err.socket.remotePort === 'number')
p.ok(typeof err.socket.bytesWritten === 'number')
p.ok(typeof err.socket.bytesRead === 'number')
})
})
await p.completed
})
test('parser error', async (t) => {
t = tspl(t, { plan: 2 })
const server = net.createServer({ joinDuplicateHeaders: true })
server.once('connection', (socket) => {
socket.write('asd\n\r213123')
})
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.destroy())
client.request({ path: '/', method: 'GET' }, (err) => {
t.ok(err)
client.close((err) => {
t.ifError(err)
})
})
})
await t.completed
})
================================================
FILE: test/node-test/debug.js
================================================
'use strict'
const { test } = require('node:test')
const { spawn } = require('node:child_process')
const { join } = require('node:path')
const { tspl } = require('@matteo.collina/tspl')
// eslint-disable-next-line no-control-regex
const removeEscapeColorsRE = /[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g
const isNode23Plus = process.versions.node.split('.')[0] >= 23
const isCITGM = !!process.env.CITGM
test('debug#websocket', { skip: !process.versions.icu || isCITGM || isNode23Plus }, async t => {
const assert = tspl(t, { plan: 6 })
const child = spawn(
process.execPath,
[
'--no-experimental-fetch',
join(__dirname, '../fixtures/websocket.js')],
{
env: {
NODE_DEBUG: 'websocket'
}
}
)
const chunks = []
const assertions = [
/(WEBSOCKET [0-9]+:) (connecting to)/,
/(WEBSOCKET [0-9]+:) (connected to)/,
/(WEBSOCKET [0-9]+:) (sending request)/,
/(WEBSOCKET [0-9]+:) (connection opened)/,
/(WEBSOCKET [0-9]+:) (closed connection to)/,
/^$/
]
child.stderr.setEncoding('utf8')
child.stderr.on('data', chunk => {
chunks.push(chunk)
})
child.stderr.on('end', () => {
const lines = extractLines(chunks)
assert.strictEqual(lines.length, assertions.length)
for (let i = 1; i < lines.length; i++) {
assert.match(lines[i], assertions[i])
}
})
await assert.completed
})
test('debug#fetch', { skip: isCITGM || isNode23Plus }, async t => {
const assert = tspl(t, { plan: 7 })
const child = spawn(
process.execPath,
[
'--no-experimental-fetch',
join(__dirname, '../fixtures/fetch.js')
],
{
env: Object.assign({}, process.env, { NODE_DEBUG: 'fetch' })
}
)
const chunks = []
const assertions = [
/(FETCH [0-9]+:) (connecting to)/,
/(FETCH [0-9]+:) (connected to)/,
/(FETCH [0-9]+:) (sending request)/,
/(FETCH [0-9]+:) (received response)/,
/(FETCH [0-9]+:) (trailers received)/,
/^$/
]
child.stderr.setEncoding('utf8')
child.stderr.on('data', chunk => {
chunks.push(chunk)
})
child.stderr.on('end', () => {
const lines = extractLines(chunks)
assert.strictEqual(lines.length, assertions.length)
for (let i = 0; i < lines.length; i++) {
assert.match(lines[i], assertions[i])
}
})
await assert.completed
})
test('debug#undici', { skip: isCITGM || isNode23Plus }, async t => {
// Due to Node.js webpage redirect
const assert = tspl(t, { plan: 7 })
const child = spawn(
process.execPath,
[
'--no-experimental-fetch',
join(__dirname, '../fixtures/undici.js')
],
{
env: {
NODE_DEBUG: 'undici'
}
}
)
const chunks = []
const assertions = [
/(UNDICI [0-9]+:) (connecting to)/,
/(UNDICI [0-9]+:) (connected to)/,
/(UNDICI [0-9]+:) (sending request)/,
/(UNDICI [0-9]+:) (received response)/,
/(UNDICI [0-9]+:) (trailers received)/,
/^$/
]
child.stderr.setEncoding('utf8')
child.stderr.on('data', chunk => {
chunks.push(chunk)
})
child.stderr.on('end', () => {
const lines = extractLines(chunks)
assert.strictEqual(lines.length, assertions.length)
for (let i = 0; i < lines.length; i++) {
assert.match(lines[i], assertions[i])
}
})
await assert.completed
})
test('debug#undici no duplicates', { skip: isCITGM || isNode23Plus }, async t => {
const assert = tspl(t, { plan: 7 })
const child = spawn(
process.execPath,
[
'--no-experimental-fetch',
join(__dirname, '../fixtures/duplicate-debug.js')
],
{
env: {
NODE_DEBUG: 'undici'
}
}
)
const chunks = []
const assertions = [
/(UNDICI [0-9]+:) (connecting to)/,
/(UNDICI [0-9]+:) (connected to)/,
/(UNDICI [0-9]+:) (sending request)/,
/(UNDICI [0-9]+:) (received response)/,
/(UNDICI [0-9]+:) (trailers received)/,
/^$/
]
child.stderr.setEncoding('utf8')
child.stderr.on('data', chunk => {
chunks.push(chunk)
})
child.stderr.on('end', () => {
const lines = extractLines(chunks)
// Should have exactly the expected number of lines, no duplicates
assert.strictEqual(lines.length, assertions.length, 'Should not have duplicate log lines')
for (let i = 0; i < lines.length; i++) {
assert.match(lines[i], assertions[i])
}
})
await assert.completed
})
function extractLines (chunks) {
return chunks
.join('')
.split('\n')
.map(v => v.replace(removeEscapeColorsRE, ''))
}
================================================
FILE: test/node-test/diagnostics-channel/connect-error.js
================================================
'use strict'
const { test } = require('node:test')
const { tspl } = require('@matteo.collina/tspl')
const diagnosticsChannel = require('node:diagnostics_channel')
const { Client } = require('../../..')
test('Diagnostics channel - connect error', (t) => {
const connectError = new Error('custom error')
const assert = tspl(t, { plan: 16 })
let _connector
diagnosticsChannel.channel('undici:client:beforeConnect').subscribe(({ connectParams, connector }) => {
_connector = connector
assert.equal(typeof _connector, 'function')
assert.equal(Object.keys(connectParams).length, 7)
const { host, hostname, protocol, port, servername } = connectParams
assert.equal(host, 'localhost:1234')
assert.equal(hostname, 'localhost')
assert.equal(port, '1234')
assert.equal(protocol, 'http:')
assert.equal(servername, null)
})
diagnosticsChannel.channel('undici:client:connectError').subscribe(({ error, connectParams, connector }) => {
assert.equal(Object.keys(connectParams).length, 7)
assert.equal(_connector, connector)
const { host, hostname, protocol, port, servername } = connectParams
assert.equal(error, connectError)
assert.equal(host, 'localhost:1234')
assert.equal(hostname, 'localhost')
assert.equal(port, '1234')
assert.equal(protocol, 'http:')
assert.equal(servername, null)
})
const client = new Client('http://localhost:1234', {
connect: (_, cb) => { cb(connectError, null) }
})
return new Promise((resolve) => {
client.request({
path: '/',
method: 'GET'
}, (err, data) => {
assert.equal(err, connectError)
client.close()
resolve()
})
})
})
================================================
FILE: test/node-test/diagnostics-channel/error.js
================================================
'use strict'
const { test, after } = require('node:test')
const { tspl } = require('@matteo.collina/tspl')
const diagnosticsChannel = require('node:diagnostics_channel')
const { Client } = require('../../..')
const { createServer } = require('node:http')
test('Diagnostics channel - error', (t) => {
const assert = tspl(t, { plan: 3 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.destroy()
})
after(server.close.bind(server))
const reqHeaders = {
foo: undefined,
bar: 'bar'
}
let _req
diagnosticsChannel.channel('undici:request:create').subscribe(({ request }) => {
_req = request
})
diagnosticsChannel.channel('undici:request:error').subscribe(({ request, error }) => {
assert.equal(_req, request)
assert.equal(error.code, 'UND_ERR_SOCKET')
})
return new Promise((resolve) => {
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`, {
keepAliveTimeout: 300e3
})
client.request({
path: '/',
method: 'GET',
headers: reqHeaders
}, (err, data) => {
assert.equal(err.code, 'UND_ERR_SOCKET')
client.close()
resolve()
})
})
})
})
================================================
FILE: test/node-test/diagnostics-channel/get-h2.js
================================================
'use strict'
const { tspl } = require('@matteo.collina/tspl')
const { test, after } = require('node:test')
const { createSecureServer } = require('node:http2')
const diagnosticsChannel = require('node:diagnostics_channel')
const { once } = require('node:events')
const pem = require('@metcoder95/https-pem')
const { Client } = require('../../..')
test('Diagnostics channel - get support H2', async t => {
const server = createSecureServer(pem)
server.on('stream', (stream, headers, _flags, rawHeaders) => {
t.strictEqual(headers['x-my-header'], 'foo')
t.strictEqual(headers[':method'], 'GET')
stream.respond({
'content-type': 'text/plain; charset=utf-8',
'x-custom-h2': 'hello',
':status': 200
})
stream.end('hello h2!')
})
server.listen(0)
await once(server, 'listening')
diagnosticsChannel.channel('undici:request:create').subscribe(({ request }) => {
t.strictEqual(request.origin, `https://localhost:${server.address().port}`)
t.strictEqual(request.completed, false)
t.strictEqual(request.method, 'GET')
t.strictEqual(request.path, '/')
})
let _socket
diagnosticsChannel.channel('undici:client:connected').subscribe(({ socket }) => {
_socket = socket
})
diagnosticsChannel.channel('undici:client:sendHeaders').subscribe(({ headers, socket }) => {
t.strictEqual(_socket, socket)
const expectedHeaders = [
'x-my-header: foo',
`:authority: localhost:${server.address().port}`,
':method: GET',
':path: /',
':scheme: https'
]
t.strictEqual(headers, expectedHeaders.join('\r\n') + '\r\n')
})
const client = new Client(`https://localhost:${server.address().port}`, {
connect: {
rejectUnauthorized: false
},
allowH2: true
})
t = tspl(t, { plan: 24 })
after(() => server.close())
after(() => client.close())
let body = []
let response = await client.request({
path: '/',
method: 'GET',
headers: {
'x-my-header': 'foo'
}
})
response.body.on('data', chunk => {
body.push(chunk)
})
await once(response.body, 'end')
t.strictEqual(response.statusCode, 200)
t.strictEqual(response.headers['content-type'], 'text/plain; charset=utf-8')
t.strictEqual(response.headers['x-custom-h2'], 'hello')
t.strictEqual(Buffer.concat(body).toString('utf8'), 'hello h2!')
// request again
body = []
response = await client.request({
path: '/',
method: 'GET',
headers: {
'x-my-header': 'foo'
}
})
response.body.on('data', chunk => {
body.push(chunk)
})
await once(response.body, 'end')
t.strictEqual(response.statusCode, 200)
t.strictEqual(response.headers['content-type'], 'text/plain; charset=utf-8')
t.strictEqual(response.headers['x-custom-h2'], 'hello')
t.strictEqual(Buffer.concat(body).toString('utf8'), 'hello h2!')
})
================================================
FILE: test/node-test/diagnostics-channel/get.js
================================================
'use strict'
const { test, after } = require('node:test')
const { tspl } = require('@matteo.collina/tspl')
const diagnosticsChannel = require('node:diagnostics_channel')
const { Client } = require('../../..')
const { createServer } = require('node:http')
test('Diagnostics channel - get', (t) => {
const assert = tspl(t, { plan: 36 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.setHeader('Content-Type', 'text/plain')
res.setHeader('trailer', 'foo')
res.write('hello')
res.addTrailers({
foo: 'oof'
})
res.end()
})
after(server.close.bind(server))
const reqHeaders = {
foo: undefined,
bar: 'bar'
}
let _req
diagnosticsChannel.channel('undici:request:create').subscribe(({ request }) => {
_req = request
assert.equal(request.origin, `http://localhost:${server.address().port}`)
assert.equal(request.completed, false)
assert.equal(request.method, 'GET')
assert.equal(request.path, '/')
assert.deepStrictEqual(request.headers, ['bar', 'bar'])
request.addHeader('hello', 'world')
assert.deepStrictEqual(request.headers, ['bar', 'bar', 'hello', 'world'])
})
let _connector
diagnosticsChannel.channel('undici:client:beforeConnect').subscribe(({ connectParams, connector }) => {
_connector = connector
assert.equal(typeof _connector, 'function')
assert.equal(Object.keys(connectParams).length, 7)
const { host, hostname, protocol, port, servername } = connectParams
assert.equal(host, `localhost:${server.address().port}`)
assert.equal(hostname, 'localhost')
assert.equal(port, String(server.address().port))
assert.equal(protocol, 'http:')
assert.equal(servername, null)
})
let _socket
diagnosticsChannel.channel('undici:client:connected').subscribe(({ connectParams, socket, connector }) => {
_socket = socket
assert.equal(_connector, connector)
assert.equal(Object.keys(connectParams).length, 7)
const { host, hostname, protocol, port, servername } = connectParams
assert.equal(host, `localhost:${server.address().port}`)
assert.equal(hostname, 'localhost')
assert.equal(port, String(server.address().port))
assert.equal(protocol, 'http:')
assert.equal(servername, null)
})
diagnosticsChannel.channel('undici:client:sendHeaders').subscribe(({ request, headers, socket }) => {
assert.equal(_req, request)
assert.equal(_socket, socket)
const expectedHeaders = [
'GET / HTTP/1.1',
`host: localhost:${server.address().port}`,
'connection: keep-alive',
'bar: bar',
'hello: world'
]
assert.deepStrictEqual(headers, expectedHeaders.join('\r\n') + '\r\n')
})
diagnosticsChannel.channel('undici:request:headers').subscribe(({ request, response }) => {
assert.equal(_req, request)
assert.equal(response.statusCode, 200)
const expectedHeaders = [
Buffer.from('Content-Type'),
Buffer.from('text/plain'),
Buffer.from('trailer'),
Buffer.from('foo'),
Buffer.from('Date'),
response.headers[5], // This is a date
Buffer.from('Connection'),
Buffer.from('keep-alive'),
Buffer.from('Keep-Alive'),
Buffer.from('timeout=5'),
Buffer.from('Transfer-Encoding'),
Buffer.from('chunked')
]
assert.deepStrictEqual(response.headers, expectedHeaders)
assert.equal(response.statusText, 'OK')
})
let bodySent = false
diagnosticsChannel.channel('undici:request:bodySent').subscribe(({ request }) => {
assert.equal(_req, request)
bodySent = true
})
diagnosticsChannel.channel('undici:request:bodyChunkSent').subscribe(() => {
assert.fail('should not emit undici:request:bodyChunkSent for GET requests')
})
let endEmitted = false
return new Promise((resolve) => {
const respChunks = []
diagnosticsChannel.channel('undici:request:bodyChunkReceived').subscribe(({ request, chunk }) => {
assert.equal(_req, request)
respChunks.push(chunk)
})
diagnosticsChannel.channel('undici:request:trailers').subscribe(({ request, trailers }) => {
assert.equal(bodySent, true)
assert.equal(request.completed, true)
assert.equal(_req, request)
// This event is emitted after the last chunk has been added to the body stream,
// not when it was consumed by the application
assert.equal(endEmitted, false)
assert.deepStrictEqual(trailers, [Buffer.from('foo'), Buffer.from('oof')])
const respData = Buffer.concat(respChunks)
assert.deepStrictEqual(respData, Buffer.from('hello'))
resolve()
})
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`, {
keepAliveTimeout: 300e3
})
client.request({
path: '/',
method: 'GET',
headers: reqHeaders
}, (err, data) => {
assert.ok(!err)
client.close()
data.body.on('end', function () {
endEmitted = true
})
})
})
})
})
================================================
FILE: test/node-test/diagnostics-channel/post-stream.js
================================================
'use strict'
const { test, after } = require('node:test')
const { tspl } = require('@matteo.collina/tspl')
const { Readable } = require('node:stream')
const diagnosticsChannel = require('node:diagnostics_channel')
const { Client } = require('../../..')
const { createServer } = require('node:http')
test('Diagnostics channel - post stream', (t) => {
const assert = tspl(t, { plan: 43 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
req.resume()
res.setHeader('Content-Type', 'text/plain')
res.setHeader('trailer', 'foo')
res.write('hello')
res.addTrailers({
foo: 'oof'
})
res.end()
})
after(server.close.bind(server))
const reqHeaders = {
foo: undefined,
bar: 'bar'
}
const body = Readable.from(['hello', ' ', 'world'])
let _req
diagnosticsChannel.channel('undici:request:create').subscribe(({ request }) => {
_req = request
assert.equal(request.completed, false)
assert.equal(request.method, 'POST')
assert.equal(request.path, '/')
assert.deepStrictEqual(request.headers, ['bar', 'bar'])
request.addHeader('hello', 'world')
assert.deepStrictEqual(request.headers, ['bar', 'bar', 'hello', 'world'])
assert.deepStrictEqual(request.body, body)
})
let _connector
diagnosticsChannel.channel('undici:client:beforeConnect').subscribe(({ connectParams, connector }) => {
_connector = connector
assert.equal(typeof _connector, 'function')
assert.equal(Object.keys(connectParams).length, 7)
const { host, hostname, protocol, port, servername } = connectParams
assert.equal(host, `localhost:${server.address().port}`)
assert.equal(hostname, 'localhost')
assert.equal(port, String(server.address().port))
assert.equal(protocol, 'http:')
assert.equal(servername, null)
})
let _socket
diagnosticsChannel.channel('undici:client:connected').subscribe(({ connectParams, socket, connector }) => {
_socket = socket
assert.equal(Object.keys(connectParams).length, 7)
assert.equal(_connector, connector)
const { host, hostname, protocol, port, servername } = connectParams
assert.equal(host, `localhost:${server.address().port}`)
assert.equal(hostname, 'localhost')
assert.equal(port, String(server.address().port))
assert.equal(protocol, 'http:')
assert.equal(servername, null)
})
diagnosticsChannel.channel('undici:client:sendHeaders').subscribe(({ request, headers, socket }) => {
assert.equal(_req, request)
assert.equal(_socket, socket)
const expectedHeaders = [
'POST / HTTP/1.1',
`host: localhost:${server.address().port}`,
'connection: keep-alive',
'bar: bar',
'hello: world'
]
assert.equal(headers, expectedHeaders.join('\r\n') + '\r\n')
})
diagnosticsChannel.channel('undici:request:headers').subscribe(({ request, response }) => {
assert.equal(_req, request)
assert.equal(response.statusCode, 200)
const expectedHeaders = [
Buffer.from('Content-Type'),
Buffer.from('text/plain'),
Buffer.from('trailer'),
Buffer.from('foo'),
Buffer.from('Date'),
response.headers[5], // This is a date
Buffer.from('Connection'),
Buffer.from('keep-alive'),
Buffer.from('Keep-Alive'),
Buffer.from('timeout=5'),
Buffer.from('Transfer-Encoding'),
Buffer.from('chunked')
]
assert.deepStrictEqual(response.headers, expectedHeaders)
assert.equal(response.statusText, 'OK')
})
let bodySent = false
const bodyChunks = []
diagnosticsChannel.channel('undici:request:bodyChunkSent').subscribe(({ request, chunk }) => {
assert.equal(_req, request)
// Chunk can be a string or a Buffer, depending on the stream writer.
assert.equal(typeof chunk, 'string')
bodyChunks.push(Buffer.from(chunk))
})
diagnosticsChannel.channel('undici:request:bodySent').subscribe(({ request }) => {
assert.equal(_req, request)
bodySent = true
const requestBody = Buffer.concat(bodyChunks)
assert.deepStrictEqual(requestBody, Buffer.from('hello world'))
})
let endEmitted = false
return new Promise((resolve) => {
const respChunks = []
diagnosticsChannel.channel('undici:request:bodyChunkReceived').subscribe(({ request, chunk }) => {
assert.equal(_req, request)
respChunks.push(chunk)
})
diagnosticsChannel.channel('undici:request:trailers').subscribe(({ request, trailers }) => {
assert.equal(bodySent, true)
assert.equal(request.completed, true)
assert.equal(_req, request)
// This event is emitted after the last chunk has been added to the body stream,
// not when it was consumed by the application
assert.equal(endEmitted, false)
assert.deepStrictEqual(trailers, [Buffer.from('foo'), Buffer.from('oof')])
const respData = Buffer.concat(respChunks)
assert.deepStrictEqual(respData, Buffer.from('hello'))
resolve()
})
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`, {
keepAliveTimeout: 300e3
})
client.request({
path: '/',
method: 'POST',
headers: reqHeaders,
body
}, (err, data) => {
assert.ok(!err)
client.close()
data.body.on('end', function () {
endEmitted = true
})
})
})
})
})
================================================
FILE: test/node-test/diagnostics-channel/post.js
================================================
'use strict'
const { test, after } = require('node:test')
const { tspl } = require('@matteo.collina/tspl')
const diagnosticsChannel = require('node:diagnostics_channel')
const { Client } = require('../../../')
const { createServer } = require('node:http')
test('Diagnostics channel - post', (t) => {
const assert = tspl(t, { plan: 39 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
req.resume()
res.setHeader('Content-Type', 'text/plain')
res.setHeader('trailer', 'foo')
res.write('hello')
res.addTrailers({
foo: 'oof'
})
res.end()
})
after(server.close.bind(server))
const reqHeaders = {
foo: undefined,
bar: 'bar'
}
let _req
diagnosticsChannel.channel('undici:request:create').subscribe(({ request }) => {
_req = request
assert.equal(request.completed, false)
assert.equal(request.method, 'POST')
assert.equal(request.path, '/')
assert.deepStrictEqual(request.headers, ['bar', 'bar'])
request.addHeader('hello', 'world')
assert.deepStrictEqual(request.headers, ['bar', 'bar', 'hello', 'world'])
assert.deepStrictEqual(request.body, Buffer.from('hello world'))
})
let _connector
diagnosticsChannel.channel('undici:client:beforeConnect').subscribe(({ connectParams, connector }) => {
_connector = connector
assert.equal(typeof _connector, 'function')
assert.equal(Object.keys(connectParams).length, 7)
const { host, hostname, protocol, port, servername } = connectParams
assert.equal(host, `localhost:${server.address().port}`)
assert.equal(hostname, 'localhost')
assert.equal(port, String(server.address().port))
assert.equal(protocol, 'http:')
assert.equal(servername, null)
})
let _socket
diagnosticsChannel.channel('undici:client:connected').subscribe(({ connectParams, socket, connector }) => {
_socket = socket
assert.equal(Object.keys(connectParams).length, 7)
assert.equal(_connector, connector)
const { host, hostname, protocol, port, servername } = connectParams
assert.equal(host, `localhost:${server.address().port}`)
assert.equal(hostname, 'localhost')
assert.equal(port, String(server.address().port))
assert.equal(protocol, 'http:')
assert.equal(servername, null)
})
diagnosticsChannel.channel('undici:client:sendHeaders').subscribe(({ request, headers, socket }) => {
assert.equal(_req, request)
assert.equal(_socket, socket)
const expectedHeaders = [
'POST / HTTP/1.1',
`host: localhost:${server.address().port}`,
'connection: keep-alive',
'bar: bar',
'hello: world'
]
assert.deepStrictEqual(headers, expectedHeaders.join('\r\n') + '\r\n')
})
diagnosticsChannel.channel('undici:request:headers').subscribe(({ request, response }) => {
assert.equal(_req, request)
assert.equal(response.statusCode, 200)
const expectedHeaders = [
Buffer.from('Content-Type'),
Buffer.from('text/plain'),
Buffer.from('trailer'),
Buffer.from('foo'),
Buffer.from('Date'),
response.headers[5], // This is a date
Buffer.from('Connection'),
Buffer.from('keep-alive'),
Buffer.from('Keep-Alive'),
Buffer.from('timeout=5'),
Buffer.from('Transfer-Encoding'),
Buffer.from('chunked')
]
assert.deepStrictEqual(response.headers, expectedHeaders)
assert.equal(response.statusText, 'OK')
})
let bodySent = false
const bodyChunks = []
diagnosticsChannel.channel('undici:request:bodyChunkSent').subscribe(({ request, chunk }) => {
assert.equal(_req, request)
assert.equal(Buffer.isBuffer(chunk), true)
bodyChunks.push(chunk)
})
diagnosticsChannel.channel('undici:request:bodySent').subscribe(({ request }) => {
assert.equal(_req, request)
bodySent = true
const requestBody = Buffer.concat(bodyChunks)
assert.deepStrictEqual(requestBody, Buffer.from('hello world'))
})
let endEmitted = false
return new Promise((resolve) => {
const respChunks = []
diagnosticsChannel.channel('undici:request:bodyChunkReceived').subscribe(({ request, chunk }) => {
assert.equal(_req, request)
respChunks.push(chunk)
})
diagnosticsChannel.channel('undici:request:trailers').subscribe(({ request, trailers }) => {
assert.equal(bodySent, true)
assert.equal(request.completed, true)
assert.equal(_req, request)
// This event is emitted after the last chunk has been added to the body stream,
// not when it was consumed by the application
assert.equal(endEmitted, false)
assert.deepStrictEqual(trailers, [Buffer.from('foo'), Buffer.from('oof')])
const respData = Buffer.concat(respChunks)
assert.deepStrictEqual(respData, Buffer.from('hello'))
resolve()
})
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`, {
keepAliveTimeout: 300e3
})
client.request({
path: '/',
method: 'POST',
headers: reqHeaders,
body: 'hello world'
}, (err, data) => {
assert.ok(!err)
client.close()
data.body.on('end', function () {
endEmitted = true
})
})
})
})
})
================================================
FILE: test/node-test/large-body.js
================================================
'use strict'
const { test } = require('node:test')
const { createServer } = require('node:http')
const { request } = require('../../')
const { strictEqual } = require('node:assert')
test('socket should not be reused unless body is consumed', async (t) => {
const LARGE_BODY = 'x'.repeat(10000000)
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
if (req.url === '/foo') {
res.end(LARGE_BODY)
return
}
if (req.url === '/bar') {
res.end('bar')
return
}
throw new Error('Unexpected request url: ' + req.url)
})
await new Promise((resolve) => { server.listen(0, resolve) })
t.after(() => { server.close() })
// Works fine
// const fooRes = await request('http://localhost:3000/foo')
// const fooBody = await fooRes.body.text()
// const barRes = await request('http://localhost:3000/bar')
// await barRes.body.text()
const port = server.address().port
// Fails with:
const fooRes = await request(`http://localhost:${port}/foo`)
const barRes = await request(`http://localhost:${port}/bar`)
const fooBody = await fooRes.body.text()
await barRes.body.text()
strictEqual(fooRes.headers['content-length'], String(LARGE_BODY.length))
strictEqual(fooBody.length, LARGE_BODY.length)
strictEqual(fooBody, LARGE_BODY)
})
================================================
FILE: test/node-test/tree.js
================================================
'use strict'
const { TernarySearchTree, tree } = require('../../lib/core/tree')
const { wellknownHeaderNames, headerNameLowerCasedRecord } = require('../../lib/core/constants')
const { describe, test } = require('node:test')
const assert = require('node:assert')
describe('Ternary Search Tree', () => {
test('The empty key cannot be added.', () => {
assert.throws(() => new TernarySearchTree().insert('', ''))
const tst = new TernarySearchTree()
tst.insert('a', 'a')
assert.throws(() => tst.insert('', ''))
})
test('looking up not inserted key returns null', () => {
const tst = new TernarySearchTree()
tst.insert('a', 'a')
assert.strictEqual(tst.lookup(Buffer.from('non-existent')), null)
})
test('not ascii string', () => {
assert.throws(() => new TernarySearchTree().insert('\x80', 'a'))
const tst = new TernarySearchTree()
tst.insert('a', 'a')
// throw on TstNode
assert.throws(() => tst.insert('\x80', 'a'))
})
test('duplicate key', () => {
const tst = new TernarySearchTree()
const key = 'a'
const lookupKey = Buffer.from(key)
tst.insert(key, 'a')
assert.strictEqual(tst.lookup(lookupKey), 'a')
tst.insert(key, 'b')
assert.strictEqual(tst.lookup(lookupKey), 'b')
})
test('tree', () => {
for (let i = 0; i < wellknownHeaderNames.length; ++i) {
const key = wellknownHeaderNames[i]
assert.strictEqual(tree.lookup(Buffer.from(key)), headerNameLowerCasedRecord[key])
}
})
test('fuzz', () => {
const LENGTH = 2000
const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
const charactersLength = characters.length
function generateAsciiString (length) {
let result = ''
for (let i = 0; i < length; ++i) {
result += characters[Math.floor(Math.random() * charactersLength)]
}
return result
}
const tst = new TernarySearchTree()
/** @type {string[]} */
const random = new Array(LENGTH)
/** @type {Buffer[]} */
const randomBuffer = new Array(LENGTH)
for (let i = 0; i < LENGTH; ++i) {
const key = generateAsciiString((Math.random() * 100 + 5) | 0)
const lowerCasedKey = random[i] = key.toLowerCase()
randomBuffer[i] = Buffer.from(key)
tst.insert(lowerCasedKey, lowerCasedKey)
}
for (let i = 0; i < LENGTH; ++i) {
assert.strictEqual(tst.lookup(randomBuffer[i]), random[i])
}
})
})
================================================
FILE: test/node-test/unix.js
================================================
'use strict'
const { test } = require('node:test')
const { Client, Pool } = require('../../')
const http = require('node:http')
const https = require('node:https')
const pem = require('@metcoder95/https-pem')
const fs = require('node:fs')
const { tspl } = require('@matteo.collina/tspl')
const skip = process.platform === 'win32'
test('http unix get', { skip }, async (t) => {
let client
const p = tspl(t, { plan: 7 })
const server = http.createServer({ joinDuplicateHeaders: true }, (req, res) => {
p.equal('/', req.url)
p.equal('GET', req.method)
p.equal('localhost', req.headers.host)
res.setHeader('Content-Type', 'text/plain')
res.end('hello')
})
t.after(() => {
server.close()
client.close()
})
try {
fs.unlinkSync('/var/tmp/test3.sock')
} catch (err) {
}
server.listen('/var/tmp/test3.sock', () => {
client = new Client({
hostname: 'localhost',
protocol: 'http:'
}, {
socketPath: '/var/tmp/test3.sock'
})
client.request({ path: '/', method: 'GET' }, (err, data) => {
p.ifError(err)
const { statusCode, headers, body } = data
p.equal(statusCode, 200)
p.equal(headers['content-type'], 'text/plain')
const bufs = []
body.on('data', (buf) => {
bufs.push(buf)
})
body.on('end', () => {
p.equal('hello', Buffer.concat(bufs).toString('utf8'))
})
})
})
await p.completed
})
test('http unix get pool', { skip }, async (t) => {
let client
const p = tspl(t, { plan: 7 })
const server = http.createServer({ joinDuplicateHeaders: true }, (req, res) => {
p.equal('/', req.url)
p.equal('GET', req.method)
p.equal('localhost', req.headers.host)
res.setHeader('Content-Type', 'text/plain')
res.end('hello')
})
t.after(() => {
server.close()
client.close()
})
try {
fs.unlinkSync('/var/tmp/test3.sock')
} catch (err) {
}
server.listen('/var/tmp/test3.sock', () => {
client = new Pool({
hostname: 'localhost',
protocol: 'http:'
}, {
socketPath: '/var/tmp/test3.sock'
})
client.request({ path: '/', method: 'GET' }, (err, data) => {
p.ifError(err)
const { statusCode, headers, body } = data
p.equal(statusCode, 200)
p.equal(headers['content-type'], 'text/plain')
const bufs = []
body.on('data', (buf) => {
bufs.push(buf)
})
body.on('end', () => {
p.equal('hello', Buffer.concat(bufs).toString('utf8'))
})
})
})
await p.completed
})
test('https get with tls opts', { skip }, async (t) => {
let client
const p = tspl(t, { plan: 6 })
const server = https.createServer({ ...pem, joinDuplicateHeaders: true }, (req, res) => {
p.equal('/', req.url)
p.equal('GET', req.method)
res.setHeader('content-type', 'text/plain')
res.end('hello')
})
t.after(() => {
server.close()
client.close()
})
try {
fs.unlinkSync('/var/tmp/test3.sock')
} catch (err) {
}
server.listen('/var/tmp/test3.sock', () => {
client = new Client({
hostname: 'localhost',
protocol: 'https:'
}, {
socketPath: '/var/tmp/test3.sock',
tls: {
rejectUnauthorized: false
}
})
client.request({ path: '/', method: 'GET' }, (err, data) => {
p.ifError(err)
const { statusCode, headers, body } = data
p.equal(statusCode, 200)
p.equal(headers['content-type'], 'text/plain')
const bufs = []
body.on('data', (buf) => {
bufs.push(buf)
})
body.on('end', () => {
p.equal('hello', Buffer.concat(bufs).toString('utf8'))
})
})
})
await p.completed
})
================================================
FILE: test/node-test/util.js
================================================
'use strict'
const { test } = require('node:test')
const assert = require('node:assert')
const { Stream } = require('node:stream')
const { EventEmitter } = require('node:events')
const util = require('../../lib/core/util')
const { headerNameLowerCasedRecord } = require('../../lib/core/constants')
const { InvalidArgumentError } = require('../../lib/core/errors')
test('isStream', () => {
const stream = new Stream()
assert.ok(util.isStream(stream))
const buffer = Buffer.alloc(0)
assert.ok(util.isStream(buffer) === false)
const ee = new EventEmitter()
assert.ok(util.isStream(ee) === false)
})
test('getServerName', () => {
assert.equal(util.getServerName('1.1.1.1'), '')
assert.equal(util.getServerName('1.1.1.1:443'), '')
assert.equal(util.getServerName('example.com'), 'example.com')
assert.equal(util.getServerName('example.com:80'), 'example.com')
assert.equal(util.getServerName('[2606:4700:4700::1111]'), '')
assert.equal(util.getServerName('[2606:4700:4700::1111]:443'), '')
})
test('assertRequestHandler', () => {
assert.throws(() => util.assertRequestHandler(null), InvalidArgumentError, 'handler must be an object')
assert.throws(() => util.assertRequestHandler({
onConnect: null
}), InvalidArgumentError, 'invalid onConnect method')
assert.throws(() => util.assertRequestHandler({
onConnect: () => {},
onError: null
}), InvalidArgumentError, 'invalid onError method')
assert.throws(() => util.assertRequestHandler({
onConnect: () => {},
onError: () => {},
onBodySent: null
}), InvalidArgumentError, 'invalid onBodySent method')
assert.throws(() => util.assertRequestHandler({
onConnect: () => {},
onError: () => {},
onBodySent: () => {},
onHeaders: null
}), InvalidArgumentError, 'invalid onHeaders method')
assert.throws(() => util.assertRequestHandler({
onConnect: () => {},
onError: () => {},
onBodySent: () => {},
onHeaders: () => {},
onData: null
}), InvalidArgumentError, 'invalid onData method')
assert.throws(() => util.assertRequestHandler({
onConnect: () => {},
onError: () => {},
onBodySent: () => {},
onHeaders: () => {},
onData: () => {},
onComplete: null
}), InvalidArgumentError, 'invalid onComplete method')
assert.throws(() => util.assertRequestHandler({
onConnect: () => {},
onError: () => {},
onBodySent: () => {},
onUpgrade: 'null'
}, 'CONNECT'), InvalidArgumentError, 'invalid onUpgrade method')
assert.throws(() => util.assertRequestHandler({
onConnect: () => {},
onError: () => {},
onBodySent: () => {},
onUpgrade: 'null'
}, 'CONNECT', () => {}), InvalidArgumentError, 'invalid onUpgrade method')
})
test('parseHeaders', () => {
assert.deepEqual(util.parseHeaders(['key', 'value']), { key: 'value' })
assert.deepEqual(util.parseHeaders([Buffer.from('key'), Buffer.from('value')]), { key: 'value' })
assert.deepEqual(util.parseHeaders(['Key', 'Value']), { key: 'Value' })
assert.deepEqual(util.parseHeaders(['Key', 'value', 'key', 'Value']), { key: ['value', 'Value'] })
assert.deepEqual(util.parseHeaders(['key', ['value1', 'value2', 'value3']]), { key: ['value1', 'value2', 'value3'] })
assert.deepEqual(util.parseHeaders([Buffer.from('key'), [Buffer.from('value1'), Buffer.from('value2'), Buffer.from('value3')]]), { key: ['value1', 'value2', 'value3'] })
})
test('parseHeaders decodes values as latin1, not utf8', () => {
// These bytes (0xE2, 0x80, 0xA6) are the UTF-8 encoding of U+2026 (ellipsis)
// When decoded as latin1, they should be 3 separate characters: â, €, ¦
// When incorrectly decoded as UTF-8, they would be a single character: …
const latin1Bytes = Buffer.from([0xe2, 0x80, 0xa6])
const result = util.parseHeaders([Buffer.from('x-test'), latin1Bytes])
assert.strictEqual(result['x-test'].length, 3)
assert.strictEqual(result['x-test'].charCodeAt(0), 0xe2)
assert.strictEqual(result['x-test'].charCodeAt(1), 0x80)
assert.strictEqual(result['x-test'].charCodeAt(2), 0xa6)
})
test('parseHeaders decodes duplicate header values as latin1', () => {
const latin1Bytes = Buffer.from([0xe2, 0x80, 0xa6])
const result = util.parseHeaders([
Buffer.from('x-test'), Buffer.from('first'),
Buffer.from('x-test'), latin1Bytes
])
assert.deepEqual(result['x-test'][0], 'first')
assert.strictEqual(result['x-test'][1].length, 3)
assert.strictEqual(result['x-test'][1].charCodeAt(0), 0xe2)
})
test('parseHeaders decodes array header values as latin1', () => {
const latin1Bytes = Buffer.from([0xe2, 0x80, 0xa6])
const result = util.parseHeaders([Buffer.from('x-test'), [latin1Bytes, latin1Bytes]])
assert.strictEqual(result['x-test'].length, 2)
assert.strictEqual(result['x-test'][0].length, 3)
assert.strictEqual(result['x-test'][0].charCodeAt(0), 0xe2)
})
test('parseRawHeaders', () => {
assert.deepEqual(util.parseRawHeaders(['key', 'value', Buffer.from('key'), Buffer.from('value')]), ['key', 'value', 'key', 'value'])
assert.deepEqual(util.parseRawHeaders(['content-length', 'value', 'content-disposition', 'form-data; name="fieldName"']), ['content-length', 'value', 'content-disposition', 'form-data; name="fieldName"'])
})
test('parseRawHeaders decodes values as latin1, not utf8', () => {
// These bytes (0xE2, 0x80, 0xA6) are the UTF-8 encoding of U+2026 (ellipsis)
// When decoded as latin1, they should be 3 separate characters
// When incorrectly decoded as UTF-8, they would be a single character
const latin1Bytes = Buffer.from([0xe2, 0x80, 0xa6])
const result = util.parseRawHeaders([Buffer.from('x-test'), latin1Bytes])
assert.strictEqual(result[0], 'x-test')
assert.strictEqual(result[1].length, 3)
assert.strictEqual(result[1].charCodeAt(0), 0xe2)
assert.strictEqual(result[1].charCodeAt(1), 0x80)
assert.strictEqual(result[1].charCodeAt(2), 0xa6)
})
test('serializePathWithQuery', () => {
const tests = [
[{ id: BigInt(123456) }, 'id=123456'],
[{ date: new Date() }, 'date='],
[{ obj: { id: 1 } }, 'obj='],
[{ params: ['a', 'b', 'c'] }, 'params=a¶ms=b¶ms=c'],
[{ bool: true }, 'bool=true'],
[{ number: 123456 }, 'number=123456'],
[{ string: 'hello' }, 'string=hello'],
[{ null: null }, 'null='],
[{ void: undefined }, 'void='],
[{ fn: function () {} }, 'fn='],
[{}, '']
]
const base = 'https://www.google.com'
for (const [input, output] of tests) {
const expected = `${base}${output ? `?${output}` : output}`
assert.deepEqual(util.serializePathWithQuery(base, input), expected)
}
})
test('headerNameLowerCasedRecord', () => {
assert.ok(typeof headerNameLowerCasedRecord.hasOwnProperty !== 'function')
})
================================================
FILE: test/node-test/validations.js
================================================
'use strict'
const { test } = require('node:test')
const { createServer } = require('node:http')
const { Client } = require('../../')
const { tspl } = require('@matteo.collina/tspl')
test('validations', async t => {
let client
const p = tspl(t, { plan: 10 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.setHeader('content-type', 'text/plain')
res.end('hello')
p.fail('server should never be called')
})
t.after(() => {
server.close()
client.close()
})
server.listen(0, async () => {
const url = `http://localhost:${server.address().port}`
client = new Client(url)
await t.test('path', () => {
client.request({ path: null, method: 'GET' }, (err, res) => {
p.equal(err.code, 'UND_ERR_INVALID_ARG')
p.equal(err.message, 'path must be a string')
})
client.request({ path: 'aaa', method: 'GET' }, (err, res) => {
p.equal(err.code, 'UND_ERR_INVALID_ARG')
p.equal(err.message, 'path must be an absolute URL or start with a slash')
})
})
await t.test('method', () => {
client.request({ path: '/', method: null }, (err, res) => {
p.equal(err.code, 'UND_ERR_INVALID_ARG')
p.equal(err.message, 'method must be a string')
})
})
await t.test('body', () => {
client.request({ path: '/', method: 'POST', body: 42 }, (err, res) => {
p.equal(err.code, 'UND_ERR_INVALID_ARG')
p.equal(err.message, 'body must be a string, a Buffer, a Readable stream, an iterable, or an async iterable')
})
client.request({ path: '/', method: 'POST', body: { hello: 'world' } }, (err, res) => {
p.equal(err.code, 'UND_ERR_INVALID_ARG')
p.equal(err.message, 'body must be a string, a Buffer, a Readable stream, an iterable, or an async iterable')
})
})
})
await p.completed
})
================================================
FILE: test/parser-issues.js
================================================
'use strict'
const { tspl } = require('@matteo.collina/tspl')
const { test, after } = require('node:test')
const net = require('node:net')
const { Client, errors } = require('..')
test('https://github.com/mcollina/undici/issues/268', async (t) => {
t = tspl(t, { plan: 2 })
const server = net.createServer(socket => {
socket.write('HTTP/1.1 200 OK\r\n')
socket.write('Transfer-Encoding: chunked\r\n\r\n')
setTimeout(() => {
socket.write('1\r\n')
socket.write('\n\r\n')
setTimeout(() => {
socket.write('1\r\n')
socket.write('\n\r\n')
}, 500)
}, 500)
})
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.destroy())
client.on('disconnect', () => {
if (!client.closed && !client.destroyed) {
t.fail('unexpected disconnect')
}
})
client.request({
method: 'GET',
path: '/nxt/_changes?feed=continuous&heartbeat=5000',
headersTimeout: 1e3
}, (err, data) => {
t.ifError(err)
data.body
.resume()
setTimeout(() => {
t.ok(true, 'pass')
data.body.on('error', () => {})
}, 2e3)
})
})
await t.completed
})
test('parser fail', async (t) => {
t = tspl(t, { plan: 2 })
const server = net.createServer(socket => {
socket.write('HTT/1.1 200 OK\r\n')
})
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.destroy())
client.request({
method: 'GET',
path: '/'
}, (err, data) => {
t.ok(err)
t.ok(err instanceof errors.HTTPParserError)
})
})
await t.completed
})
test('split header field', async (t) => {
t = tspl(t, { plan: 2 })
const server = net.createServer(socket => {
socket.write('HTTP/1.1 200 OK\r\nA')
setTimeout(() => {
socket.write('SD: asd,asd\r\n\r\n\r\n')
}, 100)
})
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.destroy())
client.request({
method: 'GET',
path: '/'
}, (err, data) => {
t.ifError(err)
t.equal(data.headers.asd, 'asd,asd')
data.body.destroy().on('error', () => {})
})
})
await t.completed
})
test('split header value', async (t) => {
t = tspl(t, { plan: 2 })
const server = net.createServer(socket => {
socket.write('HTTP/1.1 200 OK\r\nASD: asd')
setTimeout(() => {
socket.write(',asd\r\n\r\n\r\n')
}, 100)
})
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.destroy())
client.request({
method: 'GET',
path: '/'
}, (err, data) => {
t.ifError(err)
t.equal(data.headers.asd, 'asd,asd')
data.body.destroy().on('error', () => {})
})
})
await t.completed
})
================================================
FILE: test/pipeline-pipelining.js
================================================
'use strict'
const { tspl } = require('@matteo.collina/tspl')
const { test, after } = require('node:test')
const { Client } = require('..')
const { createServer } = require('node:http')
const { kConnect } = require('../lib/core/symbols')
const { kBusy, kPending, kRunning } = require('../lib/core/symbols')
test('pipeline pipelining', async (t) => {
t = tspl(t, { plan: 10 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
t.deepStrictEqual(req.headers['transfer-encoding'], undefined)
res.end()
})
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`, {
pipelining: 2
})
after(() => client.close())
client.on('disconnect', () => {
if (!client.closed && !client.destroyed) {
t.fail('unexpected disconnect')
}
})
client[kConnect](() => {
t.equal(client[kRunning], 0)
client.pipeline({
method: 'GET',
path: '/',
blocking: false
}, ({ body }) => body).end().resume()
t.equal(client[kBusy], true)
t.deepStrictEqual(client[kRunning], 0)
t.deepStrictEqual(client[kPending], 1)
client.pipeline({
method: 'GET',
path: '/',
blocking: false
}, ({ body }) => body).end().resume()
t.equal(client[kBusy], true)
t.deepStrictEqual(client[kRunning], 0)
t.deepStrictEqual(client[kPending], 2)
process.nextTick(() => {
t.equal(client[kRunning], 2)
})
})
})
await t.completed
})
test('pipeline pipelining retry', async (t) => {
t = tspl(t, { plan: 13 })
let count = 0
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
if (count++ === 0) {
res.destroy()
} else {
res.end()
}
})
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`, {
pipelining: 3
})
after(() => client.destroy())
client.once('disconnect', () => {
t.ok(true, 'pass')
})
client[kConnect](() => {
client.pipeline({
method: 'GET',
path: '/',
blocking: false
}, ({ body }) => body).end().resume()
.on('error', (err) => {
t.ok(err)
})
t.equal(client[kBusy], true)
t.deepStrictEqual(client[kRunning], 0)
t.deepStrictEqual(client[kPending], 1)
client.pipeline({
method: 'GET',
path: '/',
blocking: false
}, ({ body }) => body).end().resume()
t.equal(client[kBusy], true)
t.deepStrictEqual(client[kRunning], 0)
t.deepStrictEqual(client[kPending], 2)
client.pipeline({
method: 'GET',
path: '/',
blocking: false
}, ({ body }) => body).end().resume()
t.equal(client[kBusy], true)
t.deepStrictEqual(client[kRunning], 0)
t.deepStrictEqual(client[kPending], 3)
process.nextTick(() => {
t.equal(client[kRunning], 3)
})
client.close(() => {
t.ok(true, 'pass')
})
})
})
await t.completed
})
================================================
FILE: test/pool-connection-error-memory-leak.js
================================================
'use strict'
const { test } = require('node:test')
const assert = require('node:assert')
const { Pool } = require('..')
const { createServer } = require('node:http')
const { kClients } = require('../lib/dispatcher/pool-base')
// This test verifies that clients are properly removed from the pool when they encounter connection errors,
// which is the fix implemented for issue #3895 (memory leak with connection errors)
test('Pool client count does not grow on repeated connection errors', async (t) => {
// Setup a pool pointing to a non-existent server
const pool = new Pool('http://localhost:1', {
connections: 10,
connectTimeout: 100, // Short timeout to speed up the test
bodyTimeout: 100,
headersTimeout: 100
})
try {
const clientCounts = []
// Track initial client count
clientCounts.push(pool[kClients].length)
// Make several requests that will fail with connection errors
const requests = 5
for (let i = 0; i < requests; i++) {
try {
await pool.request({
path: `/${i}`,
method: 'GET'
})
assert.fail('Request should have failed with a connection error')
} catch (err) {
// We expect connection errors, but the error might be wrapped
assert.ok(
err.code === 'ECONNREFUSED' ||
err.cause?.code === 'ECONNREFUSED' ||
err.code === 'UND_ERR_CONNECT',
`Expected connection error but got: ${err.message} (${err.code})`
)
}
// Track client count after each request
clientCounts.push(pool[kClients].length)
// Small delay to allow for client cleanup
await new Promise(resolve => setTimeout(resolve, 10))
}
// The key test: verify that client count doesn't increase monotonically,
// which would indicate the memory leak that was fixed
const maxCount = Math.max(...clientCounts)
assert.ok(
clientCounts[clientCounts.length - 1] <= maxCount,
`Client count should not increase continuously. Counts: ${clientCounts.join(', ')}`
)
// Ensure the last two counts are similar (stabilized)
const lastCount = clientCounts[clientCounts.length - 1]
const secondLastCount = clientCounts[clientCounts.length - 2]
assert.ok(
Math.abs(lastCount - secondLastCount) <= 1,
`Client count should stabilize. Last counts: ${secondLastCount}, ${lastCount}`
)
// Additional verification: make many more requests to check for significant growth
const moreRequests = 10
const startCount = pool[kClients].length
for (let i = 0; i < moreRequests; i++) {
try {
await pool.request({
path: `/more-${i}`,
method: 'GET'
})
} catch (err) {
// Expected error
}
// Small delay to allow for client cleanup
await new Promise(resolve => setTimeout(resolve, 10))
}
const endCount = pool[kClients].length
// The maximum tolerable growth - some growth may occur due to timing issues,
// but it should be limited and not proportional to the number of requests
const maxGrowth = 3
assert.ok(
endCount - startCount <= maxGrowth,
`Client count should not grow significantly after many failed requests. Start: ${startCount}, End: ${endCount}`
)
} finally {
await pool.close()
}
})
// This test specifically verifies the fix in pool-base.js for connectionError event
test('Pool clients are removed on connectionError event', async (t) => {
// Create a server we'll use to track connection events
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.writeHead(200, { 'Content-Type': 'text/plain' })
res.end('ok')
})
await new Promise(resolve => server.listen(0, resolve))
const port = server.address().port
const pool = new Pool(`http://localhost:${port}`, {
connections: 3 // Small pool to make testing easier
})
try {
// Make an initial successful request to create a client
await pool.request({
path: '/',
method: 'GET'
})
// Save the initial number of clients
const initialCount = pool[kClients].length
assert.ok(initialCount > 0, 'Should have at least one client after a successful request')
// Manually trigger a connectionError on all clients
for (const client of [...pool[kClients]]) {
client.emit('connectionError', 'origin', [client], new Error('Simulated connection error'))
}
// Allow some time for the event to be processed
await new Promise(resolve => setTimeout(resolve, 50))
// After the fix, all clients should be removed when they emit a connectionError
assert.strictEqual(
pool[kClients].length,
0,
'All clients should be removed from pool after connectionError events'
)
// Make another request to verify the pool can create new clients
await pool.request({
path: '/after-error',
method: 'GET'
})
// Verify new clients were created
assert.ok(
pool[kClients].length > 0,
'Pool should create new clients after previous ones were removed'
)
} finally {
await pool.close()
await new Promise(resolve => server.close(resolve))
}
})
================================================
FILE: test/pool.js
================================================
'use strict'
const { tspl } = require('@matteo.collina/tspl')
const { test, after } = require('node:test')
const { EventEmitter } = require('node:events')
const { createServer } = require('node:http')
const net = require('node:net')
const {
finished,
PassThrough,
Readable
} = require('node:stream')
const { promisify } = require('node:util')
const {
kBusy,
kPending,
kRunning,
kSize,
kUrl
} = require('../lib/core/symbols')
const {
Client,
Pool,
errors
} = require('..')
test('throws when connection is infinite', async (t) => {
t = tspl(t, { plan: 2 })
try {
new Pool(null, { connections: 0 / 0 }) // eslint-disable-line
} catch (e) {
t.ok(e instanceof errors.InvalidArgumentError)
t.strictEqual(e.message, 'invalid connections')
}
})
test('throws when connections is negative', async (t) => {
t = tspl(t, { plan: 2 })
try {
new Pool(null, { connections: -1 }) // eslint-disable-line no-new
} catch (e) {
t.ok(e instanceof errors.InvalidArgumentError)
t.strictEqual(e.message, 'invalid connections')
}
})
test('throws when connection is not number', async (t) => {
t = tspl(t, { plan: 2 })
try {
new Pool(null, { connections: true }) // eslint-disable-line no-new
} catch (e) {
t.ok(e instanceof errors.InvalidArgumentError)
t.strictEqual(e.message, 'invalid connections')
}
})
test('throws when factory is not a function', async (t) => {
t = tspl(t, { plan: 2 })
try {
new Pool(null, { factory: '' }) // eslint-disable-line no-new
} catch (e) {
t.ok(e instanceof errors.InvalidArgumentError)
t.strictEqual(e.message, 'factory must be a function.')
}
})
test('does not throw when connect is a function', async (t) => {
t = tspl(t, { plan: 1 })
t.doesNotThrow(() => new Pool('http://localhost', { connect: () => {} }))
})
test('passes socketPath to custom connect function', async (t) => {
t = tspl(t, { plan: 2 })
const connectError = new Error('custom connect error')
const socketPath = '/var/run/test.sock'
const pool = new Pool('http://localhost', {
socketPath,
connect (opts, cb) {
t.strictEqual(opts.socketPath, socketPath)
cb(connectError, null)
}
})
after(() => pool.close())
pool.request({
path: '/',
method: 'GET'
}, (err) => {
t.strictEqual(err, connectError)
})
await t.completed
})
test('connect/disconnect event(s)', async (t) => {
const clients = 2
t = tspl(t, { plan: clients * 6 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.writeHead(200, {
Connection: 'keep-alive',
'Keep-Alive': 'timeout=1s'
})
res.end('ok')
})
after(() => server.close())
server.listen(0, () => {
const pool = new Pool(`http://localhost:${server.address().port}`, {
connections: clients,
keepAliveTimeoutThreshold: 100
})
after(() => pool.close())
pool.on('connect', (origin, [pool, client]) => {
t.strictEqual(client instanceof Client, true)
})
pool.on('disconnect', (origin, [pool, client], error) => {
t.ok(client instanceof Client)
t.ok(error instanceof errors.InformationalError)
t.strictEqual(error.code, 'UND_ERR_INFO')
t.strictEqual(error.message, 'socket idle timeout')
})
for (let i = 0; i < clients; i++) {
pool.request({
path: '/',
method: 'GET'
}, (err, { headers, body }) => {
t.ifError(err)
body.resume()
})
}
})
await t.completed
})
test('basic get', async (t) => {
t = tspl(t, { plan: 14 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
t.strictEqual('/', req.url)
t.strictEqual('GET', req.method)
res.setHeader('content-type', 'text/plain')
res.end('hello')
})
after(() => server.close())
server.listen(0, async () => {
const client = new Pool(`http://localhost:${server.address().port}`)
after(() => client.destroy())
t.strictEqual(client[kUrl].origin, `http://localhost:${server.address().port}`)
client.request({ path: '/', method: 'GET' }, (err, { statusCode, headers, body }) => {
t.ifError(err)
t.strictEqual(statusCode, 200)
t.strictEqual(headers['content-type'], 'text/plain')
const bufs = []
body.on('data', (buf) => {
bufs.push(buf)
})
body.on('end', () => {
t.strictEqual('hello', Buffer.concat(bufs).toString('utf8'))
})
})
t.strictEqual(client.destroyed, false)
t.strictEqual(client.closed, false)
client.close((err) => {
t.ifError(err)
t.strictEqual(client.destroyed, true)
client.destroy((err) => {
t.ifError(err)
client.close((err) => {
t.ok(err instanceof errors.ClientDestroyedError)
})
})
})
t.strictEqual(client.closed, true)
})
await t.completed
})
test('URL as arg', async (t) => {
t = tspl(t, { plan: 9 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
t.strictEqual('/', req.url)
t.strictEqual('GET', req.method)
res.setHeader('content-type', 'text/plain')
res.end('hello')
})
after(() => server.close())
server.listen(0, async () => {
const url = new URL('http://localhost')
url.port = server.address().port
const client = new Pool(url)
after(() => client.destroy())
client.request({ path: '/', method: 'GET' }, (err, { statusCode, headers, body }) => {
t.ifError(err)
t.strictEqual(statusCode, 200)
t.strictEqual(headers['content-type'], 'text/plain')
const bufs = []
body.on('data', (buf) => {
bufs.push(buf)
})
body.on('end', () => {
t.strictEqual('hello', Buffer.concat(bufs).toString('utf8'))
})
})
client.close((err) => {
t.ifError(err)
client.destroy((err) => {
t.ifError(err)
client.close((err) => {
t.ok(err instanceof errors.ClientDestroyedError)
})
})
})
})
await t.completed
})
test('basic get error async/await', async (t) => {
t = tspl(t, { plan: 2 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.destroy()
})
after(() => server.close())
server.listen(0, async () => {
const client = new Pool(`http://localhost:${server.address().port}`)
after(() => client.destroy())
await client.request({ path: '/', method: 'GET' })
.catch((err) => {
t.ok(err)
})
await client.destroy()
await client.close().catch((err) => {
t.ok(err instanceof errors.ClientDestroyedError)
})
})
await t.completed
})
test('basic get with async/await', async (t) => {
t = tspl(t, { plan: 4 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
t.strictEqual('/', req.url)
t.strictEqual('GET', req.method)
res.setHeader('content-type', 'text/plain')
res.end('hello')
})
after(() => server.close())
await promisify(server.listen.bind(server))(0)
const client = new Pool(`http://localhost:${server.address().port}`)
after(() => client.destroy())
const { statusCode, headers, body } = await client.request({ path: '/', method: 'GET' })
t.strictEqual(statusCode, 200)
t.strictEqual(headers['content-type'], 'text/plain')
body.resume()
await promisify(finished)(body)
await client.close()
await client.destroy()
})
test('stream get async/await', async (t) => {
t = tspl(t, { plan: 4 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
t.strictEqual('/', req.url)
t.strictEqual('GET', req.method)
res.setHeader('content-type', 'text/plain')
res.end('hello')
})
after(() => server.close())
await promisify(server.listen.bind(server))(0)
const client = new Pool(`http://localhost:${server.address().port}`)
after(() => client.destroy())
await client.stream({ path: '/', method: 'GET' }, ({ statusCode, headers }) => {
t.strictEqual(statusCode, 200)
t.strictEqual(headers['content-type'], 'text/plain')
return new PassThrough()
})
await t.completed
})
test('stream get error async/await', async (t) => {
t = tspl(t, { plan: 1 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.destroy()
})
after(() => server.close())
server.listen(0, async () => {
const client = new Pool(`http://localhost:${server.address().port}`)
after(() => client.destroy())
await client.stream({ path: '/', method: 'GET' }, () => {
})
.catch((err) => {
t.ok(err)
})
})
await t.completed
})
test('pipeline get', async (t) => {
t = tspl(t, { plan: 5 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
t.strictEqual('/', req.url)
t.strictEqual('GET', req.method)
res.setHeader('content-type', 'text/plain')
res.end('hello')
})
after(() => server.close())
server.listen(0, async () => {
const client = new Pool(`http://localhost:${server.address().port}`)
after(() => client.destroy())
const bufs = []
client.pipeline({ path: '/', method: 'GET' }, ({ statusCode, headers, body }) => {
t.strictEqual(statusCode, 200)
t.strictEqual(headers['content-type'], 'text/plain')
return body
})
.end()
.on('data', (buf) => {
bufs.push(buf)
})
.on('end', () => {
t.strictEqual('hello', Buffer.concat(bufs).toString('utf8'))
})
})
await t.completed
})
test('backpressure algorithm', async (t) => {
t = tspl(t, { plan: 12 })
const seen = []
let total = 0
let writeMore = true
class FakeClient extends EventEmitter {
constructor () {
super()
this.id = total++
}
dispatch (req, handler) {
seen.push({ req, client: this, id: this.id })
return writeMore
}
}
const noopHandler = {
onError (err) {
throw err
}
}
const pool = new Pool('http://notahost', {
factory: () => new FakeClient()
})
pool.dispatch({}, noopHandler)
pool.dispatch({}, noopHandler)
const d1 = seen.shift() // d1 = c0
t.strictEqual(d1.id, 0)
const d2 = seen.shift() // d2 = c0
t.strictEqual(d2.id, 0)
t.strictEqual(d1.id, d2.id)
writeMore = false
pool.dispatch({}, noopHandler) // d3 = c0
pool.dispatch({}, noopHandler) // d4 = c1
const d3 = seen.shift()
t.strictEqual(d3.id, 0)
const d4 = seen.shift()
t.strictEqual(d4.id, 1)
t.strictEqual(d3.id, d2.id)
t.notEqual(d3.id, d4.id)
writeMore = true
d4.client.emit('drain', new URL('http://notahost'), [d4.client])
pool.dispatch({}, noopHandler) // d5 = c1
d3.client.emit('drain', new URL('http://notahost'), [d3.client])
pool.dispatch({}, noopHandler) // d6 = c0
const d5 = seen.shift()
t.strictEqual(d5.id, 1)
const d6 = seen.shift()
t.strictEqual(d6.id, 0)
t.strictEqual(d5.id, d4.id)
t.strictEqual(d3.id, d6.id)
t.strictEqual(total, 3)
t.end()
})
test('busy', async (t) => {
t = tspl(t, { plan: 8 * 16 + 2 + 1 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
t.strictEqual('/', req.url)
t.strictEqual('GET', req.method)
res.setHeader('content-type', 'text/plain')
res.end('hello')
})
after(() => server.close())
const connections = 2
server.listen(0, async () => {
const client = new Pool(`http://localhost:${server.address().port}`, {
connections,
pipelining: 2
})
client.on('drain', () => {
t.ok(true, 'pass')
})
client.on('connect', () => {
t.ok(true, 'pass')
})
after(() => client.destroy())
for (let n = 1; n <= 8; ++n) {
client.request({ path: '/', method: 'GET' }, (err, { statusCode, headers, body }) => {
t.ifError(err)
t.strictEqual(statusCode, 200)
t.strictEqual(headers['content-type'], 'text/plain')
const bufs = []
body.on('data', (buf) => {
bufs.push(buf)
})
body.on('end', () => {
t.strictEqual('hello', Buffer.concat(bufs).toString('utf8'))
})
})
t.strictEqual(client[kPending], n)
t.strictEqual(client[kBusy], n > 1)
t.strictEqual(client[kSize], n)
t.strictEqual(client[kRunning], 0)
t.strictEqual(client.stats.connected, 0)
t.strictEqual(client.stats.free, 0)
t.strictEqual(client.stats.queued, Math.max(n - connections, 0))
t.strictEqual(client.stats.pending, n)
t.strictEqual(client.stats.size, n)
t.strictEqual(client.stats.running, 0)
}
})
await t.completed
})
test('invalid pool dispatch options', async (t) => {
t = tspl(t, { plan: 2 })
const pool = new Pool('http://notahost')
t.throws(() => pool.dispatch({}), errors.InvalidArgumentError, 'throws on invalid handler')
t.throws(() => pool.dispatch({}, {}), errors.InvalidArgumentError, 'throws on invalid handler')
})
test('pool upgrade promise', async (t) => {
t = tspl(t, { plan: 2 })
const server = net.createServer({ joinDuplicateHeaders: true }, (c) => {
c.on('data', (d) => {
c.write('HTTP/1.1 101\r\n')
c.write('hello: world\r\n')
c.write('connection: upgrade\r\n')
c.write('upgrade: websocket\r\n')
c.write('\r\n')
c.write('Body')
})
c.on('end', () => {
c.end()
})
})
after(() => server.close())
server.listen(0, async () => {
const client = new Pool(`http://localhost:${server.address().port}`)
after(() => client.close())
const { headers, socket } = await client.upgrade({
path: '/',
method: 'GET',
protocol: 'Websocket'
})
let recvData = ''
socket.on('data', (d) => {
recvData += d
})
socket.on('close', () => {
t.strictEqual(recvData.toString(), 'Body')
})
t.deepStrictEqual(headers, {
hello: 'world',
connection: 'upgrade',
upgrade: 'websocket'
})
socket.end()
})
await t.completed
})
test('pool connect', async (t) => {
t = tspl(t, { plan: 1 })
const server = createServer({ joinDuplicateHeaders: true }, (c) => {
t.fail()
})
server.on('connect', (req, socket, firstBodyChunk) => {
socket.write('HTTP/1.1 200 Connection established\r\n\r\n')
let data = firstBodyChunk.toString()
socket.on('data', (buf) => {
data += buf.toString()
})
socket.on('end', () => {
socket.end(data)
})
})
after(() => server.close())
server.listen(0, async () => {
const client = new Pool(`http://localhost:${server.address().port}`)
after(() => client.close())
const { socket } = await client.connect({
path: '/'
})
let recvData = ''
socket.on('data', (d) => {
recvData += d
})
socket.on('end', () => {
t.strictEqual(recvData.toString(), 'Body')
})
socket.write('Body')
socket.end()
})
await t.completed
})
test('pool connect with clientTtl specified', async (t) => {
t = tspl(t, { plan: 3 })
const server = createServer({ joinDuplicateHeaders: true }, t.fail)
server.on('connect', (req, socket, firstBodyChunk) => {
socket.write('HTTP/1.1 200 Connection established\r\n\r\n')
let data = firstBodyChunk.toString()
socket.on('data', (buf) => {
data += buf.toString()
})
socket.on('end', () => {
socket.end(data)
})
})
after(() => server.close())
server.listen(0, async () => {
const client = new Pool(`http://localhost:${server.address().port}`, {
clientTtl: 10
})
const { socket } = await client.connect({
path: '/'
})
t.strictEqual(socket.closed, false, 'client not closed yet')
let recvData = ''
socket.on('data', (d) => {
recvData += d
})
socket.on('end', () => {
t.strictEqual(recvData.toString(), 'Body')
})
socket.write('Body')
await new Promise((resolve, reject) => socket.end((e) => e ? reject(e) : resolve()))
t.strictEqual(socket.closed, false, 'client not closed yet')
await new Promise(resolve => setTimeout(resolve, 10))
t.strictEqual(socket.closed, true, 'client closed after ttl')
})
await t.completed
})
test('pool dispatch', async (t) => {
t = tspl(t, { plan: 2 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.end('asd')
})
after(() => server.close())
server.listen(0, async () => {
const client = new Pool(`http://localhost:${server.address().port}`)
after(() => client.close())
let buf = ''
client.dispatch({
path: '/',
method: 'GET'
}, {
onConnect () {
},
onHeaders (statusCode, headers) {
t.strictEqual(statusCode, 200)
},
onData (chunk) {
buf += chunk
},
onComplete () {
t.strictEqual(buf, 'asd')
},
onError () {
}
})
})
await t.completed
})
test('pool pipeline args validation', async (t) => {
t = tspl(t, { plan: 2 })
const client = new Pool('http://localhost:5000')
const ret = client.pipeline(null, () => {})
ret.on('error', (err) => {
t.ok(/opts/.test(err.message))
t.ok(err instanceof errors.InvalidArgumentError)
})
await t.completed
})
test('300 requests succeed', async (t) => {
t = tspl(t, { plan: 300 * 3 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.end('asd')
})
after(() => server.close())
server.listen(0, () => {
const client = new Pool(`http://localhost:${server.address().port}`, {
connections: 1
})
after(() => client.destroy())
for (let n = 0; n < 300; ++n) {
client.request({
path: '/',
method: 'GET'
}, (err, data) => {
t.ifError(err)
data.body.on('data', (chunk) => {
t.strictEqual(chunk.toString(), 'asd')
}).on('end', () => {
t.ok(true, 'pass')
})
})
}
})
await t.completed
})
test('pool connect error', async (t) => {
t = tspl(t, { plan: 1 })
const server = createServer({ joinDuplicateHeaders: true }, (c) => {
t.fail()
})
server.on('connect', (req, socket, firstBodyChunk) => {
socket.destroy()
})
after(() => server.close())
server.listen(0, async () => {
const client = new Pool(`http://localhost:${server.address().port}`)
after(() => client.close())
try {
await client.connect({
path: '/'
})
} catch (err) {
t.ok(err)
}
})
await t.completed
})
test('pool upgrade error', async (t) => {
t = tspl(t, { plan: 1 })
const server = net.createServer({ joinDuplicateHeaders: true }, (c) => {
c.on('data', (d) => {
c.write('HTTP/1.1 101\r\n')
c.write('hello: world\r\n')
c.write('connection: upgrade\r\n')
c.write('\r\n')
c.write('Body')
})
c.on('error', () => {
// Whether we get an error, end or close is undefined.
// Ignore error.
})
})
after(() => server.close())
server.listen(0, async () => {
const client = new Pool(`http://localhost:${server.address().port}`)
after(() => client.close())
try {
await client.upgrade({
path: '/',
method: 'GET',
protocol: 'Websocket'
})
} catch (err) {
t.ok(err)
}
})
await t.completed
})
test('pool dispatch error', async (t) => {
t = tspl(t, { plan: 3 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.end('asd')
})
after(() => server.close())
server.listen(0, async () => {
const client = new Pool(`http://localhost:${server.address().port}`, {
connections: 1,
pipelining: 1
})
after(() => client.close())
client.dispatch({
path: '/',
method: 'GET'
}, {
onConnect () {
},
onHeaders (statusCode, headers) {
t.strictEqual(statusCode, 200)
},
onData (chunk) {
},
onComplete () {
t.ok(true, 'pass')
},
onError () {
}
})
client.dispatch({
path: '/',
method: 'GET',
headers: {
'transfer-encoding': 'fail'
}
}, {
onConnect () {
t.fail()
},
onHeaders (statusCode, headers) {
t.fail()
},
onData (chunk) {
t.fail()
},
onError (err) {
t.strictEqual(err.code, 'UND_ERR_INVALID_ARG')
}
})
})
await t.completed
})
test('pool request abort in queue', async (t) => {
t = tspl(t, { plan: 3 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.end('asd')
})
after(() => server.close())
server.listen(0, async () => {
const client = new Pool(`http://localhost:${server.address().port}`, {
connections: 1,
pipelining: 1
})
after(() => client.close())
client.dispatch({
path: '/',
method: 'GET'
}, {
onConnect () {
},
onHeaders (statusCode, headers) {
t.strictEqual(statusCode, 200)
},
onData (chunk) {
},
onComplete () {
t.ok(true, 'pass')
},
onError () {
}
})
const signal = new EventEmitter()
client.request({
path: '/',
method: 'GET',
signal
}, (err) => {
t.strictEqual(err.code, 'UND_ERR_ABORTED')
})
signal.emit('abort')
})
await t.completed
})
test('pool stream abort in queue', async (t) => {
t = tspl(t, { plan: 3 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.end('asd')
})
after(() => server.close())
server.listen(0, async () => {
const client = new Pool(`http://localhost:${server.address().port}`, {
connections: 1,
pipelining: 1
})
after(() => client.close())
client.dispatch({
path: '/',
method: 'GET'
}, {
onConnect () {
},
onHeaders (statusCode, headers) {
t.strictEqual(statusCode, 200)
},
onData (chunk) {
},
onComplete () {
t.ok(true, 'pass')
},
onError () {
}
})
const signal = new EventEmitter()
client.stream({
path: '/',
method: 'GET',
signal
}, ({ body }) => body, (err) => {
t.strictEqual(err.code, 'UND_ERR_ABORTED')
})
signal.emit('abort')
})
await t.completed
})
test('pool pipeline abort in queue', async (t) => {
t = tspl(t, { plan: 3 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.end('asd')
})
after(() => server.close())
server.listen(0, async () => {
const client = new Pool(`http://localhost:${server.address().port}`, {
connections: 1,
pipelining: 1
})
after(() => client.close())
client.dispatch({
path: '/',
method: 'GET'
}, {
onConnect () {
},
onHeaders (statusCode, headers) {
t.strictEqual(statusCode, 200)
},
onData (chunk) {
},
onComplete () {
t.ok(true, 'pass')
},
onError () {
}
})
const signal = new EventEmitter()
client.pipeline({
path: '/',
method: 'GET',
signal
}, ({ body }) => body).end().on('error', (err) => {
t.strictEqual(err.code, 'UND_ERR_ABORTED')
})
signal.emit('abort')
})
await t.completed
})
test('pool stream constructor error destroy body', async (t) => {
t = tspl(t, { plan: 4 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.end('asd')
})
after(() => server.close())
server.listen(0, async () => {
const client = new Pool(`http://localhost:${server.address().port}`, {
connections: 1,
pipelining: 1
})
after(() => client.close())
{
const body = new Readable({
read () {
}
})
client.stream({
path: '/',
method: 'GET',
body,
headers: {
'transfer-encoding': 'fail'
}
}, () => {
t.fail()
}, (err) => {
t.strictEqual(err.code, 'UND_ERR_INVALID_ARG')
t.strictEqual(body.destroyed, true)
})
}
{
const body = new Readable({
read () {
}
})
client.stream({
path: '/',
method: 'CONNECT',
body
}, () => {
t.fail()
}, (err) => {
t.strictEqual(err.code, 'UND_ERR_INVALID_ARG')
t.strictEqual(body.destroyed, true)
})
}
})
await t.completed
})
test('pool request constructor error destroy body', async (t) => {
t = tspl(t, { plan: 4 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.end('asd')
})
after(() => server.close())
server.listen(0, async () => {
const client = new Pool(`http://localhost:${server.address().port}`, {
connections: 1,
pipelining: 1
})
after(() => client.close())
{
const body = new Readable({
read () {
}
})
client.request({
path: '/',
method: 'GET',
body,
headers: {
'transfer-encoding': 'fail'
}
}, (err) => {
t.strictEqual(err.code, 'UND_ERR_INVALID_ARG')
t.strictEqual(body.destroyed, true)
})
}
{
const body = new Readable({
read () {
}
})
client.request({
path: '/',
method: 'CONNECT',
body
}, (err) => {
t.strictEqual(err.code, 'UND_ERR_INVALID_ARG')
t.strictEqual(body.destroyed, true)
})
}
})
await t.completed
})
test('pool close waits for all requests', async (t) => {
t = tspl(t, { plan: 5 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.end('asd')
})
after(() => server.close())
server.listen(0, () => {
const client = new Pool(`http://localhost:${server.address().port}`, {
connections: 1,
pipelining: 1
})
after(() => client.destroy())
client.request({
path: '/',
method: 'GET'
}, (err) => {
t.ifError(err)
})
client.request({
path: '/',
method: 'GET'
}, (err) => {
t.ifError(err)
})
client.close(() => {
t.ok(true, 'pass')
})
client.close(() => {
t.ok(true, 'pass')
})
client.request({
path: '/',
method: 'GET'
}, (err) => {
t.ok(err instanceof errors.ClientClosedError)
})
})
await t.completed
})
test('pool destroyed', async (t) => {
t = tspl(t, { plan: 1 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.end('asd')
})
after(() => server.close())
server.listen(0, () => {
const client = new Pool(`http://localhost:${server.address().port}`, {
connections: 1,
pipelining: 1
})
after(() => client.destroy())
client.destroy()
client.request({
path: '/',
method: 'GET'
}, (err) => {
t.ok(err instanceof errors.ClientDestroyedError)
})
})
await t.completed
})
test('pool destroy fails queued requests', async (t) => {
t = tspl(t, { plan: 6 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.end('asd')
})
after(() => server.close())
server.listen(0, async () => {
const client = new Pool(`http://localhost:${server.address().port}`, {
connections: 1,
pipelining: 1
})
after(() => client.destroy())
const _err = new Error()
client.request({
path: '/',
method: 'GET'
}, (err) => {
t.strictEqual(err, _err)
})
client.request({
path: '/',
method: 'GET'
}, (err) => {
t.strictEqual(err, _err)
})
t.strictEqual(client.destroyed, false)
client.destroy(_err, () => {
t.ok(true, 'pass')
})
t.strictEqual(client.destroyed, true)
client.request({
path: '/',
method: 'GET'
}, (err) => {
t.ok(err instanceof errors.ClientDestroyedError)
})
})
await t.completed
})
test('stats', async (t) => {
t = tspl(t, { plan: 11 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
t.strictEqual('/', req.url)
t.strictEqual('GET', req.method)
res.setHeader('content-type', 'text/plain')
res.end('hello')
})
after(() => server.close())
server.listen(0, async () => {
const client = new Pool(`http://localhost:${server.address().port}`)
after(() => client.destroy())
t.strictEqual(client[kUrl].origin, `http://localhost:${server.address().port}`)
client.request({ path: '/', method: 'GET' }, (err, { statusCode, headers, body }) => {
t.ifError(err)
t.strictEqual(statusCode, 200)
t.strictEqual(client.stats.connected, 1)
t.strictEqual(client.stats.free, 0)
t.strictEqual(client.stats.pending, 0)
t.strictEqual(client.stats.queued, 0)
t.strictEqual(client.stats.running, 1)
t.strictEqual(client.stats.size, 1)
})
})
await t.completed
})
================================================
FILE: test/promises.js
================================================
'use strict'
const { tspl } = require('@matteo.collina/tspl')
const { test, after } = require('node:test')
const { Client, Pool } = require('..')
const { createServer } = require('node:http')
const { readFileSync, createReadStream } = require('node:fs')
const { wrapWithAsyncIterable } = require('./utils/async-iterators')
test('basic get, async await support', async (t) => {
t = tspl(t, { plan: 5 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
t.strictEqual('/', req.url)
t.strictEqual('GET', req.method)
res.setHeader('content-type', 'text/plain')
res.end('hello')
})
after(() => server.close())
server.listen(0, async () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.close())
client.on('disconnect', () => {
if (!client.closed && !client.destroyed) {
t.fail('unexpected disconnect')
}
})
try {
const { statusCode, headers, body } = await client.request({ path: '/', method: 'GET' })
t.strictEqual(statusCode, 200)
t.strictEqual(headers['content-type'], 'text/plain')
const bufs = []
body.on('data', (buf) => {
bufs.push(buf)
})
body.on('end', () => {
t.strictEqual('hello', Buffer.concat(bufs).toString('utf8'))
})
} catch (err) {
t.fail(err)
}
})
await t.completed
})
function postServer (t, expected) {
return function (req, res) {
t.strictEqual(req.url, '/')
t.strictEqual(req.method, 'POST')
req.setEncoding('utf8')
let data = ''
req.on('data', function (d) { data += d })
req.on('end', () => {
t.strictEqual(data, expected)
res.end('hello')
})
}
}
test('basic POST with string, async await support', async (t) => {
t = tspl(t, { plan: 5 })
const expected = readFileSync(__filename, 'utf8')
const server = createServer({ joinDuplicateHeaders: true }, postServer(t, expected))
after(() => server.close())
server.listen(0, async () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.close())
client.on('disconnect', () => {
if (!client.closed && !client.destroyed) {
t.fail('unexpected disconnect')
}
})
try {
const { statusCode, body } = await client.request({ path: '/', method: 'POST', body: expected })
t.strictEqual(statusCode, 200)
const bufs = []
body.on('data', (buf) => {
bufs.push(buf)
})
body.on('end', () => {
t.strictEqual('hello', Buffer.concat(bufs).toString('utf8'))
})
} catch (err) {
t.fail(err)
}
})
await t.completed
})
test('basic POST with Buffer, async await support', async (t) => {
t = tspl(t, { plan: 5 })
const expected = readFileSync(__filename)
const server = createServer({ joinDuplicateHeaders: true }, postServer(t, expected.toString()))
after(() => server.close())
server.listen(0, async () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.close())
client.on('disconnect', () => {
if (!client.closed && !client.destroyed) {
t.fail('unexpected disconnect')
}
})
try {
const { statusCode, body } = await client.request({ path: '/', method: 'POST', body: expected })
t.strictEqual(statusCode, 200)
const bufs = []
body.on('data', (buf) => {
bufs.push(buf)
})
body.on('end', () => {
t.strictEqual('hello', Buffer.concat(bufs).toString('utf8'))
})
} catch (err) {
t.fail(err)
}
})
await t.completed
})
test('basic POST with stream, async await support', async (t) => {
t = tspl(t, { plan: 5 })
const expected = readFileSync(__filename, 'utf8')
const server = createServer({ joinDuplicateHeaders: true }, postServer(t, expected))
after(() => server.close())
server.listen(0, async () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.close())
client.on('disconnect', () => {
if (!client.closed && !client.destroyed) {
t.fail('unexpected disconnect')
}
})
try {
const { statusCode, body } = await client.request({
path: '/',
method: 'POST',
headers: {
'content-length': Buffer.byteLength(expected)
},
body: createReadStream(__filename)
})
t.strictEqual(statusCode, 200)
const bufs = []
body.on('data', (buf) => {
bufs.push(buf)
})
body.on('end', () => {
t.strictEqual('hello', Buffer.concat(bufs).toString('utf8'))
})
} catch (err) {
t.fail(err)
}
})
await t.completed
})
test('basic POST with async-iterator, async await support', async (t) => {
t = tspl(t, { plan: 5 })
const expected = readFileSync(__filename, 'utf8')
const server = createServer({ joinDuplicateHeaders: true }, postServer(t, expected))
after(() => server.close())
server.listen(0, async () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.close())
client.on('disconnect', () => {
if (!client.closed && !client.destroyed) {
t.fail('unexpected disconnect')
}
})
try {
const { statusCode, body } = await client.request({
path: '/',
method: 'POST',
headers: {
'content-length': Buffer.byteLength(expected)
},
body: wrapWithAsyncIterable(createReadStream(__filename))
})
t.strictEqual(statusCode, 200)
const bufs = []
body.on('data', (buf) => {
bufs.push(buf)
})
body.on('end', () => {
t.strictEqual('hello', Buffer.concat(bufs).toString('utf8'))
})
} catch (err) {
t.fail(err)
}
})
await t.completed
})
test('20 times GET with pipelining 10, async await support', async (t) => {
const num = 20
t = tspl(t, { plan: 2 * num + 1 })
const sleep = ms => new Promise((resolve, reject) => {
setTimeout(resolve, ms)
})
let count = 0
let countGreaterThanOne = false
const server = createServer({ joinDuplicateHeaders: true }, async (req, res) => {
count++
await sleep(10)
countGreaterThanOne = countGreaterThanOne || count > 1
res.end(req.url)
})
after(() => server.close())
// needed to check for a warning on the maxListeners on the socket
function onWarning (warning) {
if (!/ExperimentalWarning/.test(warning)) {
t.fail()
}
}
process.on('warning', onWarning)
after(() => {
process.removeListener('warning', onWarning)
})
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`, {
pipelining: 10
})
after(() => client.close())
client.on('disconnect', () => {
if (!client.closed && !client.destroyed) {
t.fail('unexpected disconnect')
}
})
for (let i = 0; i < num; i++) {
makeRequest(i)
}
async function makeRequest (i) {
await makeRequestAndExpectUrl(client, i, t)
count--
if (i === num - 1) {
t.ok(countGreaterThanOne, 'seen more than one parallel request')
}
}
})
await t.completed
})
async function makeRequestAndExpectUrl (client, i, t) {
try {
const { statusCode, body } = await client.request({ path: '/' + i, method: 'GET', blocking: false })
t.strictEqual(statusCode, 200)
const bufs = []
body.on('data', (buf) => {
bufs.push(buf)
})
body.on('end', () => {
t.strictEqual('/' + i, Buffer.concat(bufs).toString('utf8'))
})
} catch (err) {
t.fail(err)
}
return true
}
test('pool, async await support', async (t) => {
t = tspl(t, { plan: 5 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
t.strictEqual('/', req.url)
t.strictEqual('GET', req.method)
res.setHeader('content-type', 'text/plain')
res.end('hello')
})
after(() => server.close())
server.listen(0, async () => {
const client = new Pool(`http://localhost:${server.address().port}`)
after(() => client.close())
client.on('disconnect', () => {
if (!client.closed && !client.destroyed) {
t.fail('unexpected disconnect')
}
})
try {
const { statusCode, headers, body } = await client.request({ path: '/', method: 'GET' })
t.strictEqual(statusCode, 200)
t.strictEqual(headers['content-type'], 'text/plain')
const bufs = []
body.on('data', (buf) => {
bufs.push(buf)
})
body.on('end', () => {
t.strictEqual('hello', Buffer.concat(bufs).toString('utf8'))
})
} catch (err) {
t.fail(err)
}
})
await t.completed
})
================================================
FILE: test/proxy-agent.js
================================================
'use strict'
const { tspl } = require('@matteo.collina/tspl')
const { test, after } = require('node:test')
const diagnosticsChannel = require('node:diagnostics_channel')
const { request, fetch, setGlobalDispatcher, getGlobalDispatcher } = require('..')
const { InvalidArgumentError, SecureProxyConnectionError } = require('../lib/core/errors')
const ProxyAgent = require('../lib/dispatcher/proxy-agent')
const Pool = require('../lib/dispatcher/pool')
const { createServer } = require('node:http')
const https = require('node:https')
const { Socket } = require('node:net')
const { createProxy } = require('proxy')
const certs = (() => {
const forge = require('node-forge')
const createCert = (cn, issuer, keyLength = 2048) => {
const keys = forge.pki.rsa.generateKeyPair(keyLength)
const cert = forge.pki.createCertificate()
cert.publicKey = keys.publicKey
cert.serialNumber = '' + Date.now()
cert.validity.notBefore = new Date()
cert.validity.notAfter = new Date()
cert.validity.notAfter.setFullYear(cert.validity.notBefore.getFullYear() + 10)
const attrs = [{
name: 'commonName',
value: cn
}]
cert.setSubject(attrs)
const isCa = issuer === undefined
cert.setExtensions([{
name: 'basicConstraints',
cA: isCa
}, {
name: 'keyUsage',
keyCertSign: true,
digitalSignature: true,
nonRepudiation: true,
keyEncipherment: true,
dataEncipherment: true
}, {
name: 'extKeyUsage',
serverAuth: true,
clientAuth: true,
codeSigning: true,
emailProtection: true,
timeStamping: true
}, {
name: 'nsCertType',
client: true,
server: true,
email: true,
objsign: true,
sslCA: isCa,
emailCA: isCa,
objCA: isCa
}])
const alg = forge.md.sha256.create()
if (issuer !== undefined) {
cert.setIssuer(issuer.certificate.subject.attributes)
cert.sign(issuer.privateKey, alg)
} else {
cert.setIssuer(attrs)
cert.sign(keys.privateKey, alg)
}
return {
privateKey: keys.privateKey,
publicKey: keys.publicKey,
certificate: cert
}
}
const root = createCert('CA')
const server = createCert('agent1', root)
const client = createCert('client', root)
const proxy = createCert('proxy', root)
return {
root: {
key: forge.pki.privateKeyToPem(root.privateKey),
crt: forge.pki.certificateToPem(root.certificate)
},
server: {
key: forge.pki.privateKeyToPem(server.privateKey),
crt: forge.pki.certificateToPem(server.certificate)
},
client: {
key: forge.pki.privateKeyToPem(client.privateKey),
crt: forge.pki.certificateToPem(client.certificate)
},
proxy: {
key: forge.pki.privateKeyToPem(proxy.privateKey),
crt: forge.pki.certificateToPem(proxy.certificate)
}
}
})()
test('should throw error when no uri is provided', (t) => {
t = tspl(t, { plan: 2 })
t.throws(() => new ProxyAgent(), InvalidArgumentError)
t.throws(() => new ProxyAgent({}), InvalidArgumentError)
})
test('using auth in combination with token should throw', (t) => {
t = tspl(t, { plan: 1 })
t.throws(() => new ProxyAgent({
auth: 'foo',
token: 'Bearer bar',
uri: 'http://example.com'
}),
InvalidArgumentError
)
})
test('should accept string, URL and object as options', (t) => {
t = tspl(t, { plan: 3 })
t.doesNotThrow(() => new ProxyAgent('http://example.com'))
t.doesNotThrow(() => new ProxyAgent(new URL('http://example.com')))
t.doesNotThrow(() => new ProxyAgent({ uri: 'http://example.com' }))
})
test('use proxy-agent to connect through proxy (keep alive)', async (t) => {
t = tspl(t, { plan: 10 })
const server = await buildServer()
const proxy = await buildProxy()
delete proxy.authenticate
let _socket, _connectParams
diagnosticsChannel.channel('undici:proxy:connected').subscribe(({ socket, connectParams }) => {
_socket = socket
_connectParams = connectParams
})
const serverUrl = `http://localhost:${server.address().port}`
const proxyUrl = `http://localhost:${proxy.address().port}`
const proxyAgent = new ProxyAgent({
uri: proxyUrl,
proxyTunnel: true
})
const parsedOrigin = new URL(serverUrl)
proxy.on('connect', (msg) => {
t.strictEqual(msg.headers['proxy-connection'], 'keep-alive')
})
server.on('request', (req, res) => {
t.strictEqual(req.url, '/')
t.strictEqual(req.headers.host, parsedOrigin.host, 'should not use proxyUrl as host')
res.setHeader('content-type', 'application/json')
res.end(JSON.stringify({ hello: 'world' }))
})
const {
statusCode,
headers,
body
} = await request(serverUrl, { dispatcher: proxyAgent })
const json = await body.json()
t.strictEqual(statusCode, 200)
t.deepStrictEqual(json, { hello: 'world' })
t.strictEqual(headers.connection, 'keep-alive', 'should remain the connection open')
t.ok(_socket instanceof Socket)
t.equal(_connectParams.origin, proxyUrl)
t.equal(_connectParams.path, serverUrl.replace('http://', ''))
t.equal(_connectParams.headers['proxy-connection'], 'keep-alive')
server.close()
proxy.close()
proxyAgent.close()
})
test('use proxy-agent to connect through proxy', async (t) => {
t = tspl(t, { plan: 6 })
const server = await buildServer()
const proxy = await buildProxy()
delete proxy.authenticate
const serverUrl = `http://localhost:${server.address().port}`
const proxyUrl = `http://localhost:${proxy.address().port}`
const proxyAgent = new ProxyAgent({ uri: proxyUrl, proxyTunnel: true })
const parsedOrigin = new URL(serverUrl)
proxy.on('connect', () => {
t.ok(true, 'should connect to proxy')
})
server.on('request', (req, res) => {
t.strictEqual(req.url, '/')
t.strictEqual(req.headers.host, parsedOrigin.host, 'should not use proxyUrl as host')
res.setHeader('content-type', 'application/json')
res.end(JSON.stringify({ hello: 'world' }))
})
const {
statusCode,
headers,
body
} = await request(serverUrl, { dispatcher: proxyAgent })
const json = await body.json()
t.strictEqual(statusCode, 200)
t.deepStrictEqual(json, { hello: 'world' })
t.strictEqual(headers.connection, 'keep-alive', 'should remain the connection open')
server.close()
proxy.close()
proxyAgent.close()
})
test('use proxy agent to connect through proxy using Pool', async (t) => {
t = tspl(t, { plan: 3 })
const server = await buildServer()
const proxy = await buildProxy()
let resolveFirstConnect
let connectCount = 0
proxy.authenticate = async function (req) {
if (++connectCount === 2) {
t.ok(true, 'second connect should arrive while first is still inflight')
resolveFirstConnect()
return true
} else {
await new Promise((resolve) => {
resolveFirstConnect = resolve
})
return true
}
}
server.on('request', (req, res) => {
res.end()
})
const serverUrl = `http://localhost:${server.address().port}`
const proxyUrl = `http://localhost:${proxy.address().port}`
const clientFactory = (url, options) => {
return new Pool(url, options)
}
const proxyAgent = new ProxyAgent({ auth: Buffer.from('user:pass').toString('base64'), uri: proxyUrl, clientFactory })
const firstRequest = request(`${serverUrl}`, { dispatcher: proxyAgent })
const secondRequest = await request(`${serverUrl}`, { dispatcher: proxyAgent })
t.strictEqual((await firstRequest).statusCode, 200)
t.strictEqual(secondRequest.statusCode, 200)
server.close()
proxy.close()
proxyAgent.close()
})
test('use proxy-agent to connect through proxy using path with params', async (t) => {
t = tspl(t, { plan: 5 })
const server = await buildServer()
const proxy = await buildProxy()
const serverUrl = `http://localhost:${server.address().port}`
const proxyUrl = `http://localhost:${proxy.address().port}`
const proxyAgent = new ProxyAgent({ uri: proxyUrl, proxyTunnel: false })
const parsedOrigin = new URL(serverUrl)
proxy.on('connect', () => {
t.fail('proxy tunnel should not be established')
})
server.on('request', (req, res) => {
t.strictEqual(req.url, '/hello?foo=bar')
t.strictEqual(req.headers.host, parsedOrigin.host, 'should not use proxyUrl as host')
res.setHeader('content-type', 'application/json')
res.end(JSON.stringify({ hello: 'world' }))
})
const {
statusCode,
headers,
body
} = await request(serverUrl + '/hello?foo=bar', { dispatcher: proxyAgent })
const json = await body.json()
t.strictEqual(statusCode, 200)
t.deepStrictEqual(json, { hello: 'world' })
t.strictEqual(headers.connection, 'keep-alive', 'should remain the connection open')
server.close()
proxy.close()
proxyAgent.close()
})
test('use proxy-agent to connect through proxy using path with params with tunneling enabled', async (t) => {
t = tspl(t, { plan: 6 })
const server = await buildServer()
const proxy = await buildProxy()
const serverUrl = `http://localhost:${server.address().port}`
const proxyUrl = `http://localhost:${proxy.address().port}`
const proxyAgent = new ProxyAgent({ uri: proxyUrl, proxyTunnel: true })
const parsedOrigin = new URL(serverUrl)
proxy.on('connect', () => {
t.ok(true, 'should call proxy')
})
server.on('request', (req, res) => {
t.strictEqual(req.url, '/hello?foo=bar')
t.strictEqual(req.headers.host, parsedOrigin.host, 'should not use proxyUrl as host')
res.setHeader('content-type', 'application/json')
res.end(JSON.stringify({ hello: 'world' }))
})
const {
statusCode,
headers,
body
} = await request(serverUrl + '/hello?foo=bar', { dispatcher: proxyAgent })
const json = await body.json()
t.strictEqual(statusCode, 200)
t.deepStrictEqual(json, { hello: 'world' })
t.strictEqual(headers.connection, 'keep-alive', 'should remain the connection open')
server.close()
proxy.close()
proxyAgent.close()
})
test('use proxy-agent to connect through proxy with basic auth in URL', async (t) => {
t = tspl(t, { plan: 6 })
const server = await buildServer()
const proxy = await buildProxy()
const serverUrl = `http://localhost:${server.address().port}`
const proxyUrl = new URL(`http://user:pass@localhost:${proxy.address().port}`)
const proxyAgent = new ProxyAgent({ uri: proxyUrl, proxyTunnel: false })
const parsedOrigin = new URL(serverUrl)
proxy.authenticate = function (req, fn) {
t.ok(true, 'authentication should be called')
return req.headers['proxy-authorization'] === `Basic ${Buffer.from('user:pass').toString('base64')}`
}
proxy.on('connect', () => {
t.fail('proxy tunnel should not be established')
})
server.on('request', (req, res) => {
t.strictEqual(req.url, '/hello?foo=bar')
t.strictEqual(req.headers.host, parsedOrigin.host, 'should not use proxyUrl as host')
res.setHeader('content-type', 'application/json')
res.end(JSON.stringify({ hello: 'world' }))
})
const {
statusCode,
headers,
body
} = await request(serverUrl + '/hello?foo=bar', { dispatcher: proxyAgent })
const json = await body.json()
t.strictEqual(statusCode, 200)
t.deepStrictEqual(json, { hello: 'world' })
t.strictEqual(headers.connection, 'keep-alive', 'should remain the connection open')
server.close()
proxy.close()
proxyAgent.close()
})
test('use proxy-agent to connect through proxy with basic auth in URL with tunneling enabled', async (t) => {
t = tspl(t, { plan: 7 })
const server = await buildServer()
const proxy = await buildProxy()
const serverUrl = `http://localhost:${server.address().port}`
const proxyUrl = new URL(`http://user:pass@localhost:${proxy.address().port}`)
const proxyAgent = new ProxyAgent({ uri: proxyUrl, proxyTunnel: true })
const parsedOrigin = new URL(serverUrl)
proxy.authenticate = function (req, fn) {
t.ok(true, 'authentication should be called')
return req.headers['proxy-authorization'] === `Basic ${Buffer.from('user:pass').toString('base64')}`
}
proxy.on('connect', () => {
t.ok(true, 'proxy should be called')
})
server.on('request', (req, res) => {
t.strictEqual(req.url, '/hello?foo=bar')
t.strictEqual(req.headers.host, parsedOrigin.host, 'should not use proxyUrl as host')
res.setHeader('content-type', 'application/json')
res.end(JSON.stringify({ hello: 'world' }))
})
const {
statusCode,
headers,
body
} = await request(serverUrl + '/hello?foo=bar', { dispatcher: proxyAgent })
const json = await body.json()
t.strictEqual(statusCode, 200)
t.deepStrictEqual(json, { hello: 'world' })
t.strictEqual(headers.connection, 'keep-alive', 'should remain the connection open')
server.close()
proxy.close()
proxyAgent.close()
})
test('use proxy-agent with auth', async (t) => {
t = tspl(t, { plan: 6 })
const server = await buildServer()
const proxy = await buildProxy()
const serverUrl = `http://localhost:${server.address().port}`
const proxyUrl = `http://localhost:${proxy.address().port}`
const proxyAgent = new ProxyAgent({
auth: Buffer.from('user:pass').toString('base64'),
uri: proxyUrl,
proxyTunnel: false
})
const parsedOrigin = new URL(serverUrl)
proxy.authenticate = function (req) {
t.ok(true, 'authentication should be called')
return req.headers['proxy-authorization'] === `Basic ${Buffer.from('user:pass').toString('base64')}`
}
proxy.on('connect', () => {
t.fail('proxy tunnel should not be established')
})
server.on('request', (req, res) => {
t.strictEqual(req.url, '/hello?foo=bar')
t.strictEqual(req.headers.host, parsedOrigin.host, 'should not use proxyUrl as host')
res.setHeader('content-type', 'application/json')
res.end(JSON.stringify({ hello: 'world' }))
})
const {
statusCode,
headers,
body
} = await request(serverUrl + '/hello?foo=bar', { dispatcher: proxyAgent })
const json = await body.json()
t.strictEqual(statusCode, 200)
t.deepStrictEqual(json, { hello: 'world' })
t.strictEqual(headers.connection, 'keep-alive', 'should remain the connection open')
server.close()
proxy.close()
proxyAgent.close()
})
test('use proxy-agent with auth with tunneling enabled', async (t) => {
t = tspl(t, { plan: 7 })
const server = await buildServer()
const proxy = await buildProxy()
const serverUrl = `http://localhost:${server.address().port}`
const proxyUrl = `http://localhost:${proxy.address().port}`
const proxyAgent = new ProxyAgent({
auth: Buffer.from('user:pass').toString('base64'),
uri: proxyUrl,
proxyTunnel: true
})
const parsedOrigin = new URL(serverUrl)
proxy.authenticate = function (req) {
t.ok(true, 'authentication should be called')
return req.headers['proxy-authorization'] === `Basic ${Buffer.from('user:pass').toString('base64')}`
}
proxy.on('connect', () => {
t.ok(true, 'proxy should be called')
})
server.on('request', (req, res) => {
t.strictEqual(req.url, '/hello?foo=bar')
t.strictEqual(req.headers.host, parsedOrigin.host, 'should not use proxyUrl as host')
res.setHeader('content-type', 'application/json')
res.end(JSON.stringify({ hello: 'world' }))
})
const {
statusCode,
headers,
body
} = await request(serverUrl + '/hello?foo=bar', { dispatcher: proxyAgent })
const json = await body.json()
t.strictEqual(statusCode, 200)
t.deepStrictEqual(json, { hello: 'world' })
t.strictEqual(headers.connection, 'keep-alive', 'should remain the connection open')
server.close()
proxy.close()
proxyAgent.close()
})
test('use proxy-agent with token', async (t) => {
t = tspl(t, { plan: 6 })
const server = await buildServer()
const proxy = await buildProxy()
const serverUrl = `http://localhost:${server.address().port}`
const proxyUrl = `http://localhost:${proxy.address().port}`
const proxyAgent = new ProxyAgent({
token: `Bearer ${Buffer.from('user:pass').toString('base64')}`,
uri: proxyUrl,
proxyTunnel: false
})
const parsedOrigin = new URL(serverUrl)
proxy.authenticate = function (req) {
t.ok(true, 'authentication should be called')
return req.headers['proxy-authorization'] === `Bearer ${Buffer.from('user:pass').toString('base64')}`
}
proxy.on('connect', () => {
t.fail('proxy tunnel should not be established')
})
server.on('request', (req, res) => {
t.strictEqual(req.url, '/hello?foo=bar')
t.strictEqual(req.headers.host, parsedOrigin.host, 'should not use proxyUrl as host')
res.setHeader('content-type', 'application/json')
res.end(JSON.stringify({ hello: 'world' }))
})
const {
statusCode,
headers,
body
} = await request(serverUrl + '/hello?foo=bar', { dispatcher: proxyAgent })
const json = await body.json()
t.strictEqual(statusCode, 200)
t.deepStrictEqual(json, { hello: 'world' })
t.strictEqual(headers.connection, 'keep-alive', 'should remain the connection open')
server.close()
proxy.close()
proxyAgent.close()
})
test('use proxy-agent with token with tunneling enabled', async (t) => {
t = tspl(t, { plan: 7 })
const server = await buildServer()
const proxy = await buildProxy()
const serverUrl = `http://localhost:${server.address().port}`
const proxyUrl = `http://localhost:${proxy.address().port}`
const proxyAgent = new ProxyAgent({
token: `Bearer ${Buffer.from('user:pass').toString('base64')}`,
uri: proxyUrl,
proxyTunnel: true
})
const parsedOrigin = new URL(serverUrl)
proxy.authenticate = function (req) {
t.ok(true, 'authentication should be called')
return req.headers['proxy-authorization'] === `Bearer ${Buffer.from('user:pass').toString('base64')}`
}
proxy.on('connect', () => {
t.ok(true, 'proxy should be called')
})
server.on('request', (req, res) => {
t.strictEqual(req.url, '/hello?foo=bar')
t.strictEqual(req.headers.host, parsedOrigin.host, 'should not use proxyUrl as host')
res.setHeader('content-type', 'application/json')
res.end(JSON.stringify({ hello: 'world' }))
})
const {
statusCode,
headers,
body
} = await request(serverUrl + '/hello?foo=bar', { dispatcher: proxyAgent })
const json = await body.json()
t.strictEqual(statusCode, 200)
t.deepStrictEqual(json, { hello: 'world' })
t.strictEqual(headers.connection, 'keep-alive', 'should remain the connection open')
server.close()
proxy.close()
proxyAgent.close()
})
test('use proxy-agent with custom headers', async (t) => {
t = tspl(t, { plan: 1 })
const server = await buildServer()
const proxy = await buildProxy()
const serverUrl = `http://localhost:${server.address().port}`
const proxyUrl = `http://localhost:${proxy.address().port}`
const proxyAgent = new ProxyAgent({
uri: proxyUrl,
proxyTunnel: false,
headers: {
'User-Agent': 'Foobar/1.0.0'
}
})
proxy.on('connect', (req) => {
t.fail('proxy tunnel should not be established')
})
server.on('request', (req, res) => {
t.strictEqual(req.headers['user-agent'], 'BarBaz/1.0.0')
res.end()
})
await request(serverUrl + '/hello?foo=bar', {
headers: { 'user-agent': 'BarBaz/1.0.0' },
dispatcher: proxyAgent
})
server.close()
proxy.close()
proxyAgent.close()
})
test('use proxy-agent with custom headers with tunneling enabled', async (t) => {
t = tspl(t, { plan: 2 })
const server = await buildServer()
const proxy = await buildProxy()
const serverUrl = `http://localhost:${server.address().port}`
const proxyUrl = `http://localhost:${proxy.address().port}`
const proxyAgent = new ProxyAgent({
uri: proxyUrl,
headers: {
'User-Agent': 'Foobar/1.0.0'
},
proxyTunnel: true
})
proxy.on('connect', (req) => {
t.strictEqual(req.headers['user-agent'], 'Foobar/1.0.0')
})
server.on('request', (req, res) => {
t.strictEqual(req.headers['user-agent'], 'BarBaz/1.0.0')
res.end()
})
await request(serverUrl + '/hello?foo=bar', {
headers: { 'user-agent': 'BarBaz/1.0.0' },
dispatcher: proxyAgent
})
server.close()
proxy.close()
proxyAgent.close()
})
test('sending proxy-authorization in request headers should throw', async (t) => {
t = tspl(t, { plan: 3 })
const server = await buildServer()
const proxy = await buildProxy()
const serverUrl = `http://localhost:${server.address().port}`
const proxyUrl = `http://localhost:${proxy.address().port}`
const proxyAgent = new ProxyAgent(proxyUrl)
server.on('request', (req, res) => {
res.end(JSON.stringify({ hello: 'world' }))
})
await t.rejects(
request(
serverUrl + '/hello?foo=bar',
{
dispatcher: proxyAgent,
headers: {
'proxy-authorization': Buffer.from('user:pass').toString('base64')
}
}
),
'Proxy-Authorization should be sent in ProxyAgent'
)
await t.rejects(
request(
serverUrl + '/hello?foo=bar',
{
dispatcher: proxyAgent,
headers: {
'PROXY-AUTHORIZATION': Buffer.from('user:pass').toString('base64')
}
}
),
'Proxy-Authorization should be sent in ProxyAgent'
)
await t.rejects(
request(
serverUrl + '/hello?foo=bar',
{
dispatcher: proxyAgent,
headers: {
'Proxy-Authorization': Buffer.from('user:pass').toString('base64')
}
}
),
'Proxy-Authorization should be sent in ProxyAgent'
)
server.close()
proxy.close()
proxyAgent.close()
})
test('use proxy-agent with setGlobalDispatcher', async (t) => {
t = tspl(t, { plan: 5 })
const defaultDispatcher = getGlobalDispatcher()
const server = await buildServer()
const proxy = await buildProxy()
const serverUrl = `http://localhost:${server.address().port}`
const proxyUrl = `http://localhost:${proxy.address().port}`
const proxyAgent = new ProxyAgent({ uri: proxyUrl, proxyTunnel: false })
const parsedOrigin = new URL(serverUrl)
setGlobalDispatcher(proxyAgent)
after(() => setGlobalDispatcher(defaultDispatcher))
proxy.on('connect', () => {
// proxyTunnel must be set to true in order to tunnel into the endpoint for HTTP->HTTP proxy connections
t.fail(true, 'connect to proxy should unreachable by default for HTTP->HTTP proxy connections')
})
server.on('request', (req, res) => {
t.strictEqual(req.url, '/hello?foo=bar')
t.strictEqual(req.headers.host, parsedOrigin.host, 'should not use proxyUrl as host')
res.setHeader('content-type', 'application/json')
res.end(JSON.stringify({ hello: 'world' }))
})
const {
statusCode,
headers,
body
} = await request(serverUrl + '/hello?foo=bar')
const json = await body.json()
t.strictEqual(statusCode, 200)
t.deepStrictEqual(json, { hello: 'world' })
t.strictEqual(headers.connection, 'keep-alive', 'should remain the connection open')
server.close()
proxy.close()
proxyAgent.close()
})
test('use proxy-agent with setGlobalDispatcher with tunneling enabled', async (t) => {
t = tspl(t, { plan: 6 })
const defaultDispatcher = getGlobalDispatcher()
const server = await buildServer()
const proxy = await buildProxy()
const serverUrl = `http://localhost:${server.address().port}`
const proxyUrl = `http://localhost:${proxy.address().port}`
const proxyAgent = new ProxyAgent({ uri: proxyUrl, proxyTunnel: true })
const parsedOrigin = new URL(serverUrl)
setGlobalDispatcher(proxyAgent)
after(() => setGlobalDispatcher(defaultDispatcher))
proxy.on('connect', () => {
t.ok(true, 'should call proxy')
})
server.on('request', (req, res) => {
t.strictEqual(req.url, '/hello?foo=bar')
t.strictEqual(req.headers.host, parsedOrigin.host, 'should not use proxyUrl as host')
res.setHeader('content-type', 'application/json')
res.end(JSON.stringify({ hello: 'world' }))
})
const {
statusCode,
headers,
body
} = await request(serverUrl + '/hello?foo=bar')
const json = await body.json()
t.strictEqual(statusCode, 200)
t.deepStrictEqual(json, { hello: 'world' })
t.strictEqual(headers.connection, 'keep-alive', 'should remain the connection open')
server.close()
proxy.close()
proxyAgent.close()
})
test('ProxyAgent correctly sends headers when using fetch - #1355, #1623', async (t) => {
t = tspl(t, { plan: 1 })
const defaultDispatcher = getGlobalDispatcher()
const server = await buildServer()
const proxy = await buildProxy()
const serverUrl = `http://localhost:${server.address().port}`
const proxyUrl = `http://localhost:${proxy.address().port}`
const proxyAgent = new ProxyAgent({ uri: proxyUrl, proxyTunnel: false })
setGlobalDispatcher(proxyAgent)
after(() => setGlobalDispatcher(defaultDispatcher))
const expectedHeaders = {
host: `localhost:${server.address().port}`,
connection: 'keep-alive',
'test-header': 'value',
accept: '*/*',
'accept-language': '*',
'sec-fetch-mode': 'cors',
'user-agent': 'undici',
'accept-encoding': 'gzip, deflate'
}
proxy.on('connect', (req, res) => {
// proxyTunnel must be set to true in order to tunnel into the endpoint for HTTP->HTTP proxy connections
t.fail(true, 'connect to proxy should unreachable by default for HTTP->HTTP proxy connections')
})
server.on('request', (req, res) => {
// The `proxy` package will add a "via" and "x-forwarded-for" header for non-tunneled Proxy requests
for (const header of ['via', 'x-forwarded-for']) {
delete req.headers[header]
}
t.deepStrictEqual(req.headers, expectedHeaders)
res.end('goodbye')
})
await fetch(serverUrl, {
headers: { 'Test-header': 'value' }
})
server.close()
proxy.close()
proxyAgent.close()
t.end()
})
test('ProxyAgent correctly sends headers when using fetch - #1355, #1623 (with proxy tunneling enabled)', async (t) => {
t = tspl(t, { plan: 2 })
const defaultDispatcher = getGlobalDispatcher()
const server = await buildServer()
const proxy = await buildProxy()
const serverUrl = `http://localhost:${server.address().port}`
const proxyUrl = `http://localhost:${proxy.address().port}`
const proxyAgent = new ProxyAgent({ uri: proxyUrl, proxyTunnel: true })
setGlobalDispatcher(proxyAgent)
after(() => setGlobalDispatcher(defaultDispatcher))
const expectedHeaders = {
host: `localhost:${server.address().port}`,
connection: 'keep-alive',
'test-header': 'value',
accept: '*/*',
'accept-language': '*',
'sec-fetch-mode': 'cors',
'user-agent': 'undici',
'accept-encoding': 'gzip, deflate'
}
const expectedProxyHeaders = {
'proxy-connection': 'keep-alive',
host: `localhost:${server.address().port}`,
connection: 'close'
}
proxy.on('connect', (req, res) => {
t.deepStrictEqual(req.headers, expectedProxyHeaders)
})
server.on('request', (req, res) => {
t.deepStrictEqual(req.headers, expectedHeaders)
res.end('goodbye')
})
await fetch(serverUrl, {
headers: { 'Test-header': 'value' }
})
server.close()
proxy.close()
proxyAgent.close()
t.end()
})
test('should throw when proxy does not return 200', async (t) => {
t = tspl(t, { plan: 3 })
const server = await buildServer()
const proxy = await buildProxy()
const serverUrl = `http://localhost:${server.address().port}`
const proxyUrl = `http://localhost:${proxy.address().port}`
proxy.on('connect', () => {
// proxyTunnel must be set to true in order to tunnel into the endpoint for HTTP->HTTP proxy connections
t.fail(true, 'connect to proxy should unreachable by default for HTTP->HTTP proxy connections')
})
proxy.authenticate = function (_req) {
t.ok(true, 'should call authenticate')
return false
}
const proxyAgent = new ProxyAgent({ uri: proxyUrl, proxyTunnel: false })
try {
await request(serverUrl, { dispatcher: proxyAgent })
t.fail()
} catch (e) {
t.ok(true, 'pass')
t.ok(e)
}
server.close()
proxy.close()
proxyAgent.close()
await t.completed
})
test('should throw when proxy does not return 200 with tunneling enabled', async (t) => {
t = tspl(t, { plan: 3 })
const server = await buildServer()
const proxy = await buildProxy()
const serverUrl = `http://localhost:${server.address().port}`
const proxyUrl = `http://localhost:${proxy.address().port}`
proxy.authenticate = function (_req) {
t.ok(true, 'should call authenticate')
return false
}
const proxyAgent = new ProxyAgent({ uri: proxyUrl, proxyTunnel: true })
try {
await request(serverUrl, { dispatcher: proxyAgent })
t.fail()
} catch (e) {
t.ok(true, 'pass')
t.ok(e)
}
server.close()
proxy.close()
proxyAgent.close()
await t.completed
})
test('pass ProxyAgent proxy status code error when using fetch - #2161', async (t) => {
t = tspl(t, { plan: 2 })
const server = await buildServer()
const proxy = await buildProxy()
const serverUrl = `http://localhost:${server.address().port}`
const proxyUrl = `http://localhost:${proxy.address().port}`
proxy.authenticate = function (_req) {
t.ok(true, 'should call authenticate')
return false
}
const proxyAgent = new ProxyAgent(proxyUrl)
try {
await fetch(serverUrl, { dispatcher: proxyAgent })
} catch (e) {
t.ok('cause' in e)
}
server.close()
proxy.close()
proxyAgent.close()
await t.completed
})
test('Proxy via HTTP to HTTPS endpoint', async (t) => {
t = tspl(t, { plan: 4 })
const server = await buildSSLServer()
const proxy = await buildProxy()
const serverUrl = `https://localhost:${server.address().port}`
const proxyUrl = `http://localhost:${proxy.address().port}`
const proxyAgent = new ProxyAgent({
uri: proxyUrl,
requestTls: {
ca: [
certs.root.crt
],
servername: 'agent1'
}
})
server.on('request', function (req, res) {
t.ok(req.connection.encrypted)
res.end(JSON.stringify(req.headers))
})
server.on('secureConnection', () => {
t.ok(true, 'server should be connected secured')
})
proxy.on('secureConnection', () => {
t.fail('proxy over http should not call secureConnection')
})
proxy.on('connect', function () {
t.ok(true, 'proxy should be connected')
})
proxy.on('request', function () {
t.fail('proxy should never receive requests')
})
const data = await request(serverUrl, { dispatcher: proxyAgent })
const json = await data.body.json()
t.deepStrictEqual(json, {
host: `localhost:${server.address().port}`,
connection: 'keep-alive'
})
server.close()
proxy.close()
proxyAgent.close()
})
test('Proxy via HTTPS to HTTPS endpoint', async (t) => {
t = tspl(t, { plan: 5 })
const server = await buildSSLServer()
const proxy = await buildSSLProxy()
const serverUrl = `https://localhost:${server.address().port}`
const proxyUrl = `https://localhost:${proxy.address().port}`
const proxyAgent = new ProxyAgent({
uri: proxyUrl,
proxyTls: {
ca: [
certs.root.crt
],
servername: 'proxy'
},
requestTls: {
ca: [
certs.root.crt
],
servername: 'agent1'
}
})
server.on('request', function (req, res) {
t.ok(req.connection.encrypted)
res.end(JSON.stringify(req.headers))
})
server.on('secureConnection', () => {
t.ok(true, 'server should be connected secured')
})
proxy.on('secureConnection', () => {
t.ok(true, 'proxy over http should call secureConnection')
})
proxy.on('connect', function () {
t.ok(true, 'proxy should be connected')
})
proxy.on('request', function () {
t.fail('proxy should never receive requests')
})
const data = await request(serverUrl, { dispatcher: proxyAgent })
const json = await data.body.json()
t.deepStrictEqual(json, {
host: `localhost:${server.address().port}`,
connection: 'keep-alive'
})
server.close()
proxy.close()
proxyAgent.close()
})
test('Proxy via HTTPS to HTTP endpoint with tunneling enabled', async (t) => {
t = tspl(t, { plan: 3 })
const server = await buildServer()
const proxy = await buildSSLProxy()
const serverUrl = `http://localhost:${server.address().port}`
const proxyUrl = `https://localhost:${proxy.address().port}`
const proxyAgent = new ProxyAgent({
uri: proxyUrl,
proxyTls: {
ca: [
certs.root.crt
],
servername: 'proxy'
},
proxyTunnel: true
})
server.on('request', function (req, res) {
t.ok(!req.connection.encrypted)
res.end(JSON.stringify(req.headers))
})
server.on('secureConnection', () => {
t.fail('server is http')
})
proxy.on('secureConnection', () => {
t.ok(true, 'proxy over http should call secureConnection')
})
proxy.on('request', function () {
t.fail('proxy should never receive requests')
})
const data = await request(serverUrl, { dispatcher: proxyAgent })
const json = await data.body.json()
t.deepStrictEqual(json, {
host: `localhost:${server.address().port}`,
connection: 'keep-alive'
})
server.close()
proxy.close()
proxyAgent.close()
})
test('Proxy via HTTP to HTTP endpoint with tunneling enabled', async (t) => {
t = tspl(t, { plan: 3 })
const server = await buildServer()
const proxy = await buildProxy()
const serverUrl = `http://localhost:${server.address().port}`
const proxyUrl = `http://localhost:${proxy.address().port}`
const proxyAgent = new ProxyAgent({ uri: proxyUrl, proxyTunnel: true })
server.on('request', function (req, res) {
t.ok(!req.connection.encrypted)
res.end(JSON.stringify(req.headers))
})
server.on('secureConnection', () => {
t.fail('server is http')
})
proxy.on('secureConnection', () => {
t.fail('proxy is http')
})
proxy.on('connect', () => {
t.ok(true, 'connect to proxy')
})
proxy.on('request', function () {
t.fail('proxy should never receive requests')
})
const data = await request(serverUrl, { dispatcher: proxyAgent })
const json = await data.body.json()
t.deepStrictEqual(json, {
host: `localhost:${server.address().port}`,
connection: 'keep-alive'
})
server.close()
proxy.close()
proxyAgent.close()
})
test('Proxy via HTTP to HTTP endpoint with tunneling disabled', async (t) => {
t = tspl(t, { plan: 3 })
const server = await buildServer()
const proxy = await buildProxy()
const serverUrl = `http://localhost:${server.address().port}`
const proxyUrl = `http://localhost:${proxy.address().port}`
const proxyAgent = new ProxyAgent({ uri: proxyUrl, proxyTunnel: false })
server.on('request', function (req, res) {
t.ok(!req.connection.encrypted)
const headers = { host: req.headers.host, connection: req.headers.connection }
res.end(JSON.stringify(headers))
})
server.on('secureConnection', () => {
t.fail('server is http')
})
proxy.on('secureConnection', () => {
t.fail('proxy is http')
})
proxy.on('connect', () => {
t.fail(true, 'connect to proxy should unreachable if proxyTunnel is false')
})
proxy.on('request', function (req) {
const bits = { method: req.method, url: req.url }
t.deepStrictEqual(bits, {
method: 'GET',
url: `${serverUrl}/`
})
})
const data = await request(serverUrl, { dispatcher: proxyAgent })
const json = await data.body.json()
t.deepStrictEqual(json, {
host: `localhost:${server.address().port}`,
connection: 'keep-alive'
})
server.close()
proxy.close()
proxyAgent.close()
})
test('Proxy via HTTPS to HTTP fails on wrong SNI', async (t) => {
t = tspl(t, { plan: 3 })
const server = await buildServer()
const proxy = await buildSSLProxy()
const serverUrl = `http://localhost:${server.address().port}`
const proxyUrl = `https://localhost:${proxy.address().port}`
const proxyAgent = new ProxyAgent({
uri: proxyUrl,
proxyTls: {
ca: [
certs.root.crt
]
}
})
server.on('request', function (req, res) {
t.ok(!req.connection.encrypted)
res.end(JSON.stringify(req.headers))
})
server.on('secureConnection', () => {
t.fail('server is http')
})
proxy.on('secureConnection', () => {
t.fail('proxy is http')
})
proxy.on('connect', () => {
t.ok(true, 'connect to proxy')
})
proxy.on('request', function () {
t.fail('proxy should never receive requests')
})
try {
await request(serverUrl, { dispatcher: proxyAgent })
throw new Error('should fail')
} catch (e) {
t.ok(e instanceof SecureProxyConnectionError)
t.ok(e.cause instanceof Error)
t.ok(e.cause.code === 'ERR_TLS_CERT_ALTNAME_INVALID')
}
server.close()
proxy.close()
proxyAgent.close()
})
test('ProxyAgent keeps customized host in request headers - #3019', async (t) => {
t = tspl(t, { plan: 2 })
const server = await buildServer()
const proxy = await buildProxy()
const serverUrl = `http://localhost:${server.address().port}`
const proxyUrl = `http://localhost:${proxy.address().port}`
const proxyAgent = new ProxyAgent({ uri: proxyUrl, proxyTunnel: true })
const customHost = 'example.com'
proxy.on('connect', (req) => {
t.strictEqual(req.headers.host, `localhost:${server.address().port}`)
})
server.on('request', (req, res) => {
t.strictEqual(req.headers.host, customHost)
res.end()
})
await request(serverUrl, {
headers: { Host: customHost },
dispatcher: proxyAgent
})
server.close()
proxy.close()
proxyAgent.close()
})
test('ProxyAgent handles multiple concurrent HTTP requests via HTTP proxy', async (t) => {
t = tspl(t, { plan: 20 })
// Start target HTTP server
const server = createServer((req, res) => {
setTimeout(() => {
res.setHeader('content-type', 'application/json')
res.end(JSON.stringify({ url: req.url }))
}, 50)
})
await new Promise(resolve => server.listen(0, resolve))
const targetPort = server.address().port
// Start HTTP proxy server
const proxy = createProxy(createServer())
await new Promise(resolve => proxy.listen(0, resolve))
const proxyPort = proxy.address().port
// Create ProxyAgent (no tunneling, plain HTTP)
const proxyAgent = new ProxyAgent(`http://localhost:${proxyPort}`)
const N = 10
const requests = []
for (let i = 0; i < N; i++) {
requests.push(
request(`http://localhost:${targetPort}/test${i}`, { dispatcher: proxyAgent })
.then(async res => {
t.strictEqual(res.statusCode, 200)
const json = await res.body.json()
t.deepStrictEqual(json, { url: `/test${i}` })
})
)
}
try {
await Promise.all(requests)
} catch (err) {
t.fail(err)
}
server.close()
proxy.close()
proxyAgent.close()
})
function buildServer () {
return new Promise((resolve) => {
const server = createServer({ joinDuplicateHeaders: true })
server.listen(0, () => resolve(server))
})
}
function buildSSLServer () {
const serverOptions = {
ca: [
certs.root.crt
],
key: certs.server.key,
cert: certs.server.crt,
joinDuplicateHeaders: true
}
return new Promise((resolve) => {
const server = https.createServer(serverOptions)
server.listen(0, () => resolve(server))
})
}
function buildProxy (listener) {
return new Promise((resolve) => {
const server = listener
? createProxy(createServer(listener))
: createProxy(createServer({ joinDuplicateHeaders: true }))
server.listen(0, () => resolve(server))
})
}
function buildSSLProxy () {
const serverOptions = {
ca: [
certs.root.crt
],
key: certs.proxy.key,
cert: certs.proxy.crt,
joinDuplicateHeaders: true
}
return new Promise((resolve) => {
const server = createProxy(https.createServer(serverOptions))
server.listen(0, () => resolve(server))
})
}
================================================
FILE: test/proxy.js
================================================
'use strict'
const { tspl } = require('@matteo.collina/tspl')
const { test } = require('node:test')
const { Client, Pool } = require('..')
const { createServer } = require('node:http')
const { createProxy } = require('proxy')
test('connect through proxy', async (t) => {
t = tspl(t, { plan: 3 })
const server = await buildServer()
const proxy = await buildProxy()
const serverUrl = `http://localhost:${server.address().port}`
const proxyUrl = `http://localhost:${proxy.address().port}`
server.on('request', (req, res) => {
t.strictEqual(req.url, '/hello?foo=bar')
res.setHeader('content-type', 'application/json')
res.end(JSON.stringify({ hello: 'world' }))
})
const client = new Client(proxyUrl)
client.on('disconnect', () => {
if (!client.closed && !client.destroyed) {
t.fail('unexpected disconnect')
}
})
const response = await client.request({
method: 'GET',
path: serverUrl + '/hello?foo=bar'
})
response.body.setEncoding('utf8')
let data = ''
for await (const chunk of response.body) {
data += chunk
}
t.strictEqual(response.statusCode, 200)
t.deepStrictEqual(JSON.parse(data), { hello: 'world' })
server.close()
proxy.close()
client.close()
})
test('connect through proxy with auth', async (t) => {
t = tspl(t, { plan: 3 })
const server = await buildServer()
const proxy = await buildProxy()
const serverUrl = `http://localhost:${server.address().port}`
const proxyUrl = `http://localhost:${proxy.address().port}`
proxy.authenticate = function (req) {
return req.headers['proxy-authorization'] === `Basic ${Buffer.from('user:pass').toString('base64')}`
}
server.on('request', (req, res) => {
t.strictEqual(req.url, '/hello?foo=bar')
res.setHeader('content-type', 'application/json')
res.end(JSON.stringify({ hello: 'world' }))
})
const client = new Client(proxyUrl)
client.on('disconnect', () => {
if (!client.closed && !client.destroyed) {
t.fail('unexpected disconnect')
}
})
const response = await client.request({
method: 'GET',
path: serverUrl + '/hello?foo=bar',
headers: {
'proxy-authorization': `Basic ${Buffer.from('user:pass').toString('base64')}`
}
})
response.body.setEncoding('utf8')
let data = ''
for await (const chunk of response.body) {
data += chunk
}
t.strictEqual(response.statusCode, 200)
t.deepStrictEqual(JSON.parse(data), { hello: 'world' })
server.close()
proxy.close()
client.close()
})
test('connect through proxy with auth but invalid credentials', async (t) => {
t = tspl(t, { plan: 1 })
const server = await buildServer()
const proxy = await buildProxy()
const serverUrl = `http://localhost:${server.address().port}`
const proxyUrl = `http://localhost:${proxy.address().port}`
proxy.authenticate = function (req) {
return req.headers['proxy-authorization'] === `Basic ${Buffer.from('user:no-pass').toString('base64')}`
}
server.on('request', (req, res) => {
t.fail('should not be called')
})
const client = new Client(proxyUrl)
client.on('disconnect', () => {
if (!client.closed && !client.destroyed) {
t.fail('unexpected disconnect')
}
})
const response = await client.request({
method: 'GET',
path: serverUrl + '/hello?foo=bar',
headers: {
'proxy-authorization': `Basic ${Buffer.from('user:pass').toString('base64')}`
}
})
t.strictEqual(response.statusCode, 407)
server.close()
proxy.close()
client.close()
})
test('connect through proxy (with pool)', async (t) => {
t = tspl(t, { plan: 3 })
const server = await buildServer()
const proxy = await buildProxy()
const serverUrl = `http://localhost:${server.address().port}`
const proxyUrl = `http://localhost:${proxy.address().port}`
server.on('request', (req, res) => {
t.strictEqual(req.url, '/hello?foo=bar')
res.setHeader('content-type', 'application/json')
res.end(JSON.stringify({ hello: 'world' }))
})
const pool = new Pool(proxyUrl)
pool.on('disconnect', () => {
if (!pool.closed && !pool.destroyed) {
t.fail('unexpected disconnect')
}
})
const response = await pool.request({
method: 'GET',
path: serverUrl + '/hello?foo=bar'
})
response.body.setEncoding('utf8')
let data = ''
for await (const chunk of response.body) {
data += chunk
}
t.strictEqual(response.statusCode, 200)
t.deepStrictEqual(JSON.parse(data), { hello: 'world' })
server.close()
proxy.close()
pool.close()
})
function buildServer () {
return new Promise((resolve, reject) => {
const server = createServer({ joinDuplicateHeaders: true })
server.listen(0, () => resolve(server))
})
}
function buildProxy () {
return new Promise((resolve, reject) => {
const server = createProxy(createServer({ joinDuplicateHeaders: true }))
server.listen(0, () => resolve(server))
})
}
================================================
FILE: test/readable.js
================================================
'use strict'
const { tspl } = require('@matteo.collina/tspl')
const { test, describe } = require('node:test')
const { Readable } = require('../lib/api/readable')
describe('Readable', () => {
test('avoid body reordering', async function (t) {
t = tspl(t, { plan: 1 })
function resume () {
}
function abort () {
}
const r = new Readable({ resume, abort })
r.push(Buffer.from('hello'))
process.nextTick(() => {
r.push(Buffer.from('world'))
r.push(null)
})
const text = await r.text()
t.strictEqual(text, 'helloworld')
})
test('destroy timing text', async function (t) {
t = tspl(t, { plan: 1 })
function resume () {
}
function abort () {
}
const r = new Readable({ resume, abort })
r.destroy(new Error('kaboom'))
await t.rejects(r.text(), new Error('kaboom'))
})
test('destroy timing promise', async function (t) {
t = tspl(t, { plan: 1 })
function resume () {
}
function abort () {
}
const r = await new Promise(resolve => {
const r = new Readable({ resume, abort })
r.destroy(new Error('kaboom'))
resolve(r)
})
await new Promise(resolve => {
r.on('error', err => {
t.ok(err)
resolve(null)
})
})
})
test('.arrayBuffer()', async function (t) {
t = tspl(t, { plan: 1 })
function resume () {
}
function abort () {
}
const r = new Readable({ resume, abort })
r.push(Buffer.from('hello world'))
process.nextTick(() => {
r.push(null)
})
const arrayBuffer = await r.arrayBuffer()
const expected = new ArrayBuffer(11)
const view = new Uint8Array(expected)
view.set(Buffer.from('hello world'))
t.deepStrictEqual(arrayBuffer, expected)
})
test('.bytes()', async function (t) {
t = tspl(t, { plan: 1 })
function resume () {
}
function abort () {
}
const r = new Readable({ resume, abort })
r.push(Buffer.from('hello'))
r.push(Buffer.from(' world'))
process.nextTick(() => {
r.push(null)
})
const bytes = await r.bytes()
t.deepStrictEqual(bytes, new TextEncoder().encode('hello world'))
})
test('.json()', async function (t) {
t = tspl(t, { plan: 1 })
function resume () {
}
function abort () {
}
const r = new Readable({ resume, abort })
r.push(Buffer.from('{"hello": "world"}'))
process.nextTick(() => {
r.push(null)
})
const obj = await r.json()
t.deepStrictEqual(obj, { hello: 'world' })
})
test('.text()', async function (t) {
t = tspl(t, { plan: 1 })
function resume () {
}
function abort () {
}
const r = new Readable({ resume, abort })
r.push(Buffer.from('hello world'))
process.nextTick(() => {
r.push(null)
})
const text = await r.text()
t.strictEqual(text, 'hello world')
})
test('ignore BOM', async function (t) {
t = tspl(t, { plan: 1 })
function resume () {
}
function abort () {
}
const r = new Readable({ resume, abort })
r.push('\uFEFF')
r.push(Buffer.from('hello world'))
process.nextTick(() => {
r.push(null)
})
const text = await r.text()
t.strictEqual(text, 'hello world')
})
test('.bodyUsed', async function (t) {
t = tspl(t, { plan: 3 })
function resume () {
}
function abort () {
}
const r = new Readable({ resume, abort })
r.push(Buffer.from('hello world'))
process.nextTick(() => {
r.push(null)
})
t.strictEqual(r.bodyUsed, false)
const text = await r.text()
t.strictEqual(r.bodyUsed, true)
t.strictEqual(text, 'hello world')
})
})
================================================
FILE: test/redirect-pipeline.js
================================================
'use strict'
const { tspl } = require('@matteo.collina/tspl')
const { test } = require('node:test')
const { pipeline: undiciPipeline, Client, interceptors } = require('..')
const { pipeline: streamPipelineCb } = require('node:stream')
const { promisify } = require('node:util')
const { createReadable, createWritable } = require('./utils/stream')
const { startRedirectingServer } = require('./utils/redirecting-servers')
const streamPipeline = promisify(streamPipelineCb)
const redirect = interceptors.redirect
test('should not follow redirection by default if not using RedirectAgent', async t => {
t = tspl(t, { plan: 3 })
const body = []
const serverRoot = await startRedirectingServer()
await streamPipeline(
createReadable('REQUEST'),
undiciPipeline(`http://${serverRoot}/`, {
dispatcher: new Client(`http://${serverRoot}/`).compose(redirect({ maxRedirections: null }))
}, ({ statusCode, headers, body }) => {
t.strictEqual(statusCode, 302)
t.strictEqual(headers.location, `http://${serverRoot}/302/1`)
return body
}),
createWritable(body)
)
t.strictEqual(body.length, 0)
})
test('should not follow redirects when using RedirectAgent within pipeline', async t => {
t = tspl(t, { plan: 3 })
const body = []
const serverRoot = await startRedirectingServer()
await streamPipeline(
createReadable('REQUEST'),
undiciPipeline(`http://${serverRoot}/`, { dispatcher: new Client(`http://${serverRoot}/`).compose(redirect({ maxRedirections: 1 })) }, ({ statusCode, headers, body }) => {
t.strictEqual(statusCode, 302)
t.strictEqual(headers.location, `http://${serverRoot}/302/1`)
return body
}),
createWritable(body)
)
t.strictEqual(body.length, 0)
})
================================================
FILE: test/redirect-request.js
================================================
'use strict'
const { tspl } = require('@matteo.collina/tspl')
const { test, after } = require('node:test')
const undici = require('..')
const {
startRedirectingServer,
startRedirectingWithBodyServer,
startRedirectingChainServers,
startRedirectingWithoutLocationServer,
startRedirectingWithAuthorization,
startRedirectingWithCookie,
startRedirectingWithQueryParams
} = require('./utils/redirecting-servers')
const { createReadable, createReadableStream } = require('./utils/stream')
const { Headers: UndiciHeaders } = require('..')
const redirect = undici.interceptors.redirect
for (const factory of [
(server, opts) => new undici.Agent(opts).compose(redirect({ maxRedirections: opts?.maxRedirections })),
(server, opts) => new undici.Pool(`http://${server}`, opts).compose(redirect({ maxRedirections: opts?.maxRedirections })),
(server, opts) => new undici.Client(`http://${server}`, opts).compose(redirect({ maxRedirections: opts?.maxRedirections }))
]) {
const request = (t, server, opts, ...args) => {
const dispatcher = factory(server, opts)
after(() => dispatcher.close())
return undici.request(args[0], { ...args[1], dispatcher }, args[2])
}
test('should always have a history with the final URL even if no redirections were followed', async t => {
t = tspl(t, { plan: 4 })
const server = await startRedirectingServer()
const { statusCode, headers, body: bodyStream, context: { history } } = await request(t, server, undefined, `http://${server}/200?key=value`, {
maxRedirections: 10
})
const body = await bodyStream.text()
t.strictEqual(statusCode, 200)
t.ok(!headers.location)
t.deepStrictEqual(history.map(x => x.toString()), [`http://${server}/200?key=value`])
t.strictEqual(body, `GET /5 key=value :: host@${server} connection@keep-alive`)
await t.completed
})
test('should not follow redirection by default if not using RedirectAgent', async t => {
t = tspl(t, { plan: 3 })
const server = await startRedirectingServer()
const { statusCode, headers, body: bodyStream } = await request(t, server, undefined, `http://${server}`)
const body = await bodyStream.text()
t.strictEqual(statusCode, 302)
t.strictEqual(headers.location, `http://${server}/302/1`)
t.strictEqual(body.length, 0)
await t.completed
})
test('should follow redirection after a HTTP 300', async t => {
t = tspl(t, { plan: 4 })
const server = await startRedirectingServer()
const { statusCode, headers, body: bodyStream, context: { history } } = await request(t, server, undefined, `http://${server}/300?key=value`, {
maxRedirections: 10
})
const body = await bodyStream.text()
t.strictEqual(statusCode, 200)
t.ok(!headers.location)
t.deepStrictEqual(history.map(x => x.toString()), [
`http://${server}/300?key=value`,
`http://${server}/300/1?key=value`,
`http://${server}/300/2?key=value`,
`http://${server}/300/3?key=value`,
`http://${server}/300/4?key=value`,
`http://${server}/300/5?key=value`
])
t.strictEqual(body, `GET /5 key=value :: host@${server} connection@keep-alive`)
await t.completed
})
test('should follow redirection after a HTTP 300 default', async t => {
t = tspl(t, { plan: 4 })
const server = await startRedirectingServer()
const { statusCode, headers, body: bodyStream, context: { history } } = await request(t, server, undefined, `http://${server}/300?key=value`, { maxRedirections: 10 })
const body = await bodyStream.text()
t.strictEqual(statusCode, 200)
t.ok(!headers.location)
t.deepStrictEqual(history.map(x => x.toString()), [
`http://${server}/300?key=value`,
`http://${server}/300/1?key=value`,
`http://${server}/300/2?key=value`,
`http://${server}/300/3?key=value`,
`http://${server}/300/4?key=value`,
`http://${server}/300/5?key=value`
])
t.strictEqual(body, `GET /5 key=value :: host@${server} connection@keep-alive`)
await t.completed
})
test('should follow redirection after a HTTP 301', async t => {
t = tspl(t, { plan: 3 })
const server = await startRedirectingServer()
const { statusCode, headers, body: bodyStream } = await request(t, server, undefined, `http://${server}/301`, {
method: 'POST',
body: 'REQUEST',
maxRedirections: 10
})
const body = await bodyStream.text()
t.strictEqual(statusCode, 200)
t.ok(!headers.location)
t.strictEqual(body, `GET /5 :: host@${server} connection@keep-alive`)
})
test('should follow redirection after a HTTP 302', async t => {
t = tspl(t, { plan: 3 })
const server = await startRedirectingServer()
const { statusCode, headers, body: bodyStream } = await request(t, server, undefined, `http://${server}/302`, {
method: 'PUT',
body: Buffer.from('REQUEST'),
maxRedirections: 10
})
const body = await bodyStream.text()
t.strictEqual(statusCode, 200)
t.ok(!headers.location)
t.strictEqual(body, `PUT /5 :: host@${server} connection@keep-alive content-length@7 :: REQUEST`)
})
test('should follow redirection after a HTTP 303 changing method to GET', async t => {
t = tspl(t, { plan: 3 })
const server = await startRedirectingServer()
const { statusCode, headers, body: bodyStream } = await request(t, server, undefined, `http://${server}/303`, {
method: 'PATCH',
body: 'REQUEST',
maxRedirections: 10
})
const body = await bodyStream.text()
t.strictEqual(statusCode, 200)
t.ok(!headers.location)
t.strictEqual(body, `GET /5 :: host@${server} connection@keep-alive`)
await t.completed
})
test('should remove Host and request body related headers when following HTTP 303 (array)', async t => {
t = tspl(t, { plan: 3 })
const server = await startRedirectingServer()
const { statusCode, headers, body: bodyStream } = await request(t, server, undefined, `http://${server}/303`, {
method: 'PATCH',
headers: [
'Content-Encoding',
'gzip',
'X-Foo1',
'1',
'X-Foo2',
'2',
'Content-Type',
'application/json',
'X-Foo3',
'3',
'Host',
'localhost',
'X-Bar',
'4'
],
maxRedirections: 10
})
const body = await bodyStream.text()
t.strictEqual(statusCode, 200)
t.ok(!headers.location)
t.strictEqual(body, `GET /5 :: host@${server} connection@keep-alive x-foo1@1 x-foo2@2 x-foo3@3 x-bar@4`)
await t.completed
})
test('should remove Host and request body related headers when following HTTP 303 (object)', async t => {
t = tspl(t, { plan: 3 })
const server = await startRedirectingServer()
const { statusCode, headers, body: bodyStream } = await request(t, server, undefined, `http://${server}/303`, {
method: 'PATCH',
headers: {
'Content-Encoding': 'gzip',
'X-Foo1': '1',
'X-Foo2': '2',
'Content-Type': 'application/json',
'X-Foo3': '3',
Host: 'localhost',
'X-Bar': '4'
},
maxRedirections: 10
})
const body = await bodyStream.text()
t.strictEqual(statusCode, 200)
t.ok(!headers.location)
t.strictEqual(body, `GET /5 :: host@${server} connection@keep-alive x-foo1@1 x-foo2@2 x-foo3@3 x-bar@4`)
await t.completed
})
test('should remove Host and request body related headers when following HTTP 303 (Global Headers)', async t => {
t = tspl(t, { plan: 3 })
const server = await startRedirectingServer()
const { statusCode, headers, body: bodyStream } = await request(t, server, undefined, `http://${server}/303`, {
method: 'PATCH',
// eslint-disable-next-line no-restricted-globals
headers: new Headers({
'Content-Encoding': 'gzip',
'X-Foo1': '1',
'X-Foo2': '2',
'Content-Type': 'application/json',
'X-Foo3': '3',
Host: 'localhost',
'X-Bar': '4'
}),
maxRedirections: 10
})
const body = await bodyStream.text()
t.strictEqual(statusCode, 200)
t.ok(!headers.location)
t.strictEqual(body, `GET /5 :: host@${server} connection@keep-alive x-bar@4 x-foo1@1 x-foo2@2 x-foo3@3`)
await t.completed
})
test('should remove Host and request body related headers when following HTTP 303 (Undici Headers)', async t => {
t = tspl(t, { plan: 3 })
const server = await startRedirectingServer()
const { statusCode, headers, body: bodyStream } = await request(t, server, undefined, `http://${server}/303`, {
method: 'PATCH',
headers: new UndiciHeaders({
'Content-Encoding': 'gzip',
'X-Foo1': '1',
'X-Foo2': '2',
'Content-Type': 'application/json',
'X-Foo3': '3',
Host: 'localhost',
'X-Bar': '4'
}),
maxRedirections: 10
})
const body = await bodyStream.text()
t.strictEqual(statusCode, 200)
t.ok(!headers.location)
t.strictEqual(body, `GET /5 :: host@${server} connection@keep-alive x-bar@4 x-foo1@1 x-foo2@2 x-foo3@3`)
await t.completed
})
test('should remove Host and request body related headers when following HTTP 303 (Maps)', async t => {
t = tspl(t, { plan: 3 })
const server = await startRedirectingServer()
const { statusCode, headers, body: bodyStream } = await request(t, server, undefined, `http://${server}/303`, {
method: 'PATCH',
headers: new Map([
['Content-Encoding', 'gzip'],
['X-Foo1', '1'],
['X-Foo2', '2'],
['Content-Type', 'application/json'],
['X-Foo3', '3'],
['Host', 'localhost'],
['X-Bar', '4']
]),
maxRedirections: 10
})
const body = await bodyStream.text()
t.strictEqual(statusCode, 200)
t.ok(!headers.location)
t.strictEqual(body, `GET /5 :: host@${server} connection@keep-alive x-foo1@1 x-foo2@2 x-foo3@3 x-bar@4`)
await t.completed
})
test('should follow redirection after a HTTP 307', async t => {
t = tspl(t, { plan: 3 })
const server = await startRedirectingServer()
const { statusCode, headers, body: bodyStream } = await request(t, server, undefined, `http://${server}/307`, {
method: 'DELETE',
maxRedirections: 10
})
const body = await bodyStream.text()
t.strictEqual(statusCode, 200)
t.ok(!headers.location)
t.strictEqual(body, `DELETE /5 :: host@${server} connection@keep-alive`)
await t.completed
})
test('should follow redirection after a HTTP 308', async t => {
t = tspl(t, { plan: 3 })
const server = await startRedirectingServer()
const { statusCode, headers, body: bodyStream } = await request(t, server, undefined, `http://${server}/308`, {
method: 'OPTIONS',
maxRedirections: 10
})
const body = await bodyStream.text()
t.strictEqual(statusCode, 200)
t.ok(!headers.location)
t.strictEqual(body, `OPTIONS /5 :: host@${server} connection@keep-alive`)
await t.completed
})
test('should ignore HTTP 3xx response bodies', async t => {
t = tspl(t, { plan: 4 })
const server = await startRedirectingWithBodyServer()
const { statusCode, headers, body: bodyStream, context: { history } } = await request(t, server, undefined, `http://${server}/`, {
maxRedirections: 10
})
const body = await bodyStream.text()
t.strictEqual(statusCode, 200)
t.ok(!headers.location)
t.deepStrictEqual(history.map(x => x.toString()), [`http://${server}/`, `http://${server}/end`])
t.strictEqual(body, 'FINAL')
await t.completed
})
test('should ignore query after redirection', async t => {
t = tspl(t, { plan: 3 })
const server = await startRedirectingWithQueryParams()
const { statusCode, headers, context: { history } } = await request(t, server, undefined, `http://${server}/`, {
maxRedirections: 10,
query: { param1: 'first' }
})
t.strictEqual(statusCode, 200)
t.ok(!headers.location)
t.deepStrictEqual(history.map(x => x.toString()), [`http://${server}/`, `http://${server}/?param2=second`])
await t.completed
})
test('should follow a redirect chain up to the allowed number of times', async t => {
t = tspl(t, { plan: 4 })
const server = await startRedirectingServer()
const { statusCode, headers, body: bodyStream, context: { history } } = await request(t, server, undefined, `http://${server}/300`, {
maxRedirections: 2
})
const body = await bodyStream.text()
t.strictEqual(statusCode, 300)
t.strictEqual(headers.location, `http://${server}/300/3`)
t.deepStrictEqual(history.map(x => x.toString()), [`http://${server}/300`, `http://${server}/300/1`, `http://${server}/300/2`])
t.strictEqual(body.length, 0)
await t.completed
})
test('should follow a redirect chain up to the allowed number of times for redirectionLimitReached', async t => {
t = tspl(t, { plan: 1 })
const server = await startRedirectingServer()
try {
await request(t, server, undefined, `http://${server}/300`, {
maxRedirections: 2,
throwOnMaxRedirect: true
})
} catch (error) {
if (error.message.startsWith('max redirects')) {
t.ok(true, 'Max redirects handled correctly')
} else {
t.fail(`Unexpected error: ${error.message}`)
}
}
await t.completed
})
test('when a Location response header is NOT present', async t => {
t = tspl(t, { plan: 6 * 3 })
const redirectCodes = [300, 301, 302, 303, 307, 308]
const server = await startRedirectingWithoutLocationServer()
for (const code of redirectCodes) {
const { statusCode, headers, body: bodyStream } = await request(t, server, undefined, `http://${server}/${code}`, {
maxRedirections: 10
})
const body = await bodyStream.text()
t.strictEqual(statusCode, code)
t.ok(!headers.location)
t.strictEqual(body.length, 0)
}
await t.completed
})
test('should not allow invalid maxRedirections arguments', async t => {
t = tspl(t, { plan: 1 })
try {
await request(t, 'localhost', undefined, 'http://localhost', {
method: 'GET',
maxRedirections: 'INVALID'
})
t.fail('Did not throw')
} catch (err) {
t.strictEqual(err.message, 'maxRedirections must be a positive number')
}
await t.completed
})
test('should not allow invalid maxRedirections arguments default', async t => {
t = tspl(t, { plan: 1 })
try {
await request(t, 'localhost', undefined, 'http://localhost', {
method: 'GET',
maxRedirections: 'INVALID'
})
t.fail('Did not throw')
} catch (err) {
t.strictEqual(err.message, 'maxRedirections must be a positive number')
}
await t.completed
})
test('should not follow redirects when using ReadableStream request bodies', async t => {
t = tspl(t, { plan: 3 })
const server = await startRedirectingServer()
const { statusCode, headers, body: bodyStream } = await request(t, server, undefined, `http://${server}/301`, {
method: 'PUT',
body: createReadableStream('REQUEST'),
maxRedirections: 10
})
const body = await bodyStream.text()
t.strictEqual(statusCode, 301)
t.strictEqual(headers.location, `http://${server}/301/2`)
t.strictEqual(body.length, 0)
await t.completed
})
test('should not follow redirects when using Readable request bodies', async t => {
t = tspl(t, { plan: 3 })
const server = await startRedirectingServer()
const { statusCode, headers, body: bodyStream } = await request(t, server, undefined, `http://${server}/301`, {
method: 'PUT',
body: createReadable('REQUEST'),
maxRedirections: 10
})
const body = await bodyStream.text()
t.strictEqual(statusCode, 301)
t.strictEqual(headers.location, `http://${server}/301/1`)
t.strictEqual(body.length, 0)
await t.completed
})
test('should follow redirects when using Readable request bodies for POST 301', async t => {
t = tspl(t, { plan: 1 })
const server = await startRedirectingServer()
const { statusCode, body: bodyStream } = await request(t, server, undefined, `http://${server}/301`, {
method: 'POST',
body: createReadable('REQUEST'),
maxRedirections: 10
})
await bodyStream.text()
t.strictEqual(statusCode, 200)
await t.completed
})
}
test('should follow redirections when going cross origin', async t => {
t = tspl(t, { plan: 4 })
const [server1, server2, server3] = await startRedirectingChainServers()
const { statusCode, headers, body: bodyStream, context: { history } } = await undici.request(`http://${server1}`, {
method: 'POST',
dispatcher: new undici.Agent({}).compose(redirect({ maxRedirections: 10 }))
})
const body = await bodyStream.text()
t.strictEqual(statusCode, 200)
t.ok(!headers.location)
t.deepStrictEqual(history.map(x => x.toString()), [
`http://${server1}/`,
`http://${server2}/`,
`http://${server3}/`,
`http://${server2}/end`,
`http://${server3}/end`,
`http://${server1}/end`
])
t.strictEqual(body, 'GET')
await t.completed
})
test('should handle errors (callback)', async t => {
t = tspl(t, { plan: 1 })
undici.request(
'http://localhost:0',
{
dispatcher: new undici.Agent({}).compose(redirect({ maxRedirections: 10 }))
},
error => {
t.match(error.code, /EADDRNOTAVAIL|ECONNREFUSED/)
}
)
await t.completed
})
test('should handle errors (promise)', async t => {
t = tspl(t, { plan: 1 })
try {
await undici.request('http://localhost:0', { dispatcher: new undici.Agent({}).compose(redirect({ maxRedirections: 10 })) })
t.fail('Did not throw')
} catch (error) {
t.match(error.code, /EADDRNOTAVAIL|ECONNREFUSED/)
}
await t.completed
})
test('removes authorization header on third party origin', async t => {
t = tspl(t, { plan: 1 })
const [server1] = await startRedirectingWithAuthorization('secret')
const { body: bodyStream } = await undici.request(`http://${server1}`, {
dispatcher: new undici.Agent({}).compose(redirect({ maxRedirections: 10 })),
headers: {
authorization: 'secret'
}
})
const body = await bodyStream.text()
t.strictEqual(body, '')
await t.completed
})
test('removes cookie header on third party origin', async t => {
t = tspl(t, { plan: 1 })
const [server1] = await startRedirectingWithCookie('a=b')
const { body: bodyStream } = await undici.request(`http://${server1}`, {
dispatcher: new undici.Agent({}).compose(redirect({ maxRedirections: 10 })),
headers: {
cookie: 'a=b'
}
})
const body = await bodyStream.text()
t.strictEqual(body, '')
await t.completed
})
================================================
FILE: test/redirect-stream.js
================================================
'use strict'
const { tspl } = require('@matteo.collina/tspl')
const { test, describe } = require('node:test')
const { stream, Agent, Client, interceptors: { redirect } } = require('..')
const {
startRedirectingServer,
startRedirectingWithBodyServer,
startRedirectingChainServers,
startRedirectingWithoutLocationServer,
startRedirectingWithAuthorization,
startRedirectingWithCookie
} = require('./utils/redirecting-servers')
const { createReadable, createWritable } = require('./utils/stream')
test('should always have a history with the final URL even if no redirections were followed', async t => {
t = tspl(t, { plan: 4 })
const body = []
const server = await startRedirectingServer()
await stream(
`http://${server}/200?key=value`,
{ opaque: body, dispatcher: new Client(`http://${server}/`).compose(redirect({ maxRedirections: 10 })) },
({ statusCode, headers, opaque, context: { history } }) => {
t.strictEqual(statusCode, 200)
t.strictEqual(headers.location, undefined)
t.deepStrictEqual(history.map(x => x.toString()), [
`http://${server}/200?key=value`
])
return createWritable(opaque)
}
)
t.strictEqual(body.join(''), `GET /5 key=value :: host@${server} connection@keep-alive`)
})
test('should not follow redirection by default if max redirect = 0', async t => {
t = tspl(t, { plan: 3 })
const body = []
const server = await startRedirectingServer()
await stream(`http://${server}`, { opaque: body, dispatcher: new Agent({}).compose(redirect({ maxRedirections: 0 })) }, ({ statusCode, headers, opaque }) => {
t.strictEqual(statusCode, 302)
t.strictEqual(headers.location, `http://${server}/302/1`)
return createWritable(opaque)
})
t.strictEqual(body.length, 0)
})
test('should follow redirection after a HTTP 300', async t => {
t = tspl(t, { plan: 4 })
const body = []
const server = await startRedirectingServer()
await stream(
`http://${server}/300?key=value`,
{ opaque: body, dispatcher: new Client(`http://${server}/`).compose(redirect({ maxRedirections: 10 })) },
({ statusCode, headers, opaque, context: { history } }) => {
t.strictEqual(statusCode, 200)
t.strictEqual(headers.location, undefined)
t.deepStrictEqual(history.map(x => x.toString()), [
`http://${server}/300?key=value`,
`http://${server}/300/1?key=value`,
`http://${server}/300/2?key=value`,
`http://${server}/300/3?key=value`,
`http://${server}/300/4?key=value`,
`http://${server}/300/5?key=value`
])
return createWritable(opaque)
}
)
t.strictEqual(body.join(''), `GET /5 key=value :: host@${server} connection@keep-alive`)
})
test('should follow redirection after a HTTP 301 changing method to GET', async t => {
t = tspl(t, { plan: 3 })
const body = []
const server = await startRedirectingServer()
await stream(
`http://${server}/301`,
{ method: 'POST', body: 'REQUEST', opaque: body, dispatcher: new Client(`http://${server}/`).compose(redirect({ maxRedirections: 10 })) },
({ statusCode, headers, opaque }) => {
t.strictEqual(statusCode, 200)
t.strictEqual(headers.location, undefined)
return createWritable(opaque)
}
)
t.strictEqual(body.join(''), `GET /5 :: host@${server} connection@keep-alive`)
})
test('should follow redirection after a HTTP 302', async t => {
t = tspl(t, { plan: 3 })
const body = []
const server = await startRedirectingServer()
await stream(
`http://${server}/302`,
{ method: 'PUT', body: Buffer.from('REQUEST'), opaque: body, dispatcher: new Client(`http://${server}/`).compose(redirect({ maxRedirections: 10 })) },
({ statusCode, headers, opaque }) => {
t.strictEqual(statusCode, 200)
t.strictEqual(headers.location, undefined)
return createWritable(opaque)
}
)
t.strictEqual(body.join(''), `PUT /5 :: host@${server} connection@keep-alive content-length@7 :: REQUEST`)
})
test('should follow redirection after a HTTP 303 changing method to GET', async t => {
t = tspl(t, { plan: 3 })
const body = []
const server = await startRedirectingServer()
await stream(`http://${server}/303`, { opaque: body, dispatcher: new Client(`http://${server}/`).compose(redirect({ maxRedirections: 10 })) }, ({ statusCode, headers, opaque }) => {
t.strictEqual(statusCode, 200)
t.strictEqual(headers.location, undefined)
return createWritable(opaque)
})
t.strictEqual(body.join(''), `GET /5 :: host@${server} connection@keep-alive`)
})
test('should remove Host and request body related headers when following HTTP 303 (array)', async t => {
t = tspl(t, { plan: 3 })
const body = []
const server = await startRedirectingServer()
await stream(
`http://${server}/303`,
{
method: 'PATCH',
headers: [
'Content-Encoding',
'gzip',
'X-Foo1',
'1',
'X-Foo2',
'2',
'Content-Type',
'application/json',
'X-Foo3',
'3',
'Host',
'localhost',
'X-Bar',
'4'
],
opaque: body,
dispatcher: new Client(`http://${server}/`).compose(redirect({ maxRedirections: 10 }))
},
({ statusCode, headers, opaque }) => {
t.strictEqual(statusCode, 200)
t.strictEqual(headers.location, undefined)
return createWritable(opaque)
}
)
t.strictEqual(body.join(''), `GET /5 :: host@${server} connection@keep-alive x-foo1@1 x-foo2@2 x-foo3@3 x-bar@4`)
})
test('should remove Host and request body related headers when following HTTP 303 (object)', async t => {
t = tspl(t, { plan: 3 })
const body = []
const server = await startRedirectingServer()
await stream(
`http://${server}/303`,
{
method: 'PATCH',
headers: {
'Content-Encoding': 'gzip',
'X-Foo1': '1',
'X-Foo2': '2',
'Content-Type': 'application/json',
'X-Foo3': '3',
Host: 'localhost',
'X-Bar': '4'
},
opaque: body,
dispatcher: new Client(`http://${server}/`).compose(redirect({ maxRedirections: 10 }))
},
({ statusCode, headers, opaque }) => {
t.strictEqual(statusCode, 200)
t.strictEqual(headers.location, undefined)
return createWritable(opaque)
}
)
t.strictEqual(body.join(''), `GET /5 :: host@${server} connection@keep-alive x-foo1@1 x-foo2@2 x-foo3@3 x-bar@4`)
})
test('should follow redirection after a HTTP 307', async t => {
t = tspl(t, { plan: 3 })
const body = []
const server = await startRedirectingServer()
await stream(
`http://${server}/307`,
{ method: 'DELETE', opaque: body, dispatcher: new Client(`http://${server}/`).compose(redirect({ maxRedirections: 10 })) },
({ statusCode, headers, opaque }) => {
t.strictEqual(statusCode, 200)
t.strictEqual(headers.location, undefined)
return createWritable(opaque)
}
)
t.strictEqual(body.join(''), `DELETE /5 :: host@${server} connection@keep-alive`)
})
test('should follow redirection after a HTTP 308', async t => {
t = tspl(t, { plan: 3 })
const body = []
const server = await startRedirectingServer()
await stream(
`http://${server}/308`,
{ method: 'OPTIONS', opaque: body, dispatcher: new Client(`http://${server}/`).compose(redirect({ maxRedirections: 10 })) },
({ statusCode, headers, opaque }) => {
t.strictEqual(statusCode, 200)
t.strictEqual(headers.location, undefined)
return createWritable(opaque)
}
)
t.strictEqual(body.join(''), `OPTIONS /5 :: host@${server} connection@keep-alive`)
})
test('should ignore HTTP 3xx response bodies', async t => {
t = tspl(t, { plan: 4 })
const body = []
const server = await startRedirectingWithBodyServer()
await stream(
`http://${server}/`,
{ opaque: body, dispatcher: new Client(`http://${server}/`).compose(redirect({ maxRedirections: 10 })) },
({ statusCode, headers, opaque, context: { history } }) => {
t.strictEqual(statusCode, 200)
t.strictEqual(headers.location, undefined)
t.deepStrictEqual(history.map(x => x.toString()), [`http://${server}/`, `http://${server}/end`])
return createWritable(opaque)
}
)
t.strictEqual(body.join(''), 'FINAL')
})
test('should follow a redirect chain up to the allowed number of times', async t => {
t = tspl(t, { plan: 4 })
const body = []
const server = await startRedirectingServer()
await stream(
`http://${server}/300`,
{ opaque: body, dispatcher: new Client(`http://${server}/`).compose(redirect({ maxRedirections: 2 })) },
({ statusCode, headers, opaque, context: { history } }) => {
t.strictEqual(statusCode, 300)
t.strictEqual(headers.location, `http://${server}/300/3`)
t.deepStrictEqual(history.map(x => x.toString()), [`http://${server}/300`, `http://${server}/300/1`, `http://${server}/300/2`])
return createWritable(opaque)
}
)
t.strictEqual(body.length, 0)
})
test('should follow redirections when going cross origin', async t => {
t = tspl(t, { plan: 4 })
const [server1, server2, server3] = await startRedirectingChainServers()
const body = []
await stream(
`http://${server1}`,
{ method: 'POST', opaque: body, dispatcher: new Agent({}).compose(redirect({ maxRedirections: 10 })) },
({ statusCode, headers, opaque, context: { history } }) => {
t.strictEqual(statusCode, 200)
t.strictEqual(headers.location, undefined)
t.deepStrictEqual(history.map(x => x.toString()), [
`http://${server1}/`,
`http://${server2}/`,
`http://${server3}/`,
`http://${server2}/end`,
`http://${server3}/end`,
`http://${server1}/end`
])
return createWritable(opaque)
}
)
t.strictEqual(body.join(''), 'GET')
})
describe('when a Location response header is NOT present', async () => {
const redirectCodes = [300, 301, 302, 303, 307, 308]
const server = await startRedirectingWithoutLocationServer()
for (const code of redirectCodes) {
test(`should return the original response after a HTTP ${code}`, async t => {
t = tspl(t, { plan: 3 })
const body = []
await stream(
`http://${server}/${code}`,
{ opaque: body },
({ statusCode, headers, opaque }) => {
t.strictEqual(statusCode, code)
t.strictEqual(headers.location, undefined)
return createWritable(opaque)
}
)
t.strictEqual(body.length, 0)
await t.completed
})
}
})
test('should not follow redirects when using Readable request bodies', async t => {
t = tspl(t, { plan: 3 })
const body = []
const server = await startRedirectingServer()
await stream(
`http://${server}`,
{
method: 'POST',
body: createReadable('REQUEST'),
opaque: body
},
({ statusCode, headers, opaque }) => {
t.strictEqual(statusCode, 302)
t.strictEqual(headers.location, `http://${server}/302/1`)
return createWritable(opaque)
}
)
t.strictEqual(body.length, 0)
})
test('should handle errors', async t => {
t = tspl(t, { plan: 2 })
const body = []
try {
await stream('http://localhost:0', { opaque: body }, ({ statusCode, headers, opaque }) => {
return createWritable(opaque)
})
throw new Error('Did not throw')
} catch (error) {
t.match(error.code, /EADDRNOTAVAIL|ECONNREFUSED/)
t.strictEqual(body.length, 0)
}
})
test('removes authorization header on third party origin', async t => {
t = tspl(t, { plan: 1 })
const body = []
const [server1] = await startRedirectingWithAuthorization('secret')
await stream(`http://${server1}`, {
opaque: body,
headers: {
authorization: 'secret'
}
}, ({ statusCode, headers, opaque }) => createWritable(opaque))
t.strictEqual(body.length, 0)
})
test('removes cookie header on third party origin', async t => {
t = tspl(t, { plan: 1 })
const body = []
const [server1] = await startRedirectingWithCookie('a=b')
await stream(`http://${server1}`, {
opaque: body,
headers: {
cookie: 'a=b'
}
}, ({ statusCode, headers, opaque }) => createWritable(opaque))
t.strictEqual(body.length, 0)
})
================================================
FILE: test/request-crlf.js
================================================
'use strict'
const { tspl } = require('@matteo.collina/tspl')
const { createServer } = require('node:http')
const { test, after } = require('node:test')
const { request, errors } = require('..')
const { once } = require('node:events')
test('should validate content-type CRLF Injection', async (t) => {
t = tspl(t, { plan: 2 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
t.fail('should not receive any request')
res.statusCode = 200
res.end('hello')
})
after(() => server.close())
server.listen(0)
await once(server, 'listening')
try {
await request(`http://localhost:${server.address().port}`, {
method: 'GET',
headers: {
'content-type': 'application/json\r\n\r\nGET /foo2 HTTP/1.1'
}
})
t.fail('request should fail')
} catch (e) {
t.ok(e instanceof errors.InvalidArgumentError)
t.strictEqual(e.message, 'invalid content-type header')
}
await t.completed
})
================================================
FILE: test/request-signal.js
================================================
'use strict'
const { createServer } = require('node:http')
const { test, after } = require('node:test')
const { tspl } = require('@matteo.collina/tspl')
const { request } = require('..')
test('pre abort signal w/ reason', async (t) => {
t = tspl(t, { plan: 1 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.end('asd')
})
after(() => server.close())
server.listen(0, async () => {
const ac = new AbortController()
const _err = new Error()
ac.abort(_err)
try {
await request(`http://0.0.0.0:${server.address().port}`, { signal: ac.signal })
} catch (err) {
t.equal(err, _err)
}
})
await t.completed
})
test('post abort signal', async (t) => {
t = tspl(t, { plan: 1 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.end('asd')
})
after(() => server.close())
server.listen(0, async () => {
const ac = new AbortController()
const ures = await request(`http://0.0.0.0:${server.address().port}`, { signal: ac.signal })
ac.abort()
try {
/* eslint-disable-next-line no-unused-vars */
for await (const chunk of ures.body) {
// Do nothing...
}
} catch (err) {
t.equal(err.name, 'AbortError')
}
})
await t.completed
})
test('post abort signal w/ reason', async (t) => {
t = tspl(t, { plan: 1 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.end('asd')
})
after(() => server.close())
server.listen(0, async () => {
const ac = new AbortController()
const _err = new Error()
const ures = await request(`http://0.0.0.0:${server.address().port}`, { signal: ac.signal })
ac.abort(_err)
try {
/* eslint-disable-next-line no-unused-vars */
for await (const chunk of ures.body) {
// Do nothing...
}
} catch (err) {
t.equal(err, _err)
}
})
await t.completed
})
================================================
FILE: test/request-timeout.js
================================================
'use strict'
const { tspl } = require('@matteo.collina/tspl')
const { resolve: pathResolve } = require('node:path')
const { test, after, beforeEach } = require('node:test')
const { createReadStream, writeFileSync, unlinkSync } = require('node:fs')
const { Client, errors } = require('..')
const { kConnect } = require('../lib/core/symbols')
const { createServer } = require('node:http')
const EventEmitter = require('node:events')
const FakeTimers = require('@sinonjs/fake-timers')
const { AbortController } = require('abort-controller')
const {
pipeline,
Readable,
Writable,
PassThrough
} = require('node:stream')
const {
tick: fastTimersTick,
reset: resetFastTimers
} = require('../lib/util/timers')
beforeEach(() => {
resetFastTimers()
})
test('request timeout', async (t) => {
t = tspl(t, { plan: 1 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
setTimeout(() => {
res.end('hello')
}, 2000)
})
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`, { headersTimeout: 500 })
after(() => client.destroy())
client.request({ path: '/', method: 'GET' }, (err, response) => {
t.ok(err instanceof errors.HeadersTimeoutError)
})
})
await t.completed
})
test('request timeout with readable body', async (t) => {
t = tspl(t, { plan: 1 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
})
after(() => server.close())
const tempfile = pathResolve(__dirname, 'request-timeout.10mb.bin')
writeFileSync(tempfile, Buffer.alloc(10 * 1024 * 1024))
after(() => unlinkSync(tempfile))
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`, { headersTimeout: 1e3 })
after(() => client.destroy())
const body = createReadStream(tempfile)
client.request({ path: '/', method: 'POST', body }, (err, response) => {
t.ok(err instanceof errors.HeadersTimeoutError)
})
})
await t.completed
})
test('body timeout', async (t) => {
t = tspl(t, { plan: 2 })
const clock = FakeTimers.install({
shouldClearNativeTimers: true,
toFake: ['setTimeout', 'clearTimeout']
})
after(() => clock.uninstall())
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.write('hello')
})
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`, { bodyTimeout: 50 })
after(() => client.destroy())
client.request({ path: '/', method: 'GET' }, (err, { body }) => {
t.ifError(err)
body.on('data', () => {
clock.tick(100)
fastTimersTick(100)
}).on('error', (err) => {
t.ok(err instanceof errors.BodyTimeoutError)
})
})
clock.tick(50)
fastTimersTick(50)
})
await t.completed
})
test('overridden request timeout', async (t) => {
t = tspl(t, { plan: 1 })
const clock = FakeTimers.install({
shouldClearNativeTimers: true,
toFake: ['setTimeout', 'clearTimeout']
})
after(() => clock.uninstall())
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
setTimeout(() => {
res.end('hello')
}, 100)
clock.tick(100)
fastTimersTick(100)
})
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`, { headersTimeout: 500 })
after(() => client.destroy())
client.request({ path: '/', method: 'GET', headersTimeout: 50 }, (err, response) => {
t.ok(err instanceof errors.HeadersTimeoutError)
})
clock.tick(50)
fastTimersTick(50)
})
await t.completed
})
test('overridden body timeout', async (t) => {
t = tspl(t, { plan: 2 })
const clock = FakeTimers.install({
shouldClearNativeTimers: true,
toFake: ['setTimeout', 'clearTimeout']
})
after(() => clock.uninstall())
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.write('hello')
})
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`, { bodyTimeout: 500 })
after(() => client.destroy())
client.request({ path: '/', method: 'GET', bodyTimeout: 50 }, (err, { body }) => {
t.ifError(err)
body.on('data', () => {
fastTimersTick()
fastTimersTick()
}).on('error', (err) => {
t.ok(err instanceof errors.BodyTimeoutError)
})
})
fastTimersTick()
fastTimersTick()
})
await t.completed
})
test('With EE signal', async (t) => {
t = tspl(t, { plan: 1 })
const clock = FakeTimers.install({
shouldClearNativeTimers: true,
toFake: ['setTimeout', 'clearTimeout']
})
after(() => clock.uninstall())
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
setTimeout(() => {
res.end('hello')
}, 100)
clock.tick(100)
fastTimersTick(100)
})
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`, {
headersTimeout: 50
})
const ee = new EventEmitter()
after(() => client.destroy())
client.request({ path: '/', method: 'GET', signal: ee }, (err, response) => {
t.ok(err instanceof errors.HeadersTimeoutError)
})
clock.tick(50)
fastTimersTick(50)
})
await t.completed
})
test('With abort-controller signal', async (t) => {
t = tspl(t, { plan: 1 })
const clock = FakeTimers.install({
shouldClearNativeTimers: true,
toFake: ['setTimeout', 'clearTimeout']
})
after(() => clock.uninstall())
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
setTimeout(() => {
res.end('hello')
}, 100)
clock.tick(100)
fastTimersTick(100)
})
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`, {
headersTimeout: 50
})
const abortController = new AbortController()
after(() => client.destroy())
client.request({ path: '/', method: 'GET', signal: abortController.signal }, (err, response) => {
t.ok(err instanceof errors.HeadersTimeoutError)
})
clock.tick(50)
fastTimersTick(50)
})
await t.completed
})
test('Abort before timeout (EE)', async (t) => {
t = tspl(t, { plan: 1 })
const clock = FakeTimers.install({
shouldClearNativeTimers: true,
toFake: ['setTimeout', 'clearTimeout']
})
after(() => clock.uninstall())
const ee = new EventEmitter()
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
setTimeout(() => {
res.end('hello')
}, 100)
ee.emit('abort')
clock.tick(50)
fastTimersTick(50)
})
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`, {
headersTimeout: 50
})
after(() => client.destroy())
client.request({ path: '/', method: 'GET', signal: ee }, (err, response) => {
t.ok(err instanceof errors.RequestAbortedError)
clock.tick(100)
fastTimersTick(100)
})
})
await t.completed
})
test('Abort before timeout (abort-controller)', async (t) => {
t = tspl(t, { plan: 1 })
const clock = FakeTimers.install({
shouldClearNativeTimers: true,
toFake: ['setTimeout', 'clearTimeout']
})
after(() => clock.uninstall())
const abortController = new AbortController()
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
setTimeout(() => {
res.end('hello')
}, 100)
abortController.abort()
clock.tick(50)
fastTimersTick(50)
})
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`, {
headersTimeout: 50
})
after(() => client.destroy())
client.request({ path: '/', method: 'GET', signal: abortController.signal }, (err, response) => {
t.ok(err instanceof errors.RequestAbortedError)
clock.tick(100)
fastTimersTick(100)
})
})
await t.completed
})
test('Timeout with pipelining', async (t) => {
t = tspl(t, { plan: 3 })
const clock = FakeTimers.install({
shouldClearNativeTimers: true,
toFake: ['setTimeout', 'clearTimeout']
})
after(() => clock.uninstall())
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
setTimeout(() => {
res.end('hello')
}, 100)
clock.tick(50)
fastTimersTick(50)
})
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`, {
pipelining: 10,
headersTimeout: 50
})
after(() => client.destroy())
client.request({ path: '/', method: 'GET' }, (err, response) => {
t.ok(err instanceof errors.HeadersTimeoutError)
})
client.request({ path: '/', method: 'GET' }, (err, response) => {
t.ok(err instanceof errors.HeadersTimeoutError)
})
client.request({ path: '/', method: 'GET' }, (err, response) => {
t.ok(err instanceof errors.HeadersTimeoutError)
})
})
await t.completed
})
test('Global option', async (t) => {
t = tspl(t, { plan: 1 })
const clock = FakeTimers.install({
shouldClearNativeTimers: true,
toFake: ['setTimeout', 'clearTimeout']
})
after(() => clock.uninstall())
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
setTimeout(() => {
res.end('hello')
}, 100)
clock.tick(100)
fastTimersTick(100)
})
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`, {
headersTimeout: 50
})
after(() => client.destroy())
client.request({ path: '/', method: 'GET' }, (err, response) => {
t.ok(err instanceof errors.HeadersTimeoutError)
})
clock.tick(50)
fastTimersTick(50)
})
await t.completed
})
test('Request options overrides global option', async (t) => {
t = tspl(t, { plan: 1 })
const clock = FakeTimers.install({
shouldClearNativeTimers: true,
toFake: ['setTimeout', 'clearTimeout']
})
after(() => clock.uninstall())
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
setTimeout(() => {
res.end('hello')
}, 100)
clock.tick(100)
fastTimersTick(100)
})
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`, {
headersTimeout: 50
})
after(() => client.destroy())
client.request({ path: '/', method: 'GET' }, (err, response) => {
t.ok(err instanceof errors.HeadersTimeoutError)
})
clock.tick(50)
fastTimersTick(50)
})
await t.completed
})
test('client.destroy should cancel the timeout', async (t) => {
t = tspl(t, { plan: 2 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.end('hello')
})
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`, {
headersTimeout: 100
})
client.request({ path: '/', method: 'GET' }, (err, response) => {
t.ok(err instanceof errors.ClientDestroyedError)
})
client.destroy(err => {
t.ifError(err)
})
})
await t.completed
})
test('client.close should wait for the timeout', async (t) => {
t = tspl(t, { plan: 2 })
const clock = FakeTimers.install({
shouldClearNativeTimers: true,
toFake: ['setTimeout', 'clearTimeout']
})
after(() => clock.uninstall())
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
})
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`, {
headersTimeout: 100
})
after(() => client.destroy())
client.request({ path: '/', method: 'GET' }, (err, response) => {
t.ok(err instanceof errors.HeadersTimeoutError)
})
client.close((err) => {
t.ifError(err)
})
client.on('connect', () => {
process.nextTick(() => {
clock.tick(100)
fastTimersTick(100)
})
})
})
await t.completed
})
test('Validation', async (t) => {
t = tspl(t, { plan: 4 })
try {
const client = new Client('http://localhost:3000', {
headersTimeout: 'foobar'
})
after(() => client.destroy())
} catch (err) {
t.ok(err instanceof errors.InvalidArgumentError)
}
try {
const client = new Client('http://localhost:3000', {
headersTimeout: -1
})
after(() => client.destroy())
} catch (err) {
t.ok(err instanceof errors.InvalidArgumentError)
}
try {
const client = new Client('http://localhost:3000', {
bodyTimeout: 'foobar'
})
after(() => client.destroy())
} catch (err) {
t.ok(err instanceof errors.InvalidArgumentError)
}
try {
const client = new Client('http://localhost:3000', {
bodyTimeout: -1
})
after(() => client.destroy())
} catch (err) {
t.ok(err instanceof errors.InvalidArgumentError)
}
await t.completed
})
test('Disable request timeout', async (t) => {
t = tspl(t, { plan: 2 })
const clock = FakeTimers.install({
shouldClearNativeTimers: true,
toFake: ['setTimeout', 'clearTimeout']
})
after(() => clock.uninstall())
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
setTimeout(() => {
res.end('hello')
}, 32e3)
clock.tick(33e3)
fastTimersTick(33e3)
})
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`, {
headersTimeout: 0,
connectTimeout: 0
})
after(() => client.destroy())
client.request({ path: '/', method: 'GET' }, (err, response) => {
t.ifError(err)
const bufs = []
response.body.on('data', (buf) => {
bufs.push(buf)
})
response.body.on('end', () => {
t.strictEqual('hello', Buffer.concat(bufs).toString('utf8'))
})
})
clock.tick(31e3)
fastTimersTick(31e3)
})
await t.completed
})
test('Disable request timeout for a single request', async (t) => {
t = tspl(t, { plan: 2 })
const clock = FakeTimers.install({
shouldClearNativeTimers: true,
toFake: ['setTimeout', 'clearTimeout']
})
after(() => clock.uninstall())
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
setTimeout(() => {
res.end('hello')
}, 32e3)
clock.tick(33e3)
fastTimersTick(33e3)
})
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`, {
headersTimeout: 0,
connectTimeout: 0
})
after(() => client.destroy())
client.request({ path: '/', method: 'GET' }, (err, response) => {
t.ifError(err)
const bufs = []
response.body.on('data', (buf) => {
bufs.push(buf)
})
response.body.on('end', () => {
t.strictEqual('hello', Buffer.concat(bufs).toString('utf8'))
})
})
clock.tick(31e3)
fastTimersTick(31e3)
})
await t.completed
})
test('stream timeout', async (t) => {
t = tspl(t, { plan: 1 })
const clock = FakeTimers.install({
shouldClearNativeTimers: true,
toFake: ['setTimeout', 'clearTimeout']
})
after(() => clock.uninstall())
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
setTimeout(() => {
res.end('hello')
}, 301e3)
clock.tick(301e3)
fastTimersTick(301e3)
})
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`, { connectTimeout: 0 })
after(() => client.destroy())
client.stream({
path: '/',
method: 'GET',
opaque: new PassThrough()
}, (result) => {
t.fail('Should not be called')
}, (err) => {
t.ok(err instanceof errors.HeadersTimeoutError)
})
})
await t.completed
})
test('stream custom timeout', async (t) => {
t = tspl(t, { plan: 1 })
const clock = FakeTimers.install({
shouldClearNativeTimers: true,
toFake: ['setTimeout', 'clearTimeout']
})
after(() => clock.uninstall())
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
setTimeout(() => {
res.end('hello')
}, 31e3)
clock.tick(31e3)
fastTimersTick(31e3)
})
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`, {
headersTimeout: 30e3
})
after(() => client.destroy())
client.stream({
path: '/',
method: 'GET',
opaque: new PassThrough()
}, (result) => {
t.fail('Should not be called')
}, (err) => {
t.ok(err instanceof errors.HeadersTimeoutError)
})
})
await t.completed
})
test('pipeline timeout', async (t) => {
t = tspl(t, { plan: 1 })
const clock = FakeTimers.install({
shouldClearNativeTimers: true,
toFake: ['setTimeout', 'clearTimeout']
})
after(() => clock.uninstall())
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
setTimeout(() => {
req.pipe(res)
}, 301e3)
clock.tick(301e3)
fastTimersTick(301e3)
})
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.destroy())
const buf = Buffer.alloc(1e6).toString()
pipeline(
new Readable({
read () {
this.push(buf)
this.push(null)
}
}),
client.pipeline({
path: '/',
method: 'PUT'
}, (result) => {
t.fail('Should not be called')
}, (e) => {
t.fail('Should not be called')
}),
new Writable({
write (chunk, encoding, callback) {
callback()
},
final (callback) {
callback()
}
}),
(err) => {
t.ok(err instanceof errors.HeadersTimeoutError)
}
)
})
await t.completed
})
test('pipeline timeout', async (t) => {
t = tspl(t, { plan: 1 })
const clock = FakeTimers.install({
shouldClearNativeTimers: true,
toFake: ['setTimeout', 'clearTimeout']
})
after(() => clock.uninstall())
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
setTimeout(() => {
req.pipe(res)
}, 31e3)
clock.tick(31e3)
fastTimersTick(31e3)
})
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`, {
headersTimeout: 30e3
})
after(() => client.destroy())
const buf = Buffer.alloc(1e6).toString()
pipeline(
new Readable({
read () {
this.push(buf)
this.push(null)
}
}),
client.pipeline({
path: '/',
method: 'PUT'
}, (result) => {
t.fail('Should not be called')
}, (e) => {
t.fail('Should not be called')
}),
new Writable({
write (chunk, encoding, callback) {
callback()
},
final (callback) {
callback()
}
}),
(err) => {
t.ok(err instanceof errors.HeadersTimeoutError)
}
)
})
await t.completed
})
test('client.close should not deadlock', async (t) => {
t = tspl(t, { plan: 2 })
const clock = FakeTimers.install({
shouldClearNativeTimers: true,
toFake: ['setTimeout', 'clearTimeout']
})
after(() => clock.uninstall())
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
})
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`, {
bodyTimeout: 200,
headersTimeout: 100
})
after(() => client.destroy())
client[kConnect](() => {
client.request({
path: '/',
method: 'GET'
}, (err, response) => {
t.ok(err instanceof errors.HeadersTimeoutError)
})
client.close((err) => {
t.ifError(err)
})
clock.tick(100)
fastTimersTick(100)
})
})
await t.completed
})
================================================
FILE: test/request-timeout2.js
================================================
'use strict'
const { tspl } = require('@matteo.collina/tspl')
const { test, after } = require('node:test')
const { once } = require('node:events')
const { Client } = require('..')
const { createServer } = require('node:http')
const { Readable } = require('node:stream')
test('request timeout with slow readable body', async (t) => {
t = tspl(t, { plan: 1 })
const server = createServer({ joinDuplicateHeaders: true }, async (req, res) => {
let str = ''
for await (const x of req) {
str += x
}
res.end(str)
})
after(() => server.close())
server.listen(0)
await once(server, 'listening')
const client = new Client(`http://localhost:${server.address().port}`, { headersTimeout: 50 })
after(() => client.close())
client.on('disconnect', () => {
if (!client.closed && !client.destroyed) {
t.fail('unexpected disconnect')
}
})
const body = new Readable({
read () {
if (this._reading) {
return
}
this._reading = true
this.push('asd')
setTimeout(() => {
this.push('asd')
this.push(null)
}, 2e3)
}
})
client.request({
path: '/',
method: 'POST',
headersTimeout: 1e3,
body
}, async (err, response) => {
t.ifError(err)
await response.body.dump()
})
await t.completed
})
================================================
FILE: test/request.js
================================================
'use strict'
const { tspl } = require('@matteo.collina/tspl')
const { createServer } = require('node:http')
const { test, after, describe } = require('node:test')
const { request, errors } = require('..')
test('no-slash/one-slash pathname should be included in req.path', async (t) => {
t = tspl(t, { plan: 24 })
const pathServer = createServer({ joinDuplicateHeaders: true }, (req, res) => {
t.fail('it shouldn\'t be called')
res.statusCode = 200
res.end('hello')
})
const requestedServer = createServer({ joinDuplicateHeaders: true }, (req, res) => {
t.strictEqual(`/localhost:${pathServer.address().port}`, req.url)
t.strictEqual('GET', req.method)
t.strictEqual(`localhost:${requestedServer.address().port}`, req.headers.host)
res.statusCode = 200
res.end('hello')
})
after(() => {
requestedServer.close()
pathServer.close()
})
await Promise.all([
requestedServer.listen(0),
pathServer.listen(0)
])
const noSlashPathname = await request({
method: 'GET',
origin: `http://localhost:${requestedServer.address().port}`,
pathname: `localhost:${pathServer.address().port}`
})
t.strictEqual(noSlashPathname.statusCode, 200)
const noSlashPath = await request({
method: 'GET',
origin: `http://localhost:${requestedServer.address().port}`,
path: `localhost:${pathServer.address().port}`
})
t.strictEqual(noSlashPath.statusCode, 200)
const noSlashPath2Arg = await request(
`http://localhost:${requestedServer.address().port}`,
{ path: `localhost:${pathServer.address().port}` }
)
t.strictEqual(noSlashPath2Arg.statusCode, 200)
const oneSlashPathname = await request({
method: 'GET',
origin: `http://localhost:${requestedServer.address().port}`,
pathname: `/localhost:${pathServer.address().port}`
})
t.strictEqual(oneSlashPathname.statusCode, 200)
const oneSlashPath = await request({
method: 'GET',
origin: `http://localhost:${requestedServer.address().port}`,
path: `/localhost:${pathServer.address().port}`
})
t.strictEqual(oneSlashPath.statusCode, 200)
const oneSlashPath2Arg = await request(
`http://localhost:${requestedServer.address().port}`,
{ path: `/localhost:${pathServer.address().port}` }
)
t.strictEqual(oneSlashPath2Arg.statusCode, 200)
t.end()
})
test('protocol-relative URL as pathname should be included in req.path', async (t) => {
t = tspl(t, { plan: 12 })
const pathServer = createServer({ joinDuplicateHeaders: true }, (req, res) => {
t.fail('it shouldn\'t be called')
res.statusCode = 200
res.end('hello')
})
const requestedServer = createServer({ joinDuplicateHeaders: true }, (req, res) => {
t.strictEqual(`//localhost:${pathServer.address().port}`, req.url)
t.strictEqual('GET', req.method)
t.strictEqual(`localhost:${requestedServer.address().port}`, req.headers.host)
res.statusCode = 200
res.end('hello')
})
after(() => {
requestedServer.close()
pathServer.close()
})
await Promise.all([
requestedServer.listen(0),
pathServer.listen(0)
])
const noSlashPathname = await request({
method: 'GET',
origin: `http://localhost:${requestedServer.address().port}`,
pathname: `//localhost:${pathServer.address().port}`
})
t.strictEqual(noSlashPathname.statusCode, 200)
const noSlashPath = await request({
method: 'GET',
origin: `http://localhost:${requestedServer.address().port}`,
path: `//localhost:${pathServer.address().port}`
})
t.strictEqual(noSlashPath.statusCode, 200)
const noSlashPath2Arg = await request(
`http://localhost:${requestedServer.address().port}`,
{ path: `//localhost:${pathServer.address().port}` }
)
t.strictEqual(noSlashPath2Arg.statusCode, 200)
t.end()
})
test('Absolute URL as pathname should be included in req.path', async (t) => {
t = tspl(t, { plan: 12 })
const pathServer = createServer({ joinDuplicateHeaders: true }, (req, res) => {
t.fail('it shouldn\'t be called')
res.statusCode = 200
res.end('hello')
})
const requestedServer = createServer({ joinDuplicateHeaders: true }, (req, res) => {
t.strictEqual(`/http://localhost:${pathServer.address().port}`, req.url)
t.strictEqual('GET', req.method)
t.strictEqual(`localhost:${requestedServer.address().port}`, req.headers.host)
res.statusCode = 200
res.end('hello')
})
after(() => {
requestedServer.close()
pathServer.close()
})
await Promise.all([
requestedServer.listen(0),
pathServer.listen(0)
])
const noSlashPathname = await request({
method: 'GET',
origin: `http://localhost:${requestedServer.address().port}`,
pathname: `http://localhost:${pathServer.address().port}`
})
t.strictEqual(noSlashPathname.statusCode, 200)
const noSlashPath = await request({
method: 'GET',
origin: `http://localhost:${requestedServer.address().port}`,
path: `http://localhost:${pathServer.address().port}`
})
t.strictEqual(noSlashPath.statusCode, 200)
const noSlashPath2Arg = await request(
`http://localhost:${requestedServer.address().port}`,
{ path: `http://localhost:${pathServer.address().port}` }
)
t.strictEqual(noSlashPath2Arg.statusCode, 200)
t.end()
})
describe('DispatchOptions#expectContinue', () => {
test('Should throw if invalid expectContinue option', async t => {
t = tspl(t, { plan: 1 })
await t.rejects(request({
method: 'GET',
origin: 'http://somehost.xyz',
expectContinue: 0
}), /invalid expectContinue/)
await t.completed
})
})
describe('DispatchOptions#maxRedirections', () => {
test('Should throw if maxRedirections option is used', async t => {
t = tspl(t, { plan: 2 })
await t.rejects(request({
method: 'GET',
origin: 'http://somehost.xyz',
maxRedirections: 5
}), /maxRedirections is not supported, use the redirect interceptor/)
await t.rejects(request({
method: 'GET',
origin: 'http://somehost.xyz',
maxRedirections: 1
}), /maxRedirections is not supported, use the redirect interceptor/)
await t.completed
})
test('Should allow maxRedirections: 0 for internal use', async t => {
t = tspl(t, { plan: 2 })
const server = createServer((req, res) => {
t.ok('request received')
res.end('hello world')
})
after(() => {
server.closeAllConnections?.()
server.close()
})
await new Promise((resolve) => server.listen(0, resolve))
const res = await request({
method: 'GET',
origin: `http://localhost:${server.address().port}`,
maxRedirections: 0
})
const body = await res.body.text()
t.strictEqual(body, 'hello world')
})
})
describe('DispatchOptions#reset', () => {
test('Should throw if invalid reset option', async t => {
t = tspl(t, { plan: 1 })
await t.rejects(request({
method: 'GET',
origin: 'http://somehost.xyz',
reset: 0
}), /invalid reset/)
await t.completed
})
test('Should include "connection:close" if reset true', async t => {
t = tspl(t, { plan: 3 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
t.strictEqual('GET', req.method)
t.strictEqual(`localhost:${server.address().port}`, req.headers.host)
t.strictEqual(req.headers.connection, 'close')
res.statusCode = 200
res.end('hello')
})
after(() => {
server.closeAllConnections?.()
server.close()
})
await new Promise((resolve, reject) => {
server.listen(0, (err) => {
if (err != null) reject(err)
else resolve()
})
})
await request({
method: 'GET',
origin: `http://localhost:${server.address().port}`,
reset: true
})
})
test('Should include "connection:keep-alive" if reset false', async t => {
t = tspl(t, { plan: 3 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
t.strictEqual('GET', req.method)
t.strictEqual(`localhost:${server.address().port}`, req.headers.host)
t.strictEqual(req.headers.connection, 'keep-alive')
res.statusCode = 200
res.end('hello')
})
after(() => {
server.closeAllConnections?.()
server.close()
})
await new Promise((resolve, reject) => {
server.listen(0, (err) => {
if (err != null) reject(err)
else resolve()
})
})
await request({
method: 'GET',
origin: `http://localhost:${server.address().port}`,
reset: false
})
})
test('Should react to manual set of "connection:close" header', async t => {
t = tspl(t, { plan: 3 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
t.strictEqual('GET', req.method)
t.strictEqual(`localhost:${server.address().port}`, req.headers.host)
t.strictEqual(req.headers.connection, 'close')
res.statusCode = 200
res.end('hello')
})
after(() => {
server.closeAllConnections?.()
server.close()
})
await new Promise((resolve, reject) => {
server.listen(0, (err) => {
if (err != null) reject(err)
else resolve()
})
})
await request({
method: 'GET',
origin: `http://localhost:${server.address().port}`,
headers: {
connection: 'close'
}
})
})
})
describe('Should include headers from iterable objects', scope => {
test('Should include headers built with Headers global object', { skip: !globalThis.Headers }, async t => {
t = tspl(t, { plan: 3 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
t.strictEqual('GET', req.method)
t.strictEqual(`localhost:${server.address().port}`, req.headers.host)
t.strictEqual(req.headers.hello, 'world')
res.statusCode = 200
res.end('hello')
})
const headers = new globalThis.Headers()
headers.set('hello', 'world')
after(() => {
server.closeAllConnections?.()
server.close()
})
await new Promise((resolve, reject) => {
server.listen(0, (err) => {
if (err != null) reject(err)
else resolve()
})
})
await request({
method: 'GET',
origin: `http://localhost:${server.address().port}`,
reset: true,
headers
})
})
test('Should include headers built with Map', async t => {
t = tspl(t, { plan: 3 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
t.strictEqual('GET', req.method)
t.strictEqual(`localhost:${server.address().port}`, req.headers.host)
t.strictEqual(req.headers.hello, 'world')
res.statusCode = 200
res.end('hello')
})
const headers = new Map()
headers.set('hello', 'world')
after(() => {
server.closeAllConnections?.()
server.close()
})
await new Promise((resolve, reject) => {
server.listen(0, (err) => {
if (err != null) reject(err)
else resolve()
})
})
await request({
method: 'GET',
origin: `http://localhost:${server.address().port}`,
reset: true,
headers
})
})
test('Should include headers built with custom iterable object', async t => {
t = tspl(t, { plan: 3 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
t.strictEqual('GET', req.method)
t.strictEqual(`localhost:${server.address().port}`, req.headers.host)
t.strictEqual(req.headers.hello, 'world')
res.statusCode = 200
res.end('hello')
})
const headers = {
* [Symbol.iterator] () {
yield ['hello', 'world']
}
}
after(() => {
server.closeAllConnections?.()
server.close()
})
await new Promise((resolve, reject) => {
server.listen(0, (err) => {
if (err != null) reject(err)
else resolve()
})
})
await request({
method: 'GET',
origin: `http://localhost:${server.address().port}`,
reset: true,
headers
})
})
test('Should include headers from plain objects with polluted Object.prototype[Symbol.iterator]', async t => {
t = tspl(t, { plan: 3 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
t.strictEqual('GET', req.method)
t.strictEqual(`localhost:${server.address().port}`, req.headers.host)
t.strictEqual(req.headers.hello, 'world')
res.statusCode = 200
res.end('hello')
})
const headers = {
hello: 'world'
}
const originalIterator = Object.prototype[Symbol.iterator]
// eslint-disable-next-line no-extend-native
Object.prototype[Symbol.iterator] = function * () {}
try {
await new Promise((resolve, reject) => {
server.listen(0, (err) => {
if (err != null) reject(err)
else resolve()
})
})
await request({
method: 'GET',
origin: `http://localhost:${server.address().port}`,
reset: true,
headers
})
} finally {
if (originalIterator === undefined) {
delete Object.prototype[Symbol.iterator]
} else {
// eslint-disable-next-line no-extend-native
Object.prototype[Symbol.iterator] = originalIterator
}
server.closeAllConnections?.()
server.close()
}
})
test('Should throw error if headers iterable object does not yield key-value pairs', async t => {
t = tspl(t, { plan: 2 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.end('hello')
})
const headers = {
* [Symbol.iterator] () {
yield 'Bad formatted header'
}
}
after(() => {
server.closeAllConnections?.()
server.close()
})
await new Promise((resolve, reject) => {
server.listen(0, (err) => {
if (err != null) reject(err)
else resolve()
})
})
await request({
method: 'GET',
origin: `http://localhost:${server.address().port}`,
reset: true,
headers
}).catch((err) => {
t.ok(err instanceof errors.InvalidArgumentError)
t.strictEqual(err.message, 'headers must be in key-value pair format')
})
})
})
test('request should include statusText in response', async t => {
t = tspl(t, { plan: 2 })
const server = createServer((req, res) => {
res.writeHead(200, 'Custom Status Text', { 'content-type': 'text/plain' })
res.end('hello')
})
after(() => {
server.closeAllConnections?.()
server.close()
})
await new Promise((resolve) => server.listen(0, resolve))
const { statusText, body } = await request({
method: 'GET',
origin: `http://localhost:${server.address().port}`,
path: '/'
})
t.strictEqual(statusText, 'Custom Status Text')
await body.dump()
t.ok('request completed')
})
================================================
FILE: test/retry-agent.js
================================================
'use strict'
const { tspl } = require('@matteo.collina/tspl')
const { test, after } = require('node:test')
const { createServer } = require('node:http')
const { once } = require('node:events')
const { RetryAgent, Client } = require('..')
test('Should retry status code', async t => {
t = tspl(t, { plan: 2 })
let counter = 0
const server = createServer({ joinDuplicateHeaders: true })
const opts = {
maxRetries: 5,
timeout: 1,
timeoutFactor: 1
}
server.on('request', (req, res) => {
switch (counter++) {
case 0:
req.destroy()
return
case 1:
res.writeHead(500)
res.end('failed')
return
case 2:
res.writeHead(200)
res.end('hello world!')
return
default:
t.fail()
}
})
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
const agent = new RetryAgent(client, opts)
after(async () => {
await agent.close()
server.close()
await once(server, 'close')
})
agent.request({
method: 'GET',
path: '/',
headers: {
'content-type': 'application/json'
}
}).then((res) => {
t.equal(res.statusCode, 200)
res.body.setEncoding('utf8')
let chunks = ''
res.body.on('data', chunk => { chunks += chunk })
res.body.on('end', () => {
t.equal(chunks, 'hello world!')
})
})
})
await t.completed
})
================================================
FILE: test/retry-handler.js
================================================
'use strict'
const { tspl } = require('@matteo.collina/tspl')
const { test, after } = require('node:test')
const { createServer } = require('node:http')
const { once } = require('node:events')
const { Readable } = require('node:stream')
const { RetryHandler, Client } = require('..')
const { RequestHandler } = require('../lib/api/api-request')
test('Should retry status code', async t => {
t = tspl(t, { plan: 3 })
let counter = 0
const chunks = []
const server = createServer({ joinDuplicateHeaders: true })
const dispatchOptions = {
retryOptions: {
retry: (err, { state, opts }, done) => {
++counter
if (
err.statusCode === 500 ||
err.message.includes('other side closed')
) {
setTimeout(done, 500)
return
}
return done(err)
}
},
method: 'GET',
path: '/',
headers: {
'content-type': 'application/json'
}
}
server.on('request', (req, res) => {
switch (counter) {
case 0:
req.destroy()
return
case 1:
res.writeHead(500)
res.end('failed')
return
case 2:
res.writeHead(200)
res.end('hello world!')
return
default:
t.fail()
}
})
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
const handler = new RetryHandler(dispatchOptions, {
dispatch: client.dispatch.bind(client),
handler: {
onConnect () {
t.ok(true, 'pass')
},
onHeaders (status, _rawHeaders, resume, _statusMessage) {
t.strictEqual(status, 200)
return true
},
onData (chunk) {
chunks.push(chunk)
return true
},
onComplete () {
t.strictEqual(Buffer.concat(chunks).toString('utf-8'), 'hello world!')
t.strictEqual(counter, 2)
},
onError () {
t.fail()
}
}
})
after(async () => {
await client.close()
server.close()
await once(server, 'close')
})
client.dispatch(
{
method: 'GET',
path: '/',
headers: {
'content-type': 'application/json'
}
},
handler
)
})
await t.completed
})
test('Should account for network and response errors', async t => {
t = tspl(t, { plan: 3 })
let counter = 0
const chunks = []
const server = createServer({ joinDuplicateHeaders: true })
const dispatchOptions = {
retryOptions: {
retry: (err, { state, opts }, done) => {
counter = state.counter
if (
err.statusCode === 500 ||
err.message.includes('other side closed')
) {
setTimeout(done, 500)
return
}
return done(err)
}
},
method: 'GET',
path: '/',
headers: {
'content-type': 'application/json'
}
}
server.on('request', (req, res) => {
switch (counter) {
case 0:
req.destroy()
return
case 1:
res.writeHead(500)
res.end('failed')
return
case 2:
res.writeHead(200)
res.end('hello world!')
return
default:
t.fail()
}
})
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
const handler = new RetryHandler(dispatchOptions, {
dispatch: client.dispatch.bind(client),
handler: {
onConnect () {
t.ok(true, 'pass')
},
onHeaders (status, _rawHeaders, resume, _statusMessage) {
t.strictEqual(status, 200)
return true
},
onData (chunk) {
chunks.push(chunk)
return true
},
onComplete () {
t.strictEqual(Buffer.concat(chunks).toString('utf-8'), 'hello world!')
t.strictEqual(counter, 2)
},
onError () {
t.fail()
}
}
})
after(async () => {
await client.close()
server.close()
await once(server, 'close')
})
client.dispatch(
{
method: 'GET',
path: '/',
headers: {
'content-type': 'application/json'
}
},
handler
)
})
await t.completed
})
test('Issue #3288 - request with body (asynciterable)', async t => {
t = tspl(t, { plan: 4 })
const server = createServer({ joinDuplicateHeaders: true })
const dispatchOptions = {
method: 'POST',
path: '/',
headers: {
'content-type': 'application/json'
},
body: (function * () {
yield 'hello'
yield 'world'
})()
}
server.on('request', (req, res) => {
res.writeHead(500, {
'content-type': 'application/json'
})
res.end('{"message": "failed"}')
})
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
const handler = new RetryHandler(dispatchOptions, {
dispatch: client.dispatch.bind(client),
handler: {
onConnect () {
t.ok(true, 'pass')
},
onHeaders (status, _rawHeaders, resume, _statusMessage) {
return true
},
onData (chunk) {
return true
},
onComplete () {
t.fail()
},
onError (err) {
t.equal(err.message, 'Request failed')
t.equal(err.statusCode, 500)
t.equal(err.data.count, 1)
}
}
})
after(async () => {
await client.close()
server.close()
await once(server, 'close')
})
client.dispatch(
dispatchOptions,
handler
)
})
await t.completed
})
test('Should use retry-after header for retries', async t => {
t = tspl(t, { plan: 3 })
let counter = 0
const chunks = []
const server = createServer({ joinDuplicateHeaders: true })
let checkpoint
const dispatchOptions = {
method: 'PUT',
path: '/',
headers: {
'content-type': 'application/json'
}
}
server.on('request', (req, res) => {
switch (counter) {
case 0:
res.writeHead(429, {
'retry-after': 1
})
res.end('rate limit')
checkpoint = Date.now()
counter++
return
case 1:
res.writeHead(200)
res.end('hello world!')
t.ok(Date.now() - checkpoint >= 500)
counter++
return
default:
t.fail('unexpected request')
}
})
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
const handler = new RetryHandler(dispatchOptions, {
dispatch: client.dispatch.bind(client),
handler: {
onConnect () {
t.ok(true, 'pass')
},
onHeaders (status, _rawHeaders, resume, _statusMessage) {
t.strictEqual(status, 200)
return true
},
onData (chunk) {
chunks.push(chunk)
return true
},
onComplete () {
t.strictEqual(Buffer.concat(chunks).toString('utf-8'), 'hello world!')
},
onError (err) {
t.ifError(err)
}
}
})
after(async () => {
await client.close()
server.close()
await once(server, 'close')
})
client.dispatch(
{
method: 'PUT',
path: '/',
headers: {
'content-type': 'application/json'
}
},
handler
)
})
await t.completed
})
test('Should use retry-after header for retries (date)', async t => {
t = tspl(t, { plan: 3 })
let counter = 0
const chunks = []
const server = createServer({ joinDuplicateHeaders: true })
let checkpoint
const dispatchOptions = {
method: 'PUT',
path: '/',
headers: {
'content-type': 'application/json'
}
}
server.on('request', (req, res) => {
switch (counter) {
case 0:
checkpoint = Date.now()
res.writeHead(429, {
'retry-after': new Date(
checkpoint + 2000
).toUTCString()
})
res.end('rate limit')
counter++
return
case 1:
res.writeHead(200)
res.end('hello world!')
t.ok(Date.now() - checkpoint >= 1000)
counter++
return
default:
t.fail('unexpected request')
}
})
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
const handler = new RetryHandler(dispatchOptions, {
dispatch: client.dispatch.bind(client),
handler: {
onConnect () {
t.ok(true, 'pass')
},
onHeaders (status, _rawHeaders, resume, _statusMessage) {
t.strictEqual(status, 200)
return true
},
onData (chunk) {
chunks.push(chunk)
return true
},
onComplete () {
t.strictEqual(Buffer.concat(chunks).toString('utf-8'), 'hello world!')
},
onError (err) {
t.ifError(err)
}
}
})
after(async () => {
await client.close()
server.close()
await once(server, 'close')
})
client.dispatch(
{
method: 'PUT',
path: '/',
headers: {
'content-type': 'application/json'
}
},
handler
)
})
await t.completed
})
test('Should retry with defaults', async t => {
t = tspl(t, { plan: 3 })
let counter = 0
const chunks = []
const server = createServer({ joinDuplicateHeaders: true })
const dispatchOptions = {
method: 'GET',
path: '/',
headers: {
'content-type': 'application/json'
}
}
server.on('request', (req, res) => {
switch (counter) {
case 0:
req.destroy()
counter++
return
case 1:
res.writeHead(500)
res.end('failed')
counter++
return
case 2:
res.writeHead(200)
res.end('hello world!')
counter++
return
default:
t.fail()
}
})
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
const handler = new RetryHandler(dispatchOptions, {
dispatch: client.dispatch.bind(client),
handler: {
onConnect () {
t.ok(true, 'pass')
},
onHeaders (status, _rawHeaders, resume, _statusMessage) {
t.strictEqual(status, 200)
return true
},
onData (chunk) {
chunks.push(chunk)
return true
},
onComplete () {
t.strictEqual(Buffer.concat(chunks).toString('utf-8'), 'hello world!')
},
onError (err) {
t.ifError(err)
}
}
})
after(async () => {
await client.close()
server.close()
await once(server, 'close')
})
client.dispatch(
{
method: 'GET',
path: '/',
headers: {
'content-type': 'application/json'
}
},
handler
)
})
await t.completed
})
test('Should handle 206 partial content', async t => {
t = tspl(t, { plan: 6 })
const chunks = []
let counter = 0
// Took from: https://github.com/nxtedition/nxt-lib/blob/4b001ebc2f22cf735a398f35ff800dd553fe5933/test/undici/retry.js#L47
let x = 0
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
if (x === 0) {
t.ok(true, 'pass')
res.setHeader('etag', 'asd')
res.write('abc')
setTimeout(() => {
res.destroy()
}, 1e2)
} else if (x === 1) {
t.deepStrictEqual(req.headers.range, 'bytes=3-')
res.setHeader('content-range', 'bytes 3-6/6')
res.setHeader('etag', 'asd')
res.statusCode = 206
res.end('def')
}
x++
})
const dispatchOptions = {
retryOptions: {
retry: function (err, _, done) {
counter++
if (err.code && err.code === 'UND_ERR_DESTROYED') {
return done(false)
}
if (err.statusCode === 206) return done(err)
setTimeout(done, 800)
}
},
method: 'GET',
path: '/',
headers: {
'content-type': 'application/json'
}
}
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
const handler = new RetryHandler(dispatchOptions, {
dispatch: (...args) => {
return client.dispatch(...args)
},
handler: {
onConnect () {
t.ok(true, 'pass')
},
onHeaders (status, _rawHeaders, _resume, _statusMessage) {
t.strictEqual(status, 200)
return true
},
onData (chunk) {
chunks.push(chunk)
return true
},
onComplete () {
t.strictEqual(Buffer.concat(chunks).toString('utf-8'), 'abcdef')
t.strictEqual(counter, 1)
},
onError () {
t.fail()
}
}
})
client.dispatch(
{
method: 'GET',
path: '/',
headers: {
'content-type': 'application/json'
}
},
handler
)
after(async () => {
await client.close()
server.close()
await once(server, 'close')
})
})
await t.completed
})
test('Should handle 206 partial content - bad-etag', async t => {
t = tspl(t, { plan: 7 })
const chunks = []
// Took from: https://github.com/nxtedition/nxt-lib/blob/4b001ebc2f22cf735a398f35ff800dd553fe5933/test/undici/retry.js#L47
let x = 0
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
if (x === 0) {
t.ok(true, 'pass')
res.setHeader('etag', 'asd')
res.write('abc')
setTimeout(() => {
res.destroy()
}, 1e2)
} else if (x === 1) {
t.deepStrictEqual(req.headers.range, 'bytes=3-')
res.setHeader('content-range', 'bytes 3-6/6')
res.setHeader('etag', 'erwsd')
res.statusCode = 206
res.end('def')
}
x++
})
const dispatchOptions = {
method: 'GET',
path: '/',
headers: {
'content-type': 'application/json'
}
}
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
const handler = new RetryHandler(
dispatchOptions,
{
dispatch: (...args) => {
return client.dispatch(...args)
},
handler: {
onConnect () {
t.ok(true, 'pass')
},
onHeaders (_status, _rawHeaders, _resume, _statusMessage) {
return true
},
onData (chunk) {
chunks.push(chunk)
return true
},
onComplete () {
t.ifError('should not complete')
},
onError (err) {
t.strictEqual(Buffer.concat(chunks).toString('utf-8'), 'abc')
t.strictEqual(err.code, 'UND_ERR_REQ_RETRY')
t.strictEqual(err.message, 'ETag mismatch')
t.deepEqual(err.data, { count: 2 })
}
}
}
)
client.dispatch(
{
method: 'GET',
path: '/',
headers: {
'content-type': 'application/json'
}
},
handler
)
after(async () => {
await client.close()
server.close()
await once(server, 'close')
})
})
await t.completed
})
test('retrying a request with a body', async t => {
let counter = 0
const server = createServer({ joinDuplicateHeaders: true })
const dispatchOptions = {
retryOptions: {
retry: (err, { state, opts }, done) => {
counter++
if (
err.statusCode === 500 ||
err.message.includes('other side closed')
) {
setTimeout(done, 500)
return
}
return done(err)
}
},
method: 'POST',
path: '/',
headers: {
'content-type': 'application/json'
},
body: JSON.stringify({ hello: 'world' })
}
t = tspl(t, { plan: 1 })
server.on('request', (req, res) => {
switch (counter) {
case 0:
req.destroy()
return
case 1:
res.writeHead(500)
res.end('failed')
return
case 2:
res.writeHead(200)
res.end('hello world!')
return
default:
t.fail()
}
})
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
const handler = new RetryHandler(dispatchOptions, {
dispatch: client.dispatch.bind(client),
handler: new RequestHandler(dispatchOptions, (err, data) => {
t.ifError(err)
})
})
after(async () => {
await client.close()
server.close()
await once(server, 'close')
})
client.dispatch(
{
method: 'POST',
path: '/',
headers: {
'content-type': 'application/json'
},
body: JSON.stringify({ hello: 'world' })
},
handler
)
})
await t.completed
})
test('retrying a request with a body (stream)', async t => {
let counter = 0
const server = createServer({ joinDuplicateHeaders: true })
const dispatchOptions = {
retryOptions: {
retry: (err, { state, opts }, done) => {
counter++
if (
err.statusCode === 500 ||
err.message.includes('other side closed')
) {
setTimeout(done, 500)
return
}
return done(err)
}
},
method: 'POST',
path: '/',
headers: {
'content-type': 'application/json'
},
body: Readable.from(Buffer.from(JSON.stringify({ hello: 'world' })))
}
t = tspl(t, { plan: 3 })
server.on('request', (req, res) => {
switch (counter) {
case 0:
res.writeHead(500)
res.end('failed')
return
default:
t.fail()
}
})
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
const handler = new RetryHandler(dispatchOptions, {
dispatch: client.dispatch.bind(client),
handler: new RequestHandler(dispatchOptions, (err, data) => {
t.equal(err.statusCode, 500)
t.equal(err.data.count, 1)
t.equal(err.code, 'UND_ERR_REQ_RETRY')
})
})
after(async () => {
await client.close()
server.close()
await once(server, 'close')
})
client.dispatch(
dispatchOptions,
handler
)
})
await t.completed
})
test('retrying a request with a body (buffer)', async t => {
let counter = 0
const server = createServer({ joinDuplicateHeaders: true })
const dispatchOptions = {
retryOptions: {
retry: (err, { state, opts }, done) => {
counter++
if (
err.statusCode === 500 ||
err.message.includes('other side closed')
) {
setTimeout(done, 500)
return
}
return done(err)
}
},
method: 'POST',
path: '/',
headers: {
'content-type': 'application/json'
},
body: Buffer.from(JSON.stringify({ hello: 'world' }))
}
t = tspl(t, { plan: 1 })
server.on('request', (req, res) => {
switch (counter) {
case 0:
req.destroy()
return
case 1:
res.writeHead(500)
res.end('failed')
return
case 2:
res.writeHead(200)
res.end('hello world!')
return
default:
t.fail()
}
})
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
const handler = new RetryHandler(dispatchOptions, {
dispatch: client.dispatch.bind(client),
handler: new RequestHandler(dispatchOptions, (err, data) => {
t.ifError(err)
})
})
after(async () => {
await client.close()
server.close()
await once(server, 'close')
})
client.dispatch(
dispatchOptions,
handler
)
})
await t.completed
})
test('should not error if request is not meant to be retried', async t => {
t = tspl(t, { plan: 3 })
const server = createServer({ joinDuplicateHeaders: true })
server.on('request', (req, res) => {
res.writeHead(400)
res.end('Bad request')
})
const dispatchOptions = {
retryOptions: {
method: 'GET',
path: '/',
headers: {
'content-type': 'application/json'
}
}
}
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
const chunks = []
const handler = new RetryHandler(dispatchOptions, {
dispatch: client.dispatch.bind(client),
handler: {
onConnect () {
t.ok(true, 'pass')
},
onHeaders (status, _rawHeaders, resume, _statusMessage) {
t.strictEqual(status, 400)
return true
},
onData (chunk) {
chunks.push(chunk)
return true
},
onComplete () {
t.strictEqual(Buffer.concat(chunks).toString('utf-8'), 'Bad request')
},
onError (err) {
t.fail(err)
}
}
})
after(async () => {
await client.close()
server.close()
await once(server, 'close')
})
client.dispatch(
{
method: 'GET',
path: '/',
headers: {
'content-type': 'application/json'
}
},
handler
)
})
await t.completed
})
test('Should be able to properly pass the minTimeout to the RetryContext when constructing a RetryCallback function', async t => {
t = tspl(t, { plan: 2 })
let counter = 0
const server = createServer({ joinDuplicateHeaders: true })
server.on('request', (req, res) => {
switch (counter) {
case 0:
res.writeHead(500)
res.end('failed')
return
case 1:
res.writeHead(200)
res.end('hello world!')
return
default:
t.fail()
}
})
const dispatchOptions = {
retryOptions: {
retry: (err, { state, opts }, done) => {
counter++
t.strictEqual(opts.retryOptions.minTimeout, 100)
if (err.statusCode === 500) {
return done()
}
return done(err)
},
minTimeout: 100
},
method: 'GET',
path: '/',
headers: {
'content-type': 'application/json'
}
}
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
const handler = new RetryHandler(dispatchOptions, {
dispatch: client.dispatch.bind(client),
handler: new RequestHandler(dispatchOptions, (err, data) => {
t.ifError(err)
})
})
after(async () => {
await client.close()
server.close()
await once(server, 'close')
})
client.dispatch(
{
method: 'GET',
path: '/',
headers: {
'content-type': 'application/json'
}
},
handler
)
})
await t.completed
})
test('Issue#2986 - Handle custom 206', async t => {
t = tspl(t, { plan: 6 })
const chunks = []
let counter = 0
// Took from: https://github.com/nxtedition/nxt-lib/blob/4b001ebc2f22cf735a398f35ff800dd553fe5933/test/undici/retry.js#L47
let x = 0
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
if (x === 0) {
t.deepStrictEqual(req.headers.range, 'bytes=0-3')
res.setHeader('etag', 'asd')
res.write('abc')
setTimeout(() => {
res.destroy()
}, 1e2)
} else if (x === 1) {
t.deepStrictEqual(req.headers.range, 'bytes=3-')
res.setHeader('content-range', 'bytes 3-6/6')
res.setHeader('etag', 'asd')
res.statusCode = 206
res.end('def')
}
x++
})
const dispatchOptions = {
retryOptions: {
retry: function (err, _, done) {
counter++
if (err.code && err.code === 'UND_ERR_DESTROYED') {
return done(false)
}
if (err.statusCode === 206) return done(err)
setTimeout(done, 800)
}
},
method: 'GET',
path: '/',
headers: {
'content-type': 'application/json'
}
}
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
const handler = new RetryHandler(dispatchOptions, {
dispatch: (...args) => {
return client.dispatch(...args)
},
handler: {
onConnect () {
t.ok(true, 'pass')
},
onHeaders (status, _rawHeaders, resume, _statusMessage) {
t.strictEqual(status, 200)
return true
},
onData (chunk) {
chunks.push(chunk)
return true
},
onComplete () {
t.strictEqual(Buffer.concat(chunks).toString('utf-8'), 'abcdef')
t.strictEqual(counter, 1)
},
onError () {
t.fail()
}
}
})
client.dispatch(
{
method: 'GET',
path: '/',
headers: {
'content-type': 'application/json',
Range: 'bytes=0-3'
}
},
handler
)
after(async () => {
await client.close()
server.close()
await once(server, 'close')
})
})
await t.completed
})
test('Issue#3128 - Support if-match', async t => {
t = tspl(t, { plan: 7 })
const chunks = []
let counter = 0
// Took from: https://github.com/nxtedition/nxt-lib/blob/4b001ebc2f22cf735a398f35ff800dd553fe5933/test/undici/retry.js#L47
let x = 0
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
if (x === 0) {
t.deepStrictEqual(req.headers.range, 'bytes=0-3')
res.setHeader('etag', 'asd')
res.write('abc')
setTimeout(() => {
res.destroy()
}, 1e2)
} else if (x === 1) {
t.deepStrictEqual(req.headers.range, 'bytes=3-')
t.deepStrictEqual(req.headers['if-match'], 'asd')
res.setHeader('content-range', 'bytes 3-6/6')
res.setHeader('etag', 'asd')
res.statusCode = 206
res.end('def')
}
x++
})
const dispatchOptions = {
retryOptions: {
retry: function (err, _, done) {
counter++
if (err.code && err.code === 'UND_ERR_DESTROYED') {
return done(false)
}
if (err.statusCode === 206) return done(err)
setTimeout(done, 800)
}
},
method: 'GET',
path: '/',
headers: {
'content-type': 'application/json'
}
}
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
const handler = new RetryHandler(dispatchOptions, {
dispatch: (...args) => {
return client.dispatch(...args)
},
handler: {
onConnect () {
t.ok(true, 'pass')
},
onHeaders (status, _rawHeaders, resume, _statusMessage) {
t.strictEqual(status, 200)
return true
},
onData (chunk) {
chunks.push(chunk)
return true
},
onComplete () {
t.strictEqual(Buffer.concat(chunks).toString('utf-8'), 'abcdef')
t.strictEqual(counter, 1)
},
onError () {
t.fail()
}
}
})
client.dispatch(
{
method: 'GET',
path: '/',
headers: {
'content-type': 'application/json',
Range: 'bytes=0-3'
}
},
handler
)
after(async () => {
await client.close()
server.close()
await once(server, 'close')
})
})
await t.completed
})
test('Issue#3128 - Should ignore weak etags', async t => {
t = tspl(t, { plan: 7 })
const chunks = []
let counter = 0
// Took from: https://github.com/nxtedition/nxt-lib/blob/4b001ebc2f22cf735a398f35ff800dd553fe5933/test/undici/retry.js#L47
let x = 0
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
if (x === 0) {
t.deepStrictEqual(req.headers.range, 'bytes=0-3')
res.setHeader('etag', 'W/asd')
res.write('abc')
setTimeout(() => {
res.destroy()
}, 1e2)
} else if (x === 1) {
t.deepStrictEqual(req.headers.range, 'bytes=3-')
t.equal(req.headers['if-match'], undefined)
res.setHeader('content-range', 'bytes 3-6/6')
res.setHeader('etag', 'W/asd')
res.statusCode = 206
res.end('def')
}
x++
})
const dispatchOptions = {
retryOptions: {
retry: function (err, _, done) {
counter++
if (err.code && err.code === 'UND_ERR_DESTROYED') {
return done(false)
}
if (err.statusCode === 206) return done(err)
setTimeout(done, 800)
}
},
method: 'GET',
path: '/',
headers: {
'content-type': 'application/json'
}
}
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
const handler = new RetryHandler(dispatchOptions, {
dispatch: (...args) => {
return client.dispatch(...args)
},
handler: {
onConnect () {
t.ok(true, 'pass')
},
onHeaders (status, _rawHeaders, resume, _statusMessage) {
t.strictEqual(status, 200)
return true
},
onData (chunk) {
chunks.push(chunk)
return true
},
onComplete () {
t.strictEqual(Buffer.concat(chunks).toString('utf-8'), 'abcdef')
t.strictEqual(counter, 1)
},
onError () {
t.fail()
}
}
})
client.dispatch(
{
method: 'GET',
path: '/',
headers: {
'content-type': 'application/json',
Range: 'bytes=0-3'
}
},
handler
)
after(async () => {
await client.close()
server.close()
await once(server, 'close')
})
})
await t.completed
})
test('Weak etags are ignored on range-requests', async t => {
t = tspl(t, { plan: 7 })
const chunks = []
let counter = 0
// Took from: https://github.com/nxtedition/nxt-lib/blob/4b001ebc2f22cf735a398f35ff800dd553fe5933/test/undici/retry.js#L47
let x = 0
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
if (x === 0) {
t.deepStrictEqual(req.headers.range, 'bytes=0-3')
res.setHeader('etag', 'W/asd')
res.write('abc')
setTimeout(() => {
res.destroy()
}, 1e2)
} else if (x === 1) {
t.deepStrictEqual(req.headers.range, 'bytes=3-')
t.equal(req.headers['if-match'], undefined)
res.setHeader('content-range', 'bytes 3-6/6')
res.setHeader('etag', 'W/efg')
res.statusCode = 206
res.end('def')
}
x++
})
const dispatchOptions = {
retryOptions: {
retry: function (err, _, done) {
counter++
if (err.code && err.code === 'UND_ERR_DESTROYED') {
return done(false)
}
if (err.statusCode === 206) return done(err)
setTimeout(done, 800)
}
},
method: 'GET',
path: '/',
headers: {
'content-type': 'application/json'
}
}
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
const handler = new RetryHandler(dispatchOptions, {
dispatch: (...args) => {
return client.dispatch(...args)
},
handler: {
onConnect () {
t.ok(true, 'pass')
},
onHeaders (status, _rawHeaders, resume, _statusMessage) {
t.strictEqual(status, 200)
return true
},
onData (chunk) {
chunks.push(chunk)
return true
},
onComplete () {
t.strictEqual(Buffer.concat(chunks).toString('utf-8'), 'abcdef')
t.strictEqual(counter, 1)
},
onError () {
t.fail()
}
}
})
client.dispatch(
{
method: 'GET',
path: '/',
headers: {
'content-type': 'application/json',
Range: 'bytes=0-3'
}
},
handler
)
after(async () => {
await client.close()
server.close()
await once(server, 'close')
})
})
await t.completed
})
test('Should throw RequestRetryError when Content-Range mismatch', async t => {
t = tspl(t, { plan: 8 })
const chunks = []
// Took from: https://github.com/nxtedition/nxt-lib/blob/4b001ebc2f22cf735a398f35ff800dd553fe5933/test/undici/retry.js#L47
let x = 0
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
if (x === 0) {
t.ok(true, 'pass')
res.setHeader('etag', 'asd')
res.write('abc')
setTimeout(() => {
res.destroy()
}, 1e2)
} else if (x === 1) {
t.deepStrictEqual(req.headers.range, 'bytes=3-')
res.setHeader('content-range', 'bytes bad') // intentionally bad to trigger error
res.setHeader('etag', 'asd')
res.statusCode = 206
res.end('def')
}
x++
})
const dispatchOptions = {
retryOptions: {
retry: function (err, _, done) {
if (err.code && err.code === 'UND_ERR_DESTROYED') {
return done(false)
}
if (err.statusCode === 206) return done(err)
setTimeout(done, 800)
}
},
method: 'GET',
path: '/',
headers: {
'content-type': 'application/json'
}
}
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
const handler = new RetryHandler(dispatchOptions, {
dispatch: (...args) => {
return client.dispatch(...args)
},
handler: {
onConnect () {
t.ok(true, 'pass')
},
onHeaders (status, _rawHeaders, _resume, _statusMessage) {
t.strictEqual(status, 200)
return true
},
onData (chunk) {
chunks.push(chunk)
return true
},
onComplete () {
t.ifError('should not complete')
},
onError (err) {
t.strictEqual(Buffer.concat(chunks).toString('utf-8'), 'abc')
t.strictEqual(err.code, 'UND_ERR_REQ_RETRY')
t.strictEqual(err.message, 'Content-Range mismatch')
t.deepEqual(err.data, { count: 2 })
}
}
})
client.dispatch(
{
method: 'GET',
path: '/',
headers: {
'content-type': 'application/json'
}
},
handler
)
after(async () => {
await client.close()
server.close()
await once(server, 'close')
})
})
await t.completed
})
test('Should use retry-after header for retries (date) but date format is wrong', async t => {
t = tspl(t, { plan: 3 })
let counter = 0
const chunks = []
const server = createServer({ joinDuplicateHeaders: true })
let checkpoint
const dispatchOptions = {
method: 'PUT',
path: '/',
headers: {
'content-type': 'application/json'
},
retryOptions: {
minTimeout: 1000
}
}
const minRetryDelay = dispatchOptions.retryOptions.minTimeout
server.on('request', (req, res) => {
switch (counter) {
case 0: {
checkpoint = process.hrtime.bigint()
res.writeHead(429, {
'retry-after': 'this is not a date'
})
res.end('rate limit')
counter++
return
}
case 1: {
res.writeHead(200)
res.end('hello world!')
const elapsedMs = Number(process.hrtime.bigint() - checkpoint) / 1e6
t.ok(elapsedMs >= minRetryDelay - 100)
counter++
return
}
default:
t.fail('unexpected request')
}
})
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
const handler = new RetryHandler(dispatchOptions, {
dispatch: client.dispatch.bind(client),
handler: {
onConnect () {
t.ok(true, 'pass')
},
onHeaders (status, _rawHeaders, resume, _statusMessage) {
t.strictEqual(status, 200)
return true
},
onData (chunk) {
chunks.push(chunk)
return true
},
onComplete () {
t.strictEqual(Buffer.concat(chunks).toString('utf-8'), 'hello world!')
},
onError (err) {
t.ifError(err)
}
}
})
after(async () => {
await client.close()
server.close()
await once(server, 'close')
})
client.dispatch(
{
method: 'PUT',
path: '/',
headers: {
'content-type': 'application/json'
}
},
handler
)
})
await t.completed
})
================================================
FILE: test/retry-handler2.js
================================================
'use strict'
const { tspl } = require('@matteo.collina/tspl')
const { test, after } = require('node:test')
const { createServer } = require('node:http')
const { once } = require('node:events')
const { Readable } = require('node:stream')
const { RetryHandler, Client, RetryAgent } = require('..')
const { RequestHandler } = require('../lib/api/api-request')
test('Reuses socket on retry instead of closing it', async t => {
t = tspl(t, { plan: 5 })
let counter = 0
let socketPort
const server = createServer({ joinDuplicateHeaders: true })
server.on('request', (req, res) => {
counter++
res.writeHead(500)
res.end('internal err')
if (!socketPort) {
socketPort = req.socket.remotePort
}
t.strictEqual(socketPort, req.socket.remotePort)
})
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
const retryAgent = new RetryAgent(client, {
throwOnError: false,
maxRetries: 2
})
retryAgent.request({
method: 'GET',
path: '/',
headers: {
'content-type': 'application/json'
}
}).then(res => {
t.strictEqual(res.statusCode, 500)
t.strictEqual(counter, 3)
})
after(async () => {
await client.close()
server.close()
await once(server, 'close')
})
})
await t.completed
})
test('throws an error on network error', async t => {
t = tspl(t, { plan: 1 })
const server = createServer({ joinDuplicateHeaders: true })
server.on('request', (req, res) => {
res.destroy()
})
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
const retryAgent = new RetryAgent(client, {
throwOnError: false,
maxRetries: 2
})
retryAgent.request({
method: 'GET',
path: '/',
headers: {
'content-type': 'application/json'
}
}).catch(err => {
t.strictEqual(err.code, 'UND_ERR_SOCKET')
})
after(async () => {
await client.close()
server.close()
await once(server, 'close')
})
})
await t.completed
})
test('Show pass status code errors when not eligible for retry, as normal response instead of throwing error', async t => {
t = tspl(t, { plan: 3 })
let counter = 0
const server = createServer({ joinDuplicateHeaders: true })
server.on('request', (req, res) => {
counter++
res.writeHead(500)
res.end('internal err')
})
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
const retryAgent = new RetryAgent(client, {
throwOnError: false,
maxRetries: 2
})
retryAgent.request({
method: 'GET',
path: '/',
headers: {
'content-type': 'application/json'
}
}).then(res => {
t.strictEqual(res.statusCode, 500)
t.strictEqual(counter, 3)
res.body.text().then(text => {
t.strictEqual(text, 'internal err')
})
})
after(async () => {
await client.close()
server.close()
await once(server, 'close')
})
})
await t.completed
})
test('Should retry status code without throwing an error | throwOnError: false', async t => {
t = tspl(t, { plan: 6 })
let counter = 0
const chunks = []
const server = createServer({ joinDuplicateHeaders: true })
const dispatchOptions = {
retryOptions: {
throwOnError: false,
retry: (err, { state, opts }, done) => {
++counter
if (
err.statusCode === 500 ||
err.message.includes('other side closed')
) {
setTimeout(done, 500)
return
}
return done(err)
}
},
method: 'GET',
path: '/',
headers: {
'content-type': 'application/json'
}
}
server.on('request', (req, res) => {
switch (counter) {
case 0:
req.destroy()
return
case 1:
res.writeHead(500)
res.end('failed')
return
case 2:
res.writeHead(200)
res.end('hello world!')
return
default:
t.fail()
}
})
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
const handler = new RetryHandler(dispatchOptions, {
dispatch: client.dispatch.bind(client),
handler: {
onConnect () {
t.ok(true, 'pass')
},
onHeaders (status, _rawHeaders, resume, _statusMessage) {
t.strictEqual(status, 200)
return true
},
onData (chunk) {
chunks.push(chunk)
return true
},
onComplete () {
t.strictEqual(Buffer.concat(chunks).toString('utf-8'), 'hello world!')
t.strictEqual(counter, 2)
},
onError () {
t.fail()
}
}
})
after(async () => {
await client.close()
server.close()
await once(server, 'close')
})
client.dispatch(
{
method: 'GET',
path: '/',
headers: {
'content-type': 'application/json'
}
},
handler
)
})
await t.completed
})
test('Should account for network and response errors | throwOnError: false', async t => {
t = tspl(t, { plan: 6 })
let counter = 0
const chunks = []
const server = createServer({ joinDuplicateHeaders: true })
const dispatchOptions = {
retryOptions: {
throwOnError: false,
retry: (err, { state, opts }, done) => {
counter = state.counter
if (
err.statusCode === 500 ||
err.message.includes('other side closed')
) {
setTimeout(done, 500)
return
}
return done(err)
}
},
method: 'GET',
path: '/',
headers: {
'content-type': 'application/json'
}
}
server.on('request', (req, res) => {
switch (counter) {
case 0:
req.destroy()
return
case 1:
res.writeHead(500)
res.end('failed')
return
case 2:
res.writeHead(200)
res.end('hello world!')
return
default:
t.fail()
}
})
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
const handler = new RetryHandler(dispatchOptions, {
dispatch: client.dispatch.bind(client),
handler: {
onConnect () {
t.ok(true, 'pass')
},
onHeaders (status, _rawHeaders, resume, _statusMessage) {
t.strictEqual(status, 200)
return true
},
onData (chunk) {
chunks.push(chunk)
return true
},
onComplete () {
t.strictEqual(Buffer.concat(chunks).toString('utf-8'), 'hello world!')
t.strictEqual(counter, 2)
},
onError () {
t.fail()
}
}
})
after(async () => {
await client.close()
server.close()
await once(server, 'close')
})
client.dispatch(
{
method: 'GET',
path: '/',
headers: {
'content-type': 'application/json'
}
},
handler
)
})
await t.completed
})
test('Issue #3288 - request with body (asynciterable) should fail, without throwing an error', async t => {
t = tspl(t, { plan: 3 })
const server = createServer({ joinDuplicateHeaders: true })
const dispatchOptions = {
retryOptions: {
throwOnError: false
},
method: 'POST',
path: '/',
headers: {
'content-type': 'application/json'
},
body: (function * () {
yield 'hello'
yield 'world'
})()
}
server.on('request', (req, res) => {
res.writeHead(500, {
'content-type': 'application/json'
})
res.end('{"message": "failed"}')
})
const chunks = []
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
const handler = new RetryHandler(dispatchOptions, {
dispatch: client.dispatch.bind(client),
handler: {
onConnect () {
t.ok(true, 'pass')
},
onHeaders (status, _rawHeaders, resume, _statusMessage) {
t.ok(true, 'pass')
},
onData (chunk) {
chunks.push(chunk)
return true
},
onComplete () {
const data = Buffer.concat(chunks).toString('utf-8')
t.strictEqual(data, '{"message": "failed"}')
},
onError () {
t.fail()
}
}
})
after(async () => {
await client.close()
server.close()
await once(server, 'close')
})
client.dispatch(
dispatchOptions,
handler
)
})
await t.completed
})
test('Should use retry-after header for retries | throwOnError: false', async t => {
t = tspl(t, { plan: 5 })
let counter = 0
const chunks = []
const server = createServer({ joinDuplicateHeaders: true })
let checkpoint
const dispatchOptions = {
retryOptions: {
throwOnError: false
},
method: 'PUT',
path: '/',
headers: {
'content-type': 'application/json'
}
}
server.on('request', (req, res) => {
switch (counter) {
case 0:
res.writeHead(429, {
'retry-after': 1
})
res.end('rate limit')
checkpoint = Date.now()
counter++
return
case 1:
res.writeHead(200)
res.end('hello world!')
t.ok(Date.now() - checkpoint >= 500)
counter++
return
default:
t.fail('unexpected request')
}
})
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
const handler = new RetryHandler(dispatchOptions, {
dispatch: client.dispatch.bind(client),
handler: {
onConnect () {
t.ok(true, 'pass')
},
onHeaders (status, _rawHeaders, resume, _statusMessage) {
t.strictEqual(status, 200)
return true
},
onData (chunk) {
chunks.push(chunk)
return true
},
onComplete () {
t.strictEqual(Buffer.concat(chunks).toString('utf-8'), 'hello world!')
},
onError () {
t.fail()
}
}
})
after(async () => {
await client.close()
server.close()
await once(server, 'close')
})
client.dispatch(
{
method: 'PUT',
path: '/',
headers: {
'content-type': 'application/json'
}
},
handler
)
})
await t.completed
})
test('Should use retry-after header for retries (date) | throwOnError: false', async t => {
t = tspl(t, { plan: 5 })
let counter = 0
const chunks = []
const server = createServer({ joinDuplicateHeaders: true })
let checkpoint
const dispatchOptions = {
retryOptions: {
throwOnError: false
},
method: 'PUT',
path: '/',
headers: {
'content-type': 'application/json'
}
}
server.on('request', (req, res) => {
switch (counter) {
case 0:
checkpoint = Date.now()
res.writeHead(429, {
'retry-after': new Date(
checkpoint + 2000
).toUTCString()
})
res.end('rate limit')
counter++
return
case 1:
res.writeHead(200)
res.end('hello world!')
t.ok(Date.now() - checkpoint >= 1000)
counter++
return
default:
t.fail('unexpected request')
}
})
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
const handler = new RetryHandler(dispatchOptions, {
dispatch: client.dispatch.bind(client),
handler: {
onConnect () {
t.ok(true, 'pass')
},
onHeaders (status, _rawHeaders, resume, _statusMessage) {
t.strictEqual(status, 200)
return true
},
onData (chunk) {
chunks.push(chunk)
return true
},
onComplete () {
t.strictEqual(Buffer.concat(chunks).toString('utf-8'), 'hello world!')
},
onError () {
t.fail()
}
}
})
after(async () => {
await client.close()
server.close()
await once(server, 'close')
})
client.dispatch(
{
method: 'PUT',
path: '/',
headers: {
'content-type': 'application/json'
}
},
handler
)
})
await t.completed
})
test('Should retry with defaults | throwOnError: false', async t => {
t = tspl(t, { plan: 5 })
let counter = 0
const chunks = []
const server = createServer({ joinDuplicateHeaders: true })
const dispatchOptions = {
retryOptions: {
throwOnError: false
},
method: 'GET',
path: '/',
headers: {
'content-type': 'application/json'
}
}
server.on('request', (req, res) => {
switch (counter) {
case 0:
req.destroy()
counter++
return
case 1:
res.writeHead(500)
res.end('failed')
counter++
return
case 2:
res.writeHead(200)
res.end('hello world!')
counter++
return
default:
t.fail()
}
})
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
const handler = new RetryHandler(dispatchOptions, {
dispatch: client.dispatch.bind(client),
handler: {
onConnect () {
t.ok(true, 'pass')
},
onHeaders (status, _rawHeaders, resume, _statusMessage) {
t.strictEqual(status, 200)
return true
},
onData (chunk) {
chunks.push(chunk)
return true
},
onComplete () {
t.strictEqual(Buffer.concat(chunks).toString('utf-8'), 'hello world!')
},
onError () {
t.fail()
}
}
})
after(async () => {
await client.close()
server.close()
await once(server, 'close')
})
client.dispatch(
{
method: 'GET',
path: '/',
headers: {
'content-type': 'application/json'
}
},
handler
)
})
await t.completed
})
test('Should handle 206 partial content | throwOnError: false', async t => {
t = tspl(t, { plan: 6 })
const chunks = []
let counter = 0
// Took from: https://github.com/nxtedition/nxt-lib/blob/4b001ebc2f22cf735a398f35ff800dd553fe5933/test/undici/retry.js#L47
let x = 0
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
if (x === 0) {
t.ok(true, 'pass')
res.setHeader('etag', 'asd')
res.write('abc')
setTimeout(() => {
res.destroy()
}, 1e2)
} else if (x === 1) {
t.deepStrictEqual(req.headers.range, 'bytes=3-')
res.setHeader('content-range', 'bytes 3-6/6')
res.setHeader('etag', 'asd')
res.statusCode = 206
res.end('def')
}
x++
})
const dispatchOptions = {
retryOptions: {
throwOnError: false,
retry: function (err, _, done) {
counter++
if (err.code && err.code === 'UND_ERR_DESTROYED') {
return done(false)
}
if (err.statusCode === 206) return done(err)
setTimeout(done, 800)
}
},
method: 'GET',
path: '/',
headers: {
'content-type': 'application/json'
}
}
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
const handler = new RetryHandler(dispatchOptions, {
dispatch: (...args) => {
return client.dispatch(...args)
},
handler: {
onConnect () {
t.ok(true, 'pass')
},
onHeaders (status, _rawHeaders, _resume, _statusMessage) {
t.strictEqual(status, 200)
return true
},
onData (chunk) {
chunks.push(chunk)
return true
},
onComplete () {
t.strictEqual(Buffer.concat(chunks).toString('utf-8'), 'abcdef')
t.strictEqual(counter, 1)
},
onError () {
t.fail()
}
}
})
client.dispatch(
{
method: 'GET',
path: '/',
headers: {
'content-type': 'application/json'
}
},
handler
)
after(async () => {
await client.close()
server.close()
await once(server, 'close')
})
})
await t.completed
})
test('Should handle 206 partial content - bad-etag | throwOnError: false', async t => {
t = tspl(t, { plan: 7 })
const chunks = []
// Took from: https://github.com/nxtedition/nxt-lib/blob/4b001ebc2f22cf735a398f35ff800dd553fe5933/test/undici/retry.js#L47
let x = 0
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
if (x === 0) {
t.ok(true, 'pass')
res.setHeader('etag', 'asd')
res.write('abc')
setTimeout(() => {
res.destroy()
}, 1e2)
} else if (x === 1) {
t.deepStrictEqual(req.headers.range, 'bytes=3-')
res.setHeader('content-range', 'bytes 3-6/6')
res.setHeader('etag', 'erwsd')
res.statusCode = 206
res.end('def')
}
x++
})
const dispatchOptions = {
retryOptions: {
throwOnError: false
},
method: 'GET',
path: '/',
headers: {
'content-type': 'application/json'
}
}
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
const handler = new RetryHandler(
dispatchOptions,
{
dispatch: (...args) => {
return client.dispatch(...args)
},
handler: {
onConnect () {
t.ok(true, 'pass')
},
onHeaders (_status, _rawHeaders, _resume, _statusMessage) {
return true
},
onData (chunk) {
chunks.push(chunk)
return true
},
onComplete () {
t.ifError('should not complete')
},
onError (err) {
t.strictEqual(Buffer.concat(chunks).toString('utf-8'), 'abc')
t.strictEqual(err.code, 'UND_ERR_REQ_RETRY')
t.strictEqual(err.message, 'ETag mismatch')
t.deepEqual(err.data, { count: 2 })
}
}
}
)
client.dispatch(
{
method: 'GET',
path: '/',
headers: {
'content-type': 'application/json'
}
},
handler
)
after(async () => {
await client.close()
server.close()
await once(server, 'close')
})
})
await t.completed
})
test('retrying a request with a body | throwOnError: false', async t => {
let counter = 0
const server = createServer({ joinDuplicateHeaders: true })
const dispatchOptions = {
retryOptions: {
throwOnError: false,
retry: (err, { state, opts }, done) => {
counter++
if (
err.statusCode === 500 ||
err.message.includes('other side closed')
) {
setTimeout(done, 500)
return
}
return done(err)
}
},
method: 'POST',
path: '/',
headers: {
'content-type': 'application/json'
},
body: JSON.stringify({ hello: 'world' })
}
t = tspl(t, { plan: 1 })
server.on('request', (req, res) => {
switch (counter) {
case 0:
req.destroy()
return
case 1:
res.writeHead(500)
res.end('failed')
return
case 2:
res.writeHead(200)
res.end('hello world!')
return
default:
t.fail()
}
})
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
const handler = new RetryHandler(dispatchOptions, {
dispatch: client.dispatch.bind(client),
handler: new RequestHandler(dispatchOptions, (err, data) => {
t.ifError(err)
})
})
after(async () => {
await client.close()
server.close()
await once(server, 'close')
})
client.dispatch(
{
method: 'POST',
path: '/',
headers: {
'content-type': 'application/json'
},
body: JSON.stringify({ hello: 'world' })
},
handler
)
})
await t.completed
})
test('retrying a request with a body (stream) | throwOnError: false', async t => {
let counter = 0
const server = createServer({ joinDuplicateHeaders: true })
const dispatchOptions = {
retryOptions: {
throwOnError: false,
retry: (err, { state, opts }, done) => {
counter++
if (
err.statusCode === 500 ||
err.message.includes('other side closed')
) {
setTimeout(done, 500)
return
}
return done(err)
}
},
method: 'POST',
path: '/',
headers: {
'content-type': 'application/json'
},
body: Readable.from(Buffer.from(JSON.stringify({ hello: 'world' })))
}
t = tspl(t, { plan: 3 })
server.on('request', (req, res) => {
switch (counter) {
case 0:
res.writeHead(500)
res.end('failed')
return
default:
t.fail()
}
})
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
const handler = new RetryHandler(dispatchOptions, {
dispatch: client.dispatch.bind(client),
handler: new RequestHandler(dispatchOptions, (err, data) => {
t.ifError(err)
t.equal(data.statusCode, 500)
data.body.text().then(text => {
t.equal(text, 'failed')
})
})
})
after(async () => {
await client.close()
server.close()
await once(server, 'close')
})
client.dispatch(
dispatchOptions,
handler
)
})
await t.completed
})
test('retrying a request with a body (buffer) | throwOnError: false', async t => {
let counter = 0
const server = createServer({ joinDuplicateHeaders: true })
const dispatchOptions = {
retryOptions: {
throwOnError: false,
retry: (err, { state, opts }, done) => {
counter++
if (
err.statusCode === 500 ||
err.message.includes('other side closed')
) {
setTimeout(done, 500)
return
}
return done(err)
}
},
method: 'POST',
path: '/',
headers: {
'content-type': 'application/json'
},
body: Buffer.from(JSON.stringify({ hello: 'world' }))
}
t = tspl(t, { plan: 1 })
server.on('request', (req, res) => {
switch (counter) {
case 0:
req.destroy()
return
case 1:
res.writeHead(500)
res.end('failed')
return
case 2:
res.writeHead(200)
res.end('hello world!')
return
default:
t.fail()
}
})
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
const handler = new RetryHandler(dispatchOptions, {
dispatch: client.dispatch.bind(client),
handler: new RequestHandler(dispatchOptions, (err, data) => {
t.ifError(err)
})
})
after(async () => {
await client.close()
server.close()
await once(server, 'close')
})
client.dispatch(
dispatchOptions,
handler
)
})
await t.completed
})
test('should not error if request is not meant to be retried | throwOnError: false', async t => {
t = tspl(t, { plan: 3 })
const server = createServer({ joinDuplicateHeaders: true })
server.on('request', (req, res) => {
res.writeHead(400)
res.end('Bad request')
})
const dispatchOptions = {
retryOptions: {
throwOnError: false
},
method: 'GET',
path: '/',
headers: {
'content-type': 'application/json'
}
}
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
const chunks = []
const handler = new RetryHandler(dispatchOptions, {
dispatch: client.dispatch.bind(client),
handler: {
onConnect () {
t.ok(true, 'pass')
},
onHeaders (status, _rawHeaders, resume, _statusMessage) {
t.strictEqual(status, 400)
return true
},
onData (chunk) {
chunks.push(chunk)
return true
},
onComplete () {
t.strictEqual(Buffer.concat(chunks).toString('utf-8'), 'Bad request')
},
onError (err) {
t.fail(err)
}
}
})
after(async () => {
await client.close()
server.close()
await once(server, 'close')
})
client.dispatch(
{
method: 'GET',
path: '/',
headers: {
'content-type': 'application/json'
}
},
handler
)
})
await t.completed
})
test('Should be able to properly pass the minTimeout to the RetryContext when constructing a RetryCallback function | throwOnError: false', async t => {
t = tspl(t, { plan: 2 })
let counter = 0
const server = createServer({ joinDuplicateHeaders: true })
server.on('request', (req, res) => {
switch (counter) {
case 0:
res.writeHead(500)
res.end('failed')
return
case 1:
res.writeHead(200)
res.end('hello world!')
return
default:
t.fail()
}
})
const dispatchOptions = {
retryOptions: {
throwOnError: false,
retry: (err, { state, opts }, done) => {
counter++
t.strictEqual(opts.retryOptions.minTimeout, 100)
if (err.statusCode === 500) {
return done()
}
return done(err)
},
minTimeout: 100
},
method: 'GET',
path: '/',
headers: {
'content-type': 'application/json'
}
}
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
const handler = new RetryHandler(dispatchOptions, {
dispatch: client.dispatch.bind(client),
handler: new RequestHandler(dispatchOptions, (err, data) => {
t.ifError(err)
})
})
after(async () => {
await client.close()
server.close()
await once(server, 'close')
})
client.dispatch(
{
method: 'GET',
path: '/',
headers: {
'content-type': 'application/json'
}
},
handler
)
})
await t.completed
})
test('Issue#2986 - Handle custom 206 | throwOnError: false', async t => {
t = tspl(t, { plan: 6 })
const chunks = []
let counter = 0
// Took from: https://github.com/nxtedition/nxt-lib/blob/4b001ebc2f22cf735a398f35ff800dd553fe5933/test/undici/retry.js#L47
let x = 0
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
if (x === 0) {
t.deepStrictEqual(req.headers.range, 'bytes=0-3')
res.setHeader('etag', 'asd')
res.write('abc')
setTimeout(() => {
res.destroy()
}, 1e2)
} else if (x === 1) {
t.deepStrictEqual(req.headers.range, 'bytes=3-')
res.setHeader('content-range', 'bytes 3-6/6')
res.setHeader('etag', 'asd')
res.statusCode = 206
res.end('def')
}
x++
})
const dispatchOptions = {
retryOptions: {
throwOnError: false,
retry: function (err, _, done) {
counter++
if (err.code && err.code === 'UND_ERR_DESTROYED') {
return done(false)
}
if (err.statusCode === 206) return done(err)
setTimeout(done, 800)
}
},
method: 'GET',
path: '/',
headers: {
'content-type': 'application/json'
}
}
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
const handler = new RetryHandler(dispatchOptions, {
dispatch: (...args) => {
return client.dispatch(...args)
},
handler: {
onConnect () {
t.ok(true, 'pass')
},
onHeaders (status, _rawHeaders, resume, _statusMessage) {
t.strictEqual(status, 200)
return true
},
onData (chunk) {
chunks.push(chunk)
return true
},
onComplete () {
t.strictEqual(Buffer.concat(chunks).toString('utf-8'), 'abcdef')
t.strictEqual(counter, 1)
},
onError () {
t.fail()
}
}
})
client.dispatch(
{
method: 'GET',
path: '/',
headers: {
'content-type': 'application/json',
Range: 'bytes=0-3'
}
},
handler
)
after(async () => {
await client.close()
server.close()
await once(server, 'close')
})
})
await t.completed
})
test('Issue#3128 - Support if-match | throwOnError: false', async t => {
t = tspl(t, { plan: 7 })
const chunks = []
let counter = 0
// Took from: https://github.com/nxtedition/nxt-lib/blob/4b001ebc2f22cf735a398f35ff800dd553fe5933/test/undici/retry.js#L47
let x = 0
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
if (x === 0) {
t.deepStrictEqual(req.headers.range, 'bytes=0-3')
res.setHeader('etag', 'asd')
res.write('abc')
setTimeout(() => {
res.destroy()
}, 1e2)
} else if (x === 1) {
t.deepStrictEqual(req.headers.range, 'bytes=3-')
t.deepStrictEqual(req.headers['if-match'], 'asd')
res.setHeader('content-range', 'bytes 3-6/6')
res.setHeader('etag', 'asd')
res.statusCode = 206
res.end('def')
}
x++
})
const dispatchOptions = {
retryOptions: {
throwOnError: false,
retry: function (err, _, done) {
counter++
if (err.code && err.code === 'UND_ERR_DESTROYED') {
return done(false)
}
if (err.statusCode === 206) return done(err)
setTimeout(done, 800)
}
},
method: 'GET',
path: '/',
headers: {
'content-type': 'application/json'
}
}
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
const handler = new RetryHandler(dispatchOptions, {
dispatch: (...args) => {
return client.dispatch(...args)
},
handler: {
onConnect () {
t.ok(true, 'pass')
},
onHeaders (status, _rawHeaders, resume, _statusMessage) {
t.strictEqual(status, 200)
return true
},
onData (chunk) {
chunks.push(chunk)
return true
},
onComplete () {
t.strictEqual(Buffer.concat(chunks).toString('utf-8'), 'abcdef')
t.strictEqual(counter, 1)
},
onError () {
t.fail()
}
}
})
client.dispatch(
{
method: 'GET',
path: '/',
headers: {
'content-type': 'application/json',
Range: 'bytes=0-3'
}
},
handler
)
after(async () => {
await client.close()
server.close()
await once(server, 'close')
})
})
await t.completed
})
test('Issue#3128 - Should ignore weak etags | throwOnError: false', async t => {
t = tspl(t, { plan: 7 })
const chunks = []
let counter = 0
// Took from: https://github.com/nxtedition/nxt-lib/blob/4b001ebc2f22cf735a398f35ff800dd553fe5933/test/undici/retry.js#L47
let x = 0
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
if (x === 0) {
t.deepStrictEqual(req.headers.range, 'bytes=0-3')
res.setHeader('etag', 'W/asd')
res.write('abc')
setTimeout(() => {
res.destroy()
}, 1e2)
} else if (x === 1) {
t.deepStrictEqual(req.headers.range, 'bytes=3-')
t.equal(req.headers['if-match'], undefined)
res.setHeader('content-range', 'bytes 3-6/6')
res.setHeader('etag', 'W/asd')
res.statusCode = 206
res.end('def')
}
x++
})
const dispatchOptions = {
retryOptions: {
throwOnError: false,
retry: function (err, _, done) {
counter++
if (err.code && err.code === 'UND_ERR_DESTROYED') {
return done(false)
}
if (err.statusCode === 206) return done(err)
setTimeout(done, 800)
}
},
method: 'GET',
path: '/',
headers: {
'content-type': 'application/json'
}
}
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
const handler = new RetryHandler(dispatchOptions, {
dispatch: (...args) => {
return client.dispatch(...args)
},
handler: {
onConnect () {
t.ok(true, 'pass')
},
onHeaders (status, _rawHeaders, resume, _statusMessage) {
t.strictEqual(status, 200)
return true
},
onData (chunk) {
chunks.push(chunk)
return true
},
onComplete () {
t.strictEqual(Buffer.concat(chunks).toString('utf-8'), 'abcdef')
t.strictEqual(counter, 1)
},
onError () {
t.fail()
}
}
})
client.dispatch(
{
method: 'GET',
path: '/',
headers: {
'content-type': 'application/json',
Range: 'bytes=0-3'
}
},
handler
)
after(async () => {
await client.close()
server.close()
await once(server, 'close')
})
})
await t.completed
})
test('Weak etags are ignored on range-requests | throwOnError: false', async t => {
t = tspl(t, { plan: 7 })
const chunks = []
let counter = 0
// Took from: https://github.com/nxtedition/nxt-lib/blob/4b001ebc2f22cf735a398f35ff800dd553fe5933/test/undici/retry.js#L47
let x = 0
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
if (x === 0) {
t.deepStrictEqual(req.headers.range, 'bytes=0-3')
res.setHeader('etag', 'W/asd')
res.write('abc')
setTimeout(() => {
res.destroy()
}, 1e2)
} else if (x === 1) {
t.deepStrictEqual(req.headers.range, 'bytes=3-')
t.equal(req.headers['if-match'], undefined)
res.setHeader('content-range', 'bytes 3-6/6')
res.setHeader('etag', 'W/efg')
res.statusCode = 206
res.end('def')
}
x++
})
const dispatchOptions = {
retryOptions: {
throwOnError: false,
retry: function (err, _, done) {
counter++
if (err.code && err.code === 'UND_ERR_DESTROYED') {
return done(false)
}
if (err.statusCode === 206) return done(err)
setTimeout(done, 800)
}
},
method: 'GET',
path: '/',
headers: {
'content-type': 'application/json'
}
}
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
const handler = new RetryHandler(dispatchOptions, {
dispatch: (...args) => {
return client.dispatch(...args)
},
handler: {
onConnect () {
t.ok(true, 'pass')
},
onHeaders (status, _rawHeaders, resume, _statusMessage) {
t.strictEqual(status, 200)
return true
},
onData (chunk) {
chunks.push(chunk)
return true
},
onComplete () {
t.strictEqual(Buffer.concat(chunks).toString('utf-8'), 'abcdef')
t.strictEqual(counter, 1)
},
onError () {
t.fail()
}
}
})
client.dispatch(
{
method: 'GET',
path: '/',
headers: {
'content-type': 'application/json',
Range: 'bytes=0-3'
}
},
handler
)
after(async () => {
await client.close()
server.close()
await once(server, 'close')
})
})
await t.completed
})
test('Should throw RequestRetryError when Content-Range mismatch | throwOnError: false', async t => {
t = tspl(t, { plan: 8 })
const chunks = []
// Took from: https://github.com/nxtedition/nxt-lib/blob/4b001ebc2f22cf735a398f35ff800dd553fe5933/test/undici/retry.js#L47
let x = 0
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
if (x === 0) {
t.ok(true, 'pass')
res.setHeader('etag', 'asd')
res.write('abc')
setTimeout(() => {
res.destroy()
}, 1e2)
} else if (x === 1) {
t.deepStrictEqual(req.headers.range, 'bytes=3-')
res.setHeader('content-range', 'bytes bad') // intentionally bad to trigger error
res.setHeader('etag', 'asd')
res.statusCode = 206
res.end('def')
}
x++
})
const dispatchOptions = {
retryOptions: {
throwOnError: false,
retry: function (err, _, done) {
if (err.code && err.code === 'UND_ERR_DESTROYED') {
return done(false)
}
if (err.statusCode === 206) return done(err)
setTimeout(done, 800)
}
},
method: 'GET',
path: '/',
headers: {
'content-type': 'application/json'
}
}
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
const handler = new RetryHandler(dispatchOptions, {
dispatch: (...args) => {
return client.dispatch(...args)
},
handler: {
onConnect () {
t.ok(true, 'pass')
},
onHeaders (status, _rawHeaders, _resume, _statusMessage) {
t.strictEqual(status, 200)
return true
},
onData (chunk) {
chunks.push(chunk)
return true
},
onComplete () {
t.ifError('should not complete')
},
onError (err) {
t.strictEqual(Buffer.concat(chunks).toString('utf-8'), 'abc')
t.strictEqual(err.code, 'UND_ERR_REQ_RETRY')
t.strictEqual(err.message, 'Content-Range mismatch')
t.deepEqual(err.data, { count: 2 })
}
}
})
client.dispatch(
{
method: 'GET',
path: '/',
headers: {
'content-type': 'application/json'
}
},
handler
)
after(async () => {
await client.close()
server.close()
await once(server, 'close')
})
})
await t.completed
})
test('Should use retry-after header for retries (date) but date format is wrong | throwOnError: false', async t => {
t = tspl(t, { plan: 3 })
let counter = 0
const chunks = []
const server = createServer({ joinDuplicateHeaders: true })
let checkpoint
const dispatchOptions = {
method: 'PUT',
path: '/',
headers: {
'content-type': 'application/json'
},
retryOptions: {
minTimeout: 1000,
throwOnError: false
}
}
server.on('request', (req, res) => {
switch (counter) {
case 0:
checkpoint = Date.now()
res.writeHead(429, {
'retry-after': 'this is not a date'
})
res.end('rate limit')
counter++
return
case 1:
res.writeHead(200)
res.end('hello world!')
t.ok(Date.now() - checkpoint >= 1000)
counter++
return
default:
t.fail('unexpected request')
}
})
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
const handler = new RetryHandler(dispatchOptions, {
dispatch: client.dispatch.bind(client),
handler: {
onConnect () {
t.ok(true, 'pass')
},
onHeaders (status, _rawHeaders, resume, _statusMessage) {
t.strictEqual(status, 200)
return true
},
onData (chunk) {
chunks.push(chunk)
return true
},
onComplete () {
t.strictEqual(Buffer.concat(chunks).toString('utf-8'), 'hello world!')
},
onError (err) {
t.ifError(err)
}
}
})
after(async () => {
await client.close()
server.close()
await once(server, 'close')
})
client.dispatch(
{
method: 'PUT',
path: '/',
headers: {
'content-type': 'application/json'
}
},
handler
)
})
await t.completed
})
================================================
FILE: test/round-robin-pool.js
================================================
'use strict'
const { tspl } = require('@matteo.collina/tspl')
const { test, after } = require('node:test')
const { createServer } = require('node:http')
const { promisify } = require('node:util')
const {
RoundRobinPool,
Client,
errors
} = require('..')
test('throws when connection is infinite', async (t) => {
t = tspl(t, { plan: 2 })
try {
new RoundRobinPool(null, { connections: 0 / 0 }) // eslint-disable-line
} catch (e) {
t.ok(e instanceof errors.InvalidArgumentError)
t.strictEqual(e.message, 'invalid connections')
}
})
test('throws when connections is negative', async (t) => {
t = tspl(t, { plan: 2 })
try {
new RoundRobinPool(null, { connections: -1 }) // eslint-disable-line
} catch (e) {
t.ok(e instanceof errors.InvalidArgumentError)
t.strictEqual(e.message, 'invalid connections')
}
})
test('throws when factory is not a function', (t) => {
const p = tspl(t, { plan: 2 })
try {
new RoundRobinPool('http://localhost', { factory: '' }) // eslint-disable-line
} catch (err) {
p.ok(err instanceof errors.InvalidArgumentError)
p.strictEqual(err.message, 'factory must be a function.')
}
})
test('passes socketPath to custom connect function', async (t) => {
const p = tspl(t, { plan: 2 })
const connectError = new Error('custom connect error')
const socketPath = '/var/run/test.sock'
const pool = new RoundRobinPool('http://localhost', {
socketPath,
connect (opts, cb) {
p.strictEqual(opts.socketPath, socketPath)
cb(connectError, null)
}
})
t.after(() => pool.close())
pool.request({
path: '/',
method: 'GET'
}, (err) => {
p.strictEqual(err, connectError)
})
await p.completed
})
test('basic get', async (t) => {
t = tspl(t, { plan: 4 })
const server = createServer((req, res) => {
t.strictEqual('/', req.url)
t.strictEqual('GET', req.method)
res.setHeader('content-type', 'text/plain')
res.end('hello')
})
after(() => server.close())
await new Promise(resolve => server.listen(0, resolve))
const pool = new RoundRobinPool(`http://localhost:${server.address().port}`, {
connections: 1
})
after(() => pool.close())
const { statusCode, body } = await pool.request({ path: '/', method: 'GET' })
t.strictEqual(statusCode, 200)
const text = await body.text()
t.strictEqual(text, 'hello')
await t.completed
})
test('connect/disconnect event(s)', async (t) => {
const clients = 2
const p = tspl(t, { plan: clients * 5 })
const server = createServer((req, res) => {
res.writeHead(200, {
Connection: 'keep-alive',
'Keep-Alive': 'timeout=1s'
})
res.end('ok')
})
t.after(server.close.bind(server))
server.listen(0, () => {
const pool = new RoundRobinPool(`http://localhost:${server.address().port}`, {
connections: clients,
keepAliveTimeoutThreshold: 100
})
t.after(() => pool.close())
pool.on('connect', (origin, [pool, client]) => {
p.ok(client instanceof Client)
})
pool.on('disconnect', (origin, [pool, client], error) => {
p.ok(client instanceof Client)
p.ok(error instanceof errors.InformationalError)
p.strictEqual(error.code, 'UND_ERR_INFO')
})
for (let i = 0; i < clients; i++) {
pool.request({
path: '/',
method: 'GET'
}, (err, { body }) => {
p.ifError(err)
body.resume()
})
}
})
await p.completed
})
test('busy', async (t) => {
const p = tspl(t, { plan: 8 * 6 + 2 + 1 })
const server = createServer((req, res) => {
p.strictEqual('/', req.url)
p.strictEqual('GET', req.method)
res.setHeader('content-type', 'text/plain')
res.end('hello')
})
t.after(server.close.bind(server))
server.listen(0, async () => {
const client = new RoundRobinPool(`http://localhost:${server.address().port}`, {
connections: 2,
pipelining: 2
})
client.on('drain', () => {
p.ok(1)
})
client.on('connect', () => {
p.ok(1)
})
t.after(client.destroy.bind(client))
for (let n = 1; n <= 8; ++n) {
client.request({ path: '/', method: 'GET' }, (err, { statusCode, headers, body }) => {
p.ifError(err)
p.strictEqual(statusCode, 200)
p.strictEqual(headers['content-type'], 'text/plain')
const bufs = []
body.on('data', (buf) => {
bufs.push(buf)
})
body.on('end', () => {
p.strictEqual('hello', Buffer.concat(bufs).toString('utf8'))
})
})
}
})
await p.completed
})
test('factory option with basic get request', async (t) => {
const p = tspl(t, { plan: 8 })
let factoryCalled = 0
const opts = {
connections: 1,
factory: (origin, opts) => {
factoryCalled++
return new Client(origin, opts)
}
}
const server = createServer((req, res) => {
res.setHeader('content-type', 'text/plain')
res.end('hello')
})
t.after(server.close.bind(server))
await promisify(server.listen).call(server, 0)
const client = new RoundRobinPool(`http://localhost:${server.address().port}`, opts)
t.after(client.destroy.bind(client))
const { statusCode, headers, body } = await client.request({ path: '/', method: 'GET' })
p.strictEqual(statusCode, 200)
p.strictEqual(headers['content-type'], 'text/plain')
p.strictEqual('hello', await body.text())
p.ok(factoryCalled >= 1) // May create one or more clients
p.strictEqual(client.destroyed, false)
p.strictEqual(client.closed, false)
await client.close()
p.strictEqual(client.destroyed, true)
p.strictEqual(client.closed, true)
})
test('round-robin distribution with multiple requests', async (t) => {
const p = tspl(t, { plan: 2 })
let totalRequests = 0
const clientRequests = new Map() // Track requests per client connection
const server = createServer((req, res) => {
totalRequests++
// Track which connection this request came from via socket remote port
const clientKey = `${req.socket.remoteAddress}:${req.socket.remotePort}`
clientRequests.set(clientKey, (clientRequests.get(clientKey) || 0) + 1)
// Add delay to make clients busy and force creation of multiple connections
setTimeout(() => {
res.writeHead(200, { 'Content-Type': 'text/plain' })
res.end('ok')
}, 50)
})
after(() => server.close())
await new Promise(resolve => server.listen(0, resolve))
const pool = new RoundRobinPool(`http://localhost:${server.address().port}`, {
connections: 3
})
after(() => pool.close())
// This forces creation of multiple connections
const requests = []
for (let i = 0; i < 30; i++) {
requests.push(pool.request({ path: '/', method: 'GET' }).then(({ body }) => body.text()))
}
await Promise.all(requests)
p.strictEqual(totalRequests, 30)
// Check that multiple connections were used (not all requests on one connection)
// With round-robin, we should have close to equal distribution
const requestCounts = Array.from(clientRequests.values())
const max = Math.max(...requestCounts)
const min = Math.min(...requestCounts)
const ratio = max / min
// With round-robin and concurrent requests forcing multiple connections:
// should see relatively even distribution (ratio < 2.5)
p.ok(ratio < 2.5, `Distribution ratio ${ratio.toFixed(2)}x should be < 2.5 (counts: ${requestCounts.join(', ')})`)
await p.completed
})
test('round-robin wraps around correctly', async (t) => {
t = tspl(t, { plan: 2 })
let requestCount = 0
const server = createServer((req, res) => {
requestCount++
res.writeHead(200)
res.end('ok')
})
after(() => server.close())
await new Promise(resolve => server.listen(0, resolve))
const pool = new RoundRobinPool(`http://localhost:${server.address().port}`, {
connections: 2
})
after(() => pool.close())
// Make more requests than connections to ensure wrapping
for (let i = 0; i < 5; i++) {
const { body } = await pool.request({ path: '/', method: 'GET' })
await body.text()
}
t.strictEqual(requestCount, 5)
t.ok(pool.stats.connected <= 2)
await t.completed
})
test('close/destroy behavior', async (t) => {
t = tspl(t, { plan: 4 })
const server = createServer((req, res) => {
res.end('ok')
})
after(() => server.close())
await new Promise(resolve => server.listen(0, resolve))
const pool = new RoundRobinPool(`http://localhost:${server.address().port}`)
t.strictEqual(pool.destroyed, false)
t.strictEqual(pool.closed, false)
await pool.close()
t.strictEqual(pool.destroyed, true)
t.strictEqual(pool.closed, true)
await t.completed
})
test('verifies round-robin kGetDispatcher cycling algorithm', async (t) => {
t = tspl(t, { plan: 4 })
const server = createServer((req, res) => {
res.end('ok')
})
after(() => server.close())
await new Promise(resolve => server.listen(0, resolve))
const clientOrder = []
let clientIdCounter = 0
const pool = new RoundRobinPool(`http://localhost:${server.address().port}`, {
connections: 3,
factory: (origin, opts) => {
const client = new Client(origin, opts)
const id = clientIdCounter++
// Intercept dispatch to track which client handles each request
const originalDispatch = client.dispatch.bind(client)
client.dispatch = function (opts, handler) {
clientOrder.push(id)
return originalDispatch(opts, handler)
}
return client
}
})
after(() => pool.close())
// Make 6 requests concurrently
const responses = await Promise.all([
pool.request({ path: '/', method: 'GET' }),
pool.request({ path: '/', method: 'GET' }),
pool.request({ path: '/', method: 'GET' }),
pool.request({ path: '/', method: 'GET' }),
pool.request({ path: '/', method: 'GET' }),
pool.request({ path: '/', method: 'GET' })
])
await Promise.all(responses.map(({ body }) => body.text()))
// Verify core round-robin behavior
t.strictEqual(clientIdCounter, 3, 'Should create exactly 3 clients')
t.deepStrictEqual(clientOrder.slice(0, 3), [0, 1, 2], 'First 3 dispatches create clients 0,1,2 in order')
t.ok(clientOrder.every(id => id < 3), 'All dispatches use one of the 3 clients')
// Verify all clients were used (proves cycling)
const uniqueClients = new Set(clientOrder)
t.strictEqual(uniqueClients.size, 3, 'All 3 clients used (cycling verified)')
await t.completed
})
================================================
FILE: test/snapshot-recorder.js
================================================
'use strict'
const { test } = require('node:test')
const assert = require('node:assert')
const { tmpdir } = require('node:os')
const { join } = require('node:path')
const { unlink } = require('node:fs/promises')
const { SnapshotRecorder, formatRequestKey, createRequestHash, filterHeadersForMatching, filterHeadersForStorage, createHeaderFilters } = require('../lib/mock/snapshot-recorder')
test('SnapshotRecorder - basic recording and retrieval', (t) => {
const recorder = new SnapshotRecorder()
const requestOpts = {
origin: 'https://api.example.com',
path: '/users/123',
method: 'GET',
headers: { authorization: 'Bearer token' }
}
const response = {
statusCode: 200,
headers: { 'content-type': 'application/json' },
body: Buffer.from('{"id": 123, "name": "Test User"}'),
trailers: {}
}
// Record the interaction
recorder.record(requestOpts, response)
// Verify it was recorded
assert.strictEqual(recorder.size(), 1)
// Retrieve the snapshot
const snapshot = recorder.findSnapshot(requestOpts)
assert(snapshot)
assert.strictEqual(snapshot.request.method, 'GET')
assert.strictEqual(snapshot.request.url, 'https://api.example.com/users/123')
assert.strictEqual(snapshot.response.statusCode, 200)
// Body is stored as base64 string
assert.strictEqual(snapshot.response.body, response.body.toString('base64'))
})
test('SnapshotRecorder - request key formatting', (t) => {
const requestOpts = {
origin: 'https://api.example.com',
path: '/search?q=test&limit=10',
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: 'Bearer token'
},
body: '{"filter": "active"}'
}
const cachedSets = createHeaderFilters({})
const formatted = formatRequestKey(requestOpts, cachedSets)
assert.strictEqual(formatted.method, 'POST')
assert.strictEqual(formatted.url, 'https://api.example.com/search?q=test&limit=10')
assert.strictEqual(formatted.headers['content-type'], 'application/json')
assert.strictEqual(formatted.headers.authorization, 'Bearer token')
assert.strictEqual(formatted.body, '{"filter": "active"}')
})
test('SnapshotRecorder - request hashing', (t) => {
const request1 = {
method: 'GET',
url: 'https://api.example.com/users',
headers: { authorization: 'Bearer token' },
body: undefined
}
const request2 = {
method: 'GET',
url: 'https://api.example.com/users',
headers: { authorization: 'Bearer token' },
body: undefined
}
const request3 = {
method: 'POST',
url: 'https://api.example.com/users',
headers: { authorization: 'Bearer token' },
body: undefined
}
const hash1 = createRequestHash(request1)
const hash2 = createRequestHash(request2)
const hash3 = createRequestHash(request3)
// Same requests should have same hash
assert.strictEqual(hash1, hash2)
// Different requests should have different hashes
assert.notStrictEqual(hash1, hash3)
// Hashes should be URL-safe base64
assert(hash1.match(/^[A-Za-z0-9_-]+$/))
})
test('SnapshotRecorder - header normalization', (t) => {
const requestOpts1 = {
origin: 'https://api.example.com',
path: '/test',
headers: {
'Content-Type': 'application/json',
AUTHORIZATION: 'Bearer token'
}
}
const requestOpts2 = {
origin: 'https://api.example.com',
path: '/test',
headers: {
'content-type': 'application/json',
authorization: 'Bearer token'
}
}
const cachedSets = createHeaderFilters({})
const formatted1 = formatRequestKey(requestOpts1, cachedSets)
const formatted2 = formatRequestKey(requestOpts2, cachedSets)
// Headers should be normalized to lowercase
assert.deepStrictEqual(formatted1.headers, formatted2.headers)
assert.strictEqual(formatted1.headers['content-type'], 'application/json')
assert.strictEqual(formatted1.headers.authorization, 'Bearer token')
})
test('SnapshotRecorder - file persistence', async (t) => {
const snapshotPath = join(tmpdir(), `test-recorder-${Date.now()}.json`)
const recorder = new SnapshotRecorder({ snapshotPath })
t.after(() => unlink(snapshotPath).catch(() => {}))
// Record some interactions
await recorder.record(
{ origin: 'https://api.example.com', path: '/users', method: 'GET' },
{ statusCode: 200, headers: {}, body: Buffer.from('user data'), trailers: {} }
)
await recorder.record(
{ origin: 'https://api.example.com', path: '/posts', method: 'GET' },
{ statusCode: 200, headers: {}, body: Buffer.from('post data'), trailers: {} }
)
assert.strictEqual(recorder.size(), 2)
// Save to file
await recorder.saveSnapshots()
// Create new recorder and load from file
const newRecorder = new SnapshotRecorder({ snapshotPath })
await newRecorder.loadSnapshots()
assert.strictEqual(newRecorder.size(), 2)
// Verify snapshots were loaded correctly
const userSnapshot = newRecorder.findSnapshot({
origin: 'https://api.example.com',
path: '/users',
method: 'GET'
})
assert(userSnapshot)
assert.strictEqual(userSnapshot.response.statusCode, 200)
// Body is now stored as base64 string
assert.strictEqual(userSnapshot.response.body, Buffer.from('user data').toString('base64'))
})
test('SnapshotRecorder - loading non-existent file', async (t) => {
const snapshotPath = join(tmpdir(), `non-existent-${Date.now()}.json`)
const recorder = new SnapshotRecorder({ snapshotPath })
// Should not throw, just create empty recorder
await recorder.loadSnapshots()
assert.strictEqual(recorder.size(), 0)
})
test('SnapshotRecorder - array header handling', (t) => {
const requestOpts = {
origin: 'https://api.example.com',
path: '/test',
headers: {
accept: ['application/json', 'text/plain'],
'x-custom': 'single-value'
}
}
const cachedSets = createHeaderFilters({})
const formatted = formatRequestKey(requestOpts, cachedSets)
// Array headers should be joined with comma
assert.strictEqual(formatted.headers.accept, 'application/json, text/plain')
assert.strictEqual(formatted.headers['x-custom'], 'single-value')
})
test('SnapshotRecorder - query parameter handling', (t) => {
const requestOpts1 = {
origin: 'https://api.example.com',
path: '/search?q=test&sort=date',
method: 'GET'
}
const requestOpts2 = {
origin: 'https://api.example.com',
path: '/search?sort=date&q=test', // Different order
method: 'GET'
}
const cachedSets = createHeaderFilters({})
const formatted1 = formatRequestKey(requestOpts1, cachedSets)
const formatted2 = formatRequestKey(requestOpts2, cachedSets)
// URLs with different query parameter order should be normalized
assert.strictEqual(formatted1.url, 'https://api.example.com/search?q=test&sort=date')
// But they should still create different hashes if params are truly different
const hash1 = createRequestHash(formatted1)
const hash2 = createRequestHash(formatted2)
// This tests that parameter order matters in our current implementation
// We might want to normalize parameter order in the future
assert.notStrictEqual(hash1, hash2)
})
test('SnapshotRecorder - clear functionality', async (t) => {
const recorder = new SnapshotRecorder()
// Record some snapshots
await recorder.record(
{ origin: 'https://api.example.com', path: '/test1' },
{ statusCode: 200, headers: {}, body: Buffer.from('data1'), trailers: {} }
)
await recorder.record(
{ origin: 'https://api.example.com', path: '/test2' },
{ statusCode: 200, headers: {}, body: Buffer.from('data2'), trailers: {} }
)
assert.strictEqual(recorder.size(), 2)
// Clear and verify
recorder.clear()
assert.strictEqual(recorder.size(), 0)
// Should not find any snapshots
const snapshot = recorder.findSnapshot({
origin: 'https://api.example.com',
path: '/test1'
})
assert.strictEqual(snapshot, undefined)
})
test('SnapshotRecorder - custom header matching', (t) => {
const headers = {
'content-type': 'application/json',
authorization: 'Bearer token',
'x-request-id': '123',
accept: 'application/json'
}
// Test matchHeaders option
const matchSpecificOptions = { matchHeaders: ['content-type', 'accept'] }
const matchSpecificCachedSets = createHeaderFilters(matchSpecificOptions)
const matchSpecific = filterHeadersForMatching(headers, matchSpecificCachedSets, matchSpecificOptions)
assert.deepStrictEqual(matchSpecific, {
'content-type': 'application/json',
accept: 'application/json'
})
// Test ignoreHeaders option
const ignoreOptions = { ignoreHeaders: ['authorization', 'x-request-id'] }
const ignoreCachedSets = createHeaderFilters(ignoreOptions)
const ignoreAuth = filterHeadersForMatching(headers, ignoreCachedSets, ignoreOptions)
assert.deepStrictEqual(ignoreAuth, {
'content-type': 'application/json',
accept: 'application/json'
})
// Test excludeHeaders option
const excludeOptions = { excludeHeaders: ['authorization'] }
const excludeCachedSets = createHeaderFilters(excludeOptions)
const excludeSensitive = filterHeadersForMatching(headers, excludeCachedSets, excludeOptions)
assert.deepStrictEqual(excludeSensitive, {
'content-type': 'application/json',
'x-request-id': '123',
accept: 'application/json'
})
})
test('SnapshotRecorder - header filtering for storage', (t) => {
const headers = {
'content-type': 'application/json',
'set-cookie': 'session=secret',
authorization: 'Bearer token',
'cache-control': 'no-cache'
}
// Test excluding sensitive headers from storage
const filtered = filterHeadersForStorage(headers, {
exclude: new Set(['set-cookie', 'authorization'])
})
assert.deepStrictEqual(filtered, {
'content-type': 'application/json',
'cache-control': 'no-cache'
})
})
test('SnapshotRecorder - case sensitivity in header filtering', (t) => {
const headers = {
'Content-Type': 'application/json',
AUTHORIZATION: 'Bearer token',
'X-Request-ID': '123'
}
// Test case insensitive (default)
const caseInsensitiveOptions = { ignoreHeaders: ['authorization', 'x-request-id'] }
const caseInsensitiveCachedSets = createHeaderFilters(caseInsensitiveOptions)
const caseInsensitive = filterHeadersForMatching(headers, caseInsensitiveCachedSets, caseInsensitiveOptions)
assert.deepStrictEqual(caseInsensitive, {
'content-type': 'application/json'
})
// Test case sensitive
const caseSensitiveOptions = { ignoreHeaders: ['authorization', 'x-request-id'], caseSensitive: true }
const caseSensitiveCachedSets = createHeaderFilters(caseSensitiveOptions)
const caseSensitive = filterHeadersForMatching(headers, caseSensitiveCachedSets, caseSensitiveOptions)
// Should keep all headers since case doesn't match
assert.deepStrictEqual(caseSensitive, {
'Content-Type': 'application/json',
AUTHORIZATION: 'Bearer token',
'X-Request-ID': '123'
})
})
test('SnapshotRecorder - request formatting with match options', (t) => {
const requestOpts = {
origin: 'https://api.example.com',
path: '/search?q=test&limit=10',
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: 'Bearer token',
'X-Request-ID': '123'
},
body: '{"filter": "active"}'
}
// Test with matchHeaders option
const matchOptions = {
matchHeaders: ['content-type'],
matchBody: false,
matchQuery: false
}
const cachedSets = createHeaderFilters(matchOptions)
const formatted = formatRequestKey(requestOpts, cachedSets, matchOptions)
assert.strictEqual(formatted.method, 'POST')
assert.strictEqual(formatted.url, 'https://api.example.com/search') // No query
assert.deepStrictEqual(formatted.headers, {
'content-type': 'application/json'
})
assert.strictEqual(formatted.body, '') // No body
})
test('SnapshotRecorder - redirect responses are stored correctly', (t) => {
const recorder = new SnapshotRecorder()
// Initial request to the redirect URL
const redirectRequestOpts = {
origin: 'https://api.example.com',
path: '/redirect-start',
method: 'GET',
headers: { accept: 'application/json' }
}
// First response: 302 redirect (this should not be stored)
const redirectResponse = {
statusCode: 302,
headers: { location: '/redirect-target' },
body: Buffer.from('Redirecting...'),
trailers: {}
}
// Final response: 200 success (this should be stored)
const finalResponse = {
statusCode: 200,
headers: { 'content-type': 'application/json' },
body: Buffer.from('{"message": "Final destination"}'),
trailers: {}
}
// Record the redirect response (this will be stored as it's a valid response)
recorder.record(redirectRequestOpts, redirectResponse)
assert.strictEqual(recorder.size(), 1, 'Redirect response (302) should be stored')
// First snapshot should contain the redirect response
let snapshot = recorder.findSnapshot(redirectRequestOpts)
assert(snapshot, 'Should find snapshot for redirect request')
assert.strictEqual(snapshot.request.url, 'https://api.example.com/redirect-start')
assert.strictEqual(snapshot.response.statusCode, 302, 'First stored response should be the 302 redirect')
// Record the final response (this will create a second response for the same request)
recorder.record(redirectRequestOpts, finalResponse)
assert.strictEqual(recorder.size(), 1, 'Should still have one snapshot (same request)')
// Retrieve the snapshot again - should now have multiple responses
snapshot = recorder.findSnapshot(redirectRequestOpts)
assert(snapshot, 'Should find snapshot for redirect request')
assert.strictEqual(snapshot.request.url, 'https://api.example.com/redirect-start')
// The recorder supports sequential responses, so it should have both
assert(Array.isArray(snapshot.responses), 'Should have responses array')
assert.strictEqual(snapshot.responses.length, 2, 'Should have two responses')
assert.strictEqual(snapshot.responses[0].statusCode, 302, 'First response should be redirect')
assert.strictEqual(snapshot.responses[1].statusCode, 200, 'Second response should be final')
})
================================================
FILE: test/snapshot-redirect-interceptor.js
================================================
'use strict'
const { test } = require('node:test')
const assert = require('node:assert')
const { createServer } = require('node:http')
const { promisify } = require('node:util')
const { unlink } = require('node:fs/promises')
const { tmpdir } = require('node:os')
const { join } = require('node:path')
const { SnapshotAgent, setGlobalDispatcher, getGlobalDispatcher, request } = require('..')
test('SnapshotAgent - integration with redirect interceptor', async (t) => {
const snapshotPath = join(tmpdir(), `test-snapshot-redirect-${Date.now()}.json`)
const originalDispatcher = getGlobalDispatcher()
t.after(() => unlink(snapshotPath).catch(() => {}))
t.after(() => setGlobalDispatcher(originalDispatcher))
// Create a server that handles redirects
const server = createServer((req, res) => {
if (req.url === '/redirect-start') {
res.writeHead(302, { location: '/redirect-target' })
res.end('Redirecting...')
} else if (req.url === '/redirect-target') {
res.writeHead(200, { 'content-type': 'application/json' })
res.end(JSON.stringify({ message: 'Final destination' }))
} else {
res.writeHead(404)
res.end('Not Found')
}
})
await promisify(server.listen.bind(server))(0)
const { port } = server.address()
const origin = `http://localhost:${port}`
t.after(() => server.close())
// Demonstrates the intended usage pattern: SnapshotAgent and redirect interceptor together
const { interceptors, Agent } = require('..')
// First use redirect interceptor to capture the complete redirect flow
const redirectAgent = new Agent().compose(interceptors.redirect({ maxRedirections: 5 }))
setGlobalDispatcher(redirectAgent)
const redirectResponse = await request(`${origin}/redirect-start`)
const redirectBody = await redirectResponse.body.json()
// Verify redirect worked
assert.strictEqual(redirectResponse.statusCode, 200)
assert.deepStrictEqual(redirectBody, { message: 'Final destination' })
assert(redirectResponse.context && redirectResponse.context.history)
assert.strictEqual(redirectResponse.context.history.length, 2)
await redirectAgent.close()
// Record redirected responses using SnapshotAgent with redirect interceptor
// This tests the fixed integration where SnapshotAgent automatically records final responses
const recordingAgent = new SnapshotAgent({
mode: 'record',
snapshotPath
}).compose(interceptors.redirect({ maxRedirections: 5 }))
setGlobalDispatcher(recordingAgent)
// Make request to redirect URL - should automatically record the final response
const recordingResponse = await request(`${origin}/redirect-start`)
const recordingBody = await recordingResponse.body.json()
// Verify that we got the final response (not the 302)
assert.strictEqual(recordingResponse.statusCode, 200)
assert.deepStrictEqual(recordingBody, { message: 'Final destination' })
// Note: context.history is not preserved in SnapshotAgent recording mode
// since we capture the final response directly
await recordingAgent.close()
// Playback mode - SnapshotAgent provides recorded responses
// In playback mode, SnapshotAgent returns the recorded final response directly
// Also include redirect interceptor to handle any redirect scenarios consistently
const playbackAgent = new SnapshotAgent({
mode: 'playback',
snapshotPath
}).compose(interceptors.redirect({ maxRedirections: 5 }))
setGlobalDispatcher(playbackAgent)
// This should return the recorded final response directly from snapshot
const playbackResponse = await request(`${origin}/redirect-start`)
const playbackBody = await playbackResponse.body.json()
assert.strictEqual(playbackResponse.statusCode, 200)
assert.deepStrictEqual(playbackBody, { message: 'Final destination' })
// In playback mode, context is not preserved since we're replaying recorded responses
// The important thing is that we get the correct final response content
// Verify the snapshot recorded the redirect request with final response
const playbackRecorder = playbackAgent.getRecorder()
assert.strictEqual(playbackRecorder.size(), 2, 'Should have two snapshots')
const snapshots = playbackRecorder.getSnapshots()
{
const snapshot = snapshots[0]
assert.strictEqual(snapshot.request.url, `${origin}/redirect-start`)
assert.strictEqual(snapshot.responses[0].statusCode, 302)
assert.strictEqual(Buffer.from(snapshot.responses[0].body, 'base64').toString(), 'Redirecting...')
}
{
const snapshot = snapshots[1]
assert.strictEqual(snapshot.request.url, `${origin}/redirect-target`)
assert.strictEqual(snapshot.responses[0].statusCode, 200)
assert.deepStrictEqual(JSON.parse(Buffer.from(snapshot.responses[0].body, 'base64')), {
message: 'Final destination'
})
}
await playbackAgent.close()
})
================================================
FILE: test/snapshot-testing.js
================================================
'use strict'
const { describe, it } = require('node:test')
const assert = require('node:assert')
const { createServer } = require('node:http')
const { promisify } = require('node:util')
const { unlink, writeFile, readFile } = require('node:fs/promises')
const { tmpdir } = require('node:os')
const { join } = require('node:path')
const { SnapshotAgent, setGlobalDispatcher, getGlobalDispatcher, request } = require('..')
// Test constants
const TEST_CONSTANTS = {
KEEP_ALIVE_TIMEOUT: 10,
KEEP_ALIVE_MAX_TIMEOUT: 10,
AUTO_FLUSH_INTERVAL: 100,
SEQUENTIAL_RESPONSE_DELAY: 200,
TEST_TIMESTAMP: '2024-01-01T00:00:00Z',
TEST_MESSAGE: 'Hello World',
MAX_SNAPSHOTS_FOR_LRU: 2,
TEST_ORIGINS: {
LOCALHOST_3000: 'http://localhost:3000'
},
ERROR_MESSAGES: {
INVALID_MODE: 'Invalid snapshot mode: invalid. Must be one of: record, playback, update',
MISSING_SNAPSHOT_PATH_PLAYBACK: "snapshotPath is required when mode is 'playback'",
MISSING_SNAPSHOT_PATH_UPDATE: "snapshotPath is required when mode is 'update'",
NO_SNAPSHOT_FOUND: 'No snapshot found for GET /nonexistent'
}
}
// Test helper functions
function createSnapshotPath (prefix = 'test-snapshots') {
return join(tmpdir(), `${prefix}-${Date.now()}.json`)
}
function createTestServer (handler) {
return createServer(handler)
}
async function setupServer (server) {
await promisify(server.listen.bind(server))(0)
const { port } = server.address()
const origin = `http://localhost:${port}`
return { port, origin }
}
function setupCleanup (t, resources) {
if (resources.server) {
t.after(() => {
resources.server.closeAllConnections?.()
resources.server.close()
})
}
if (resources.snapshotPath) {
t.after(() => unlink(resources.snapshotPath).catch(() => {}))
}
if (resources.agent) {
t.after(async () => await resources.agent.close())
}
if (resources.originalDispatcher) {
t.after(() => setGlobalDispatcher(resources.originalDispatcher))
}
}
function createJsonResponse (data) {
return JSON.stringify(data)
}
function createDefaultHandler () {
return (req, res) => {
if (req.url === '/test') {
res.writeHead(200, { 'content-type': 'application/json' })
res.end(createJsonResponse({
message: TEST_CONSTANTS.TEST_MESSAGE,
timestamp: TEST_CONSTANTS.TEST_TIMESTAMP
}))
} else {
res.writeHead(404)
res.end('Not Found')
}
}
}
function createEchoHandler () {
return (req, res) => {
let body = ''
req.on('data', chunk => { body += chunk })
req.on('end', async (t) => {
res.writeHead(200, { 'content-type': 'application/json' })
res.end(createJsonResponse({
received: body,
method: req.method,
headers: req.headers
}))
})
}
}
function createSequentialHandler (responses) {
let callCount = 0
return (req, res) => {
res.writeHead(200, { 'content-type': 'text/plain' })
res.end(responses[callCount++] || responses[responses.length - 1])
}
}
async function createLargeSnapshotFile (path, size = 1000) {
const { createRequestHash, formatRequestKey, createHeaderFilters } = require('../lib/mock/snapshot-recorder')
const snapshots = []
for (let i = 0; i < size; i++) {
const requestOpts = {
origin: 'http://localhost:3000',
path: `/api/test-${i}`,
method: 'GET'
}
const cachedSets = createHeaderFilters({})
const requestKey = formatRequestKey(requestOpts, cachedSets)
const hash = createRequestHash(requestKey)
snapshots.push({
hash,
snapshot: {
request: requestKey,
responses: [{
statusCode: 200,
headers: { 'content-type': 'application/json' },
body: Buffer.from(`{"data": "test-${i}"}`).toString('base64'),
trailers: {}
}],
callCount: 0,
timestamp: new Date().toISOString()
}
})
}
await writeFile(path, JSON.stringify(snapshots, null, 2))
}
// Organize tests with describe blocks
describe('SnapshotAgent - Basic Operations', () => {
it('record mode', async (t) => {
const server = createTestServer(createDefaultHandler())
const { origin } = await setupServer(server)
const snapshotPath = createSnapshotPath('record-mode')
setupCleanup(t, { server, snapshotPath })
const agent = new SnapshotAgent({
keepAliveTimeout: TEST_CONSTANTS.KEEP_ALIVE_TIMEOUT,
keepAliveMaxTimeout: TEST_CONSTANTS.KEEP_ALIVE_MAX_TIMEOUT,
mode: 'record',
snapshotPath
})
// Make a request that should be recorded
const response = await request(`${origin}/test`, {
dispatcher: agent
})
const body = await response.body.json()
assert.strictEqual(response.statusCode, 200, 'Response should have status 200')
assert.deepStrictEqual(body, {
message: TEST_CONSTANTS.TEST_MESSAGE,
timestamp: TEST_CONSTANTS.TEST_TIMESTAMP
}, 'Response body should match expected data')
// Save snapshots
await agent.saveSnapshots()
// Verify snapshot was recorded
const recorder = agent.getRecorder()
assert.strictEqual(recorder.size(), 1, 'Should have recorded exactly one snapshot')
const snapshots = recorder.getSnapshots()
assert.strictEqual(snapshots.length, 1, 'Snapshots array should contain one item')
assert.strictEqual(snapshots[0].request.method, 'GET', 'Recorded request method should be GET')
assert.strictEqual(snapshots[0].request.url, `${origin}/test`, 'Recorded request URL should match')
assert.strictEqual(snapshots[0].responses[0].statusCode, 200, 'Recorded response status should be 200')
})
it('playback mode', async (t) => {
const snapshotPath = createSnapshotPath('playback-mode')
setupCleanup(t, { snapshotPath })
// First, create a recording
const recordingAgent = new SnapshotAgent({
mode: 'record',
snapshotPath
})
// Create a simple server for recording
const server = createTestServer((req, res) => {
res.writeHead(200, { 'content-type': 'text/plain' })
res.end('Recorded response')
})
const { origin } = await setupServer(server)
const originalDispatcher = getGlobalDispatcher()
setupCleanup(t, { server, originalDispatcher })
setGlobalDispatcher(recordingAgent)
// Record the request
await request(`${origin}/api/test`)
await recordingAgent.saveSnapshots()
// Now test playback mode
const playbackAgent = new SnapshotAgent({
mode: 'playback',
snapshotPath
})
setGlobalDispatcher(playbackAgent)
// This should use the recorded response, not make a real request
const response = await request(`${origin}/api/test`)
const body = await response.body.text()
assert.strictEqual(response.statusCode, 200, 'Playback response should have status 200')
assert.strictEqual(body, 'Recorded response', 'Playback should return recorded response')
})
it('update mode', async (t) => {
const snapshotPath = createSnapshotPath('update-mode')
setupCleanup(t, { snapshotPath })
// Create agent in update mode
const agent = new SnapshotAgent({
mode: 'update',
snapshotPath
})
// Create a simple server
const server = createTestServer((req, res) => {
if (req.url === '/existing') {
res.writeHead(200, { 'content-type': 'text/plain' })
res.end('Existing endpoint')
} else if (req.url === '/new') {
res.writeHead(200, { 'content-type': 'text/plain' })
res.end('New endpoint')
} else {
res.writeHead(404)
res.end('Not Found')
}
})
const { origin } = await setupServer(server)
const originalDispatcher = getGlobalDispatcher()
setupCleanup(t, { server, originalDispatcher })
setGlobalDispatcher(agent)
// First request - should be recorded as new
const response1 = await request(`${origin}/existing`)
const body1 = await response1.body.text()
assert.strictEqual(body1, 'Existing endpoint', 'First request should get live response')
// Save and reload to simulate existing snapshots
await agent.saveSnapshots()
// Second request to same endpoint - should use existing snapshot
const response2 = await request(`${origin}/existing`)
const body2 = await response2.body.text()
assert.strictEqual(body2, 'Existing endpoint', 'Second request should use cached response')
// Request to new endpoint - should be recorded
const response3 = await request(`${origin}/new`)
const body3 = await response3.body.text()
assert.strictEqual(body3, 'New endpoint', 'New endpoint should get live response')
// Verify we have 2 different snapshots
const recorder = agent.getRecorder()
assert.strictEqual(recorder.size(), 2, 'Should have exactly two snapshots recorded')
})
})
describe('SnapshotAgent - Request Handling', () => {
it('handles POST requests with body', async (t) => {
const snapshotPath = createSnapshotPath('post-requests')
setupCleanup(t, { snapshotPath })
const server = createTestServer(createEchoHandler())
const { origin } = await setupServer(server)
setupCleanup(t, { server })
// Record mode
const recordingAgent = new SnapshotAgent({
mode: 'record',
snapshotPath
})
const originalDispatcher = getGlobalDispatcher()
setupCleanup(t, { originalDispatcher })
setGlobalDispatcher(recordingAgent)
const requestBody = createJsonResponse({ test: 'data' })
const response = await request(`${origin}/api/submit`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: requestBody
})
const responseBody = await response.body.json()
assert.strictEqual(responseBody.received, requestBody, 'Server should receive the request body')
assert.strictEqual(responseBody.method, 'POST', 'Server should receive POST method')
await recordingAgent.saveSnapshots()
// Playback mode
const playbackAgent = new SnapshotAgent({
mode: 'playback',
snapshotPath
})
setGlobalDispatcher(playbackAgent)
// Make the same request - should get recorded response
const playbackResponse = await request(`${origin}/api/submit`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: requestBody
})
const playbackBody = await playbackResponse.body.json()
assert.strictEqual(playbackBody.received, requestBody, 'Playback should return recorded request body')
assert.strictEqual(playbackBody.method, 'POST', 'Playback should return recorded method')
})
it('sequential response support', async (t) => {
const responses = ['First response', 'Second response', 'Third response']
const server = createTestServer(createSequentialHandler(responses))
const { origin } = await setupServer(server)
const snapshotPath = createSnapshotPath('sequential')
setupCleanup(t, { server, snapshotPath })
// Record multiple responses to the same endpoint
const recordingAgent = new SnapshotAgent({
mode: 'record',
snapshotPath
})
const originalDispatcher = getGlobalDispatcher()
setupCleanup(t, { originalDispatcher })
setGlobalDispatcher(recordingAgent)
// Make multiple requests to record sequential responses
{
const res = await request(`${origin}/api/test`)
await res.body.text()
}
{
const res = await request(`${origin}/api/test`)
await res.body.text()
}
{
const res = await request(`${origin}/api/test`)
await res.body.text()
}
// Ensure all recordings are saved and verify the recording state
await recordingAgent.saveSnapshots()
// Verify recording worked correctly before switching to playback
const recordingRecorder = recordingAgent.getRecorder()
assert.strictEqual(recordingRecorder.size(), 1, 'Should have recorded exactly one snapshot')
const recordedSnapshots = recordingRecorder.getSnapshots()
assert.strictEqual(recordedSnapshots[0].responses.length, 3, 'Should have recorded three responses')
// Close recording agent cleanly before starting playback
await recordingAgent.close()
// Switch to playback mode and test sequential responses
const playbackAgent = new SnapshotAgent({
mode: 'playback',
snapshotPath
})
setupCleanup(t, { agent: playbackAgent })
setGlobalDispatcher(playbackAgent)
// Ensure snapshots are loaded and call counts are reset before setting dispatcher
await playbackAgent.loadSnapshots()
// Reset call counts after loading to ensure clean state
playbackAgent.resetCallCounts()
// Verify we have the expected snapshots before proceeding
const recorder = playbackAgent.getRecorder()
assert.strictEqual(recorder.size(), 1, 'Should have exactly one snapshot loaded')
const snapshots = recorder.getSnapshots()
assert.strictEqual(snapshots.length, 1, 'Should have exactly one snapshot')
assert.strictEqual(snapshots[0].responses.length, 3, 'Should have three sequential responses')
// Test sequential responses
const response1 = await request(`${origin}/api/test`)
const body1 = await response1.body.text()
assert.strictEqual(body1, 'First response', 'First call should return first response')
const response2 = await request(`${origin}/api/test`)
const body2 = await response2.body.text()
assert.strictEqual(body2, 'Second response', 'Second call should return second response')
const response3 = await request(`${origin}/api/test`)
const body3 = await response3.body.text()
assert.strictEqual(body3, 'Third response', 'Third call should return third response')
// Fourth call should repeat the last response
const response4 = await request(`${origin}/api/test`)
const body4 = await response4.body.text()
assert.strictEqual(body4, 'Third response', 'Fourth call should repeat the last response')
})
})
describe('SnapshotAgent - Error Handling', () => {
it('error handling in playback mode', async (t) => {
const snapshotPath = createSnapshotPath('error-handling')
setupCleanup(t, { snapshotPath })
const agent = new SnapshotAgent({
mode: 'playback',
snapshotPath // File doesn't exist
})
const originalDispatcher = getGlobalDispatcher()
setupCleanup(t, { agent, originalDispatcher })
setGlobalDispatcher(agent)
// This should throw because no snapshot exists for this request
let errorThrown = false
try {
await request('http://localhost:9999/nonexistent')
} catch (error) {
errorThrown = true
assert.strictEqual(error.name, 'UndiciError', 'Error should be UndiciError')
assert(error.message.includes(TEST_CONSTANTS.ERROR_MESSAGES.NO_SNAPSHOT_FOUND),
'Error message should indicate no snapshot found')
assert.strictEqual(error.code, 'UND_ERR', 'Error code should be UND_ERR')
}
assert(errorThrown, 'Expected an error to be thrown for missing snapshot')
})
it('constructor options validation', async (t) => {
// Test invalid mode
assert.throws(() => {
return new SnapshotAgent({ mode: 'invalid' })
}, {
name: 'InvalidArgumentError',
message: new RegExp(TEST_CONSTANTS.ERROR_MESSAGES.INVALID_MODE.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))
}, 'Should throw for invalid mode')
// Test missing snapshotPath for playback mode
assert.throws(() => {
return new SnapshotAgent({ mode: 'playback' })
}, {
name: 'InvalidArgumentError',
message: new RegExp(TEST_CONSTANTS.ERROR_MESSAGES.MISSING_SNAPSHOT_PATH_PLAYBACK)
}, 'Should throw for missing snapshotPath in playback mode')
// Test missing snapshotPath for update mode
assert.throws(() => {
return new SnapshotAgent({ mode: 'update' })
}, {
name: 'InvalidArgumentError',
message: new RegExp(TEST_CONSTANTS.ERROR_MESSAGES.MISSING_SNAPSHOT_PATH_UPDATE)
}, 'Should throw for missing snapshotPath in update mode')
// Test valid configurations should not throw
await assert.doesNotReject(async () => {
const agent1 = new SnapshotAgent({ mode: 'record' })
await agent1.close()
}, 'Should not throw for valid record mode')
await assert.doesNotReject(async () => {
const snapshotPath = createSnapshotPath('valid-playback')
const agent2 = new SnapshotAgent({ mode: 'playback', snapshotPath })
await agent2.close()
}, 'Should not throw for valid playback mode')
await assert.doesNotReject(async () => {
const snapshotPath = createSnapshotPath('valid-update')
const agent3 = new SnapshotAgent({ mode: 'update', snapshotPath })
await agent3.close()
}, 'Should not throw for valid update mode')
})
})
describe('SnapshotAgent - Edge Cases', () => {
it('handles large snapshot files', async (t) => {
const snapshotPath = createSnapshotPath('large')
setupCleanup(t, { snapshotPath })
await createLargeSnapshotFile(snapshotPath, 100)
const agent = new SnapshotAgent({
mode: 'playback',
snapshotPath
})
const originalDispatcher = getGlobalDispatcher()
setupCleanup(t, { agent, originalDispatcher })
// Should load large files without issues
await agent.loadSnapshots()
const recorder = agent.getRecorder()
assert.strictEqual(recorder.size(), 100, 'Should load all 100 snapshots from large file')
setGlobalDispatcher(agent)
// Should be able to find and use snapshots from large file
const response = await request('http://localhost:3000/api/test-0')
const body = await response.body.json()
assert.deepStrictEqual(body, { data: 'test-0' }, 'Should return correct data from large snapshot file')
})
it('concurrent access scenarios', async (t) => {
const snapshotPath = createSnapshotPath('concurrent')
setupCleanup(t, { snapshotPath })
const server = createTestServer((req, res) => {
res.writeHead(200, { 'content-type': 'application/json' })
res.end(createJsonResponse({ path: req.url }))
})
const { origin } = await setupServer(server)
setupCleanup(t, { server })
const agent = new SnapshotAgent({
mode: 'record',
snapshotPath
})
const originalDispatcher = getGlobalDispatcher()
setupCleanup(t, { agent, originalDispatcher })
setGlobalDispatcher(agent)
// Make multiple concurrent requests
const promises = []
for (let i = 0; i < 10; i++) {
promises.push(request(`${origin}/api/test-${i}`))
}
const responses = await Promise.all(promises)
// Verify all responses were handled correctly
for (let i = 0; i < responses.length; i++) {
const body = await responses[i].body.json()
assert.deepStrictEqual(body, { path: `/api/test-${i}` },
`Concurrent request ${i} should return correct response`)
}
await agent.saveSnapshots()
const recorder = agent.getRecorder()
assert.strictEqual(recorder.size(), 10, 'Should record all 10 concurrent requests')
})
})
describe('SnapshotAgent - Advanced Features', () => {
it('snapshot file format validation', async (t) => {
const snapshotPath = createSnapshotPath('format-validation')
setupCleanup(t, { snapshotPath })
const server = createTestServer((req, res) => {
res.writeHead(200, { 'x-custom-header': 'test-value' })
res.end('Test response')
})
const { origin } = await setupServer(server)
setupCleanup(t, { server })
const agent = new SnapshotAgent({
mode: 'record',
snapshotPath
})
const originalDispatcher = getGlobalDispatcher()
setupCleanup(t, { agent, originalDispatcher })
setGlobalDispatcher(agent)
await request(`${origin}/test-endpoint`)
await agent.saveSnapshots()
// Read and verify the snapshot file format
const snapshotData = JSON.parse(await readFile(snapshotPath, 'utf8'))
assert(Array.isArray(snapshotData), 'Snapshot data should be an array')
assert.strictEqual(snapshotData.length, 1, 'Should contain exactly one snapshot')
const snapshot = snapshotData[0]
assert(typeof snapshot.hash === 'string', 'Snapshot should have string hash')
assert(typeof snapshot.snapshot === 'object', 'Snapshot should have snapshot object')
const { request: req, responses, timestamp } = snapshot.snapshot
assert.strictEqual(req.method, 'GET', 'Request method should be GET')
assert.strictEqual(req.url, `${origin}/test-endpoint`, 'Request URL should match')
assert.strictEqual(responses[0].statusCode, 200, 'Response status should be 200')
// Headers should be normalized to lowercase
assert(responses[0].headers['x-custom-header'], 'Custom header should be present')
assert.strictEqual(responses[0].headers['x-custom-header'], 'test-value', 'Custom header value should match')
assert(typeof timestamp === 'string', 'Timestamp should be a string')
})
it('maxSnapshots and LRU eviction', async (t) => {
const server = createTestServer((req, res) => {
res.writeHead(200, { 'content-type': 'text/plain' })
res.end(`Response for ${req.url}`)
})
const { origin } = await setupServer(server)
const snapshotPath = createSnapshotPath('lru-eviction')
setupCleanup(t, { server, snapshotPath })
const agent = new SnapshotAgent({
mode: 'record',
snapshotPath,
maxSnapshots: TEST_CONSTANTS.MAX_SNAPSHOTS_FOR_LRU
})
const originalDispatcher = getGlobalDispatcher()
setupCleanup(t, { agent, originalDispatcher })
setGlobalDispatcher(agent)
// Make 3 requests to trigger LRU eviction
await request(`${origin}/first`)
await request(`${origin}/second`)
await request(`${origin}/third`)
const recorder = agent.getRecorder()
// Should only have 2 snapshots due to LRU eviction
assert.strictEqual(recorder.size(), TEST_CONSTANTS.MAX_SNAPSHOTS_FOR_LRU,
`Should only keep ${TEST_CONSTANTS.MAX_SNAPSHOTS_FOR_LRU} snapshots due to LRU eviction`)
const snapshots = recorder.getSnapshots()
const urls = snapshots.map(s => s.request.url)
// First snapshot should be evicted, should have second and third
assert(urls.includes(`${origin}/second`), 'Should contain second request')
assert(urls.includes(`${origin}/third`), 'Should contain third request')
assert(!urls.includes(`${origin}/first`), 'Should not contain first request (evicted)')
})
it('auto-flush functionality', async (t) => {
const server = createTestServer((req, res) => {
res.writeHead(200, { 'content-type': 'text/plain' })
res.end('Auto-flush test')
})
const { origin } = await setupServer(server)
const snapshotPath = createSnapshotPath('auto-flush')
setupCleanup(t, { server, snapshotPath })
const agent = new SnapshotAgent({
mode: 'record',
snapshotPath,
autoFlush: true,
flushInterval: TEST_CONSTANTS.AUTO_FLUSH_INTERVAL
})
const originalDispatcher = getGlobalDispatcher()
setupCleanup(t, { agent, originalDispatcher })
setGlobalDispatcher(agent)
// Make a request
await request(`${origin}/autoflush-test`)
// Wait for auto-flush to trigger and ensure it completes
await new Promise(resolve => setTimeout(resolve, TEST_CONSTANTS.SEQUENTIAL_RESPONSE_DELAY))
// Force a final flush to ensure all data is written
await agent.saveSnapshots()
// Verify file was written automatically
const fileData = await readFile(snapshotPath, 'utf8')
const snapshots = JSON.parse(fileData)
assert(Array.isArray(snapshots), 'Auto-flushed data should be an array')
assert.strictEqual(snapshots.length, 1, 'Should contain exactly one auto-flushed snapshot')
assert.strictEqual(snapshots[0].snapshot.request.url, `${origin}/autoflush-test`,
'Auto-flushed snapshot should have correct URL')
})
})
describe('SnapshotAgent - Header Management', () => {
it('custom header matching with matchHeaders', async (t) => {
const server = createTestServer((req, res) => {
res.writeHead(200, {
'content-type': 'application/json',
'x-request-id': '12345',
authorization: 'Bearer secret-token'
})
res.end('{"message": "test"}')
})
const { origin } = await setupServer(server)
const snapshotPath = createSnapshotPath('match-headers')
setupCleanup(t, { server, snapshotPath })
const agent = new SnapshotAgent({
mode: 'record',
snapshotPath,
matchHeaders: ['content-type'] // Only match on content-type header
})
const originalDispatcher = getGlobalDispatcher()
setupCleanup(t, { agent, originalDispatcher })
setGlobalDispatcher(agent)
// Make first request with authorization header
await request(`${origin}/test`, {
headers: {
authorization: 'Bearer secret-token',
'content-type': 'application/json'
}
})
// Save snapshots before switching to playback
await agent.saveSnapshots()
// Make second request with different authorization but same content-type
// This should match the first request due to matchHeaders config
const playbackAgent = new SnapshotAgent({
mode: 'playback',
snapshotPath,
matchHeaders: ['content-type']
})
setupCleanup(t, { agent: playbackAgent })
setGlobalDispatcher(playbackAgent)
const response = await request(`${origin}/test`, {
headers: {
authorization: 'Bearer different-token', // Different auth token
'content-type': 'application/json' // Same content-type
}
})
assert.strictEqual(response.statusCode, 200, 'Should match despite different auth token')
const body = await response.body.text()
assert.strictEqual(body, '{"message": "test"}', 'Should return recorded response')
})
it('ignore headers functionality', async (t) => {
const server = createTestServer((req, res) => {
res.writeHead(200, { 'content-type': 'text/plain' })
res.end('ignore headers test')
})
const { origin } = await setupServer(server)
const snapshotPath = createSnapshotPath('ignore-headers')
setupCleanup(t, { server, snapshotPath })
const agent = new SnapshotAgent({
mode: 'record',
snapshotPath,
ignoreHeaders: ['authorization', 'x-request-id'] // Ignore these for matching
})
const originalDispatcher = getGlobalDispatcher()
setupCleanup(t, { agent, originalDispatcher })
setGlobalDispatcher(agent)
// Make first request
await request(`${origin}/test`, {
headers: {
authorization: 'Bearer token1',
'x-request-id': 'req-123',
'content-type': 'application/json'
}
})
// Save snapshots before switching to playback
await agent.saveSnapshots()
// Switch to playback mode and make request with different ignored headers
const playbackAgent = new SnapshotAgent({
mode: 'playback',
snapshotPath,
ignoreHeaders: ['authorization', 'x-request-id']
})
setupCleanup(t, { agent: playbackAgent })
setGlobalDispatcher(playbackAgent)
const response = await request(`${origin}/test`, {
headers: {
authorization: 'Bearer different-token', // Different (ignored)
'x-request-id': 'req-456', // Different (ignored)
'content-type': 'application/json' // Same (not ignored)
}
})
assert.strictEqual(response.statusCode, 200, 'Should match despite different ignored headers')
const body = await response.body.text()
assert.strictEqual(body, 'ignore headers test', 'Should return recorded response')
})
it('exclude headers for security', async (t) => {
const server = createTestServer((req, res) => {
res.writeHead(200, {
'content-type': 'application/json',
'set-cookie': 'session=secret123; HttpOnly',
authorization: 'Bearer server-token'
})
res.end('{"data": "sensitive"}')
})
const { origin } = await setupServer(server)
const snapshotPath = createSnapshotPath('exclude-headers')
setupCleanup(t, { server, snapshotPath })
const agent = new SnapshotAgent({
mode: 'record',
snapshotPath,
excludeHeaders: ['authorization', 'set-cookie'] // Don't store these sensitive headers
})
const originalDispatcher = getGlobalDispatcher()
setupCleanup(t, { agent, originalDispatcher })
setGlobalDispatcher(agent)
await request(`${origin}/test`)
await agent.saveSnapshots()
// Read snapshot file and verify sensitive headers are not stored
const fileData = await readFile(snapshotPath, 'utf8')
const snapshots = JSON.parse(fileData)
assert.strictEqual(snapshots.length, 1, 'Should contain exactly one snapshot')
const snapshot = snapshots[0].snapshot
// Verify excluded headers are not in stored response
assert(!snapshot.responses[0].headers.authorization, 'Authorization header should be excluded from storage')
assert(!snapshot.responses[0].headers['set-cookie'], 'Set-Cookie header should be excluded from storage')
assert(snapshot.responses[0].headers['content-type'], 'Content-Type header should be preserved')
})
})
describe('SnapshotAgent - Request Matching', () => {
it('query parameter matching control', async (t) => {
const server = createTestServer((req, res) => {
res.writeHead(200, { 'content-type': 'text/plain' })
res.end(`Response for ${req.url}`)
})
const { origin } = await setupServer(server)
const snapshotPath = createSnapshotPath('query-matching')
setupCleanup(t, { server, snapshotPath })
const agent = new SnapshotAgent({
mode: 'record',
snapshotPath,
matchQuery: false // Ignore query parameters in matching
})
const originalDispatcher = getGlobalDispatcher()
setupCleanup(t, { agent, originalDispatcher })
setGlobalDispatcher(agent)
// Record request with query parameters
await request(`${origin}/api/data?timestamp=123&session=abc`)
// Save snapshots before switching to playback
await agent.saveSnapshots()
// Switch to playback with different query parameters
const playbackAgent = new SnapshotAgent({
mode: 'playback',
snapshotPath,
matchQuery: false
})
setupCleanup(t, { agent: playbackAgent })
setGlobalDispatcher(playbackAgent)
// This should match the recorded request despite different query params
const response = await request(`${origin}/api/data?timestamp=456&session=xyz`)
assert.strictEqual(response.statusCode, 200, 'Should match despite different query parameters')
const body = await response.body.text()
assert.strictEqual(body, 'Response for /api/data?timestamp=123&session=abc',
'Should return original recorded response with original query params')
})
it('body matching control', async (t) => {
const server = createTestServer((req, res) => {
let body = ''
req.on('data', chunk => { body += chunk })
req.on('end', async (t) => {
res.writeHead(200, { 'content-type': 'application/json' })
res.end(`{"received": "${body}"}`)
})
})
const { origin } = await setupServer(server)
const snapshotPath = createSnapshotPath('body-matching')
setupCleanup(t, { server, snapshotPath })
const agent = new SnapshotAgent({
mode: 'record',
snapshotPath,
matchBody: false // Ignore request body in matching
})
const originalDispatcher = getGlobalDispatcher()
setupCleanup(t, { agent, originalDispatcher })
setGlobalDispatcher(agent)
// Record request with specific body
await request(`${origin}/api/submit`, {
method: 'POST',
body: 'original-data',
headers: { 'content-type': 'text/plain' }
})
// Save snapshots before switching to playback
await agent.saveSnapshots()
// Switch to playback with different body
const playbackAgent = new SnapshotAgent({
mode: 'playback',
snapshotPath,
matchBody: false
})
setupCleanup(t, { agent: playbackAgent })
setGlobalDispatcher(playbackAgent)
// This should match despite different body content
const response = await request(`${origin}/api/submit`, {
method: 'POST',
body: 'different-data',
headers: { 'content-type': 'text/plain' }
})
assert.strictEqual(response.statusCode, 200, 'Should match despite different request body')
const responseBody = await response.body.json()
assert.strictEqual(responseBody.received, 'original-data',
'Should return recorded response with original body')
})
})
describe('SnapshotAgent - Management Features', () => {
it('call count reset functionality', async (t) => {
const server = createTestServer((req, res) => {
res.writeHead(200, { 'content-type': 'text/plain' })
res.end('Test response')
})
const { origin } = await setupServer(server)
const snapshotPath = createSnapshotPath('reset-functionality')
setupCleanup(t, { server, snapshotPath })
const agent = new SnapshotAgent({
mode: 'record',
snapshotPath
})
const originalDispatcher = getGlobalDispatcher()
setupCleanup(t, { agent, originalDispatcher })
setGlobalDispatcher(agent)
// Record a snapshot
await request(`${origin}/api/test`)
await agent.saveSnapshots()
// Check initial call count
const info1 = agent.getSnapshotInfo({
origin,
path: '/api/test',
method: 'GET'
})
assert(info1, 'Should find snapshot info')
assert.strictEqual(info1.callCount, 0, 'Call count should be 0 initially (only incremented during findSnapshot)')
// Reset call counts
agent.resetCallCounts()
const info2 = agent.getSnapshotInfo({
origin,
path: '/api/test',
method: 'GET'
})
assert(info2, 'Should still find snapshot info after reset')
assert.strictEqual(info2.callCount, 0, 'Call count should remain 0 after reset')
})
it('snapshot management methods', async (t) => {
const server = createTestServer((req, res) => {
res.writeHead(200, { 'content-type': 'text/plain' })
res.end(`Response for ${req.url}`)
})
const { origin } = await setupServer(server)
const snapshotPath = createSnapshotPath('management-methods')
setupCleanup(t, { server, snapshotPath })
const agent = new SnapshotAgent({
mode: 'record',
snapshotPath
})
const originalDispatcher = getGlobalDispatcher()
setupCleanup(t, { agent, originalDispatcher })
setGlobalDispatcher(agent)
// Record multiple snapshots
await request(`${origin}/api/users`)
await request(`${origin}/api/posts`)
// Test getSnapshotInfo
const userInfo = agent.getSnapshotInfo({
origin,
path: '/api/users',
method: 'GET'
})
assert(userInfo, 'Should find user snapshot info')
assert.strictEqual(userInfo.request.method, 'GET', 'User snapshot method should be GET')
assert.strictEqual(userInfo.request.url, `${origin}/api/users`, 'User snapshot URL should match')
assert.strictEqual(userInfo.responseCount, 1, 'User snapshot should have one response')
// Test deleteSnapshot
const deleted = agent.deleteSnapshot({
origin,
path: '/api/users',
method: 'GET'
})
assert.strictEqual(deleted, true, 'Should successfully delete user snapshot')
// Verify deletion
const deletedInfo = agent.getSnapshotInfo({
origin,
path: '/api/users',
method: 'GET'
})
assert.strictEqual(deletedInfo, null, 'Deleted snapshot should not be found')
// Post snapshot should still exist
const postInfo = agent.getSnapshotInfo({
origin,
path: '/api/posts',
method: 'GET'
})
assert(postInfo, 'Post snapshot should still exist after deleting user snapshot')
// Test replaceSnapshots - create a snapshot with proper hash
const { createRequestHash, formatRequestKey, createHeaderFilters } = require('../lib/mock/snapshot-recorder')
const mockRequestOpts = {
origin,
path: '/api/mock',
method: 'GET'
}
const cachedSets = createHeaderFilters({})
const mockRequest = formatRequestKey(mockRequestOpts, cachedSets)
const mockHash = createRequestHash(mockRequest)
const mockData = [
{
hash: mockHash,
snapshot: {
request: mockRequest,
responses: [{ statusCode: 200, headers: {}, body: 'bW9jaw==', trailers: {} }],
callCount: 0,
timestamp: new Date().toISOString()
}
}
]
agent.replaceSnapshots(mockData)
// Should only have the mock snapshot now
const recorder = agent.getRecorder()
assert.strictEqual(recorder.size(), 1, 'Should have only one snapshot after replacement')
const mockInfo = agent.getSnapshotInfo(mockRequestOpts)
assert(mockInfo, 'Should find mock snapshot after replacement')
assert.strictEqual(mockInfo.request.url, `${origin}/api/mock`, 'Mock snapshot URL should match')
})
})
describe('SnapshotAgent - Filtering', () => {
it('shouldRecord filtering', async (t) => {
const server = createTestServer((req, res) => {
res.writeHead(200, { 'content-type': 'text/plain' })
res.end(`Response for ${req.url}`)
})
const { origin } = await setupServer(server)
const snapshotPath = createSnapshotPath('should-record-filter')
setupCleanup(t, { server, snapshotPath })
const agent = new SnapshotAgent({
mode: 'record',
snapshotPath,
shouldRecord: (requestOpts) => {
// Only record requests to /api/allowed
return requestOpts.path === '/api/allowed'
}
})
const originalDispatcher = getGlobalDispatcher()
setupCleanup(t, { agent, originalDispatcher })
setGlobalDispatcher(agent)
// Make requests - only one should be recorded
await request(`${origin}/api/allowed`)
await request(`${origin}/api/filtered`)
const recorder = agent.getRecorder()
assert.strictEqual(recorder.size(), 1, 'Should record only the allowed request')
const snapshots = recorder.getSnapshots()
assert.strictEqual(snapshots[0].request.url, `${origin}/api/allowed`,
'Recorded snapshot should be the allowed request')
})
it('shouldPlayback filtering', async (t) => {
const server = createTestServer((req, res) => {
res.writeHead(200, { 'content-type': 'text/plain' })
res.end(`Live response for ${req.url}`)
})
const { origin } = await setupServer(server)
const snapshotPath = createSnapshotPath('should-playback-filter')
setupCleanup(t, { server, snapshotPath })
// First, record some snapshots without filtering
const recordingAgent = new SnapshotAgent({
mode: 'record',
snapshotPath
})
const originalDispatcher = getGlobalDispatcher()
setupCleanup(t, { agent: recordingAgent, originalDispatcher })
setGlobalDispatcher(recordingAgent)
await request(`${origin}/api/cached`)
await request(`${origin}/api/live`)
await recordingAgent.saveSnapshots()
// Now test playback with filtering
const playbackAgent = new SnapshotAgent({
mode: 'playback',
snapshotPath,
shouldPlayback: (requestOpts) => {
// Only playback requests to /api/cached
return requestOpts.path === '/api/cached'
}
})
setupCleanup(t, { agent: playbackAgent })
setGlobalDispatcher(playbackAgent)
// This should use cached response
const cachedResponse = await request(`${origin}/api/cached`)
const cachedBody = await cachedResponse.body.text()
assert.strictEqual(cachedBody, 'Live response for /api/cached',
'Should return cached response for allowed path')
// This should fail because playback is filtered and no live server
// Need to close the recording server to ensure no fallback
server.close()
let errorThrown = false
try {
await request(`${origin}/api/live`)
} catch (error) {
errorThrown = true
assert.strictEqual(error.name, 'UndiciError', 'Should throw UndiciError for filtered request')
}
assert(errorThrown, 'Expected an error for filtered playback request')
})
it('URL exclusion patterns (string)', async (t) => {
const server = createTestServer((req, res) => {
res.writeHead(200, { 'content-type': 'text/plain' })
res.end(`Response for ${req.url}`)
})
const { origin } = await setupServer(server)
const snapshotPath = createSnapshotPath('url-exclusion-string')
setupCleanup(t, { server, snapshotPath })
const agent = new SnapshotAgent({
mode: 'record',
snapshotPath,
excludeUrls: ['/private', 'secret']
})
const originalDispatcher = getGlobalDispatcher()
setupCleanup(t, { agent, originalDispatcher })
setGlobalDispatcher(agent)
// Make requests - some should be excluded
await request(`${origin}/api/public`)
await request(`${origin}/private/data`)
await request(`${origin}/api/secret-endpoint`)
const recorder = agent.getRecorder()
assert.strictEqual(recorder.size(), 1, 'Should record only non-excluded requests')
const snapshots = recorder.getSnapshots()
assert.strictEqual(snapshots[0].request.url, `${origin}/api/public`,
'Should record only the public API request')
})
it('URL exclusion patterns (regex)', async (t) => {
const server = createTestServer((req, res) => {
res.writeHead(200, { 'content-type': 'text/plain' })
res.end(`Response for ${req.url}`)
})
const { origin } = await setupServer(server)
const snapshotPath = createSnapshotPath('url-exclusion-regex')
setupCleanup(t, { server, snapshotPath })
const agent = new SnapshotAgent({
mode: 'record',
snapshotPath,
excludeUrls: [/\/admin\/.*/, /.*\?token=.*/]
})
const originalDispatcher = getGlobalDispatcher()
setupCleanup(t, { agent, originalDispatcher })
setGlobalDispatcher(agent)
// Make requests - some should be excluded by regex patterns
await request(`${origin}/api/data`)
await request(`${origin}/admin/users`)
await request(`${origin}/api/auth?token=secret`)
const recorder = agent.getRecorder()
assert.strictEqual(recorder.size(), 1, 'Should record only requests not matching exclusion patterns')
const snapshots = recorder.getSnapshots()
assert.strictEqual(snapshots[0].request.url, `${origin}/api/data`,
'Should record only the non-excluded API request')
})
it('complex filtering scenarios', async (t) => {
const server = createTestServer((req, res) => {
res.writeHead(200, { 'content-type': 'text/plain' })
res.end(`Response for ${req.url}`)
})
const { origin } = await setupServer(server)
const snapshotPath = createSnapshotPath('complex-filtering')
setupCleanup(t, { server, snapshotPath })
const agent = new SnapshotAgent({
mode: 'record',
snapshotPath,
shouldRecord: (requestOpts) => {
// Only record GET requests
return (requestOpts.method || 'GET') === 'GET'
},
excludeUrls: ['/health']
})
const originalDispatcher = getGlobalDispatcher()
setupCleanup(t, { agent, originalDispatcher })
setGlobalDispatcher(agent)
// Make various requests with multiple filters
await request(`${origin}/api/users`) // Should record (GET, not excluded)
await request(`${origin}/health`) // Should not record (excluded URL)
await request(`${origin}/api/data`, { method: 'POST' }) // Should not record (POST method)
const recorder = agent.getRecorder()
assert.strictEqual(recorder.size(), 1, 'Should record only requests passing all filters')
const snapshots = recorder.getSnapshots()
assert.strictEqual(snapshots[0].request.url, `${origin}/api/users`,
'Should record only the allowed GET request')
assert.strictEqual(snapshots[0].request.method, 'GET',
'Recorded request should have GET method')
})
it('excluded URLs should not error in playback mode', async (t) => {
const server = createTestServer((req, res) => {
res.writeHead(200, { 'content-type': 'text/plain' })
res.end(`Response from ${req.url}`)
})
const { origin } = await setupServer(server)
const snapshotPath = createSnapshotPath('exclude-playback-bug')
setupCleanup(t, { server, snapshotPath })
// Record mode: record one request, exclude another
const recordingAgent = new SnapshotAgent({
mode: 'record',
snapshotPath,
excludeUrls: [`${origin}/excluded`]
})
const originalDispatcher = getGlobalDispatcher()
setupCleanup(t, { agent: recordingAgent, originalDispatcher })
setGlobalDispatcher(recordingAgent)
// Request to included endpoint - should be recorded
const res1 = await request(`${origin}/included`)
await res1.body.text()
// Request to excluded endpoint - should NOT be recorded
const res2 = await request(`${origin}/excluded`)
await res2.body.text()
await recordingAgent.saveSnapshots()
const recorder = recordingAgent.getRecorder()
assert.strictEqual(recorder.size(), 1, 'Should have recorded only the included request')
// Playback mode: should allow excluded URL to pass through without error
const playbackAgent = new SnapshotAgent({
mode: 'playback',
snapshotPath,
excludeUrls: [`${origin}/excluded`]
})
setupCleanup(t, { agent: playbackAgent })
setGlobalDispatcher(playbackAgent)
// This should work - replays from snapshot
const res3 = await request(`${origin}/included`)
await res3.body.text()
assert.strictEqual(res3.statusCode, 200, 'Included request should replay successfully')
// Excluded URL should pass through to real server
const res4 = await request(`${origin}/excluded`)
const body4 = await res4.body.text()
assert.strictEqual(res4.statusCode, 200, 'Excluded request should pass through to real server')
assert.strictEqual(body4, 'Response from /excluded', 'Should get live response from server')
})
})
describe('SnapshotAgent - Close Method', () => {
it('close() saves recordings before cleanup', async (t) => {
const snapshotPath = createSnapshotPath('close-saves')
setupCleanup(t, { snapshotPath })
const server = createTestServer(createDefaultHandler())
const { origin } = await setupServer(server)
setupCleanup(t, { server })
const agent = new SnapshotAgent({
mode: 'record',
snapshotPath,
autoFlush: false // Disable auto-flush to test manual save on close
})
const originalDispatcher = getGlobalDispatcher()
setupCleanup(t, { originalDispatcher })
setGlobalDispatcher(agent)
// Make a request that should be recorded
await request(`${origin}/test`)
// Verify snapshot is in memory but not yet saved to file
const recorder = agent.getRecorder()
assert.strictEqual(recorder.size(), 1, 'Should have recorded one snapshot in memory')
// Check that file doesn't exist yet (since autoFlush is false)
let fileExists = false
try {
await readFile(snapshotPath)
fileExists = true
} catch {
// File doesn't exist, which is expected
}
assert.strictEqual(fileExists, false, 'File should not exist before close()')
// Close the agent - this should save the snapshots
await agent.close()
// Verify the snapshots were saved to file
let savedData
try {
const fileContent = await readFile(snapshotPath, 'utf8')
savedData = JSON.parse(fileContent)
} catch (error) {
assert.fail(`Failed to read saved snapshot file: ${error.message}`)
}
assert(Array.isArray(savedData), 'Saved data should be an array')
assert.strictEqual(savedData.length, 1, 'Should have saved one snapshot')
assert.strictEqual(savedData[0].snapshot.request.method, 'GET', 'Saved snapshot should have correct method')
assert.strictEqual(savedData[0].snapshot.request.url, `${origin}/test`, 'Saved snapshot should have correct URL')
})
it('close() works when no recordings exist', async (t) => {
const snapshotPath = createSnapshotPath('close-no-recordings')
setupCleanup(t, { snapshotPath })
const agent = new SnapshotAgent({
mode: 'record',
snapshotPath
})
// Close agent immediately without making any requests or setting as dispatcher
await assert.doesNotReject(async () => {
await agent.close()
}, 'Should not throw when closing agent with no recordings')
// Verify no file was created
let fileExists = false
try {
await readFile(snapshotPath)
fileExists = true
} catch {
// File doesn't exist, which is expected
}
assert.strictEqual(fileExists, false, 'No file should be created when no recordings exist')
})
it('close() works when no snapshot path is configured', async (t) => {
const agent = new SnapshotAgent({
mode: 'record'
// No snapshotPath provided
})
const server = createTestServer(createDefaultHandler())
const { origin } = await setupServer(server)
setupCleanup(t, { server })
const originalDispatcher = getGlobalDispatcher()
t.after(() => setGlobalDispatcher(originalDispatcher))
setGlobalDispatcher(agent)
// Make a request
await request(`${origin}/test`)
// Close should not throw even without snapshot path
await assert.doesNotReject(async () => {
await agent.close()
}, 'Should not throw when closing agent without snapshot path')
})
it('recorder close() method works independently', async (t) => {
const { SnapshotRecorder } = require('../lib/mock/snapshot-recorder')
const snapshotPath = createSnapshotPath('recorder-close')
setupCleanup(t, { snapshotPath })
const recorder = new SnapshotRecorder({
snapshotPath,
mode: 'record'
})
// Manually add a snapshot to test saving
await recorder.record(
{ origin: 'http://test.com', path: '/api', method: 'GET' },
{
statusCode: 200,
headers: { 'content-type': 'application/json' },
body: Buffer.from('{"test": true}'),
trailers: {}
}
)
assert.strictEqual(recorder.size(), 1, 'Should have one recorded snapshot')
// Close the recorder
await recorder.close()
// Verify the snapshot was saved
let savedData
try {
const fileContent = await readFile(snapshotPath, 'utf8')
savedData = JSON.parse(fileContent)
} catch (error) {
assert.fail(`Failed to read saved snapshot file: ${error.message}`)
}
assert(Array.isArray(savedData), 'Saved data should be an array')
assert.strictEqual(savedData.length, 1, 'Should have saved one snapshot')
assert.strictEqual(savedData[0].snapshot.request.method, 'GET', 'Should have correct method')
})
})
================================================
FILE: test/socket-back-pressure.js
================================================
'use strict'
const { tspl } = require('@matteo.collina/tspl')
const { once } = require('node:events')
const { Client } = require('..')
const { createServer } = require('node:http')
const { Readable } = require('node:stream')
const { test, after } = require('node:test')
test('socket back-pressure', async (t) => {
t = tspl(t, { plan: 3 })
const server = createServer({ joinDuplicateHeaders: true })
let bytesWritten = 0
const buf = Buffer.allocUnsafe(16384)
const src = new Readable({
read () {
bytesWritten += buf.length
this.push(buf)
if (bytesWritten >= 1e6) {
this.push(null)
}
}
})
server.on('request', (req, res) => {
src.pipe(res)
})
after(() => server.close())
server.listen(0)
await once(server, 'listening')
const client = new Client(`http://localhost:${server.address().port}`, {
pipelining: 1
})
after(() => client.close())
client.on('disconnect', () => {
if (!client.closed && !client.destroyed) {
t.fail('unexpected disconnect')
}
})
client.request({ path: '/', method: 'GET', opaque: 'asd' }, (err, data) => {
t.ifError(err)
data.body
.resume()
.once('data', () => {
data.body.pause()
// TODO: Try to avoid timeout.
setTimeout(() => {
t.ok(data.body._readableState.length < bytesWritten - data.body._readableState.highWaterMark)
src.push(null)
data.body.resume()
}, 1e3)
})
.on('end', () => {
t.ok(true, 'pass')
})
})
await t.completed
})
================================================
FILE: test/socket-timeout.js
================================================
'use strict'
const { tspl } = require('@matteo.collina/tspl')
const { test, after } = require('node:test')
const { Client, errors } = require('..')
const { createServer } = require('node:http')
const FakeTimers = require('@sinonjs/fake-timers')
test('timeout with pipelining 1', async (t) => {
t = tspl(t, { plan: 9 })
const server = createServer({ joinDuplicateHeaders: true })
server.once('request', (req, res) => {
t.ok(true, 'first request received, we are letting this timeout on the client')
server.once('request', (req, res) => {
t.strictEqual('/', req.url)
t.strictEqual('GET', req.method)
res.setHeader('content-type', 'text/plain')
res.end('hello')
})
})
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`, {
pipelining: 1,
headersTimeout: 500,
bodyTimeout: 500
})
after(() => client.close())
client.request({
path: '/',
method: 'GET',
opaque: 'asd'
}, (err, data) => {
t.ok(err instanceof errors.HeadersTimeoutError) // we are expecting an error
t.strictEqual(data.opaque, 'asd')
})
client.request({
path: '/',
method: 'GET'
}, (err, { statusCode, headers, body }) => {
t.ifError(err)
t.strictEqual(statusCode, 200)
t.strictEqual(headers['content-type'], 'text/plain')
const bufs = []
body.on('data', (buf) => {
bufs.push(buf)
})
body.on('end', () => {
t.strictEqual('hello', Buffer.concat(bufs).toString('utf8'))
})
})
})
await t.completed
})
test('Disable socket timeout', async (t) => {
t = tspl(t, { plan: 2 })
const server = createServer({ joinDuplicateHeaders: true })
const clock = FakeTimers.install()
after(clock.uninstall.bind(clock))
server.once('request', (req, res) => {
setTimeout(() => {
res.end('hello')
}, 31e3)
clock.tick(32e3)
})
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`, {
bodyTimeout: 0,
headersTimeout: 0
})
after(() => client.close())
client.request({ path: '/', method: 'GET' }, (err, result) => {
t.ifError(err)
const bufs = []
result.body.on('data', (buf) => {
bufs.push(buf)
})
result.body.on('end', () => {
t.strictEqual('hello', Buffer.concat(bufs).toString('utf8'))
})
})
})
await t.completed
})
================================================
FILE: test/socks5-client.js
================================================
'use strict'
const { tspl } = require('@matteo.collina/tspl')
const { test } = require('node:test')
const net = require('node:net')
const { Socks5Client, STATES, AUTH_METHODS, REPLY_CODES } = require('../lib/core/socks5-client')
const { InvalidArgumentError, Socks5ProxyError } = require('../lib/core/errors')
test('Socks5Client - constructor validation', async (t) => {
const p = tspl(t, { plan: 1 })
p.throws(() => {
// eslint-disable-next-line no-new
new Socks5Client()
}, InvalidArgumentError, 'should throw when socket is not provided')
await p.completed
})
test('Socks5Client - handshake flow', async (t) => {
const p = tspl(t, { plan: 6 })
// Create a mock SOCKS5 server
const server = net.createServer((socket) => {
socket.on('data', (data) => {
// First message should be handshake
if (data[0] === 0x05 && data.length === 3) {
p.equal(data[0], 0x05, 'should send SOCKS version 5')
p.equal(data[1], 1, 'should send 1 auth method')
p.equal(data[2], AUTH_METHODS.NO_AUTH, 'should send NO_AUTH method')
// Send response accepting NO_AUTH
socket.write(Buffer.from([0x05, AUTH_METHODS.NO_AUTH]))
}
})
})
await new Promise((resolve) => {
server.listen(0, '127.0.0.1', resolve)
})
const { port } = server.address()
const socket = net.connect(port, '127.0.0.1')
await new Promise((resolve) => {
socket.on('connect', resolve)
})
const client = new Socks5Client(socket)
p.equal(client.state, STATES.INITIAL, 'should start in INITIAL state')
client.on('authenticated', () => {
p.equal(client.state, STATES.HANDSHAKING, 'should be in HANDSHAKING state after auth')
p.ok(true, 'should emit authenticated event')
})
await client.handshake()
// Wait for the authenticated event
await new Promise((resolve) => {
if (client.state !== STATES.HANDSHAKING) {
resolve()
} else {
client.once('authenticated', resolve)
}
})
socket.destroy()
server.close()
await p.completed
})
test('Socks5Client - username/password authentication', async (t) => {
const p = tspl(t, { plan: 7 })
const testUsername = 'testuser'
const testPassword = 'testpass'
// Create a mock SOCKS5 server with auth
const server = net.createServer((socket) => {
let stage = 'handshake'
socket.on('data', (data) => {
if (stage === 'handshake' && data[0] === 0x05) {
p.equal(data[0], 0x05, 'should send SOCKS version 5')
p.equal(data[1], 2, 'should send 2 auth methods')
p.equal(data[2], AUTH_METHODS.USERNAME_PASSWORD, 'should send USERNAME_PASSWORD first')
p.equal(data[3], AUTH_METHODS.NO_AUTH, 'should send NO_AUTH second')
// Send response selecting USERNAME_PASSWORD
socket.write(Buffer.from([0x05, AUTH_METHODS.USERNAME_PASSWORD]))
stage = 'auth'
} else if (stage === 'auth') {
// Parse username/password auth request
p.equal(data[0], 0x01, 'should send auth version 1')
const usernameLen = data[1]
const username = data.subarray(2, 2 + usernameLen).toString()
p.equal(username, testUsername, 'should send correct username')
const passwordLen = data[2 + usernameLen]
const password = data.subarray(3 + usernameLen, 3 + usernameLen + passwordLen).toString()
p.equal(password, testPassword, 'should send correct password')
// Send auth success response
socket.write(Buffer.from([0x01, 0x00]))
}
})
})
await new Promise((resolve) => {
server.listen(0, '127.0.0.1', resolve)
})
const { port } = server.address()
const socket = net.connect(port, '127.0.0.1')
await new Promise((resolve) => {
socket.on('connect', resolve)
})
const client = new Socks5Client(socket, {
username: testUsername,
password: testPassword
})
client.on('authenticated', () => {
// Test passed
})
await client.handshake()
// Wait for the authenticated event
await new Promise((resolve) => {
client.once('authenticated', resolve)
})
socket.destroy()
server.close()
await p.completed
})
test('Socks5Client - connect command', async (t) => {
const p = tspl(t, { plan: 8 })
const targetHost = 'example.com'
const targetPort = 80
// Create a mock SOCKS5 server
const server = net.createServer((socket) => {
let stage = 'handshake'
socket.on('data', (data) => {
if (stage === 'handshake' && data[0] === 0x05) {
// Send NO_AUTH response
socket.write(Buffer.from([0x05, AUTH_METHODS.NO_AUTH]))
stage = 'connect'
} else if (stage === 'connect') {
// Parse CONNECT request
p.equal(data[0], 0x05, 'should send SOCKS version 5')
p.equal(data[1], 0x01, 'should send CONNECT command')
p.equal(data[2], 0x00, 'should send reserved byte')
p.equal(data[3], 0x03, 'should send domain address type')
const domainLen = data[4]
const domain = data.subarray(5, 5 + domainLen).toString()
p.equal(domain, targetHost, 'should send correct domain')
const port = data.readUInt16BE(5 + domainLen)
p.equal(port, targetPort, 'should send correct port')
// Send success response with bound address
const response = Buffer.from([
0x05, // Version
REPLY_CODES.SUCCEEDED, // Success
0x00, // Reserved
0x01, // IPv4 address type
127, 0, 0, 1, // Bound address
0x00, 0x50 // Bound port (80)
])
socket.write(response)
}
})
})
await new Promise((resolve) => {
server.listen(0, '127.0.0.1', resolve)
})
const { port } = server.address()
const socket = net.connect(port, '127.0.0.1')
await new Promise((resolve) => {
socket.on('connect', resolve)
})
const client = new Socks5Client(socket)
client.on('authenticated', async () => {
await client.connect(targetHost, targetPort)
})
client.on('connected', (info) => {
p.equal(info.address, '127.0.0.1', 'should return bound address')
p.equal(info.port, 80, 'should return bound port')
})
await client.handshake()
// Wait for the connected event
await new Promise((resolve) => {
client.once('connected', resolve)
})
socket.destroy()
server.close()
await p.completed
})
test('Socks5Client - authentication failure', async (t) => {
const p = tspl(t, { plan: 3 })
// Create a mock SOCKS5 server
const server = net.createServer((socket) => {
socket.on('data', (data) => {
if (data[0] === 0x05) {
// Send NO_ACCEPTABLE response
socket.write(Buffer.from([0x05, AUTH_METHODS.NO_ACCEPTABLE]))
}
})
})
await new Promise((resolve) => {
server.listen(0, '127.0.0.1', resolve)
})
const { port } = server.address()
const socket = net.connect(port, '127.0.0.1')
await new Promise((resolve) => {
socket.on('connect', resolve)
})
const client = new Socks5Client(socket)
client.on('error', (err) => {
p.ok(err instanceof Socks5ProxyError, 'should emit Socks5ProxyError')
p.equal(err.code, 'UND_ERR_SOCKS5_AUTH_REJECTED', 'should have correct error code')
p.equal(err.message, 'No acceptable authentication method', 'should have correct error message')
})
await client.handshake()
// Wait for the error event
await new Promise((resolve) => {
client.once('error', resolve)
})
socket.destroy()
server.close()
await p.completed
})
test('Socks5Client - connection refused', async (t) => {
const p = tspl(t, { plan: 3 })
// Create a mock SOCKS5 server
const server = net.createServer((socket) => {
let stage = 'handshake'
socket.on('data', (data) => {
if (stage === 'handshake' && data[0] === 0x05) {
// Send NO_AUTH response
socket.write(Buffer.from([0x05, AUTH_METHODS.NO_AUTH]))
stage = 'connect'
} else if (stage === 'connect') {
// Send connection refused response
const response = Buffer.from([
0x05, // Version
REPLY_CODES.CONNECTION_REFUSED, // Connection refused
0x00, // Reserved
0x01, // IPv4 address type
0, 0, 0, 0, // Bound address
0x00, 0x00 // Bound port
])
socket.write(response)
}
})
})
await new Promise((resolve) => {
server.listen(0, '127.0.0.1', resolve)
})
const { port } = server.address()
const socket = net.connect(port, '127.0.0.1')
await new Promise((resolve) => {
socket.on('connect', resolve)
})
const client = new Socks5Client(socket)
client.on('authenticated', () => {
client.connect('example.com', 80)
})
client.on('error', (err) => {
p.ok(err instanceof Socks5ProxyError, 'should throw Socks5ProxyError')
p.equal(err.code, 'UND_ERR_SOCKS5_REPLY_5', 'should have correct error code')
p.match(err.message, /Connection refused/, 'should have correct error message')
})
await client.handshake()
// Wait for the error event
await new Promise((resolve) => {
client.once('error', resolve)
})
socket.destroy()
server.close()
await p.completed
})
================================================
FILE: test/socks5-proxy-agent.js
================================================
'use strict'
const { tspl } = require('@matteo.collina/tspl')
const { test } = require('node:test')
const { request } = require('..')
const { InvalidArgumentError } = require('../lib/core/errors')
const Socks5ProxyAgent = require('../lib/dispatcher/socks5-proxy-agent')
const { createServer } = require('node:http')
const { TestSocks5Server } = require('./fixtures/socks5-test-server')
test('Socks5ProxyAgent - constructor validation', async (t) => {
const p = tspl(t, { plan: 4 })
p.throws(() => {
// eslint-disable-next-line no-new
new Socks5ProxyAgent()
}, InvalidArgumentError, 'should throw when proxy URL is not provided')
p.throws(() => {
// eslint-disable-next-line no-new
new Socks5ProxyAgent('http://localhost:1080')
}, InvalidArgumentError, 'should throw when proxy URL protocol is not socks5')
p.doesNotThrow(() => {
// eslint-disable-next-line no-new
new Socks5ProxyAgent('socks5://localhost:1080')
}, 'should accept socks5:// URLs')
p.doesNotThrow(() => {
// eslint-disable-next-line no-new
new Socks5ProxyAgent('socks://localhost:1080')
}, 'should accept socks:// URLs for compatibility')
await p.completed
})
test('Socks5ProxyAgent - basic HTTP connection', async (t) => {
const p = tspl(t, { plan: 2 })
// Create target HTTP server
const server = createServer((req, res) => {
res.writeHead(200, { 'content-type': 'application/json' })
res.end(JSON.stringify({ message: 'Hello from target server', path: req.url }))
})
// Start target server
await new Promise((resolve) => {
server.listen(0, resolve)
})
const serverPort = server.address().port
// Create SOCKS5 proxy server
const socksServer = new TestSocks5Server()
const socksAddress = await socksServer.listen()
try {
// Create Socks5ProxyAgent
const proxyWrapper = new Socks5ProxyAgent(`socks5://localhost:${socksAddress.port}`)
// Make request through SOCKS5 proxy
const response = await request(`http://localhost:${serverPort}/test`, {
dispatcher: proxyWrapper
})
p.equal(response.statusCode, 200, 'should get 200 status code')
const body = await response.body.json()
p.deepEqual(body, {
message: 'Hello from target server',
path: '/test'
}, 'should get correct response body')
} finally {
await socksServer.close()
server.close()
}
await p.completed
})
test.skip('Socks5ProxyAgent - HTTPS connection', async (t) => {
// Skip HTTPS test for now - TLS option passing needs additional work
t.skip('HTTPS test requires TLS option refinement')
})
test('Socks5ProxyAgent - with authentication', async (t) => {
const p = tspl(t, { plan: 2 })
// Create target HTTP server
const server = createServer((req, res) => {
res.writeHead(200, { 'content-type': 'application/json' })
res.end(JSON.stringify({ message: 'Authenticated request successful' }))
})
// Start target server
await new Promise((resolve) => {
server.listen(0, resolve)
})
const serverPort = server.address().port
// Create SOCKS5 proxy server with auth
const socksServer = new TestSocks5Server({
requireAuth: true,
credentials: { username: 'testuser', password: 'testpass' }
})
const socksAddress = await socksServer.listen()
try {
// Create Socks5ProxyAgent with auth
const proxyWrapper = new Socks5ProxyAgent(`socks5://testuser:testpass@localhost:${socksAddress.port}`)
// Make request through SOCKS5 proxy
const response = await request(`http://localhost:${serverPort}/auth-test`, {
dispatcher: proxyWrapper
})
p.equal(response.statusCode, 200, 'should get 200 status code')
const body = await response.body.json()
p.deepEqual(body, {
message: 'Authenticated request successful'
}, 'should get correct response body')
} finally {
await socksServer.close()
server.close()
}
await p.completed
})
test('Socks5ProxyAgent - authentication with options', async (t) => {
const p = tspl(t, { plan: 2 })
// Create target HTTP server
const server = createServer((req, res) => {
res.writeHead(200, { 'content-type': 'application/json' })
res.end(JSON.stringify({ message: 'Options auth successful' }))
})
// Start target server
await new Promise((resolve) => {
server.listen(0, resolve)
})
const serverPort = server.address().port
// Create SOCKS5 proxy server with auth
const socksServer = new TestSocks5Server({
requireAuth: true,
credentials: { username: 'optuser', password: 'optpass' }
})
const socksAddress = await socksServer.listen()
try {
// Create Socks5ProxyAgent with auth in options
const proxyWrapper = new Socks5ProxyAgent(`socks5://localhost:${socksAddress.port}`, {
username: 'optuser',
password: 'optpass'
})
// Make request through SOCKS5 proxy
const response = await request(`http://localhost:${serverPort}/options-auth`, {
dispatcher: proxyWrapper
})
p.equal(response.statusCode, 200, 'should get 200 status code')
const body = await response.body.json()
p.deepEqual(body, {
message: 'Options auth successful'
}, 'should get correct response body')
} finally {
await socksServer.close()
server.close()
}
await p.completed
})
test('Socks5ProxyAgent - multiple requests through same proxy', async (t) => {
const p = tspl(t, { plan: 4 })
// Create target HTTP server
let requestCount = 0
const server = createServer((req, res) => {
requestCount++
res.writeHead(200, { 'content-type': 'application/json' })
res.end(JSON.stringify({ message: `Request ${requestCount}`, path: req.url }))
})
// Start target server
await new Promise((resolve) => {
server.listen(0, resolve)
})
const serverPort = server.address().port
// Create SOCKS5 proxy server
const socksServer = new TestSocks5Server()
const socksAddress = await socksServer.listen()
try {
// Create Socks5ProxyAgent
const proxyWrapper = new Socks5ProxyAgent(`socks5://localhost:${socksAddress.port}`)
// Make first request
const response1 = await request(`http://localhost:${serverPort}/request1`, {
dispatcher: proxyWrapper
})
p.equal(response1.statusCode, 200, 'should get 200 status code for first request')
const body1 = await response1.body.json()
p.deepEqual(body1, { message: 'Request 1', path: '/request1' }, 'should get correct response body for first request')
// Make second request through same proxy
const response2 = await request(`http://localhost:${serverPort}/request2`, {
dispatcher: proxyWrapper
})
p.equal(response2.statusCode, 200, 'should get 200 status code for second request')
const body2 = await response2.body.json()
p.deepEqual(body2, { message: 'Request 2', path: '/request2' }, 'should get correct response body for second request')
} finally {
await socksServer.close()
server.close()
}
await p.completed
})
test('Socks5ProxyAgent - connection failure', async (t) => {
const p = tspl(t, { plan: 1 })
// Create Socks5ProxyAgent pointing to non-existent proxy
const proxyWrapper = new Socks5ProxyAgent('socks5://localhost:9999')
try {
await request('http://example.com/', {
dispatcher: proxyWrapper
})
p.fail('should have thrown an error')
} catch (err) {
p.ok(err, 'should throw error when SOCKS5 proxy is not available')
}
await p.completed
})
test('Socks5ProxyAgent - proxy connection refused', async (t) => {
const p = tspl(t, { plan: 1 })
// Create target HTTP server
const server = createServer((req, res) => {
res.writeHead(200)
res.end('OK')
})
await new Promise((resolve) => {
server.listen(0, resolve)
})
const serverPort = server.address().port
// Create SOCKS5 proxy server that simulates connection failure
const socksServer = new TestSocks5Server({ simulateFailure: true })
const socksAddress = await socksServer.listen()
try {
const proxyWrapper = new Socks5ProxyAgent(`socks5://localhost:${socksAddress.port}`)
await request(`http://localhost:${serverPort}/`, {
dispatcher: proxyWrapper
})
p.fail('should have thrown an error')
} catch (err) {
p.ok(err, 'should throw error when SOCKS5 proxy refuses connection')
} finally {
await socksServer.close()
server.close()
}
await p.completed
})
test('Socks5ProxyAgent - close and destroy', async (t) => {
const p = tspl(t, { plan: 2 })
const proxyWrapper = new Socks5ProxyAgent('socks5://localhost:1080')
// Test close
await proxyWrapper.close()
p.ok(true, 'should close without error')
// Test destroy
await proxyWrapper.destroy()
p.ok(true, 'should destroy without error')
await p.completed
})
test('Socks5ProxyAgent - URL parsing edge cases', async (t) => {
const p = tspl(t, { plan: 3 })
// Test with URL object
const url = new URL('socks5://user:pass@proxy.example.com:1080')
p.doesNotThrow(() => {
// eslint-disable-next-line no-new
new Socks5ProxyAgent(url)
}, 'should accept URL object')
// Test with encoded credentials
p.doesNotThrow(() => {
// eslint-disable-next-line no-new
new Socks5ProxyAgent('socks5://user%40domain:p%40ss@localhost:1080')
}, 'should handle URL-encoded credentials')
// Test default port
p.doesNotThrow(() => {
// eslint-disable-next-line no-new
new Socks5ProxyAgent('socks5://localhost')
}, 'should use default port 1080')
await p.completed
})
================================================
FILE: test/socks5-utils.js
================================================
'use strict'
const { tspl } = require('@matteo.collina/tspl')
const { test } = require('node:test')
const {
parseAddress,
parseIPv6,
buildAddressBuffer,
parseResponseAddress,
createReplyError
} = require('../lib/core/socks5-utils')
const { InvalidArgumentError } = require('../lib/core/errors')
test('parseAddress - IPv4', async (t) => {
const p = tspl(t, { plan: 3 })
const result = parseAddress('192.168.1.1')
p.equal(result.type, 0x01, 'should return IPv4 type')
p.equal(result.buffer.length, 4, 'should return 4-byte buffer')
p.deepEqual(Array.from(result.buffer), [192, 168, 1, 1], 'should parse IPv4 correctly')
await p.completed
})
test('parseAddress - IPv6', async (t) => {
const p = tspl(t, { plan: 2 })
const result = parseAddress('2001:db8::1')
p.equal(result.type, 0x04, 'should return IPv6 type')
p.equal(result.buffer.length, 16, 'should return 16-byte buffer')
await p.completed
})
test('parseAddress - Domain', async (t) => {
const p = tspl(t, { plan: 4 })
const result = parseAddress('example.com')
p.equal(result.type, 0x03, 'should return domain type')
p.equal(result.buffer[0], 11, 'should have correct length byte')
p.equal(result.buffer.subarray(1).toString(), 'example.com', 'should contain domain name')
// Test domain too long
const longDomain = 'a'.repeat(256)
p.throws(() => parseAddress(longDomain), InvalidArgumentError, 'should throw for domain > 255 bytes')
await p.completed
})
test('parseIPv6', async (t) => {
const p = tspl(t, { plan: 3 })
// Test full IPv6
const buffer1 = parseIPv6('2001:0db8:0000:0042:0000:8a2e:0370:7334')
p.equal(buffer1.length, 16, 'should return 16-byte buffer')
// Test compressed IPv6
const buffer2 = parseIPv6('2001:db8::1')
p.equal(buffer2.length, 16, 'should return 16-byte buffer for compressed')
// Test loopback
const buffer3 = parseIPv6('::1')
p.equal(buffer3.length, 16, 'should return 16-byte buffer for loopback')
await p.completed
})
test('buildAddressBuffer', async (t) => {
const p = tspl(t, { plan: 5 })
// IPv4 address
const ipv4Buffer = buildAddressBuffer(0x01, Buffer.from([192, 168, 1, 1]), 80)
p.equal(ipv4Buffer[0], 0x01, 'should have IPv4 type')
p.deepEqual(Array.from(ipv4Buffer.subarray(1, 5)), [192, 168, 1, 1], 'should have IPv4 address')
p.equal(ipv4Buffer.readUInt16BE(5), 80, 'should have correct port')
// Domain address
const domainBuffer = Buffer.concat([Buffer.from([11]), Buffer.from('example.com')])
const result = buildAddressBuffer(0x03, domainBuffer, 443)
p.equal(result[0], 0x03, 'should have domain type')
p.equal(result.readUInt16BE(result.length - 2), 443, 'should have correct port')
await p.completed
})
test('parseResponseAddress - IPv4', async (t) => {
const p = tspl(t, { plan: 4 })
const buffer = Buffer.from([
0x01, // IPv4 type
192, 168, 1, 1, // IP address
0x00, 0x50 // Port 80
])
const result = parseResponseAddress(buffer)
p.equal(result.address, '192.168.1.1', 'should parse IPv4 address')
p.equal(result.port, 80, 'should parse port')
p.equal(result.bytesRead, 7, 'should read 7 bytes')
// Test with offset
const bufferWithOffset = Buffer.concat([Buffer.from([0, 0]), buffer])
const resultWithOffset = parseResponseAddress(bufferWithOffset, 2)
p.equal(resultWithOffset.address, '192.168.1.1', 'should parse with offset')
await p.completed
})
test('parseResponseAddress - Domain', async (t) => {
const p = tspl(t, { plan: 3 })
const buffer = Buffer.from([
0x03, // Domain type
11, // Length
...Buffer.from('example.com'),
0x01, 0xBB // Port 443
])
const result = parseResponseAddress(buffer)
p.equal(result.address, 'example.com', 'should parse domain')
p.equal(result.port, 443, 'should parse port')
p.equal(result.bytesRead, 15, 'should read correct bytes')
await p.completed
})
test('parseResponseAddress - IPv6', async (t) => {
const p = tspl(t, { plan: 3 })
const buffer = Buffer.alloc(19)
buffer[0] = 0x04 // IPv6 type
// Simple IPv6 address (all zeros except last byte)
buffer[17] = 1
buffer[17] = 0x00
buffer[18] = 0x50 // Port 80
const result = parseResponseAddress(buffer)
p.match(result.address, /:/, 'should return IPv6 format')
p.equal(result.port, 80, 'should parse port')
p.equal(result.bytesRead, 19, 'should read 19 bytes')
await p.completed
})
test('parseResponseAddress - errors', async (t) => {
const p = tspl(t, { plan: 5 })
// Buffer too small for type
p.throws(() => parseResponseAddress(Buffer.alloc(0)), InvalidArgumentError)
// Buffer too small for IPv4
p.throws(() => parseResponseAddress(Buffer.from([0x01, 192])), InvalidArgumentError)
// Buffer too small for domain length
p.throws(() => parseResponseAddress(Buffer.from([0x03])), InvalidArgumentError)
// Buffer too small for domain
p.throws(() => parseResponseAddress(Buffer.from([0x03, 10, 65])), InvalidArgumentError)
// Invalid address type
p.throws(() => parseResponseAddress(Buffer.from([0x99, 0, 0, 0, 0, 0, 0])), InvalidArgumentError)
await p.completed
})
test('createReplyError', async (t) => {
const p = tspl(t, { plan: 6 })
const err1 = createReplyError(0x01)
p.equal(err1.message, 'General SOCKS server failure')
p.equal(err1.code, 'SOCKS5_1')
const err2 = createReplyError(0x05)
p.equal(err2.message, 'Connection refused')
p.equal(err2.code, 'SOCKS5_5')
const err3 = createReplyError(0x99)
p.equal(err3.message, 'Unknown SOCKS5 error code: 153')
p.equal(err3.code, 'SOCKS5_153')
await p.completed
})
================================================
FILE: test/stream-compat.js
================================================
'use strict'
const { tspl } = require('@matteo.collina/tspl')
const { test, after } = require('node:test')
const { Client } = require('..')
const { createServer } = require('node:http')
const { Readable } = require('node:stream')
const EE = require('node:events')
test('stream body without destroy', async (t) => {
t = tspl(t, { plan: 2 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.end()
})
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.destroy())
const signal = new EE()
const body = new Readable({ read () {} })
body.destroy = undefined
body.on('error', (err) => {
t.ok(err)
})
client.request({
path: '/',
method: 'PUT',
signal,
body
}, (err, data) => {
t.ok(err)
})
signal.emit('abort')
})
await t.completed
})
test('IncomingMessage', async (t) => {
t = tspl(t, { plan: 2 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.end()
})
after(() => server.close())
server.listen(0, () => {
const proxyClient = new Client(`http://localhost:${server.address().port}`)
after(() => proxyClient.destroy())
proxyClient.on('disconnect', () => {
if (!proxyClient.closed && !proxyClient.destroyed) {
t.fail('unexpected disconnect')
}
})
const proxy = createServer({ joinDuplicateHeaders: true }, (req, res) => {
proxyClient.request({
path: '/',
method: 'PUT',
body: req
}, (err, data) => {
t.ifError(err)
data.body.pipe(res)
})
})
after(() => proxy.close())
proxy.listen(0, () => {
const client = new Client(`http://localhost:${proxy.address().port}`)
after(() => client.destroy())
client.on('disconnect', () => {
if (!client.closed && !client.destroyed) {
t.fail('unexpected disconnect')
}
})
client.request({
path: '/',
method: 'PUT',
body: 'hello world'
}, (err, data) => {
t.ifError(err)
})
})
})
await t.completed
})
================================================
FILE: test/subresource-integrity/apply-algorithm-to-bytes.js
================================================
'use strict'
const { test, describe } = require('node:test')
const { applyAlgorithmToBytes } = require('../../lib/web/subresource-integrity/subresource-integrity')
const { runtimeFeatures } = require('../../lib/util/runtime-features')
const skip = runtimeFeatures.has('crypto') === false
describe('applyAlgorithmToBytes', () => {
/* Hash values generated with for "Hello world!" */
const hash256 = 'wFNeS+K3n/2TKRMFQ2v4iTFOSj+uwF7P/Lt98xrZ5Ro='
const hash384 = 'hiVfosNuSzCWnq4X3DTHcsvr38WLWEA5AL6HYU6xo0uHgCY/JV615lypu7hkHMz+'
const hash512 = '9s3ioPgZMUzd5V/CJ9jX2uPSjMVWIioKitZtkcytSq1glPUXohgjYMmqz2o9wyMWLLb9jN/+2w/gOPVehf+1tg=='
test('valid sha256', { skip }, (t) => {
const result = applyAlgorithmToBytes('sha256', Buffer.from('Hello world!'))
t.assert.strictEqual(result, hash256)
})
test('valid sha384', { skip }, (t) => {
const result = applyAlgorithmToBytes('sha384', Buffer.from('Hello world!'))
t.assert.strictEqual(result, hash384)
})
test('valid sha512', { skip }, (t) => {
const result = applyAlgorithmToBytes('sha512', Buffer.from('Hello world!'))
t.assert.strictEqual(result, hash512)
})
})
================================================
FILE: test/subresource-integrity/bytes-match.js
================================================
'use strict'
const { test, describe } = require('node:test')
const { bytesMatch } = require('../../lib/web/subresource-integrity/subresource-integrity')
const { runtimeFeatures } = require('../../lib/util/runtime-features')
const skip = runtimeFeatures.has('crypto') === false
describe('bytesMatch', () => {
test('valid sha256 and base64', { skip }, (t) => {
const data = Buffer.from('Hello world!')
const hash256 = 'sha256-wFNeS+K3n/2TKRMFQ2v4iTFOSj+uwF7P/Lt98xrZ5Ro='
t.assert.ok(bytesMatch(data, hash256))
})
})
================================================
FILE: test/subresource-integrity/case-sensitive-match.js
================================================
'use strict'
const { test, describe } = require('node:test')
const { caseSensitiveMatch } = require('../../lib/web/subresource-integrity/subresource-integrity')
describe('caseSensitiveMatch', () => {
test('identical strings', (t) => {
const actualValue = 'ypeBEsobvcr6wjGzmiPcTaeG7/gUfE5yuYB3ha/uSLs'
const expectedValue = 'ypeBEsobvcr6wjGzmiPcTaeG7/gUfE5yuYB3ha/uSLs'
t.assert.ok(caseSensitiveMatch(actualValue, expectedValue))
})
test('identical strings, actualValue has one padding char', (t) => {
const actualValue = 'ypeBEsobvcr6wjGzmiPcTaeG7/gUfE5yuYB3ha/uSLs='
const expectedValue = 'ypeBEsobvcr6wjGzmiPcTaeG7/gUfE5yuYB3ha/uSLs'
t.assert.ok(caseSensitiveMatch(actualValue, expectedValue))
})
test('identical strings, expectedValue has one padding char', (t) => {
const actualValue = 'ypeBEsobvcr6wjGzmiPcTaeG7/gUfE5yuYB3ha/uSLs'
const expectedValue = 'ypeBEsobvcr6wjGzmiPcTaeG7/gUfE5yuYB3ha/uSLs='
t.assert.ok(caseSensitiveMatch(actualValue, expectedValue))
})
test('identical strings, expectedValue has two padding chars', (t) => {
const actualValue = 'ypeBEsobvcr6wjGzmiPcTaeG7/gUfE5yuYB3ha/uSLs'
const expectedValue = 'ypeBEsobvcr6wjGzmiPcTaeG7/gUfE5yuYB3ha/uSLs=='
t.assert.ok(caseSensitiveMatch(actualValue, expectedValue))
})
test('identical strings, both have one padding char', (t) => {
const actualValue = 'ypeBEsobvcr6wjGzmiPcTaeG7/gUfE5yuYB3ha/uSLs='
const expectedValue = 'ypeBEsobvcr6wjGzmiPcTaeG7/gUfE5yuYB3ha/uSLs='
t.assert.ok(caseSensitiveMatch(actualValue, expectedValue))
})
test('identical strings, both have two padding chars', (t) => {
const actualValue = 'ypeBEsobvcr6wjGzmiPcTaeG7/gUfE5yuYB3ha/uSLs=='
const expectedValue = 'ypeBEsobvcr6wjGzmiPcTaeG7/gUfE5yuYB3ha/uSLs=='
t.assert.ok(caseSensitiveMatch(actualValue, expectedValue))
})
test('identical strings, expectedValue has invalid third padding char', (t) => {
const actualValue = 'ypeBEsobvcr6wjGzmiPcTaeG7/gUfE5yuYB3ha/uSLs=='
const expectedValue = 'ypeBEsobvcr6wjGzmiPcTaeG7/gUfE5yuYB3ha/uSLs==='
t.assert.ok(caseSensitiveMatch(actualValue, expectedValue) === false)
})
test('expectedValue can be base64Url - match `_`', (t) => {
const actualValue = 'ypeBEsobvcr6wjGzmiPcTaeG7/gUfE5yuYB3ha/uSLs'
const expectedValue = 'ypeBEsobvcr6wjGzmiPcTaeG7_gUfE5yuYB3ha/uSLs'
t.assert.ok(caseSensitiveMatch(actualValue, expectedValue))
})
test('expectedValue can be base64Url - match `+`', (t) => {
const actualValue = 'ypeBEsobvcr6wjGzmiPcTaeG7+gUfE5yuYB3ha/uSLs'
const expectedValue = 'ypeBEsobvcr6wjGzmiPcTaeG7-gUfE5yuYB3ha/uSLs'
t.assert.ok(caseSensitiveMatch(actualValue, expectedValue))
})
test('should be case sensitive', (t) => {
const actualValue = 'ypeBEsobvcr6wjGzmiPcTaeG7/gUfE5yuYB3ha/uSLs'
const expectedValue = 'ypeBEsobvcr6wjGzmiPcTaeG7/gUfE5yuYB3ha/uSLS'
t.assert.ok(caseSensitiveMatch(actualValue, expectedValue) === false)
})
test('empty string should return true', (t) => {
const actualValue = ''
const expectedValue = ''
t.assert.ok(caseSensitiveMatch(actualValue, expectedValue))
})
})
================================================
FILE: test/subresource-integrity/get-strongest-metadata.js
================================================
'use strict'
const { test, describe } = require('node:test')
const { getStrongestMetadata } = require('../../lib/web/subresource-integrity/subresource-integrity')
describe('getStrongestMetadata', () => {
test('should return strongest sha512 /1', (t) => {
const result = getStrongestMetadata([
{ alg: 'sha256', val: 'sha256-abc' },
{ alg: 'sha384', val: 'sha384-def' },
{ alg: 'sha512', val: 'sha512-ghi' }
])
t.assert.deepEqual(result, [
{ alg: 'sha512', val: 'sha512-ghi' }
])
})
test('should return strongest sha512 /2', (t) => {
const result = getStrongestMetadata([
{ alg: 'sha512', val: 'sha512-ghi' },
{ alg: 'sha256', val: 'sha256-abc' },
{ alg: 'sha384', val: 'sha384-def' }
])
t.assert.deepEqual(result, [
{ alg: 'sha512', val: 'sha512-ghi' }
])
})
test('should return strongest sha384', (t) => {
const result = getStrongestMetadata([
{ alg: 'sha256', val: 'sha256-abc' },
{ alg: 'sha384', val: 'sha384-def' }
])
t.assert.deepEqual(result, [
{ alg: 'sha384', val: 'sha384-def' }
])
})
test('should return both strongest sha384', (t) => {
const result = getStrongestMetadata([
{ alg: 'sha384', val: 'sha384-abc' },
{ alg: 'sha256', val: 'sha256-def' },
{ alg: 'sha384', val: 'sha384-ghi' }
])
t.assert.deepEqual(result, [
{ alg: 'sha384', val: 'sha384-abc' },
{ alg: 'sha384', val: 'sha384-ghi' }
])
})
test('should return multiple metadata with the same strength', (t) => {
const result = getStrongestMetadata([
{ alg: 'sha256', val: 'sha256-abc' }
])
t.assert.deepEqual(result, [
{ alg: 'sha256', val: 'sha256-abc' }
])
})
test('should return empty array when no metadata is provided', (t) => {
const result = getStrongestMetadata([])
t.assert.deepEqual(result, [])
})
test('should throw when invalid hash algorithm is provided', (t) => {
t.assert.throws(() => getStrongestMetadata([
{ alg: 'sha1024', val: 'sha1024-xyz' }
]), {
name: 'AssertionError',
message: 'Invalid SRI hash algorithm token'
})
})
})
================================================
FILE: test/subresource-integrity/is-valid-sri-hash-algorithm.js
================================================
'use strict'
const { test, describe } = require('node:test')
const { runtimeFeatures } = require('../../lib/util/runtime-features.js')
const { isValidSRIHashAlgorithm } = require('../../lib/web/subresource-integrity/subresource-integrity')
const skip = runtimeFeatures.has('crypto') === false
describe('isValidSRIHashAlgorithm', () => {
test('valid sha256', { skip }, (t) => {
t.assert.ok(isValidSRIHashAlgorithm('sha256'))
})
test('valid sha384', { skip }, (t) => {
t.assert.ok(isValidSRIHashAlgorithm('sha384'))
})
test('valid sha512', { skip }, (t) => {
t.assert.ok(isValidSRIHashAlgorithm('sha512'))
})
test('invalid sha1024', (t) => {
t.assert.ok(isValidSRIHashAlgorithm('sha1024') === false)
})
})
================================================
FILE: test/subresource-integrity/parse-metadata.js
================================================
'use strict'
const { test, describe } = require('node:test')
const { parseMetadata } = require('../../lib/web/subresource-integrity/subresource-integrity')
const { runtimeFeatures } = require('../../lib/util/runtime-features')
const skip = runtimeFeatures.has('crypto') === false
describe('parseMetadata', () => {
/* Hash values generated with for "Hello world!" */
const hash256 = 'wFNeS+K3n/2TKRMFQ2v4iTFOSj+uwF7P/Lt98xrZ5Ro='
const hash384 = 'hiVfosNuSzCWnq4X3DTHcsvr38WLWEA5AL6HYU6xo0uHgCY/JV615lypu7hkHMz+'
const hash512 = '9s3ioPgZMUzd5V/CJ9jX2uPSjMVWIioKitZtkcytSq1glPUXohgjYMmqz2o9wyMWLLb9jN/+2w/gOPVehf+1tg=='
test('should parse valid metadata with option', { skip }, (t) => {
const validMetadata = `sha256-${hash256} !@ sha384-${hash384} !@ sha512-${hash512} !@`
const result = parseMetadata(validMetadata)
t.assert.deepEqual(result, [
{ alg: 'sha256', val: hash256 },
{ alg: 'sha384', val: hash384 },
{ alg: 'sha512', val: hash512 }
])
})
test('should parse valid metadata with non ASCII chars option', { skip }, (t) => {
const validMetadata = `sha256-${hash256} !© sha384-${hash384} !€ sha512-${hash512} !µ`
const result = parseMetadata(validMetadata)
t.assert.deepEqual(result, [
{ alg: 'sha256', val: hash256 },
{ alg: 'sha384', val: hash384 },
{ alg: 'sha512', val: hash512 }
])
})
test('should parse valid metadata without option', { skip }, (t) => {
const validMetadata = `sha256-${hash256} sha384-${hash384} sha512-${hash512}`
const result = parseMetadata(validMetadata)
t.assert.deepEqual(result, [
{ alg: 'sha256', val: hash256 },
{ alg: 'sha384', val: hash384 },
{ alg: 'sha512', val: hash512 }
])
})
test('should not set hash as undefined when invalid base64 chars are provided', { skip }, (t) => {
const invalidHash384 = 'zifp5hE1Xl5LQQqQz[]Bq/iaq9Wb6jVb//T7EfTmbXD2aEP5c2ZdJr9YTDfcTE1ZH+'
const validMetadata = `sha256-${hash256} sha384-${invalidHash384} sha512-${hash512}`
const result = parseMetadata(validMetadata)
t.assert.deepEqual(result, [
{ alg: 'sha256', val: hash256 },
{ alg: 'sha384', val: invalidHash384 },
{ alg: 'sha512', val: hash512 }
])
})
})
================================================
FILE: test/sync-error-in-callback.js
================================================
'use strict'
const { tspl } = require('@matteo.collina/tspl')
const { test, after } = require('node:test')
const { Client } = require('..')
const { createServer } = require('node:http')
test('synchronous error in request callback should reach uncaughtException handler', async (t) => {
const p = tspl(t, { plan: 3 })
const server = createServer((req, res) => {
res.end('hello')
})
after(() => server.close())
await new Promise((resolve) => {
server.listen(0, resolve)
})
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.close())
const testError = new Error('sync error in callback')
// Set up uncaughtException handler
const originalHandler = process.listenerCount('uncaughtException') > 0
? process.listeners('uncaughtException')[0]
: null
process.removeAllListeners('uncaughtException')
const uncaughtHandler = (err) => {
p.strictEqual(err, testError, 'Error should reach uncaughtException handler')
// Clean up
process.removeListener('uncaughtException', uncaughtHandler)
if (originalHandler) {
process.on('uncaughtException', originalHandler)
}
}
process.on('uncaughtException', uncaughtHandler)
client.request({
path: '/',
method: 'GET'
}, (err, data) => {
p.ifError(err)
p.strictEqual(data.statusCode, 200)
// Destroy the stream to simulate the described scenario
data.body.destroy()
// This synchronous error should reach the uncaughtException handler
throw testError
})
// Wait a bit to ensure the uncaughtException handler is triggered
await new Promise(resolve => setTimeout(resolve, 100))
// Clean up handler if not triggered
process.removeListener('uncaughtException', uncaughtHandler)
if (originalHandler) {
process.on('uncaughtException', originalHandler)
}
await p.completed
})
test('synchronous error thrown immediately in request callback', async (t) => {
const p = tspl(t, { plan: 3 })
const server = createServer((req, res) => {
res.end('hello')
})
after(() => server.close())
await new Promise((resolve) => {
server.listen(0, resolve)
})
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.close())
const testError = new Error('immediate sync error')
// Set up uncaughtException handler
const originalHandler = process.listenerCount('uncaughtException') > 0
? process.listeners('uncaughtException')[0]
: null
process.removeAllListeners('uncaughtException')
const uncaughtHandler = (err) => {
p.strictEqual(err, testError, 'Error should reach uncaughtException handler')
// Clean up
process.removeListener('uncaughtException', uncaughtHandler)
if (originalHandler) {
process.on('uncaughtException', originalHandler)
}
}
process.on('uncaughtException', uncaughtHandler)
client.request({
path: '/',
method: 'GET'
}, (err, data) => {
p.ifError(err)
p.strictEqual(data.statusCode, 200)
// Throw immediately without any stream operations
throw testError
})
// Wait a bit to ensure the uncaughtException handler is triggered
await new Promise(resolve => setTimeout(resolve, 100))
// Clean up handler if not triggered
process.removeListener('uncaughtException', uncaughtHandler)
if (originalHandler) {
process.on('uncaughtException', originalHandler)
}
await p.completed
})
test('synchronous error in request callback with error parameter', async (t) => {
const p = tspl(t, { plan: 1 })
const server = createServer((req, res) => {
// Force an error by destroying the socket
req.socket.destroy()
})
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.close())
client.request({
path: '/',
method: 'GET'
}, (err, data) => {
// We expect an error from the destroyed socket
p.ok(err)
// Don't throw here as it would interfere with test completion
// The important tests are the ones where we get successful responses
})
})
await p.completed
})
================================================
FILE: test/timers.js
================================================
'use strict'
const { tspl } = require('@matteo.collina/tspl')
const { describe, test } = require('node:test')
const FakeTimers = require('@sinonjs/fake-timers')
const clock = FakeTimers.install()
const timers = require('../lib/util/timers')
const { eventLoopBlocker } = require('./utils/event-loop-blocker')
// timers.setTimeout implements a low resolution timer with a 500 ms granularity
// It is expected that in the worst case, a timer will fire about 500 ms after the
// intended amount of time, an extra 200 ms is added to account event loop overhead
// Timers should never fire excessively early, 1ms early is tolerated
const ACCEPTABLE_DELTA = 700
function tick (duration) {
for (let i = 0; i < duration; ++i) {
clock.tick(1)
}
}
describe('timers', () => {
test('timers exports a clearTimeout', (t) => {
t = tspl(t, { plan: 1 })
t.ok(typeof timers.clearTimeout === 'function')
})
test('timers exports a setTimeout', (t) => {
t = tspl(t, { plan: 1 })
t.ok(typeof timers.setTimeout === 'function')
})
test('setTimeout instantiates a native NodeJS.Timeout when delay is lower or equal 1e3 ms', (t) => {
t = tspl(t, { plan: 2 })
t.strictEqual(timers.setTimeout(() => { }, 999)[timers.kFastTimer], undefined)
t.strictEqual(timers.setTimeout(() => { }, 1e3)[timers.kFastTimer], undefined)
})
test('setTimeout instantiates a FastTimer when delay is bigger than 1e3 ms', (t) => {
t = tspl(t, { plan: 1 })
const timeout = timers.setTimeout(() => { }, 1001)
t.strictEqual(timeout[timers.kFastTimer], true)
})
test('clearTimeout can clear a node native Timeout', (t) => {
t = tspl(t, { plan: 1 })
const nativeTimeoutId = setTimeout(() => { t.fail() }, 1)
t.ok(timers.clearTimeout(nativeTimeoutId) === undefined)
tick(10)
})
test('a FastTimer will get a _idleStart value after short time', async (t) => {
t = tspl(t, { plan: 3 })
const timer = timers.setTimeout(() => {
t.fail('timer should not have fired')
}, 1e4)
t.strictEqual(timer[timers.kFastTimer], true)
t.strictEqual(timer._idleStart, -1)
tick(1e3)
t.notStrictEqual(timer._idleStart, -1)
timers.clearTimeout(timer)
})
test('a cleared FastTimer will reset the _idleStart value to -1', async (t) => {
t = tspl(t, { plan: 4 })
const timer = timers.setTimeout(() => {
t.fail('timer should not have fired')
}, 1e4)
t.strictEqual(timer[timers.kFastTimer], true)
t.strictEqual(timer._idleStart, -1)
tick(750)
t.notStrictEqual(timer._idleStart, -1)
timers.clearTimeout(timer)
t.strictEqual(timer._idleStart, -1)
})
test('a FastTimer can be cleared', async (t) => {
t = tspl(t, { plan: 3 })
const timer = timers.setTimeout(() => {
t.fail('timer should not have fired')
}, 1001)
t.strictEqual(timer[timers.kFastTimer], true)
timers.clearTimeout(timer)
t.strictEqual(timer._idleStart, -1)
tick(750)
t.strictEqual(timer._idleStart, -1)
})
test('a cleared FastTimer can be refreshed', async (t) => {
t = tspl(t, { plan: 2 })
const timer = timers.setFastTimeout(() => {
t.ok('pass')
}, 1001)
t.strictEqual(timer[timers.kFastTimer], true)
timers.clearTimeout(timer)
timer.refresh()
tick(2000)
timers.clearTimeout(timer)
})
const getDelta = (start, target) => {
const end = performance.now()
const actual = end - start
return actual - target
}
test('refresh correctly with timeout < TICK_MS', async (t) => {
t = tspl(t, { plan: 3 })
const start = performance.now()
const timeout = timers.setTimeout(() => {
// 80 ms timer was refreshed after 120 ms; total target is 200 ms
const delta = getDelta(start, 200)
t.ok(delta >= -1, 'refreshed timer fired early')
t.ok(delta < ACCEPTABLE_DELTA, 'refreshed timer fired late')
}, 80)
setTimeout(() => timeout.refresh(), 40)
setTimeout(() => timeout.refresh(), 80)
setTimeout(() => timeout.refresh(), 120)
setTimeout(() => t.ok(true), 260)
tick(500)
await t.completed
})
test('refresh correctly with timeout > TICK_MS', async (t) => {
t = tspl(t, { plan: 3 })
const start = performance.now()
const timeout = timers.setTimeout(() => {
// 501ms timer was refreshed after 1250ms; total target is 1751
const delta = getDelta(start, 1751)
t.ok(delta >= -1, 'refreshed timer fired early')
t.ok(delta < ACCEPTABLE_DELTA, 'refreshed timer fired late')
}, 501)
setTimeout(() => timeout.refresh(), 250)
setTimeout(() => timeout.refresh(), 750)
setTimeout(() => timeout.refresh(), 1250)
setTimeout(() => t.ok(true), 1800)
tick(2000)
await t.completed
})
test('refresh correctly FastTimer with timeout > TICK_MS', async (t) => {
t = tspl(t, { plan: 3 })
// The long running FastTimer will ensure that the internal clock is
// incremented by the TICK_MS value in the onTick function
const longRunningFastTimer = timers.setTimeout(() => {}, 1e10)
const start = timers.now()
const timeout = timers.setFastTimeout(() => {
const delta = (timers.now() - start) - 2493
t.ok(delta >= -1, `refreshed timer fired early (${delta} ms)`)
t.ok(delta < ACCEPTABLE_DELTA, `refreshed timer fired late (${delta} ms)`)
}, 1001)
tick(250)
timeout.refresh()
tick(250)
timeout.refresh()
tick(250)
timeout.refresh()
tick(250)
timeout.refresh()
timers.clearTimeout(longRunningFastTimer)
setTimeout(() => t.ok(true), 500)
tick(5000)
await t.completed
})
test('a FastTimer will only increment by the defined TICK_MS value', async (t) => {
t = tspl(t, { plan: 6 })
const startInternalClock = timers.now()
// The long running FastTimer will ensure that the internal clock is
// incremented by the TICK_MS value in the onTick function
const longRunningFastTimer = timers.setTimeout(() => {}, 1e10)
eventLoopBlocker(1000)
// wait to ensure the timer has fired in the next loop
await new Promise((resolve) => resolve())
tick(250)
t.strictEqual(timers.now() - startInternalClock, 0)
tick(250)
t.strictEqual(timers.now() - startInternalClock, 499)
tick(250)
t.strictEqual(timers.now() - startInternalClock, 499)
tick(250)
t.strictEqual(timers.now() - startInternalClock, 998)
tick(250)
t.strictEqual(timers.now() - startInternalClock, 998)
tick(250)
t.strictEqual(timers.now() - startInternalClock, 1497)
timers.clearTimeout(longRunningFastTimer)
})
test('meet acceptable resolution time', async (t) => {
const testTimeouts = [0, 1, 499, 500, 501, 990, 999, 1000, 1001, 1100, 1400, 1499, 1500, 4000, 5000]
t = tspl(t, { plan: testTimeouts.length * 2 })
const start = performance.now()
for (const target of testTimeouts) {
timers.setTimeout(() => {
const delta = getDelta(start, target)
t.ok(delta >= -1, `${target}ms fired early`)
t.ok(delta < ACCEPTABLE_DELTA, `${target}ms fired late, got difference of ${delta}ms`)
}, target)
}
for (let i = 0; i < 6000; ++i) {
clock.tick(1)
}
await t.completed
})
})
================================================
FILE: test/tls-cert-leak.js
================================================
'use strict'
const { test } = require('node:test')
const assert = require('node:assert')
const { tspl } = require('@matteo.collina/tspl')
const { fetch } = require('..')
const https = require('node:https')
const fs = require('node:fs')
const path = require('node:path')
const { closeServerAsPromise } = require('./utils/node-http')
const hasGC = typeof global.gc !== 'undefined'
// This test verifies that there is no memory leak when handling TLS certificate errors.
// It simulates the error by using a server with a self-signed certificate.
test('no memory leak with TLS certificate errors', { timeout: 20000 }, async (t) => {
if (!hasGC) {
throw new Error('gc is not available. Run with \'--expose-gc\'.')
}
const { ok } = tspl(t, { plan: 1 })
// Create HTTPS server with self-signed certificate
const serverOptions = {
key: fs.readFileSync(path.join(__dirname, 'fixtures', 'key.pem')),
cert: fs.readFileSync(path.join(__dirname, 'fixtures', 'cert.pem')),
joinDuplicateHeaders: true
}
// Create a server that always responds with a simple message
const server = https.createServer(serverOptions, (req, res) => {
res.writeHead(200)
res.end('test response')
})
// Start server on a random port
await new Promise(resolve => server.listen(0, resolve))
const serverUrl = `https://localhost:${server.address().port}`
t.after(closeServerAsPromise(server))
// Function to make a request that will trigger a certificate error
async function makeRequest (i) {
try {
// The request will fail with CERT_SIGNATURE_FAILURE or similar
// because we're using a self-signed certificate and not telling
// Node.js to accept it
const res = await fetch(`${serverUrl}/request-${i}`, {
signal: AbortSignal.timeout(2000) // Short timeout to prevent hanging
})
const text = await res.text()
return { status: res.status, text }
} catch (e) {
// In real code, without the fix, this would leak memory
if (e?.cause?.code === 'CERT_SIGNATURE_FAILURE' ||
e?.cause?.code === 'DEPTH_ZERO_SELF_SIGNED_CERT' ||
e?.cause?.code === 'ERR_TLS_CERT_ALTNAME_INVALID') {
return { status: 524, text: 'Certificate Error' }
}
// Return for any other error to avoid test interruption
return { status: 500, text: e.message }
}
}
// Counter for completed requests
let complete = 0
const requestCount = 400
// Track memory usage
const measurements = []
let baselineMemory = 0
// Process a batch of requests
async function processBatch (start, batchSize) {
const promises = []
const end = Math.min(start + batchSize, requestCount)
for (let i = start; i < end; i++) {
promises.push(makeRequest(i))
}
await Promise.all(promises)
complete += promises.length
// Measure memory after each batch
if (complete % 50 === 0 || complete === end) {
// Run GC multiple times to get more stable readings
global.gc()
await new Promise(resolve => setTimeout(resolve, 50))
global.gc()
const memUsage = process.memoryUsage()
// Establish baseline after first batch
if (measurements.length === 0) {
baselineMemory = memUsage.heapUsed
}
measurements.push({
complete,
heapUsed: memUsage.heapUsed
})
console.log(`Completed ${complete}/${requestCount}: Heap: ${Math.round(memUsage.heapUsed / 1024 / 1024)}MB`)
// Check memory trend after we have enough data
if (measurements.length >= 4) {
const hasLeak = checkMemoryTrend()
if (hasLeak) {
return true // Indicates a leak was detected
}
}
}
return false // No leak detected
}
// Main test logic
async function runTest () {
const batchSize = 50
for (let i = 0; i < requestCount; i += batchSize) {
const leakDetected = await processBatch(i, batchSize)
if (leakDetected) {
// If a leak is detected, fail the test
assert.fail('Memory leak detected: heap usage is consistently increasing at a significant rate')
return
}
// Check if we have sufficient measurements or have done 350 requests
if (measurements.length >= 7 || complete >= 350) {
break
}
}
// Final check
const finalCheckResult = finalMemoryCheck()
if (finalCheckResult) {
assert.fail(`Memory leak detected: ${finalCheckResult}`)
} else {
ok(true, 'Memory usage has stabilized')
}
}
// Check if memory usage has a concerning trend
function checkMemoryTrend () {
// Calculate memory growth between each measurement
const growthRates = []
for (let i = 1; i < measurements.length; i++) {
const prev = measurements[i - 1].heapUsed
const current = measurements[i].heapUsed
growthRates.push((current - prev) / prev)
}
// Calculate growth from baseline
const totalGrowthFromBaseline = (measurements[measurements.length - 1].heapUsed - baselineMemory) / baselineMemory
// Calculate average growth rate
const avgGrowthRate = growthRates.reduce((sum, rate) => sum + rate, 0) / growthRates.length
console.log(`Growth from baseline: ${(totalGrowthFromBaseline * 100).toFixed(2)}%`)
console.log(`Average growth rate: ${(avgGrowthRate * 100).toFixed(2)}%`)
console.log(`Growth rates: ${growthRates.map(r => (r * 100).toFixed(2) + '%').join(', ')}`)
// Only flag as leak if all conditions are met:
// 1. Consistent growth (majority of measurements show growth)
// 2. Average growth rate is significant (>2%)
// 3. Total growth from baseline is significant (>20%)
// Count how many positive growth rates we have
const positiveGrowthRates = growthRates.filter(rate => rate > 0.01).length
return (
positiveGrowthRates >= Math.ceil(growthRates.length * 0.75) && // 75% of measurements show growth >1%
avgGrowthRate > 0.02 && // Average growth >2%
totalGrowthFromBaseline > 0.2 // Total growth >20%
)
}
// Final memory check with adjusted requirements
function finalMemoryCheck () {
if (measurements.length < 4) return false
// Calculate growth from baseline to the last measurement
const totalGrowthFromBaseline = (measurements[measurements.length - 1].heapUsed - baselineMemory) / baselineMemory
console.log(`Final growth from baseline: ${(totalGrowthFromBaseline * 100).toFixed(2)}%`)
// Calculate final slope over the last 150 requests
const lastMeasurements = measurements.slice(-3)
const finalSlope = (lastMeasurements[2].heapUsed - lastMeasurements[0].heapUsed) /
(lastMeasurements[2].complete - lastMeasurements[0].complete)
console.log(`Final memory slope: ${finalSlope.toFixed(2)} bytes per request`)
// Only consider it a leak if:
// 1. Total growth is very significant (>25%)
if (totalGrowthFromBaseline > 0.25) {
return `Excessive memory growth of ${(totalGrowthFromBaseline * 100).toFixed(2)}%`
}
// 2. Memory is still growing rapidly at the end (>2000 bytes per request)
if (finalSlope > 2000) {
return `Memory still growing rapidly at ${finalSlope.toFixed(2)} bytes per request`
}
return false
}
await runTest()
})
================================================
FILE: test/tls-session-reuse.js
================================================
'use strict'
const { tspl } = require('@matteo.collina/tspl')
const { test, after, describe } = require('node:test')
const { readFileSync } = require('node:fs')
const { join } = require('node:path')
const https = require('node:https')
const crypto = require('node:crypto')
const { Client, Pool } = require('..')
const { kSocket } = require('../lib/core/symbols')
const options = {
key: readFileSync(join(__dirname, 'fixtures', 'key.pem'), 'utf8'),
cert: readFileSync(join(__dirname, 'fixtures', 'cert.pem'), 'utf8'),
joinDuplicateHeaders: true
}
const ca = readFileSync(join(__dirname, 'fixtures', 'ca.pem'), 'utf8')
describe('A client should disable session caching', () => {
const clientSessions = {}
let serverRequests = 0
test('Prepare request', async t => {
t = tspl(t, { plan: 3 })
const server = https.createServer(options, (req, res) => {
if (req.url === '/drop-key') {
server.setTicketKeys(crypto.randomBytes(48))
}
serverRequests++
res.end()
})
server.listen(0, function () {
const tls = {
ca,
rejectUnauthorized: false,
servername: 'agent1'
}
const client = new Client(`https://localhost:${server.address().port}`, {
pipelining: 0,
tls,
maxCachedSessions: 0
})
after(() => {
client.close()
server.close()
})
const queue = [{
name: 'first',
method: 'GET',
path: '/'
}, {
name: 'second',
method: 'GET',
path: '/'
}]
function request () {
const options = queue.shift()
if (options.ciphers) {
// Choose different cipher to use different cache entry
tls.ciphers = options.ciphers
} else {
delete tls.ciphers
}
client.request(options, (err, data) => {
t.ifError(err)
clientSessions[options.name] = client[kSocket].getSession()
data.body.resume().on('end', () => {
if (queue.length !== 0) {
return request()
}
t.ok(true, 'pass')
})
})
}
request()
})
await t.completed
})
test('Verify cached sessions', async t => {
t = tspl(t, { plan: 2 })
t.strictEqual(serverRequests, 2)
t.notEqual(
clientSessions.first.toString('hex'),
clientSessions.second.toString('hex')
)
await t.completed
})
})
describe('A pool should be able to reuse TLS sessions between clients', () => {
let serverRequests = 0
const REQ_COUNT = 10
const ASSERT_PERFORMANCE_GAIN = false
test('Prepare request', async t => {
t = tspl(t, { plan: 2 + 1 + (ASSERT_PERFORMANCE_GAIN ? 1 : 0) })
const server = https.createServer(options, (req, res) => {
serverRequests++
res.end()
})
let numSessions = 0
const sessions = []
server.listen(0, async () => {
const poolWithSessionReuse = new Pool(`https://localhost:${server.address().port}`, {
pipelining: 0,
connections: 100,
maxCachedSessions: 1,
tls: {
ca,
rejectUnauthorized: false,
servername: 'agent1'
}
})
const poolWithoutSessionReuse = new Pool(`https://localhost:${server.address().port}`, {
pipelining: 0,
connections: 100,
maxCachedSessions: 0,
tls: {
ca,
rejectUnauthorized: false,
servername: 'agent1'
}
})
poolWithSessionReuse.on('connect', (url, targets) => {
const y = targets[1][kSocket].getSession()
if (sessions.some(x => x.equals(y))) {
return
}
sessions.push(y)
numSessions++
})
after(() => {
poolWithSessionReuse.close()
poolWithoutSessionReuse.close()
server.close()
})
function request (pool, expectTLSSessionCache) {
return new Promise((resolve, reject) => {
pool.request({
method: 'GET',
path: '/'
}, (err, data) => {
if (err) return reject(err)
data.body.resume().on('end', resolve)
})
})
}
async function runRequests (pool, numIterations, expectTLSSessionCache) {
const requests = []
// For the session reuse, we first need one client to connect to receive a valid tls session to reuse
await request(pool, false)
while (numIterations--) {
requests.push(request(pool, expectTLSSessionCache))
}
return await Promise.all(requests)
}
await runRequests(poolWithoutSessionReuse, REQ_COUNT, false)
await runRequests(poolWithSessionReuse, REQ_COUNT, true)
t.strictEqual(numSessions, 2)
t.strictEqual(serverRequests, 2 + REQ_COUNT * 2)
t.ok(true, 'pass')
})
await t.completed
})
})
================================================
FILE: test/tls.js
================================================
'use strict'
// TODO: Don't depend on external URLs.
// const { test } = require('tap')
// const { Client } = require('..')
// const { kSocket } = require('../lib/core/symbols')
// const { Readable } = require('node:stream')
// const { kRunning } = require('../lib/core/symbols')
// test('tls get 1', (t) => {
// t.plan(4)
// const client = new Client('https://www.github.com')
// t.teardown(client.close.bind(client))
// client.request({ method: 'GET', path: '/' }, (err, data) => {
// t.error(err)
// t.equal(data.statusCode, 301)
// t.equal(client[kSocket].authorized, true)
// data.body
// .resume()
// .on('end', () => {
// t.ok(true, 'pass')
// })
// })
// })
// test('tls get 2', (t) => {
// t.plan(4)
// const client = new Client('https://140.82.112.4', {
// tls: {
// servername: 'www.github.com'
// }
// })
// t.teardown(client.close.bind(client))
// client.request({ method: 'GET', path: '/' }, (err, data) => {
// t.error(err)
// t.equal(data.statusCode, 301)
// t.equal(client[kSocket].authorized, true)
// data.body
// .resume()
// .on('end', () => {
// t.ok(true, 'pass')
// })
// })
// })
// test('tls get 3', (t) => {
// t.plan(8)
// const client = new Client('https://140.82.112.4')
// t.teardown(client.destroy.bind(client))
// let didDisconnect = false
// client.request({
// method: 'GET',
// path: '/',
// headers: {
// host: 'www.github.com'
// }
// }, (err, data) => {
// t.error(err)
// t.equal(data.statusCode, 301)
// t.equal(client[kSocket].authorized, true)
// data.body
// .resume()
// .on('end', () => {
// t.ok(true, 'pass')
// })
// client.once('disconnect', () => {
// t.ok(true, 'pass')
// didDisconnect = true
// })
// })
// const body = new Readable({ read () {} })
// body.on('error', (err) => {
// t.ok(err)
// })
// client.request({
// method: 'POST',
// path: '/',
// body,
// headers: {
// host: 'www.asd.com'
// }
// }, (err, data) => {
// t.equal(didDisconnect, true)
// t.ok(err)
// })
// })
// test('tls get 4', (t) => {
// t.plan(9)
// const client = new Client('https://140.82.112.4', {
// tls: {
// servername: 'www.github.com'
// },
// pipelining: 2
// })
// t.teardown(client.close.bind(client))
// client.request({
// method: 'GET',
// path: '/',
// headers: {
// host: '140.82.112.4'
// }
// }, (err, data) => {
// t.error(err)
// t.equal(client[kRunning], 1)
// t.equal(data.statusCode, 301)
// t.equal(client[kSocket].authorized, true)
// client.request({
// method: 'GET',
// path: '/',
// headers: {
// host: 'www.github.com'
// }
// }, (err, data) => {
// t.error(err)
// t.equal(data.statusCode, 301)
// t.equal(client[kSocket].authorized, true)
// data.body
// .resume()
// .on('end', () => {
// t.ok(true, 'pass')
// })
// })
// data.body
// .resume()
// .on('end', () => {
// t.ok(true, 'pass')
// })
// })
// })
// test('tls get 5', (t) => {
// t.plan(7)
// const client = new Client('https://140.82.112.4')
// t.teardown(client.destroy.bind(client))
// let didDisconnect = false
// client.request({
// method: 'GET',
// path: '/',
// headers: {
// host: 'www.github.com'
// }
// }, (err, data) => {
// t.error(err)
// t.equal(data.statusCode, 301)
// t.equal(client[kSocket].authorized, true)
// data.body
// .resume()
// .on('end', () => {
// t.ok(true, 'pass')
// })
// client.once('disconnect', () => {
// t.ok(true, 'pass')
// didDisconnect = true
// })
// })
// client.request({
// method: 'POST',
// path: '/',
// body: [],
// headers: {
// host: 'www.asd.com'
// }
// }, (err, data) => {
// t.equal(didDisconnect, true)
// t.ok(err)
// })
// })
================================================
FILE: test/trailers.js
================================================
'use strict'
const { tspl } = require('@matteo.collina/tspl')
const { test, after } = require('node:test')
const { Client } = require('..')
const { createServer } = require('node:http')
test('response trailers missing is OK', async (t) => {
t = tspl(t, { plan: 1 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.writeHead(200, {
Trailer: 'content-length'
})
res.end('response')
})
after(() => server.close())
server.listen(0, async () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.destroy())
client.on('disconnect', () => {
if (!client.closed && !client.destroyed) {
t.fail('unexpected disconnect')
}
})
const { body } = await client.request({
path: '/',
method: 'GET',
body: 'asd'
})
t.strictEqual(await body.text(), 'response')
})
await t.completed
})
test('response trailers missing w trailers is OK', async (t) => {
t = tspl(t, { plan: 2 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.writeHead(200, {
Trailer: 'content-length'
})
res.addTrailers({
asd: 'foo'
})
res.end('response')
})
after(() => server.close())
server.listen(0, async () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.destroy())
client.on('disconnect', () => {
if (!client.closed && !client.destroyed) {
t.fail('unexpected disconnect')
}
})
const { body, trailers } = await client.request({
path: '/',
method: 'GET',
body: 'asd'
})
t.strictEqual(await body.text(), 'response')
t.deepStrictEqual(trailers, { asd: 'foo' })
})
await t.completed
})
================================================
FILE: test/types/agent.test-d.ts
================================================
import { Duplex, Readable, Writable } from 'node:stream'
import { expectAssignable } from 'tsd'
import { Agent, Dispatcher } from '../..'
import { URL } from 'node:url'
expectAssignable(new Agent())
expectAssignable(new Agent({}))
expectAssignable(new Agent({ factory: () => new Dispatcher() }))
{
const agent = new Agent()
// properties
expectAssignable(agent.closed)
expectAssignable(agent.destroyed)
// request
expectAssignable>(agent.request({ origin: '', path: '', method: 'GET' }))
expectAssignable>(agent.request({ origin: '', path: '', method: 'GET', onInfo: (info) => {} }))
expectAssignable>(agent.request({ origin: new URL('http://localhost'), path: '', method: 'GET' }))
expectAssignable