mirror of https://github.com/fafhrd91/actix-web
Merge branch 'main' into feat-files-array-support
This commit is contained in:
commit
c55a37a6da
|
|
@ -49,7 +49,7 @@ jobs:
|
|||
toolchain: ${{ matrix.version.version }}
|
||||
|
||||
- name: Install just, cargo-hack, cargo-nextest, cargo-ci-cache-clean
|
||||
uses: taiki-e/install-action@f176c07a0a40cbfdd08ee9aa8bf1655701d11e69 # v2.67.25
|
||||
uses: taiki-e/install-action@f92912fad184299a31e22ad070a5059fd07d4f59 # v2.68.7
|
||||
with:
|
||||
tool: just,cargo-hack,cargo-nextest,cargo-ci-cache-clean
|
||||
|
||||
|
|
@ -83,7 +83,7 @@ jobs:
|
|||
uses: actions-rust-lang/setup-rust-toolchain@1780873c7b576612439a134613cc4cc74ce5538c # v1.15.2
|
||||
|
||||
- name: Install just, cargo-hack
|
||||
uses: taiki-e/install-action@f176c07a0a40cbfdd08ee9aa8bf1655701d11e69 # v2.67.25
|
||||
uses: taiki-e/install-action@f92912fad184299a31e22ad070a5059fd07d4f59 # v2.68.7
|
||||
with:
|
||||
tool: just,cargo-hack
|
||||
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@ jobs:
|
|||
toolchain: ${{ matrix.version.version }}
|
||||
|
||||
- name: Install just, cargo-hack, cargo-nextest, cargo-ci-cache-clean
|
||||
uses: taiki-e/install-action@f176c07a0a40cbfdd08ee9aa8bf1655701d11e69 # v2.67.25
|
||||
uses: taiki-e/install-action@f92912fad184299a31e22ad070a5059fd07d4f59 # v2.68.7
|
||||
with:
|
||||
tool: just,cargo-hack,cargo-nextest,cargo-ci-cache-clean
|
||||
|
||||
|
|
@ -79,7 +79,7 @@ jobs:
|
|||
run: just check-default
|
||||
|
||||
- name: tests
|
||||
timeout-minutes: 60
|
||||
timeout-minutes: 30
|
||||
run: just test
|
||||
|
||||
- name: CI cache clean
|
||||
|
|
@ -101,7 +101,7 @@ jobs:
|
|||
toolchain: nightly
|
||||
|
||||
- name: tests (io-uring)
|
||||
timeout-minutes: 60
|
||||
timeout-minutes: 30
|
||||
run: >
|
||||
sudo bash -c "ulimit -Sl 512 && ulimit -Hl 512 && PATH=$PATH:/usr/share/rust/.cargo/bin && RUSTUP_TOOLCHAIN=stable cargo test --lib --tests -p=actix-files --all-features"
|
||||
|
||||
|
|
@ -117,7 +117,7 @@ jobs:
|
|||
toolchain: nightly
|
||||
|
||||
- name: Install just
|
||||
uses: taiki-e/install-action@f176c07a0a40cbfdd08ee9aa8bf1655701d11e69 # v2.67.25
|
||||
uses: taiki-e/install-action@f92912fad184299a31e22ad070a5059fd07d4f59 # v2.68.7
|
||||
with:
|
||||
tool: just
|
||||
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ jobs:
|
|||
components: llvm-tools
|
||||
|
||||
- name: Install just, cargo-llvm-cov, cargo-nextest
|
||||
uses: taiki-e/install-action@f176c07a0a40cbfdd08ee9aa8bf1655701d11e69 # v2.67.25
|
||||
uses: taiki-e/install-action@f92912fad184299a31e22ad070a5059fd07d4f59 # v2.68.7
|
||||
with:
|
||||
tool: just,cargo-llvm-cov,cargo-nextest
|
||||
|
||||
|
|
|
|||
|
|
@ -77,12 +77,12 @@ jobs:
|
|||
toolchain: ${{ vars.RUST_VERSION_EXTERNAL_TYPES }}
|
||||
|
||||
- name: Install just
|
||||
uses: taiki-e/install-action@f176c07a0a40cbfdd08ee9aa8bf1655701d11e69 # v2.67.25
|
||||
uses: taiki-e/install-action@f92912fad184299a31e22ad070a5059fd07d4f59 # v2.68.7
|
||||
with:
|
||||
tool: just
|
||||
|
||||
- name: Install cargo-check-external-types
|
||||
uses: taiki-e/cache-cargo-install-action@34ce5120836e5f9f1508d8713d7fdea0e8facd6f # v3.0.1
|
||||
uses: taiki-e/cache-cargo-install-action@2bfc3cedaf2ee5e7fa5d0ae034ccd5fb50cf8e1f # v3.0.2
|
||||
with:
|
||||
tool: cargo-check-external-types
|
||||
|
||||
|
|
|
|||
|
|
@ -2,36 +2,13 @@
|
|||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "actix"
|
||||
version = "0.13.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "de7fa236829ba0841304542f7614c42b80fca007455315c45c785ccfa873a85b"
|
||||
dependencies = [
|
||||
"actix-rt",
|
||||
"bitflags 2.10.0",
|
||||
"bytes",
|
||||
"crossbeam-channel",
|
||||
"futures-core",
|
||||
"futures-sink",
|
||||
"futures-task",
|
||||
"futures-util",
|
||||
"log",
|
||||
"once_cell",
|
||||
"parking_lot",
|
||||
"pin-project-lite",
|
||||
"smallvec",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "actix-codec"
|
||||
version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5f7b0a21988c1bf877cf4759ef5ddaac04c1c9fe808c9142ecb78ba97d97a28a"
|
||||
dependencies = [
|
||||
"bitflags 2.10.0",
|
||||
"bitflags 2.11.0",
|
||||
"bytes",
|
||||
"futures-core",
|
||||
"futures-sink",
|
||||
|
|
@ -53,7 +30,7 @@ dependencies = [
|
|||
"actix-test",
|
||||
"actix-utils",
|
||||
"actix-web",
|
||||
"bitflags 2.10.0",
|
||||
"bitflags 2.11.0",
|
||||
"bytes",
|
||||
"derive_more",
|
||||
"env_logger",
|
||||
|
|
@ -72,7 +49,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "actix-http"
|
||||
version = "3.11.2"
|
||||
version = "3.12.0"
|
||||
dependencies = [
|
||||
"actix-codec",
|
||||
"actix-http-test",
|
||||
|
|
@ -83,8 +60,9 @@ dependencies = [
|
|||
"actix-utils",
|
||||
"actix-web",
|
||||
"async-stream",
|
||||
"awc",
|
||||
"base64 0.22.1",
|
||||
"bitflags 2.10.0",
|
||||
"bitflags 2.11.0",
|
||||
"brotli",
|
||||
"bytes",
|
||||
"bytestring",
|
||||
|
|
@ -225,7 +203,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "actix-router"
|
||||
version = "0.5.3"
|
||||
version = "0.5.4"
|
||||
dependencies = [
|
||||
"bytestring",
|
||||
"cfg-if",
|
||||
|
|
@ -346,7 +324,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "actix-web"
|
||||
version = "4.12.1"
|
||||
version = "4.13.0"
|
||||
dependencies = [
|
||||
"actix-codec",
|
||||
"actix-files",
|
||||
|
|
@ -404,28 +382,6 @@ dependencies = [
|
|||
"zstd",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "actix-web-actors"
|
||||
version = "4.3.1+deprecated"
|
||||
dependencies = [
|
||||
"actix",
|
||||
"actix-codec",
|
||||
"actix-http",
|
||||
"actix-rt",
|
||||
"actix-test",
|
||||
"actix-web",
|
||||
"awc",
|
||||
"bytes",
|
||||
"bytestring",
|
||||
"env_logger",
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
"mime",
|
||||
"pin-project-lite",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "actix-web-codegen"
|
||||
version = "4.3.0"
|
||||
|
|
@ -612,7 +568,7 @@ checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
|
|||
|
||||
[[package]]
|
||||
name = "awc"
|
||||
version = "3.8.1"
|
||||
version = "3.8.2"
|
||||
dependencies = [
|
||||
"actix-codec",
|
||||
"actix-http",
|
||||
|
|
@ -701,9 +657,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
|
|||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.10.0"
|
||||
version = "2.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3"
|
||||
checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af"
|
||||
|
||||
[[package]]
|
||||
name = "block-buffer"
|
||||
|
|
@ -1221,9 +1177,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "env_filter"
|
||||
version = "0.1.4"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1bf3c259d255ca70051b30e2e95b5446cdb8949ac4cd22c0d7fd634d89f568e2"
|
||||
checksum = "7a1c3cc8e57274ec99de65301228b537f1e4eedc1b8e0f9411c6caac8ae7308f"
|
||||
dependencies = [
|
||||
"log",
|
||||
"regex",
|
||||
|
|
@ -1231,9 +1187,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "env_logger"
|
||||
version = "0.11.8"
|
||||
version = "0.11.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f"
|
||||
checksum = "b2daee4ea451f429a58296525ddf28b45a3b64f1acf6587e2067437bb11e218d"
|
||||
dependencies = [
|
||||
"anstream",
|
||||
"anstyle",
|
||||
|
|
@ -1344,15 +1300,15 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "futures-core"
|
||||
version = "0.3.31"
|
||||
version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
|
||||
checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d"
|
||||
|
||||
[[package]]
|
||||
name = "futures-executor"
|
||||
version = "0.3.31"
|
||||
version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f"
|
||||
checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-task",
|
||||
|
|
@ -1361,15 +1317,15 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "futures-io"
|
||||
version = "0.3.31"
|
||||
version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6"
|
||||
checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718"
|
||||
|
||||
[[package]]
|
||||
name = "futures-macro"
|
||||
version = "0.3.31"
|
||||
version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650"
|
||||
checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
|
|
@ -1378,21 +1334,21 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "futures-sink"
|
||||
version = "0.3.31"
|
||||
version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7"
|
||||
checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893"
|
||||
|
||||
[[package]]
|
||||
name = "futures-task"
|
||||
version = "0.3.31"
|
||||
version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988"
|
||||
checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393"
|
||||
|
||||
[[package]]
|
||||
name = "futures-test"
|
||||
version = "0.3.31"
|
||||
version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5961fb6311645f46e2cdc2964a8bfae6743fd72315eaec181a71ae3eb2467113"
|
||||
checksum = "32d24b40cb9018c6b0f9d891b74a86a777d5db37972a115016d1150257b1c793"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-executor",
|
||||
|
|
@ -1406,9 +1362,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "futures-util"
|
||||
version = "0.3.31"
|
||||
version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
|
||||
checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-io",
|
||||
|
|
@ -1416,7 +1372,6 @@ dependencies = [
|
|||
"futures-task",
|
||||
"memchr",
|
||||
"pin-project-lite",
|
||||
"pin-utils",
|
||||
"slab",
|
||||
]
|
||||
|
||||
|
|
@ -1875,7 +1830,7 @@ version = "0.1.12"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616"
|
||||
dependencies = [
|
||||
"bitflags 2.10.0",
|
||||
"bitflags 2.11.0",
|
||||
"libc",
|
||||
"redox_syscall 0.7.1",
|
||||
]
|
||||
|
|
@ -2061,7 +2016,7 @@ version = "0.10.75"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "08838db121398ad17ab8531ce9de97b244589089e290a384c900cb9ff7434328"
|
||||
dependencies = [
|
||||
"bitflags 2.10.0",
|
||||
"bitflags 2.11.0",
|
||||
"cfg-if",
|
||||
"foreign-types",
|
||||
"libc",
|
||||
|
|
@ -2164,12 +2119,6 @@ version = "0.2.16"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b"
|
||||
|
||||
[[package]]
|
||||
name = "pin-utils"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
|
||||
|
||||
[[package]]
|
||||
name = "pkg-config"
|
||||
version = "0.3.32"
|
||||
|
|
@ -2377,7 +2326,7 @@ version = "0.5.18"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
|
||||
dependencies = [
|
||||
"bitflags 2.10.0",
|
||||
"bitflags 2.11.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -2386,7 +2335,7 @@ version = "0.7.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "35985aa610addc02e24fc232012c86fd11f14111180f902b67e2d5331f8ebf2b"
|
||||
dependencies = [
|
||||
"bitflags 2.10.0",
|
||||
"bitflags 2.11.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -2474,7 +2423,7 @@ version = "1.1.3"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34"
|
||||
dependencies = [
|
||||
"bitflags 2.10.0",
|
||||
"bitflags 2.11.0",
|
||||
"errno",
|
||||
"libc",
|
||||
"linux-raw-sys",
|
||||
|
|
@ -2646,7 +2595,7 @@ version = "3.5.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b3297343eaf830f66ede390ea39da1d462b6b0c1b000f420d0a83f898bbbe6ef"
|
||||
dependencies = [
|
||||
"bitflags 2.10.0",
|
||||
"bitflags 2.11.0",
|
||||
"core-foundation",
|
||||
"core-foundation-sys",
|
||||
"libc",
|
||||
|
|
@ -2900,9 +2849,9 @@ checksum = "591ef38edfb78ca4771ee32cf494cb8771944bee237a9b91fc9c1424ac4b777b"
|
|||
|
||||
[[package]]
|
||||
name = "tempfile"
|
||||
version = "3.24.0"
|
||||
version = "3.25.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "655da9c7eb6305c55742045d5a8d2037996d61d8de95806335c7c86ce0f82e9c"
|
||||
checksum = "0136791f7c95b1f6dd99f9cc786b91bb81c3800b639b3478e561ddb7be95e5f1"
|
||||
dependencies = [
|
||||
"fastrand",
|
||||
"getrandom 0.3.4",
|
||||
|
|
@ -3158,9 +3107,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "toml"
|
||||
version = "0.9.11+spec-1.1.0"
|
||||
version = "1.0.1+spec-1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f3afc9a848309fe1aaffaed6e1546a7a14de1f935dc9d89d32afd9a44bab7c46"
|
||||
checksum = "bbe30f93627849fa362d4a602212d41bb237dc2bd0f8ba0b2ce785012e124220"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
"serde_core",
|
||||
|
|
@ -3173,18 +3122,18 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "toml_datetime"
|
||||
version = "0.7.5+spec-1.1.0"
|
||||
version = "1.0.0+spec-1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347"
|
||||
checksum = "32c2555c699578a4f59f0cc68e5116c8d7cabbd45e1409b989d4be085b53f13e"
|
||||
dependencies = [
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_parser"
|
||||
version = "1.0.6+spec-1.1.0"
|
||||
version = "1.0.8+spec-1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a3198b4b0a8e11f09dd03e133c0280504d0801269e9afa46362ffde1cbeebf44"
|
||||
checksum = "0742ff5ff03ea7e67c8ae6c93cac239e0d9784833362da3f9a9c1da8dfefcbdc"
|
||||
dependencies = [
|
||||
"winnow",
|
||||
]
|
||||
|
|
@ -3229,9 +3178,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "trybuild"
|
||||
version = "1.0.115"
|
||||
version = "1.0.116"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5f614c21bd3a61bad9501d75cbb7686f00386c806d7f456778432c25cf86948a"
|
||||
checksum = "47c635f0191bd3a2941013e5062667100969f8c4e9cd787c14f977265d73616e"
|
||||
dependencies = [
|
||||
"glob",
|
||||
"serde",
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ members = [
|
|||
"actix-multipart-derive",
|
||||
"actix-router",
|
||||
"actix-test",
|
||||
"actix-web-actors",
|
||||
"actix-web-codegen",
|
||||
"actix-web",
|
||||
"awc",
|
||||
|
|
@ -39,7 +38,6 @@ actix-multipart-derive = { path = "actix-multipart-derive" }
|
|||
actix-router = { path = "actix-router" }
|
||||
actix-test = { path = "actix-test" }
|
||||
actix-web = { path = "actix-web" }
|
||||
actix-web-actors = { path = "actix-web-actors" }
|
||||
actix-web-codegen = { path = "actix-web-codegen" }
|
||||
awc = { path = "awc" }
|
||||
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ pub struct HttpRange {
|
|||
impl HttpRange {
|
||||
/// Parses Range HTTP header string as per RFC 2616.
|
||||
///
|
||||
/// `header` is HTTP Range header (e.g. `bytes=bytes=0-9`).
|
||||
/// `header` is HTTP Range header (e.g. `bytes=0-9`).
|
||||
/// `size` is full size of response (file).
|
||||
pub fn parse(header: &str, size: u64) -> Result<Vec<HttpRange>, ParseRangeErr> {
|
||||
let ranges =
|
||||
|
|
|
|||
|
|
@ -2,17 +2,25 @@
|
|||
|
||||
## Unreleased
|
||||
|
||||
- Encode the HTTP/1 `Connection: Upgrade` header in Camel-Case when camel-case header formatting is enabled.[#3953]
|
||||
|
||||
[#3953]: https://github.com/actix/actix-web/pull/3953
|
||||
|
||||
## 3.12.0
|
||||
|
||||
- Minimum supported Rust version (MSRV) is now 1.88.
|
||||
- Increase default HTTP/2 flow control window sizes. [#3638]
|
||||
- Expose configuration methods to improve upload throughput. [#3638]
|
||||
- Fix truncated body ending without error when connection closed abnormally. [#3067]
|
||||
- Add config/method for `TCP_NODELAY`. [#3918]
|
||||
- Do not compress 206 Partial Content responses. [#3191]
|
||||
- Fix lingering sockets and client stalls when responding early to dropped chunked request payloads. [#2972]
|
||||
|
||||
[#3638]: https://github.com/actix/actix-web/issues/3638
|
||||
[#3067]: https://github.com/actix/actix-web/pull/3067
|
||||
[#3918]: https://github.com/actix/actix-web/pull/3918
|
||||
[#3191]: https://github.com/actix/actix-web/issues/3191
|
||||
[#2972]: https://github.com/actix/actix-web/issues/2972
|
||||
|
||||
## 3.11.2
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "actix-http"
|
||||
version = "3.11.2"
|
||||
version = "3.12.0"
|
||||
authors = ["Nikolay Kim <fafhrd91@gmail.com>", "Rob Ede <robjtede@icloud.com>"]
|
||||
description = "HTTP types and services for the Actix ecosystem"
|
||||
keywords = ["actix", "http", "framework", "async", "futures"]
|
||||
|
|
@ -139,6 +139,7 @@ actix-http-test = { version = "3", features = ["openssl"] }
|
|||
actix-server = "2"
|
||||
actix-tls = { version = "3.4", features = ["openssl", "rustls-0_23-webpki-roots"] }
|
||||
actix-web = "4"
|
||||
awc = { version = "3", default-features = false, features = ["openssl"] }
|
||||
|
||||
async-stream = "0.3"
|
||||
criterion = { version = "0.5", features = ["html_reports"] }
|
||||
|
|
|
|||
|
|
@ -5,11 +5,11 @@
|
|||
<!-- prettier-ignore-start -->
|
||||
|
||||
[](https://crates.io/crates/actix-http)
|
||||
[](https://docs.rs/actix-http/3.11.2)
|
||||
[](https://docs.rs/actix-http/3.12.0)
|
||||

|
||||

|
||||
<br />
|
||||
[](https://deps.rs/crate/actix-http/3.11.2)
|
||||
[](https://deps.rs/crate/actix-http/3.12.0)
|
||||
[](https://crates.io/crates/actix-http)
|
||||
[](https://discord.gg/NWpN5mmg3x)
|
||||
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ use crate::{
|
|||
config::ServiceConfig,
|
||||
error::{DispatchError, ParseError, PayloadError},
|
||||
service::HttpFlow,
|
||||
Error, Extensions, OnConnectData, Request, Response, StatusCode,
|
||||
Error, Extensions, HttpMessage, OnConnectData, Request, Response, StatusCode,
|
||||
};
|
||||
|
||||
const LW_BUFFER_SIZE: usize = 1024;
|
||||
|
|
@ -157,6 +157,8 @@ pin_project! {
|
|||
pub(super) state: State<S, B, X>,
|
||||
// when Some(_) dispatcher is in state of receiving request payload
|
||||
payload: Option<PayloadSender>,
|
||||
// true when current request uses chunked transfer encoding (drainable when payload is dropped)
|
||||
payload_drainable: bool,
|
||||
messages: VecDeque<DispatcherMessage>,
|
||||
|
||||
head_timer: TimerState,
|
||||
|
|
@ -269,6 +271,7 @@ where
|
|||
|
||||
state: State::None,
|
||||
payload: None,
|
||||
payload_drainable: false,
|
||||
messages: VecDeque::new(),
|
||||
|
||||
head_timer: TimerState::new(config.client_request_deadline().is_some()),
|
||||
|
|
@ -308,7 +311,10 @@ where
|
|||
if self.flags.contains(Flags::READ_DISCONNECT) {
|
||||
false
|
||||
} else if let Some(ref info) = self.payload {
|
||||
info.need_read(cx) == PayloadStatus::Read
|
||||
matches!(
|
||||
info.need_read(cx),
|
||||
PayloadStatus::Read | PayloadStatus::Dropped
|
||||
)
|
||||
} else {
|
||||
true
|
||||
}
|
||||
|
|
@ -387,8 +393,10 @@ where
|
|||
this.state.set(match size {
|
||||
BodySize::None | BodySize::Sized(0) => {
|
||||
let payload_unfinished = this.payload.is_some();
|
||||
let drain_payload = this.payload.as_ref().is_some_and(|pl| pl.is_dropped())
|
||||
&& *this.payload_drainable;
|
||||
|
||||
if payload_unfinished {
|
||||
if payload_unfinished && !drain_payload {
|
||||
this.flags.insert(Flags::SHUTDOWN | Flags::FINISHED);
|
||||
} else {
|
||||
this.flags.insert(Flags::FINISHED);
|
||||
|
|
@ -412,8 +420,10 @@ where
|
|||
this.state.set(match size {
|
||||
BodySize::None | BodySize::Sized(0) => {
|
||||
let payload_unfinished = this.payload.is_some();
|
||||
let drain_payload = this.payload.as_ref().is_some_and(|pl| pl.is_dropped())
|
||||
&& *this.payload_drainable;
|
||||
|
||||
if payload_unfinished {
|
||||
if payload_unfinished && !drain_payload {
|
||||
this.flags.insert(Flags::SHUTDOWN | Flags::FINISHED);
|
||||
} else {
|
||||
this.flags.insert(Flags::FINISHED);
|
||||
|
|
@ -469,8 +479,11 @@ where
|
|||
|
||||
// all messages are dealt with
|
||||
None => {
|
||||
// start keep-alive if last request allowed it
|
||||
this.flags.set(Flags::KEEP_ALIVE, this.codec.keep_alive());
|
||||
// start keep-alive only if request payload is fully read/drained
|
||||
this.flags.set(
|
||||
Flags::KEEP_ALIVE,
|
||||
this.payload.is_none() && this.codec.keep_alive(),
|
||||
);
|
||||
|
||||
return Ok(PollResponse::DoNothing);
|
||||
}
|
||||
|
|
@ -522,13 +535,16 @@ where
|
|||
// responding to. We can check to see if we finished reading it
|
||||
// yet, and if not, shutdown the connection.
|
||||
let payload_unfinished = this.payload.is_some();
|
||||
let drain_payload =
|
||||
this.payload.as_ref().is_some_and(|pl| pl.is_dropped())
|
||||
&& *this.payload_drainable;
|
||||
let not_pipelined = this.messages.is_empty();
|
||||
|
||||
// payload stream finished.
|
||||
// set state to None and handle next message
|
||||
this.state.set(State::None);
|
||||
|
||||
if not_pipelined && payload_unfinished {
|
||||
if not_pipelined && payload_unfinished && !drain_payload {
|
||||
this.flags.insert(Flags::SHUTDOWN | Flags::FINISHED);
|
||||
} else {
|
||||
this.flags.insert(Flags::FINISHED);
|
||||
|
|
@ -573,13 +589,16 @@ where
|
|||
// responding to. We can check to see if we finished reading it
|
||||
// yet, and if not, shutdown the connection.
|
||||
let payload_unfinished = this.payload.is_some();
|
||||
let drain_payload =
|
||||
this.payload.as_ref().is_some_and(|pl| pl.is_dropped())
|
||||
&& *this.payload_drainable;
|
||||
let not_pipelined = this.messages.is_empty();
|
||||
|
||||
// payload stream finished.
|
||||
// set state to None and handle next message
|
||||
this.state.set(State::None);
|
||||
|
||||
if not_pipelined && payload_unfinished {
|
||||
if not_pipelined && payload_unfinished && !drain_payload {
|
||||
this.flags.insert(Flags::SHUTDOWN | Flags::FINISHED);
|
||||
} else {
|
||||
this.flags.insert(Flags::FINISHED);
|
||||
|
|
@ -748,12 +767,13 @@ where
|
|||
|
||||
match this.codec.message_type() {
|
||||
// request has no payload
|
||||
MessageType::None => {}
|
||||
MessageType::None => *this.payload_drainable = false,
|
||||
|
||||
// Request is upgradable. Add upgrade message and break.
|
||||
// Everything remaining in read buffer will be handed to
|
||||
// upgraded Request.
|
||||
MessageType::Stream if this.flow.upgrade.is_some() => {
|
||||
*this.payload_drainable = false;
|
||||
this.messages.push_back(DispatcherMessage::Upgrade(req));
|
||||
break;
|
||||
}
|
||||
|
|
@ -768,6 +788,7 @@ where
|
|||
let (sender, payload) = Payload::create(false);
|
||||
*req.payload() = crate::Payload::H1 { payload };
|
||||
*this.payload = Some(sender);
|
||||
*this.payload_drainable = req.chunked().unwrap_or(false);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -797,6 +818,7 @@ where
|
|||
Message::Chunk(None) => {
|
||||
if let Some(mut payload) = this.payload.take() {
|
||||
payload.feed_eof();
|
||||
*this.payload_drainable = false;
|
||||
} else {
|
||||
error!("Internal server error: unexpected eof");
|
||||
this.flags.insert(Flags::READ_DISCONNECT);
|
||||
|
|
@ -999,23 +1021,14 @@ where
|
|||
//
|
||||
// A Request head too large to parse is only checked on `httparse::Status::Partial`.
|
||||
|
||||
match this.payload {
|
||||
// When dispatcher has a payload the responsibility of wake ups is shifted to
|
||||
// `h1::payload::Payload` unless the payload is needing a read, in which case it
|
||||
// might not have access to the waker and could result in the dispatcher
|
||||
// getting stuck until timeout.
|
||||
//
|
||||
// Reason:
|
||||
// Self wake up when there is payload would waste poll and/or result in
|
||||
// over read.
|
||||
//
|
||||
// Case:
|
||||
// When payload is (partial) dropped by user there is no need to do
|
||||
// read anymore. At this case read_buf could always remain beyond
|
||||
// MAX_BUFFER_SIZE and self wake up would be busy poll dispatcher and
|
||||
// waste resources.
|
||||
Some(ref p) if p.need_read(cx) != PayloadStatus::Read => {}
|
||||
_ => cx.waker().wake_by_ref(),
|
||||
match this.payload.as_ref().map(|p| p.need_read(cx)) {
|
||||
// Payload consumer is alive but applying backpressure. Wait for its waker.
|
||||
Some(PayloadStatus::Pause) => {}
|
||||
|
||||
// Consumer dropped means drain/discard mode; keep polling to make progress.
|
||||
Some(PayloadStatus::Dropped) | Some(PayloadStatus::Read) | None => {
|
||||
cx.waker().wake_by_ref()
|
||||
}
|
||||
}
|
||||
|
||||
return Ok(false);
|
||||
|
|
@ -1029,7 +1042,11 @@ where
|
|||
|
||||
match tokio_util::io::poll_read_buf(io.as_mut(), cx, this.read_buf) {
|
||||
Poll::Ready(Ok(n)) => {
|
||||
this.flags.remove(Flags::FINISHED);
|
||||
// When draining a dropped request payload, keep FINISHED set so the
|
||||
// disconnect/keep-alive decision can be made once the payload is fully drained.
|
||||
if !this.payload.as_ref().is_some_and(|pl| pl.is_dropped()) {
|
||||
this.flags.remove(Flags::FINISHED);
|
||||
}
|
||||
|
||||
if n == 0 {
|
||||
return Ok(true);
|
||||
|
|
@ -1244,6 +1261,7 @@ where
|
|||
// disconnect if keep-alive is not enabled
|
||||
if inner_p.flags.contains(Flags::FINISHED)
|
||||
&& !inner_p.flags.contains(Flags::KEEP_ALIVE)
|
||||
&& inner_p.payload.is_none()
|
||||
{
|
||||
inner_p.flags.remove(Flags::FINISHED);
|
||||
inner_p.flags.insert(Flags::SHUTDOWN);
|
||||
|
|
|
|||
|
|
@ -1017,6 +1017,128 @@ async fn handler_drop_payload() {
|
|||
.await;
|
||||
}
|
||||
|
||||
// Handler drops request payload without reading it. Server should keep reading and discarding the
|
||||
// rest of the request body so clients that do not read the response until they've finished
|
||||
// writing the request (like `requests` in Python) do not deadlock.
|
||||
// ref. https://github.com/actix/actix-web/issues/2972
|
||||
#[actix_rt::test]
|
||||
async fn handler_drop_payload_drains_body() {
|
||||
let _ = env_logger::try_init();
|
||||
|
||||
let mut buf = TestSeqBuffer::new(http_msg(
|
||||
r"
|
||||
POST /drop-payload HTTP/1.1
|
||||
Transfer-Encoding: chunked
|
||||
|
||||
",
|
||||
));
|
||||
|
||||
let services = HttpFlow::new(
|
||||
drop_payload_service(),
|
||||
ExpectHandler,
|
||||
None::<UpgradeHandler>,
|
||||
);
|
||||
|
||||
let h1 = Dispatcher::new(
|
||||
buf.clone(),
|
||||
services,
|
||||
ServiceConfig::default(),
|
||||
None,
|
||||
OnConnectData::default(),
|
||||
);
|
||||
pin!(h1);
|
||||
|
||||
lazy(|cx| {
|
||||
assert!(h1.as_mut().poll(cx).is_pending());
|
||||
|
||||
let mut res = BytesMut::from(buf.take_write_buf().as_ref());
|
||||
stabilize_date_header(&mut res);
|
||||
let res = &res[..];
|
||||
|
||||
let exp = http_msg(
|
||||
r"
|
||||
HTTP/1.1 200 OK
|
||||
content-length: 15
|
||||
date: Thu, 01 Jan 1970 12:34:56 UTC
|
||||
|
||||
payload dropped
|
||||
",
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
res,
|
||||
exp,
|
||||
"\nexpected response not in write buffer:\n\
|
||||
response: {:?}\n\
|
||||
expected: {:?}",
|
||||
String::from_utf8_lossy(res),
|
||||
String::from_utf8_lossy(&exp)
|
||||
);
|
||||
})
|
||||
.await;
|
||||
|
||||
// stream a body larger than the dispatcher read buffer limit; it should still be drained
|
||||
// (read + decoded + discarded) without stalling.
|
||||
for _ in 0..32 {
|
||||
let data = vec![b'a'; 8192];
|
||||
let mut chunk = BytesMut::new();
|
||||
chunk.extend_from_slice(format!("{:x}\r\n", data.len()).as_bytes());
|
||||
chunk.extend_from_slice(&data);
|
||||
chunk.extend_from_slice(b"\r\n");
|
||||
|
||||
buf.extend_read_buf(chunk);
|
||||
|
||||
lazy(|cx| {
|
||||
assert!(h1.as_mut().poll(cx).is_pending());
|
||||
assert!(buf.take_write_buf().is_empty());
|
||||
assert!(buf.read_buf().is_empty());
|
||||
})
|
||||
.await;
|
||||
}
|
||||
|
||||
// terminating chunk
|
||||
buf.extend_read_buf(b"0\r\n\r\n");
|
||||
|
||||
lazy(|cx| {
|
||||
assert!(h1.as_mut().poll(cx).is_pending());
|
||||
assert!(buf.take_write_buf().is_empty());
|
||||
assert!(buf.read_buf().is_empty());
|
||||
})
|
||||
.await;
|
||||
|
||||
// connection should be able to accept another request after draining the previous body
|
||||
buf.extend_read_buf(http_msg("GET /drop-payload HTTP/1.1"));
|
||||
|
||||
lazy(|cx| {
|
||||
assert!(h1.as_mut().poll(cx).is_pending());
|
||||
|
||||
let mut res = BytesMut::from(buf.take_write_buf().as_ref());
|
||||
stabilize_date_header(&mut res);
|
||||
let res = &res[..];
|
||||
|
||||
let exp = http_msg(
|
||||
r"
|
||||
HTTP/1.1 200 OK
|
||||
content-length: 15
|
||||
date: Thu, 01 Jan 1970 12:34:56 UTC
|
||||
|
||||
payload dropped
|
||||
",
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
res,
|
||||
exp,
|
||||
"\nexpected response not in write buffer:\n\
|
||||
response: {:?}\n\
|
||||
expected: {:?}",
|
||||
String::from_utf8_lossy(res),
|
||||
String::from_utf8_lossy(&exp)
|
||||
);
|
||||
})
|
||||
.await;
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn allow_half_closed() {
|
||||
let buf = TestSeqBuffer::new(http_msg("GET / HTTP/1.1"));
|
||||
|
|
|
|||
|
|
@ -111,7 +111,13 @@ pub(crate) trait MessageType: Sized {
|
|||
|
||||
// Connection
|
||||
match conn_type {
|
||||
ConnectionType::Upgrade => dst.put_slice(b"connection: upgrade\r\n"),
|
||||
ConnectionType::Upgrade => {
|
||||
if camel_case {
|
||||
dst.put_slice(b"Connection: Upgrade\r\n")
|
||||
} else {
|
||||
dst.put_slice(b"connection: upgrade\r\n")
|
||||
}
|
||||
}
|
||||
ConnectionType::KeepAlive if version < Version::HTTP_11 => {
|
||||
if camel_case {
|
||||
dst.put_slice(b"Connection: keep-alive\r\n")
|
||||
|
|
@ -580,6 +586,16 @@ mod tests {
|
|||
assert!(data.contains("Date: date\r\n"));
|
||||
assert!(data.contains("Upgrade-Insecure-Requests: 1\r\n"));
|
||||
|
||||
let _ = head.encode_headers(
|
||||
&mut bytes,
|
||||
Version::HTTP_11,
|
||||
BodySize::None,
|
||||
ConnectionType::Upgrade,
|
||||
&ServiceConfig::default(),
|
||||
);
|
||||
let data = String::from_utf8(Vec::from(bytes.split().freeze().as_ref())).unwrap();
|
||||
assert!(data.contains("Connection: Upgrade\r\n"));
|
||||
|
||||
let _ = head.encode_headers(
|
||||
&mut bytes,
|
||||
Version::HTTP_11,
|
||||
|
|
|
|||
|
|
@ -133,6 +133,11 @@ impl PayloadSender {
|
|||
PayloadStatus::Dropped
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn is_dropped(&self) -> bool {
|
||||
self.inner.strong_count() == 0
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
|
|
|
|||
|
|
@ -61,14 +61,15 @@ impl RequestHead {
|
|||
&mut self.headers
|
||||
}
|
||||
|
||||
/// Is to uppercase headers with Camel-Case.
|
||||
/// Default is `false`
|
||||
/// Returns whether headers should be sent in Camel-Case.
|
||||
///
|
||||
/// Default is `false`.
|
||||
#[inline]
|
||||
pub fn camel_case_headers(&self) -> bool {
|
||||
self.flags.contains(Flags::CAMEL_CASE)
|
||||
}
|
||||
|
||||
/// Set `true` to send headers which are formatted as Camel-Case.
|
||||
/// Sets whether to send headers formatted as Camel-Case.
|
||||
#[inline]
|
||||
pub fn set_camel_case_headers(&mut self, val: bool) {
|
||||
if val {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
#![cfg(feature = "rustls-0_23")]
|
||||
|
||||
extern crate tls_openssl as openssl;
|
||||
extern crate tls_rustls_023 as rustls;
|
||||
|
||||
use std::{
|
||||
|
|
@ -22,6 +23,7 @@ use actix_rt::{net::TcpStream as RtTcpStream, pin};
|
|||
use actix_service::{fn_factory_with_config, fn_service};
|
||||
use actix_tls::{accept::rustls_0_23::TlsStream, connect::rustls_0_23::webpki_roots_cert_store};
|
||||
use actix_utils::future::{err, ok, poll_fn};
|
||||
use awc::{Client, Connector};
|
||||
use bytes::{Bytes, BytesMut};
|
||||
use derive_more::{Display, Error};
|
||||
use futures_core::{ready, Stream};
|
||||
|
|
@ -79,6 +81,21 @@ fn tls_config_h2() -> RustlsServerConfig {
|
|||
tls_config_with_alpn(&[H2_ALPN_PROTOCOL])
|
||||
}
|
||||
|
||||
fn h1_client() -> Client {
|
||||
use openssl::ssl::{SslConnector, SslMethod, SslVerifyMode};
|
||||
|
||||
let mut builder = SslConnector::builder(SslMethod::tls()).unwrap();
|
||||
builder.set_verify(SslVerifyMode::NONE);
|
||||
builder.set_alpn_protos(b"\x08http/1.1").unwrap();
|
||||
|
||||
let connector = Connector::new()
|
||||
.conn_lifetime(Duration::from_secs(0))
|
||||
.timeout(Duration::from_millis(30_000))
|
||||
.openssl(builder.build());
|
||||
|
||||
Client::builder().connector(connector).finish()
|
||||
}
|
||||
|
||||
pub fn get_negotiated_alpn_protocol(
|
||||
addr: SocketAddr,
|
||||
client_alpn_protocol: &[u8],
|
||||
|
|
@ -106,21 +123,22 @@ pub fn get_negotiated_alpn_protocol(
|
|||
|
||||
#[actix_rt::test]
|
||||
async fn h1() -> io::Result<()> {
|
||||
let srv = test_server(move || {
|
||||
let mut srv = test_server(move || {
|
||||
HttpService::build()
|
||||
.h1(|_| ok::<_, Error>(Response::ok()))
|
||||
.rustls_0_23(tls_config_h1())
|
||||
})
|
||||
.await;
|
||||
|
||||
let response = srv.sget("/").send().await.unwrap();
|
||||
let response = h1_client().get(srv.surl("/")).send().await.unwrap();
|
||||
assert!(response.status().is_success());
|
||||
srv.stop().await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn h2() -> io::Result<()> {
|
||||
let srv = test_server(move || {
|
||||
let mut srv = test_server(move || {
|
||||
HttpService::build()
|
||||
.h2(|_| ok::<_, Error>(Response::ok()))
|
||||
.rustls_0_23(tls_config_h2())
|
||||
|
|
@ -129,12 +147,13 @@ async fn h2() -> io::Result<()> {
|
|||
|
||||
let response = srv.sget("/").send().await.unwrap();
|
||||
assert!(response.status().is_success());
|
||||
srv.stop().await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn h1_1() -> io::Result<()> {
|
||||
let srv = test_server(move || {
|
||||
let mut srv = test_server(move || {
|
||||
HttpService::build()
|
||||
.h1(|req: Request| {
|
||||
assert!(req.peer_addr().is_some());
|
||||
|
|
@ -145,14 +164,15 @@ async fn h1_1() -> io::Result<()> {
|
|||
})
|
||||
.await;
|
||||
|
||||
let response = srv.sget("/").send().await.unwrap();
|
||||
let response = h1_client().get(srv.surl("/")).send().await.unwrap();
|
||||
assert!(response.status().is_success());
|
||||
srv.stop().await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn h2_1() -> io::Result<()> {
|
||||
let srv = test_server(move || {
|
||||
let mut srv = test_server(move || {
|
||||
HttpService::build()
|
||||
.finish(|req: Request| {
|
||||
assert!(req.peer_addr().is_some());
|
||||
|
|
@ -168,12 +188,13 @@ async fn h2_1() -> io::Result<()> {
|
|||
|
||||
let response = srv.sget("/").send().await.unwrap();
|
||||
assert!(response.status().is_success());
|
||||
srv.stop().await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn h2_tcp_nodelay_override_true() -> io::Result<()> {
|
||||
let srv = test_server(move || {
|
||||
let mut srv = test_server(move || {
|
||||
HttpService::build()
|
||||
.tcp_nodelay(true)
|
||||
.on_connect_ext(|io: &TlsStream<RtTcpStream>, data| {
|
||||
|
|
@ -189,12 +210,13 @@ async fn h2_tcp_nodelay_override_true() -> io::Result<()> {
|
|||
|
||||
let response = srv.sget("/").send().await.unwrap();
|
||||
assert!(response.status().is_success());
|
||||
srv.stop().await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn h2_tcp_nodelay_override_false() -> io::Result<()> {
|
||||
let srv = test_server(move || {
|
||||
let mut srv = test_server(move || {
|
||||
HttpService::build()
|
||||
.tcp_nodelay(false)
|
||||
.on_connect_ext(|io: &TlsStream<RtTcpStream>, data| {
|
||||
|
|
@ -210,6 +232,7 @@ async fn h2_tcp_nodelay_override_false() -> io::Result<()> {
|
|||
|
||||
let response = srv.sget("/").send().await.unwrap();
|
||||
assert!(response.status().is_success());
|
||||
srv.stop().await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
|
@ -231,12 +254,13 @@ async fn h2_body1() -> io::Result<()> {
|
|||
|
||||
let body = srv.load_body(response).await.unwrap();
|
||||
assert_eq!(&body, data.as_bytes());
|
||||
srv.stop().await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn h2_content_length() {
|
||||
let srv = test_server(move || {
|
||||
let mut srv = test_server(move || {
|
||||
HttpService::build()
|
||||
.h2(|req: Request| {
|
||||
let indx: usize = req.uri().path()[1..].parse().unwrap();
|
||||
|
|
@ -294,6 +318,8 @@ async fn h2_content_length() {
|
|||
assert_eq!(response.headers().get(&header), Some(&value));
|
||||
}
|
||||
}
|
||||
|
||||
srv.stop().await;
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
|
|
@ -336,6 +362,7 @@ async fn h2_headers() {
|
|||
// read response
|
||||
let bytes = srv.load_body(response).await.unwrap();
|
||||
assert_eq!(bytes, Bytes::from(data2));
|
||||
srv.stop().await;
|
||||
}
|
||||
|
||||
const STR: &str = "Hello World Hello World Hello World Hello World Hello World \
|
||||
|
|
@ -375,6 +402,7 @@ async fn h2_body2() {
|
|||
// read response
|
||||
let bytes = srv.load_body(response).await.unwrap();
|
||||
assert_eq!(bytes, Bytes::from_static(STR.as_ref()));
|
||||
srv.stop().await;
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
|
|
@ -401,6 +429,7 @@ async fn h2_head_empty() {
|
|||
// read response
|
||||
let bytes = srv.load_body(response).await.unwrap();
|
||||
assert!(bytes.is_empty());
|
||||
srv.stop().await;
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
|
|
@ -426,11 +455,12 @@ async fn h2_head_binary() {
|
|||
// read response
|
||||
let bytes = srv.load_body(response).await.unwrap();
|
||||
assert!(bytes.is_empty());
|
||||
srv.stop().await;
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn h2_head_binary2() {
|
||||
let srv = test_server(move || {
|
||||
let mut srv = test_server(move || {
|
||||
HttpService::build()
|
||||
.h2(|_| ok::<_, Infallible>(Response::ok().set_body(STR)))
|
||||
.rustls_0_23(tls_config_h2())
|
||||
|
|
@ -447,6 +477,8 @@ async fn h2_head_binary2() {
|
|||
.unwrap();
|
||||
assert_eq!(format!("{}", STR.len()), len.to_str().unwrap());
|
||||
}
|
||||
|
||||
srv.stop().await;
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
|
|
@ -469,6 +501,7 @@ async fn h2_body_length() {
|
|||
// read response
|
||||
let bytes = srv.load_body(response).await.unwrap();
|
||||
assert_eq!(bytes, Bytes::from_static(STR.as_ref()));
|
||||
srv.stop().await;
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
|
|
@ -496,6 +529,7 @@ async fn h2_body_chunked_explicit() {
|
|||
|
||||
// decode
|
||||
assert_eq!(bytes, Bytes::from_static(STR.as_ref()));
|
||||
srv.stop().await;
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
|
|
@ -525,6 +559,7 @@ async fn h2_response_http_error_handling() {
|
|||
bytes,
|
||||
Bytes::from_static(b"error processing HTTP: failed to parse header value")
|
||||
);
|
||||
srv.stop().await;
|
||||
}
|
||||
|
||||
#[derive(Debug, Display, Error)]
|
||||
|
|
@ -552,6 +587,7 @@ async fn h2_service_error() {
|
|||
// read response
|
||||
let bytes = srv.load_body(response).await.unwrap();
|
||||
assert_eq!(bytes, Bytes::from_static(b"error"));
|
||||
srv.stop().await;
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
|
|
@ -563,12 +599,13 @@ async fn h1_service_error() {
|
|||
})
|
||||
.await;
|
||||
|
||||
let response = srv.sget("/").send().await.unwrap();
|
||||
let response = h1_client().get(srv.surl("/")).send().await.unwrap();
|
||||
assert_eq!(response.status(), http::StatusCode::BAD_REQUEST);
|
||||
|
||||
// read response
|
||||
let bytes = srv.load_body(response).await.unwrap();
|
||||
assert_eq!(bytes, Bytes::from_static(b"error"));
|
||||
srv.stop().await;
|
||||
}
|
||||
|
||||
const H2_ALPN_PROTOCOL: &[u8] = b"h2";
|
||||
|
|
@ -577,7 +614,7 @@ const CUSTOM_ALPN_PROTOCOL: &[u8] = b"custom";
|
|||
|
||||
#[actix_rt::test]
|
||||
async fn alpn_h1() -> io::Result<()> {
|
||||
let srv = test_server(move || {
|
||||
let mut srv = test_server(move || {
|
||||
let mut config = tls_config_h1();
|
||||
config.alpn_protocols.push(CUSTOM_ALPN_PROTOCOL.to_vec());
|
||||
HttpService::build()
|
||||
|
|
@ -591,15 +628,16 @@ async fn alpn_h1() -> io::Result<()> {
|
|||
Some(CUSTOM_ALPN_PROTOCOL.to_vec())
|
||||
);
|
||||
|
||||
let response = srv.sget("/").send().await.unwrap();
|
||||
let response = h1_client().get(srv.surl("/")).send().await.unwrap();
|
||||
assert!(response.status().is_success());
|
||||
|
||||
srv.stop().await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn alpn_h2() -> io::Result<()> {
|
||||
let srv = test_server(move || {
|
||||
let mut srv = test_server(move || {
|
||||
let mut config = tls_config_h2();
|
||||
config.alpn_protocols.push(CUSTOM_ALPN_PROTOCOL.to_vec());
|
||||
HttpService::build()
|
||||
|
|
@ -620,12 +658,13 @@ async fn alpn_h2() -> io::Result<()> {
|
|||
let response = srv.sget("/").send().await.unwrap();
|
||||
assert!(response.status().is_success());
|
||||
|
||||
srv.stop().await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn alpn_h2_1() -> io::Result<()> {
|
||||
let srv = test_server(move || {
|
||||
let mut srv = test_server(move || {
|
||||
let mut config = tls_config();
|
||||
config.alpn_protocols.push(CUSTOM_ALPN_PROTOCOL.to_vec());
|
||||
HttpService::build()
|
||||
|
|
@ -650,5 +689,6 @@ async fn alpn_h2_1() -> io::Result<()> {
|
|||
let response = srv.sget("/").send().await.unwrap();
|
||||
assert!(response.status().is_success());
|
||||
|
||||
srv.stop().await;
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -962,7 +962,13 @@ async fn h2_flow_control_window_sizes() {
|
|||
let mut srv = test_server(|| {
|
||||
HttpService::build()
|
||||
.keep_alive(KeepAlive::Disabled)
|
||||
.finish(|_req: Request| ok::<_, Infallible>(Response::ok()))
|
||||
.finish(|mut req: Request| async move {
|
||||
while let Some(item) = req.take_payload().next().await {
|
||||
item?;
|
||||
}
|
||||
|
||||
Ok::<_, Error>(Response::ok())
|
||||
})
|
||||
.tcp_auto_h2c()
|
||||
})
|
||||
.await;
|
||||
|
|
@ -992,8 +998,8 @@ async fn h2_flow_control_window_sizes() {
|
|||
loop {
|
||||
let cap = std::future::poll_fn(|cx| send.poll_capacity(cx))
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
.expect("request stream closed before flow control capacity became available")
|
||||
.expect("failed polling flow control capacity");
|
||||
|
||||
if cap >= 1024 * 1024 {
|
||||
break cap;
|
||||
|
|
@ -1001,7 +1007,7 @@ async fn h2_flow_control_window_sizes() {
|
|||
}
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
.expect("timed out waiting for flow control capacity");
|
||||
|
||||
assert!(
|
||||
cap >= 1024 * 1024,
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@
|
|||
|
||||
## Unreleased
|
||||
|
||||
## 0.5.4
|
||||
|
||||
- Minimum supported Rust version (MSRV) is now 1.88.
|
||||
- Support `deserialize_any` in `PathDeserializer` (enables derived `#[serde(untagged)]` enums in path segments). [#2881]
|
||||
- Fix stale path segment indices after path rewrites, preventing out-of-bounds access during extraction. [#3562]
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "actix-router"
|
||||
version = "0.5.3"
|
||||
version = "0.5.4"
|
||||
authors = [
|
||||
"Nikolay Kim <fafhrd91@gmail.com>",
|
||||
"Ali MJ Al-Nasrawy <alimjalnasrawy@gmail.com>",
|
||||
|
|
|
|||
|
|
@ -1,11 +1,16 @@
|
|||
use crate::Path;
|
||||
|
||||
// TODO: this trait is necessary, document it
|
||||
// see impl Resource for ServiceRequest
|
||||
/// Abstraction over types that can provide a mutable [`Path`] for routing.
|
||||
///
|
||||
/// This trait is used by the router to extract the request path in a uniform way across different
|
||||
/// request types (e.g., Actix Web's `ServiceRequest`). Implementors return a mutable [`Path`]
|
||||
/// wrapper so routing can read and potentially normalize/parse the path without requiring the
|
||||
/// original request type.
|
||||
pub trait Resource {
|
||||
/// Type of resource's path returned in `resource_path`.
|
||||
type Path: ResourcePath;
|
||||
|
||||
/// Returns a mutable reference to the path wrapper used by the router.
|
||||
fn resource_path(&mut self) -> &mut Path<Self::Path>;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,14 @@
|
|||
|
||||
## Unreleased
|
||||
|
||||
- Panic when calling `Route::to()` or `Route::service()` after `Route::wrap()` to prevent silently dropping route middleware. [#3944]
|
||||
- Fix `HttpRequest::{match_pattern,match_name}` reporting path-only matches when route guards disambiguate overlapping resources. [#3346]
|
||||
|
||||
[#3944]: https://github.com/actix/actix-web/pull/3944
|
||||
[#3346]: https://github.com/actix/actix-web/issues/3346
|
||||
|
||||
## 4.13.0
|
||||
|
||||
- Minimum supported Rust version (MSRV) is now 1.88.
|
||||
- Improve HTTP/2 upload throughput by increasing default flow control window sizes. [#3638]
|
||||
- Add `HttpServer::{h2_initial_window_size, h2_initial_connection_window_size}` methods for tuning. [#3638]
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "actix-web"
|
||||
version = "4.12.1"
|
||||
version = "4.13.0"
|
||||
description = "Actix Web is a powerful, pragmatic, and extremely fast web framework for Rust"
|
||||
authors = ["Nikolay Kim <fafhrd91@gmail.com>", "Rob Ede <robjtede@icloud.com>"]
|
||||
keywords = ["actix", "http", "web", "framework", "async"]
|
||||
|
|
@ -137,8 +137,8 @@ actix-service = "2"
|
|||
actix-tls = { version = "3.4", default-features = false, optional = true }
|
||||
actix-utils = "3"
|
||||
|
||||
actix-http = "3.11.2"
|
||||
actix-router = { version = "0.5.3", default-features = false, features = ["http"] }
|
||||
actix-http = "3.12.0"
|
||||
actix-router = { version = "0.5.4", default-features = false, features = ["http"] }
|
||||
actix-web-codegen = { version = "4.3", optional = true, default-features = false }
|
||||
|
||||
bytes = "1"
|
||||
|
|
|
|||
|
|
@ -8,10 +8,10 @@
|
|||
<!-- prettier-ignore-start -->
|
||||
|
||||
[](https://crates.io/crates/actix-web)
|
||||
[](https://docs.rs/actix-web/4.12.1)
|
||||
[](https://docs.rs/actix-web/4.13.0)
|
||||

|
||||

|
||||
[](https://deps.rs/crate/actix-web/4.12.1)
|
||||
[](https://deps.rs/crate/actix-web/4.13.0)
|
||||
<br />
|
||||
[](https://github.com/actix/actix-web/actions/workflows/ci.yml)
|
||||
[](https://codecov.io/gh/actix/actix-web)
|
||||
|
|
@ -45,6 +45,10 @@ To enable faster release iterations, we mark some features as experimental.
|
|||
These features are prefixed with `experimental` and a breaking change may happen at any release.
|
||||
Please use them in a production environment at your own risk.
|
||||
|
||||
- `experimental-introspection`: exposes route and method reporting helpers for local diagnostics
|
||||
and tooling. See [`examples/introspection.rs`](examples/introspection.rs) and
|
||||
[`examples/introspection_multi_servers.rs`](examples/introspection_multi_servers.rs).
|
||||
|
||||
## Documentation
|
||||
|
||||
- [Website & User Guide](https://actix.rs)
|
||||
|
|
|
|||
|
|
@ -228,6 +228,8 @@ where
|
|||
let inner = Rc::get_mut(&mut req.inner).unwrap();
|
||||
inner.path.get_mut().update(&head.uri);
|
||||
inner.path.reset();
|
||||
inner.resource_path.clear();
|
||||
inner.resource_path_matched = false;
|
||||
inner.head = head;
|
||||
inner.conn_data = conn_data;
|
||||
inner.extensions = extensions;
|
||||
|
|
@ -332,7 +334,15 @@ impl Service<ServiceRequest> for AppRouting {
|
|||
guards.iter().all(|guard| guard.check(&guard_ctx))
|
||||
});
|
||||
|
||||
if let Some((srv, _info)) = res {
|
||||
if let Some((srv, info)) = res {
|
||||
req.push_resource_id(info.0);
|
||||
|
||||
let matched = req
|
||||
.resource_map()
|
||||
.is_resource_path_match(req.resource_id_path());
|
||||
|
||||
req.mark_resource_path(matched);
|
||||
|
||||
srv.call(req)
|
||||
} else {
|
||||
self.default.call(req)
|
||||
|
|
|
|||
|
|
@ -74,6 +74,9 @@
|
|||
//! To enable faster release iterations, we mark some features as experimental.
|
||||
//! These features are prefixed with `experimental` and a breaking change may happen at any release.
|
||||
//! Please use them in a production environment at your own risk.
|
||||
//!
|
||||
//! - `experimental-introspection` - route and method reporting utilities for local diagnostics
|
||||
//! and tooling. See `examples/introspection.rs` and `examples/introspection_multi_servers.rs`.
|
||||
|
||||
#![doc(html_logo_url = "https://actix.rs/img/logo.png")]
|
||||
#![doc(html_favicon_url = "https://actix.rs/favicon.ico")]
|
||||
|
|
|
|||
|
|
@ -42,6 +42,8 @@ pub struct HttpRequest {
|
|||
pub(crate) struct HttpRequestInner {
|
||||
pub(crate) head: Message<RequestHead>,
|
||||
pub(crate) path: Path<Url>,
|
||||
pub(crate) resource_path: SmallVec<[u16; 4]>,
|
||||
pub(crate) resource_path_matched: bool,
|
||||
pub(crate) app_data: SmallVec<[Rc<Extensions>; 4]>,
|
||||
pub(crate) conn_data: Option<Rc<Extensions>>,
|
||||
pub(crate) extensions: Rc<RefCell<Extensions>>,
|
||||
|
|
@ -65,6 +67,8 @@ impl HttpRequest {
|
|||
inner: Rc::new(HttpRequestInner {
|
||||
head,
|
||||
path,
|
||||
resource_path: SmallVec::new(),
|
||||
resource_path_matched: false,
|
||||
app_state,
|
||||
app_data: data,
|
||||
conn_data,
|
||||
|
|
@ -180,6 +184,26 @@ impl HttpRequest {
|
|||
&mut Rc::get_mut(&mut self.inner).unwrap().path
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn push_resource_id(&mut self, id: u16) {
|
||||
Rc::get_mut(&mut self.inner).unwrap().resource_path.push(id);
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn mark_resource_path(&mut self, is_matched: bool) {
|
||||
Rc::get_mut(&mut self.inner).unwrap().resource_path_matched = is_matched;
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn resource_path(&self) -> &[u16] {
|
||||
&self.inner.resource_path
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn is_resource_path_matched(&self) -> bool {
|
||||
self.inner.resource_path_matched
|
||||
}
|
||||
|
||||
/// The resource definition pattern that matched the path. Useful for logging and metrics.
|
||||
///
|
||||
/// For example, when a resource with pattern `/user/{id}/profile` is defined and a call is made
|
||||
|
|
@ -188,6 +212,15 @@ impl HttpRequest {
|
|||
/// Returns a None when no resource is fully matched, including default services.
|
||||
#[inline]
|
||||
pub fn match_pattern(&self) -> Option<String> {
|
||||
if self.is_resource_path_matched() {
|
||||
if let Some(pattern) = self
|
||||
.resource_map()
|
||||
.match_pattern_by_resource_path(self.resource_path())
|
||||
{
|
||||
return Some(pattern);
|
||||
}
|
||||
}
|
||||
|
||||
self.resource_map().match_pattern(self.path())
|
||||
}
|
||||
|
||||
|
|
@ -196,6 +229,15 @@ impl HttpRequest {
|
|||
/// Returns a None when no resource is fully matched, including default services.
|
||||
#[inline]
|
||||
pub fn match_name(&self) -> Option<&str> {
|
||||
if self.is_resource_path_matched() {
|
||||
if let Some(name) = self
|
||||
.resource_map()
|
||||
.match_name_by_resource_path(self.resource_path())
|
||||
{
|
||||
return Some(name);
|
||||
}
|
||||
}
|
||||
|
||||
self.resource_map().match_name(self.path())
|
||||
}
|
||||
|
||||
|
|
@ -633,6 +675,7 @@ mod tests {
|
|||
use super::*;
|
||||
use crate::{
|
||||
dev::{ResourceDef, Service},
|
||||
guard,
|
||||
http::{header, StatusCode},
|
||||
test::{self, call_service, init_service, read_body, TestRequest},
|
||||
web, App, HttpResponse,
|
||||
|
|
@ -1019,6 +1062,44 @@ mod tests {
|
|||
assert_eq!(res.status(), StatusCode::OK);
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn extract_path_pattern_with_guards() {
|
||||
let srv = init_service(
|
||||
App::new().service(
|
||||
web::scope("/widgets")
|
||||
.service(
|
||||
web::resource("/{id}")
|
||||
.name("get_widget")
|
||||
.guard(guard::Get())
|
||||
.to(|req: HttpRequest| {
|
||||
assert_eq!(req.match_pattern(), Some("/widgets/{id}".to_owned()));
|
||||
assert_eq!(req.match_name(), Some("get_widget"));
|
||||
HttpResponse::Ok().finish()
|
||||
}),
|
||||
)
|
||||
.service(
|
||||
web::resource("/action")
|
||||
.name("widget_action")
|
||||
.guard(guard::Post())
|
||||
.to(|req: HttpRequest| {
|
||||
assert_eq!(req.match_pattern(), Some("/widgets/action".to_owned()));
|
||||
assert_eq!(req.match_name(), Some("widget_action"));
|
||||
HttpResponse::Ok().finish()
|
||||
}),
|
||||
),
|
||||
),
|
||||
)
|
||||
.await;
|
||||
|
||||
let req = TestRequest::get().uri("/widgets/42").to_request();
|
||||
let res = call_service(&srv, req).await;
|
||||
assert_eq!(res.status(), StatusCode::OK);
|
||||
|
||||
let req = TestRequest::post().uri("/widgets/action").to_request();
|
||||
let res = call_service(&srv, req).await;
|
||||
assert_eq!(res.status(), StatusCode::OK);
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn extract_path_pattern_complex() {
|
||||
let srv = init_service(
|
||||
|
|
|
|||
|
|
@ -240,10 +240,40 @@ impl ResourceMap {
|
|||
)
|
||||
}
|
||||
|
||||
pub(crate) fn is_resource_path_match(&self, resource_path: &[u16]) -> bool {
|
||||
self.find_node_by_resource_path(resource_path)
|
||||
.is_some_and(|node| node.nodes.is_none())
|
||||
}
|
||||
|
||||
pub(crate) fn match_name_by_resource_path(&self, resource_path: &[u16]) -> Option<&str> {
|
||||
self.find_node_by_resource_path(resource_path)?
|
||||
.pattern
|
||||
.name()
|
||||
}
|
||||
|
||||
pub(crate) fn match_pattern_by_resource_path(&self, resource_path: &[u16]) -> Option<String> {
|
||||
self.find_node_by_resource_path(resource_path)?
|
||||
.root_rmap_fn(String::with_capacity(AVG_PATH_LEN), |mut acc, node| {
|
||||
let pattern = node.pattern.pattern()?;
|
||||
acc.push_str(pattern);
|
||||
Some(acc)
|
||||
})
|
||||
}
|
||||
|
||||
fn find_matching_node(&self, path: &str) -> Option<&ResourceMap> {
|
||||
self._find_matching_node(path).flatten()
|
||||
}
|
||||
|
||||
fn find_node_by_resource_path(&self, resource_path: &[u16]) -> Option<&ResourceMap> {
|
||||
let mut node = self;
|
||||
|
||||
for id in resource_path {
|
||||
node = node.nodes.as_ref()?.get(*id as usize)?;
|
||||
}
|
||||
|
||||
Some(node)
|
||||
}
|
||||
|
||||
/// Returns `None` if root pattern doesn't match;
|
||||
/// `Some(None)` if root pattern matches but there is no matching child pattern.
|
||||
/// Don't search sideways when `Some(none)` is returned.
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ use crate::{
|
|||
pub struct Route {
|
||||
service: BoxedHttpServiceFactory,
|
||||
guards: Rc<Vec<Box<dyn Guard>>>,
|
||||
wrapped: bool,
|
||||
}
|
||||
|
||||
impl Route {
|
||||
|
|
@ -34,6 +35,7 @@ impl Route {
|
|||
Ok(req.into_response(HttpResponse::NotFound()))
|
||||
})),
|
||||
guards: Rc::new(Vec::new()),
|
||||
wrapped: false,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -42,6 +44,17 @@ impl Route {
|
|||
/// `mw` is a middleware component (type), that can modify the requests and responses handled by
|
||||
/// this `Route`.
|
||||
///
|
||||
/// This middleware wraps the currently configured route service. Call this method after
|
||||
/// [`Route::to`] or [`Route::service`] so the middleware is applied to the final handler.
|
||||
///
|
||||
/// # Examples
|
||||
/// ```
|
||||
/// # use actix_web::{web, HttpResponse, middleware};
|
||||
/// web::get()
|
||||
/// .to(|| async { HttpResponse::Ok() })
|
||||
/// .wrap(middleware::Logger::default());
|
||||
/// ```
|
||||
///
|
||||
/// See [`App::wrap`](crate::App::wrap) for more details.
|
||||
#[doc(alias = "middleware")]
|
||||
#[doc(alias = "use")] // nodejs terminology
|
||||
|
|
@ -59,12 +72,24 @@ impl Route {
|
|||
Route {
|
||||
service: boxed::factory(apply(Compat::new(mw), self.service)),
|
||||
guards: self.guards,
|
||||
wrapped: true,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn take_guards(&mut self) -> Vec<Box<dyn Guard>> {
|
||||
mem::take(Rc::get_mut(&mut self.guards).unwrap())
|
||||
}
|
||||
|
||||
#[cold]
|
||||
#[inline(never)]
|
||||
#[track_caller]
|
||||
fn panic_after_wrap(replaced: &str, example: &str) -> ! {
|
||||
panic!(
|
||||
"Route middleware was already registered with `.wrap()`. \
|
||||
Calling `.{replaced}()` now would replace the wrapped service and silently drop middleware. \
|
||||
Call `.{replaced}()` before `.wrap()` (for example: `{example}`)."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
impl ServiceFactory<ServiceRequest> for Route {
|
||||
|
|
@ -212,12 +237,21 @@ impl Route {
|
|||
/// .route(web::get().to(index))
|
||||
/// );
|
||||
/// ```
|
||||
///
|
||||
/// # Panics
|
||||
/// Panics if called after [`Route::wrap`], since this would replace the wrapped service and
|
||||
/// silently discard middleware.
|
||||
#[track_caller]
|
||||
pub fn to<F, Args>(mut self, handler: F) -> Self
|
||||
where
|
||||
F: Handler<Args>,
|
||||
Args: FromRequest + 'static,
|
||||
F::Output: Responder + 'static,
|
||||
{
|
||||
if self.wrapped {
|
||||
Self::panic_after_wrap("to", "web::get().to(handler).wrap(mw)");
|
||||
}
|
||||
|
||||
self.service = handler_service(handler);
|
||||
self
|
||||
}
|
||||
|
|
@ -254,6 +288,11 @@ impl Route {
|
|||
/// web::get().service(fn_factory(|| async { Ok(HelloWorld) })),
|
||||
/// );
|
||||
/// ```
|
||||
///
|
||||
/// # Panics
|
||||
/// Panics if called after [`Route::wrap`], since this would replace the wrapped service and
|
||||
/// silently discard middleware.
|
||||
#[track_caller]
|
||||
pub fn service<S, E>(mut self, service_factory: S) -> Self
|
||||
where
|
||||
S: ServiceFactory<
|
||||
|
|
@ -265,6 +304,10 @@ impl Route {
|
|||
> + 'static,
|
||||
E: Into<Error> + 'static,
|
||||
{
|
||||
if self.wrapped {
|
||||
Self::panic_after_wrap("service", "web::get().service(factory).wrap(mw)");
|
||||
}
|
||||
|
||||
self.service = boxed::factory(service_factory.map_err(Into::into));
|
||||
self
|
||||
}
|
||||
|
|
@ -459,4 +502,25 @@ mod tests {
|
|||
Bytes::from_static(b"Goodbye, and thanks for all the fish!")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic(expected = "Route middleware was already registered with `.wrap()`")]
|
||||
fn wrap_before_to_panics() {
|
||||
web::get()
|
||||
.wrap(DefaultHeaders::new().add(("x-test", "x-value")))
|
||||
.to(HttpResponse::Ok);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic(expected = "Route middleware was already registered with `.wrap()`")]
|
||||
fn wrap_before_service_panics() {
|
||||
web::get()
|
||||
.wrap(DefaultHeaders::new().add(("x-test", "x-value")))
|
||||
.service(fn_factory(|| async {
|
||||
Ok::<_, ()>(fn_service(|req: ServiceRequest| async {
|
||||
let (req, _) = req.into_parts();
|
||||
Ok::<_, Infallible>(ServiceResponse::new(req, HttpResponse::Ok().finish()))
|
||||
}))
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -533,7 +533,15 @@ impl Service<ServiceRequest> for ScopeService {
|
|||
guards.iter().all(|guard| guard.check(&guard_ctx))
|
||||
});
|
||||
|
||||
if let Some((srv, _info)) = res {
|
||||
if let Some((srv, info)) = res {
|
||||
req.push_resource_id(info.0);
|
||||
|
||||
let matched = req
|
||||
.resource_map()
|
||||
.is_resource_path_match(req.resource_id_path());
|
||||
|
||||
req.mark_resource_path(matched);
|
||||
|
||||
srv.call(req)
|
||||
} else {
|
||||
self.default.call(req)
|
||||
|
|
|
|||
|
|
@ -321,6 +321,21 @@ impl ServiceRequest {
|
|||
.push(extensions);
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn push_resource_id(&mut self, id: u16) {
|
||||
self.req.push_resource_id(id);
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn mark_resource_path(&mut self, is_matched: bool) {
|
||||
self.req.mark_resource_path(is_matched);
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn resource_id_path(&self) -> &[u16] {
|
||||
self.req.resource_path()
|
||||
}
|
||||
|
||||
/// Creates a context object for use with a routing [guard](crate::guard).
|
||||
#[inline]
|
||||
pub fn guard_ctx(&self) -> GuardContext<'_> {
|
||||
|
|
|
|||
|
|
@ -2,6 +2,12 @@
|
|||
|
||||
## Unreleased
|
||||
|
||||
- Add camel-case header controls to `WebsocketsRequest` via `camel_case_headers()` and `set_camel_case_headers()`. [#3953]
|
||||
|
||||
[#3953]: https://github.com/actix/actix-web/pull/3953
|
||||
|
||||
## 3.8.2
|
||||
|
||||
- Minimum supported Rust version (MSRV) is now 1.88.
|
||||
- Fix empty streaming request bodies being sent with chunked transfer encoding.
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "awc"
|
||||
version = "3.8.1"
|
||||
version = "3.8.2"
|
||||
authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
|
||||
description = "Async HTTP and WebSocket client library"
|
||||
keywords = ["actix", "http", "framework", "async", "web"]
|
||||
|
|
@ -98,7 +98,7 @@ dangerous-h2c = []
|
|||
|
||||
[dependencies]
|
||||
actix-codec = "0.5"
|
||||
actix-http = { version = "3.10", features = ["http2", "ws"] }
|
||||
actix-http = { version = "3.12.0", features = ["http2", "ws"] }
|
||||
actix-rt = { version = "2.1", default-features = false }
|
||||
actix-service = "2"
|
||||
actix-tls = { version = "3.4", features = ["connect", "uri"] }
|
||||
|
|
@ -134,13 +134,13 @@ tls-rustls-0_23 = { package = "rustls", version = "0.23", optional = true, defau
|
|||
hickory-resolver = { version = "0.25", optional = true, features = ["system-config", "tokio"] }
|
||||
|
||||
[dev-dependencies]
|
||||
actix-http = { version = "3.7", features = ["openssl"] }
|
||||
actix-http = { version = "3.12", features = ["openssl"] }
|
||||
actix-http-test = { version = "3", features = ["openssl"] }
|
||||
actix-server = "2"
|
||||
actix-test = { version = "0.1", features = ["openssl", "rustls-0_23"] }
|
||||
actix-tls = { version = "3.4", features = ["openssl", "rustls-0_23"] }
|
||||
actix-utils = "3"
|
||||
actix-web = { version = "4", features = ["openssl"] }
|
||||
actix-web = { version = "4.13", features = ["openssl"] }
|
||||
|
||||
brotli = "8"
|
||||
const-str = "0.5" # TODO(MSRV 1.77): update to 0.6
|
||||
|
|
|
|||
|
|
@ -5,9 +5,9 @@
|
|||
<!-- prettier-ignore-start -->
|
||||
|
||||
[](https://crates.io/crates/awc)
|
||||
[](https://docs.rs/awc/3.8.1)
|
||||
[](https://docs.rs/awc/3.8.2)
|
||||

|
||||
[](https://deps.rs/crate/awc/3.8.1)
|
||||
[](https://deps.rs/crate/awc/3.8.2)
|
||||
[](https://discord.gg/NWpN5mmg3x)
|
||||
|
||||
<!-- prettier-ignore-end -->
|
||||
|
|
|
|||
|
|
@ -245,6 +245,21 @@ impl WebsocketsRequest {
|
|||
self.header(AUTHORIZATION, format!("Bearer {}", token))
|
||||
}
|
||||
|
||||
/// Returns whether headers should be sent in Camel-Case.
|
||||
///
|
||||
/// Default is `false`.
|
||||
#[inline]
|
||||
pub fn camel_case_headers(&self) -> bool {
|
||||
self.head.camel_case_headers()
|
||||
}
|
||||
|
||||
/// Sets whether to send headers formatted as Camel-Case.
|
||||
#[inline]
|
||||
pub fn set_camel_case_headers(mut self, val: bool) -> Self {
|
||||
self.head.set_camel_case_headers(val);
|
||||
self
|
||||
}
|
||||
|
||||
/// Complete request construction and connect to a WebSocket server.
|
||||
pub async fn connect(
|
||||
mut self,
|
||||
|
|
@ -529,6 +544,12 @@ mod tests {
|
|||
let _ = req.connect();
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn camel_case_headers() {
|
||||
let req = Client::new().ws("/").set_camel_case_headers(true);
|
||||
assert!(req.camel_case_headers());
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn basics() {
|
||||
let req = Client::new()
|
||||
|
|
|
|||
Loading…
Reference in New Issue