├── .github ├── FUNDING.yml ├── dependabot.yml └── workflows │ └── ci.yml ├── .gitignore ├── CHANGELOG.md ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── examples ├── blocking.rs ├── connect_via_lower_priority_tokio_runtime.rs ├── form.rs ├── h3_simple.rs ├── json_dynamic.rs ├── json_typed.rs ├── simple.rs ├── tor_socks.rs └── wasm_github_fetch │ ├── .gitignore │ ├── Cargo.toml │ ├── README.md │ ├── index.js │ ├── osv-scanner.toml │ ├── package-lock.json │ ├── package.json │ ├── src │ └── lib.rs │ └── webpack.config.js ├── src ├── async_impl │ ├── body.rs │ ├── client.rs │ ├── decoder.rs │ ├── h3_client │ │ ├── connect.rs │ │ ├── dns.rs │ │ ├── mod.rs │ │ └── pool.rs │ ├── mod.rs │ ├── multipart.rs │ ├── request.rs │ ├── response.rs │ └── upgrade.rs ├── blocking │ ├── body.rs │ ├── client.rs │ ├── mod.rs │ ├── multipart.rs │ ├── request.rs │ ├── response.rs │ └── wait.rs ├── config.rs ├── connect.rs ├── cookie.rs ├── dns │ ├── gai.rs │ ├── hickory.rs │ ├── mod.rs │ └── resolve.rs ├── error.rs ├── into_url.rs ├── lib.rs ├── proxy.rs ├── redirect.rs ├── response.rs ├── tls.rs ├── util.rs └── wasm │ ├── body.rs │ ├── client.rs │ ├── mod.rs │ ├── multipart.rs │ ├── request.rs │ └── response.rs └── tests ├── badssl.rs ├── blocking.rs ├── brotli.rs ├── ci.rs ├── client.rs ├── connector_layers.rs ├── cookie.rs ├── deflate.rs ├── gzip.rs ├── http3.rs ├── multipart.rs ├── proxy.rs ├── redirect.rs ├── support ├── crl.pem ├── delay_layer.rs ├── delay_server.rs ├── error.rs ├── mod.rs ├── server.cert ├── server.key └── server.rs ├── timeouts.rs ├── upgrade.rs ├── wasm_simple.rs └── zstd.rs /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [seanmonstar] 2 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | # Only enable cargo, turn off npm from wasm example 4 | updates: 5 | - package-ecosystem: "github-actions" 6 | # Workflow files stored in the 7 | # default location of `.github/workflows` 8 | directory: "/" 9 | schedule: 10 | interval: "daily" 11 | - package-ecosystem: "cargo" 12 | directory: "/" 13 | schedule: 14 | interval: "daily" 15 | # todo: if only this worked, see https://github.com/dependabot/dependabot-core/issues/4009 16 | # only tell us if there's a new 'breaking' change we could upgrade to 17 | # versioning-strategy: increase-if-necessary 18 | # disable regular version updates, security updates are unaffected 19 | open-pull-requests-limit: 0 20 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - master 8 | 9 | env: 10 | REQWEST_TEST_BODY_FULL: 1 11 | RUST_BACKTRACE: 1 12 | CARGO_INCREMENTAL: 0 13 | CARGO_REGISTRIES_CRATES_IO_PROTOCOL: sparse 14 | 15 | jobs: 16 | ci-pass: 17 | name: CI is green 18 | runs-on: ubuntu-latest 19 | needs: 20 | - style 21 | - test 22 | - features 23 | - unstable 24 | - nightly 25 | - msrv 26 | - android 27 | - wasm 28 | - docs 29 | steps: 30 | - run: exit 0 31 | 32 | style: 33 | name: Check Style 34 | 35 | runs-on: ubuntu-latest 36 | 37 | steps: 38 | - name: Checkout 39 | uses: actions/checkout@v4 40 | 41 | - name: Install rust 42 | uses: dtolnay/rust-toolchain@stable 43 | with: 44 | components: rustfmt 45 | 46 | - name: cargo fmt -- --check 47 | run: cargo fmt -- --check 48 | 49 | - name: temporary workaround - fmt all files under src 50 | # Workaround for rust-lang/cargo#7732 51 | run: cargo fmt -- --check $(find . -name '*.rs' -print) 52 | 53 | test: 54 | name: ${{ matrix.name }} 55 | needs: [style] 56 | 57 | runs-on: ${{ matrix.os || 'ubuntu-latest' }} 58 | 59 | # The build matrix does not yet support 'allow failures' at job level. 60 | # See `jobs.nightly` for the active nightly job definition. 61 | strategy: 62 | matrix: 63 | name: 64 | - linux / stable 65 | - linux / beta 66 | # - linux / nightly 67 | - macOS / stable 68 | - windows / stable-x86_64-msvc 69 | - windows / stable-i686-msvc 70 | - windows / stable-x86_64-gnu 71 | - windows / stable-i686-gnu 72 | - "feat.: default-tls disabled" 73 | - "feat.: rustls-tls" 74 | - "feat.: rustls-tls-manual-roots" 75 | - "feat.: rustls-tls-native-roots" 76 | - "feat.: rustls-tls-no-provider" 77 | - "feat.: native-tls" 78 | - "feat.: default-tls and rustls-tls" 79 | - "feat.: rustls-tls and rustls-tls-no-provider" 80 | - "feat.: cookies" 81 | - "feat.: blocking" 82 | - "feat.: blocking only" 83 | - "feat.: gzip" 84 | - "feat.: brotli" 85 | - "feat.: deflate" 86 | - "feat.: json" 87 | - "feat.: multipart" 88 | - "feat.: stream" 89 | - "feat.: socks/default-tls" 90 | - "feat.: socks/rustls-tls" 91 | - "feat.: hickory-dns" 92 | 93 | include: 94 | - name: linux / stable 95 | - name: linux / beta 96 | rust: beta 97 | # - name: linux / nightly 98 | # rust: nightly 99 | - name: macOS / stable 100 | os: macOS-latest 101 | 102 | - name: windows / stable-x86_64-msvc 103 | os: windows-latest 104 | target: x86_64-pc-windows-msvc 105 | features: "--features blocking,gzip,brotli,zstd,deflate,json,multipart,stream" 106 | - name: windows / stable-i686-msvc 107 | os: windows-latest 108 | target: i686-pc-windows-msvc 109 | features: "--features blocking,gzip,brotli,zstd,deflate,json,multipart,stream" 110 | - name: windows / stable-x86_64-gnu 111 | os: windows-latest 112 | rust: stable-x86_64-pc-windows-gnu 113 | target: x86_64-pc-windows-gnu 114 | features: "--features blocking,gzip,brotli,zstd,deflate,json,multipart,stream" 115 | package_name: mingw-w64-x86_64-gcc 116 | mingw64_path: "C:\\msys64\\mingw64\\bin" 117 | - name: windows / stable-i686-gnu 118 | os: windows-latest 119 | rust: stable-i686-pc-windows-gnu 120 | target: i686-pc-windows-gnu 121 | features: "--features blocking,gzip,brotli,zstd,deflate,json,multipart,stream" 122 | package_name: mingw-w64-i686-gcc 123 | mingw64_path: "C:\\msys64\\mingw32\\bin" 124 | 125 | - name: "feat.: default-tls disabled" 126 | features: "--no-default-features" 127 | - name: "feat.: rustls-tls" 128 | features: "--no-default-features --features rustls-tls" 129 | - name: "feat.: rustls-tls-manual-roots" 130 | features: "--no-default-features --features rustls-tls-manual-roots" 131 | - name: "feat.: rustls-tls-native-roots" 132 | features: "--no-default-features --features rustls-tls-native-roots" 133 | - name: "feat.: rustls-tls-no-provider" 134 | features: "--no-default-features --features rustls-tls-no-provider" 135 | - name: "feat.: native-tls" 136 | features: "--features native-tls" 137 | - name: "feat.: rustls-tls and rustls-tls-no-provider" 138 | features: "--features rustls-tls,rustls-tls-no-provider" 139 | - name: "feat.: default-tls and rustls-tls" 140 | features: "--features rustls-tls" 141 | - name: "feat.: cookies" 142 | features: "--features cookies" 143 | - name: "feat.: blocking" 144 | features: "--features blocking" 145 | - name: "feat.: blocking only" 146 | features: "--no-default-features --features blocking" 147 | - name: "feat.: gzip" 148 | features: "--features gzip,stream" 149 | - name: "feat.: brotli" 150 | features: "--features brotli,stream" 151 | - name: "feat.: zstd" 152 | features: "--features zstd,stream" 153 | - name: "feat.: deflate" 154 | features: "--features deflate,stream" 155 | - name: "feat.: json" 156 | features: "--features json" 157 | - name: "feat.: multipart" 158 | features: "--features multipart" 159 | - name: "feat.: stream" 160 | features: "--features stream" 161 | - name: "feat.: socks/default-tls" 162 | features: "--features socks" 163 | - name: "feat.: socks/rustls-tls" 164 | features: "--features socks,rustls-tls" 165 | - name: "feat.: hickory-dns" 166 | features: "--features hickory-dns" 167 | 168 | steps: 169 | - name: Checkout 170 | uses: actions/checkout@v4 171 | 172 | - name: Install rust 173 | uses: dtolnay/rust-toolchain@master 174 | with: 175 | toolchain: ${{ matrix.rust || 'stable' }} 176 | targets: ${{ matrix.target }} 177 | 178 | - name: Add mingw-w64 to path for i686-gnu 179 | run: | 180 | echo "${{ matrix.mingw64_path }}" >> $GITHUB_PATH 181 | echo "C:\msys64\usr\bin" >> $GITHUB_PATH 182 | if: matrix.mingw64_path 183 | shell: bash 184 | 185 | - name: Update gcc 186 | if: matrix.package_name 187 | run: pacman.exe -Sy --noconfirm ${{ matrix.package_name }} 188 | 189 | - name: Create Cargo.lock 190 | run: cargo update 191 | 192 | - uses: Swatinem/rust-cache@v2 193 | 194 | - uses: taiki-e/install-action@v2 195 | with: 196 | tool: cargo-nextest 197 | 198 | - name: Run tests 199 | run: | 200 | set -euxo pipefail 201 | cargo nextest run --locked --workspace ${{ matrix.features }} ${{ matrix.test-features }} 202 | cargo test --locked --workspace --doc ${{ matrix.features }} ${{ matrix.test-features }} 203 | shell: bash 204 | 205 | features: 206 | name: features 207 | needs: [style] 208 | runs-on: ubuntu-latest 209 | steps: 210 | - name: Checkout 211 | uses: actions/checkout@v4 212 | 213 | - name: Install Rust 214 | uses: dtolnay/rust-toolchain@stable 215 | 216 | - name: Install cargo-hack 217 | uses: taiki-e/install-action@cargo-hack 218 | 219 | - uses: Swatinem/rust-cache@v2 220 | 221 | - name: check --feature-powerset 222 | run: cargo hack --no-dev-deps check --feature-powerset --depth 2 --skip http3,__tls,__rustls,__rustls-ring,native-tls-vendored,trust-dns 223 | env: 224 | RUSTFLAGS: "-D dead_code -D unused_imports" 225 | 226 | unstable: 227 | name: "unstable features" 228 | needs: [style] 229 | runs-on: ubuntu-latest 230 | steps: 231 | - name: Checkout 232 | uses: actions/checkout@v4 233 | 234 | - name: Install rust 235 | uses: dtolnay/rust-toolchain@master 236 | with: 237 | toolchain: 'stable' 238 | 239 | - name: Check 240 | run: cargo test --features http3,stream 241 | env: 242 | RUSTFLAGS: --cfg reqwest_unstable 243 | RUSTDOCFLAGS: --cfg reqwest_unstable 244 | 245 | docs: 246 | name: Docs 247 | needs: [test] 248 | runs-on: ubuntu-latest 249 | 250 | steps: 251 | - name: Checkout repository 252 | uses: actions/checkout@v4 253 | 254 | - name: Install Rust 255 | uses: dtolnay/rust-toolchain@stable 256 | 257 | - name: Check documentation 258 | env: 259 | RUSTDOCFLAGS: --cfg reqwest_unstable -D warnings 260 | run: cargo doc --no-deps --document-private-items --all-features 261 | 262 | # Separate build job for nightly because of the missing feature for allowed failures at 263 | # job level. See `jobs.build.strategy.matrix`. 264 | nightly: 265 | name: linux / nightly 266 | needs: [style] 267 | 268 | runs-on: ubuntu-latest 269 | 270 | steps: 271 | - name: Checkout 272 | uses: actions/checkout@v4 273 | 274 | - name: Install rust 275 | uses: dtolnay/rust-toolchain@nightly 276 | 277 | - name: Check minimal versions 278 | env: 279 | RUSTFLAGS: --cfg reqwest_unstable 280 | # See https://github.com/rust-lang/rust/issues/113152 281 | # We don't force a newer openssl, but a newer one is required for 282 | # this CI runner, because of the version of Ubuntu. 283 | run: | 284 | cargo clean 285 | cargo update -Z minimal-versions 286 | cargo update -p proc-macro2 --precise 1.0.87 287 | cargo update -p openssl-sys 288 | cargo update -p openssl 289 | cargo check 290 | cargo check --all-features 291 | 292 | msrv: 293 | name: MSRV 294 | needs: [style] 295 | 296 | runs-on: ubuntu-latest 297 | 298 | steps: 299 | - name: Checkout 300 | uses: actions/checkout@v4 301 | 302 | - name: Get MSRV package metadata 303 | id: metadata 304 | run: cargo metadata --no-deps --format-version 1 | jq -r '"msrv=" + .packages[0].rust_version' >> $GITHUB_OUTPUT 305 | 306 | - name: Install rust (${{ steps.metadata.outputs.msrv }}) 307 | uses: dtolnay/rust-toolchain@master 308 | with: 309 | toolchain: ${{ steps.metadata.outputs.msrv }} 310 | 311 | - name: Fix log and tokio versions 312 | run: | 313 | cargo update 314 | cargo update -p log --precise 0.4.18 315 | cargo update -p tokio --precise 1.29.1 316 | cargo update -p tokio-util --precise 0.7.11 317 | cargo update -p idna_adapter --precise 1.1.0 318 | cargo update -p hashbrown --precise 0.15.0 319 | cargo update -p native-tls --precise 0.2.13 320 | cargo update -p once_cell --precise 1.20.3 321 | 322 | - uses: Swatinem/rust-cache@v2 323 | 324 | - name: Check 325 | run: cargo check 326 | 327 | android: 328 | name: Android 329 | needs: [style] 330 | 331 | runs-on: ubuntu-latest 332 | 333 | steps: 334 | - name: Checkout 335 | uses: actions/checkout@v4 336 | 337 | - name: Install rust 338 | uses: dtolnay/rust-toolchain@stable 339 | with: 340 | target: aarch64-linux-android 341 | 342 | - name: Build 343 | # disable default-tls feature since cross-compiling openssl is dragons 344 | run: cargo build --target aarch64-linux-android --no-default-features 345 | 346 | wasm: 347 | name: WASM 348 | needs: [style] 349 | 350 | runs-on: ubuntu-latest 351 | 352 | steps: 353 | - name: Checkout 354 | uses: actions/checkout@v4 355 | 356 | - name: Install rust 357 | uses: dtolnay/rust-toolchain@stable 358 | with: 359 | targets: wasm32-unknown-unknown 360 | 361 | - name: Check 362 | run: cargo check --target wasm32-unknown-unknown 363 | 364 | - name: Check cookies 365 | run: cargo check --target wasm32-unknown-unknown --features cookies 366 | 367 | - name: Install wasm-pack 368 | run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh 369 | 370 | - name: Wasm-pack test firefox 371 | run: wasm-pack test --headless --firefox 372 | 373 | - name: Wasm-pack test chrome 374 | run: wasm-pack test --headless --chrome 375 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | Cargo.lock 3 | *.swp 4 | .idea -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "reqwest" 3 | version = "0.12.19" 4 | description = "higher level HTTP client library" 5 | keywords = ["http", "request", "client"] 6 | categories = ["web-programming::http-client", "wasm"] 7 | repository = "https://github.com/seanmonstar/reqwest" 8 | documentation = "https://docs.rs/reqwest" 9 | authors = ["Sean McArthur "] 10 | readme = "README.md" 11 | license = "MIT OR Apache-2.0" 12 | edition = "2021" 13 | rust-version = "1.64.0" 14 | autotests = true 15 | 16 | [package.metadata.docs.rs] 17 | all-features = true 18 | rustdoc-args = ["--cfg", "docsrs", "--cfg", "reqwest_unstable"] 19 | targets = ["x86_64-unknown-linux-gnu", "wasm32-unknown-unknown"] 20 | 21 | [package.metadata.playground] 22 | features = [ 23 | "blocking", 24 | "cookies", 25 | "json", 26 | "multipart", 27 | ] 28 | 29 | [features] 30 | default = ["default-tls", "charset", "http2", "system-proxy"] 31 | 32 | # Note: this doesn't enable the 'native-tls' feature, which adds specific 33 | # functionality for it. 34 | default-tls = ["dep:hyper-tls", "dep:native-tls-crate", "__tls", "dep:tokio-native-tls"] 35 | 36 | http2 = ["h2", "hyper/http2", "hyper-util/http2", "hyper-rustls?/http2"] 37 | 38 | # Enables native-tls specific functionality not available by default. 39 | native-tls = ["default-tls"] 40 | native-tls-alpn = ["native-tls", "native-tls-crate?/alpn", "hyper-tls?/alpn"] 41 | native-tls-vendored = ["native-tls", "native-tls-crate?/vendored"] 42 | 43 | rustls-tls = ["rustls-tls-webpki-roots"] 44 | rustls-tls-no-provider = ["rustls-tls-manual-roots-no-provider"] 45 | 46 | rustls-tls-manual-roots-no-provider = ["__rustls"] 47 | rustls-tls-webpki-roots-no-provider = ["dep:webpki-roots", "hyper-rustls?/webpki-tokio", "__rustls"] 48 | rustls-tls-native-roots-no-provider = ["dep:rustls-native-certs", "hyper-rustls?/native-tokio", "__rustls"] 49 | 50 | rustls-tls-manual-roots = ["rustls-tls-manual-roots-no-provider", "__rustls-ring"] 51 | rustls-tls-webpki-roots = ["rustls-tls-webpki-roots-no-provider", "__rustls-ring"] 52 | rustls-tls-native-roots = ["rustls-tls-native-roots-no-provider", "__rustls-ring"] 53 | 54 | blocking = ["dep:futures-channel", "futures-channel?/sink", "dep:futures-util", "futures-util?/io", "futures-util?/sink", "tokio/sync"] 55 | 56 | charset = ["dep:encoding_rs"] 57 | 58 | cookies = ["dep:cookie_crate", "dep:cookie_store"] 59 | 60 | gzip = ["dep:async-compression", "async-compression?/gzip", "dep:futures-util", "dep:tokio-util"] 61 | 62 | brotli = ["dep:async-compression", "async-compression?/brotli", "dep:futures-util", "dep:tokio-util"] 63 | 64 | zstd = ["dep:async-compression", "async-compression?/zstd", "dep:futures-util", "dep:tokio-util"] 65 | 66 | deflate = ["dep:async-compression", "async-compression?/zlib", "dep:futures-util", "dep:tokio-util"] 67 | 68 | json = ["dep:serde_json"] 69 | 70 | multipart = ["dep:mime_guess", "dep:futures-util"] 71 | 72 | # Deprecated, remove this feature while bumping minor versions. 73 | trust-dns = [] 74 | hickory-dns = ["dep:hickory-resolver"] 75 | 76 | stream = ["tokio/fs", "dep:futures-util", "dep:tokio-util", "dep:wasm-streams"] 77 | 78 | socks = ["dep:tokio-socks"] 79 | 80 | # Use the system's proxy configuration. 81 | system-proxy = ["hyper-util/client-proxy-system"] 82 | 83 | # Deprecated, switch to system-proxy. 84 | macos-system-configuration = ["system-proxy"] 85 | 86 | # Experimental HTTP/3 client. 87 | http3 = ["rustls-tls-manual-roots", "dep:h3", "dep:h3-quinn", "dep:quinn", "dep:slab", "dep:futures-channel", "tokio/macros"] 88 | 89 | 90 | # Internal (PRIVATE!) features used to aid testing. 91 | # Don't rely on these whatsoever. They may disappear at any time. 92 | 93 | # Enables common types used for TLS. Useless on its own. 94 | __tls = ["dep:rustls-pki-types", "tokio/io-util"] 95 | 96 | # Enables common rustls code. 97 | # Equivalent to rustls-tls-manual-roots but shorter :) 98 | __rustls = ["dep:hyper-rustls", "dep:tokio-rustls", "dep:rustls", "__tls"] 99 | __rustls-ring = ["hyper-rustls?/ring", "tokio-rustls?/ring", "rustls?/ring", "quinn?/ring"] 100 | 101 | [dependencies] 102 | base64 = "0.22" 103 | http = "1.1" 104 | url = "2.4" 105 | bytes = "1.2" 106 | serde = "1.0" 107 | serde_urlencoded = "0.7.1" 108 | tower-service = "0.3" 109 | futures-core = { version = "0.3.28", default-features = false } 110 | futures-util = { version = "0.3.28", default-features = false, optional = true } 111 | sync_wrapper = { version = "1.0", features = ["futures"] } 112 | 113 | # Optional deps... 114 | 115 | ## json 116 | serde_json = { version = "1.0", optional = true } 117 | ## multipart 118 | mime_guess = { version = "2.0", default-features = false, optional = true } 119 | 120 | [target.'cfg(not(target_arch = "wasm32"))'.dependencies] 121 | encoding_rs = { version = "0.8", optional = true } 122 | http-body = "1" 123 | http-body-util = "0.1" 124 | hyper = { version = "1.1", features = ["http1", "client"] } 125 | hyper-util = { version = "0.1.12", features = ["http1", "client", "client-legacy", "client-proxy", "tokio"] } 126 | h2 = { version = "0.4", optional = true } 127 | once_cell = "1.18" 128 | log = "0.4.17" 129 | mime = "0.3.16" 130 | percent-encoding = "2.3" 131 | tokio = { version = "1.0", default-features = false, features = ["net", "time"] } 132 | tower = { version = "0.5.2", default-features = false, features = ["timeout", "util"] } 133 | tower-http = { version = "0.6.5", default-features = false, features = ["follow-redirect"] } 134 | pin-project-lite = "0.2.11" 135 | ipnet = "2.3" 136 | 137 | # Optional deps... 138 | rustls-pki-types = { version = "1.9.0", features = ["std"], optional = true } 139 | 140 | ## default-tls 141 | hyper-tls = { version = "0.6", optional = true } 142 | native-tls-crate = { version = "0.2.10", optional = true, package = "native-tls" } 143 | tokio-native-tls = { version = "0.3.0", optional = true } 144 | 145 | # rustls-tls 146 | hyper-rustls = { version = "0.27.0", default-features = false, optional = true, features = ["http1", "tls12"] } 147 | rustls = { version = "0.23.4", optional = true, default-features = false, features = ["std", "tls12"] } 148 | tokio-rustls = { version = "0.26", optional = true, default-features = false, features = ["tls12"] } 149 | webpki-roots = { version = "1", optional = true } 150 | rustls-native-certs = { version = "0.8.0", optional = true } 151 | 152 | ## cookies 153 | cookie_crate = { version = "0.18.0", package = "cookie", optional = true } 154 | cookie_store = { version = "0.21.0", optional = true } 155 | 156 | ## compression 157 | async-compression = { version = "0.4.0", default-features = false, features = ["tokio"], optional = true } 158 | tokio-util = { version = "0.7.9", default-features = false, features = ["codec", "io"], optional = true } 159 | 160 | ## socks 161 | tokio-socks = { version = "0.5.2", optional = true } 162 | 163 | ## hickory-dns 164 | hickory-resolver = { version = "0.24", optional = true, features = ["tokio-runtime"] } 165 | 166 | # HTTP/3 experimental support 167 | h3 = { version = "0.0.8", optional = true } 168 | h3-quinn = { version = "0.0.10", optional = true } 169 | quinn = { version = "0.11.1", default-features = false, features = ["rustls", "runtime-tokio"], optional = true } 170 | slab = { version = "0.4.9", optional = true } # just to get minimal versions working with quinn 171 | futures-channel = { version = "0.3", optional = true } 172 | 173 | [target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies] 174 | env_logger = "0.10" 175 | hyper = { version = "1.1.0", default-features = false, features = ["http1", "http2", "client", "server"] } 176 | hyper-util = { version = "0.1.12", features = ["http1", "http2", "client", "client-legacy", "server-auto", "server-graceful", "tokio"] } 177 | serde = { version = "1.0", features = ["derive"] } 178 | flate2 = "1.0.13" 179 | brotli_crate = { package = "brotli", version = "7.0.0" } 180 | zstd_crate = { package = "zstd", version = "0.13" } 181 | doc-comment = "0.3" 182 | tokio = { version = "1.0", default-features = false, features = ["macros", "rt-multi-thread"] } 183 | futures-util = { version = "0.3.28", default-features = false, features = ["std", "alloc"] } 184 | 185 | # wasm 186 | 187 | [target.'cfg(target_arch = "wasm32")'.dependencies] 188 | js-sys = "0.3.77" 189 | serde_json = "1.0" 190 | wasm-bindgen = "0.2.89" 191 | wasm-bindgen-futures = "0.4.18" 192 | wasm-streams = { version = "0.4", optional = true } 193 | 194 | [target.'cfg(target_arch = "wasm32")'.dependencies.web-sys] 195 | version = "0.3.28" 196 | features = [ 197 | "AbortController", 198 | "AbortSignal", 199 | "Headers", 200 | "Request", 201 | "RequestInit", 202 | "RequestMode", 203 | "Response", 204 | "Window", 205 | "FormData", 206 | "Blob", 207 | "BlobPropertyBag", 208 | "ServiceWorkerGlobalScope", 209 | "RequestCredentials", 210 | "File", 211 | "ReadableStream" 212 | ] 213 | 214 | [target.'cfg(target_arch = "wasm32")'.dev-dependencies] 215 | wasm-bindgen = { version = "0.2.89", features = ["serde-serialize"] } 216 | wasm-bindgen-test = "0.3" 217 | 218 | [dev-dependencies] 219 | tower = { version = "0.5.2", default-features = false, features = ["limit"] } 220 | num_cpus = "1.0" 221 | libc = "0" 222 | 223 | [lints.rust] 224 | unexpected_cfgs = { level = "warn", check-cfg = ['cfg(reqwest_unstable)'] } 225 | 226 | [[example]] 227 | name = "blocking" 228 | path = "examples/blocking.rs" 229 | required-features = ["blocking"] 230 | 231 | [[example]] 232 | name = "json_dynamic" 233 | path = "examples/json_dynamic.rs" 234 | required-features = ["json"] 235 | 236 | [[example]] 237 | name = "json_typed" 238 | path = "examples/json_typed.rs" 239 | required-features = ["json"] 240 | 241 | [[example]] 242 | name = "tor_socks" 243 | path = "examples/tor_socks.rs" 244 | required-features = ["socks"] 245 | 246 | [[example]] 247 | name = "form" 248 | path = "examples/form.rs" 249 | 250 | [[example]] 251 | name = "simple" 252 | path = "examples/simple.rs" 253 | 254 | [[example]] 255 | name = "h3_simple" 256 | path = "examples/h3_simple.rs" 257 | required-features = ["http3", "rustls-tls"] 258 | 259 | [[example]] 260 | name = "connect_via_lower_priority_tokio_runtime" 261 | path = "examples/connect_via_lower_priority_tokio_runtime.rs" 262 | 263 | [[test]] 264 | name = "blocking" 265 | path = "tests/blocking.rs" 266 | required-features = ["blocking"] 267 | 268 | [[test]] 269 | name = "cookie" 270 | path = "tests/cookie.rs" 271 | required-features = ["cookies"] 272 | 273 | [[test]] 274 | name = "gzip" 275 | path = "tests/gzip.rs" 276 | required-features = ["gzip", "stream"] 277 | 278 | [[test]] 279 | name = "brotli" 280 | path = "tests/brotli.rs" 281 | required-features = ["brotli", "stream"] 282 | 283 | [[test]] 284 | name = "zstd" 285 | path = "tests/zstd.rs" 286 | required-features = ["zstd", "stream"] 287 | 288 | [[test]] 289 | name = "deflate" 290 | path = "tests/deflate.rs" 291 | required-features = ["deflate", "stream"] 292 | 293 | [[test]] 294 | name = "multipart" 295 | path = "tests/multipart.rs" 296 | required-features = ["multipart"] 297 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2016 Sean McArthur 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016-2025 Sean McArthur 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # reqwest 2 | 3 | [![crates.io](https://img.shields.io/crates/v/reqwest.svg)](https://crates.io/crates/reqwest) 4 | [![Documentation](https://docs.rs/reqwest/badge.svg)](https://docs.rs/reqwest) 5 | [![MIT/Apache-2 licensed](https://img.shields.io/crates/l/reqwest.svg)](./LICENSE-APACHE) 6 | [![CI](https://github.com/seanmonstar/reqwest/workflows/CI/badge.svg)](https://github.com/seanmonstar/reqwest/actions?query=workflow%3ACI) 7 | 8 | An ergonomic, batteries-included HTTP Client for Rust. 9 | 10 | - Async and blocking `Client`s 11 | - Plain bodies, JSON, urlencoded, multipart 12 | - Customizable redirect policy 13 | - HTTP Proxies 14 | - HTTPS via system-native TLS (or optionally, rustls) 15 | - Cookie Store 16 | - WASM 17 | 18 | 19 | ## Example 20 | 21 | This asynchronous example uses [Tokio](https://tokio.rs) and enables some 22 | optional features, so your `Cargo.toml` could look like this: 23 | 24 | ```toml 25 | [dependencies] 26 | reqwest = { version = "0.12", features = ["json"] } 27 | tokio = { version = "1", features = ["full"] } 28 | ``` 29 | 30 | And then the code: 31 | 32 | ```rust,no_run 33 | use std::collections::HashMap; 34 | 35 | #[tokio::main] 36 | async fn main() -> Result<(), Box> { 37 | let resp = reqwest::get("https://httpbin.org/ip") 38 | .await? 39 | .json::>() 40 | .await?; 41 | println!("{resp:#?}"); 42 | Ok(()) 43 | } 44 | ``` 45 | 46 | ## Commercial Support 47 | 48 | For private advice, support, reviews, access to the maintainer, and the like, reach out for [commercial support][sponsor]. 49 | 50 | ## Requirements 51 | 52 | On Linux: 53 | 54 | - OpenSSL with headers. See https://docs.rs/openssl for supported versions 55 | and more details. Alternatively you can enable the `native-tls-vendored` 56 | feature to compile a copy of OpenSSL. Or, you can use [rustls](https://github.com/rustls/rustls) 57 | via `rustls-tls` or other `rustls-tls-*` features. 58 | 59 | On Windows and macOS: 60 | 61 | - Nothing. 62 | 63 | By default, Reqwest uses [rust-native-tls](https://github.com/sfackler/rust-native-tls), 64 | which will use the operating system TLS framework if available, meaning Windows 65 | and macOS. On Linux, it will use the available OpenSSL or fail to build if 66 | not found. 67 | 68 | 69 | ## License 70 | 71 | Licensed under either of 72 | 73 | - Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE) or http://apache.org/licenses/LICENSE-2.0) 74 | - MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) 75 | 76 | ### Contribution 77 | 78 | Unless you explicitly state otherwise, any contribution intentionally submitted 79 | for inclusion in the work by you, as defined in the Apache-2.0 license, shall 80 | be dual licensed as above, without any additional terms or conditions. 81 | 82 | ## Sponsors 83 | 84 | Support this project by becoming a [sponsor][]. 85 | 86 | [sponsor]: https://seanmonstar.com/sponsor 87 | -------------------------------------------------------------------------------- /examples/blocking.rs: -------------------------------------------------------------------------------- 1 | //! `cargo run --example blocking --features=blocking` 2 | #![deny(warnings)] 3 | 4 | fn main() -> Result<(), Box> { 5 | env_logger::init(); 6 | 7 | // Some simple CLI args requirements... 8 | let url = match std::env::args().nth(1) { 9 | Some(url) => url, 10 | None => { 11 | println!("No CLI URL provided, using default."); 12 | "https://hyper.rs".into() 13 | } 14 | }; 15 | 16 | eprintln!("Fetching {url:?}..."); 17 | 18 | // reqwest::blocking::get() is a convenience function. 19 | // 20 | // In most cases, you should create/build a reqwest::Client and reuse 21 | // it for all requests. 22 | let mut res = reqwest::blocking::get(url)?; 23 | 24 | eprintln!("Response: {:?} {}", res.version(), res.status()); 25 | eprintln!("Headers: {:#?}\n", res.headers()); 26 | 27 | // copy the response body directly to stdout 28 | res.copy_to(&mut std::io::stdout())?; 29 | 30 | Ok(()) 31 | } 32 | -------------------------------------------------------------------------------- /examples/connect_via_lower_priority_tokio_runtime.rs: -------------------------------------------------------------------------------- 1 | #![deny(warnings)] 2 | // This example demonstrates how to delegate the connect calls, which contain TLS handshakes, 3 | // to a secondary tokio runtime of lower OS thread priority using a custom tower layer. 4 | // This helps to ensure that long-running futures during handshake crypto operations don't block other I/O futures. 5 | // 6 | // This does introduce overhead of additional threads, channels, extra vtables, etc, 7 | // so it is best suited to services with large numbers of incoming connections or that 8 | // are otherwise very sensitive to any blocking futures. Or, you might want fewer threads 9 | // and/or to use the current_thread runtime. 10 | // 11 | // This is using the `tokio` runtime and certain other dependencies: 12 | // 13 | // `tokio = { version = "1", features = ["full"] }` 14 | // `num_cpus = "1.0"` 15 | // `libc = "0"` 16 | // `pin-project-lite = "0.2"` 17 | // `tower = { version = "0.5", default-features = false}` 18 | 19 | #[cfg(not(target_arch = "wasm32"))] 20 | #[tokio::main] 21 | async fn main() -> Result<(), reqwest::Error> { 22 | background_threadpool::init_background_runtime(); 23 | tokio::time::sleep(std::time::Duration::from_millis(10)).await; 24 | 25 | let client = reqwest::Client::builder() 26 | .connector_layer(background_threadpool::BackgroundProcessorLayer::new()) 27 | .build() 28 | .expect("should be able to build reqwest client"); 29 | 30 | let url = if let Some(url) = std::env::args().nth(1) { 31 | url 32 | } else { 33 | println!("No CLI URL provided, using default."); 34 | "https://hyper.rs".into() 35 | }; 36 | 37 | eprintln!("Fetching {url:?}..."); 38 | 39 | let res = client.get(url).send().await?; 40 | 41 | eprintln!("Response: {:?} {}", res.version(), res.status()); 42 | eprintln!("Headers: {:#?}\n", res.headers()); 43 | 44 | let body = res.text().await?; 45 | 46 | println!("{body}"); 47 | 48 | Ok(()) 49 | } 50 | 51 | // separating out for convenience to avoid a million #[cfg(not(target_arch = "wasm32"))] 52 | #[cfg(not(target_arch = "wasm32"))] 53 | mod background_threadpool { 54 | use std::{ 55 | future::Future, 56 | pin::Pin, 57 | sync::OnceLock, 58 | task::{Context, Poll}, 59 | }; 60 | 61 | use futures_util::TryFutureExt; 62 | use pin_project_lite::pin_project; 63 | use tokio::{runtime::Handle, select, sync::mpsc::error::TrySendError}; 64 | use tower::{BoxError, Layer, Service}; 65 | 66 | static CPU_HEAVY_THREAD_POOL: OnceLock< 67 | tokio::sync::mpsc::Sender + Send + 'static>>>, 68 | > = OnceLock::new(); 69 | 70 | pub(crate) fn init_background_runtime() { 71 | std::thread::Builder::new() 72 | .name("cpu-heavy-background-threadpool".to_string()) 73 | .spawn(move || { 74 | let rt = tokio::runtime::Builder::new_multi_thread() 75 | .thread_name("cpu-heavy-background-pool-thread") 76 | .worker_threads(num_cpus::get() as usize) 77 | // ref: https://github.com/tokio-rs/tokio/issues/4941 78 | // consider uncommenting if seeing heavy task contention 79 | // .disable_lifo_slot() 80 | .on_thread_start(move || { 81 | #[cfg(target_os = "linux")] 82 | unsafe { 83 | // Increase thread pool thread niceness, so they are lower priority 84 | // than the foreground executor and don't interfere with I/O tasks 85 | { 86 | *libc::__errno_location() = 0; 87 | if libc::nice(10) == -1 && *libc::__errno_location() != 0 { 88 | let error = std::io::Error::last_os_error(); 89 | log::error!("failed to set threadpool niceness: {}", error); 90 | } 91 | } 92 | } 93 | }) 94 | .enable_all() 95 | .build() 96 | .unwrap_or_else(|e| panic!("cpu heavy runtime failed_to_initialize: {}", e)); 97 | rt.block_on(async { 98 | log::debug!("starting background cpu-heavy work"); 99 | process_cpu_work().await; 100 | }); 101 | }) 102 | .unwrap_or_else(|e| panic!("cpu heavy thread failed_to_initialize: {}", e)); 103 | } 104 | 105 | #[cfg(not(target_arch = "wasm32"))] 106 | async fn process_cpu_work() { 107 | // we only use this channel for routing work, it should move pretty quick, it can be small 108 | let (tx, mut rx) = tokio::sync::mpsc::channel(10); 109 | // share the handle to the background channel globally 110 | CPU_HEAVY_THREAD_POOL.set(tx).unwrap(); 111 | 112 | while let Some(work) = rx.recv().await { 113 | tokio::task::spawn(work); 114 | } 115 | } 116 | 117 | // retrieve the sender to the background channel, and send the future over to it for execution 118 | fn send_to_background_runtime(future: impl Future + Send + 'static) { 119 | let tx = CPU_HEAVY_THREAD_POOL.get().expect( 120 | "start up the secondary tokio runtime before sending to `CPU_HEAVY_THREAD_POOL`", 121 | ); 122 | 123 | match tx.try_send(Box::pin(future)) { 124 | Ok(_) => (), 125 | Err(TrySendError::Closed(_)) => { 126 | panic!("background cpu heavy runtime channel is closed") 127 | } 128 | Err(TrySendError::Full(msg)) => { 129 | log::warn!( 130 | "background cpu heavy runtime channel is full, task spawning loop delayed" 131 | ); 132 | let tx = tx.clone(); 133 | Handle::current().spawn(async move { 134 | tx.send(msg) 135 | .await 136 | .expect("background cpu heavy runtime channel is closed") 137 | }); 138 | } 139 | } 140 | } 141 | 142 | // This tower layer injects futures with a oneshot channel, and then sends them to the background runtime for processing. 143 | // We don't use the Buffer service because that is intended to process sequentially on a single task, whereas we want to 144 | // spawn a new task per call. 145 | #[derive(Copy, Clone)] 146 | pub struct BackgroundProcessorLayer {} 147 | impl BackgroundProcessorLayer { 148 | pub fn new() -> Self { 149 | Self {} 150 | } 151 | } 152 | impl Layer for BackgroundProcessorLayer { 153 | type Service = BackgroundProcessor; 154 | fn layer(&self, service: S) -> Self::Service { 155 | BackgroundProcessor::new(service) 156 | } 157 | } 158 | 159 | impl std::fmt::Debug for BackgroundProcessorLayer { 160 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 161 | f.debug_struct("BackgroundProcessorLayer").finish() 162 | } 163 | } 164 | 165 | // This tower service injects futures with a oneshot channel, and then sends them to the background runtime for processing. 166 | #[derive(Debug, Clone)] 167 | pub struct BackgroundProcessor { 168 | inner: S, 169 | } 170 | 171 | impl BackgroundProcessor { 172 | pub fn new(inner: S) -> Self { 173 | BackgroundProcessor { inner } 174 | } 175 | } 176 | 177 | impl Service for BackgroundProcessor 178 | where 179 | S: Service, 180 | S::Response: Send + 'static, 181 | S::Error: Into + Send, 182 | S::Future: Send + 'static, 183 | { 184 | type Response = S::Response; 185 | 186 | type Error = BoxError; 187 | 188 | type Future = BackgroundResponseFuture; 189 | 190 | fn poll_ready( 191 | &mut self, 192 | cx: &mut std::task::Context<'_>, 193 | ) -> std::task::Poll> { 194 | match self.inner.poll_ready(cx) { 195 | Poll::Pending => Poll::Pending, 196 | Poll::Ready(r) => Poll::Ready(r.map_err(Into::into)), 197 | } 198 | } 199 | 200 | fn call(&mut self, req: Request) -> Self::Future { 201 | let response = self.inner.call(req); 202 | 203 | // wrap our inner service's future with a future that writes to this oneshot channel 204 | let (mut tx, rx) = tokio::sync::oneshot::channel(); 205 | let future = async move { 206 | select!( 207 | _ = tx.closed() => { 208 | // receiver already dropped, don't need to do anything 209 | } 210 | result = response.map_err(|err| Into::::into(err)) => { 211 | // if this fails, the receiver already dropped, so we don't need to do anything 212 | let _ = tx.send(result); 213 | } 214 | ) 215 | }; 216 | // send the wrapped future to the background 217 | send_to_background_runtime(future); 218 | 219 | BackgroundResponseFuture::new(rx) 220 | } 221 | } 222 | 223 | // `BackgroundProcessor` response future 224 | pin_project! { 225 | #[derive(Debug)] 226 | pub struct BackgroundResponseFuture { 227 | #[pin] 228 | rx: tokio::sync::oneshot::Receiver>, 229 | } 230 | } 231 | 232 | impl BackgroundResponseFuture { 233 | pub(crate) fn new(rx: tokio::sync::oneshot::Receiver>) -> Self { 234 | BackgroundResponseFuture { rx } 235 | } 236 | } 237 | 238 | impl Future for BackgroundResponseFuture 239 | where 240 | S: Send + 'static, 241 | { 242 | type Output = Result; 243 | 244 | fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { 245 | let this = self.project(); 246 | 247 | // now poll on the receiver end of the oneshot to get the result 248 | match this.rx.poll(cx) { 249 | Poll::Ready(v) => match v { 250 | Ok(v) => Poll::Ready(v.map_err(Into::into)), 251 | Err(err) => Poll::Ready(Err(Box::new(err) as BoxError)), 252 | }, 253 | Poll::Pending => Poll::Pending, 254 | } 255 | } 256 | } 257 | } 258 | 259 | // The [cfg(not(target_arch = "wasm32"))] above prevent building the tokio::main function 260 | // for wasm32 target, because tokio isn't compatible with wasm32. 261 | // If you aren't building for wasm32, you don't need that line. 262 | // The two lines below avoid the "'main' function not found" error when building for wasm32 target. 263 | #[cfg(any(target_arch = "wasm32"))] 264 | fn main() {} 265 | -------------------------------------------------------------------------------- /examples/form.rs: -------------------------------------------------------------------------------- 1 | // Short example of a POST request with form data. 2 | // 3 | // This is using the `tokio` runtime. You'll need the following dependency: 4 | // 5 | // `tokio = { version = "1", features = ["full"] }` 6 | #[cfg(not(target_arch = "wasm32"))] 7 | #[tokio::main] 8 | async fn main() { 9 | let response = reqwest::Client::new() 10 | .post("http://www.baidu.com") 11 | .form(&[("one", "1")]) 12 | .send() 13 | .await 14 | .expect("send"); 15 | println!("Response status {}", response.status()); 16 | } 17 | 18 | // The [cfg(not(target_arch = "wasm32"))] above prevent building the tokio::main function 19 | // for wasm32 target, because tokio isn't compatible with wasm32. 20 | // If you aren't building for wasm32, you don't need that line. 21 | // The two lines below avoid the "'main' function not found" error when building for wasm32 target. 22 | #[cfg(target_arch = "wasm32")] 23 | fn main() {} 24 | -------------------------------------------------------------------------------- /examples/h3_simple.rs: -------------------------------------------------------------------------------- 1 | #![deny(warnings)] 2 | 3 | // This is using the `tokio` runtime. You'll need the following dependency: 4 | // 5 | // `tokio = { version = "1", features = ["full"] }` 6 | #[cfg(feature = "http3")] 7 | #[cfg(not(target_arch = "wasm32"))] 8 | #[tokio::main] 9 | async fn main() -> Result<(), reqwest::Error> { 10 | let client = reqwest::Client::builder().http3_prior_knowledge().build()?; 11 | 12 | // Some simple CLI args requirements... 13 | let url = match std::env::args().nth(1) { 14 | Some(url) => url, 15 | None => { 16 | println!("No CLI URL provided, using default."); 17 | "https://hyper.rs".into() 18 | } 19 | }; 20 | 21 | eprintln!("Fetching {url:?}..."); 22 | 23 | let res = client 24 | .get(url) 25 | .version(http::Version::HTTP_3) 26 | .send() 27 | .await?; 28 | 29 | eprintln!("Response: {:?} {}", res.version(), res.status()); 30 | eprintln!("Headers: {:#?}\n", res.headers()); 31 | 32 | let body = res.text().await?; 33 | 34 | println!("{body}"); 35 | 36 | Ok(()) 37 | } 38 | 39 | // The [cfg(not(target_arch = "wasm32"))] above prevent building the tokio::main function 40 | // for wasm32 target, because tokio isn't compatible with wasm32. 41 | // If you aren't building for wasm32, you don't need that line. 42 | // The two lines below avoid the "'main' function not found" error when building for wasm32 target. 43 | #[cfg(any(target_arch = "wasm32", not(feature = "http3")))] 44 | fn main() {} 45 | -------------------------------------------------------------------------------- /examples/json_dynamic.rs: -------------------------------------------------------------------------------- 1 | //! This example illustrates the way to send and receive arbitrary JSON. 2 | //! 3 | //! This is useful for some ad-hoc experiments and situations when you don't 4 | //! really care about the structure of the JSON and just need to display it or 5 | //! process it at runtime. 6 | 7 | // This is using the `tokio` runtime. You'll need the following dependency: 8 | // 9 | // `tokio = { version = "1", features = ["full"] }` 10 | #[tokio::main] 11 | async fn main() -> Result<(), reqwest::Error> { 12 | let echo_json: serde_json::Value = reqwest::Client::new() 13 | .post("https://jsonplaceholder.typicode.com/posts") 14 | .json(&serde_json::json!({ 15 | "title": "Reqwest.rs", 16 | "body": "https://docs.rs/reqwest", 17 | "userId": 1 18 | })) 19 | .send() 20 | .await? 21 | .json() 22 | .await?; 23 | 24 | println!("{echo_json:#?}"); 25 | // Object( 26 | // { 27 | // "body": String( 28 | // "https://docs.rs/reqwest" 29 | // ), 30 | // "id": Number( 31 | // 101 32 | // ), 33 | // "title": String( 34 | // "Reqwest.rs" 35 | // ), 36 | // "userId": Number( 37 | // 1 38 | // ) 39 | // } 40 | // ) 41 | Ok(()) 42 | } 43 | -------------------------------------------------------------------------------- /examples/json_typed.rs: -------------------------------------------------------------------------------- 1 | //! This example illustrates the way to send and receive statically typed JSON. 2 | //! 3 | //! In contrast to the arbitrary JSON example, this brings up the full power of 4 | //! Rust compile-time type system guaranties though it requires a little bit 5 | //! more code. 6 | 7 | // These require the `serde` dependency. 8 | use serde::{Deserialize, Serialize}; 9 | 10 | #[derive(Debug, Serialize, Deserialize)] 11 | struct Post { 12 | id: Option, 13 | title: String, 14 | body: String, 15 | #[serde(rename = "userId")] 16 | user_id: i32, 17 | } 18 | 19 | // This is using the `tokio` runtime. You'll need the following dependency: 20 | // 21 | // `tokio = { version = "1", features = ["full"] }` 22 | #[tokio::main] 23 | async fn main() -> Result<(), reqwest::Error> { 24 | let new_post = Post { 25 | id: None, 26 | title: "Reqwest.rs".into(), 27 | body: "https://docs.rs/reqwest".into(), 28 | user_id: 1, 29 | }; 30 | let new_post: Post = reqwest::Client::new() 31 | .post("https://jsonplaceholder.typicode.com/posts") 32 | .json(&new_post) 33 | .send() 34 | .await? 35 | .json() 36 | .await?; 37 | 38 | println!("{new_post:#?}"); 39 | // Post { 40 | // id: Some( 41 | // 101 42 | // ), 43 | // title: "Reqwest.rs", 44 | // body: "https://docs.rs/reqwest", 45 | // user_id: 1 46 | // } 47 | Ok(()) 48 | } 49 | -------------------------------------------------------------------------------- /examples/simple.rs: -------------------------------------------------------------------------------- 1 | #![deny(warnings)] 2 | 3 | // This is using the `tokio` runtime. You'll need the following dependency: 4 | // 5 | // `tokio = { version = "1", features = ["full"] }` 6 | #[cfg(not(target_arch = "wasm32"))] 7 | #[tokio::main] 8 | async fn main() -> Result<(), reqwest::Error> { 9 | // Some simple CLI args requirements... 10 | let url = if let Some(url) = std::env::args().nth(1) { 11 | url 12 | } else { 13 | println!("No CLI URL provided, using default."); 14 | "https://hyper.rs".into() 15 | }; 16 | 17 | eprintln!("Fetching {url:?}..."); 18 | 19 | // reqwest::get() is a convenience function. 20 | // 21 | // In most cases, you should create/build a reqwest::Client and reuse 22 | // it for all requests. 23 | let res = reqwest::get(url).await?; 24 | 25 | eprintln!("Response: {:?} {}", res.version(), res.status()); 26 | eprintln!("Headers: {:#?}\n", res.headers()); 27 | 28 | let body = res.text().await?; 29 | 30 | println!("{body}"); 31 | 32 | Ok(()) 33 | } 34 | 35 | // The [cfg(not(target_arch = "wasm32"))] above prevent building the tokio::main function 36 | // for wasm32 target, because tokio isn't compatible with wasm32. 37 | // If you aren't building for wasm32, you don't need that line. 38 | // The two lines below avoid the "'main' function not found" error when building for wasm32 target. 39 | #[cfg(target_arch = "wasm32")] 40 | fn main() {} 41 | -------------------------------------------------------------------------------- /examples/tor_socks.rs: -------------------------------------------------------------------------------- 1 | #![deny(warnings)] 2 | 3 | // This is using the `tokio` runtime. You'll need the following dependency: 4 | // 5 | // `tokio = { version = "1", features = ["full"] }` 6 | #[tokio::main] 7 | async fn main() -> Result<(), reqwest::Error> { 8 | // Make sure you are running tor and this is your socks port 9 | let proxy = reqwest::Proxy::all("socks5h://127.0.0.1:9050").expect("tor proxy should be there"); 10 | let client = reqwest::Client::builder() 11 | .proxy(proxy) 12 | .build() 13 | .expect("should be able to build reqwest client"); 14 | 15 | let res = client.get("https://check.torproject.org").send().await?; 16 | println!("Status: {}", res.status()); 17 | 18 | let text = res.text().await?; 19 | let is_tor = text.contains("Congratulations. This browser is configured to use Tor."); 20 | println!("Is Tor: {is_tor}"); 21 | assert!(is_tor); 22 | 23 | Ok(()) 24 | } 25 | -------------------------------------------------------------------------------- /examples/wasm_github_fetch/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | pkg 3 | target 4 | Cargo.lock 5 | *.swp 6 | -------------------------------------------------------------------------------- /examples/wasm_github_fetch/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "wasm" 3 | version = "0.1.0" 4 | authors = ["John Gallagher "] 5 | edition = "2021" 6 | 7 | # Config mostly pulled from: https://github.com/rustwasm/wasm-bindgen/blob/master/examples/fetch/Cargo.toml 8 | 9 | [lib] 10 | crate-type = ["cdylib"] 11 | 12 | [dependencies] 13 | reqwest = {path = "../../"} 14 | serde = { version = "1.0.101", features = ["derive"] } 15 | serde_derive = "^1.0.59" 16 | wasm-bindgen-futures = "0.4.1" 17 | serde_json = "1.0.41" 18 | wasm-bindgen = { version = "0.2.51", features = ["serde-serialize"] } 19 | -------------------------------------------------------------------------------- /examples/wasm_github_fetch/README.md: -------------------------------------------------------------------------------- 1 | ## Example usage of Reqwest from WASM 2 | 3 | Install wasm-pack with 4 | 5 | npm install 6 | 7 | Then you can build the example locally with: 8 | 9 | 10 | npm run serve 11 | 12 | and then visiting http://localhost:8080 in a browser should run the example! 13 | 14 | 15 | This example is loosely based off of [this example](https://github.com/rustwasm/wasm-bindgen/blob/master/examples/fetch/src/lib.rs), an example usage of `fetch` from `wasm-bindgen`. -------------------------------------------------------------------------------- /examples/wasm_github_fetch/index.js: -------------------------------------------------------------------------------- 1 | const rust = import('./pkg'); 2 | 3 | rust 4 | .then(m => { 5 | return m.run().then((data) => { 6 | console.log(data); 7 | 8 | console.log("The latest commit to the wasm-bindgen %s branch is:", data.name); 9 | console.log("%s, authored by %s <%s>", data.commit.sha, data.commit.commit.author.name, data.commit.commit.author.email); 10 | }) 11 | }) 12 | .catch(console.error); -------------------------------------------------------------------------------- /examples/wasm_github_fetch/osv-scanner.toml: -------------------------------------------------------------------------------- 1 | [[PackageOverrides]] 2 | ecosystem = "npm" 3 | ignore = true 4 | -------------------------------------------------------------------------------- /examples/wasm_github_fetch/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "build": "webpack", 4 | "serve": "webpack-dev-server" 5 | }, 6 | "devDependencies": { 7 | "@wasm-tool/wasm-pack-plugin": "1.0.1", 8 | "text-encoding": "^0.7.0", 9 | "html-webpack-plugin": "^3.2.0", 10 | "webpack": "^4.29.4", 11 | "webpack-cli": "^3.1.1", 12 | "webpack-dev-server": "^3.1.0" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /examples/wasm_github_fetch/src/lib.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | use wasm_bindgen::prelude::*; 3 | 4 | // NOTE: This test is a clone of https://github.com/rustwasm/wasm-bindgen/blob/master/examples/fetch/src/lib.rs 5 | // but uses Reqwest instead of the web_sys fetch api directly 6 | 7 | /// A struct to hold some data from the GitHub Branch API. 8 | /// 9 | /// Note how we don't have to define every member -- serde will ignore extra 10 | /// data when deserializing 11 | #[derive(Debug, Serialize, Deserialize)] 12 | pub struct Branch { 13 | pub name: String, 14 | pub commit: Commit, 15 | } 16 | 17 | #[derive(Debug, Serialize, Deserialize)] 18 | pub struct Commit { 19 | pub sha: String, 20 | pub commit: CommitDetails, 21 | } 22 | 23 | #[derive(Debug, Serialize, Deserialize)] 24 | pub struct CommitDetails { 25 | pub author: Signature, 26 | pub committer: Signature, 27 | } 28 | 29 | #[derive(Debug, Serialize, Deserialize)] 30 | pub struct Signature { 31 | pub name: String, 32 | pub email: String, 33 | } 34 | 35 | #[wasm_bindgen] 36 | pub async fn run() -> Result { 37 | let res = reqwest::Client::new() 38 | .get("https://api.github.com/repos/rustwasm/wasm-bindgen/branches/master") 39 | .header("Accept", "application/vnd.github.v3+json") 40 | .send() 41 | .await?; 42 | 43 | let text = res.text().await?; 44 | let branch_info: Branch = serde_json::from_str(&text).unwrap(); 45 | 46 | Ok(JsValue::from_serde(&branch_info).unwrap()) 47 | } 48 | -------------------------------------------------------------------------------- /examples/wasm_github_fetch/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 3 | const webpack = require('webpack'); 4 | const WasmPackPlugin = require("@wasm-tool/wasm-pack-plugin"); 5 | 6 | module.exports = { 7 | entry: './index.js', 8 | output: { 9 | path: path.resolve(__dirname, 'dist'), 10 | filename: 'index.js', 11 | }, 12 | plugins: [ 13 | new HtmlWebpackPlugin(), 14 | new WasmPackPlugin({ 15 | crateDirectory: path.resolve(__dirname, ".") 16 | }), 17 | // Have this example work in Edge which doesn't ship `TextEncoder` or 18 | // `TextDecoder` at this time. 19 | new webpack.ProvidePlugin({ 20 | TextDecoder: ['text-encoding', 'TextDecoder'], 21 | TextEncoder: ['text-encoding', 'TextEncoder'] 22 | }) 23 | ], 24 | mode: 'development' 25 | }; -------------------------------------------------------------------------------- /src/async_impl/h3_client/connect.rs: -------------------------------------------------------------------------------- 1 | use crate::async_impl::h3_client::dns::resolve; 2 | use crate::dns::DynResolver; 3 | use crate::error::BoxError; 4 | use bytes::Bytes; 5 | use h3::client::SendRequest; 6 | use h3_quinn::{Connection, OpenStreams}; 7 | use http::Uri; 8 | use hyper_util::client::legacy::connect::dns::Name; 9 | use quinn::crypto::rustls::QuicClientConfig; 10 | use quinn::{ClientConfig, Endpoint, TransportConfig}; 11 | use std::net::{IpAddr, SocketAddr}; 12 | use std::str::FromStr; 13 | use std::sync::Arc; 14 | 15 | type H3Connection = ( 16 | h3::client::Connection, 17 | SendRequest, 18 | ); 19 | 20 | /// H3 Client Config 21 | #[derive(Clone)] 22 | pub(crate) struct H3ClientConfig { 23 | /// Set the maximum HTTP/3 header size this client is willing to accept. 24 | /// 25 | /// See [header size constraints] section of the specification for details. 26 | /// 27 | /// [header size constraints]: https://www.rfc-editor.org/rfc/rfc9114.html#name-header-size-constraints 28 | /// 29 | /// Please see docs in [`Builder`] in [`h3`]. 30 | /// 31 | /// [`Builder`]: https://docs.rs/h3/latest/h3/client/struct.Builder.html#method.max_field_section_size 32 | pub(crate) max_field_section_size: Option, 33 | 34 | /// Enable whether to send HTTP/3 protocol grease on the connections. 35 | /// 36 | /// Just like in HTTP/2, HTTP/3 also uses the concept of "grease" 37 | /// 38 | /// to prevent potential interoperability issues in the future. 39 | /// In HTTP/3, the concept of grease is used to ensure that the protocol can evolve 40 | /// and accommodate future changes without breaking existing implementations. 41 | /// 42 | /// Please see docs in [`Builder`] in [`h3`]. 43 | /// 44 | /// [`Builder`]: https://docs.rs/h3/latest/h3/client/struct.Builder.html#method.send_grease 45 | pub(crate) send_grease: Option, 46 | } 47 | 48 | impl Default for H3ClientConfig { 49 | fn default() -> Self { 50 | Self { 51 | max_field_section_size: None, 52 | send_grease: None, 53 | } 54 | } 55 | } 56 | 57 | #[derive(Clone)] 58 | pub(crate) struct H3Connector { 59 | resolver: DynResolver, 60 | endpoint: Endpoint, 61 | client_config: H3ClientConfig, 62 | } 63 | 64 | impl H3Connector { 65 | pub fn new( 66 | resolver: DynResolver, 67 | tls: rustls::ClientConfig, 68 | local_addr: Option, 69 | transport_config: TransportConfig, 70 | client_config: H3ClientConfig, 71 | ) -> Result { 72 | let quic_client_config = Arc::new(QuicClientConfig::try_from(tls)?); 73 | let mut config = ClientConfig::new(quic_client_config); 74 | // FIXME: Replace this when there is a setter. 75 | config.transport_config(Arc::new(transport_config)); 76 | 77 | let socket_addr = match local_addr { 78 | Some(ip) => SocketAddr::new(ip, 0), 79 | None => "[::]:0".parse::().unwrap(), 80 | }; 81 | 82 | let mut endpoint = Endpoint::client(socket_addr)?; 83 | endpoint.set_default_client_config(config); 84 | 85 | Ok(Self { 86 | resolver, 87 | endpoint, 88 | client_config, 89 | }) 90 | } 91 | 92 | pub async fn connect(&mut self, dest: Uri) -> Result { 93 | let host = dest 94 | .host() 95 | .ok_or("destination must have a host")? 96 | .trim_start_matches('[') 97 | .trim_end_matches(']'); 98 | let port = dest.port_u16().unwrap_or(443); 99 | 100 | let addrs = if let Some(addr) = IpAddr::from_str(host).ok() { 101 | // If the host is already an IP address, skip resolving. 102 | vec![SocketAddr::new(addr, port)] 103 | } else { 104 | let addrs = resolve(&mut self.resolver, Name::from_str(host)?).await?; 105 | let addrs = addrs.map(|mut addr| { 106 | addr.set_port(port); 107 | addr 108 | }); 109 | addrs.collect() 110 | }; 111 | 112 | self.remote_connect(addrs, host).await 113 | } 114 | 115 | async fn remote_connect( 116 | &mut self, 117 | addrs: Vec, 118 | server_name: &str, 119 | ) -> Result { 120 | let mut err = None; 121 | for addr in addrs { 122 | match self.endpoint.connect(addr, server_name)?.await { 123 | Ok(new_conn) => { 124 | let quinn_conn = Connection::new(new_conn); 125 | let mut h3_client_builder = h3::client::builder(); 126 | if let Some(max_field_section_size) = self.client_config.max_field_section_size 127 | { 128 | h3_client_builder.max_field_section_size(max_field_section_size); 129 | } 130 | if let Some(send_grease) = self.client_config.send_grease { 131 | h3_client_builder.send_grease(send_grease); 132 | } 133 | return Ok(h3_client_builder.build(quinn_conn).await?); 134 | } 135 | Err(e) => err = Some(e), 136 | } 137 | } 138 | 139 | match err { 140 | Some(e) => Err(Box::new(e) as BoxError), 141 | None => Err("failed to establish connection for HTTP/3 request".into()), 142 | } 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /src/async_impl/h3_client/dns.rs: -------------------------------------------------------------------------------- 1 | use core::task; 2 | use hyper_util::client::legacy::connect::dns::Name; 3 | use std::future::Future; 4 | use std::net::SocketAddr; 5 | use std::task::Poll; 6 | use tower_service::Service; 7 | 8 | // Trait from hyper to implement DNS resolution for HTTP/3 client. 9 | pub trait Resolve { 10 | type Addrs: Iterator; 11 | type Error: Into>; 12 | type Future: Future>; 13 | 14 | fn poll_ready(&mut self, cx: &mut task::Context<'_>) -> Poll>; 15 | fn resolve(&mut self, name: Name) -> Self::Future; 16 | } 17 | 18 | impl Resolve for S 19 | where 20 | S: Service, 21 | S::Response: Iterator, 22 | S::Error: Into>, 23 | { 24 | type Addrs = S::Response; 25 | type Error = S::Error; 26 | type Future = S::Future; 27 | 28 | fn poll_ready(&mut self, cx: &mut task::Context<'_>) -> Poll> { 29 | Service::poll_ready(self, cx) 30 | } 31 | 32 | fn resolve(&mut self, name: Name) -> Self::Future { 33 | Service::call(self, name) 34 | } 35 | } 36 | 37 | pub(super) async fn resolve(resolver: &mut R, name: Name) -> Result 38 | where 39 | R: Resolve, 40 | { 41 | std::future::poll_fn(|cx| resolver.poll_ready(cx)).await?; 42 | resolver.resolve(name).await 43 | } 44 | -------------------------------------------------------------------------------- /src/async_impl/h3_client/mod.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "http3")] 2 | 3 | pub(crate) mod connect; 4 | pub(crate) mod dns; 5 | mod pool; 6 | 7 | use crate::async_impl::body::ResponseBody; 8 | use crate::async_impl::h3_client::pool::{Key, Pool, PoolClient}; 9 | #[cfg(feature = "cookies")] 10 | use crate::cookie; 11 | use crate::error::{BoxError, Error, Kind}; 12 | use crate::{error, Body}; 13 | use connect::H3Connector; 14 | use http::{Request, Response}; 15 | use log::trace; 16 | use std::future::{self, Future}; 17 | use std::pin::Pin; 18 | #[cfg(feature = "cookies")] 19 | use std::sync::Arc; 20 | use std::task::{Context, Poll}; 21 | use std::time::Duration; 22 | use sync_wrapper::SyncWrapper; 23 | use tower::Service; 24 | 25 | #[derive(Clone)] 26 | pub(crate) struct H3Client { 27 | pool: Pool, 28 | connector: H3Connector, 29 | #[cfg(feature = "cookies")] 30 | cookie_store: Option>, 31 | } 32 | 33 | impl H3Client { 34 | #[cfg(not(feature = "cookies"))] 35 | pub fn new(connector: H3Connector, pool_timeout: Option) -> Self { 36 | H3Client { 37 | pool: Pool::new(pool_timeout), 38 | connector, 39 | } 40 | } 41 | 42 | #[cfg(feature = "cookies")] 43 | pub fn new( 44 | connector: H3Connector, 45 | pool_timeout: Option, 46 | cookie_store: Option>, 47 | ) -> Self { 48 | H3Client { 49 | pool: Pool::new(pool_timeout), 50 | connector, 51 | cookie_store, 52 | } 53 | } 54 | 55 | async fn get_pooled_client(&mut self, key: Key) -> Result { 56 | if let Some(client) = self.pool.try_pool(&key) { 57 | trace!("getting client from pool with key {key:?}"); 58 | return Ok(client); 59 | } 60 | 61 | trace!("did not find connection {key:?} in pool so connecting..."); 62 | 63 | let dest = pool::domain_as_uri(key.clone()); 64 | 65 | let lock = match self.pool.connecting(&key) { 66 | pool::Connecting::InProgress(waiter) => { 67 | trace!("connecting to {key:?} is already in progress, subscribing..."); 68 | 69 | match waiter.receive().await { 70 | Some(client) => return Ok(client), 71 | None => return Err("failed to establish connection for HTTP/3 request".into()), 72 | } 73 | } 74 | pool::Connecting::Acquired(lock) => lock, 75 | }; 76 | trace!("connecting to {key:?}..."); 77 | let (driver, tx) = self.connector.connect(dest).await?; 78 | trace!("saving new pooled connection to {key:?}"); 79 | Ok(self.pool.new_connection(lock, driver, tx)) 80 | } 81 | 82 | #[cfg(not(feature = "cookies"))] 83 | async fn send_request( 84 | mut self, 85 | key: Key, 86 | req: Request, 87 | ) -> Result, Error> { 88 | let mut pooled = match self.get_pooled_client(key).await { 89 | Ok(client) => client, 90 | Err(e) => return Err(error::request(e)), 91 | }; 92 | pooled 93 | .send_request(req) 94 | .await 95 | .map_err(|e| Error::new(Kind::Request, Some(e))) 96 | } 97 | 98 | #[cfg(feature = "cookies")] 99 | async fn send_request( 100 | mut self, 101 | key: Key, 102 | mut req: Request, 103 | ) -> Result, Error> { 104 | let mut pooled = match self.get_pooled_client(key).await { 105 | Ok(client) => client, 106 | Err(e) => return Err(error::request(e)), 107 | }; 108 | 109 | let url = url::Url::parse(req.uri().to_string().as_str()).unwrap(); 110 | if let Some(cookie_store) = self.cookie_store.as_ref() { 111 | if req.headers().get(crate::header::COOKIE).is_none() { 112 | let headers = req.headers_mut(); 113 | crate::util::add_cookie_header(headers, &**cookie_store, &url); 114 | } 115 | } 116 | 117 | let res = pooled 118 | .send_request(req) 119 | .await 120 | .map_err(|e| Error::new(Kind::Request, Some(e))); 121 | 122 | if let Some(ref cookie_store) = self.cookie_store { 123 | if let Ok(res) = &res { 124 | let mut cookies = cookie::extract_response_cookie_headers(res.headers()).peekable(); 125 | if cookies.peek().is_some() { 126 | cookie_store.set_cookies(&mut cookies, &url); 127 | } 128 | } 129 | } 130 | 131 | res 132 | } 133 | 134 | pub fn request(&self, mut req: Request) -> H3ResponseFuture { 135 | let pool_key = match pool::extract_domain(req.uri_mut()) { 136 | Ok(s) => s, 137 | Err(e) => { 138 | return H3ResponseFuture { 139 | inner: SyncWrapper::new(Box::pin(future::ready(Err(e)))), 140 | } 141 | } 142 | }; 143 | H3ResponseFuture { 144 | inner: SyncWrapper::new(Box::pin(self.clone().send_request(pool_key, req))), 145 | } 146 | } 147 | } 148 | 149 | impl Service> for H3Client { 150 | type Response = Response; 151 | type Error = Error; 152 | type Future = H3ResponseFuture; 153 | 154 | fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll> { 155 | Poll::Ready(Ok(())) 156 | } 157 | 158 | fn call(&mut self, req: Request) -> Self::Future { 159 | self.request(req) 160 | } 161 | } 162 | 163 | pub(crate) struct H3ResponseFuture { 164 | inner: SyncWrapper, Error>> + Send>>>, 165 | } 166 | 167 | impl Future for H3ResponseFuture { 168 | type Output = Result, Error>; 169 | 170 | fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { 171 | self.inner.get_mut().as_mut().poll(cx) 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /src/async_impl/mod.rs: -------------------------------------------------------------------------------- 1 | pub use self::body::Body; 2 | pub use self::client::{Client, ClientBuilder}; 3 | pub use self::request::{Request, RequestBuilder}; 4 | pub use self::response::Response; 5 | pub use self::upgrade::Upgraded; 6 | 7 | #[cfg(feature = "blocking")] 8 | pub(crate) use self::decoder::Decoder; 9 | 10 | pub mod body; 11 | pub mod client; 12 | pub mod decoder; 13 | pub mod h3_client; 14 | #[cfg(feature = "multipart")] 15 | pub mod multipart; 16 | pub(crate) mod request; 17 | mod response; 18 | mod upgrade; 19 | -------------------------------------------------------------------------------- /src/async_impl/upgrade.rs: -------------------------------------------------------------------------------- 1 | use std::pin::Pin; 2 | use std::task::{self, Poll}; 3 | use std::{fmt, io}; 4 | 5 | use hyper_util::rt::TokioIo; 6 | use tokio::io::{AsyncRead, AsyncWrite, ReadBuf}; 7 | 8 | /// An upgraded HTTP connection. 9 | pub struct Upgraded { 10 | inner: TokioIo, 11 | } 12 | 13 | impl AsyncRead for Upgraded { 14 | fn poll_read( 15 | mut self: Pin<&mut Self>, 16 | cx: &mut task::Context<'_>, 17 | buf: &mut ReadBuf<'_>, 18 | ) -> Poll> { 19 | Pin::new(&mut self.inner).poll_read(cx, buf) 20 | } 21 | } 22 | 23 | impl AsyncWrite for Upgraded { 24 | fn poll_write( 25 | mut self: Pin<&mut Self>, 26 | cx: &mut task::Context<'_>, 27 | buf: &[u8], 28 | ) -> Poll> { 29 | Pin::new(&mut self.inner).poll_write(cx, buf) 30 | } 31 | 32 | fn poll_write_vectored( 33 | mut self: Pin<&mut Self>, 34 | cx: &mut task::Context<'_>, 35 | bufs: &[io::IoSlice<'_>], 36 | ) -> Poll> { 37 | Pin::new(&mut self.inner).poll_write_vectored(cx, bufs) 38 | } 39 | 40 | fn poll_flush(mut self: Pin<&mut Self>, cx: &mut task::Context<'_>) -> Poll> { 41 | Pin::new(&mut self.inner).poll_flush(cx) 42 | } 43 | 44 | fn poll_shutdown(mut self: Pin<&mut Self>, cx: &mut task::Context<'_>) -> Poll> { 45 | Pin::new(&mut self.inner).poll_shutdown(cx) 46 | } 47 | 48 | fn is_write_vectored(&self) -> bool { 49 | self.inner.is_write_vectored() 50 | } 51 | } 52 | 53 | impl fmt::Debug for Upgraded { 54 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 55 | f.debug_struct("Upgraded").finish() 56 | } 57 | } 58 | 59 | impl From for Upgraded { 60 | fn from(inner: hyper::upgrade::Upgraded) -> Self { 61 | Upgraded { 62 | inner: TokioIo::new(inner), 63 | } 64 | } 65 | } 66 | 67 | impl super::response::Response { 68 | /// Consumes the response and returns a future for a possible HTTP upgrade. 69 | pub async fn upgrade(self) -> crate::Result { 70 | hyper::upgrade::on(self.res) 71 | .await 72 | .map(Upgraded::from) 73 | .map_err(crate::error::upgrade) 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/blocking/body.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | use std::fs::File; 3 | use std::future::Future; 4 | #[cfg(feature = "multipart")] 5 | use std::io::Cursor; 6 | use std::io::{self, Read}; 7 | use std::mem::{self, MaybeUninit}; 8 | use std::ptr; 9 | 10 | use bytes::Bytes; 11 | use futures_channel::mpsc; 12 | 13 | use crate::async_impl; 14 | 15 | /// The body of a `Request`. 16 | /// 17 | /// In most cases, this is not needed directly, as the 18 | /// [`RequestBuilder.body`][builder] method uses `Into`, which allows 19 | /// passing many things (like a string or vector of bytes). 20 | /// 21 | /// [builder]: ./struct.RequestBuilder.html#method.body 22 | #[derive(Debug)] 23 | pub struct Body { 24 | kind: Kind, 25 | } 26 | 27 | impl Body { 28 | /// Instantiate a `Body` from a reader. 29 | /// 30 | /// # Note 31 | /// 32 | /// While allowing for many types to be used, these bodies do not have 33 | /// a way to reset to the beginning and be reused. This means that when 34 | /// encountering a 307 or 308 status code, instead of repeating the 35 | /// request at the new location, the `Response` will be returned with 36 | /// the redirect status code set. 37 | /// 38 | /// ```rust 39 | /// # use std::fs::File; 40 | /// # use reqwest::blocking::Body; 41 | /// # fn run() -> Result<(), Box> { 42 | /// let file = File::open("national_secrets.txt")?; 43 | /// let body = Body::new(file); 44 | /// # Ok(()) 45 | /// # } 46 | /// ``` 47 | /// 48 | /// If you have a set of bytes, like `String` or `Vec`, using the 49 | /// `From` implementations for `Body` will store the data in a manner 50 | /// it can be reused. 51 | /// 52 | /// ```rust 53 | /// # use reqwest::blocking::Body; 54 | /// # fn run() -> Result<(), Box> { 55 | /// let s = "A stringy body"; 56 | /// let body = Body::from(s); 57 | /// # Ok(()) 58 | /// # } 59 | /// ``` 60 | pub fn new(reader: R) -> Body { 61 | Body { 62 | kind: Kind::Reader(Box::from(reader), None), 63 | } 64 | } 65 | 66 | /// Create a `Body` from a `Read` where the size is known in advance 67 | /// but the data should not be fully loaded into memory. This will 68 | /// set the `Content-Length` header and stream from the `Read`. 69 | /// 70 | /// ```rust 71 | /// # use std::fs::File; 72 | /// # use reqwest::blocking::Body; 73 | /// # fn run() -> Result<(), Box> { 74 | /// let file = File::open("a_large_file.txt")?; 75 | /// let file_size = file.metadata()?.len(); 76 | /// let body = Body::sized(file, file_size); 77 | /// # Ok(()) 78 | /// # } 79 | /// ``` 80 | pub fn sized(reader: R, len: u64) -> Body { 81 | Body { 82 | kind: Kind::Reader(Box::from(reader), Some(len)), 83 | } 84 | } 85 | 86 | /// Returns the body as a byte slice if the body is already buffered in 87 | /// memory. For streamed requests this method returns `None`. 88 | pub fn as_bytes(&self) -> Option<&[u8]> { 89 | match self.kind { 90 | Kind::Reader(_, _) => None, 91 | Kind::Bytes(ref bytes) => Some(bytes.as_ref()), 92 | } 93 | } 94 | 95 | /// Converts streamed requests to their buffered equivalent and 96 | /// returns a reference to the buffer. If the request is already 97 | /// buffered, this has no effect. 98 | /// 99 | /// Be aware that for large requests this method is expensive 100 | /// and may cause your program to run out of memory. 101 | pub fn buffer(&mut self) -> Result<&[u8], crate::Error> { 102 | match self.kind { 103 | Kind::Reader(ref mut reader, maybe_len) => { 104 | let mut bytes = if let Some(len) = maybe_len { 105 | Vec::with_capacity(len as usize) 106 | } else { 107 | Vec::new() 108 | }; 109 | io::copy(reader, &mut bytes).map_err(crate::error::builder)?; 110 | self.kind = Kind::Bytes(bytes.into()); 111 | self.buffer() 112 | } 113 | Kind::Bytes(ref bytes) => Ok(bytes.as_ref()), 114 | } 115 | } 116 | 117 | #[cfg(feature = "multipart")] 118 | pub(crate) fn len(&self) -> Option { 119 | match self.kind { 120 | Kind::Reader(_, len) => len, 121 | Kind::Bytes(ref bytes) => Some(bytes.len() as u64), 122 | } 123 | } 124 | 125 | #[cfg(feature = "multipart")] 126 | pub(crate) fn into_reader(self) -> Reader { 127 | match self.kind { 128 | Kind::Reader(r, _) => Reader::Reader(r), 129 | Kind::Bytes(b) => Reader::Bytes(Cursor::new(b)), 130 | } 131 | } 132 | 133 | pub(crate) fn into_async(self) -> (Option, async_impl::Body, Option) { 134 | match self.kind { 135 | Kind::Reader(read, len) => { 136 | let (tx, rx) = mpsc::channel(0); 137 | let tx = Sender { 138 | body: (read, len), 139 | tx, 140 | }; 141 | (Some(tx), async_impl::Body::stream(rx), len) 142 | } 143 | Kind::Bytes(chunk) => { 144 | let len = chunk.len() as u64; 145 | (None, async_impl::Body::reusable(chunk), Some(len)) 146 | } 147 | } 148 | } 149 | 150 | pub(crate) fn try_clone(&self) -> Option { 151 | self.kind.try_clone().map(|kind| Body { kind }) 152 | } 153 | } 154 | 155 | enum Kind { 156 | Reader(Box, Option), 157 | Bytes(Bytes), 158 | } 159 | 160 | impl Kind { 161 | fn try_clone(&self) -> Option { 162 | match self { 163 | Kind::Reader(..) => None, 164 | Kind::Bytes(v) => Some(Kind::Bytes(v.clone())), 165 | } 166 | } 167 | } 168 | 169 | impl From> for Body { 170 | #[inline] 171 | fn from(v: Vec) -> Body { 172 | Body { 173 | kind: Kind::Bytes(v.into()), 174 | } 175 | } 176 | } 177 | 178 | impl From for Body { 179 | #[inline] 180 | fn from(s: String) -> Body { 181 | s.into_bytes().into() 182 | } 183 | } 184 | 185 | impl From<&'static [u8]> for Body { 186 | #[inline] 187 | fn from(s: &'static [u8]) -> Body { 188 | Body { 189 | kind: Kind::Bytes(Bytes::from_static(s)), 190 | } 191 | } 192 | } 193 | 194 | impl From<&'static str> for Body { 195 | #[inline] 196 | fn from(s: &'static str) -> Body { 197 | s.as_bytes().into() 198 | } 199 | } 200 | 201 | impl From for Body { 202 | #[inline] 203 | fn from(f: File) -> Body { 204 | let len = f.metadata().map(|m| m.len()).ok(); 205 | Body { 206 | kind: Kind::Reader(Box::new(f), len), 207 | } 208 | } 209 | } 210 | impl From for Body { 211 | #[inline] 212 | fn from(b: Bytes) -> Body { 213 | Body { 214 | kind: Kind::Bytes(b), 215 | } 216 | } 217 | } 218 | 219 | impl fmt::Debug for Kind { 220 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 221 | match *self { 222 | Kind::Reader(_, ref v) => f 223 | .debug_struct("Reader") 224 | .field("length", &DebugLength(v)) 225 | .finish(), 226 | Kind::Bytes(ref v) => fmt::Debug::fmt(v, f), 227 | } 228 | } 229 | } 230 | 231 | struct DebugLength<'a>(&'a Option); 232 | 233 | impl<'a> fmt::Debug for DebugLength<'a> { 234 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 235 | match *self.0 { 236 | Some(ref len) => fmt::Debug::fmt(len, f), 237 | None => f.write_str("Unknown"), 238 | } 239 | } 240 | } 241 | 242 | #[cfg(feature = "multipart")] 243 | pub(crate) enum Reader { 244 | Reader(Box), 245 | Bytes(Cursor), 246 | } 247 | 248 | #[cfg(feature = "multipart")] 249 | impl Read for Reader { 250 | fn read(&mut self, buf: &mut [u8]) -> io::Result { 251 | match *self { 252 | Reader::Reader(ref mut rdr) => rdr.read(buf), 253 | Reader::Bytes(ref mut rdr) => rdr.read(buf), 254 | } 255 | } 256 | } 257 | 258 | pub(crate) struct Sender { 259 | body: (Box, Option), 260 | tx: mpsc::Sender>, 261 | } 262 | 263 | #[derive(Debug)] 264 | struct Abort; 265 | 266 | impl fmt::Display for Abort { 267 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 268 | f.write_str("abort request body") 269 | } 270 | } 271 | 272 | impl std::error::Error for Abort {} 273 | 274 | async fn send_future(sender: Sender) -> Result<(), crate::Error> { 275 | use bytes::{BufMut, BytesMut}; 276 | use futures_util::SinkExt; 277 | use std::cmp; 278 | 279 | let con_len = sender.body.1; 280 | let cap = cmp::min(sender.body.1.unwrap_or(8192), 8192); 281 | let mut written = 0; 282 | let mut buf = BytesMut::zeroed(cap as usize); 283 | buf.clear(); 284 | let mut body = sender.body.0; 285 | // Put in an option so that it can be consumed on error to call abort() 286 | let mut tx = Some(sender.tx); 287 | 288 | loop { 289 | if Some(written) == con_len { 290 | // Written up to content-length, so stop. 291 | return Ok(()); 292 | } 293 | 294 | // The input stream is read only if the buffer is empty so 295 | // that there is only one read in the buffer at any time. 296 | // 297 | // We need to know whether there is any data to send before 298 | // we check the transmission channel (with poll_ready below) 299 | // because sometimes the receiver disappears as soon as it 300 | // considers the data is completely transmitted, which may 301 | // be true. 302 | // 303 | // The use case is a web server that closes its 304 | // input stream as soon as the data received is valid JSON. 305 | // This behaviour is questionable, but it exists and the 306 | // fact is that there is actually no remaining data to read. 307 | if buf.is_empty() { 308 | if buf.capacity() == buf.len() { 309 | buf.reserve(8192); 310 | // zero out the reserved memory 311 | let uninit = buf.spare_capacity_mut(); 312 | let uninit_len = uninit.len(); 313 | unsafe { 314 | ptr::write_bytes(uninit.as_mut_ptr().cast::(), 0, uninit_len); 315 | } 316 | } 317 | 318 | let bytes = unsafe { 319 | mem::transmute::<&mut [MaybeUninit], &mut [u8]>(buf.spare_capacity_mut()) 320 | }; 321 | match body.read(bytes) { 322 | Ok(0) => { 323 | // The buffer was empty and nothing's left to 324 | // read. Return. 325 | return Ok(()); 326 | } 327 | Ok(n) => unsafe { 328 | buf.advance_mut(n); 329 | }, 330 | Err(e) => { 331 | let _ = tx 332 | .take() 333 | .expect("tx only taken on error") 334 | .clone() 335 | .try_send(Err(Abort)); 336 | return Err(crate::error::body(e)); 337 | } 338 | } 339 | } 340 | 341 | // The only way to get here is when the buffer is not empty. 342 | // We can check the transmission channel 343 | 344 | let buf_len = buf.len() as u64; 345 | tx.as_mut() 346 | .expect("tx only taken on error") 347 | .send(Ok(buf.split().freeze())) 348 | .await 349 | .map_err(crate::error::body)?; 350 | 351 | written += buf_len; 352 | } 353 | } 354 | 355 | impl Sender { 356 | // A `Future` that may do blocking read calls. 357 | // As a `Future`, this integrates easily with `wait::timeout`. 358 | pub(crate) fn send(self) -> impl Future> { 359 | send_future(self) 360 | } 361 | } 362 | 363 | // useful for tests, but not publicly exposed 364 | #[cfg(test)] 365 | pub(crate) fn read_to_string(mut body: Body) -> io::Result { 366 | let mut s = String::new(); 367 | match body.kind { 368 | Kind::Reader(ref mut reader, _) => reader.read_to_string(&mut s), 369 | Kind::Bytes(ref mut bytes) => (&**bytes).read_to_string(&mut s), 370 | } 371 | .map(|_| s) 372 | } 373 | -------------------------------------------------------------------------------- /src/blocking/mod.rs: -------------------------------------------------------------------------------- 1 | //! A blocking Client API. 2 | //! 3 | //! The blocking `Client` will block the current thread to execute, instead 4 | //! of returning futures that need to be executed on a runtime. 5 | //! 6 | //! Conversely, the functionality in `reqwest::blocking` must *not* be executed 7 | //! within an async runtime, or it will panic when attempting to block. If 8 | //! calling directly from an async function, consider using an async 9 | //! [`reqwest::Client`][crate::Client] instead. If the immediate context is only 10 | //! synchronous, but a transitive caller is async, consider changing that caller 11 | //! to use [`tokio::task::spawn_blocking`] around the calls that need to block. 12 | //! 13 | //! # Optional 14 | //! 15 | //! This requires the optional `blocking` feature to be enabled. 16 | //! 17 | //! # Making a GET request 18 | //! 19 | //! For a single request, you can use the [`get`] shortcut method. 20 | //! 21 | //! ```rust 22 | //! # use reqwest::{Error, Response}; 23 | //! 24 | //! # fn run() -> Result<(), Error> { 25 | //! let body = reqwest::blocking::get("https://www.rust-lang.org")? 26 | //! .text()?; 27 | //! 28 | //! println!("body = {body:?}"); 29 | //! # Ok(()) 30 | //! # } 31 | //! ``` 32 | //! 33 | //! Additionally, the blocking [`Response`] struct implements Rust's 34 | //! `Read` trait, so many useful standard library and third party crates will 35 | //! have convenience methods that take a `Response` anywhere `T: Read` is 36 | //! acceptable. 37 | //! 38 | //! **NOTE**: If you plan to perform multiple requests, it is best to create a 39 | //! [`Client`] and reuse it, taking advantage of keep-alive connection pooling. 40 | //! 41 | //! # Making POST requests (or setting request bodies) 42 | //! 43 | //! There are several ways you can set the body of a request. The basic one is 44 | //! by using the `body()` method of a [`RequestBuilder`]. This lets you set the 45 | //! exact raw bytes of what the body should be. It accepts various types, 46 | //! including `String`, `Vec`, and `File`. If you wish to pass a custom 47 | //! Reader, you can use the `reqwest::blocking::Body::new()` constructor. 48 | //! 49 | //! ```rust 50 | //! # use reqwest::Error; 51 | //! # 52 | //! # fn run() -> Result<(), Error> { 53 | //! let client = reqwest::blocking::Client::new(); 54 | //! let res = client.post("http://httpbin.org/post") 55 | //! .body("the exact body that is sent") 56 | //! .send()?; 57 | //! # Ok(()) 58 | //! # } 59 | //! ``` 60 | //! 61 | //! ## And More 62 | //! 63 | //! Most features available to the asynchronous `Client` are also available, 64 | //! on the blocking `Client`, see those docs for more. 65 | 66 | mod body; 67 | mod client; 68 | #[cfg(feature = "multipart")] 69 | pub mod multipart; 70 | mod request; 71 | mod response; 72 | mod wait; 73 | 74 | pub use self::body::Body; 75 | pub use self::client::{Client, ClientBuilder}; 76 | pub use self::request::{Request, RequestBuilder}; 77 | pub use self::response::Response; 78 | 79 | /// Shortcut method to quickly make a *blocking* `GET` request. 80 | /// 81 | /// **NOTE**: This function creates a new internal `Client` on each call, 82 | /// and so should not be used if making many requests. Create a 83 | /// [`Client`](./struct.Client.html) instead. 84 | /// 85 | /// # Examples 86 | /// 87 | /// ```rust 88 | /// # fn run() -> Result<(), reqwest::Error> { 89 | /// let body = reqwest::blocking::get("https://www.rust-lang.org")? 90 | /// .text()?; 91 | /// # Ok(()) 92 | /// # } 93 | /// # fn main() { } 94 | /// ``` 95 | /// 96 | /// # Errors 97 | /// 98 | /// This function fails if: 99 | /// 100 | /// - the native TLS backend cannot be initialized, 101 | /// - the supplied `Url` cannot be parsed, 102 | /// - there was an error while sending request, 103 | /// - a redirect loop was detected, 104 | /// - the redirect limit was exhausted, or 105 | /// - the total download time exceeds 30 seconds. 106 | pub fn get(url: T) -> crate::Result { 107 | Client::builder().build()?.get(url).send() 108 | } 109 | -------------------------------------------------------------------------------- /src/blocking/wait.rs: -------------------------------------------------------------------------------- 1 | use std::future::Future; 2 | use std::sync::Arc; 3 | use std::task::{Context, Poll, Wake, Waker}; 4 | use std::thread::{self, Thread}; 5 | use std::time::Duration; 6 | 7 | use tokio::time::Instant; 8 | 9 | pub(crate) fn timeout(fut: F, timeout: Option) -> Result> 10 | where 11 | F: Future>, 12 | { 13 | enter(); 14 | 15 | let deadline = timeout.map(|d| { 16 | log::trace!("wait at most {d:?}"); 17 | Instant::now() + d 18 | }); 19 | 20 | let thread = ThreadWaker(thread::current()); 21 | // Arc shouldn't be necessary, since `Thread` is reference counted internally, 22 | // but let's just stay safe for now. 23 | let waker = Waker::from(Arc::new(thread)); 24 | let mut cx = Context::from_waker(&waker); 25 | 26 | futures_util::pin_mut!(fut); 27 | 28 | loop { 29 | match fut.as_mut().poll(&mut cx) { 30 | Poll::Ready(Ok(val)) => return Ok(val), 31 | Poll::Ready(Err(err)) => return Err(Waited::Inner(err)), 32 | Poll::Pending => (), // fallthrough 33 | } 34 | 35 | if let Some(deadline) = deadline { 36 | let now = Instant::now(); 37 | if now >= deadline { 38 | log::trace!("wait timeout exceeded"); 39 | return Err(Waited::TimedOut(crate::error::TimedOut)); 40 | } 41 | 42 | log::trace!( 43 | "({:?}) park timeout {:?}", 44 | thread::current().id(), 45 | deadline - now 46 | ); 47 | thread::park_timeout(deadline - now); 48 | } else { 49 | log::trace!("({:?}) park without timeout", thread::current().id()); 50 | thread::park(); 51 | } 52 | } 53 | } 54 | 55 | #[derive(Debug)] 56 | pub(crate) enum Waited { 57 | TimedOut(crate::error::TimedOut), 58 | Inner(E), 59 | } 60 | 61 | struct ThreadWaker(Thread); 62 | 63 | impl Wake for ThreadWaker { 64 | fn wake(self: Arc) { 65 | self.wake_by_ref(); 66 | } 67 | 68 | fn wake_by_ref(self: &Arc) { 69 | self.0.unpark(); 70 | } 71 | } 72 | 73 | fn enter() { 74 | // Check we aren't already in a runtime 75 | #[cfg(debug_assertions)] 76 | { 77 | let _enter = tokio::runtime::Builder::new_current_thread() 78 | .build() 79 | .expect("build shell runtime") 80 | .enter(); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/config.rs: -------------------------------------------------------------------------------- 1 | //! The `config` module provides a generic mechanism for loading and managing 2 | //! request-scoped configuration. 3 | //! 4 | //! # Design Overview 5 | //! 6 | //! This module is centered around two abstractions: 7 | //! 8 | //! - The [`RequestConfigValue`] trait, used to associate a config key type with its value type. 9 | //! - The [`RequestConfig`] struct, which wraps an optional value of the type linked via [`RequestConfigValue`]. 10 | //! 11 | //! Under the hood, the [`RequestConfig`] struct holds a single value for the associated config type. 12 | //! This value can be conveniently accessed, inserted, or mutated using [`http::Extensions`], 13 | //! enabling type-safe configuration storage and retrieval on a per-request basis. 14 | //! 15 | //! # Motivation 16 | //! 17 | //! The key design benefit is the ability to store multiple config types—potentially even with the same 18 | //! value type (e.g., [`Duration`])—without code duplication or ambiguity. By leveraging trait association, 19 | //! each config key is distinct at the type level, while code for storage and access remains totally generic. 20 | //! 21 | //! # Usage 22 | //! 23 | //! Implement [`RequestConfigValue`] for any marker type you wish to use as a config key, 24 | //! specifying the associated value type. Then use [`RequestConfig`] in [`Extensions`] 25 | //! to set or retrieve config values for each key type in a uniform way. 26 | 27 | use std::any::type_name; 28 | use std::fmt::Debug; 29 | use std::time::Duration; 30 | 31 | use http::Extensions; 32 | 33 | /// This trait is empty and is only used to associate a configuration key type with its 34 | /// corresponding value type. 35 | pub(crate) trait RequestConfigValue: Copy + Clone + 'static { 36 | type Value: Clone + Debug + Send + Sync + 'static; 37 | } 38 | 39 | /// RequestConfig carries a request-scoped configuration value. 40 | #[derive(Clone, Copy)] 41 | pub(crate) struct RequestConfig(Option); 42 | 43 | impl Default for RequestConfig { 44 | fn default() -> Self { 45 | RequestConfig(None) 46 | } 47 | } 48 | 49 | impl RequestConfig 50 | where 51 | T: RequestConfigValue, 52 | { 53 | pub(crate) fn new(v: Option) -> Self { 54 | RequestConfig(v) 55 | } 56 | 57 | /// format request config value as struct field. 58 | /// 59 | /// We provide this API directly to avoid leak internal value to callers. 60 | pub(crate) fn fmt_as_field(&self, f: &mut std::fmt::DebugStruct<'_, '_>) { 61 | if let Some(v) = &self.0 { 62 | f.field(type_name::(), v); 63 | } 64 | } 65 | 66 | /// Retrieve the value from the request-scoped configuration. 67 | /// 68 | /// If the request specifies a value, use that value; otherwise, attempt to retrieve it from the current instance (typically a client instance). 69 | pub(crate) fn fetch<'client, 'request>( 70 | &'client self, 71 | ext: &'request Extensions, 72 | ) -> Option<&'request T::Value> 73 | where 74 | 'client: 'request, 75 | { 76 | ext.get::>() 77 | .and_then(|v| v.0.as_ref()) 78 | .or(self.0.as_ref()) 79 | } 80 | 81 | /// Retrieve the value from the request's Extensions. 82 | pub(crate) fn get(ext: &Extensions) -> Option<&T::Value> { 83 | ext.get::>().and_then(|v| v.0.as_ref()) 84 | } 85 | 86 | /// Retrieve the mutable value from the request's Extensions. 87 | pub(crate) fn get_mut(ext: &mut Extensions) -> &mut Option { 88 | let cfg = ext.get_or_insert_default::>(); 89 | &mut cfg.0 90 | } 91 | } 92 | 93 | // ================================ 94 | // 95 | // The following sections are all configuration types 96 | // provided by reqwets. 97 | // 98 | // To add a new config: 99 | // 100 | // 1. create a new struct for the config key like `RequestTimeout`. 101 | // 2. implement `RequestConfigValue` for the struct, the `Value` is the config value's type. 102 | // 103 | // ================================ 104 | 105 | #[derive(Clone, Copy)] 106 | pub(crate) struct RequestTimeout; 107 | 108 | impl RequestConfigValue for RequestTimeout { 109 | type Value = Duration; 110 | } 111 | -------------------------------------------------------------------------------- /src/cookie.rs: -------------------------------------------------------------------------------- 1 | //! HTTP Cookies 2 | 3 | use std::convert::TryInto; 4 | use std::fmt; 5 | use std::sync::RwLock; 6 | use std::time::SystemTime; 7 | 8 | use crate::header::{HeaderValue, SET_COOKIE}; 9 | use bytes::Bytes; 10 | 11 | /// Actions for a persistent cookie store providing session support. 12 | pub trait CookieStore: Send + Sync { 13 | /// Store a set of Set-Cookie header values received from `url` 14 | fn set_cookies(&self, cookie_headers: &mut dyn Iterator, url: &url::Url); 15 | /// Get any Cookie values in the store for `url` 16 | fn cookies(&self, url: &url::Url) -> Option; 17 | } 18 | 19 | /// A single HTTP cookie. 20 | pub struct Cookie<'a>(cookie_crate::Cookie<'a>); 21 | 22 | /// A good default `CookieStore` implementation. 23 | /// 24 | /// This is the implementation used when simply calling `cookie_store(true)`. 25 | /// This type is exposed to allow creating one and filling it with some 26 | /// existing cookies more easily, before creating a `Client`. 27 | /// 28 | /// For more advanced scenarios, such as needing to serialize the store or 29 | /// manipulate it between requests, you may refer to the 30 | /// [reqwest_cookie_store crate](https://crates.io/crates/reqwest_cookie_store). 31 | #[derive(Debug, Default)] 32 | pub struct Jar(RwLock); 33 | 34 | // ===== impl Cookie ===== 35 | 36 | impl<'a> Cookie<'a> { 37 | fn parse(value: &'a HeaderValue) -> Result, CookieParseError> { 38 | std::str::from_utf8(value.as_bytes()) 39 | .map_err(cookie_crate::ParseError::from) 40 | .and_then(cookie_crate::Cookie::parse) 41 | .map_err(CookieParseError) 42 | .map(Cookie) 43 | } 44 | 45 | /// The name of the cookie. 46 | pub fn name(&self) -> &str { 47 | self.0.name() 48 | } 49 | 50 | /// The value of the cookie. 51 | pub fn value(&self) -> &str { 52 | self.0.value() 53 | } 54 | 55 | /// Returns true if the 'HttpOnly' directive is enabled. 56 | pub fn http_only(&self) -> bool { 57 | self.0.http_only().unwrap_or(false) 58 | } 59 | 60 | /// Returns true if the 'Secure' directive is enabled. 61 | pub fn secure(&self) -> bool { 62 | self.0.secure().unwrap_or(false) 63 | } 64 | 65 | /// Returns true if 'SameSite' directive is 'Lax'. 66 | pub fn same_site_lax(&self) -> bool { 67 | self.0.same_site() == Some(cookie_crate::SameSite::Lax) 68 | } 69 | 70 | /// Returns true if 'SameSite' directive is 'Strict'. 71 | pub fn same_site_strict(&self) -> bool { 72 | self.0.same_site() == Some(cookie_crate::SameSite::Strict) 73 | } 74 | 75 | /// Returns the path directive of the cookie, if set. 76 | pub fn path(&self) -> Option<&str> { 77 | self.0.path() 78 | } 79 | 80 | /// Returns the domain directive of the cookie, if set. 81 | pub fn domain(&self) -> Option<&str> { 82 | self.0.domain() 83 | } 84 | 85 | /// Get the Max-Age information. 86 | pub fn max_age(&self) -> Option { 87 | self.0.max_age().map(|d| { 88 | d.try_into() 89 | .expect("time::Duration into std::time::Duration") 90 | }) 91 | } 92 | 93 | /// The cookie expiration time. 94 | pub fn expires(&self) -> Option { 95 | match self.0.expires() { 96 | Some(cookie_crate::Expiration::DateTime(offset)) => Some(SystemTime::from(offset)), 97 | None | Some(cookie_crate::Expiration::Session) => None, 98 | } 99 | } 100 | } 101 | 102 | impl<'a> fmt::Debug for Cookie<'a> { 103 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 104 | self.0.fmt(f) 105 | } 106 | } 107 | 108 | pub(crate) fn extract_response_cookie_headers<'a>( 109 | headers: &'a hyper::HeaderMap, 110 | ) -> impl Iterator + 'a { 111 | headers.get_all(SET_COOKIE).iter() 112 | } 113 | 114 | pub(crate) fn extract_response_cookies<'a>( 115 | headers: &'a hyper::HeaderMap, 116 | ) -> impl Iterator, CookieParseError>> + 'a { 117 | headers 118 | .get_all(SET_COOKIE) 119 | .iter() 120 | .map(|value| Cookie::parse(value)) 121 | } 122 | 123 | /// Error representing a parse failure of a 'Set-Cookie' header. 124 | pub(crate) struct CookieParseError(cookie_crate::ParseError); 125 | 126 | impl<'a> fmt::Debug for CookieParseError { 127 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 128 | self.0.fmt(f) 129 | } 130 | } 131 | 132 | impl<'a> fmt::Display for CookieParseError { 133 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 134 | self.0.fmt(f) 135 | } 136 | } 137 | 138 | impl std::error::Error for CookieParseError {} 139 | 140 | // ===== impl Jar ===== 141 | 142 | impl Jar { 143 | /// Add a cookie to this jar. 144 | /// 145 | /// # Example 146 | /// 147 | /// ``` 148 | /// use reqwest::{cookie::Jar, Url}; 149 | /// 150 | /// let cookie = "foo=bar; Domain=yolo.local"; 151 | /// let url = "https://yolo.local".parse::().unwrap(); 152 | /// 153 | /// let jar = Jar::default(); 154 | /// jar.add_cookie_str(cookie, &url); 155 | /// 156 | /// // and now add to a `ClientBuilder`? 157 | /// ``` 158 | pub fn add_cookie_str(&self, cookie: &str, url: &url::Url) { 159 | let cookies = cookie_crate::Cookie::parse(cookie) 160 | .ok() 161 | .map(|c| c.into_owned()) 162 | .into_iter(); 163 | self.0.write().unwrap().store_response_cookies(cookies, url); 164 | } 165 | } 166 | 167 | impl CookieStore for Jar { 168 | fn set_cookies(&self, cookie_headers: &mut dyn Iterator, url: &url::Url) { 169 | let iter = 170 | cookie_headers.filter_map(|val| Cookie::parse(val).map(|c| c.0.into_owned()).ok()); 171 | 172 | self.0.write().unwrap().store_response_cookies(iter, url); 173 | } 174 | 175 | fn cookies(&self, url: &url::Url) -> Option { 176 | let s = self 177 | .0 178 | .read() 179 | .unwrap() 180 | .get_request_values(url) 181 | .map(|(name, value)| format!("{name}={value}")) 182 | .collect::>() 183 | .join("; "); 184 | 185 | if s.is_empty() { 186 | return None; 187 | } 188 | 189 | HeaderValue::from_maybe_shared(Bytes::from(s)).ok() 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /src/dns/gai.rs: -------------------------------------------------------------------------------- 1 | use hyper_util::client::legacy::connect::dns::GaiResolver as HyperGaiResolver; 2 | use tower_service::Service; 3 | 4 | use crate::dns::{Addrs, Name, Resolve, Resolving}; 5 | use crate::error::BoxError; 6 | 7 | #[derive(Debug)] 8 | pub struct GaiResolver(HyperGaiResolver); 9 | 10 | impl GaiResolver { 11 | pub fn new() -> Self { 12 | Self(HyperGaiResolver::new()) 13 | } 14 | } 15 | 16 | impl Default for GaiResolver { 17 | fn default() -> Self { 18 | GaiResolver::new() 19 | } 20 | } 21 | 22 | impl Resolve for GaiResolver { 23 | fn resolve(&self, name: Name) -> Resolving { 24 | let mut this = self.0.clone(); 25 | Box::pin(async move { 26 | this.call(name.0) 27 | .await 28 | .map(|addrs| Box::new(addrs) as Addrs) 29 | .map_err(|err| Box::new(err) as BoxError) 30 | }) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/dns/hickory.rs: -------------------------------------------------------------------------------- 1 | //! DNS resolution via the [hickory-resolver](https://github.com/hickory-dns/hickory-dns) crate 2 | 3 | use hickory_resolver::{ 4 | config::LookupIpStrategy, error::ResolveError, lookup_ip::LookupIpIntoIter, system_conf, 5 | TokioAsyncResolver, 6 | }; 7 | use once_cell::sync::OnceCell; 8 | 9 | use std::fmt; 10 | use std::net::SocketAddr; 11 | use std::sync::Arc; 12 | 13 | use super::{Addrs, Name, Resolve, Resolving}; 14 | 15 | /// Wrapper around an `AsyncResolver`, which implements the `Resolve` trait. 16 | #[derive(Debug, Default, Clone)] 17 | pub(crate) struct HickoryDnsResolver { 18 | /// Since we might not have been called in the context of a 19 | /// Tokio Runtime in initialization, so we must delay the actual 20 | /// construction of the resolver. 21 | state: Arc>, 22 | } 23 | 24 | struct SocketAddrs { 25 | iter: LookupIpIntoIter, 26 | } 27 | 28 | #[derive(Debug)] 29 | struct HickoryDnsSystemConfError(ResolveError); 30 | 31 | impl Resolve for HickoryDnsResolver { 32 | fn resolve(&self, name: Name) -> Resolving { 33 | let resolver = self.clone(); 34 | Box::pin(async move { 35 | let resolver = resolver.state.get_or_try_init(new_resolver)?; 36 | 37 | let lookup = resolver.lookup_ip(name.as_str()).await?; 38 | let addrs: Addrs = Box::new(SocketAddrs { 39 | iter: lookup.into_iter(), 40 | }); 41 | Ok(addrs) 42 | }) 43 | } 44 | } 45 | 46 | impl Iterator for SocketAddrs { 47 | type Item = SocketAddr; 48 | 49 | fn next(&mut self) -> Option { 50 | self.iter.next().map(|ip_addr| SocketAddr::new(ip_addr, 0)) 51 | } 52 | } 53 | 54 | /// Create a new resolver with the default configuration, 55 | /// which reads from `/etc/resolve.conf`. The options are 56 | /// overridden to look up for both IPv4 and IPv6 addresses 57 | /// to work with "happy eyeballs" algorithm. 58 | fn new_resolver() -> Result { 59 | let (config, mut opts) = system_conf::read_system_conf().map_err(HickoryDnsSystemConfError)?; 60 | opts.ip_strategy = LookupIpStrategy::Ipv4AndIpv6; 61 | Ok(TokioAsyncResolver::tokio(config, opts)) 62 | } 63 | 64 | impl fmt::Display for HickoryDnsSystemConfError { 65 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 66 | f.write_str("error reading DNS system conf for hickory-dns") 67 | } 68 | } 69 | 70 | impl std::error::Error for HickoryDnsSystemConfError { 71 | fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { 72 | Some(&self.0) 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/dns/mod.rs: -------------------------------------------------------------------------------- 1 | //! DNS resolution 2 | 3 | pub use resolve::{Addrs, Name, Resolve, Resolving}; 4 | pub(crate) use resolve::{DnsResolverWithOverrides, DynResolver}; 5 | 6 | pub(crate) mod gai; 7 | #[cfg(feature = "hickory-dns")] 8 | pub(crate) mod hickory; 9 | pub(crate) mod resolve; 10 | -------------------------------------------------------------------------------- /src/dns/resolve.rs: -------------------------------------------------------------------------------- 1 | use hyper_util::client::legacy::connect::dns::Name as HyperName; 2 | use tower_service::Service; 3 | 4 | use std::collections::HashMap; 5 | use std::future::Future; 6 | use std::net::SocketAddr; 7 | use std::pin::Pin; 8 | use std::str::FromStr; 9 | use std::sync::Arc; 10 | use std::task::{Context, Poll}; 11 | 12 | use crate::error::BoxError; 13 | 14 | /// Alias for an `Iterator` trait object over `SocketAddr`. 15 | pub type Addrs = Box + Send>; 16 | 17 | /// Alias for the `Future` type returned by a DNS resolver. 18 | pub type Resolving = Pin> + Send>>; 19 | 20 | /// Trait for customizing DNS resolution in reqwest. 21 | pub trait Resolve: Send + Sync { 22 | /// Performs DNS resolution on a `Name`. 23 | /// The return type is a future containing an iterator of `SocketAddr`. 24 | /// 25 | /// It differs from `tower_service::Service` in several ways: 26 | /// * It is assumed that `resolve` will always be ready to poll. 27 | /// * It does not need a mutable reference to `self`. 28 | /// * Since trait objects cannot make use of associated types, it requires 29 | /// wrapping the returned `Future` and its contained `Iterator` with `Box`. 30 | /// 31 | /// Explicitly specified port in the URL will override any port in the resolved `SocketAddr`s. 32 | /// Otherwise, port `0` will be replaced by the conventional port for the given scheme (e.g. 80 for http). 33 | fn resolve(&self, name: Name) -> Resolving; 34 | } 35 | 36 | /// A name that must be resolved to addresses. 37 | #[derive(Debug)] 38 | pub struct Name(pub(super) HyperName); 39 | 40 | impl Name { 41 | /// View the name as a string. 42 | pub fn as_str(&self) -> &str { 43 | self.0.as_str() 44 | } 45 | } 46 | 47 | impl FromStr for Name { 48 | type Err = sealed::InvalidNameError; 49 | 50 | fn from_str(host: &str) -> Result { 51 | HyperName::from_str(host) 52 | .map(Name) 53 | .map_err(|_| sealed::InvalidNameError { _ext: () }) 54 | } 55 | } 56 | 57 | #[derive(Clone)] 58 | pub(crate) struct DynResolver { 59 | resolver: Arc, 60 | } 61 | 62 | impl DynResolver { 63 | pub(crate) fn new(resolver: Arc) -> Self { 64 | Self { resolver } 65 | } 66 | 67 | #[cfg(feature = "socks")] 68 | pub(crate) fn gai() -> Self { 69 | Self::new(Arc::new(super::gai::GaiResolver::new())) 70 | } 71 | 72 | /// Resolve an HTTP host and port, not just a domain name. 73 | /// 74 | /// This does the same thing that hyper-util's HttpConnector does, before 75 | /// calling out to its underlying DNS resolver. 76 | #[cfg(feature = "socks")] 77 | pub(crate) async fn http_resolve( 78 | &self, 79 | target: &http::Uri, 80 | ) -> Result, BoxError> { 81 | let host = target.host().ok_or("missing host")?; 82 | let port = target 83 | .port_u16() 84 | .unwrap_or_else(|| match target.scheme_str() { 85 | Some("https") => 443, 86 | Some("socks4") | Some("socks4h") | Some("socks5") | Some("socks5h") => 1080, 87 | _ => 80, 88 | }); 89 | 90 | let explicit_port = target.port().is_some(); 91 | 92 | let addrs = self.resolver.resolve(host.parse()?).await?; 93 | 94 | Ok(addrs.map(move |mut addr| { 95 | if explicit_port || addr.port() == 0 { 96 | addr.set_port(port); 97 | } 98 | addr 99 | })) 100 | } 101 | } 102 | 103 | impl Service for DynResolver { 104 | type Response = Addrs; 105 | type Error = BoxError; 106 | type Future = Resolving; 107 | 108 | fn poll_ready(&mut self, _: &mut Context<'_>) -> Poll> { 109 | Poll::Ready(Ok(())) 110 | } 111 | 112 | fn call(&mut self, name: HyperName) -> Self::Future { 113 | self.resolver.resolve(Name(name)) 114 | } 115 | } 116 | 117 | pub(crate) struct DnsResolverWithOverrides { 118 | dns_resolver: Arc, 119 | overrides: Arc>>, 120 | } 121 | 122 | impl DnsResolverWithOverrides { 123 | pub(crate) fn new( 124 | dns_resolver: Arc, 125 | overrides: HashMap>, 126 | ) -> Self { 127 | DnsResolverWithOverrides { 128 | dns_resolver, 129 | overrides: Arc::new(overrides), 130 | } 131 | } 132 | } 133 | 134 | impl Resolve for DnsResolverWithOverrides { 135 | fn resolve(&self, name: Name) -> Resolving { 136 | match self.overrides.get(name.as_str()) { 137 | Some(dest) => { 138 | let addrs: Addrs = Box::new(dest.clone().into_iter()); 139 | Box::pin(std::future::ready(Ok(addrs))) 140 | } 141 | None => self.dns_resolver.resolve(name), 142 | } 143 | } 144 | } 145 | 146 | mod sealed { 147 | use std::fmt; 148 | 149 | #[derive(Debug)] 150 | pub struct InvalidNameError { 151 | pub(super) _ext: (), 152 | } 153 | 154 | impl fmt::Display for InvalidNameError { 155 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 156 | f.write_str("invalid DNS name") 157 | } 158 | } 159 | 160 | impl std::error::Error for InvalidNameError {} 161 | } 162 | -------------------------------------------------------------------------------- /src/into_url.rs: -------------------------------------------------------------------------------- 1 | use url::Url; 2 | 3 | /// A trait to try to convert some type into a `Url`. 4 | /// 5 | /// This trait is "sealed", such that only types within reqwest can 6 | /// implement it. 7 | pub trait IntoUrl: IntoUrlSealed {} 8 | 9 | impl IntoUrl for Url {} 10 | impl IntoUrl for String {} 11 | impl<'a> IntoUrl for &'a str {} 12 | impl<'a> IntoUrl for &'a String {} 13 | 14 | pub trait IntoUrlSealed { 15 | // Besides parsing as a valid `Url`, the `Url` must be a valid 16 | // `http::Uri`, in that it makes sense to use in a network request. 17 | fn into_url(self) -> crate::Result; 18 | 19 | fn as_str(&self) -> &str; 20 | } 21 | 22 | impl IntoUrlSealed for Url { 23 | fn into_url(self) -> crate::Result { 24 | // With blob url the `self.has_host()` check is always false, so we 25 | // remove the `blob:` scheme and check again if the url is valid. 26 | #[cfg(target_arch = "wasm32")] 27 | if self.scheme() == "blob" 28 | && self.path().starts_with("http") // Check if the path starts with http or https to avoid validating a `blob:blob:...` url. 29 | && self.as_str()[5..].into_url().is_ok() 30 | { 31 | return Ok(self); 32 | } 33 | 34 | if self.has_host() { 35 | Ok(self) 36 | } else { 37 | Err(crate::error::url_bad_scheme(self)) 38 | } 39 | } 40 | 41 | fn as_str(&self) -> &str { 42 | self.as_ref() 43 | } 44 | } 45 | 46 | impl<'a> IntoUrlSealed for &'a str { 47 | fn into_url(self) -> crate::Result { 48 | Url::parse(self).map_err(crate::error::builder)?.into_url() 49 | } 50 | 51 | fn as_str(&self) -> &str { 52 | self 53 | } 54 | } 55 | 56 | impl<'a> IntoUrlSealed for &'a String { 57 | fn into_url(self) -> crate::Result { 58 | (&**self).into_url() 59 | } 60 | 61 | fn as_str(&self) -> &str { 62 | self.as_ref() 63 | } 64 | } 65 | 66 | impl IntoUrlSealed for String { 67 | fn into_url(self) -> crate::Result { 68 | (&*self).into_url() 69 | } 70 | 71 | fn as_str(&self) -> &str { 72 | self.as_ref() 73 | } 74 | } 75 | 76 | if_hyper! { 77 | pub(crate) fn try_uri(url: &Url) -> crate::Result { 78 | url.as_str() 79 | .parse() 80 | .map_err(|_| crate::error::url_invalid_uri(url.clone())) 81 | } 82 | } 83 | 84 | #[cfg(test)] 85 | mod tests { 86 | use super::*; 87 | use std::error::Error; 88 | 89 | #[test] 90 | fn into_url_file_scheme() { 91 | let err = "file:///etc/hosts".into_url().unwrap_err(); 92 | assert_eq!( 93 | err.source().unwrap().to_string(), 94 | "URL scheme is not allowed" 95 | ); 96 | } 97 | 98 | #[test] 99 | fn into_url_blob_scheme() { 100 | let err = "blob:https://example.com".into_url().unwrap_err(); 101 | assert_eq!( 102 | err.source().unwrap().to_string(), 103 | "URL scheme is not allowed" 104 | ); 105 | } 106 | 107 | if_wasm! { 108 | use wasm_bindgen_test::*; 109 | 110 | #[wasm_bindgen_test] 111 | fn into_url_blob_scheme_wasm() { 112 | let url = "blob:http://example.com".into_url().unwrap(); 113 | 114 | assert_eq!(url.as_str(), "blob:http://example.com"); 115 | } 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/response.rs: -------------------------------------------------------------------------------- 1 | use url::Url; 2 | 3 | #[derive(Debug, Clone, PartialEq)] 4 | pub(crate) struct ResponseUrl(pub Url); 5 | 6 | /// Extension trait for http::response::Builder objects 7 | /// 8 | /// Allows the user to add a `Url` to the http::Response 9 | pub trait ResponseBuilderExt { 10 | /// A builder method for the `http::response::Builder` type that allows the user to add a `Url` 11 | /// to the `http::Response` 12 | fn url(self, url: Url) -> Self; 13 | } 14 | 15 | impl ResponseBuilderExt for http::response::Builder { 16 | fn url(self, url: Url) -> Self { 17 | self.extension(ResponseUrl(url)) 18 | } 19 | } 20 | 21 | #[cfg(test)] 22 | mod tests { 23 | use super::{ResponseBuilderExt, ResponseUrl}; 24 | use http::response::Builder; 25 | use url::Url; 26 | 27 | #[test] 28 | fn test_response_builder_ext() { 29 | let url = Url::parse("http://example.com").unwrap(); 30 | let response = Builder::new() 31 | .status(200) 32 | .url(url.clone()) 33 | .body(()) 34 | .unwrap(); 35 | 36 | assert_eq!( 37 | response.extensions().get::(), 38 | Some(&ResponseUrl(url)) 39 | ); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/util.rs: -------------------------------------------------------------------------------- 1 | use crate::header::{Entry, HeaderMap, HeaderValue, OccupiedEntry}; 2 | use std::fmt; 3 | 4 | pub fn basic_auth(username: U, password: Option

) -> HeaderValue 5 | where 6 | U: std::fmt::Display, 7 | P: std::fmt::Display, 8 | { 9 | use base64::prelude::BASE64_STANDARD; 10 | use base64::write::EncoderWriter; 11 | use std::io::Write; 12 | 13 | let mut buf = b"Basic ".to_vec(); 14 | { 15 | let mut encoder = EncoderWriter::new(&mut buf, &BASE64_STANDARD); 16 | let _ = write!(encoder, "{username}:"); 17 | if let Some(password) = password { 18 | let _ = write!(encoder, "{password}"); 19 | } 20 | } 21 | let mut header = HeaderValue::from_bytes(&buf).expect("base64 is always valid HeaderValue"); 22 | header.set_sensitive(true); 23 | header 24 | } 25 | 26 | // xor-shift 27 | #[cfg(not(target_arch = "wasm32"))] 28 | pub(crate) fn fast_random() -> u64 { 29 | use std::cell::Cell; 30 | use std::collections::hash_map::RandomState; 31 | use std::hash::{BuildHasher, Hasher}; 32 | use std::num::Wrapping; 33 | 34 | thread_local! { 35 | static RNG: Cell> = Cell::new(Wrapping(seed())); 36 | } 37 | 38 | fn seed() -> u64 { 39 | let seed = RandomState::new(); 40 | 41 | let mut out = 0; 42 | let mut cnt = 0; 43 | while out == 0 { 44 | cnt += 1; 45 | let mut hasher = seed.build_hasher(); 46 | hasher.write_usize(cnt); 47 | out = hasher.finish(); 48 | } 49 | out 50 | } 51 | 52 | RNG.with(|rng| { 53 | let mut n = rng.get(); 54 | debug_assert_ne!(n.0, 0); 55 | n ^= n >> 12; 56 | n ^= n << 25; 57 | n ^= n >> 27; 58 | rng.set(n); 59 | n.0.wrapping_mul(0x2545_f491_4f6c_dd1d) 60 | }) 61 | } 62 | 63 | pub(crate) fn replace_headers(dst: &mut HeaderMap, src: HeaderMap) { 64 | // IntoIter of HeaderMap yields (Option, HeaderValue). 65 | // The first time a name is yielded, it will be Some(name), and if 66 | // there are more values with the same name, the next yield will be 67 | // None. 68 | 69 | let mut prev_entry: Option> = None; 70 | for (key, value) in src { 71 | match key { 72 | Some(key) => match dst.entry(key) { 73 | Entry::Occupied(mut e) => { 74 | e.insert(value); 75 | prev_entry = Some(e); 76 | } 77 | Entry::Vacant(e) => { 78 | let e = e.insert_entry(value); 79 | prev_entry = Some(e); 80 | } 81 | }, 82 | None => match prev_entry { 83 | Some(ref mut entry) => { 84 | entry.append(value); 85 | } 86 | None => unreachable!("HeaderMap::into_iter yielded None first"), 87 | }, 88 | } 89 | } 90 | } 91 | 92 | #[cfg(feature = "cookies")] 93 | #[cfg(not(target_arch = "wasm32"))] 94 | pub(crate) fn add_cookie_header( 95 | headers: &mut HeaderMap, 96 | cookie_store: &dyn crate::cookie::CookieStore, 97 | url: &url::Url, 98 | ) { 99 | if let Some(header) = cookie_store.cookies(url) { 100 | headers.insert(crate::header::COOKIE, header); 101 | } 102 | } 103 | 104 | pub(crate) struct Escape<'a>(&'a [u8]); 105 | 106 | #[cfg(not(target_arch = "wasm32"))] 107 | impl<'a> Escape<'a> { 108 | pub(crate) fn new(bytes: &'a [u8]) -> Self { 109 | Escape(bytes) 110 | } 111 | } 112 | 113 | impl fmt::Debug for Escape<'_> { 114 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 115 | write!(f, "b\"{}\"", self)?; 116 | Ok(()) 117 | } 118 | } 119 | 120 | impl fmt::Display for Escape<'_> { 121 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 122 | for &c in self.0 { 123 | // https://doc.rust-lang.org/reference.html#byte-escapes 124 | if c == b'\n' { 125 | write!(f, "\\n")?; 126 | } else if c == b'\r' { 127 | write!(f, "\\r")?; 128 | } else if c == b'\t' { 129 | write!(f, "\\t")?; 130 | } else if c == b'\\' || c == b'"' { 131 | write!(f, "\\{}", c as char)?; 132 | } else if c == b'\0' { 133 | write!(f, "\\0")?; 134 | // ASCII printable 135 | } else if c >= 0x20 && c < 0x7f { 136 | write!(f, "{}", c as char)?; 137 | } else { 138 | write!(f, "\\x{c:02x}")?; 139 | } 140 | } 141 | Ok(()) 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /src/wasm/body.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "multipart")] 2 | use super::multipart::Form; 3 | /// dox 4 | use bytes::Bytes; 5 | use js_sys::Uint8Array; 6 | use std::{borrow::Cow, fmt}; 7 | use wasm_bindgen::JsValue; 8 | 9 | /// The body of a `Request`. 10 | /// 11 | /// In most cases, this is not needed directly, as the 12 | /// [`RequestBuilder.body`][builder] method uses `Into`, which allows 13 | /// passing many things (like a string or vector of bytes). 14 | /// 15 | /// [builder]: ./struct.RequestBuilder.html#method.body 16 | pub struct Body { 17 | inner: Inner, 18 | } 19 | 20 | enum Inner { 21 | Single(Single), 22 | /// MultipartForm holds a multipart/form-data body. 23 | #[cfg(feature = "multipart")] 24 | MultipartForm(Form), 25 | } 26 | 27 | #[derive(Clone)] 28 | pub(crate) enum Single { 29 | Bytes(Bytes), 30 | Text(Cow<'static, str>), 31 | } 32 | 33 | impl Single { 34 | fn as_bytes(&self) -> &[u8] { 35 | match self { 36 | Single::Bytes(bytes) => bytes.as_ref(), 37 | Single::Text(text) => text.as_bytes(), 38 | } 39 | } 40 | 41 | pub(crate) fn to_js_value(&self) -> JsValue { 42 | match self { 43 | Single::Bytes(bytes) => { 44 | let body_bytes: &[u8] = bytes.as_ref(); 45 | let body_uint8_array: Uint8Array = body_bytes.into(); 46 | let js_value: &JsValue = body_uint8_array.as_ref(); 47 | js_value.to_owned() 48 | } 49 | Single::Text(text) => JsValue::from_str(text), 50 | } 51 | } 52 | 53 | fn is_empty(&self) -> bool { 54 | match self { 55 | Single::Bytes(bytes) => bytes.is_empty(), 56 | Single::Text(text) => text.is_empty(), 57 | } 58 | } 59 | } 60 | 61 | impl Body { 62 | /// Returns a reference to the internal data of the `Body`. 63 | /// 64 | /// `None` is returned, if the underlying data is a multipart form. 65 | #[inline] 66 | pub fn as_bytes(&self) -> Option<&[u8]> { 67 | match &self.inner { 68 | Inner::Single(single) => Some(single.as_bytes()), 69 | #[cfg(feature = "multipart")] 70 | Inner::MultipartForm(_) => None, 71 | } 72 | } 73 | 74 | pub(crate) fn to_js_value(&self) -> crate::Result { 75 | match &self.inner { 76 | Inner::Single(single) => Ok(single.to_js_value()), 77 | #[cfg(feature = "multipart")] 78 | Inner::MultipartForm(form) => { 79 | let form_data = form.to_form_data()?; 80 | let js_value: &JsValue = form_data.as_ref(); 81 | Ok(js_value.to_owned()) 82 | } 83 | } 84 | } 85 | 86 | #[cfg(feature = "multipart")] 87 | pub(crate) fn as_single(&self) -> Option<&Single> { 88 | match &self.inner { 89 | Inner::Single(single) => Some(single), 90 | Inner::MultipartForm(_) => None, 91 | } 92 | } 93 | 94 | #[inline] 95 | #[cfg(feature = "multipart")] 96 | pub(crate) fn from_form(f: Form) -> Body { 97 | Self { 98 | inner: Inner::MultipartForm(f), 99 | } 100 | } 101 | 102 | /// into_part turns a regular body into the body of a multipart/form-data part. 103 | #[cfg(feature = "multipart")] 104 | pub(crate) fn into_part(self) -> Body { 105 | match self.inner { 106 | Inner::Single(single) => Self { 107 | inner: Inner::Single(single), 108 | }, 109 | Inner::MultipartForm(form) => Self { 110 | inner: Inner::MultipartForm(form), 111 | }, 112 | } 113 | } 114 | 115 | pub(crate) fn is_empty(&self) -> bool { 116 | match &self.inner { 117 | Inner::Single(single) => single.is_empty(), 118 | #[cfg(feature = "multipart")] 119 | Inner::MultipartForm(form) => form.is_empty(), 120 | } 121 | } 122 | 123 | pub(crate) fn try_clone(&self) -> Option { 124 | match &self.inner { 125 | Inner::Single(single) => Some(Self { 126 | inner: Inner::Single(single.clone()), 127 | }), 128 | #[cfg(feature = "multipart")] 129 | Inner::MultipartForm(_) => None, 130 | } 131 | } 132 | } 133 | 134 | impl From for Body { 135 | #[inline] 136 | fn from(bytes: Bytes) -> Body { 137 | Body { 138 | inner: Inner::Single(Single::Bytes(bytes)), 139 | } 140 | } 141 | } 142 | 143 | impl From> for Body { 144 | #[inline] 145 | fn from(vec: Vec) -> Body { 146 | Body { 147 | inner: Inner::Single(Single::Bytes(vec.into())), 148 | } 149 | } 150 | } 151 | 152 | impl From<&'static [u8]> for Body { 153 | #[inline] 154 | fn from(s: &'static [u8]) -> Body { 155 | Body { 156 | inner: Inner::Single(Single::Bytes(Bytes::from_static(s))), 157 | } 158 | } 159 | } 160 | 161 | impl From for Body { 162 | #[inline] 163 | fn from(s: String) -> Body { 164 | Body { 165 | inner: Inner::Single(Single::Text(s.into())), 166 | } 167 | } 168 | } 169 | 170 | impl From<&'static str> for Body { 171 | #[inline] 172 | fn from(s: &'static str) -> Body { 173 | Body { 174 | inner: Inner::Single(Single::Text(s.into())), 175 | } 176 | } 177 | } 178 | 179 | impl fmt::Debug for Body { 180 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 181 | f.debug_struct("Body").finish() 182 | } 183 | } 184 | 185 | // Can use new methods in web-sys when requiring v0.2.93. 186 | // > `init.method(m)` to `init.set_method(m)` 187 | // For now, ignore their deprecation. 188 | #[allow(deprecated)] 189 | #[cfg(test)] 190 | mod tests { 191 | use crate::Body; 192 | use js_sys::Uint8Array; 193 | use wasm_bindgen::prelude::*; 194 | use wasm_bindgen_test::*; 195 | 196 | wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); 197 | 198 | #[wasm_bindgen] 199 | extern "C" { 200 | // Use `js_namespace` here to bind `console.log(..)` instead of just 201 | // `log(..)` 202 | #[wasm_bindgen(js_namespace = console)] 203 | fn log(s: String); 204 | } 205 | 206 | #[wasm_bindgen_test] 207 | async fn test_body() { 208 | let body = Body::from("TEST"); 209 | assert_eq!([84, 69, 83, 84], body.as_bytes().unwrap()); 210 | } 211 | 212 | #[wasm_bindgen_test] 213 | async fn test_body_js_static_str() { 214 | let body_value = "TEST"; 215 | let body = Body::from(body_value); 216 | 217 | let mut init = web_sys::RequestInit::new(); 218 | init.method("POST"); 219 | init.body(Some( 220 | body.to_js_value() 221 | .expect("could not convert body to JsValue") 222 | .as_ref(), 223 | )); 224 | 225 | let js_req = web_sys::Request::new_with_str_and_init("", &init) 226 | .expect("could not create JS request"); 227 | let text_promise = js_req.text().expect("could not get text promise"); 228 | let text = crate::wasm::promise::(text_promise) 229 | .await 230 | .expect("could not get request body as text"); 231 | 232 | assert_eq!(text.as_string().expect("text is not a string"), body_value); 233 | } 234 | #[wasm_bindgen_test] 235 | async fn test_body_js_string() { 236 | let body_value = "TEST".to_string(); 237 | let body = Body::from(body_value.clone()); 238 | 239 | let mut init = web_sys::RequestInit::new(); 240 | init.method("POST"); 241 | init.body(Some( 242 | body.to_js_value() 243 | .expect("could not convert body to JsValue") 244 | .as_ref(), 245 | )); 246 | 247 | let js_req = web_sys::Request::new_with_str_and_init("", &init) 248 | .expect("could not create JS request"); 249 | let text_promise = js_req.text().expect("could not get text promise"); 250 | let text = crate::wasm::promise::(text_promise) 251 | .await 252 | .expect("could not get request body as text"); 253 | 254 | assert_eq!(text.as_string().expect("text is not a string"), body_value); 255 | } 256 | 257 | #[wasm_bindgen_test] 258 | async fn test_body_js_static_u8_slice() { 259 | let body_value: &'static [u8] = b"\x00\x42"; 260 | let body = Body::from(body_value); 261 | 262 | let mut init = web_sys::RequestInit::new(); 263 | init.method("POST"); 264 | init.body(Some( 265 | body.to_js_value() 266 | .expect("could not convert body to JsValue") 267 | .as_ref(), 268 | )); 269 | 270 | let js_req = web_sys::Request::new_with_str_and_init("", &init) 271 | .expect("could not create JS request"); 272 | 273 | let array_buffer_promise = js_req 274 | .array_buffer() 275 | .expect("could not get array_buffer promise"); 276 | let array_buffer = crate::wasm::promise::(array_buffer_promise) 277 | .await 278 | .expect("could not get request body as array buffer"); 279 | 280 | let v = Uint8Array::new(&array_buffer).to_vec(); 281 | 282 | assert_eq!(v, body_value); 283 | } 284 | 285 | #[wasm_bindgen_test] 286 | async fn test_body_js_vec_u8() { 287 | let body_value = vec![0u8, 42]; 288 | let body = Body::from(body_value.clone()); 289 | 290 | let mut init = web_sys::RequestInit::new(); 291 | init.method("POST"); 292 | init.body(Some( 293 | body.to_js_value() 294 | .expect("could not convert body to JsValue") 295 | .as_ref(), 296 | )); 297 | 298 | let js_req = web_sys::Request::new_with_str_and_init("", &init) 299 | .expect("could not create JS request"); 300 | 301 | let array_buffer_promise = js_req 302 | .array_buffer() 303 | .expect("could not get array_buffer promise"); 304 | let array_buffer = crate::wasm::promise::(array_buffer_promise) 305 | .await 306 | .expect("could not get request body as array buffer"); 307 | 308 | let v = Uint8Array::new(&array_buffer).to_vec(); 309 | 310 | assert_eq!(v, body_value); 311 | } 312 | } 313 | -------------------------------------------------------------------------------- /src/wasm/mod.rs: -------------------------------------------------------------------------------- 1 | use std::convert::TryInto; 2 | use std::time::Duration; 3 | 4 | use js_sys::Function; 5 | use wasm_bindgen::prelude::{wasm_bindgen, Closure}; 6 | use wasm_bindgen::{JsCast, JsValue}; 7 | use web_sys::{AbortController, AbortSignal}; 8 | 9 | mod body; 10 | mod client; 11 | /// TODO 12 | #[cfg(feature = "multipart")] 13 | pub mod multipart; 14 | mod request; 15 | mod response; 16 | 17 | pub use self::body::Body; 18 | pub use self::client::{Client, ClientBuilder}; 19 | pub use self::request::{Request, RequestBuilder}; 20 | pub use self::response::Response; 21 | 22 | #[wasm_bindgen] 23 | extern "C" { 24 | #[wasm_bindgen(js_name = "setTimeout")] 25 | fn set_timeout(handler: &Function, timeout: i32) -> JsValue; 26 | 27 | #[wasm_bindgen(js_name = "clearTimeout")] 28 | fn clear_timeout(handle: JsValue) -> JsValue; 29 | } 30 | 31 | async fn promise(promise: js_sys::Promise) -> Result 32 | where 33 | T: JsCast, 34 | { 35 | use wasm_bindgen_futures::JsFuture; 36 | 37 | let js_val = JsFuture::from(promise).await.map_err(crate::error::wasm)?; 38 | 39 | js_val 40 | .dyn_into::() 41 | .map_err(|_js_val| "promise resolved to unexpected type".into()) 42 | } 43 | 44 | /// A guard that cancels a fetch request when dropped. 45 | struct AbortGuard { 46 | ctrl: AbortController, 47 | timeout: Option<(JsValue, Closure)>, 48 | } 49 | 50 | impl AbortGuard { 51 | fn new() -> crate::Result { 52 | Ok(AbortGuard { 53 | ctrl: AbortController::new() 54 | .map_err(crate::error::wasm) 55 | .map_err(crate::error::builder)?, 56 | timeout: None, 57 | }) 58 | } 59 | 60 | fn signal(&self) -> AbortSignal { 61 | self.ctrl.signal() 62 | } 63 | 64 | fn timeout(&mut self, timeout: Duration) { 65 | let ctrl = self.ctrl.clone(); 66 | let abort = 67 | Closure::once(move || ctrl.abort_with_reason(&"reqwest::errors::TimedOut".into())); 68 | let timeout = set_timeout( 69 | abort.as_ref().unchecked_ref::(), 70 | timeout.as_millis().try_into().expect("timeout"), 71 | ); 72 | if let Some((id, _)) = self.timeout.replace((timeout, abort)) { 73 | clear_timeout(id); 74 | } 75 | } 76 | } 77 | 78 | impl Drop for AbortGuard { 79 | fn drop(&mut self) { 80 | self.ctrl.abort(); 81 | if let Some((id, _)) = self.timeout.take() { 82 | clear_timeout(id); 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/wasm/response.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | 3 | use bytes::Bytes; 4 | use http::{HeaderMap, StatusCode}; 5 | use js_sys::Uint8Array; 6 | use url::Url; 7 | 8 | use crate::wasm::AbortGuard; 9 | 10 | #[cfg(feature = "stream")] 11 | use wasm_bindgen::JsCast; 12 | 13 | #[cfg(feature = "stream")] 14 | use futures_util::stream::{self, StreamExt}; 15 | 16 | #[cfg(feature = "json")] 17 | use serde::de::DeserializeOwned; 18 | 19 | /// A Response to a submitted `Request`. 20 | pub struct Response { 21 | http: http::Response, 22 | _abort: AbortGuard, 23 | // Boxed to save space (11 words to 1 word), and it's not accessed 24 | // frequently internally. 25 | url: Box, 26 | } 27 | 28 | impl Response { 29 | pub(super) fn new( 30 | res: http::Response, 31 | url: Url, 32 | abort: AbortGuard, 33 | ) -> Response { 34 | Response { 35 | http: res, 36 | url: Box::new(url), 37 | _abort: abort, 38 | } 39 | } 40 | 41 | /// Get the `StatusCode` of this `Response`. 42 | #[inline] 43 | pub fn status(&self) -> StatusCode { 44 | self.http.status() 45 | } 46 | 47 | /// Get the `Headers` of this `Response`. 48 | #[inline] 49 | pub fn headers(&self) -> &HeaderMap { 50 | self.http.headers() 51 | } 52 | 53 | /// Get a mutable reference to the `Headers` of this `Response`. 54 | #[inline] 55 | pub fn headers_mut(&mut self) -> &mut HeaderMap { 56 | self.http.headers_mut() 57 | } 58 | 59 | /// Get the content-length of this response, if known. 60 | /// 61 | /// Reasons it may not be known: 62 | /// 63 | /// - The server didn't send a `content-length` header. 64 | /// - The response is compressed and automatically decoded (thus changing 65 | /// the actual decoded length). 66 | pub fn content_length(&self) -> Option { 67 | self.headers() 68 | .get(http::header::CONTENT_LENGTH)? 69 | .to_str() 70 | .ok()? 71 | .parse() 72 | .ok() 73 | } 74 | 75 | /// Get the final `Url` of this `Response`. 76 | #[inline] 77 | pub fn url(&self) -> &Url { 78 | &self.url 79 | } 80 | 81 | /* It might not be possible to detect this in JS? 82 | /// Get the HTTP `Version` of this `Response`. 83 | #[inline] 84 | pub fn version(&self) -> Version { 85 | self.http.version() 86 | } 87 | */ 88 | 89 | /// Try to deserialize the response body as JSON. 90 | #[cfg(feature = "json")] 91 | #[cfg_attr(docsrs, doc(cfg(feature = "json")))] 92 | pub async fn json(self) -> crate::Result { 93 | let full = self.bytes().await?; 94 | 95 | serde_json::from_slice(&full).map_err(crate::error::decode) 96 | } 97 | 98 | /// Get the response text. 99 | pub async fn text(self) -> crate::Result { 100 | let p = self 101 | .http 102 | .body() 103 | .text() 104 | .map_err(crate::error::wasm) 105 | .map_err(crate::error::decode)?; 106 | let js_val = super::promise::(p) 107 | .await 108 | .map_err(crate::error::decode)?; 109 | if let Some(s) = js_val.as_string() { 110 | Ok(s) 111 | } else { 112 | Err(crate::error::decode("response.text isn't string")) 113 | } 114 | } 115 | 116 | /// Get the response as bytes 117 | pub async fn bytes(self) -> crate::Result { 118 | let p = self 119 | .http 120 | .body() 121 | .array_buffer() 122 | .map_err(crate::error::wasm) 123 | .map_err(crate::error::decode)?; 124 | 125 | let buf_js = super::promise::(p) 126 | .await 127 | .map_err(crate::error::decode)?; 128 | 129 | let buffer = Uint8Array::new(&buf_js); 130 | let mut bytes = vec![0; buffer.length() as usize]; 131 | buffer.copy_to(&mut bytes); 132 | Ok(bytes.into()) 133 | } 134 | 135 | /// Convert the response into a `Stream` of `Bytes` from the body. 136 | #[cfg(feature = "stream")] 137 | pub fn bytes_stream(self) -> impl futures_core::Stream> { 138 | use futures_core::Stream; 139 | use std::pin::Pin; 140 | 141 | let web_response = self.http.into_body(); 142 | let abort = self._abort; 143 | 144 | if let Some(body) = web_response.body() { 145 | let body = wasm_streams::ReadableStream::from_raw(body.unchecked_into()); 146 | Box::pin(body.into_stream().map(move |buf_js| { 147 | // Keep the abort guard alive as long as this stream is. 148 | let _abort = &abort; 149 | let buffer = Uint8Array::new( 150 | &buf_js 151 | .map_err(crate::error::wasm) 152 | .map_err(crate::error::decode)?, 153 | ); 154 | let mut bytes = vec![0; buffer.length() as usize]; 155 | buffer.copy_to(&mut bytes); 156 | Ok(bytes.into()) 157 | })) as Pin>>> 158 | } else { 159 | // If there's no body, return an empty stream. 160 | Box::pin(stream::empty()) as Pin>>> 161 | } 162 | } 163 | 164 | // util methods 165 | 166 | /// Turn a response into an error if the server returned an error. 167 | pub fn error_for_status(self) -> crate::Result { 168 | let status = self.status(); 169 | if status.is_client_error() || status.is_server_error() { 170 | Err(crate::error::status_code(*self.url, status)) 171 | } else { 172 | Ok(self) 173 | } 174 | } 175 | 176 | /// Turn a reference to a response into an error if the server returned an error. 177 | pub fn error_for_status_ref(&self) -> crate::Result<&Self> { 178 | let status = self.status(); 179 | if status.is_client_error() || status.is_server_error() { 180 | Err(crate::error::status_code(*self.url.clone(), status)) 181 | } else { 182 | Ok(self) 183 | } 184 | } 185 | } 186 | 187 | impl fmt::Debug for Response { 188 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 189 | f.debug_struct("Response") 190 | //.field("url", self.url()) 191 | .field("status", &self.status()) 192 | .field("headers", self.headers()) 193 | .finish() 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /tests/badssl.rs: -------------------------------------------------------------------------------- 1 | #![cfg(not(target_arch = "wasm32"))] 2 | #![cfg(not(feature = "rustls-tls-manual-roots-no-provider"))] 3 | 4 | #[cfg(all(feature = "__tls", not(feature = "rustls-tls-manual-roots")))] 5 | #[tokio::test] 6 | async fn test_badssl_modern() { 7 | let text = reqwest::Client::builder() 8 | .no_proxy() 9 | .build() 10 | .unwrap() 11 | .get("https://mozilla-modern.badssl.com/") 12 | .send() 13 | .await 14 | .unwrap() 15 | .text() 16 | .await 17 | .unwrap(); 18 | 19 | assert!(text.contains("mozilla-modern.badssl.com")); 20 | } 21 | 22 | #[cfg(any( 23 | feature = "rustls-tls-webpki-roots-no-provider", 24 | feature = "rustls-tls-native-roots-no-provider" 25 | ))] 26 | #[tokio::test] 27 | async fn test_rustls_badssl_modern() { 28 | let text = reqwest::Client::builder() 29 | .use_rustls_tls() 30 | .no_proxy() 31 | .build() 32 | .unwrap() 33 | .get("https://mozilla-modern.badssl.com/") 34 | .send() 35 | .await 36 | .unwrap() 37 | .text() 38 | .await 39 | .unwrap(); 40 | 41 | assert!(text.contains("mozilla-modern.badssl.com")); 42 | } 43 | 44 | #[cfg(feature = "__tls")] 45 | #[tokio::test] 46 | async fn test_badssl_self_signed() { 47 | let text = reqwest::Client::builder() 48 | .danger_accept_invalid_certs(true) 49 | .no_proxy() 50 | .build() 51 | .unwrap() 52 | .get("https://self-signed.badssl.com/") 53 | .send() 54 | .await 55 | .unwrap() 56 | .text() 57 | .await 58 | .unwrap(); 59 | 60 | assert!(text.contains("self-signed.badssl.com")); 61 | } 62 | 63 | #[cfg(feature = "__tls")] 64 | #[tokio::test] 65 | async fn test_badssl_no_built_in_roots() { 66 | let result = reqwest::Client::builder() 67 | .tls_built_in_root_certs(false) 68 | .no_proxy() 69 | .build() 70 | .unwrap() 71 | .get("https://mozilla-modern.badssl.com/") 72 | .send() 73 | .await; 74 | 75 | assert!(result.is_err()); 76 | } 77 | 78 | #[cfg(any(feature = "native-tls", feature = "rustls-tls"))] 79 | #[tokio::test] 80 | async fn test_badssl_wrong_host() { 81 | let text = reqwest::Client::builder() 82 | .danger_accept_invalid_hostnames(true) 83 | .no_proxy() 84 | .build() 85 | .unwrap() 86 | .get("https://wrong.host.badssl.com/") 87 | .send() 88 | .await 89 | .unwrap() 90 | .text() 91 | .await 92 | .unwrap(); 93 | 94 | assert!(text.contains("wrong.host.badssl.com")); 95 | 96 | let result = reqwest::Client::builder() 97 | .danger_accept_invalid_hostnames(true) 98 | .build() 99 | .unwrap() 100 | .get("https://self-signed.badssl.com/") 101 | .send() 102 | .await; 103 | 104 | assert!(result.is_err()); 105 | } 106 | -------------------------------------------------------------------------------- /tests/brotli.rs: -------------------------------------------------------------------------------- 1 | mod support; 2 | use std::io::Read; 3 | use support::server; 4 | use tokio::io::AsyncWriteExt; 5 | 6 | #[tokio::test] 7 | async fn brotli_response() { 8 | brotli_case(10_000, 4096).await; 9 | } 10 | 11 | #[tokio::test] 12 | async fn brotli_single_byte_chunks() { 13 | brotli_case(10, 1).await; 14 | } 15 | 16 | #[tokio::test] 17 | async fn test_brotli_empty_body() { 18 | let server = server::http(move |req| async move { 19 | assert_eq!(req.method(), "HEAD"); 20 | 21 | http::Response::builder() 22 | .header("content-encoding", "br") 23 | .body(Default::default()) 24 | .unwrap() 25 | }); 26 | 27 | let client = reqwest::Client::new(); 28 | let res = client 29 | .head(&format!("http://{}/brotli", server.addr())) 30 | .send() 31 | .await 32 | .unwrap(); 33 | 34 | let body = res.text().await.unwrap(); 35 | 36 | assert_eq!(body, ""); 37 | } 38 | 39 | #[tokio::test] 40 | async fn test_accept_header_is_not_changed_if_set() { 41 | let server = server::http(move |req| async move { 42 | assert_eq!(req.headers()["accept"], "application/json"); 43 | assert!(req.headers()["accept-encoding"] 44 | .to_str() 45 | .unwrap() 46 | .contains("br")); 47 | http::Response::default() 48 | }); 49 | 50 | let client = reqwest::Client::new(); 51 | 52 | let res = client 53 | .get(&format!("http://{}/accept", server.addr())) 54 | .header( 55 | reqwest::header::ACCEPT, 56 | reqwest::header::HeaderValue::from_static("application/json"), 57 | ) 58 | .send() 59 | .await 60 | .unwrap(); 61 | 62 | assert_eq!(res.status(), reqwest::StatusCode::OK); 63 | } 64 | 65 | #[tokio::test] 66 | async fn test_accept_encoding_header_is_not_changed_if_set() { 67 | let server = server::http(move |req| async move { 68 | assert_eq!(req.headers()["accept"], "*/*"); 69 | assert_eq!(req.headers()["accept-encoding"], "identity"); 70 | http::Response::default() 71 | }); 72 | 73 | let client = reqwest::Client::new(); 74 | 75 | let res = client 76 | .get(&format!("http://{}/accept-encoding", server.addr())) 77 | .header( 78 | reqwest::header::ACCEPT_ENCODING, 79 | reqwest::header::HeaderValue::from_static("identity"), 80 | ) 81 | .send() 82 | .await 83 | .unwrap(); 84 | 85 | assert_eq!(res.status(), reqwest::StatusCode::OK); 86 | } 87 | 88 | async fn brotli_case(response_size: usize, chunk_size: usize) { 89 | use futures_util::stream::StreamExt; 90 | 91 | let content: String = (0..response_size) 92 | .into_iter() 93 | .map(|i| format!("test {i}")) 94 | .collect(); 95 | 96 | let mut encoder = brotli_crate::CompressorReader::new(content.as_bytes(), 4096, 5, 20); 97 | let mut brotlied_content = Vec::new(); 98 | encoder.read_to_end(&mut brotlied_content).unwrap(); 99 | 100 | let mut response = format!( 101 | "\ 102 | HTTP/1.1 200 OK\r\n\ 103 | Server: test-accept\r\n\ 104 | Content-Encoding: br\r\n\ 105 | Content-Length: {}\r\n\ 106 | \r\n", 107 | &brotlied_content.len() 108 | ) 109 | .into_bytes(); 110 | response.extend(&brotlied_content); 111 | 112 | let server = server::http(move |req| { 113 | assert!(req.headers()["accept-encoding"] 114 | .to_str() 115 | .unwrap() 116 | .contains("br")); 117 | 118 | let brotlied = brotlied_content.clone(); 119 | async move { 120 | let len = brotlied.len(); 121 | let stream = 122 | futures_util::stream::unfold((brotlied, 0), move |(brotlied, pos)| async move { 123 | let chunk = brotlied.chunks(chunk_size).nth(pos)?.to_vec(); 124 | 125 | Some((chunk, (brotlied, pos + 1))) 126 | }); 127 | 128 | let body = reqwest::Body::wrap_stream(stream.map(Ok::<_, std::convert::Infallible>)); 129 | 130 | http::Response::builder() 131 | .header("content-encoding", "br") 132 | .header("content-length", len) 133 | .body(body) 134 | .unwrap() 135 | } 136 | }); 137 | 138 | let client = reqwest::Client::new(); 139 | 140 | let res = client 141 | .get(&format!("http://{}/brotli", server.addr())) 142 | .send() 143 | .await 144 | .expect("response"); 145 | 146 | let body = res.text().await.expect("text"); 147 | assert_eq!(body, content); 148 | } 149 | 150 | const COMPRESSED_RESPONSE_HEADERS: &[u8] = b"HTTP/1.1 200 OK\x0d\x0a\ 151 | Content-Type: text/plain\x0d\x0a\ 152 | Connection: keep-alive\x0d\x0a\ 153 | Content-Encoding: br\x0d\x0a"; 154 | 155 | const RESPONSE_CONTENT: &str = "some message here"; 156 | 157 | fn brotli_compress(input: &[u8]) -> Vec { 158 | let mut encoder = brotli_crate::CompressorReader::new(input, 4096, 5, 20); 159 | let mut brotlied_content = Vec::new(); 160 | encoder.read_to_end(&mut brotlied_content).unwrap(); 161 | brotlied_content 162 | } 163 | 164 | #[tokio::test] 165 | async fn test_non_chunked_non_fragmented_response() { 166 | let server = server::low_level_with_response(|_raw_request, client_socket| { 167 | Box::new(async move { 168 | let brotlied_content = brotli_compress(RESPONSE_CONTENT.as_bytes()); 169 | let content_length_header = 170 | format!("Content-Length: {}\r\n\r\n", brotlied_content.len()).into_bytes(); 171 | let response = [ 172 | COMPRESSED_RESPONSE_HEADERS, 173 | &content_length_header, 174 | &brotlied_content, 175 | ] 176 | .concat(); 177 | 178 | client_socket 179 | .write_all(response.as_slice()) 180 | .await 181 | .expect("response write_all failed"); 182 | client_socket.flush().await.expect("response flush failed"); 183 | }) 184 | }); 185 | 186 | let res = reqwest::Client::new() 187 | .get(&format!("http://{}/", server.addr())) 188 | .send() 189 | .await 190 | .expect("response"); 191 | 192 | assert_eq!(res.text().await.expect("text"), RESPONSE_CONTENT); 193 | } 194 | 195 | #[tokio::test] 196 | async fn test_chunked_fragmented_response_1() { 197 | const DELAY_BETWEEN_RESPONSE_PARTS: tokio::time::Duration = 198 | tokio::time::Duration::from_millis(1000); 199 | const DELAY_MARGIN: tokio::time::Duration = tokio::time::Duration::from_millis(50); 200 | 201 | let server = server::low_level_with_response(|_raw_request, client_socket| { 202 | Box::new(async move { 203 | let brotlied_content = brotli_compress(RESPONSE_CONTENT.as_bytes()); 204 | let response_first_part = [ 205 | COMPRESSED_RESPONSE_HEADERS, 206 | format!( 207 | "Transfer-Encoding: chunked\r\n\r\n{:x}\r\n", 208 | brotlied_content.len() 209 | ) 210 | .as_bytes(), 211 | &brotlied_content, 212 | ] 213 | .concat(); 214 | let response_second_part = b"\r\n0\r\n\r\n"; 215 | 216 | client_socket 217 | .write_all(response_first_part.as_slice()) 218 | .await 219 | .expect("response_first_part write_all failed"); 220 | client_socket 221 | .flush() 222 | .await 223 | .expect("response_first_part flush failed"); 224 | 225 | tokio::time::sleep(DELAY_BETWEEN_RESPONSE_PARTS).await; 226 | 227 | client_socket 228 | .write_all(response_second_part) 229 | .await 230 | .expect("response_second_part write_all failed"); 231 | client_socket 232 | .flush() 233 | .await 234 | .expect("response_second_part flush failed"); 235 | }) 236 | }); 237 | 238 | let start = tokio::time::Instant::now(); 239 | let res = reqwest::Client::new() 240 | .get(&format!("http://{}/", server.addr())) 241 | .send() 242 | .await 243 | .expect("response"); 244 | 245 | assert_eq!(res.text().await.expect("text"), RESPONSE_CONTENT); 246 | assert!(start.elapsed() >= DELAY_BETWEEN_RESPONSE_PARTS - DELAY_MARGIN); 247 | } 248 | 249 | #[tokio::test] 250 | async fn test_chunked_fragmented_response_2() { 251 | const DELAY_BETWEEN_RESPONSE_PARTS: tokio::time::Duration = 252 | tokio::time::Duration::from_millis(1000); 253 | const DELAY_MARGIN: tokio::time::Duration = tokio::time::Duration::from_millis(50); 254 | 255 | let server = server::low_level_with_response(|_raw_request, client_socket| { 256 | Box::new(async move { 257 | let brotlied_content = brotli_compress(RESPONSE_CONTENT.as_bytes()); 258 | let response_first_part = [ 259 | COMPRESSED_RESPONSE_HEADERS, 260 | format!( 261 | "Transfer-Encoding: chunked\r\n\r\n{:x}\r\n", 262 | brotlied_content.len() 263 | ) 264 | .as_bytes(), 265 | &brotlied_content, 266 | b"\r\n", 267 | ] 268 | .concat(); 269 | let response_second_part = b"0\r\n\r\n"; 270 | 271 | client_socket 272 | .write_all(response_first_part.as_slice()) 273 | .await 274 | .expect("response_first_part write_all failed"); 275 | client_socket 276 | .flush() 277 | .await 278 | .expect("response_first_part flush failed"); 279 | 280 | tokio::time::sleep(DELAY_BETWEEN_RESPONSE_PARTS).await; 281 | 282 | client_socket 283 | .write_all(response_second_part) 284 | .await 285 | .expect("response_second_part write_all failed"); 286 | client_socket 287 | .flush() 288 | .await 289 | .expect("response_second_part flush failed"); 290 | }) 291 | }); 292 | 293 | let start = tokio::time::Instant::now(); 294 | let res = reqwest::Client::new() 295 | .get(&format!("http://{}/", server.addr())) 296 | .send() 297 | .await 298 | .expect("response"); 299 | 300 | assert_eq!(res.text().await.expect("text"), RESPONSE_CONTENT); 301 | assert!(start.elapsed() >= DELAY_BETWEEN_RESPONSE_PARTS - DELAY_MARGIN); 302 | } 303 | 304 | #[tokio::test] 305 | async fn test_chunked_fragmented_response_with_extra_bytes() { 306 | const DELAY_BETWEEN_RESPONSE_PARTS: tokio::time::Duration = 307 | tokio::time::Duration::from_millis(1000); 308 | const DELAY_MARGIN: tokio::time::Duration = tokio::time::Duration::from_millis(50); 309 | 310 | let server = server::low_level_with_response(|_raw_request, client_socket| { 311 | Box::new(async move { 312 | let brotlied_content = brotli_compress(RESPONSE_CONTENT.as_bytes()); 313 | let response_first_part = [ 314 | COMPRESSED_RESPONSE_HEADERS, 315 | format!( 316 | "Transfer-Encoding: chunked\r\n\r\n{:x}\r\n", 317 | brotlied_content.len() 318 | ) 319 | .as_bytes(), 320 | &brotlied_content, 321 | ] 322 | .concat(); 323 | let response_second_part = b"\r\n2ab\r\n0\r\n\r\n"; 324 | 325 | client_socket 326 | .write_all(response_first_part.as_slice()) 327 | .await 328 | .expect("response_first_part write_all failed"); 329 | client_socket 330 | .flush() 331 | .await 332 | .expect("response_first_part flush failed"); 333 | 334 | tokio::time::sleep(DELAY_BETWEEN_RESPONSE_PARTS).await; 335 | 336 | client_socket 337 | .write_all(response_second_part) 338 | .await 339 | .expect("response_second_part write_all failed"); 340 | client_socket 341 | .flush() 342 | .await 343 | .expect("response_second_part flush failed"); 344 | }) 345 | }); 346 | 347 | let start = tokio::time::Instant::now(); 348 | let res = reqwest::Client::new() 349 | .get(&format!("http://{}/", server.addr())) 350 | .send() 351 | .await 352 | .expect("response"); 353 | 354 | let err = res.text().await.expect_err("there must be an error"); 355 | assert!(err.is_decode()); 356 | assert!(start.elapsed() >= DELAY_BETWEEN_RESPONSE_PARTS - DELAY_MARGIN); 357 | } 358 | -------------------------------------------------------------------------------- /tests/ci.rs: -------------------------------------------------------------------------------- 1 | #![cfg(not(target_arch = "wasm32"))] 2 | #![cfg(not(feature = "rustls-tls-manual-roots-no-provider"))] 3 | mod support; 4 | use support::server; 5 | 6 | #[tokio::test] 7 | #[should_panic(expected = "test server should not panic")] 8 | async fn server_panics_should_propagate() { 9 | let server = server::http(|_| async { 10 | panic!("kaboom"); 11 | }); 12 | 13 | let _ = reqwest::get(format!("http://{}/ci", server.addr())).await; 14 | } 15 | -------------------------------------------------------------------------------- /tests/cookie.rs: -------------------------------------------------------------------------------- 1 | mod support; 2 | use support::server; 3 | 4 | #[tokio::test] 5 | async fn cookie_response_accessor() { 6 | let server = server::http(move |_req| async move { 7 | http::Response::builder() 8 | .header("Set-Cookie", "key=val") 9 | .header( 10 | "Set-Cookie", 11 | "expires=1; Expires=Wed, 21 Oct 2015 07:28:00 GMT", 12 | ) 13 | .header("Set-Cookie", "path=1; Path=/the-path") 14 | .header("Set-Cookie", "maxage=1; Max-Age=100") 15 | .header("Set-Cookie", "domain=1; Domain=mydomain") 16 | .header("Set-Cookie", "secure=1; Secure") 17 | .header("Set-Cookie", "httponly=1; HttpOnly") 18 | .header("Set-Cookie", "samesitelax=1; SameSite=Lax") 19 | .header("Set-Cookie", "samesitestrict=1; SameSite=Strict") 20 | .body(Default::default()) 21 | .unwrap() 22 | }); 23 | 24 | let client = reqwest::Client::new(); 25 | 26 | let url = format!("http://{}/", server.addr()); 27 | let res = client.get(&url).send().await.unwrap(); 28 | 29 | let cookies = res.cookies().collect::>(); 30 | 31 | // key=val 32 | assert_eq!(cookies[0].name(), "key"); 33 | assert_eq!(cookies[0].value(), "val"); 34 | 35 | // expires 36 | assert_eq!(cookies[1].name(), "expires"); 37 | assert_eq!( 38 | cookies[1].expires().unwrap(), 39 | std::time::SystemTime::UNIX_EPOCH + std::time::Duration::from_secs(1_445_412_480) 40 | ); 41 | 42 | // path 43 | assert_eq!(cookies[2].name(), "path"); 44 | assert_eq!(cookies[2].path().unwrap(), "/the-path"); 45 | 46 | // max-age 47 | assert_eq!(cookies[3].name(), "maxage"); 48 | assert_eq!( 49 | cookies[3].max_age().unwrap(), 50 | std::time::Duration::from_secs(100) 51 | ); 52 | 53 | // domain 54 | assert_eq!(cookies[4].name(), "domain"); 55 | assert_eq!(cookies[4].domain().unwrap(), "mydomain"); 56 | 57 | // secure 58 | assert_eq!(cookies[5].name(), "secure"); 59 | assert_eq!(cookies[5].secure(), true); 60 | 61 | // httponly 62 | assert_eq!(cookies[6].name(), "httponly"); 63 | assert_eq!(cookies[6].http_only(), true); 64 | 65 | // samesitelax 66 | assert_eq!(cookies[7].name(), "samesitelax"); 67 | assert!(cookies[7].same_site_lax()); 68 | 69 | // samesitestrict 70 | assert_eq!(cookies[8].name(), "samesitestrict"); 71 | assert!(cookies[8].same_site_strict()); 72 | } 73 | 74 | #[tokio::test] 75 | async fn cookie_store_simple() { 76 | let server = server::http(move |req| async move { 77 | if req.uri() == "/2" { 78 | assert_eq!(req.headers()["cookie"], "key=val"); 79 | } 80 | http::Response::builder() 81 | .header("Set-Cookie", "key=val; HttpOnly") 82 | .body(Default::default()) 83 | .unwrap() 84 | }); 85 | 86 | let client = reqwest::Client::builder() 87 | .cookie_store(true) 88 | .build() 89 | .unwrap(); 90 | 91 | let url = format!("http://{}/", server.addr()); 92 | client.get(&url).send().await.unwrap(); 93 | 94 | let url = format!("http://{}/2", server.addr()); 95 | client.get(&url).send().await.unwrap(); 96 | } 97 | 98 | #[tokio::test] 99 | async fn cookie_store_overwrite_existing() { 100 | let server = server::http(move |req| async move { 101 | if req.uri() == "/" { 102 | http::Response::builder() 103 | .header("Set-Cookie", "key=val") 104 | .body(Default::default()) 105 | .unwrap() 106 | } else if req.uri() == "/2" { 107 | assert_eq!(req.headers()["cookie"], "key=val"); 108 | http::Response::builder() 109 | .header("Set-Cookie", "key=val2") 110 | .body(Default::default()) 111 | .unwrap() 112 | } else { 113 | assert_eq!(req.uri(), "/3"); 114 | assert_eq!(req.headers()["cookie"], "key=val2"); 115 | http::Response::default() 116 | } 117 | }); 118 | 119 | let client = reqwest::Client::builder() 120 | .cookie_store(true) 121 | .build() 122 | .unwrap(); 123 | 124 | let url = format!("http://{}/", server.addr()); 125 | client.get(&url).send().await.unwrap(); 126 | 127 | let url = format!("http://{}/2", server.addr()); 128 | client.get(&url).send().await.unwrap(); 129 | 130 | let url = format!("http://{}/3", server.addr()); 131 | client.get(&url).send().await.unwrap(); 132 | } 133 | 134 | #[tokio::test] 135 | async fn cookie_store_max_age() { 136 | let server = server::http(move |req| async move { 137 | assert_eq!(req.headers().get("cookie"), None); 138 | http::Response::builder() 139 | .header("Set-Cookie", "key=val; Max-Age=0") 140 | .body(Default::default()) 141 | .unwrap() 142 | }); 143 | 144 | let client = reqwest::Client::builder() 145 | .cookie_store(true) 146 | .build() 147 | .unwrap(); 148 | let url = format!("http://{}/", server.addr()); 149 | client.get(&url).send().await.unwrap(); 150 | client.get(&url).send().await.unwrap(); 151 | } 152 | 153 | #[tokio::test] 154 | async fn cookie_store_expires() { 155 | let server = server::http(move |req| async move { 156 | assert_eq!(req.headers().get("cookie"), None); 157 | http::Response::builder() 158 | .header( 159 | "Set-Cookie", 160 | "key=val; Expires=Wed, 21 Oct 2015 07:28:00 GMT", 161 | ) 162 | .body(Default::default()) 163 | .unwrap() 164 | }); 165 | 166 | let client = reqwest::Client::builder() 167 | .cookie_store(true) 168 | .build() 169 | .unwrap(); 170 | 171 | let url = format!("http://{}/", server.addr()); 172 | client.get(&url).send().await.unwrap(); 173 | client.get(&url).send().await.unwrap(); 174 | } 175 | 176 | #[tokio::test] 177 | async fn cookie_store_path() { 178 | let server = server::http(move |req| async move { 179 | if req.uri() == "/" { 180 | assert_eq!(req.headers().get("cookie"), None); 181 | http::Response::builder() 182 | .header("Set-Cookie", "key=val; Path=/subpath") 183 | .body(Default::default()) 184 | .unwrap() 185 | } else { 186 | assert_eq!(req.uri(), "/subpath"); 187 | assert_eq!(req.headers()["cookie"], "key=val"); 188 | http::Response::default() 189 | } 190 | }); 191 | 192 | let client = reqwest::Client::builder() 193 | .cookie_store(true) 194 | .build() 195 | .unwrap(); 196 | 197 | let url = format!("http://{}/", server.addr()); 198 | client.get(&url).send().await.unwrap(); 199 | client.get(&url).send().await.unwrap(); 200 | 201 | let url = format!("http://{}/subpath", server.addr()); 202 | client.get(&url).send().await.unwrap(); 203 | } 204 | -------------------------------------------------------------------------------- /tests/gzip.rs: -------------------------------------------------------------------------------- 1 | mod support; 2 | use flate2::write::GzEncoder; 3 | use flate2::Compression; 4 | use support::server; 5 | 6 | use std::io::Write; 7 | use tokio::io::AsyncWriteExt; 8 | use tokio::time::Duration; 9 | 10 | #[tokio::test] 11 | async fn gzip_response() { 12 | gzip_case(10_000, 4096).await; 13 | } 14 | 15 | #[tokio::test] 16 | async fn gzip_single_byte_chunks() { 17 | gzip_case(10, 1).await; 18 | } 19 | 20 | #[tokio::test] 21 | async fn test_gzip_empty_body() { 22 | let server = server::http(move |req| async move { 23 | assert_eq!(req.method(), "HEAD"); 24 | 25 | http::Response::builder() 26 | .header("content-encoding", "gzip") 27 | .body(Default::default()) 28 | .unwrap() 29 | }); 30 | 31 | let client = reqwest::Client::new(); 32 | let res = client 33 | .head(&format!("http://{}/gzip", server.addr())) 34 | .send() 35 | .await 36 | .unwrap(); 37 | 38 | let body = res.text().await.unwrap(); 39 | 40 | assert_eq!(body, ""); 41 | } 42 | 43 | #[tokio::test] 44 | async fn test_accept_header_is_not_changed_if_set() { 45 | let server = server::http(move |req| async move { 46 | assert_eq!(req.headers()["accept"], "application/json"); 47 | assert!(req.headers()["accept-encoding"] 48 | .to_str() 49 | .unwrap() 50 | .contains("gzip")); 51 | http::Response::default() 52 | }); 53 | 54 | let client = reqwest::Client::new(); 55 | 56 | let res = client 57 | .get(&format!("http://{}/accept", server.addr())) 58 | .header( 59 | reqwest::header::ACCEPT, 60 | reqwest::header::HeaderValue::from_static("application/json"), 61 | ) 62 | .send() 63 | .await 64 | .unwrap(); 65 | 66 | assert_eq!(res.status(), reqwest::StatusCode::OK); 67 | } 68 | 69 | #[tokio::test] 70 | async fn test_accept_encoding_header_is_not_changed_if_set() { 71 | let server = server::http(move |req| async move { 72 | assert_eq!(req.headers()["accept"], "*/*"); 73 | assert_eq!(req.headers()["accept-encoding"], "identity"); 74 | http::Response::default() 75 | }); 76 | 77 | let client = reqwest::Client::new(); 78 | 79 | let res = client 80 | .get(&format!("http://{}/accept-encoding", server.addr())) 81 | .header( 82 | reqwest::header::ACCEPT_ENCODING, 83 | reqwest::header::HeaderValue::from_static("identity"), 84 | ) 85 | .send() 86 | .await 87 | .unwrap(); 88 | 89 | assert_eq!(res.status(), reqwest::StatusCode::OK); 90 | } 91 | 92 | async fn gzip_case(response_size: usize, chunk_size: usize) { 93 | use futures_util::stream::StreamExt; 94 | 95 | let content: String = (0..response_size) 96 | .into_iter() 97 | .map(|i| format!("test {i}")) 98 | .collect(); 99 | 100 | let mut encoder = GzEncoder::new(Vec::new(), Compression::default()); 101 | encoder.write_all(content.as_bytes()).unwrap(); 102 | let gzipped_content = encoder.finish().unwrap(); 103 | 104 | let mut response = format!( 105 | "\ 106 | HTTP/1.1 200 OK\r\n\ 107 | Server: test-accept\r\n\ 108 | Content-Encoding: gzip\r\n\ 109 | Content-Length: {}\r\n\ 110 | \r\n", 111 | &gzipped_content.len() 112 | ) 113 | .into_bytes(); 114 | response.extend(&gzipped_content); 115 | 116 | let server = server::http(move |req| { 117 | assert!(req.headers()["accept-encoding"] 118 | .to_str() 119 | .unwrap() 120 | .contains("gzip")); 121 | 122 | let gzipped = gzipped_content.clone(); 123 | async move { 124 | let len = gzipped.len(); 125 | let stream = 126 | futures_util::stream::unfold((gzipped, 0), move |(gzipped, pos)| async move { 127 | let chunk = gzipped.chunks(chunk_size).nth(pos)?.to_vec(); 128 | 129 | Some((chunk, (gzipped, pos + 1))) 130 | }); 131 | 132 | let body = reqwest::Body::wrap_stream(stream.map(Ok::<_, std::convert::Infallible>)); 133 | 134 | http::Response::builder() 135 | .header("content-encoding", "gzip") 136 | .header("content-length", len) 137 | .body(body) 138 | .unwrap() 139 | } 140 | }); 141 | 142 | let client = reqwest::Client::new(); 143 | 144 | let res = client 145 | .get(&format!("http://{}/gzip", server.addr())) 146 | .send() 147 | .await 148 | .expect("response"); 149 | 150 | let body = res.text().await.expect("text"); 151 | assert_eq!(body, content); 152 | } 153 | 154 | const COMPRESSED_RESPONSE_HEADERS: &[u8] = b"HTTP/1.1 200 OK\x0d\x0a\ 155 | Content-Type: text/plain\x0d\x0a\ 156 | Connection: keep-alive\x0d\x0a\ 157 | Content-Encoding: gzip\x0d\x0a"; 158 | 159 | const RESPONSE_CONTENT: &str = "some message here"; 160 | 161 | fn gzip_compress(input: &[u8]) -> Vec { 162 | let mut encoder = GzEncoder::new(Vec::new(), Compression::default()); 163 | encoder.write_all(input).unwrap(); 164 | encoder.finish().unwrap() 165 | } 166 | 167 | #[tokio::test] 168 | async fn test_non_chunked_non_fragmented_response() { 169 | let server = server::low_level_with_response(|_raw_request, client_socket| { 170 | Box::new(async move { 171 | let gzipped_content = gzip_compress(RESPONSE_CONTENT.as_bytes()); 172 | let content_length_header = 173 | format!("Content-Length: {}\r\n\r\n", gzipped_content.len()).into_bytes(); 174 | let response = [ 175 | COMPRESSED_RESPONSE_HEADERS, 176 | &content_length_header, 177 | &gzipped_content, 178 | ] 179 | .concat(); 180 | 181 | client_socket 182 | .write_all(response.as_slice()) 183 | .await 184 | .expect("response write_all failed"); 185 | client_socket.flush().await.expect("response flush failed"); 186 | }) 187 | }); 188 | 189 | let res = reqwest::Client::new() 190 | .get(&format!("http://{}/", server.addr())) 191 | .send() 192 | .await 193 | .expect("response"); 194 | 195 | assert_eq!(res.text().await.expect("text"), RESPONSE_CONTENT); 196 | } 197 | 198 | #[tokio::test] 199 | async fn test_chunked_fragmented_response_1() { 200 | const DELAY_BETWEEN_RESPONSE_PARTS: tokio::time::Duration = 201 | tokio::time::Duration::from_millis(1000); 202 | const DELAY_MARGIN: tokio::time::Duration = tokio::time::Duration::from_millis(50); 203 | 204 | let server = server::low_level_with_response(|_raw_request, client_socket| { 205 | Box::new(async move { 206 | let gzipped_content = gzip_compress(RESPONSE_CONTENT.as_bytes()); 207 | let response_first_part = [ 208 | COMPRESSED_RESPONSE_HEADERS, 209 | format!( 210 | "Transfer-Encoding: chunked\r\n\r\n{:x}\r\n", 211 | gzipped_content.len() 212 | ) 213 | .as_bytes(), 214 | &gzipped_content, 215 | ] 216 | .concat(); 217 | let response_second_part = b"\r\n0\r\n\r\n"; 218 | 219 | client_socket 220 | .write_all(response_first_part.as_slice()) 221 | .await 222 | .expect("response_first_part write_all failed"); 223 | client_socket 224 | .flush() 225 | .await 226 | .expect("response_first_part flush failed"); 227 | 228 | tokio::time::sleep(DELAY_BETWEEN_RESPONSE_PARTS).await; 229 | 230 | client_socket 231 | .write_all(response_second_part) 232 | .await 233 | .expect("response_second_part write_all failed"); 234 | client_socket 235 | .flush() 236 | .await 237 | .expect("response_second_part flush failed"); 238 | }) 239 | }); 240 | 241 | let start = tokio::time::Instant::now(); 242 | let res = reqwest::Client::new() 243 | .get(&format!("http://{}/", server.addr())) 244 | .send() 245 | .await 246 | .expect("response"); 247 | 248 | assert_eq!(res.text().await.expect("text"), RESPONSE_CONTENT); 249 | assert!(start.elapsed() >= DELAY_BETWEEN_RESPONSE_PARTS - DELAY_MARGIN); 250 | } 251 | 252 | #[tokio::test] 253 | async fn test_chunked_fragmented_response_2() { 254 | const DELAY_BETWEEN_RESPONSE_PARTS: tokio::time::Duration = 255 | tokio::time::Duration::from_millis(1000); 256 | const DELAY_MARGIN: tokio::time::Duration = tokio::time::Duration::from_millis(50); 257 | 258 | let server = server::low_level_with_response(|_raw_request, client_socket| { 259 | Box::new(async move { 260 | let gzipped_content = gzip_compress(RESPONSE_CONTENT.as_bytes()); 261 | let response_first_part = [ 262 | COMPRESSED_RESPONSE_HEADERS, 263 | format!( 264 | "Transfer-Encoding: chunked\r\n\r\n{:x}\r\n", 265 | gzipped_content.len() 266 | ) 267 | .as_bytes(), 268 | &gzipped_content, 269 | b"\r\n", 270 | ] 271 | .concat(); 272 | let response_second_part = b"0\r\n\r\n"; 273 | 274 | client_socket 275 | .write_all(response_first_part.as_slice()) 276 | .await 277 | .expect("response_first_part write_all failed"); 278 | client_socket 279 | .flush() 280 | .await 281 | .expect("response_first_part flush failed"); 282 | 283 | tokio::time::sleep(DELAY_BETWEEN_RESPONSE_PARTS).await; 284 | 285 | client_socket 286 | .write_all(response_second_part) 287 | .await 288 | .expect("response_second_part write_all failed"); 289 | client_socket 290 | .flush() 291 | .await 292 | .expect("response_second_part flush failed"); 293 | }) 294 | }); 295 | 296 | let start = tokio::time::Instant::now(); 297 | let res = reqwest::Client::new() 298 | .get(&format!("http://{}/", server.addr())) 299 | .send() 300 | .await 301 | .expect("response"); 302 | 303 | assert_eq!(res.text().await.expect("text"), RESPONSE_CONTENT); 304 | assert!(start.elapsed() >= DELAY_BETWEEN_RESPONSE_PARTS - DELAY_MARGIN); 305 | } 306 | 307 | #[tokio::test] 308 | async fn test_chunked_fragmented_response_with_extra_bytes() { 309 | const DELAY_BETWEEN_RESPONSE_PARTS: tokio::time::Duration = 310 | tokio::time::Duration::from_millis(1000); 311 | const DELAY_MARGIN: tokio::time::Duration = tokio::time::Duration::from_millis(50); 312 | 313 | let server = server::low_level_with_response(|_raw_request, client_socket| { 314 | Box::new(async move { 315 | let gzipped_content = gzip_compress(RESPONSE_CONTENT.as_bytes()); 316 | let response_first_part = [ 317 | COMPRESSED_RESPONSE_HEADERS, 318 | format!( 319 | "Transfer-Encoding: chunked\r\n\r\n{:x}\r\n", 320 | gzipped_content.len() 321 | ) 322 | .as_bytes(), 323 | &gzipped_content, 324 | ] 325 | .concat(); 326 | let response_second_part = b"\r\n2ab\r\n0\r\n\r\n"; 327 | 328 | client_socket 329 | .write_all(response_first_part.as_slice()) 330 | .await 331 | .expect("response_first_part write_all failed"); 332 | client_socket 333 | .flush() 334 | .await 335 | .expect("response_first_part flush failed"); 336 | 337 | tokio::time::sleep(DELAY_BETWEEN_RESPONSE_PARTS).await; 338 | 339 | client_socket 340 | .write_all(response_second_part) 341 | .await 342 | .expect("response_second_part write_all failed"); 343 | client_socket 344 | .flush() 345 | .await 346 | .expect("response_second_part flush failed"); 347 | }) 348 | }); 349 | 350 | let start = tokio::time::Instant::now(); 351 | let res = reqwest::Client::new() 352 | .get(&format!("http://{}/", server.addr())) 353 | .send() 354 | .await 355 | .expect("response"); 356 | 357 | let err = res.text().await.expect_err("there must be an error"); 358 | assert!(err.is_decode()); 359 | assert!(start.elapsed() >= DELAY_BETWEEN_RESPONSE_PARTS - DELAY_MARGIN); 360 | } 361 | -------------------------------------------------------------------------------- /tests/http3.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "http3")] 2 | #![cfg(not(target_arch = "wasm32"))] 3 | 4 | mod support; 5 | 6 | use http::header::CONTENT_LENGTH; 7 | use std::error::Error; 8 | use support::server; 9 | 10 | fn assert_send_sync(_: &T) {} 11 | 12 | #[tokio::test] 13 | async fn http3_request_full() { 14 | use http_body_util::BodyExt; 15 | 16 | let server = server::Http3::new().build(move |req| async move { 17 | assert_eq!(req.headers()[CONTENT_LENGTH], "5"); 18 | let reqb = req.collect().await.unwrap().to_bytes(); 19 | assert_eq!(reqb, "hello"); 20 | http::Response::default() 21 | }); 22 | 23 | let url = format!("https://{}/content-length", server.addr()); 24 | let res_fut = reqwest::Client::builder() 25 | .http3_prior_knowledge() 26 | .danger_accept_invalid_certs(true) 27 | .build() 28 | .expect("client builder") 29 | .post(url) 30 | .version(http::Version::HTTP_3) 31 | .body("hello") 32 | .send(); 33 | 34 | assert_send_sync(&res_fut); 35 | let res = res_fut.await.expect("request"); 36 | 37 | assert_eq!(res.version(), http::Version::HTTP_3); 38 | assert_eq!(res.status(), reqwest::StatusCode::OK); 39 | } 40 | 41 | async fn find_free_tcp_addr() -> std::net::SocketAddr { 42 | let listener = tokio::net::TcpListener::bind("[::1]:0").await.unwrap(); 43 | listener.local_addr().unwrap() 44 | } 45 | 46 | #[cfg(feature = "http3")] 47 | #[tokio::test] 48 | async fn http3_test_failed_connection() { 49 | let addr = find_free_tcp_addr().await; 50 | let port = addr.port(); 51 | 52 | let url = format!("https://[::1]:{port}/"); 53 | let client = reqwest::Client::builder() 54 | .http3_prior_knowledge() 55 | .danger_accept_invalid_certs(true) 56 | .http3_max_idle_timeout(std::time::Duration::from_millis(20)) 57 | .build() 58 | .expect("client builder"); 59 | 60 | let err = client 61 | .get(&url) 62 | .version(http::Version::HTTP_3) 63 | .send() 64 | .await 65 | .unwrap_err(); 66 | 67 | let err = err 68 | .source() 69 | .unwrap() 70 | .source() 71 | .unwrap() 72 | .downcast_ref::() 73 | .unwrap(); 74 | assert_eq!(*err, quinn::ConnectionError::TimedOut); 75 | 76 | let err = client 77 | .get(&url) 78 | .version(http::Version::HTTP_3) 79 | .send() 80 | .await 81 | .unwrap_err(); 82 | 83 | let err = err 84 | .source() 85 | .unwrap() 86 | .source() 87 | .unwrap() 88 | .downcast_ref::() 89 | .unwrap(); 90 | assert_eq!(*err, quinn::ConnectionError::TimedOut); 91 | 92 | let server = server::Http3::new() 93 | .with_addr(addr) 94 | .build(|_| async { http::Response::default() }); 95 | 96 | let res = client 97 | .post(&url) 98 | .version(http::Version::HTTP_3) 99 | .body("hello") 100 | .send() 101 | .await 102 | .expect("request"); 103 | 104 | assert_eq!(res.version(), http::Version::HTTP_3); 105 | assert_eq!(res.status(), reqwest::StatusCode::OK); 106 | drop(server); 107 | } 108 | 109 | #[cfg(feature = "http3")] 110 | #[tokio::test] 111 | async fn http3_test_concurrent_request() { 112 | let server = server::Http3::new().build(|req| async move { 113 | let mut res = http::Response::default(); 114 | *res.body_mut() = reqwest::Body::from(format!("hello {}", req.uri().path())); 115 | res 116 | }); 117 | let addr = server.addr(); 118 | 119 | let client = reqwest::Client::builder() 120 | .http3_prior_knowledge() 121 | .danger_accept_invalid_certs(true) 122 | .http3_max_idle_timeout(std::time::Duration::from_millis(20)) 123 | .build() 124 | .expect("client builder"); 125 | 126 | let mut tasks = vec![]; 127 | for i in 0..10 { 128 | let client = client.clone(); 129 | tasks.push(async move { 130 | let url = format!("https://{}/{}", addr, i); 131 | 132 | client 133 | .post(&url) 134 | .version(http::Version::HTTP_3) 135 | .send() 136 | .await 137 | .expect("request") 138 | }); 139 | } 140 | 141 | let handlers = tasks.into_iter().map(tokio::spawn).collect::>(); 142 | 143 | for (i, handler) in handlers.into_iter().enumerate() { 144 | let result = handler.await.unwrap(); 145 | 146 | assert_eq!(result.version(), http::Version::HTTP_3); 147 | assert_eq!(result.status(), reqwest::StatusCode::OK); 148 | 149 | let body = result.text().await.unwrap(); 150 | assert_eq!(body, format!("hello /{}", i)); 151 | } 152 | 153 | drop(server); 154 | } 155 | 156 | #[cfg(feature = "http3")] 157 | #[tokio::test] 158 | async fn http3_test_reconnection() { 159 | use std::error::Error; 160 | 161 | use h3::error::StreamError; 162 | 163 | let server = server::Http3::new().build(|_| async { http::Response::default() }); 164 | let addr = server.addr(); 165 | 166 | let url = format!("https://{}/", addr); 167 | let client = reqwest::Client::builder() 168 | .http3_prior_knowledge() 169 | .danger_accept_invalid_certs(true) 170 | .http3_max_idle_timeout(std::time::Duration::from_millis(20)) 171 | .build() 172 | .expect("client builder"); 173 | 174 | let res = client 175 | .post(&url) 176 | .version(http::Version::HTTP_3) 177 | .send() 178 | .await 179 | .expect("request"); 180 | 181 | assert_eq!(res.version(), http::Version::HTTP_3); 182 | assert_eq!(res.status(), reqwest::StatusCode::OK); 183 | drop(server); 184 | 185 | let err = client 186 | .get(&url) 187 | .version(http::Version::HTTP_3) 188 | .send() 189 | .await 190 | .unwrap_err(); 191 | 192 | let err = err 193 | .source() 194 | .unwrap() 195 | .source() 196 | .unwrap() 197 | .downcast_ref::() 198 | .unwrap(); 199 | 200 | // Why is it so hard to inspect h3 errors? :/ 201 | assert!(err.to_string().contains("Timeout")); 202 | 203 | let server = server::Http3::new() 204 | .with_addr(addr) 205 | .build(|_| async { http::Response::default() }); 206 | 207 | let res = client 208 | .post(&url) 209 | .version(http::Version::HTTP_3) 210 | .body("hello") 211 | .send() 212 | .await 213 | .expect("request"); 214 | 215 | assert_eq!(res.version(), http::Version::HTTP_3); 216 | assert_eq!(res.status(), reqwest::StatusCode::OK); 217 | drop(server); 218 | } 219 | 220 | #[cfg(all(feature = "http3", feature = "stream"))] 221 | #[tokio::test] 222 | async fn http3_request_stream() { 223 | use http_body_util::BodyExt; 224 | 225 | let server = server::Http3::new().build(move |req| async move { 226 | let reqb = req.collect().await.unwrap().to_bytes(); 227 | assert_eq!(reqb, "hello world"); 228 | http::Response::default() 229 | }); 230 | 231 | let url = format!("https://{}", server.addr()); 232 | let body = reqwest::Body::wrap_stream(futures_util::stream::iter(vec![ 233 | Ok::<_, std::convert::Infallible>("hello"), 234 | Ok::<_, std::convert::Infallible>(" "), 235 | Ok::<_, std::convert::Infallible>("world"), 236 | ])); 237 | 238 | let res = reqwest::Client::builder() 239 | .http3_prior_knowledge() 240 | .danger_accept_invalid_certs(true) 241 | .build() 242 | .expect("client builder") 243 | .post(url) 244 | .version(http::Version::HTTP_3) 245 | .body(body) 246 | .send() 247 | .await 248 | .expect("request"); 249 | 250 | assert_eq!(res.version(), http::Version::HTTP_3); 251 | assert_eq!(res.status(), reqwest::StatusCode::OK); 252 | } 253 | 254 | #[cfg(all(feature = "http3", feature = "stream"))] 255 | #[tokio::test] 256 | async fn http3_request_stream_error() { 257 | use http_body_util::BodyExt; 258 | 259 | let server = server::Http3::new().build(move |req| async move { 260 | // HTTP/3 response can start and finish before the entire request body has been received. 261 | // To avoid prematurely terminating the session, collect full request body before responding. 262 | let _ = req.collect().await; 263 | 264 | http::Response::default() 265 | }); 266 | 267 | let url = format!("https://{}", server.addr()); 268 | let body = reqwest::Body::wrap_stream(futures_util::stream::iter(vec![ 269 | Ok::<_, std::io::Error>("first chunk"), 270 | Err::<_, std::io::Error>(std::io::Error::other("oh no!")), 271 | ])); 272 | 273 | let res = reqwest::Client::builder() 274 | .http3_prior_knowledge() 275 | .danger_accept_invalid_certs(true) 276 | .build() 277 | .expect("client builder") 278 | .post(url) 279 | .version(http::Version::HTTP_3) 280 | .body(body) 281 | .send() 282 | .await; 283 | 284 | let err = res.unwrap_err(); 285 | assert!(err.is_request()); 286 | let err = err 287 | .source() 288 | .unwrap() 289 | .source() 290 | .unwrap() 291 | .downcast_ref::() 292 | .unwrap(); 293 | assert!(err.is_body()); 294 | } 295 | -------------------------------------------------------------------------------- /tests/multipart.rs: -------------------------------------------------------------------------------- 1 | #![cfg(not(target_arch = "wasm32"))] 2 | mod support; 3 | use http_body_util::BodyExt; 4 | use support::server; 5 | 6 | #[tokio::test] 7 | async fn text_part() { 8 | let _ = env_logger::try_init(); 9 | 10 | let form = reqwest::multipart::Form::new().text("foo", "bar"); 11 | 12 | let expected_body = format!( 13 | "\ 14 | --{0}\r\n\ 15 | Content-Disposition: form-data; name=\"foo\"\r\n\r\n\ 16 | bar\r\n\ 17 | --{0}--\r\n\ 18 | ", 19 | form.boundary() 20 | ); 21 | 22 | let ct = format!("multipart/form-data; boundary={}", form.boundary()); 23 | 24 | let server = server::http(move |mut req| { 25 | let ct = ct.clone(); 26 | let expected_body = expected_body.clone(); 27 | async move { 28 | assert_eq!(req.method(), "POST"); 29 | assert_eq!(req.headers()["content-type"], ct); 30 | assert_eq!( 31 | req.headers()["content-length"], 32 | expected_body.len().to_string() 33 | ); 34 | 35 | let mut full: Vec = Vec::new(); 36 | while let Some(item) = req.body_mut().frame().await { 37 | full.extend(&*item.unwrap().into_data().unwrap()); 38 | } 39 | 40 | assert_eq!(full, expected_body.as_bytes()); 41 | 42 | http::Response::default() 43 | } 44 | }); 45 | 46 | let url = format!("http://{}/multipart/1", server.addr()); 47 | 48 | let res = reqwest::Client::new() 49 | .post(&url) 50 | .multipart(form) 51 | .send() 52 | .await 53 | .unwrap(); 54 | 55 | assert_eq!(res.url().as_str(), &url); 56 | assert_eq!(res.status(), reqwest::StatusCode::OK); 57 | } 58 | 59 | #[cfg(feature = "stream")] 60 | #[tokio::test] 61 | async fn stream_part() { 62 | use futures_util::stream; 63 | use std::future; 64 | 65 | let _ = env_logger::try_init(); 66 | 67 | let stream = reqwest::Body::wrap_stream(stream::once(future::ready(Ok::<_, reqwest::Error>( 68 | "part1 part2".to_owned(), 69 | )))); 70 | let part = reqwest::multipart::Part::stream(stream); 71 | 72 | let form = reqwest::multipart::Form::new() 73 | .text("foo", "bar") 74 | .part("part_stream", part); 75 | 76 | let expected_body = format!( 77 | "\ 78 | --{0}\r\n\ 79 | Content-Disposition: form-data; name=\"foo\"\r\n\ 80 | \r\n\ 81 | bar\r\n\ 82 | --{0}\r\n\ 83 | Content-Disposition: form-data; name=\"part_stream\"\r\n\ 84 | \r\n\ 85 | part1 part2\r\n\ 86 | --{0}--\r\n\ 87 | ", 88 | form.boundary() 89 | ); 90 | 91 | let ct = format!("multipart/form-data; boundary={}", form.boundary()); 92 | 93 | let server = server::http(move |req| { 94 | let ct = ct.clone(); 95 | let expected_body = expected_body.clone(); 96 | async move { 97 | assert_eq!(req.method(), "POST"); 98 | assert_eq!(req.headers()["content-type"], ct); 99 | assert_eq!(req.headers()["transfer-encoding"], "chunked"); 100 | 101 | let full = req.collect().await.unwrap().to_bytes(); 102 | 103 | assert_eq!(full, expected_body.as_bytes()); 104 | 105 | http::Response::default() 106 | } 107 | }); 108 | 109 | let url = format!("http://{}/multipart/1", server.addr()); 110 | 111 | let client = reqwest::Client::new(); 112 | 113 | let res = client 114 | .post(&url) 115 | .multipart(form) 116 | .send() 117 | .await 118 | .expect("Failed to post multipart"); 119 | assert_eq!(res.url().as_str(), &url); 120 | assert_eq!(res.status(), reqwest::StatusCode::OK); 121 | } 122 | 123 | #[cfg(feature = "blocking")] 124 | #[test] 125 | fn blocking_file_part() { 126 | let _ = env_logger::try_init(); 127 | 128 | let form = reqwest::blocking::multipart::Form::new() 129 | .file("foo", "Cargo.lock") 130 | .unwrap(); 131 | 132 | let fcontents = std::fs::read_to_string("Cargo.lock").unwrap(); 133 | 134 | let expected_body = format!( 135 | "\ 136 | --{0}\r\n\ 137 | Content-Disposition: form-data; name=\"foo\"; filename=\"Cargo.lock\"\r\n\ 138 | Content-Type: application/octet-stream\r\n\r\n\ 139 | {1}\r\n\ 140 | --{0}--\r\n\ 141 | ", 142 | form.boundary(), 143 | fcontents 144 | ); 145 | 146 | let ct = format!("multipart/form-data; boundary={}", form.boundary()); 147 | 148 | let server = server::http(move |req| { 149 | let ct = ct.clone(); 150 | let expected_body = expected_body.clone(); 151 | async move { 152 | assert_eq!(req.method(), "POST"); 153 | assert_eq!(req.headers()["content-type"], ct); 154 | // files know their exact size 155 | assert_eq!( 156 | req.headers()["content-length"], 157 | expected_body.len().to_string() 158 | ); 159 | 160 | let full = req.collect().await.unwrap().to_bytes(); 161 | 162 | assert_eq!(full, expected_body.as_bytes()); 163 | 164 | http::Response::default() 165 | } 166 | }); 167 | 168 | let url = format!("http://{}/multipart/2", server.addr()); 169 | 170 | let res = reqwest::blocking::Client::new() 171 | .post(&url) 172 | .multipart(form) 173 | .send() 174 | .unwrap(); 175 | 176 | assert_eq!(res.url().as_str(), &url); 177 | assert_eq!(res.status(), reqwest::StatusCode::OK); 178 | } 179 | 180 | #[cfg(feature = "stream")] 181 | #[tokio::test] 182 | async fn async_impl_file_part() { 183 | let _ = env_logger::try_init(); 184 | 185 | let form = reqwest::multipart::Form::new() 186 | .file("foo", "Cargo.lock") 187 | .await 188 | .unwrap(); 189 | 190 | let fcontents = std::fs::read_to_string("Cargo.lock").unwrap(); 191 | 192 | let expected_body = format!( 193 | "\ 194 | --{0}\r\n\ 195 | Content-Disposition: form-data; name=\"foo\"; filename=\"Cargo.lock\"\r\n\ 196 | Content-Type: application/octet-stream\r\n\r\n\ 197 | {1}\r\n\ 198 | --{0}--\r\n\ 199 | ", 200 | form.boundary(), 201 | fcontents 202 | ); 203 | 204 | let ct = format!("multipart/form-data; boundary={}", form.boundary()); 205 | 206 | let server = server::http(move |req| { 207 | let ct = ct.clone(); 208 | let expected_body = expected_body.clone(); 209 | async move { 210 | assert_eq!(req.method(), "POST"); 211 | assert_eq!(req.headers()["content-type"], ct); 212 | // files know their exact size 213 | assert_eq!( 214 | req.headers()["content-length"], 215 | expected_body.len().to_string() 216 | ); 217 | let full = req.collect().await.unwrap().to_bytes(); 218 | 219 | assert_eq!(full, expected_body.as_bytes()); 220 | 221 | http::Response::default() 222 | } 223 | }); 224 | 225 | let url = format!("http://{}/multipart/3", server.addr()); 226 | 227 | let res = reqwest::Client::new() 228 | .post(&url) 229 | .multipart(form) 230 | .send() 231 | .await 232 | .unwrap(); 233 | 234 | assert_eq!(res.url().as_str(), &url); 235 | assert_eq!(res.status(), reqwest::StatusCode::OK); 236 | } 237 | -------------------------------------------------------------------------------- /tests/proxy.rs: -------------------------------------------------------------------------------- 1 | #![cfg(not(target_arch = "wasm32"))] 2 | #![cfg(not(feature = "rustls-tls-manual-roots-no-provider"))] 3 | mod support; 4 | use support::server; 5 | 6 | use std::env; 7 | 8 | use once_cell::sync::Lazy; 9 | use tokio::sync::Mutex; 10 | 11 | // serialize tests that read from / write to environment variables 12 | static HTTP_PROXY_ENV_MUTEX: Lazy> = Lazy::new(|| Mutex::new(())); 13 | 14 | #[tokio::test] 15 | async fn http_proxy() { 16 | let url = "http://hyper.rs.local/prox"; 17 | let server = server::http(move |req| { 18 | assert_eq!(req.method(), "GET"); 19 | assert_eq!(req.uri(), url); 20 | assert_eq!(req.headers()["host"], "hyper.rs.local"); 21 | 22 | async { http::Response::default() } 23 | }); 24 | 25 | let proxy = format!("http://{}", server.addr()); 26 | 27 | let res = reqwest::Client::builder() 28 | .proxy(reqwest::Proxy::http(&proxy).unwrap()) 29 | .build() 30 | .unwrap() 31 | .get(url) 32 | .send() 33 | .await 34 | .unwrap(); 35 | 36 | assert_eq!(res.url().as_str(), url); 37 | assert_eq!(res.status(), reqwest::StatusCode::OK); 38 | } 39 | 40 | #[tokio::test] 41 | async fn http_proxy_basic_auth() { 42 | let url = "http://hyper.rs.local/prox"; 43 | let server = server::http(move |req| { 44 | assert_eq!(req.method(), "GET"); 45 | assert_eq!(req.uri(), url); 46 | assert_eq!(req.headers()["host"], "hyper.rs.local"); 47 | assert_eq!( 48 | req.headers()["proxy-authorization"], 49 | "Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==" 50 | ); 51 | 52 | async { http::Response::default() } 53 | }); 54 | 55 | let proxy = format!("http://{}", server.addr()); 56 | 57 | let res = reqwest::Client::builder() 58 | .proxy( 59 | reqwest::Proxy::http(&proxy) 60 | .unwrap() 61 | .basic_auth("Aladdin", "open sesame"), 62 | ) 63 | .build() 64 | .unwrap() 65 | .get(url) 66 | .send() 67 | .await 68 | .unwrap(); 69 | 70 | assert_eq!(res.url().as_str(), url); 71 | assert_eq!(res.status(), reqwest::StatusCode::OK); 72 | } 73 | 74 | #[tokio::test] 75 | async fn http_proxy_basic_auth_parsed() { 76 | let url = "http://hyper.rs.local/prox"; 77 | let server = server::http(move |req| { 78 | assert_eq!(req.method(), "GET"); 79 | assert_eq!(req.uri(), url); 80 | assert_eq!(req.headers()["host"], "hyper.rs.local"); 81 | assert_eq!( 82 | req.headers()["proxy-authorization"], 83 | "Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==" 84 | ); 85 | 86 | async { http::Response::default() } 87 | }); 88 | 89 | let proxy = format!("http://Aladdin:open sesame@{}", server.addr()); 90 | 91 | let res = reqwest::Client::builder() 92 | .proxy(reqwest::Proxy::http(&proxy).unwrap()) 93 | .build() 94 | .unwrap() 95 | .get(url) 96 | .send() 97 | .await 98 | .unwrap(); 99 | 100 | assert_eq!(res.url().as_str(), url); 101 | assert_eq!(res.status(), reqwest::StatusCode::OK); 102 | } 103 | 104 | #[tokio::test] 105 | async fn system_http_proxy_basic_auth_parsed() { 106 | let url = "http://hyper.rs.local/prox"; 107 | let server = server::http(move |req| { 108 | assert_eq!(req.method(), "GET"); 109 | assert_eq!(req.uri(), url); 110 | assert_eq!(req.headers()["host"], "hyper.rs.local"); 111 | assert_eq!( 112 | req.headers()["proxy-authorization"], 113 | "Basic QWxhZGRpbjpvcGVuc2VzYW1l" 114 | ); 115 | 116 | async { http::Response::default() } 117 | }); 118 | 119 | // avoid races with other tests that change "http_proxy" 120 | let _env_lock = HTTP_PROXY_ENV_MUTEX.lock().await; 121 | 122 | // save system setting first. 123 | let system_proxy = env::var("http_proxy"); 124 | 125 | // set-up http proxy. 126 | env::set_var( 127 | "http_proxy", 128 | format!("http://Aladdin:opensesame@{}", server.addr()), 129 | ); 130 | 131 | let res = reqwest::Client::builder() 132 | .build() 133 | .unwrap() 134 | .get(url) 135 | .send() 136 | .await 137 | .unwrap(); 138 | 139 | assert_eq!(res.url().as_str(), url); 140 | assert_eq!(res.status(), reqwest::StatusCode::OK); 141 | 142 | // reset user setting. 143 | match system_proxy { 144 | Err(_) => env::remove_var("http_proxy"), 145 | Ok(proxy) => env::set_var("http_proxy", proxy), 146 | } 147 | } 148 | 149 | #[tokio::test] 150 | async fn test_no_proxy() { 151 | let server = server::http(move |req| { 152 | assert_eq!(req.method(), "GET"); 153 | assert_eq!(req.uri(), "/4"); 154 | 155 | async { http::Response::default() } 156 | }); 157 | let proxy = format!("http://{}", server.addr()); 158 | let url = format!("http://{}/4", server.addr()); 159 | 160 | // set up proxy and use no_proxy to clear up client builder proxies. 161 | let res = reqwest::Client::builder() 162 | .proxy(reqwest::Proxy::http(&proxy).unwrap()) 163 | .no_proxy() 164 | .build() 165 | .unwrap() 166 | .get(&url) 167 | .send() 168 | .await 169 | .unwrap(); 170 | 171 | assert_eq!(res.url().as_str(), &url); 172 | assert_eq!(res.status(), reqwest::StatusCode::OK); 173 | } 174 | 175 | #[tokio::test] 176 | async fn test_custom_headers() { 177 | let url = "http://hyper.rs.local/prox"; 178 | let server = server::http(move |req| { 179 | assert_eq!(req.method(), "GET"); 180 | assert_eq!(req.uri(), url); 181 | assert_eq!(req.headers()["host"], "hyper.rs.local"); 182 | assert_eq!( 183 | req.headers()["proxy-authorization"], 184 | "Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==" 185 | ); 186 | async { http::Response::default() } 187 | }); 188 | 189 | let proxy = format!("http://{}", server.addr()); 190 | let mut headers = reqwest::header::HeaderMap::new(); 191 | headers.insert( 192 | // reqwest::header::HeaderName::from_static("Proxy-Authorization"), 193 | reqwest::header::PROXY_AUTHORIZATION, 194 | "Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==".parse().unwrap(), 195 | ); 196 | 197 | let res = reqwest::Client::builder() 198 | .proxy(reqwest::Proxy::http(&proxy).unwrap().headers(headers)) 199 | .build() 200 | .unwrap() 201 | .get(url) 202 | .send() 203 | .await 204 | .unwrap(); 205 | 206 | assert_eq!(res.url().as_str(), url); 207 | assert_eq!(res.status(), reqwest::StatusCode::OK); 208 | } 209 | 210 | #[tokio::test] 211 | async fn test_using_system_proxy() { 212 | let url = "http://not.a.real.sub.hyper.rs.local/prox"; 213 | let server = server::http(move |req| { 214 | assert_eq!(req.method(), "GET"); 215 | assert_eq!(req.uri(), url); 216 | assert_eq!(req.headers()["host"], "not.a.real.sub.hyper.rs.local"); 217 | 218 | async { http::Response::default() } 219 | }); 220 | 221 | // avoid races with other tests that change "http_proxy" 222 | let _env_lock = HTTP_PROXY_ENV_MUTEX.lock().await; 223 | 224 | // save system setting first. 225 | let system_proxy = env::var("http_proxy"); 226 | // set-up http proxy. 227 | env::set_var("http_proxy", format!("http://{}", server.addr())); 228 | 229 | // system proxy is used by default 230 | let res = reqwest::get(url).await.unwrap(); 231 | 232 | assert_eq!(res.url().as_str(), url); 233 | assert_eq!(res.status(), reqwest::StatusCode::OK); 234 | 235 | // reset user setting. 236 | match system_proxy { 237 | Err(_) => env::remove_var("http_proxy"), 238 | Ok(proxy) => env::set_var("http_proxy", proxy), 239 | } 240 | } 241 | 242 | #[tokio::test] 243 | async fn http_over_http() { 244 | let url = "http://hyper.rs.local/prox"; 245 | 246 | let server = server::http(move |req| { 247 | assert_eq!(req.method(), "GET"); 248 | assert_eq!(req.uri(), url); 249 | assert_eq!(req.headers()["host"], "hyper.rs.local"); 250 | 251 | async { http::Response::default() } 252 | }); 253 | 254 | let proxy = format!("http://{}", server.addr()); 255 | 256 | let res = reqwest::Client::builder() 257 | .proxy(reqwest::Proxy::http(&proxy).unwrap()) 258 | .build() 259 | .unwrap() 260 | .get(url) 261 | .send() 262 | .await 263 | .unwrap(); 264 | 265 | assert_eq!(res.url().as_str(), url); 266 | assert_eq!(res.status(), reqwest::StatusCode::OK); 267 | } 268 | 269 | #[cfg(feature = "__tls")] 270 | #[tokio::test] 271 | async fn tunnel_detects_auth_required() { 272 | let url = "https://hyper.rs.local/prox"; 273 | 274 | let server = server::http(move |req| { 275 | assert_eq!(req.method(), "CONNECT"); 276 | assert_eq!(req.uri(), "hyper.rs.local:443"); 277 | assert!(!req 278 | .headers() 279 | .contains_key(http::header::PROXY_AUTHORIZATION)); 280 | 281 | async { 282 | let mut res = http::Response::default(); 283 | *res.status_mut() = http::StatusCode::PROXY_AUTHENTICATION_REQUIRED; 284 | res 285 | } 286 | }); 287 | 288 | let proxy = format!("http://{}", server.addr()); 289 | 290 | let err = reqwest::Client::builder() 291 | .proxy(reqwest::Proxy::https(&proxy).unwrap()) 292 | .build() 293 | .unwrap() 294 | .get(url) 295 | .send() 296 | .await 297 | .unwrap_err(); 298 | 299 | let err = support::error::inspect(err).pop().unwrap(); 300 | assert!( 301 | err.contains("auth"), 302 | "proxy auth err expected, got: {:?}", 303 | err 304 | ); 305 | } 306 | 307 | #[cfg(feature = "__tls")] 308 | #[tokio::test] 309 | async fn tunnel_includes_proxy_auth() { 310 | let url = "https://hyper.rs.local/prox"; 311 | 312 | let server = server::http(move |req| { 313 | assert_eq!(req.method(), "CONNECT"); 314 | assert_eq!(req.uri(), "hyper.rs.local:443"); 315 | assert_eq!( 316 | req.headers()["proxy-authorization"], 317 | "Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==" 318 | ); 319 | 320 | async { 321 | // return 400 to not actually deal with TLS tunneling 322 | let mut res = http::Response::default(); 323 | *res.status_mut() = http::StatusCode::BAD_REQUEST; 324 | res 325 | } 326 | }); 327 | 328 | let proxy = format!("http://Aladdin:open%20sesame@{}", server.addr()); 329 | 330 | let err = reqwest::Client::builder() 331 | .proxy(reqwest::Proxy::https(&proxy).unwrap()) 332 | .build() 333 | .unwrap() 334 | .get(url) 335 | .send() 336 | .await 337 | .unwrap_err(); 338 | 339 | let err = support::error::inspect(err).pop().unwrap(); 340 | assert!( 341 | err.contains("unsuccessful"), 342 | "tunnel unsuccessful expected, got: {:?}", 343 | err 344 | ); 345 | } 346 | 347 | #[cfg(feature = "__tls")] 348 | #[tokio::test] 349 | async fn tunnel_includes_user_agent() { 350 | let url = "https://hyper.rs.local/prox"; 351 | 352 | let server = server::http(move |req| { 353 | assert_eq!(req.method(), "CONNECT"); 354 | assert_eq!(req.uri(), "hyper.rs.local:443"); 355 | assert_eq!(req.headers()["user-agent"], "reqwest-test"); 356 | 357 | async { 358 | // return 400 to not actually deal with TLS tunneling 359 | let mut res = http::Response::default(); 360 | *res.status_mut() = http::StatusCode::BAD_REQUEST; 361 | res 362 | } 363 | }); 364 | 365 | let proxy = format!("http://{}", server.addr()); 366 | 367 | let err = reqwest::Client::builder() 368 | .proxy(reqwest::Proxy::https(&proxy).unwrap()) 369 | .user_agent("reqwest-test") 370 | .build() 371 | .unwrap() 372 | .get(url) 373 | .send() 374 | .await 375 | .unwrap_err(); 376 | 377 | let err = support::error::inspect(err).pop().unwrap(); 378 | assert!( 379 | err.contains("unsuccessful"), 380 | "tunnel unsuccessful expected, got: {:?}", 381 | err 382 | ); 383 | } 384 | -------------------------------------------------------------------------------- /tests/support/crl.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN X509 CRL----- 2 | MIIBnjCBhwIBATANBgkqhkiG9w0BAQsFADANMQswCQYDVQQDDAJjYRcNMjQwOTI2 3 | MDA0MjU1WhcNMjQxMDI2MDA0MjU1WjAUMBICAQEXDTI0MDkyNjAwNDI0NlqgMDAu 4 | MB8GA1UdIwQYMBaAFDxOaZI8zUaGX7mXAZ9Zd8jhyC3sMAsGA1UdFAQEAgIQATAN 5 | BgkqhkiG9w0BAQsFAAOCAQEAsqBa289UYKAOaH2gp3yC7YBF7uVZ25i3WV/InKjK 6 | zT/fFzZ9rL87ofl0VuR0GPAfwLXFQ96vYUg/nrlxF/A6FmQKf9JSlVBIVXaS2uyk 7 | fmdVX8fdU13uD2uKThT5Fojk5nKAeui0xwjTHqe9BjyDscQ5d5pkLIJUj/JbQmRF 8 | D/OtEpYQZMAdHLDF0a/9v69g/evlPlpTcikAU+T8rXp45rrsuuUgyhJ00UnE41j8 9 | MmMi3cn23JjFTyOrYx5g/0VFUNcwZpgZSnxNvFbcoh9oHHqS+UDESrwQmkmwrVvH 10 | a7PEJq5ZPtjUPa0i7oFNa9cC+11Doo5bxkpCWhypvgTUzw== 11 | -----END X509 CRL----- 12 | -------------------------------------------------------------------------------- /tests/support/delay_layer.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | future::Future, 3 | pin::Pin, 4 | task::{Context, Poll}, 5 | time::Duration, 6 | }; 7 | 8 | use pin_project_lite::pin_project; 9 | use tokio::time::Sleep; 10 | use tower::{BoxError, Layer, Service}; 11 | 12 | /// This tower layer injects an arbitrary delay before calling downstream layers. 13 | #[derive(Clone)] 14 | pub struct DelayLayer { 15 | delay: Duration, 16 | } 17 | 18 | impl DelayLayer { 19 | pub const fn new(delay: Duration) -> Self { 20 | DelayLayer { delay } 21 | } 22 | } 23 | 24 | impl Layer for DelayLayer { 25 | type Service = Delay; 26 | fn layer(&self, service: S) -> Self::Service { 27 | Delay::new(service, self.delay) 28 | } 29 | } 30 | 31 | impl std::fmt::Debug for DelayLayer { 32 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 33 | f.debug_struct("DelayLayer") 34 | .field("delay", &self.delay) 35 | .finish() 36 | } 37 | } 38 | 39 | /// This tower service injects an arbitrary delay before calling downstream layers. 40 | #[derive(Debug, Clone)] 41 | pub struct Delay { 42 | inner: S, 43 | delay: Duration, 44 | } 45 | impl Delay { 46 | pub fn new(inner: S, delay: Duration) -> Self { 47 | Delay { inner, delay } 48 | } 49 | } 50 | 51 | impl Service for Delay 52 | where 53 | S: Service, 54 | S::Error: Into, 55 | { 56 | type Response = S::Response; 57 | 58 | type Error = BoxError; 59 | 60 | type Future = ResponseFuture; 61 | 62 | fn poll_ready( 63 | &mut self, 64 | cx: &mut std::task::Context<'_>, 65 | ) -> std::task::Poll> { 66 | match self.inner.poll_ready(cx) { 67 | Poll::Pending => Poll::Pending, 68 | Poll::Ready(r) => Poll::Ready(r.map_err(Into::into)), 69 | } 70 | } 71 | 72 | fn call(&mut self, req: Request) -> Self::Future { 73 | let response = self.inner.call(req); 74 | let sleep = tokio::time::sleep(self.delay); 75 | 76 | ResponseFuture::new(response, sleep) 77 | } 78 | } 79 | 80 | // `Delay` response future 81 | pin_project! { 82 | #[derive(Debug)] 83 | pub struct ResponseFuture { 84 | #[pin] 85 | response: S, 86 | #[pin] 87 | sleep: Sleep, 88 | } 89 | } 90 | 91 | impl ResponseFuture { 92 | pub(crate) fn new(response: S, sleep: Sleep) -> Self { 93 | ResponseFuture { response, sleep } 94 | } 95 | } 96 | 97 | impl Future for ResponseFuture 98 | where 99 | F: Future>, 100 | E: Into, 101 | { 102 | type Output = Result; 103 | 104 | fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { 105 | let this = self.project(); 106 | 107 | // First poll the sleep until complete 108 | match this.sleep.poll(cx) { 109 | Poll::Pending => return Poll::Pending, 110 | Poll::Ready(_) => {} 111 | } 112 | 113 | // Then poll the inner future 114 | match this.response.poll(cx) { 115 | Poll::Ready(v) => Poll::Ready(v.map_err(Into::into)), 116 | Poll::Pending => Poll::Pending, 117 | } 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /tests/support/delay_server.rs: -------------------------------------------------------------------------------- 1 | #![cfg(not(target_arch = "wasm32"))] 2 | #![allow(unused)] 3 | use std::convert::Infallible; 4 | use std::future::Future; 5 | use std::net; 6 | use std::time::Duration; 7 | 8 | use futures_util::FutureExt; 9 | use http::{Request, Response}; 10 | use hyper::service::service_fn; 11 | use tokio::net::TcpListener; 12 | use tokio::select; 13 | use tokio::sync::oneshot; 14 | 15 | /// This server, unlike [`super::server::Server`], allows for delaying the 16 | /// specified amount of time after each TCP connection is established. This is 17 | /// useful for testing the behavior of the client when the server is slow. 18 | /// 19 | /// For example, in case of HTTP/2, once the TCP/TLS connection is established, 20 | /// both endpoints are supposed to send a preface and an initial `SETTINGS` 21 | /// frame (See [RFC9113 3.4] for details). What if these frames are delayed for 22 | /// whatever reason? This server allows for testing such scenarios. 23 | /// 24 | /// [RFC9113 3.4]: https://www.rfc-editor.org/rfc/rfc9113.html#name-http-2-connection-preface 25 | pub struct Server { 26 | addr: net::SocketAddr, 27 | shutdown_tx: Option>, 28 | server_terminated_rx: oneshot::Receiver<()>, 29 | } 30 | 31 | type Builder = hyper_util::server::conn::auto::Builder; 32 | 33 | impl Server { 34 | pub async fn new(func: F1, apply_config: F2, delay: Duration) -> Self 35 | where 36 | F1: Fn(Request) -> Fut + Clone + Send + 'static, 37 | Fut: Future> + Send + 'static, 38 | F2: FnOnce(&mut Builder) -> Bu + Send + 'static, 39 | { 40 | let (shutdown_tx, shutdown_rx) = oneshot::channel(); 41 | let (server_terminated_tx, server_terminated_rx) = oneshot::channel(); 42 | 43 | let tcp_listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); 44 | let addr = tcp_listener.local_addr().unwrap(); 45 | 46 | tokio::spawn(async move { 47 | let mut builder = 48 | hyper_util::server::conn::auto::Builder::new(hyper_util::rt::TokioExecutor::new()); 49 | apply_config(&mut builder); 50 | 51 | tokio::spawn(async move { 52 | let builder = builder; 53 | let (connection_shutdown_tx, connection_shutdown_rx) = oneshot::channel(); 54 | let connection_shutdown_rx = connection_shutdown_rx.shared(); 55 | let mut shutdown_rx = std::pin::pin!(shutdown_rx); 56 | 57 | let mut handles = Vec::new(); 58 | loop { 59 | select! { 60 | _ = shutdown_rx.as_mut() => { 61 | connection_shutdown_tx.send(()).unwrap(); 62 | break; 63 | } 64 | res = tcp_listener.accept() => { 65 | let (stream, _) = res.unwrap(); 66 | let io = hyper_util::rt::TokioIo::new(stream); 67 | 68 | 69 | let handle = tokio::spawn({ 70 | let connection_shutdown_rx = connection_shutdown_rx.clone(); 71 | let func = func.clone(); 72 | let svc = service_fn(move |req| { 73 | let fut = func(req); 74 | async move { 75 | Ok::<_, Infallible>(fut.await) 76 | }}); 77 | let builder = builder.clone(); 78 | 79 | async move { 80 | let fut = builder.serve_connection_with_upgrades(io, svc); 81 | tokio::time::sleep(delay).await; 82 | 83 | let mut conn = std::pin::pin!(fut); 84 | 85 | select! { 86 | _ = conn.as_mut() => {} 87 | _ = connection_shutdown_rx => { 88 | conn.as_mut().graceful_shutdown(); 89 | conn.await.unwrap(); 90 | } 91 | } 92 | } 93 | }); 94 | 95 | handles.push(handle); 96 | } 97 | } 98 | } 99 | 100 | futures_util::future::join_all(handles).await; 101 | server_terminated_tx.send(()).unwrap(); 102 | }); 103 | }); 104 | 105 | Self { 106 | addr, 107 | shutdown_tx: Some(shutdown_tx), 108 | server_terminated_rx, 109 | } 110 | } 111 | 112 | pub async fn shutdown(mut self) { 113 | if let Some(tx) = self.shutdown_tx.take() { 114 | let _ = tx.send(()); 115 | } 116 | 117 | self.server_terminated_rx.await.unwrap(); 118 | } 119 | 120 | pub fn addr(&self) -> net::SocketAddr { 121 | self.addr 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /tests/support/error.rs: -------------------------------------------------------------------------------- 1 | use std::error::Error as StdError; 2 | 3 | pub fn inspect(err: E) -> Vec 4 | where 5 | E: Into>, 6 | { 7 | let berr = err.into(); 8 | let mut err = Some(&*berr as &(dyn StdError + 'static)); 9 | let mut errs = Vec::new(); 10 | while let Some(e) = err { 11 | errs.push(e.to_string()); 12 | err = e.source(); 13 | } 14 | errs 15 | } 16 | -------------------------------------------------------------------------------- /tests/support/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod delay_layer; 2 | pub mod delay_server; 3 | pub mod error; 4 | pub mod server; 5 | 6 | // TODO: remove once done converting to new support server? 7 | #[allow(unused)] 8 | pub static DEFAULT_USER_AGENT: &str = 9 | concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION")); 10 | -------------------------------------------------------------------------------- /tests/support/server.cert: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seanmonstar/reqwest/8cf142bd1f1722a1728a89af21747260bb993294/tests/support/server.cert -------------------------------------------------------------------------------- /tests/support/server.key: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seanmonstar/reqwest/8cf142bd1f1722a1728a89af21747260bb993294/tests/support/server.key -------------------------------------------------------------------------------- /tests/upgrade.rs: -------------------------------------------------------------------------------- 1 | #![cfg(not(target_arch = "wasm32"))] 2 | #![cfg(not(feature = "rustls-tls-manual-roots-no-provider"))] 3 | mod support; 4 | use support::server; 5 | use tokio::io::{AsyncReadExt, AsyncWriteExt}; 6 | 7 | #[tokio::test] 8 | async fn http_upgrade() { 9 | let server = server::http(move |req| { 10 | assert_eq!(req.method(), "GET"); 11 | assert_eq!(req.headers()["connection"], "upgrade"); 12 | assert_eq!(req.headers()["upgrade"], "foobar"); 13 | 14 | tokio::spawn(async move { 15 | let mut upgraded = hyper_util::rt::TokioIo::new(hyper::upgrade::on(req).await.unwrap()); 16 | 17 | let mut buf = vec![0; 7]; 18 | upgraded.read_exact(&mut buf).await.unwrap(); 19 | assert_eq!(buf, b"foo=bar"); 20 | 21 | upgraded.write_all(b"bar=foo").await.unwrap(); 22 | }); 23 | 24 | async { 25 | http::Response::builder() 26 | .status(http::StatusCode::SWITCHING_PROTOCOLS) 27 | .header(http::header::CONNECTION, "upgrade") 28 | .header(http::header::UPGRADE, "foobar") 29 | .body(reqwest::Body::default()) 30 | .unwrap() 31 | } 32 | }); 33 | 34 | let res = reqwest::Client::builder() 35 | .build() 36 | .unwrap() 37 | .get(format!("http://{}", server.addr())) 38 | .header(http::header::CONNECTION, "upgrade") 39 | .header(http::header::UPGRADE, "foobar") 40 | .send() 41 | .await 42 | .unwrap(); 43 | 44 | assert_eq!(res.status(), http::StatusCode::SWITCHING_PROTOCOLS); 45 | let mut upgraded = res.upgrade().await.unwrap(); 46 | 47 | upgraded.write_all(b"foo=bar").await.unwrap(); 48 | 49 | let mut buf = vec![]; 50 | upgraded.read_to_end(&mut buf).await.unwrap(); 51 | assert_eq!(buf, b"bar=foo"); 52 | } 53 | -------------------------------------------------------------------------------- /tests/wasm_simple.rs: -------------------------------------------------------------------------------- 1 | #![cfg(target_arch = "wasm32")] 2 | use std::time::Duration; 3 | 4 | use wasm_bindgen::prelude::*; 5 | use wasm_bindgen_test::*; 6 | wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); 7 | 8 | #[wasm_bindgen] 9 | extern "C" { 10 | // Use `js_namespace` here to bind `console.log(..)` instead of just 11 | // `log(..)` 12 | #[wasm_bindgen(js_namespace = console)] 13 | fn log(s: &str); 14 | } 15 | 16 | #[wasm_bindgen_test] 17 | async fn simple_example() { 18 | let res = reqwest::get("https://hyper.rs") 19 | .await 20 | .expect("http get example"); 21 | log(&format!("Status: {}", res.status())); 22 | 23 | let body = res.text().await.expect("response to utf-8 text"); 24 | log(&format!("Body:\n\n{body}")); 25 | } 26 | 27 | #[wasm_bindgen_test] 28 | async fn request_with_timeout() { 29 | let client = reqwest::Client::new(); 30 | let err = client 31 | .get("https://hyper.rs") 32 | .timeout(Duration::from_millis(1)) 33 | .send() 34 | .await 35 | .expect_err("Expected error from aborted request"); 36 | 37 | assert!(err.is_request()); 38 | assert!(err.is_timeout()); 39 | } 40 | --------------------------------------------------------------------------------