From aa88a83e6fc83b218b5e0264676397001c045cc4 Mon Sep 17 00:00:00 2001 From: StopMakingThatBigFace <9166849+StopMakingThatBigFace@users.noreply.github.com> Date: Mon, 24 Aug 2026 05:56:15 +0000 Subject: [PATCH 1/3] chore: bump upstream wreq crates --- rust/Cargo.lock | 8 ++++---- rust/Cargo.toml | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/rust/Cargo.lock b/rust/Cargo.lock index 3857048..059483f 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -816,9 +816,9 @@ dependencies = [ [[package]] name = "http-body-util" -version = "0.1.3" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +checksum = "23169fe34a5fbcdd3f3862e78fb9b6fccd5f02a6dc6f732547005d45631ce71c" dependencies = [ "bytes", "futures-core", @@ -2587,9 +2587,9 @@ dependencies = [ [[package]] name = "wreq" -version = "6.0.0-rc.29" +version = "6.0.0-rc.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f0eba5f5814a94e5f1a99156f187133464e525b66bdbc69a9627d46530af2e1" +checksum = "0b100d81c46113fa7f0719e43b26b13264b38589a4f059e6ebfccb5308159336" dependencies = [ "btls", "btls-sys", diff --git a/rust/Cargo.toml b/rust/Cargo.toml index beb73c3..d6d0b13 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -9,7 +9,7 @@ crate-type = ["cdylib"] [dependencies] # HTTP client with browser impersonation -wreq = { version = "6.0.0-rc.29", default-features = false, features = ["tokio-rt", "gzip", "brotli", "deflate", "zstd", "socks", "cookies", "json", "charset", "multipart", "stream", "hickory-dns", "system-proxy", "webpki-roots", "ws"] } +wreq = { version = "6.0.0-rc.31", default-features = false, features = ["tokio-rt", "gzip", "brotli", "deflate", "zstd", "socks", "cookies", "json", "charset", "multipart", "stream", "hickory-dns", "system-proxy", "webpki-roots", "ws"] } wreq-util = { version = "3.0.0-rc.14", features = ["emulation-serde"] } http-body-util = "0.1" From cd61aa3dfb956471ac828dd52734858815161280 Mon Sep 17 00:00:00 2001 From: ruby Date: Tue, 25 Aug 2026 03:03:03 +0400 Subject: [PATCH 2/3] feat: stabilize wreq integration --- .github/dependabot.yml | 23 ++ .github/workflows/build.yml | 102 ++++++++- .github/workflows/test.yml | 45 ++-- .github/workflows/wreq-upstream.yml | 2 +- README.md | 6 +- package-lock.json | 318 ++++++++++++++-------------- package.json | 2 +- rust-toolchain.toml | 4 + rust/Cargo.lock | 8 +- rust/Cargo.toml | 4 +- rust/src/emulation/builders.rs | 2 +- rust/src/napi/body.rs | 25 ++- rust/src/napi/convert.rs | 4 + rust/src/napi/error.rs | 25 +++ rust/src/napi/mod.rs | 1 + rust/src/napi/request.rs | 15 +- rust/src/napi/websocket.rs | 3 +- rust/src/store/body_store.rs | 63 +++++- rust/src/transport/client.rs | 6 + rust/src/transport/request.rs | 1 + rust/src/transport/types.rs | 2 + rust/src/transport/websocket.rs | 1 + scripts/smoke-native.mjs | 18 ++ scripts/update-wreq-upstream.mjs | 4 +- scripts/verify-linux-binary.mjs | 112 ++++++++++ src/config/network.ts | 12 ++ src/http/fetch.ts | 10 + src/http/pipeline/cookies.ts | 14 +- src/http/pipeline/dispatch.ts | 6 +- src/http/pipeline/options.ts | 2 + src/http/pipeline/redirects.ts | 159 +++++++++++++- src/http/response.ts | 41 +++- src/native/binding.ts | 1 + src/native/client-cache.ts | 1 + src/native/index.ts | 1 + src/native/request.ts | 4 + src/test/cookies-redirects.spec.ts | 78 +++++++ src/test/helpers/local-server.ts | 25 ++- src/test/http-client.spec.ts | 32 ++- src/test/response.spec.ts | 19 ++ src/test/websocket.spec.ts | 40 +++- src/types/http.ts | 2 + src/types/native.ts | 2 + src/types/websocket.ts | 4 + src/websocket/index.ts | 2 + 45 files changed, 1017 insertions(+), 234 deletions(-) create mode 100644 rust-toolchain.toml create mode 100644 rust/src/napi/error.rs create mode 100644 scripts/smoke-native.mjs create mode 100644 scripts/verify-linux-binary.mjs diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 1b5725a..cfa65a6 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -27,3 +27,26 @@ updates: applies-to: security-updates patterns: - "*" + + - package-ecosystem: github-actions + directory: / + schedule: + interval: weekly + day: monday + time: "09:00" + timezone: Europe/Moscow + open-pull-requests-limit: 1 + labels: + - dependencies + groups: + github-actions-major: + applies-to: version-updates + patterns: + - "*" + update-types: + - major + ignore: + - dependency-name: "*" + update-types: + - version-update:semver-minor + - version-update:semver-patch diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f9d9c59..43fd951 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -6,6 +6,7 @@ on: - '.github/workflows/build.yml' - 'package.json' - 'package-lock.json' + - 'rust-toolchain.toml' - 'rust/**' - 'scripts/**' release: @@ -23,12 +24,6 @@ jobs: - host: macos-latest target: aarch64-apple-darwin build: npm run build:rust -- --target aarch64-apple-darwin - - host: ubuntu-latest - target: x86_64-unknown-linux-gnu - build: npm run build:rust -- --target x86_64-unknown-linux-gnu - - host: ubuntu-24.04-arm - target: aarch64-unknown-linux-gnu - build: npm run build:rust -- --target aarch64-unknown-linux-gnu - host: ubuntu-latest target: x86_64-unknown-linux-musl setup: | @@ -58,19 +53,34 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v7 with: - node-version: 20 + node-version: 20.20.2 + cache: npm - name: Install Rust uses: dtolnay/rust-toolchain@stable with: + toolchain: 1.98.0 targets: ${{ matrix.settings.target }} + - name: Cache Rust builds + uses: Swatinem/rust-cache@v2 + with: + workspaces: rust -> target + shared-key: ${{ matrix.settings.target }} + + - name: Cache musl cross toolchain + if: matrix.settings.target == 'x86_64-unknown-linux-musl' + uses: actions/cache@v6 + with: + path: ~/musl-cross + key: musl-cross-${{ runner.os }}-x86_64-11-20211120 + - name: Install target toolchain dependencies if: matrix.settings.setup != '' run: ${{ matrix.settings.setup }} - name: Install dependencies - run: npm install + run: npm ci --ignore-scripts - name: Build native module run: ${{ matrix.settings.build }} @@ -82,10 +92,84 @@ jobs: path: rust/*.node if-no-files-found: error + build-linux-gnu: + strategy: + fail-fast: false + matrix: + settings: + - host: ubuntu-latest + image: quay.io/pypa/manylinux_2_28_x86_64@sha256:4dc41da7df20400310c80d162a2fe2d2c2f3d9734d8dec20f6b9843711618deb + node_arch: x64 + target: x86_64-unknown-linux-gnu + binary: rust/node-wreq.linux-x64-gnu.node + - host: ubuntu-24.04-arm + image: quay.io/pypa/manylinux_2_28_aarch64@sha256:29a6ceb78de5b00494e6e7456a72782a4cb54238de102e7491d15fe47789b4d1 + node_arch: arm64 + target: aarch64-unknown-linux-gnu + binary: rust/node-wreq.linux-arm64-gnu.node + + name: Build - ${{ matrix.settings.target }} + runs-on: ${{ matrix.settings.host }} + container: + image: ${{ matrix.settings.image }} + defaults: + run: + shell: bash + + steps: + - name: Install container dependencies + run: dnf install -y --disablerepo=epel --setopt=install_weak_deps=False clang zstd + + - uses: actions/checkout@v7 + + - name: Setup Node.js + uses: actions/setup-node@v7 + with: + node-version: 20.20.2 + architecture: ${{ matrix.settings.node_arch }} + cache: npm + + - name: Install Rust + uses: dtolnay/rust-toolchain@stable + with: + toolchain: 1.98.0 + targets: ${{ matrix.settings.target }} + + - name: Cache Rust builds + uses: Swatinem/rust-cache@v2 + with: + workspaces: rust -> target + shared-key: manylinux-${{ matrix.settings.target }} + + - name: Install dependencies + run: npm ci --ignore-scripts + + - name: Build native module + env: + LIBCLANG_PATH: /usr/lib64 + run: npm run build:rust -- --target "${{ matrix.settings.target }}" + + - name: Verify and smoke test native module + run: | + if [ ! -f "${{ matrix.settings.binary }}" ]; then + echo "Native build produced no ${{ matrix.settings.binary }} binary" >&2 + exit 1 + fi + + node ./scripts/verify-linux-binary.mjs "${{ matrix.settings.binary }}" + node ./scripts/smoke-native.mjs "${{ matrix.settings.binary }}" + + - name: Upload artifacts + uses: actions/upload-artifact@v7 + with: + name: bindings-${{ matrix.settings.target }} + path: ${{ matrix.settings.binary }} + if-no-files-found: error + publish: name: Publish platform package - ${{ matrix.settings.target }} runs-on: ubuntu-latest - needs: build + needs: [build, build-linux-gnu] if: github.event_name == 'release' permissions: contents: read diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 293f399..e8aa63a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -6,6 +6,7 @@ on: - '.github/workflows/test.yml' - 'package.json' - 'package-lock.json' + - 'rust-toolchain.toml' - 'tsconfig.json' - 'src/**' - 'rust/**' @@ -15,6 +16,7 @@ on: - '.github/workflows/test.yml' - 'package.json' - 'package-lock.json' + - 'rust-toolchain.toml' - 'tsconfig.json' - 'src/**' - 'rust/**' @@ -43,37 +45,17 @@ jobs: - name: Install Rust uses: dtolnay/rust-toolchain@stable + with: + toolchain: 1.98.0 - - name: Cache Rust dependencies - uses: actions/cache@v6 + - name: Cache Rust builds + uses: Swatinem/rust-cache@v2 with: - path: | - ~/.cargo/bin/ - ~/.cargo/registry/index/ - ~/.cargo/registry/cache/ - ~/.cargo/git/db/ - rust/target/ - key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} - restore-keys: | - ${{ runner.os }}-cargo- + workspaces: rust -> target + shared-key: test-${{ matrix.os }} - name: Install dependencies - run: npm install - - - name: Build Rust module - run: npm run build:rust - - - name: Debug - List built files - shell: bash - run: | - echo "=== Files in rust/ ===" - ls -la rust/*.node || echo "No .node files in rust/" - echo "" - echo "=== Node platform info ===" - node -e "console.log('Platform:', process.platform, 'Arch:', process.arch)" - - - name: Build TypeScript - run: npm run build:ts + run: npm ci --ignore-scripts - name: Run tests run: npm test @@ -93,13 +75,20 @@ jobs: cache: 'npm' - name: Install dependencies - run: npm install + run: npm ci --ignore-scripts - name: Install Rust linting components uses: dtolnay/rust-toolchain@stable with: + toolchain: 1.98.0 components: rustfmt, clippy + - name: Cache Rust builds + uses: Swatinem/rust-cache@v2 + with: + workspaces: rust -> target + shared-key: lint + - name: Check TypeScript types run: npx tsc --noEmit diff --git a/.github/workflows/wreq-upstream.yml b/.github/workflows/wreq-upstream.yml index 836b97b..f65f572 100644 --- a/.github/workflows/wreq-upstream.yml +++ b/.github/workflows/wreq-upstream.yml @@ -63,7 +63,7 @@ jobs: body: | Automated upstream bump for the Rust crates. - This workflow checks `crates.io` for the latest published `max_version` of: + This workflow checks `crates.io` for the latest published stable version of: - `wreq` - `wreq-util` labels: | diff --git a/README.md b/README.md index e347535..8b378c4 100644 --- a/README.md +++ b/README.md @@ -565,7 +565,6 @@ Useful redirect facts: - `301` / `302` rewrite `POST` to `GET` - `303` rewrites to `GET` unless current method is `HEAD` - `307` / `308` preserve method and body -- `authorization` is stripped on cross-origin redirect ### simple retries @@ -661,6 +660,7 @@ Main error classes: - `TimeoutError` - `AbortError` - `WebSocketError` +- `ERR_DNS` Typical patterns: @@ -875,6 +875,7 @@ const client = createClient({ poolMaxIdlePerHost: 8, poolMaxSize: 128, tlsSessionCacheCapacity: 8, + tcpLinger: 5_000, }); await client.get('/account', { @@ -894,7 +895,8 @@ client.close(); ``` Set `poolIdleTimeout: false` to disable idle expiry. `connectionGroup` accepts a string or a -non-negative integer. +non-negative integer. `tcpLinger` configures `SO_LINGER` in milliseconds; `0` requests an abortive +close with TCP `RST`, while omitting the option keeps the operating-system default. ### mTLS and custom CAs diff --git a/package-lock.json b/package-lock.json index ac1feaa..438d5d7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,7 +23,7 @@ "@types/node": "^20.19.39", "@types/ws": "^8.18.1", "lint-staged": "^15.5.2", - "oxfmt": "^0.63.0", + "oxfmt": "^0.64.0", "oxlint": "^1.60.0", "rimraf": "^6.1.3", "typescript": "^5.0.0", @@ -218,9 +218,9 @@ } }, "node_modules/@oxfmt/binding-android-arm-eabi": { - "version": "0.63.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-android-arm-eabi/-/binding-android-arm-eabi-0.63.0.tgz", - "integrity": "sha512-YmRth4ZPGgEXcgmkhvANbC9uD67dxmSobW7DQuyt5tOBOKvPnIpk5SVHBj88E+7wMNRI2FhqaDbOhQFBix+b8A==", + "version": "0.64.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-android-arm-eabi/-/binding-android-arm-eabi-0.64.0.tgz", + "integrity": "sha512-o6uzh/jTOQeAY5TdkAeXdqv7MBRcPxiRA08zrcBtkKj5cSu/FMu0Hl7Q6Fi1KCKyCWZ6lJVjBzdsJvsKltUsGQ==", "cpu": [ "arm" ], @@ -235,9 +235,9 @@ } }, "node_modules/@oxfmt/binding-android-arm64": { - "version": "0.63.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-android-arm64/-/binding-android-arm64-0.63.0.tgz", - "integrity": "sha512-icbahX8X2X3sRamOMecvdYeZXWjPDazRDIfvWfy7Ca1nc/ZDT2Y9k5Nt7s46EqFd7NQPdgk+CM3/SgIT5LPCaQ==", + "version": "0.64.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-android-arm64/-/binding-android-arm64-0.64.0.tgz", + "integrity": "sha512-jRGSUeeP7p3Gynw2YaCVtjBIA6ZxY6bEB/ES5i54OhqmRTyuVg7ZgstEtzgq6GOAJd+2QZ5pvf+bFfmW5Mp9cw==", "cpu": [ "arm64" ], @@ -252,9 +252,9 @@ } }, "node_modules/@oxfmt/binding-darwin-arm64": { - "version": "0.63.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-darwin-arm64/-/binding-darwin-arm64-0.63.0.tgz", - "integrity": "sha512-WV+Ze5v5gI2qoj8jpAovt8KBTW8pjEz/AiMXXjeTQS+Bmf/MmZXTS40S8xNPDszX+W8WDv2Bbk6qKrMTtUGu1A==", + "version": "0.64.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-darwin-arm64/-/binding-darwin-arm64-0.64.0.tgz", + "integrity": "sha512-JINwtU2lW7nOFSqi+H2qplipNUqah9Gc1jgGmB82kTD4UnZrZIVxCJ9qEmFiKfjNq27gYLFhrUb0to86aCwMjw==", "cpu": [ "arm64" ], @@ -269,9 +269,9 @@ } }, "node_modules/@oxfmt/binding-darwin-x64": { - "version": "0.63.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-darwin-x64/-/binding-darwin-x64-0.63.0.tgz", - "integrity": "sha512-CJGSBdDxXOWIpoFXHpverimCvz084KA7L483rqJ44c3jDtzv6d4qOSoR/V9ywSHfV+Ks1lwIj2P49BFhunLNAA==", + "version": "0.64.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-darwin-x64/-/binding-darwin-x64-0.64.0.tgz", + "integrity": "sha512-gCmuswrgrOSajV4HCRFkVCGIruPq8bjYuPYgSE2WQB3mD6XrdyZ3JMSRZCkQ8zCxOyGWriBo6QoZ5nmMHQ1BfA==", "cpu": [ "x64" ], @@ -286,9 +286,9 @@ } }, "node_modules/@oxfmt/binding-freebsd-x64": { - "version": "0.63.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-freebsd-x64/-/binding-freebsd-x64-0.63.0.tgz", - "integrity": "sha512-BDfKY+KhL2078cgswBBFQPAYuxCy93bS/iC5frdSeSbTLcGrR6VC2hsuPTanoJmg84+wSyWl0wWC1eR+uTnkRg==", + "version": "0.64.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-freebsd-x64/-/binding-freebsd-x64-0.64.0.tgz", + "integrity": "sha512-Ab8g7a38pT0MMImjh7anRSTve6buWBIlcXIFBYa5xl4s6UxEgKSc2xOOhbGtLwvXnEi2PsEDGoJh3oUU7xkehQ==", "cpu": [ "x64" ], @@ -303,9 +303,9 @@ } }, "node_modules/@oxfmt/binding-linux-arm-gnueabihf": { - "version": "0.63.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-0.63.0.tgz", - "integrity": "sha512-Ov1cQEXT4mj7cojAokWSS1eoxkoyvbDfAbxNsGIKY2o36kvdAaFzPxRN6NxFRk9fD72B8oCoTTX/NuYTUWlpsg==", + "version": "0.64.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-0.64.0.tgz", + "integrity": "sha512-BgvS3CoQ+Xy2deoZqEN8JVKabcCZi2RxA3yant8G9OAv9KuPJ9TCjHkqigzdHUVwErZxEP5d2bzLIEyKYyBDLg==", "cpu": [ "arm" ], @@ -320,9 +320,9 @@ } }, "node_modules/@oxfmt/binding-linux-arm-musleabihf": { - "version": "0.63.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-0.63.0.tgz", - "integrity": "sha512-0LE7ro3+6L79jcMANycAZfRaC7zxr9YZ2+vEL5uMD9QlEep+rS/r1kSJsnuLl991NXJZD60euh0PC1GHrR20vw==", + "version": "0.64.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-0.64.0.tgz", + "integrity": "sha512-QXpNxwoMj0YvnceCNZadNSden3bIcnvjn/sDp/rwZhRoZoZYGpHvtPyhGsdJz9uvT9GkaMW7SsLddurU56dt8w==", "cpu": [ "arm" ], @@ -337,9 +337,9 @@ } }, "node_modules/@oxfmt/binding-linux-arm64-gnu": { - "version": "0.63.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-0.63.0.tgz", - "integrity": "sha512-izPk+2Z4gjuZK32Fqh5qXoMpT/2NXzLh++ob57HiEiVSQZ1iYXu8EKMzb+K5AvWyIEXhdDIt7ADjGGtFhkT9Bw==", + "version": "0.64.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-0.64.0.tgz", + "integrity": "sha512-BBgH3I1ppDsI5pZ4Pdhw0ceYxwVCfbU/bZEBCeZ6caRS9x0ZabErxubP7riGUn11PXZBhe8DYdjkDKP1FlVQ5w==", "cpu": [ "arm64" ], @@ -357,9 +357,9 @@ } }, "node_modules/@oxfmt/binding-linux-arm64-musl": { - "version": "0.63.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm64-musl/-/binding-linux-arm64-musl-0.63.0.tgz", - "integrity": "sha512-alPmbOuWXFXiSo+lOtv6X71C7SYMEDW2WVvywOvf9BwKgEhSNGhMTLeFVSjKUMCamcjbbgVdsWF8GN1uy8xshg==", + "version": "0.64.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm64-musl/-/binding-linux-arm64-musl-0.64.0.tgz", + "integrity": "sha512-v19HSjC/BGXdt26qEvKZtwAHgGmQ2Agcap2kQP+KIqoRZqivVzYth3ui2dJA1i+6/fjpjga85lIOaJJjQ/bOOw==", "cpu": [ "arm64" ], @@ -377,9 +377,9 @@ } }, "node_modules/@oxfmt/binding-linux-ppc64-gnu": { - "version": "0.63.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-0.63.0.tgz", - "integrity": "sha512-BdzCPvolJc4AWZ+YMzgUDJcDzbQWrFjYuqBHoNHNqP1aCaluQRJNs4k3vNU5IG7vTpjf9zeD73D7MFM1TecZpg==", + "version": "0.64.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-0.64.0.tgz", + "integrity": "sha512-PElLnOo4xFTBZrxPhgTIj0eHqZXwEBQoNWtb7facUV170T0B0FRET0iNbb3LUeLWTybkUW+vsdyv4ihOdyXGyw==", "cpu": [ "ppc64" ], @@ -397,9 +397,9 @@ } }, "node_modules/@oxfmt/binding-linux-riscv64-gnu": { - "version": "0.63.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-0.63.0.tgz", - "integrity": "sha512-7sIgfLzqtNKSkMGsGVyRpHwpjNezRg2XONvUOheFZs95TSZpM0JAuPpA8KrQFsWc4wPU95roX2O69JgH8igOgw==", + "version": "0.64.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-0.64.0.tgz", + "integrity": "sha512-Qzsg15n4F5CH+MorcRW4MkAEMiLzXmeG+DiDSbP/bBTqCmWOH3K9DHryNrve+JHlV0txS+B6Z9P5Xz+cmWeL+g==", "cpu": [ "riscv64" ], @@ -417,9 +417,9 @@ } }, "node_modules/@oxfmt/binding-linux-riscv64-musl": { - "version": "0.63.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-0.63.0.tgz", - "integrity": "sha512-9Tcg0y0WcVa6Mm9AgcgFMseDS+VkFJZpKZ8We9SpDY4gg5jewSwln+0sO04QLcTS1BtfDl9MwR+NfID8L7PUTg==", + "version": "0.64.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-0.64.0.tgz", + "integrity": "sha512-/GZ358wnQ/Ez4UVnCcZIi56JkY0sOdZ+B108pqXKqZz3jLS59F4KEAB1Qv3fRlObrFEk+3L2vUQ/xoPx+3vjXw==", "cpu": [ "riscv64" ], @@ -437,9 +437,9 @@ } }, "node_modules/@oxfmt/binding-linux-s390x-gnu": { - "version": "0.63.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-0.63.0.tgz", - "integrity": "sha512-qWKC1pEOpx1qYhXaugPhHUeXwSfqEOk2wJH2LqVXGPV5iQYfdAZdt+d2XDiX4DTSWA2QDMUcFB+wEORh3Xn/sA==", + "version": "0.64.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-0.64.0.tgz", + "integrity": "sha512-/C9We3DXegowfLXtVCYHeNiU9azwCDr5cQkEtCVlc74vyn+lLQSPApJ1CZmxAduqeq/Oi3gQ+IVptyhCaTMtkQ==", "cpu": [ "s390x" ], @@ -457,9 +457,9 @@ } }, "node_modules/@oxfmt/binding-linux-x64-gnu": { - "version": "0.63.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-x64-gnu/-/binding-linux-x64-gnu-0.63.0.tgz", - "integrity": "sha512-S9wXYOiGSqYGS4Fx/TFsY+xDd/7dE5s+rUgbA4TsHiVF9e8J3ZcKmP7dsP/7iqLI9Wz7Ic7TzEr3mdthRCTdrA==", + "version": "0.64.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-x64-gnu/-/binding-linux-x64-gnu-0.64.0.tgz", + "integrity": "sha512-91KM2CeRWscIEHlj1NsW2WSnzGeq1Ehq+39bfDowTdkn+fcvK/x4Y1RcyqT7glyBjZio0ldkeCG6Usj3v7ASog==", "cpu": [ "x64" ], @@ -477,9 +477,9 @@ } }, "node_modules/@oxfmt/binding-linux-x64-musl": { - "version": "0.63.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-x64-musl/-/binding-linux-x64-musl-0.63.0.tgz", - "integrity": "sha512-5eGyTJuMZNwBSHCivXt8Yuta6GeTYksOPXRk2MIhajiyFGQx7bjaHIwY+ZusAoFHhT157A9x6sktLjYo9D5oMQ==", + "version": "0.64.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-x64-musl/-/binding-linux-x64-musl-0.64.0.tgz", + "integrity": "sha512-gw7uEk9I+7zoT1EYLra1eWArIzNcz8e3jkv+Noo2+o2T7wPvsNSQbfoa4DSfZlvn1i6mJ05RiZ4/omaXPDNhQg==", "cpu": [ "x64" ], @@ -497,9 +497,9 @@ } }, "node_modules/@oxfmt/binding-openharmony-arm64": { - "version": "0.63.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-openharmony-arm64/-/binding-openharmony-arm64-0.63.0.tgz", - "integrity": "sha512-Rz7hx+Dv3DoW/S6pwVAyjfFXp7/trdQ1zg+vNmsdsdDNlUccugp4XNqambSuEAeP0DaG9k72AtNyfDXCEg0AGw==", + "version": "0.64.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-openharmony-arm64/-/binding-openharmony-arm64-0.64.0.tgz", + "integrity": "sha512-HYHFf616FHSPSO07c09mjmXBfQ73wIVM3m0txOiooa5XZkGoxFd6B14PVj0LB0DXIqJ6wAO/dDR/NX/5UUaqnw==", "cpu": [ "arm64" ], @@ -514,9 +514,9 @@ } }, "node_modules/@oxfmt/binding-win32-arm64-msvc": { - "version": "0.63.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-0.63.0.tgz", - "integrity": "sha512-T/IuizKN9mr4Xw6YYnptkXRNdLkyIlUZ7c8zfTOBpoytZyJ1BAsMUvsMDEx0X4YvSMpaivm+DR8112rQfzC25g==", + "version": "0.64.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-0.64.0.tgz", + "integrity": "sha512-uQjFp081IZSWD6VAofX2iO2z01awAdHmfC+NrieWIPKrT2hZKQDyq/U18M7ifC0sm0Wz8aHY/p6+FDYIzs/CrQ==", "cpu": [ "arm64" ], @@ -531,9 +531,9 @@ } }, "node_modules/@oxfmt/binding-win32-ia32-msvc": { - "version": "0.63.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-0.63.0.tgz", - "integrity": "sha512-XjrO5FJ5Wl9vsAxtCP1G/eaeT6y1K2s9CICUHGE42cEjou32/J6S+B1KnrOAboj6E7uhJnwPbRSvznWcxNdA0g==", + "version": "0.64.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-0.64.0.tgz", + "integrity": "sha512-lNM6byTAQ881jugzFu8juJTbNRgsUTlswMA6pJmwi1XDvmIqnnb49lcUAs5gz94fCJLrVN+/X3s3jOKqx23WIQ==", "cpu": [ "ia32" ], @@ -548,9 +548,9 @@ } }, "node_modules/@oxfmt/binding-win32-x64-msvc": { - "version": "0.63.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-win32-x64-msvc/-/binding-win32-x64-msvc-0.63.0.tgz", - "integrity": "sha512-sgsHCQy432OTQH4Ikk3tZptp3GqwnhwUDuY0loBH41zyHWfMZY9v8Dy78wsnSofHejvFozZGgJgBB1A0LQRwMQ==", + "version": "0.64.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-win32-x64-msvc/-/binding-win32-x64-msvc-0.64.0.tgz", + "integrity": "sha512-BtmbtL/QjMtF1a6C3CqoDluH2IfB6fJt62E+B9RFfUPtFk4Iz9PFS6+y/SzzOvSxc7aUk2Kphwg7Dh8lMbwu6g==", "cpu": [ "x64" ], @@ -565,9 +565,9 @@ } }, "node_modules/@oxlint/binding-android-arm-eabi": { - "version": "1.78.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm-eabi/-/binding-android-arm-eabi-1.78.0.tgz", - "integrity": "sha512-Bu819lmAfZMUHErrpe0cEWj3iaefuUODHSU8+UbXy67V/r7/7f4K3FL0NmbD85E+wiFLDYuhP8Zlv0XnVeXshw==", + "version": "1.79.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm-eabi/-/binding-android-arm-eabi-1.79.0.tgz", + "integrity": "sha512-TebFaaMklO/RXzTv7PucaCq9l3X6D1gA+C8H6K4njtjFOV+zWE9MKLpulcJZN9bzytbUbQIY0mZuz12nQ5Kv4Q==", "cpu": [ "arm" ], @@ -582,9 +582,9 @@ } }, "node_modules/@oxlint/binding-android-arm64": { - "version": "1.78.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm64/-/binding-android-arm64-1.78.0.tgz", - "integrity": "sha512-CDfxZgB61B7buRdY2FJoAYYPPXCZ1EoC1LKscnC5dg3kjobdxiconvAvvN1BmHyW4PyFT3jRLDag/BY/roSNBQ==", + "version": "1.79.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm64/-/binding-android-arm64-1.79.0.tgz", + "integrity": "sha512-KqqnOtAVgNsPPF0YSodkFZA1O80jcKoCZCTu3bgsszxA+MrMP9TLzfXitKjEj1FmrPprKDMdRDMmY3weESO9sg==", "cpu": [ "arm64" ], @@ -599,9 +599,9 @@ } }, "node_modules/@oxlint/binding-darwin-arm64": { - "version": "1.78.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-arm64/-/binding-darwin-arm64-1.78.0.tgz", - "integrity": "sha512-2Y2U9Ahrz+OO0Ej88f9SJYq51/jUBp1Mc7iZu0ukrbeeZ3gpRGfzIFnoqfHDY96xr0GEfNrPUBFEy0nN5aD7HA==", + "version": "1.79.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-arm64/-/binding-darwin-arm64-1.79.0.tgz", + "integrity": "sha512-BVC2nsMzqQzRDPc5RhixkZ+m1p7iH4bxRRvqkbwDXX0PlQKm1BPy8J8cRjnAFafOq2QzI+BfO3vE8w2GZ3CBag==", "cpu": [ "arm64" ], @@ -616,9 +616,9 @@ } }, "node_modules/@oxlint/binding-darwin-x64": { - "version": "1.78.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-x64/-/binding-darwin-x64-1.78.0.tgz", - "integrity": "sha512-rpych6eJq6m9jDRypTEaPD1xysaEW5h9+xuxhGK/QhOg+/xaqPZrCrTNoIl/f3nEjuJeCEmstNDlrE9rJi/3/g==", + "version": "1.79.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-x64/-/binding-darwin-x64-1.79.0.tgz", + "integrity": "sha512-p6Lm+snmhGuLKL1+CpCV8L6ijkE/qJzK2H2jG9+eKJT0n31RbY4FLsdhexekgP3bLpw4Kgde+9DZuDZQ4yIInA==", "cpu": [ "x64" ], @@ -633,9 +633,9 @@ } }, "node_modules/@oxlint/binding-freebsd-x64": { - "version": "1.78.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-freebsd-x64/-/binding-freebsd-x64-1.78.0.tgz", - "integrity": "sha512-IcMGrQT3QizkOESUJd5et+rOhVqSkNDfNik1cvrKDqIbzqx9KMtRswpFgkCuNTSwylCFLKhGUu8KmqY1ZnC0Dg==", + "version": "1.79.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-freebsd-x64/-/binding-freebsd-x64-1.79.0.tgz", + "integrity": "sha512-qDMm0dXZnoHyRqSL4N4xUq82T4sqK5cbKSjvd/dF/YbMUXc2R1wEPf+vmA5S0qUmi0nwXfNbjXBtZaIqzQLIMg==", "cpu": [ "x64" ], @@ -650,9 +650,9 @@ } }, "node_modules/@oxlint/binding-linux-arm-gnueabihf": { - "version": "1.78.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.78.0.tgz", - "integrity": "sha512-/uLdoJ0IXE6vo/0f0LKjinQAp+re+VMaCWaNT8ENIv2EOCkSsc8SGaflXAuW0Jua2dq5+GLVWm1NQK7P3UFSNQ==", + "version": "1.79.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.79.0.tgz", + "integrity": "sha512-2od7s0nuKPzqyUZAWk9KkCyGg7eI9dwFPZg+20lB15fKFkVZ0c9ZFxqPfiBAyDTlTkh9stPI0t+JlPCqMbItVA==", "cpu": [ "arm" ], @@ -667,9 +667,9 @@ } }, "node_modules/@oxlint/binding-linux-arm-musleabihf": { - "version": "1.78.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-1.78.0.tgz", - "integrity": "sha512-7xi4Wb/O8NRJhLoUXmDJMUVpNYvB5kefdhFU1Jb8rtae4QoXlTiLwI14X4YvAXVZLNZChP8m5qO9SQAlWQTbkQ==", + "version": "1.79.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-1.79.0.tgz", + "integrity": "sha512-ZOQUjkzDnvlhSE3+tWC3YXx94MMl+sYMlwH+u1+YGApGHOJP/YAc8ZBRFOXZ6eOBmxtXAWuS/fBcdZr8qqNO1A==", "cpu": [ "arm" ], @@ -684,9 +684,9 @@ } }, "node_modules/@oxlint/binding-linux-arm64-gnu": { - "version": "1.78.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.78.0.tgz", - "integrity": "sha512-4hFW0+fVXa3OIh1Y4A5SPkmvI4wuuBSrCVKzOyE7PTjhc7yEqZ1pmvEEeS5Lj/MaqvegFxXyF33N+6jkehxdyg==", + "version": "1.79.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.79.0.tgz", + "integrity": "sha512-lu158FR4nGqGeRS3BQvtG85wRgU/Fy4MD5Cxp1hzJXizGiLo6u2742wJSCDKh8cFcZntvX7fcxlq4mMmfryH1g==", "cpu": [ "arm64" ], @@ -704,9 +704,9 @@ } }, "node_modules/@oxlint/binding-linux-arm64-musl": { - "version": "1.78.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.78.0.tgz", - "integrity": "sha512-oC0mvsgBJjlMijSDEhx9KuvR9zYeHXceA9MjbuXB1F8NSR78Yj2unOBrstEvTVaq+pko+kuue6DajC00eqvTdg==", + "version": "1.79.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.79.0.tgz", + "integrity": "sha512-mbpKQeE2aflTjddaHK7MP8KP/OFbUM++lt5M635ENM8IyIdK0jm2t9pb+2v9mVVIvhF6TqA4l7F79Pll1mi+uw==", "cpu": [ "arm64" ], @@ -724,9 +724,9 @@ } }, "node_modules/@oxlint/binding-linux-ppc64-gnu": { - "version": "1.78.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.78.0.tgz", - "integrity": "sha512-XAllT5SUZS+ohjuZ3/5S0cwe0r7eboiuigeStCZ5DXRYx/2KVM2UvQXvAfyzXEimtQjAB7cDQ2YxDe2Zl2WNQQ==", + "version": "1.79.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.79.0.tgz", + "integrity": "sha512-WpGNua7gaxaHnpSDeog2ji8IDHn/QLPl9LPzwkR/FvVv58vT5BcXjRXnU+wbu3N75cpeha8CdC7ho/U2OIsB4g==", "cpu": [ "ppc64" ], @@ -744,9 +744,9 @@ } }, "node_modules/@oxlint/binding-linux-riscv64-gnu": { - "version": "1.78.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-1.78.0.tgz", - "integrity": "sha512-trucMER/0QtecoXvc1y/UVqE3kwJipDwrx4oHfj+nNm3dq2zjP44WT0CfHNDPM3G1DXIkx/gY6lAD21NSCZVhA==", + "version": "1.79.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-1.79.0.tgz", + "integrity": "sha512-tK1E93A5LVzISg4ngpKJnfTs7EqtIUceGI7MQ4GyDjJiLi8wPCkEyKlj2xkyKWZ1yzkDJyLHTBJ5/iFWRdnJvg==", "cpu": [ "riscv64" ], @@ -764,9 +764,9 @@ } }, "node_modules/@oxlint/binding-linux-riscv64-musl": { - "version": "1.78.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-1.78.0.tgz", - "integrity": "sha512-cm3O4F/HQbdzOUX5mKHqG5KDL6E5w0pnlZ+fbBy2rmLryPOowkuLagFHTopQsEIpjcaZoPOrL+BmmAytAG9HFg==", + "version": "1.79.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-1.79.0.tgz", + "integrity": "sha512-qhQvUIrngXivA2A9pQ+xPCychztn/5qUv7yS3gDwXv3w7Rag+eTeeXWmRyx+t7XsW5x6LuY/8AsTq36UgFIblg==", "cpu": [ "riscv64" ], @@ -784,9 +784,9 @@ } }, "node_modules/@oxlint/binding-linux-s390x-gnu": { - "version": "1.78.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.78.0.tgz", - "integrity": "sha512-33wRf6HqGNsybJ3qX4cGaQN2ODPxNmc1rMa0mrTmx3eFq1VzOnvQooi9bIGVYakW8a/wmqVx1mgsUm8R2xfTiw==", + "version": "1.79.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.79.0.tgz", + "integrity": "sha512-sv6AaVgU/eE6u+6WFiQVDcPPwTxP6IJMSB9k701W2r/r6Tx465e8vPvVyRxquNH4Vy6KwRNu90mVbxXJN8+5gg==", "cpu": [ "s390x" ], @@ -804,9 +804,9 @@ } }, "node_modules/@oxlint/binding-linux-x64-gnu": { - "version": "1.78.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.78.0.tgz", - "integrity": "sha512-rRdISSYegj6VganMZ9tjRjijowfHJ09IZU01i0toBAqr6n5LEtwHq2IeS4FjW2RoskOHlb6efB26H5izYb3GEQ==", + "version": "1.79.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.79.0.tgz", + "integrity": "sha512-iFZL02deziHslb3jEX9KdqlAkYoo4fGyotchKDzdfK1f5mxlIBeiQeHhvK3iFpuEJSB4ma/qeFn9oxPiwnhUPQ==", "cpu": [ "x64" ], @@ -824,9 +824,9 @@ } }, "node_modules/@oxlint/binding-linux-x64-musl": { - "version": "1.78.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-musl/-/binding-linux-x64-musl-1.78.0.tgz", - "integrity": "sha512-GmsP4rW0xTL6u5CVdcDsaN5Fbc7hBc382Wmar1kttbnwSEviM+rSINKOMQ+UQ6iH+AGwC+8gaAiwu134Tgh6Lg==", + "version": "1.79.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-musl/-/binding-linux-x64-musl-1.79.0.tgz", + "integrity": "sha512-3DtZR2raqObnh7wXZoFYFd0Fw7skBvcb3f7A+/lkEiDuh8hrE6vv9b/62Qxao1a9/OeHLw/FcXlXzgsW9wTRFg==", "cpu": [ "x64" ], @@ -844,9 +844,9 @@ } }, "node_modules/@oxlint/binding-openharmony-arm64": { - "version": "1.78.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-openharmony-arm64/-/binding-openharmony-arm64-1.78.0.tgz", - "integrity": "sha512-sy9yeYuADc8a+n4TLBayzMCZiHPW78DcIFVpOXTmdKHWQeM9xe5uzkqIIZmi326D5hY9XVwacipEB1p7tQjPAg==", + "version": "1.79.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-openharmony-arm64/-/binding-openharmony-arm64-1.79.0.tgz", + "integrity": "sha512-Oatt4GuA1WJkqzk2ozx4HrWROOi7opV3AKDw/U8qDIqeTqzsjn5K2x3REJMNjU3/KU/Bkq96Zi3CknaiDTaC/Q==", "cpu": [ "arm64" ], @@ -861,9 +861,9 @@ } }, "node_modules/@oxlint/binding-win32-arm64-msvc": { - "version": "1.78.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.78.0.tgz", - "integrity": "sha512-rjc2hF1KfMi8fZj1X/m3AmnHbdsF3rL0v6KQg0Uc880Yb2khjz+3U14sfdZ7jWTpRnN1m1NQa/TT7uU9lJWPrA==", + "version": "1.79.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.79.0.tgz", + "integrity": "sha512-NAgZr9Qp8nIA9rpo0JEvwiabTF/2UVqBNnupBG9X4kxXcQoScJUTi+qHhvabb9s/thgj5wQ4XcIaJvb+ZMgoKw==", "cpu": [ "arm64" ], @@ -878,9 +878,9 @@ } }, "node_modules/@oxlint/binding-win32-ia32-msvc": { - "version": "1.78.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.78.0.tgz", - "integrity": "sha512-zcuXFVrEFHIafRfkCQT8w/Xe41o07ozl/vwHq7p94vB29xVzsB0sZGYORU1jhcYKv3Lr0J3HbJ2T4fHH5rWmvA==", + "version": "1.79.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.79.0.tgz", + "integrity": "sha512-+KyXjIvcpaXmWW/j9NNY5yWjrIVxaX18VyIheQy3jwc2GSYgpCr7MGI/HxIGQ/shAL5IWEKbhsqoMpAO5Stiog==", "cpu": [ "ia32" ], @@ -895,9 +895,9 @@ } }, "node_modules/@oxlint/binding-win32-x64-msvc": { - "version": "1.78.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.78.0.tgz", - "integrity": "sha512-Sb5ocmLSuYeOuXd+CFOToGKp/gjXUEWDnvIGwhnh8aq8wY4TMmEnKnvbogSW7RdMZv77JSARduS7/gv+khYEjA==", + "version": "1.79.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.79.0.tgz", + "integrity": "sha512-mEelcCMMBS57sIXh2veGMNy+pQwuGtcMxHxGIZWQ5Ba9pJ5jCCUFOZB9E2JhBaxGsURe+WGe0zJp4RVre52gpQ==", "cpu": [ "x64" ], @@ -2073,9 +2073,9 @@ } }, "node_modules/oxfmt": { - "version": "0.63.0", - "resolved": "https://registry.npmjs.org/oxfmt/-/oxfmt-0.63.0.tgz", - "integrity": "sha512-kgdDwv35wvVf6554U2Ab8Jnd0zTM+TsEQWwaB70RAjK3gICFAFGO+2Hd3Be27GMoXj3XRL9IKSNRVl7KBQL6iw==", + "version": "0.64.0", + "resolved": "https://registry.npmjs.org/oxfmt/-/oxfmt-0.64.0.tgz", + "integrity": "sha512-XZ4GFBN/PLbXKq+0zrgpQfPKYuJlUuj+nzZJY7UpIbFMNyefNLCdN9EwViycNqnYcv0wrn0jXcQLlqJp8RCKBg==", "dev": true, "license": "MIT", "dependencies": { @@ -2091,25 +2091,25 @@ "url": "https://github.com/sponsors/Boshen" }, "optionalDependencies": { - "@oxfmt/binding-android-arm-eabi": "0.63.0", - "@oxfmt/binding-android-arm64": "0.63.0", - "@oxfmt/binding-darwin-arm64": "0.63.0", - "@oxfmt/binding-darwin-x64": "0.63.0", - "@oxfmt/binding-freebsd-x64": "0.63.0", - "@oxfmt/binding-linux-arm-gnueabihf": "0.63.0", - "@oxfmt/binding-linux-arm-musleabihf": "0.63.0", - "@oxfmt/binding-linux-arm64-gnu": "0.63.0", - "@oxfmt/binding-linux-arm64-musl": "0.63.0", - "@oxfmt/binding-linux-ppc64-gnu": "0.63.0", - "@oxfmt/binding-linux-riscv64-gnu": "0.63.0", - "@oxfmt/binding-linux-riscv64-musl": "0.63.0", - "@oxfmt/binding-linux-s390x-gnu": "0.63.0", - "@oxfmt/binding-linux-x64-gnu": "0.63.0", - "@oxfmt/binding-linux-x64-musl": "0.63.0", - "@oxfmt/binding-openharmony-arm64": "0.63.0", - "@oxfmt/binding-win32-arm64-msvc": "0.63.0", - "@oxfmt/binding-win32-ia32-msvc": "0.63.0", - "@oxfmt/binding-win32-x64-msvc": "0.63.0" + "@oxfmt/binding-android-arm-eabi": "0.64.0", + "@oxfmt/binding-android-arm64": "0.64.0", + "@oxfmt/binding-darwin-arm64": "0.64.0", + "@oxfmt/binding-darwin-x64": "0.64.0", + "@oxfmt/binding-freebsd-x64": "0.64.0", + "@oxfmt/binding-linux-arm-gnueabihf": "0.64.0", + "@oxfmt/binding-linux-arm-musleabihf": "0.64.0", + "@oxfmt/binding-linux-arm64-gnu": "0.64.0", + "@oxfmt/binding-linux-arm64-musl": "0.64.0", + "@oxfmt/binding-linux-ppc64-gnu": "0.64.0", + "@oxfmt/binding-linux-riscv64-gnu": "0.64.0", + "@oxfmt/binding-linux-riscv64-musl": "0.64.0", + "@oxfmt/binding-linux-s390x-gnu": "0.64.0", + "@oxfmt/binding-linux-x64-gnu": "0.64.0", + "@oxfmt/binding-linux-x64-musl": "0.64.0", + "@oxfmt/binding-openharmony-arm64": "0.64.0", + "@oxfmt/binding-win32-arm64-msvc": "0.64.0", + "@oxfmt/binding-win32-ia32-msvc": "0.64.0", + "@oxfmt/binding-win32-x64-msvc": "0.64.0" }, "peerDependencies": { "svelte": "^5.0.0", @@ -2125,9 +2125,9 @@ } }, "node_modules/oxlint": { - "version": "1.78.0", - "resolved": "https://registry.npmjs.org/oxlint/-/oxlint-1.78.0.tgz", - "integrity": "sha512-QgQePuxIqKOzo1KSjG2EnITEeWvWnKAm77eq8nrMtf6AGoA+zyGc4PFYtDNJSD25g/ibOwfQ851hZ4/SPkMVoA==", + "version": "1.79.0", + "resolved": "https://registry.npmjs.org/oxlint/-/oxlint-1.79.0.tgz", + "integrity": "sha512-hVJ9hq9m2unPS+Of4eJJgCPdIeCC+3DHEUX3tkmrPJr3OK2hz7PhXwgC+ZP71ZcYu8cCDEtQrqLxWNvxBppBVg==", "dev": true, "license": "MIT", "bin": { @@ -2140,25 +2140,25 @@ "url": "https://github.com/sponsors/Boshen" }, "optionalDependencies": { - "@oxlint/binding-android-arm-eabi": "1.78.0", - "@oxlint/binding-android-arm64": "1.78.0", - "@oxlint/binding-darwin-arm64": "1.78.0", - "@oxlint/binding-darwin-x64": "1.78.0", - "@oxlint/binding-freebsd-x64": "1.78.0", - "@oxlint/binding-linux-arm-gnueabihf": "1.78.0", - "@oxlint/binding-linux-arm-musleabihf": "1.78.0", - "@oxlint/binding-linux-arm64-gnu": "1.78.0", - "@oxlint/binding-linux-arm64-musl": "1.78.0", - "@oxlint/binding-linux-ppc64-gnu": "1.78.0", - "@oxlint/binding-linux-riscv64-gnu": "1.78.0", - "@oxlint/binding-linux-riscv64-musl": "1.78.0", - "@oxlint/binding-linux-s390x-gnu": "1.78.0", - "@oxlint/binding-linux-x64-gnu": "1.78.0", - "@oxlint/binding-linux-x64-musl": "1.78.0", - "@oxlint/binding-openharmony-arm64": "1.78.0", - "@oxlint/binding-win32-arm64-msvc": "1.78.0", - "@oxlint/binding-win32-ia32-msvc": "1.78.0", - "@oxlint/binding-win32-x64-msvc": "1.78.0" + "@oxlint/binding-android-arm-eabi": "1.79.0", + "@oxlint/binding-android-arm64": "1.79.0", + "@oxlint/binding-darwin-arm64": "1.79.0", + "@oxlint/binding-darwin-x64": "1.79.0", + "@oxlint/binding-freebsd-x64": "1.79.0", + "@oxlint/binding-linux-arm-gnueabihf": "1.79.0", + "@oxlint/binding-linux-arm-musleabihf": "1.79.0", + "@oxlint/binding-linux-arm64-gnu": "1.79.0", + "@oxlint/binding-linux-arm64-musl": "1.79.0", + "@oxlint/binding-linux-ppc64-gnu": "1.79.0", + "@oxlint/binding-linux-riscv64-gnu": "1.79.0", + "@oxlint/binding-linux-riscv64-musl": "1.79.0", + "@oxlint/binding-linux-s390x-gnu": "1.79.0", + "@oxlint/binding-linux-x64-gnu": "1.79.0", + "@oxlint/binding-linux-x64-musl": "1.79.0", + "@oxlint/binding-openharmony-arm64": "1.79.0", + "@oxlint/binding-win32-arm64-msvc": "1.79.0", + "@oxlint/binding-win32-ia32-msvc": "1.79.0", + "@oxlint/binding-win32-x64-msvc": "1.79.0" }, "peerDependencies": { "oxlint-tsgolint": ">=7.0.2001", diff --git a/package.json b/package.json index 8ac8fce..5f315d2 100644 --- a/package.json +++ b/package.json @@ -75,7 +75,7 @@ "@types/node": "^20.19.39", "@types/ws": "^8.18.1", "lint-staged": "^15.5.2", - "oxfmt": "^0.63.0", + "oxfmt": "^0.64.0", "oxlint": "^1.60.0", "rimraf": "^6.1.3", "typescript": "^5.0.0", diff --git a/rust-toolchain.toml b/rust-toolchain.toml new file mode 100644 index 0000000..d1ad785 --- /dev/null +++ b/rust-toolchain.toml @@ -0,0 +1,4 @@ +[toolchain] +channel = "1.98.0" +profile = "minimal" +components = ["clippy", "rustfmt"] diff --git a/rust/Cargo.lock b/rust/Cargo.lock index 059483f..e10bb2c 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -2587,9 +2587,9 @@ dependencies = [ [[package]] name = "wreq" -version = "6.0.0-rc.31" +version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b100d81c46113fa7f0719e43b26b13264b38589a4f059e6ebfccb5308159336" +checksum = "6c6564665357ec634ef9fae06dc26796b773e3cc0a94baf4e0ae17f9a12437ad" dependencies = [ "btls", "btls-sys", @@ -2662,9 +2662,9 @@ dependencies = [ [[package]] name = "wreq-util" -version = "3.0.0-rc.14" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d8d73c75be86fbde675988dbe5cb15f02567025c13f6ffab910361ad1ba6c89" +checksum = "833a5528fd9346f1f61a351104878be96024b5905f39774fc66909502da3acd5" dependencies = [ "brotli", "flate2", diff --git a/rust/Cargo.toml b/rust/Cargo.toml index d6d0b13..180402b 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -9,8 +9,8 @@ crate-type = ["cdylib"] [dependencies] # HTTP client with browser impersonation -wreq = { version = "6.0.0-rc.31", default-features = false, features = ["tokio-rt", "gzip", "brotli", "deflate", "zstd", "socks", "cookies", "json", "charset", "multipart", "stream", "hickory-dns", "system-proxy", "webpki-roots", "ws"] } -wreq-util = { version = "3.0.0-rc.14", features = ["emulation-serde"] } +wreq = { version = "0.16.0", default-features = false, features = ["tokio-rt", "gzip", "brotli", "deflate", "zstd", "socks", "cookies", "json", "charset", "multipart", "stream", "hickory-dns", "system-proxy", "webpki-roots", "ws"] } +wreq-util = { version = "0.2.0", features = ["emulation-serde"] } http-body-util = "0.1" # Neon for Node.js bindings diff --git a/rust/src/emulation/builders.rs b/rust/src/emulation/builders.rs index 5d3a29c..fc66651 100644 --- a/rust/src/emulation/builders.rs +++ b/rust/src/emulation/builders.rs @@ -276,7 +276,7 @@ pub fn apply_http2_options( if let Some(experimental_settings) = options.experimental_settings { if !experimental_settings.is_empty() { bail!( - "Unsupported emulation http2Options.experimentalSettings: wreq 6.0.0-rc.29 no longer exposes custom HTTP/2 settings" + "Unsupported emulation http2Options.experimentalSettings: wreq no longer exposes custom HTTP/2 settings" ); } } diff --git a/rust/src/napi/body.rs b/rust/src/napi/body.rs index dea151f..8dc501f 100644 --- a/rust/src/napi/body.rs +++ b/rust/src/napi/body.rs @@ -1,4 +1,5 @@ -use crate::store::body_store::{cancel_body, forbid_body_recycle, read_body_chunk}; +use crate::store::body_store::{cancel_body, forbid_body_recycle, read_body_all, read_body_chunk}; +use crate::store::runtime::runtime; use neon::prelude::*; use neon::types::JsBuffer; @@ -13,8 +14,8 @@ fn read_body_chunk_js(mut cx: FunctionContext) -> JsResult { let channel = cx.channel(); let (deferred, promise) = cx.promise(); - std::thread::spawn(move || { - let result = read_body_chunk(handle, size); + let _body_task = runtime().spawn(async move { + let result = read_body_chunk(handle, size).await; deferred.settle_with(&channel, move |mut cx| match result { Ok((chunk, done)) => { @@ -37,6 +38,23 @@ fn cancel_body_js(mut cx: FunctionContext) -> JsResult { Ok(cx.boolean(cancel_body(handle))) } +fn read_body_all_js(mut cx: FunctionContext) -> JsResult { + let handle = cx.argument::(0)?.value(&mut cx) as u64; + let channel = cx.channel(); + let (deferred, promise) = cx.promise(); + + let _body_task = runtime().spawn(async move { + let result = read_body_all(handle).await; + + deferred.settle_with(&channel, move |mut cx| match result { + Ok(body) => Ok(JsBuffer::from_slice(&mut cx, &body)?), + Err(error) => cx.throw_error(format!("{error:#}")), + }); + }); + + Ok(promise) +} + fn forbid_body_recycle_js(mut cx: FunctionContext) -> JsResult { let handle = cx.argument::(0)?.value(&mut cx) as u64; @@ -48,6 +66,7 @@ fn forbid_body_recycle_js(mut cx: FunctionContext) -> JsResult { pub fn register(cx: &mut ModuleContext) -> NeonResult<()> { cx.export_function("readBodyChunk", read_body_chunk_js)?; + cx.export_function("readBodyAll", read_body_all_js)?; cx.export_function("cancelBody", cancel_body_js)?; cx.export_function("forbidBodyRecycle", forbid_body_recycle_js)?; Ok(()) diff --git a/rust/src/napi/convert.rs b/rust/src/napi/convert.rs index 6a12757..f71d6b8 100644 --- a/rust/src/napi/convert.rs +++ b/rust/src/napi/convert.rs @@ -622,6 +622,7 @@ pub(crate) fn js_object_to_request_options( let timeout = js_value_to_non_negative_timeout_ms(cx, obj, "timeout")?; let read_timeout = js_value_to_non_negative_timeout_ms(cx, obj, "readTimeout")?; let connect_timeout = js_value_to_non_negative_timeout_ms(cx, obj, "connectTimeout")?; + let tcp_linger = js_value_to_non_negative_timeout_ms(cx, obj, "tcpLinger")?; let pool_idle_timeout = js_object_to_pool_idle_timeout(cx, obj)?; let pool_max_idle_per_host = obj .get_opt(cx, "poolMaxIdlePerHost")? @@ -700,6 +701,7 @@ pub(crate) fn js_object_to_request_options( timeout, read_timeout, connect_timeout, + tcp_linger, pool_idle_timeout, pool_max_idle_per_host, pool_max_size, @@ -760,6 +762,7 @@ pub(crate) fn js_object_to_websocket_options( Some(timeout) => Some(timeout), None => Some(30000), }; + let tcp_linger = js_value_to_non_negative_timeout_ms(cx, obj, "tcpLinger")?; let pool_idle_timeout = js_object_to_pool_idle_timeout(cx, obj)?; let pool_max_idle_per_host = obj .get_opt(cx, "poolMaxIdlePerHost")? @@ -844,6 +847,7 @@ pub(crate) fn js_object_to_websocket_options( disable_system_proxy, dns, timeout, + tcp_linger, pool_idle_timeout, pool_max_idle_per_host, pool_max_size, diff --git a/rust/src/napi/error.rs b/rust/src/napi/error.rs new file mode 100644 index 0000000..f9cc205 --- /dev/null +++ b/rust/src/napi/error.rs @@ -0,0 +1,25 @@ +use neon::prelude::*; + +fn is_dns_error(error: &anyhow::Error) -> bool { + error.chain().any(|cause| { + cause + .downcast_ref::() + .is_some_and(wreq::Error::is_dns) + }) +} + +pub(crate) fn throw_anyhow_error<'a, C, T>(cx: &mut C, error: anyhow::Error) -> JsResult<'a, T> +where + C: Context<'a>, + T: Value, +{ + let is_dns = is_dns_error(&error); + let js_error = JsError::error(cx, format!("{error:#}"))?; + + if is_dns { + let code = cx.string("ERR_DNS"); + js_error.set(cx, "code", code)?; + } + + cx.throw(js_error) +} diff --git a/rust/src/napi/mod.rs b/rust/src/napi/mod.rs index bdf2197..d1d57c3 100644 --- a/rust/src/napi/mod.rs +++ b/rust/src/napi/mod.rs @@ -1,5 +1,6 @@ mod body; mod convert; +mod error; mod profiles; mod request; mod upload; diff --git a/rust/src/napi/request.rs b/rust/src/napi/request.rs index 457df72..24823a2 100644 --- a/rust/src/napi/request.rs +++ b/rust/src/napi/request.rs @@ -1,4 +1,5 @@ use crate::napi::convert::{js_object_to_request_options, response_to_js_object}; +use crate::napi::error::throw_anyhow_error; use crate::store::client_store::remove_clients; use crate::store::request_store::{ cancel_request as cancel_request_handle, insert_request, remove_request, @@ -16,19 +17,17 @@ fn request(mut cx: FunctionContext) -> JsResult { let (cancel_tx, cancel_rx) = tokio::sync::oneshot::channel::<()>(); let handle = insert_request(cancel_tx); - std::thread::spawn(move || { - let result = runtime().block_on(async move { - tokio::select! { - result = make_request(options) => result, - _ = cancel_rx => Err(anyhow::anyhow!("Request aborted")), - } - }); + let _request_task = runtime().spawn(async move { + let result = tokio::select! { + result = make_request(options) => result, + _ = cancel_rx => Err(anyhow::anyhow!("Request aborted")), + }; remove_request(handle); deferred.settle_with(&channel, move |mut cx| match result { Ok(response) => response_to_js_object(&mut cx, response), - Err(error) => cx.throw_error(format!("{:#}", error)), + Err(error) => throw_anyhow_error(&mut cx, error), }); }); diff --git a/rust/src/napi/websocket.rs b/rust/src/napi/websocket.rs index a6d9f4b..eea6ec6 100644 --- a/rust/src/napi/websocket.rs +++ b/rust/src/napi/websocket.rs @@ -1,4 +1,5 @@ use crate::napi::convert::{js_object_to_websocket_options, websocket_to_js_object}; +use crate::napi::error::throw_anyhow_error; use crate::store::runtime::runtime; use crate::store::websocket_connect_store::{ cancel_websocket_connect, insert_websocket_connect, remove_websocket_connect, @@ -33,7 +34,7 @@ fn websocket_connect_js(mut cx: FunctionContext) -> JsResult { deferred.settle_with(&channel, move |mut cx| match result { Ok(websocket) => websocket_to_js_object(&mut cx, websocket), - Err(error) => cx.throw_error(format!("{:#}", error)), + Err(error) => throw_anyhow_error(&mut cx, error), }); }); diff --git a/rust/src/store/body_store.rs b/rust/src/store/body_store.rs index 5d24471..5f44425 100644 --- a/rust/src/store/body_store.rs +++ b/rust/src/store/body_store.rs @@ -1,4 +1,3 @@ -use crate::store::runtime::runtime; use anyhow::{Context, Result}; use http_body_util::BodyExt; use std::collections::HashMap; @@ -48,9 +47,9 @@ fn remove_body(handle: u64) -> Option { .remove(&handle) } -pub fn read_body_chunk(handle: u64, _size: usize) -> Result<(Vec, bool)> { +pub async fn read_body_chunk(handle: u64, _size: usize) -> Result<(Vec, bool)> { let body = get_body(handle)?; - let chunk = match runtime().block_on(async { + let chunk = match async { let mut body = body.lock().await; loop { match body.response.frame().await { @@ -64,7 +63,9 @@ pub fn read_body_chunk(handle: u64, _size: usize) -> Result<(Vec, bool)> { } } .context("Failed to read response body chunk") - }) { + } + .await + { Ok(chunk) => chunk, Err(error) => { // The body errored, so the connection is unusable — drop the stored body to release it and its @@ -82,6 +83,27 @@ pub fn read_body_chunk(handle: u64, _size: usize) -> Result<(Vec, bool)> { Ok((chunk.to_vec(), false)) } +pub async fn read_body_all(handle: u64) -> Result> { + let body = + remove_body(handle).ok_or_else(|| anyhow::anyhow!("Unknown body handle: {handle}"))?; + let mut body = body.lock().await; + let mut bytes = Vec::new(); + + loop { + match body.response.frame().await { + Some(Ok(frame)) => { + if let Ok(data) = frame.into_data() { + bytes.extend_from_slice(&data); + } + } + Some(Err(error)) => { + return Err(error).context("Failed to read response body"); + } + None => return Ok(bytes), + } + } +} + pub fn cancel_body(handle: u64) -> bool { remove_body(handle).is_some() } @@ -100,6 +122,7 @@ pub fn forbid_body_recycle(handle: u64) -> Result { #[cfg(test)] mod tests { use super::*; + use crate::store::runtime::runtime; use std::io::{Read, Write}; use std::net::TcpListener; use std::thread; @@ -128,7 +151,7 @@ mod tests { let mut read_error = None; for _ in 0..3 { - match read_body_chunk(handle, 65_536) { + match runtime().block_on(read_body_chunk(handle, 65_536)) { Ok((_, done)) => assert!(!done, "truncated response unexpectedly completed"), Err(error) => { read_error = Some(error); @@ -147,4 +170,34 @@ mod tests { "failed response remained reachable in BODY_STORE" ); } + + #[test] + fn reads_complete_body_and_removes_handle() { + let listener = TcpListener::bind("127.0.0.1:0").expect("bind test server"); + let address = listener.local_addr().expect("read test server address"); + let server = thread::spawn(move || { + let (mut stream, _) = listener.accept().expect("accept test connection"); + let mut request = [0_u8; 4096]; + let _ = stream.read(&mut request).expect("read test request"); + stream + .write_all(b"HTTP/1.1 200 OK\r\nContent-Length: 11\r\n\r\nhello world") + .expect("write response"); + stream.flush().expect("flush response"); + }); + + let response = runtime() + .block_on(wreq::get(format!("http://{address}/")).send()) + .expect("receive response headers"); + let handle = store_body(response); + let body = runtime() + .block_on(read_body_all(handle)) + .expect("read response body"); + + server.join().expect("join test server"); + assert_eq!(body, b"hello world"); + assert!( + get_body(handle).is_err(), + "fully read response remained reachable in BODY_STORE" + ); + } } diff --git a/rust/src/transport/client.rs b/rust/src/transport/client.rs index da1a267..a42f703 100644 --- a/rust/src/transport/client.rs +++ b/rust/src/transport/client.rs @@ -20,6 +20,7 @@ struct ClientConfig { dns: Option, timeout: Option, connect_timeout: Option, + tcp_linger: Option, pool_idle_timeout: Option, pool_max_idle_per_host: Option, pool_max_size: Option, @@ -44,6 +45,7 @@ impl From<&RequestOptions> for ClientConfig { dns: options.dns.clone(), timeout: None, connect_timeout: options.connect_timeout, + tcp_linger: options.tcp_linger, pool_idle_timeout: options.pool_idle_timeout.clone(), pool_max_idle_per_host: options.pool_max_idle_per_host, pool_max_size: options.pool_max_size, @@ -70,6 +72,7 @@ impl From<&WebSocketConnectOptions> for ClientConfig { dns: options.dns.clone(), timeout: options.timeout, connect_timeout: None, + tcp_linger: options.tcp_linger, pool_idle_timeout: options.pool_idle_timeout.clone(), pool_max_idle_per_host: options.pool_max_idle_per_host, pool_max_size: options.pool_max_size, @@ -135,6 +138,9 @@ async fn build_client(config: ClientConfig) -> Result { if let Some(connect_timeout) = config.connect_timeout { builder = builder.connect_timeout(Duration::from_millis(connect_timeout)); } + if let Some(tcp_linger) = config.tcp_linger { + builder = builder.tcp_linger(Duration::from_millis(tcp_linger)); + } if let Some(pool_idle_timeout) = config.pool_idle_timeout { builder = match pool_idle_timeout { PoolIdleTimeout::Disabled => builder.pool_idle_timeout(None), diff --git a/rust/src/transport/request.rs b/rust/src/transport/request.rs index 17b09a2..ae15013 100644 --- a/rust/src/transport/request.rs +++ b/rust/src/transport/request.rs @@ -63,6 +63,7 @@ pub async fn make_request(options: RequestOptions) -> Result { timeout, read_timeout, connect_timeout: _, + tcp_linger: _, pool_idle_timeout: _, pool_max_idle_per_host: _, pool_max_size: _, diff --git a/rust/src/transport/types.rs b/rust/src/transport/types.rs index d807f79..daca6dc 100644 --- a/rust/src/transport/types.rs +++ b/rust/src/transport/types.rs @@ -121,6 +121,7 @@ pub struct RequestOptions { pub timeout: Option, pub read_timeout: Option, pub connect_timeout: Option, + pub tcp_linger: Option, pub pool_idle_timeout: Option, pub pool_max_idle_per_host: Option, pub pool_max_size: Option, @@ -161,6 +162,7 @@ pub struct WebSocketConnectOptions { pub disable_system_proxy: bool, pub dns: Option, pub timeout: Option, + pub tcp_linger: Option, pub pool_idle_timeout: Option, pub pool_max_idle_per_host: Option, pub pool_max_size: Option, diff --git a/rust/src/transport/websocket.rs b/rust/src/transport/websocket.rs index 478d0df..1c9efc5 100644 --- a/rust/src/transport/websocket.rs +++ b/rust/src/transport/websocket.rs @@ -144,6 +144,7 @@ pub(crate) async fn make_websocket( disable_system_proxy: _, dns: _, timeout: _, + tcp_linger: _, pool_idle_timeout: _, pool_max_idle_per_host: _, pool_max_size: _, diff --git a/scripts/smoke-native.mjs b/scripts/smoke-native.mjs new file mode 100644 index 0000000..08c644b --- /dev/null +++ b/scripts/smoke-native.mjs @@ -0,0 +1,18 @@ +import { createRequire } from 'node:module'; +import { resolve } from 'node:path'; + +const binaryPath = process.argv[2]; + +if (!binaryPath) { + throw new Error('Expected a native binary path'); +} + +const require = createRequire(import.meta.url); +const binding = require(resolve(binaryPath)); +const profiles = binding.getProfiles(); + +if (!Array.isArray(profiles) || profiles.length === 0) { + throw new Error('Native binding returned no browser profiles'); +} + +console.log(`Loaded ${binaryPath} with ${profiles.length} browser profiles`); diff --git a/scripts/update-wreq-upstream.mjs b/scripts/update-wreq-upstream.mjs index b7f2a89..b146d31 100644 --- a/scripts/update-wreq-upstream.mjs +++ b/scripts/update-wreq-upstream.mjs @@ -30,10 +30,10 @@ async function fetchLatestVersion(crate) { } const payload = await response.json(); - const latestVersion = payload?.crate?.max_version; + const latestVersion = payload?.crate?.max_stable_version; if (typeof latestVersion !== 'string' || latestVersion.length === 0) { - throw new Error(`crates.io did not return max_version for ${crate}`); + throw new Error(`crates.io did not return max_stable_version for ${crate}`); } return latestVersion; diff --git a/scripts/verify-linux-binary.mjs b/scripts/verify-linux-binary.mjs new file mode 100644 index 0000000..6748ce2 --- /dev/null +++ b/scripts/verify-linux-binary.mjs @@ -0,0 +1,112 @@ +import { spawnSync } from 'node:child_process'; +import { existsSync } from 'node:fs'; +import { resolve } from 'node:path'; + +const binaryArgument = process.argv[2]; + +if (!binaryArgument) { + throw new Error('Expected a native binary path'); +} + +const binaryPath = resolve(binaryArgument); + +if (!existsSync(binaryPath)) { + throw new Error(`Native binary does not exist: ${binaryPath}`); +} + +const abiLimits = new Map([ + ['GLIBC_', '2.28'], + ['GLIBCXX_', '3.4.25'], + ['CXXABI_', '1.3.11'], +]); + +const allowedLibraries = new Set([ + 'libatomic.so.1', + 'libc.so.6', + 'libdl.so.2', + 'libgcc_s.so.1', + 'libm.so.6', + 'libpthread.so.0', + 'libresolv.so.2', + 'librt.so.1', + 'libstdc++.so.6', + 'libutil.so.1', +]); + +function readelf(...args) { + const result = spawnSync('readelf', [...args, binaryPath], { + encoding: 'utf8', + }); + + if (result.error) { + throw result.error; + } + + if (result.status !== 0) { + throw new Error( + `readelf ${args.join(' ')} failed with exit code ${result.status}:\n${result.stderr}` + ); + } + + return result.stdout; +} + +function compareVersions(left, right) { + const leftParts = left.split('.').map(Number); + const rightParts = right.split('.').map(Number); + const length = Math.max(leftParts.length, rightParts.length); + + for (let index = 0; index < length; index += 1) { + const difference = (leftParts[index] ?? 0) - (rightParts[index] ?? 0); + + if (difference !== 0) { + return difference; + } + } + + return 0; +} + +function collectVersions(output, prefix) { + const escapedPrefix = prefix.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + const pattern = new RegExp(`\\b${escapedPrefix}(\\d+(?:\\.\\d+)*)\\b`, 'g'); + + return [...new Set([...output.matchAll(pattern)].map((match) => match[1]))].toSorted( + compareVersions + ); +} + +const versionInfo = readelf('--version-info', '--wide'); +const summaries = []; + +for (const [prefix, limit] of abiLimits) { + const versions = collectVersions(versionInfo, prefix); + const unsupported = versions.filter((version) => compareVersions(version, limit) > 0); + + if (unsupported.length > 0) { + throw new Error( + `${binaryPath} requires unsupported ${prefix}${unsupported.join(`, ${prefix}`)}; maximum supported version is ${prefix}${limit}` + ); + } + + if (versions.length > 0) { + summaries.push(`${prefix}${versions.at(-1)}`); + } +} + +const dynamicInfo = readelf('--dynamic', '--wide'); +const requiredLibraries = [...dynamicInfo.matchAll(/Shared library: \[([^\]]+)]/g)].map( + (match) => match[1] +); + +const unexpectedLibraries = requiredLibraries.filter((library) => !allowedLibraries.has(library)); + +if (unexpectedLibraries.length > 0) { + throw new Error( + `${binaryPath} requires unexpected shared libraries: ${unexpectedLibraries.join(', ')}` + ); +} + +console.log( + `Verified ${binaryPath}: ${summaries.join(', ') || 'no versioned GNU ABI imports'}; shared libraries: ${requiredLibraries.join(', ') || 'none'}` +); diff --git a/src/config/network.ts b/src/config/network.ts index 64e3c7e..8b76e3e 100644 --- a/src/config/network.ts +++ b/src/config/network.ts @@ -24,6 +24,18 @@ export function normalizeProxyOptions(proxy: WreqInit['proxy']): { }; } +export function normalizeTcpLinger(value?: number): number | undefined { + if (value === undefined) { + return undefined; + } + + if (!Number.isFinite(value) || value < 0) { + throw new TypeError('tcpLinger must be a finite non-negative number'); + } + + return value === 0 ? 0 : Math.max(1, Math.ceil(value)); +} + export function normalizeDnsOptions(dns?: DnsOptions): NativeDnsOptions | undefined { if (!dns) { return undefined; diff --git a/src/http/fetch.ts b/src/http/fetch.ts index ad230d6..14a16ee 100644 --- a/src/http/fetch.ts +++ b/src/http/fetch.ts @@ -15,6 +15,8 @@ import { isResponseStatusAllowed, normalizeRequestError, throwIfAborted } from ' import { mergeInputAndInit } from './pipeline/input'; import { buildNativeRequest, createRequest, resolveOptions } from './pipeline/options'; import { + applyRedirectReferrerPolicy, + DEFAULT_REDIRECT_REFERRER_POLICY, finalizeResponse, isRedirectResponse, resolveRedirectLocation, @@ -59,6 +61,7 @@ export async function fetchWithNativeClient( let request = createRequest(merged.urlInput, options); const redirectChain: RedirectEntry[] = []; const visitedRedirectTargets = new Set([request.url]); + let referrerPolicy = DEFAULT_REDIRECT_REFERRER_POLICY; let attempt = 1; while (true) { @@ -180,6 +183,13 @@ export async function fetchWithNativeClient( rewritten.bodyDropped ); + referrerPolicy = applyRedirectReferrerPolicy( + nextRequest.headers, + response, + nextUrl, + referrerPolicy + ); + if (options.cookieJar) { nextRequest.headers.delete('cookie'); } diff --git a/src/http/pipeline/cookies.ts b/src/http/pipeline/cookies.ts index 89d6345..cd23679 100644 --- a/src/http/pipeline/cookies.ts +++ b/src/http/pipeline/cookies.ts @@ -3,6 +3,18 @@ import { Headers } from '../../headers'; import { Request } from '../request'; import { Response } from '../response'; +export function toCookieOriginUrl(url: string): string { + const cookieUrl = new URL(url); + + if (cookieUrl.protocol === 'ws:') { + cookieUrl.protocol = 'http:'; + } else if (cookieUrl.protocol === 'wss:') { + cookieUrl.protocol = 'https:'; + } + + return cookieUrl.href; +} + export async function loadCookiesIntoRequest( cookieJar: CookieJar | undefined, request: Request @@ -32,7 +44,7 @@ export async function loadCookiesIntoHeaders( return; } - const cookies = await cookieJar.getCookies(url); + const cookies = await cookieJar.getCookies(toCookieOriginUrl(url)); if (cookies.length === 0) { return; diff --git a/src/http/pipeline/dispatch.ts b/src/http/pipeline/dispatch.ts index 4369632..9d791b2 100644 --- a/src/http/pipeline/dispatch.ts +++ b/src/http/pipeline/dispatch.ts @@ -2,6 +2,7 @@ import type { NativeRequestOptions, RequestStats, WreqInit } from '../../types'; import { AbortError, RequestError, TimeoutError } from '../../errors'; import { nativeRequest } from '../../native/index'; import { Response } from '../response'; +import { inferErrorCode } from './errors'; export async function reportStats( callback: WreqInit['onStats'] | undefined, @@ -31,7 +32,10 @@ export async function dispatchNativeRequest( throw new TimeoutError(message, { cause: error }); } - throw new RequestError(message, { cause: error }); + throw new RequestError(message, { + cause: error, + code: inferErrorCode(error), + }); }); const responseStart = Date.now(); diff --git a/src/http/pipeline/options.ts b/src/http/pipeline/options.ts index ecc2d81..98bcd54 100644 --- a/src/http/pipeline/options.ts +++ b/src/http/pipeline/options.ts @@ -10,6 +10,7 @@ import { normalizeDnsOptions, normalizeLocalBindOptions, normalizeProxyOptions, + normalizeTcpLinger, } from '../../config/network'; import { normalizeCertificateAuthority, @@ -170,6 +171,7 @@ export async function buildNativeRequest( ...timeout, ...readTimeout, ...connectTimeout, + tcpLinger: normalizeTcpLinger(options.tcpLinger), poolIdleTimeout: options.poolIdleTimeout, poolMaxIdlePerHost: options.poolMaxIdlePerHost, poolMaxSize: options.poolMaxSize, diff --git a/src/http/pipeline/redirects.ts b/src/http/pipeline/redirects.ts index 3b0ea87..9e72e08 100644 --- a/src/http/pipeline/redirects.ts +++ b/src/http/pipeline/redirects.ts @@ -4,7 +4,22 @@ import { Headers } from '../../headers'; import { normalizeMethod } from '../../native/index'; import { Response } from '../response'; -const REDIRECT_STATUS_CODES = new Set([300, 301, 302, 303, 307, 308]); +const REDIRECT_STATUS_CODES = new Set([301, 302, 303, 307, 308]); +const REFERRER_POLICIES = new Set([ + 'no-referrer', + 'no-referrer-when-downgrade', + 'same-origin', + 'origin', + 'strict-origin', + 'origin-when-cross-origin', + 'strict-origin-when-cross-origin', + 'unsafe-url', +]); + +export type RedirectReferrerPolicy = Exclude; + +export const DEFAULT_REDIRECT_REFERRER_POLICY: RedirectReferrerPolicy = + 'strict-origin-when-cross-origin'; export function isRedirectResponse(response: Response): boolean { return REDIRECT_STATUS_CODES.has(response.status) && response.headers.has('location'); @@ -41,7 +56,149 @@ export function stripRedirectSensitiveHeaders( if (new URL(previousUrl).origin !== new URL(nextUrl).origin) { headers.delete('authorization'); + headers.delete('cookie'); + headers.delete('cookie2'); + headers.delete('proxy-authorization'); + } +} + +function responseReferrerPolicy(response: Response): RedirectReferrerPolicy | undefined { + const value = response.headers.get('referrer-policy'); + + if (!value) { + return undefined; + } + + let policy: RedirectReferrerPolicy | undefined; + + for (const token of value.split(',')) { + const normalized = token.trim().toLowerCase() as RedirectReferrerPolicy; + + if (REFERRER_POLICIES.has(normalized)) { + policy = normalized; + } + } + + return policy; +} + +function parseReferrer(headers: Headers): URL | undefined { + const value = headers.get('referer'); + + if (!value) { + return undefined; } + + let referrer: URL; + + try { + referrer = new URL(value); + } catch { + headers.delete('referer'); + + return undefined; + } + + if (referrer.protocol !== 'http:' && referrer.protocol !== 'https:') { + headers.delete('referer'); + + return undefined; + } + + referrer.username = ''; + referrer.password = ''; + referrer.hash = ''; + + if (referrer.toString().length > 4096) { + referrer.pathname = '/'; + referrer.search = ''; + } + + return referrer; +} + +export function applyRedirectReferrerPolicy( + headers: Headers, + response: Response, + nextUrl: string, + currentPolicy: RedirectReferrerPolicy +): RedirectReferrerPolicy { + const policy = responseReferrerPolicy(response) ?? currentPolicy; + const referrer = parseReferrer(headers); + + if (!referrer) { + return policy; + } + + const destination = new URL(nextUrl); + const sameOrigin = referrer.origin === destination.origin; + const downgrade = referrer.protocol === 'https:' && destination.protocol === 'http:'; + let stripToOrigin = false; + + switch (policy) { + case 'no-referrer': + headers.delete('referer'); + + return policy; + + case 'no-referrer-when-downgrade': + if (downgrade) { + headers.delete('referer'); + + return policy; + } + + break; + + case 'same-origin': + if (!sameOrigin) { + headers.delete('referer'); + + return policy; + } + + break; + + case 'origin': + stripToOrigin = true; + break; + + case 'strict-origin': + if (downgrade) { + headers.delete('referer'); + + return policy; + } + + stripToOrigin = true; + break; + + case 'origin-when-cross-origin': + stripToOrigin = !sameOrigin; + break; + + case 'strict-origin-when-cross-origin': + if (!sameOrigin && downgrade) { + headers.delete('referer'); + + return policy; + } + + stripToOrigin = !sameOrigin; + break; + + case 'unsafe-url': + break; + } + + if (stripToOrigin) { + referrer.pathname = '/'; + referrer.search = ''; + } + + headers.set('Referer', referrer.toString()); + + return policy; } export function rewriteRedirectMethod( diff --git a/src/http/response.ts b/src/http/response.ts index 137f159..963845e 100644 --- a/src/http/response.ts +++ b/src/http/response.ts @@ -12,7 +12,12 @@ import { ReadableStream } from 'node:stream/web'; import { TextDecoder } from 'node:util'; import { RequestError, TimeoutError } from '../errors'; import { Headers } from '../headers'; -import { nativeCancelBody, nativeForbidBodyRecycle, nativeReadBodyChunk } from '../native/index'; +import { + nativeCancelBody, + nativeForbidBodyRecycle, + nativeReadBodyAll, + nativeReadBodyChunk, +} from '../native/index'; import { cloneBytes } from './body/bytes'; import { parseResponseFormData } from './body/form-data'; import { ResponseMeta } from './response-meta'; @@ -420,11 +425,45 @@ export class Response { return this.#stream; } + #lockConsumedBodyStream(): void { + const stream = new ReadableStream({ + start(controller) { + controller.close(); + }, + }); + + this.#streamSource = stream; + this.#stream = stream; + this.#orphanedStreamReaders.push(stream.getReader()); + } + async #consumeBytes(): Promise { if (this.#isBodyUnusable()) { throw new TypeError('Body is unusable: Body has already been read'); } + if (this.#bodyHandle !== null) { + const handle = this.#bodyHandle; + + this.#bodyHandle = null; + this.#bodyUsed = true; + bodyFinalizer.unregister(this.#bodyFinalizerToken); + this.#lockConsumedBodyStream(); + + try { + const bytes = await nativeReadBodyAll(handle); + + this.#payloadBytes = new Uint8Array(bytes); + + return new Uint8Array(this.#payloadBytes); + } catch (error) { + nativeCancelBody(handle); + throw toBodyReadError(error); + } finally { + this.#markBodyComplete(); + } + } + const stream = this.#ensureStream(); if (stream) { diff --git a/src/native/binding.ts b/src/native/binding.ts index ab5fba3..1b4e8c6 100644 --- a/src/native/binding.ts +++ b/src/native/binding.ts @@ -36,6 +36,7 @@ export type NativeBinding = { chunk: Buffer; done: boolean; }>; + readBodyAll: (handle: number) => Promise; cancelBody: (handle: number) => boolean; forbidBodyRecycle: (handle: number) => boolean; getProfiles: () => string[]; diff --git a/src/native/client-cache.ts b/src/native/client-cache.ts index d7c83fb..0f90ff2 100644 --- a/src/native/client-cache.ts +++ b/src/native/client-cache.ts @@ -16,6 +16,7 @@ export function createNativeClientCacheKey(options: NativeClientOptions): string dns: options.dns, timeout: 'protocols' in options ? options.timeout : undefined, connectTimeout: 'connectTimeout' in options ? options.connectTimeout : undefined, + tcpLinger: options.tcpLinger, poolIdleTimeout: options.poolIdleTimeout, poolMaxIdlePerHost: options.poolMaxIdlePerHost, poolMaxSize: options.poolMaxSize, diff --git a/src/native/index.ts b/src/native/index.ts index 8b194a4..2dbbd6e 100644 --- a/src/native/index.ts +++ b/src/native/index.ts @@ -6,6 +6,7 @@ export { nativeCreateUpload, nativeFailUpload, nativeFinishUpload, + nativeReadBodyAll, nativeReadBodyChunk, nativeReleaseClient, nativeRequest, diff --git a/src/native/request.ts b/src/native/request.ts index 4406c5e..7b24e43 100644 --- a/src/native/request.ts +++ b/src/native/request.ts @@ -120,6 +120,10 @@ export async function nativeReadBodyChunk( return getBinding().readBodyChunk(handle, size); } +export async function nativeReadBodyAll(handle: number): Promise { + return getBinding().readBodyAll(handle); +} + export function nativeCancelBody(handle: number): boolean { return getBinding().cancelBody(handle); } diff --git a/src/test/cookies-redirects.spec.ts b/src/test/cookies-redirects.spec.ts index 3710229..d4d07d2 100644 --- a/src/test/cookies-redirects.spec.ts +++ b/src/test/cookies-redirects.spec.ts @@ -163,6 +163,84 @@ describe('cookies and redirects', () => { assert.strictEqual(body.method, 'GET', '302 redirect from POST should be rewritten to GET'); }); + test('should return 300 Multiple Choices without following Location', async () => { + const response = await fetch(`${getBaseUrl()}/redirect/multiple-choices`); + + assert.strictEqual(response.status, 300); + assert.strictEqual(response.headers.get('location'), '/redirect/final'); + assert.strictEqual(response.redirected, false); + assert.deepStrictEqual(response.wreq.redirectChain, []); + }); + + test('should apply response Referrer-Policy across redirects', async () => { + const sameOriginReferrer = `${getBaseUrl()}/private/path?token=secret#fragment`; + const crossOriginReferrer = 'https://source.example/private/path?token=secret#fragment'; + const cases = [ + { policy: 'no-referrer', referrer: sameOriginReferrer, expected: '' }, + { policy: 'no-referrer-when-downgrade', referrer: crossOriginReferrer, expected: '' }, + { + policy: 'same-origin', + referrer: sameOriginReferrer, + expected: `${getBaseUrl()}/private/path?token=secret`, + }, + { policy: 'origin', referrer: crossOriginReferrer, expected: 'https://source.example/' }, + { policy: 'strict-origin', referrer: crossOriginReferrer, expected: '' }, + { + policy: 'origin-when-cross-origin', + referrer: crossOriginReferrer, + expected: 'https://source.example/', + }, + { policy: 'strict-origin-when-cross-origin', referrer: crossOriginReferrer, expected: '' }, + { + policy: 'unsafe-url', + referrer: crossOriginReferrer, + expected: 'https://source.example/private/path?token=secret', + }, + ]; + + for (const { policy, referrer, expected } of cases) { + const start = new URL('/redirect/start', getBaseUrl()); + + start.searchParams.set('referrerPolicy', policy); + + const response = await fetch(start, { + headers: { Referer: referrer }, + }); + + const body = await response.json<{ referrer: string }>(); + + assert.strictEqual(body.referrer, expected, policy); + } + }); + + test('should strip credentials and cookies on cross-origin redirects', async () => { + const target = new URL('/redirect/final', getBaseUrl()); + + target.hostname = 'localhost'; + + const start = new URL('/redirect/start', getBaseUrl()); + + start.searchParams.set('target', target.toString()); + + const response = await fetch(start, { + headers: { + authorization: 'Bearer secret', + cookie: 'manual_session=secret', + 'proxy-authorization': 'Basic secret', + }, + }); + + const body = await response.json<{ + authorization: string; + cookie: string; + proxyAuthorization: string; + }>(); + + assert.strictEqual(body.authorization, ''); + assert.strictEqual(body.cookie, ''); + assert.strictEqual(body.proxyAuthorization, ''); + }); + test('should support manual redirect mode', async () => { const response = await fetch(`${getBaseUrl()}/redirect/start`, { redirect: 'manual', diff --git a/src/test/helpers/local-server.ts b/src/test/helpers/local-server.ts index 8a08078..5ae1528 100644 --- a/src/test/helpers/local-server.ts +++ b/src/test/helpers/local-server.ts @@ -383,9 +383,27 @@ export function setupLocalTestServer() { } if (url.pathname === '/redirect/start') { - response.writeHead(302, { - location: '/redirect/final', + const headers: Record = { + location: url.searchParams.get('target') ?? '/redirect/final', 'set-cookie': 'redirect_session=1; Path=/', + }; + + const referrerPolicy = url.searchParams.get('referrerPolicy'); + + if (referrerPolicy) { + headers['referrer-policy'] = referrerPolicy; + } + + response.writeHead(302, headers); + + response.end(); + + return; + } + + if (url.pathname === '/redirect/multiple-choices') { + response.writeHead(300, { + location: '/redirect/final', }); response.end(); @@ -462,6 +480,9 @@ export function setupLocalTestServer() { method: request.method, cookie: readCookieHeader(request), hookHeader: request.headers['x-redirect-hook'] ?? '', + authorization: request.headers.authorization ?? '', + proxyAuthorization: request.headers['proxy-authorization'] ?? '', + referrer: request.headers.referer ?? '', }); return; diff --git a/src/test/http-client.spec.ts b/src/test/http-client.spec.ts index 0ef638c..b0ece91 100644 --- a/src/test/http-client.spec.ts +++ b/src/test/http-client.spec.ts @@ -114,6 +114,20 @@ describe('http client', () => { assert.deepStrictEqual(await response.json(), { delayed: true }); }); + test('should expose DNS failures with a stable error code', async () => { + await assert.rejects( + fetch('http://node-wreq-dns-error.invalid/', { + proxy: false, + timeout: 5_000, + }), + (error: unknown) => + error instanceof Error && + error.name === 'RequestError' && + 'code' in error && + error.code === 'ERR_DNS' + ); + }); + test('should abort requests after dispatch has started', async () => { const controller = new AbortController(); const responsePromise = fetch(`${getBaseUrl()}/timings/delay?ms=250`, { @@ -312,6 +326,22 @@ describe('http client', () => { ); }); + test('should configure and validate TCP linger', async () => { + const response = await fetch(`${getBaseUrl()}/headers/raw`, { + tcpLinger: 0, + }); + + assert.strictEqual(response.status, 200); + + await assert.rejects( + fetch(`${getBaseUrl()}/headers/raw`, { + tcpLinger: Number.NaN, + }), + (error: unknown) => + error instanceof Error && error.message.includes('tcpLinger must be a finite non-negative') + ); + }); + test('should support fetch-style requests', async () => { const response = await fetch(`${getBaseUrl()}/get`, { browser: 'chrome_137', @@ -498,7 +528,7 @@ describe('http client', () => { experimentalSettings: [{ id: 10, value: 1 }], }, }), - /wreq 6\.0\.0-rc\.29 no longer exposes custom HTTP\/2 settings/ + /wreq no longer exposes custom HTTP\/2 settings/ ); }); diff --git a/src/test/response.spec.ts b/src/test/response.spec.ts index a46aa05..7f864e8 100644 --- a/src/test/response.spec.ts +++ b/src/test/response.spec.ts @@ -213,6 +213,25 @@ describe('response behavior', () => { ); }); + test('should preserve the consumed body lifecycle after native convenience reads', async () => { + const response = await fetch(`${getBaseUrl()}/cookies/echo`); + const pendingText = response.text(); + + assert.strictEqual(response.bodyUsed, true, 'starting a convenience read should disturb body'); + assert.ok(response.body, 'a consumed native response should retain its body stream'); + assert.strictEqual(response.body.locked, true, 'the consumed body stream should remain locked'); + + const text = await pendingText; + + assert.ok(text.includes('"cookie":""'), 'the native fast path should return the full body'); + + await assert.rejects( + response.arrayBuffer(), + (error: unknown) => error instanceof TypeError && error.message.includes('already been read'), + 'a native response body should remain single-use' + ); + }); + test('should support parallel clone consumption for native-backed streamed responses', async () => { const response = await fetch(`${getBaseUrl()}/cookies/echo`); const cloned = response.clone(); diff --git a/src/test/websocket.spec.ts b/src/test/websocket.spec.ts index 28dc68e..ba01d8f 100644 --- a/src/test/websocket.spec.ts +++ b/src/test/websocket.spec.ts @@ -1,6 +1,7 @@ import assert from 'node:assert'; import { Buffer } from 'node:buffer'; import { describe, test } from 'node:test'; +import { toCookieOriginUrl } from '../http/pipeline/cookies'; import { CloseEvent as WreqCloseEvent, WebSocket as WreqWebSocket, @@ -185,8 +186,13 @@ describe('websocket', () => { }); test('should send cookieJar cookies during websocket handshake', async () => { + const cookieLookups: string[] = []; const cookieJar = { - getCookies: () => [{ name: 'session', value: 'ws123' }], + getCookies: (url: string) => { + cookieLookups.push(url); + + return [{ name: 'session', value: 'ws123' }]; + }, setCookie: () => {}, }; @@ -202,6 +208,8 @@ describe('websocket', () => { 'cookieJar cookies should be sent during the websocket handshake' ); + assert.deepStrictEqual(cookieLookups, [`${getBaseUrl()}/ws`]); + const closePromise = onceEvent(socket, 'close'); socket.close(1000, 'done'); @@ -209,6 +217,18 @@ describe('websocket', () => { await closePromise; }); + test('should map websocket URLs to their HTTP cookie origins', () => { + assert.strictEqual( + toCookieOriginUrl('ws://example.com/socket?channel=updates'), + 'http://example.com/socket?channel=updates' + ); + + assert.strictEqual( + toCookieOriginUrl('wss://example.com/socket?channel=updates'), + 'https://example.com/socket?channel=updates' + ); + }); + test('should reject websocket URLs with fragments', () => { assert.throws( () => new WreqWebSocket(getBaseUrl().replace('http://', 'ws://') + '/ws#fragment'), @@ -295,6 +315,24 @@ describe('websocket', () => { ); }); + test('should configure and validate WebSocket TCP linger', async () => { + const socket = await websocket(getBaseUrl().replace('http://', 'ws://') + '/ws', { + tcpLinger: 0, + }); + + const closePromise = onceEvent(socket, 'close'); + + socket.close(1000, 'done'); + + await closePromise; + + const invalid = new WreqWebSocket(getBaseUrl().replace('http://', 'ws://') + '/ws', { + tcpLinger: -1, + }); + + await assert.rejects(invalid.opened, /tcpLinger must be a finite non-negative number/); + }); + test('should support explicit WebSocket HTTP version selection', async () => { const socket = await websocket(getBaseUrl().replace('http://', 'ws://') + '/ws', { httpVersion: '1.1', diff --git a/src/types/http.ts b/src/types/http.ts index 24c4c34..7557de9 100644 --- a/src/types/http.ts +++ b/src/types/http.ts @@ -119,6 +119,8 @@ export interface WreqInit extends Omit< readTimeout?: number; /** Connection establishment timeout in milliseconds. */ connectTimeout?: number; + /** Socket linger duration in milliseconds; zero requests an abortive close. */ + tcpLinger?: number; /** Maximum idle time for pooled connections, or `false` to disable idle expiry. */ poolIdleTimeout?: number | false; /** Maximum idle pooled connections retained per origin. */ diff --git a/src/types/native.ts b/src/types/native.ts index 7f894ab..22ca93e 100644 --- a/src/types/native.ts +++ b/src/types/native.ts @@ -164,6 +164,8 @@ export interface NativeRequestOptions { readTimeout?: number; /** Connection establishment timeout in milliseconds. */ connectTimeout?: number; + /** Socket linger duration in milliseconds; zero requests an abortive close. */ + tcpLinger?: number; /** Maximum idle time for pooled connections, or `false` to disable idle expiry. */ poolIdleTimeout?: number | false; /** Maximum idle pooled connections retained per origin. */ diff --git a/src/types/websocket.ts b/src/types/websocket.ts index c087ae9..044206b 100644 --- a/src/types/websocket.ts +++ b/src/types/websocket.ts @@ -33,6 +33,8 @@ export interface WebSocketInit { dns?: DnsOptions; /** Handshake timeout in milliseconds. */ timeout?: number; + /** Socket linger duration in milliseconds; zero requests an abortive close. */ + tcpLinger?: number; /** Maximum idle time for pooled connections, or `false` to disable idle expiry. */ poolIdleTimeout?: number | false; /** Maximum idle pooled connections retained per origin. */ @@ -117,6 +119,8 @@ export interface NativeWebSocketConnectOptions { dns?: import('./native').NativeDnsOptions; /** Handshake timeout in milliseconds. */ timeout?: number; + /** Socket linger duration in milliseconds; zero requests an abortive close. */ + tcpLinger?: number; /** Maximum idle time for pooled connections, or `false` to disable idle expiry. */ poolIdleTimeout?: number | false; /** Maximum idle pooled connections retained per origin. */ diff --git a/src/websocket/index.ts b/src/websocket/index.ts index 51429b5..fdac671 100644 --- a/src/websocket/index.ts +++ b/src/websocket/index.ts @@ -4,6 +4,7 @@ import { normalizeDnsOptions, normalizeLocalBindOptions, normalizeProxyOptions, + normalizeTcpLinger, } from '../config/network'; import { normalizeCertificateAuthority, @@ -380,6 +381,7 @@ export class WebSocket extends EventTarget { disableSystemProxy, dns: normalizeDnsOptions(init.dns), ...resolveNativeTimeout(init.timeout), + tcpLinger: normalizeTcpLinger(init.tcpLinger), poolIdleTimeout: init.poolIdleTimeout, poolMaxIdlePerHost: init.poolMaxIdlePerHost, poolMaxSize: init.poolMaxSize, From 55b73be1f0171ad42019da57c2935ab18d3813b6 Mon Sep 17 00:00:00 2001 From: ruby Date: Tue, 25 Aug 2026 03:23:23 +0400 Subject: [PATCH 3/3] fix: allowed deps --- scripts/verify-linux-binary.mjs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/scripts/verify-linux-binary.mjs b/scripts/verify-linux-binary.mjs index 6748ce2..594221c 100644 --- a/scripts/verify-linux-binary.mjs +++ b/scripts/verify-linux-binary.mjs @@ -21,6 +21,8 @@ const abiLimits = new Map([ ]); const allowedLibraries = new Set([ + 'ld-linux-aarch64.so.1', + 'ld-linux-x86-64.so.2', 'libatomic.so.1', 'libc.so.6', 'libdl.so.2',