diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
deleted file mode 100644
index 04f5d9f01a..0000000000
--- a/.github/ISSUE_TEMPLATE/bug_report.md
+++ /dev/null
@@ -1,28 +0,0 @@
----
-name: "Bug report \U0001F41B"
-about: Create a report to help us improve
-title: ''
-labels: C-bug
-assignees: ''
-
----
-
-**Version**
-List the version(s) of `hyper`, and any relevant hyper dependency (such as `h2` if this is related to HTTP/2).
-
-**Platform**
-The output of `uname -a` (UNIX), or version and 32 or 64-bit (Windows)
-
-**Description**
-Enter your issue details here.
-One way to structure the description:
-
-[short summary of the bug]
-
-I tried this code:
-
-[code sample that causes the bug]
-
-I expected to see this happen: [explanation]
-
-Instead, this happened: [explanation]
diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml
new file mode 100644
index 0000000000..23abc6275d
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug_report.yml
@@ -0,0 +1,65 @@
+name: "Bug report š"
+description: Create a report to help us improve
+labels: ["C-bug"]
+body:
+ - type: input
+ id: version
+ attributes:
+ label: Version
+ description: List the version(s) of `hyper`, and any relevant hyper dependency (such as `h2` if this is related to HTTP/2).
+ placeholder: "hyper 1.0.0, h2 0.4.0"
+ validations:
+ required: true
+
+ - type: input
+ id: platform
+ attributes:
+ label: Platform
+ description: The output of `uname -a` (UNIX), or version and 32 or 64-bit (Windows)
+ placeholder: "Linux 5.15.0 x86_64 / Windows 11 64-bit"
+ validations:
+ required: true
+
+ - type: textarea
+ id: summary
+ attributes:
+ label: Summary
+ description: A short summary of the bug
+ placeholder: "Brief description of what went wrong"
+ validations:
+ required: true
+
+ - type: textarea
+ id: code
+ attributes:
+ label: Code Sample
+ description: Code sample that causes the bug (if applicable)
+ render: rust
+ placeholder: |
+ use hyper::...;
+
+ fn main() {
+ // code that reproduces the bug
+ }
+
+ - type: textarea
+ id: expected
+ attributes:
+ label: Expected Behavior
+ description: What you expected to happen
+ validations:
+ required: true
+
+ - type: textarea
+ id: actual
+ attributes:
+ label: Actual Behavior
+ description: What actually happened instead
+ validations:
+ required: true
+
+ - type: textarea
+ id: additional
+ attributes:
+ label: Additional Context
+ description: Any other context about the problem (stack traces, related issues, etc.)
diff --git a/.github/PULL_REQUEST_TEMPLATE b/.github/PULL_REQUEST_TEMPLATE
deleted file mode 100644
index e69de29bb2..0000000000
diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml
index e7e12dc195..5d625ae140 100644
--- a/.github/workflows/CI.yml
+++ b/.github/workflows/CI.yml
@@ -17,6 +17,7 @@ jobs:
runs-on: ubuntu-latest
needs:
- style
+ - lint
- test
- msrv
- miri
@@ -37,7 +38,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
- uses: actions/checkout@v5
+ uses: actions/checkout@v6
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
@@ -51,6 +52,23 @@ jobs:
exit 1
fi
+ lint:
+ name: Linter
+ #needs: [style]
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v6
+
+ - name: Install Rust
+ uses: dtolnay/rust-toolchain@stable
+
+ - uses: Swatinem/rust-cache@v2
+
+ # not --all-targets, lints can't seem to tell integrations tests are tests
+ - name: Clippy
+ run: cargo clippy --features full -- -D warnings
+
test:
name: Test ${{ matrix.rust }} on ${{ matrix.os }}
needs: [style]
@@ -79,7 +97,7 @@ jobs:
steps:
- name: Checkout
- uses: actions/checkout@v5
+ uses: actions/checkout@v6
- name: Install Rust (${{ matrix.rust }})
uses: dtolnay/rust-toolchain@master
@@ -103,7 +121,7 @@ jobs:
steps:
- name: Checkout
- uses: actions/checkout@v5
+ uses: actions/checkout@v6
- uses: dtolnay/rust-toolchain@stable
@@ -133,7 +151,7 @@ jobs:
steps:
- name: Checkout
- uses: actions/checkout@v5
+ uses: actions/checkout@v6
- name: Install Rust
uses: dtolnay/rust-toolchain@nightly
@@ -152,7 +170,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
- uses: actions/checkout@v5
+ uses: actions/checkout@v6
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
@@ -178,7 +196,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
- uses: actions/checkout@v5
+ uses: actions/checkout@v6
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
@@ -203,7 +221,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
- uses: actions/checkout@v5
+ uses: actions/checkout@v6
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
@@ -234,7 +252,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
- uses: actions/checkout@v5
+ uses: actions/checkout@v6
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
@@ -259,7 +277,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
- uses: actions/checkout@v5
+ uses: actions/checkout@v6
- name: Install Rust
uses: dtolnay/rust-toolchain@nightly
@@ -275,17 +293,17 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
- uses: actions/checkout@v5
+ uses: actions/checkout@v6
- name: Install Rust
uses: dtolnay/rust-toolchain@master
with:
- toolchain: nightly-2025-08-06 # Compatible version for cargo-check-external-types
+ toolchain: nightly-2026-03-20 # Compatible version for cargo-check-external-types
- name: Install cargo-check-external-types
uses: taiki-e/cache-cargo-install-action@v2
with:
- tool: cargo-check-external-types@0.3.0
+ tool: cargo-check-external-types@0.5.0
- uses: Swatinem/rust-cache@v2
@@ -297,7 +315,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
- uses: actions/checkout@v5
+ uses: actions/checkout@v6
- name: Install Rust
uses: dtolnay/rust-toolchain@nightly
@@ -317,7 +335,7 @@ jobs:
runs-on: ubuntu-latest
needs: [style]
steps:
- - uses: actions/checkout@v5
+ - uses: actions/checkout@v6
- uses: dtolnay/rust-toolchain@nightly
- uses: dtolnay/rust-toolchain@stable
- uses: taiki-e/install-action@cargo-hack
@@ -330,7 +348,7 @@ jobs:
name: semver
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v5
+ - uses: actions/checkout@v6
- name: Check semver
uses: obi1kenobi/cargo-semver-checks-action@v2
with:
diff --git a/.github/workflows/bench.yml b/.github/workflows/bench.yml
index 60c1a2efb4..af535d942f 100644
--- a/.github/workflows/bench.yml
+++ b/.github/workflows/bench.yml
@@ -14,7 +14,7 @@ jobs:
- end_to_end
- pipeline
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v6
- name: Install Rust
uses: dtolnay/rust-toolchain@nightly
diff --git a/.github/workflows/cargo-audit.yml b/.github/workflows/cargo-audit.yml
index 256991ca76..8486c4d93a 100644
--- a/.github/workflows/cargo-audit.yml
+++ b/.github/workflows/cargo-audit.yml
@@ -1,7 +1,7 @@
name: cargo-audit
on:
push:
- paths:
+ paths:
- '**/Cargo.toml'
- '**/Cargo.lock'
schedule:
@@ -11,7 +11,9 @@ jobs:
security_audit:
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v5
+ - uses: actions/checkout@v6
+ - name: Generate lockfile
+ run: cargo generate-lockfile
- uses: rustsec/audit-check@v2
with:
token: ${{ secrets.GITHUB_TOKEN }}
diff --git a/.github/workflows/pr-template-check.yml b/.github/workflows/pr-template-check.yml
new file mode 100644
index 0000000000..3d73e536b4
--- /dev/null
+++ b/.github/workflows/pr-template-check.yml
@@ -0,0 +1,11 @@
+name: PR Template Verification
+
+on:
+ pull_request_target:
+ types: [opened, edited, reopened, synchronize]
+
+permissions: {}
+
+jobs:
+ verify-template:
+ uses: hyperium/.github/.github/workflows/pr-template-check.yml@master
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 883a0620c5..30fbb11685 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,107 @@
+## v1.12.0 (2026-10-06)
+
+
+#### Bug Fixes
+
+* **http1:**
+ * withdraw a stale want when the client dispatcher takes a request (#4208) ([c44f1cc5](https://github.com/hyperium/hyper/commit/c44f1cc51c2bc45b502031a646d3d54315bd7a16))
+ * remove content-length if transfer-encoding is auto-added (#4224) ([69396fdd](https://github.com/hyperium/hyper/commit/69396fdd960629d69b09cc6c9b367ef8650c001d))
+ * always remove content-length if transfer-encoding is sent (#4223) ([a5d7fd78](https://github.com/hyperium/hyper/commit/a5d7fd7861598c0b8c857cb75240a75bcfbd96a3))
+ * let a Connection close token win over a later keep-alive (#4201) ([5dadff76](https://github.com/hyperium/hyper/commit/5dadff76d1cb5d21046218df3568569fbba8ed21))
+ * preserve hop-by-hop when setting close or keep-alive (#4196) ([74a39081](https://github.com/hyperium/hyper/commit/74a39081ff1496970fb75da75c5282b23a9b69e5))
+* **http2:** do not reserve capacity for idle Upgraded streams (#4198) ([e89d0d93](https://github.com/hyperium/hyper/commit/e89d0d93d9566210ee95aea234a00952ebc4a202))
+
+
+#### Features
+
+* **http1:** add max_header_size limit for server and client (#4183) ([d1cd14ef](https://github.com/hyperium/hyper/commit/d1cd14eff392be63a2318d690cc9380723ec61a1), closes [#3832](https://github.com/hyperium/hyper/issues/3832))
+
+
+### v1.11.1 (2026-08-27)
+
+
+#### Bug Fixes
+
+* **http1:**
+ * detect TE: trailers caselessly and with other values (#4152) ([72fcf545](https://github.com/hyperium/hyper/commit/72fcf545c9b4a261f31d89bafd3019a2d4e9e1d7))
+ * recognize `\n\r\n` as a head terminator in the partial-read fast path (#4147) ([3534d75c](https://github.com/hyperium/hyper/commit/3534d75c898da17b023e6bff9b4cf71afed123ad), closes [#4145](https://github.com/hyperium/hyper/issues/4145))
+ * flush bytes buffered by the write re-check before yielding (#4143) ([f660f5bf](https://github.com/hyperium/hyper/commit/f660f5bf6eed3fe793f899507ff5bb9e266d4b0d))
+ * evict pooled conn on request-side Connection: close (#4110) ([da19be08](https://github.com/hyperium/hyper/commit/da19be08f34d3da01c4c0f9cfe2837e7d377f73d))
+
+
+## v1.11.0 (2026-07-20)
+
+
+#### Bug Fixes
+
+* **http1:**
+ * discard content-length header when received before transfer-encoding (#4124) ([540fff91](https://github.com/hyperium/hyper/commit/540fff9180ce47ee5fab01b6cc2126eb6c286eda), closes [#4123](https://github.com/hyperium/hyper/issues/4123))
+ * use append for repeat trailer values in encoder (#4118) ([de1483d7](https://github.com/hyperium/hyper/commit/de1483d7db70477cc8799a344634ae6ee020a7db))
+ * allow up to max_headers trailers (#4108) ([f584091a](https://github.com/hyperium/hyper/commit/f584091ac096bd5dd478f73256188c3261a945b9))
+ * use append for repeat trailers (#4107) ([876effe1](https://github.com/hyperium/hyper/commit/876effe10fd8f8ad4535ade1f84e88f199f2cf6b))
+ * flush buffered data before shutdown (#4018) ([72046cc7](https://github.com/hyperium/hyper/commit/72046cc72e7aa82c439eed00850b8b1ad3f7e4dc), closes [#4022](https://github.com/hyperium/hyper/issues/4022))
+ * more strictly enforce max_buf_size when parsing (#4093) ([90ede307](https://github.com/hyperium/hyper/commit/90ede307470dba98b4e184ad88d6f5aae8b0afd7), closes [#4081](https://github.com/hyperium/hyper/issues/4081))
+* **http2:** avoid buffering `Upgraded` writes without send capacity (#4102) ([aecf5abf](https://github.com/hyperium/hyper/commit/aecf5abfbc3dc95f21ac1538db1aa3f690fa6ab6))
+
+
+#### Features
+
+* **rt:** add `ReadBufCursor::initialized_unfilled()` method (#4115) ([ccc1e850](https://github.com/hyperium/hyper/commit/ccc1e850dc0cda3e71b0acd11f60ca3d48d09034))
+
+
+### v1.10.1 (2026-05-29)
+
+
+#### Bug Fixes
+
+* **http1:** fix busy loop when peer half-closes and open body (#4086) ([c6cb906f](https://github.com/hyperium/hyper/commit/c6cb906fdcbc3df64fc3fb613be57430e0387e48), closes [#4085](https://github.com/hyperium/hyper/issues/4085))
+
+
+## v1.10.0 (2026-05-27)
+
+
+#### Bug Fixes
+
+* **http1:**
+ * send error when dispatcher is dropped mid-body (#4069) ([b7a679ba](https://github.com/hyperium/hyper/commit/b7a679bad5e17d106b4b54835153af4e72027f82), closes [#3995](https://github.com/hyperium/hyper/issues/3995), [#4016](https://github.com/hyperium/hyper/issues/4016))
+ * fix reading large bodies on 32-bit systems (#4056) ([b12f6525](https://github.com/hyperium/hyper/commit/b12f6525432e7fbe80b749fec26f8ed7723006fc), closes [#4055](https://github.com/hyperium/hyper/issues/4055))
+ * fix rare missed write wakeup on connections v2 ([743a3ba0](https://github.com/hyperium/hyper/commit/743a3ba0706fde95e2095ad42ffefe219d807117))
+* **http2:**
+ * do not reserve capacity before body data is available (#4061) ([99f24345](https://github.com/hyperium/hyper/commit/99f243450268cfc8125ff232e0b7de016a1dce5b), closes [#4003](https://github.com/hyperium/hyper/issues/4003))
+ * reading trailers shouldn't propagate `NO_ERROR` from early response (#3998) ([e5ad96b1](https://github.com/hyperium/hyper/commit/e5ad96b1c511b568f086100538586231628b1eab))
+
+
+#### Features
+
+* **http2:**
+ * add `reset_stream_duration()` client option (#4068) ([156a6f6a](https://github.com/hyperium/hyper/commit/156a6f6aaacdc782861a4cab23ab3940a029d6ac), closes [#2599](https://github.com/hyperium/hyper/issues/2599))
+ * Add 'header_table_size()' method to server builder (#4062) ([6c9182c4](https://github.com/hyperium/hyper/commit/6c9182c4e566e716d23e7b54ce2c75e064a9213a))
+
+
+## v1.9.0 (2026-03-31)
+
+
+#### Bug Fixes
+
+* **ffi:** validate null pointers before dereferencing in request/response functions (#4038 ([28e73ccd](https://github.com/hyperium/hyper/commit/28e73ccd230cff8d7e5b5880ce2ab5fb8ced36be))
+* **http1:**
+ * allow keep-alive for chunked requests with trailers (#4043) ([7211ec25](https://github.com/hyperium/hyper/commit/7211ec25eff2ea6ee783817fee2a221d4eb2ed03), closes [#4044](https://github.com/hyperium/hyper/issues/4044))
+ * use case-insensitive matching for trailer fields (#4011) ([3b344cac](https://github.com/hyperium/hyper/commit/3b344cac9f96a9365409086dde51d06aa797ffc3), closes [#4010](https://github.com/hyperium/hyper/issues/4010))
+ * use httparse config for Servers (#4002) ([bcb8ec57](https://github.com/hyperium/hyper/commit/bcb8ec576619650d3388604e6c246829e7be133a), closes [#3923](https://github.com/hyperium/hyper/issues/3923))
+* **http2:**
+ * cancel sending client request body on response future drop (#4042) ([5b17a69e](https://github.com/hyperium/hyper/commit/5b17a69ebcf969471c1a19b25ed2cb81299d1be6), closes [#4040](https://github.com/hyperium/hyper/issues/4040))
+ * non-utf8 char in Connection header may cause panic when calling to_str (#4019) ([c36ca8a5](https://github.com/hyperium/hyper/commit/c36ca8a5c50e4a05a78aa3e158f13456ee674fb1))
+
+
+#### Features
+
+* **client:**
+ * expose HTTP/2 current max stream count (#4026) ([d51cb715](https://github.com/hyperium/hyper/commit/d51cb71569bbca7927b3828ef11e3bec4fa97eb4))
+ * add HTTP/2 `max_local_error_reset_streams` option (#4021) ([57787459](https://github.com/hyperium/hyper/commit/577874591cae246dfb2d72d7140d794ac2fa605a))
+* **error:** add 'Error::is_parse_version_h2' method ([393c77c7](https://github.com/hyperium/hyper/commit/393c77c71138ff6e33d7089deac770a3f3f1436b))
+* **http1:** add UpgradeableConnection::into_parts ([e21205cf](https://github.com/hyperium/hyper/commit/e21205cfe4066edbcddc56150d963dabbc7d3ec4))
+
+
### v1.8.1 (2025-11-13)
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 66043e1cb3..1ca6639734 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -2,11 +2,11 @@
You want to contribute? You're awesome!
-Contributions come in all shapes and sizes. Let's take a tour of some of the different wants you could contribute.
+Contributions come in all shapes and sizes. Let's take a tour of some of the different ways you could contribute.
## [Code of Conduct](./docs/CODE_OF_CONDUCT.md)
-Firstly, all interactions with the project need to abide by the code of conduct. This is to make sure everyone is treated kindly.
+Firstly, all interactions with the project need to abide by the code of conduct. This makes sure everyone is treated kindly.
## [Issues](./docs/ISSUES.md)
@@ -29,7 +29,7 @@ By the way, consider checking the [list of easy issues](https://github.com/hyper
Improving hyper's documentation is a huge help for everyone who is trying to _use_ hyper.
-- The API documentation (rendered at https://docs.rs/hyper) is stored as rustdoc comments directly in the source.
+- The API [documentation](https://docs.rs/hyper) is stored as rustdoc comments directly in the source.
- The main website has [tutorial-style guides](https://hyper.rs/guides). As of v1, they are currently in a [revamp](https://github.com/hyperium/hyper/issues/3411), and would greatly benefit from being filled out.
## Help
diff --git a/Cargo.toml b/Cargo.toml
index 4441bdcdea..a937dceba8 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "hyper"
-version = "1.8.1"
+version = "1.12.0"
description = "A protective and efficient HTTP library for all."
readme = "README.md"
homepage = "https://hyper.rs"
@@ -31,16 +31,16 @@ atomic-waker = { version = "1.1.2", optional = true }
futures-channel = { version = "0.3", optional = true }
futures-core = { version = "0.3.31", optional = true }
futures-util = { version = "0.3", default-features = false, features = ["alloc"], optional = true }
-h2 = { version = "0.4.2", optional = true }
+h2 = { version = "0.4.14", optional = true }
+h3 = { version = "0.0.8", optional = true }
http-body-util = { version = "0.1", optional = true }
httparse = { version = "1.9", optional = true }
httpdate = { version = "1.0", optional = true }
itoa = { version = "1", optional = true }
pin-project-lite = { version = "0.2.4", optional = true }
-pin-utils = { version = "0.1", optional = true } # TODO: replace with std::pin::pin! once MSRV >= 1.68
smallvec = { version = "1.12", features = ["const_generics", "const_new"], optional = true }
tracing = { version = "0.1", default-features = false, features = ["std"], optional = true }
-want = { version = "0.3", optional = true }
+want = { version = "0.3.2", optional = true }
[dev-dependencies]
form_urlencoded = "1"
@@ -52,6 +52,8 @@ pin-project-lite = "0.2.4"
spmc = "0.3"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
+tracing = "0.1"
+tracing-subscriber = "0.3"
tokio = { version = "1", features = [
"fs",
"macros",
@@ -59,7 +61,7 @@ tokio = { version = "1", features = [
"io-std",
"io-util",
"rt",
- "rt-multi-thread", # so examples can use #[tokio::main]
+ "rt-multi-thread",
"sync",
"time",
"test-util",
@@ -69,7 +71,7 @@ tokio-util = "0.7.10"
[features]
# Nothing by default
-default = []
+default = ["http3", "server"]
# Easily turn it all on
full = [
@@ -80,8 +82,9 @@ full = [
]
# HTTP versions
-http1 = ["dep:atomic-waker", "dep:futures-channel", "dep:futures-core", "dep:httparse", "dep:itoa", "dep:pin-utils"]
-http2 = ["dep:futures-channel", "dep:futures-core", "dep:h2"]
+http1 = ["dep:atomic-waker", "dep:futures-core", "dep:httparse", "dep:itoa"]
+http2 = ["dep:atomic-waker", "dep:futures-channel", "dep:futures-core", "dep:h2"]
+http3 = ["dep:h3"]
# Client/Server
client = ["dep:want", "dep:pin-project-lite", "dep:smallvec"]
@@ -101,9 +104,97 @@ nightly = []
level = "warn"
check-cfg = [
'cfg(hyper_unstable_tracing)',
- 'cfg(hyper_unstable_ffi)'
+ 'cfg(hyper_unstable_ffi)',
+ 'cfg(hyper_unstable_quic)',
]
+[lints.clippy]
+pedantic = { level = "warn", priority = -1 }
+restriction = { level = "warn", priority = -2 }
+
+# keep an allow list of lints
+
+# lints to decide on
+
+arithmetic_side_effects = "allow" # TODO: consider
+as_conversions = "allow" # TODO: tricky
+cast_precision_loss = "allow" # TODO: consider
+checked_conversions = "allow"
+else_if_without_else = "allow"
+float_arithmetic = "allow"
+indexing_slicing = "allow"
+match_wild_err_arm = "allow"
+multiple_inherent_impl = "allow"
+multiple_unsafe_ops_per_block = "allow"
+needless_pass_by_value = "allow"
+pattern_type_mismatch = "allow"
+redundant_closure_for_method_calls = "allow"
+redundant_else = "allow"
+struct_excessive_bools = "allow" # TODO: bogus lint?
+trivially_copy_pass_by_ref = "allow"
+unnested_or_patterns = "allow"
+unused_async = "allow" # TODO: is it for API?
+unused_trait_names = "allow" # TODO: kinda annoying, but might be good to deny
+unwrap_in_result = "allow"
+wildcard_enum_match_arm = "allow"
+definition_in_module_root = "allow"
+unnecessary_rest_pattern = "allow"
+rest_pattern_accessible_field = "allow"
+needless_bool = "allow"
+
+# explicitly allowed
+absolute_paths = "allow" # sometimes its cleaner
+arbitrary_source_item_ordering = "allow" # order: std, deps, crate
+blanket_clippy_restriction_lints = "allow" # allowlist is better
+clone_on_ref_ptr = "allow" # Arc::clone(blah) is needlessly noisy
+cognitive_complexity = "allow" # is this ever useful?
+decimal_literal_representation = "allow" # arguably more human-readable
+default_numeric_fallback = "allow" # too many false positives
+expect_used = "allow" # expect is self-documenting
+error_impl_error = "allow" # mod::Error is a fine name
+field_scoped_visibility_modifiers = "allow" # possibly good idea, noisy for now
+if_not_else = "allow" # order depends on which is more common, not truthiness
+if_then_some_else_none = "allow" # control flow as if better than closures
+items_after_statements = "allow" # these can be useful
+implicit_return = "allow" # standard rust
+impl_trait_in_params = "allow" # turbofish ignored on purpose
+inline_modules = "allow" # common for sealed types
+inline_trait_bounds = "allow" # more concise if shorter bounds
+let_underscore_must_use = "allow" # the entire point was to ignore must_use
+let_underscore_untyped = "allow"
+mod_module_files = "allow" # easier to find than self-named modules
+module_inception = "allow" # sometimes that happens
+min_ident_chars = "allow" # not to be abused, nor forced
+missing_assert_message = "allow" # not much value
+missing_docs_in_private_items = "allow" # these docs aren't rendered
+missing_inline_in_public_items = "allow" # bad lint
+missing_trait_methods = "allow"
+must_use_candidate = "allow" # bad lint
+module_name_repetitions = "allow" # sometimes it happens, not bad at all
+new_without_default = "allow" # not everything needs a Default impl
+panic_in_result_fn = "allow" # panics are for invariants
+pub_use = "allow"
+pub_with_shorthand = "allow"
+question_mark_used = "allow" # question mark is excellent
+unreachable = "allow" # self-documenting invariants
+renamed_function_params = "allow" # we can pick clearer names
+same_name_method = "allow"
+shadow_reuse = "allow" # shadowing is useful
+shadow_same = "allow" # shadowing is useful
+shadow_unrelated = "allow" # shadowing is useful
+single_call_fn = "allow" # abstracting to a function is the point
+self_named_module_files = "deny" # already denied but, rly, dont do it
+semicolon_inside_block ="allow" # depends on context
+semicolon_outside_block ="allow" # depends on context
+std_instead_of_alloc = "allow" # std is more idiomatic
+std_instead_of_core = "allow" # std is more idiomatic
+struct_field_names = "allow" # not really helpful
+too_many_lines = "allow" # reconsider someday?
+type_complexity = "allow" # not helpful
+used_underscore_items = "allow"
+unnecessary_safety_comment = "allow" # more safety comments are a good thing
+unseparated_literal_suffix = "allow" # i don't like 0_u8 (do i?)
+
[package.metadata.docs.rs]
features = ["ffi", "full", "tracing"]
rustdoc-args = ["--cfg", "hyper_unstable_ffi", "--cfg", "hyper_unstable_tracing"]
@@ -239,7 +330,27 @@ name = "integration"
path = "tests/integration.rs"
required-features = ["full"]
+[[test]]
+name = "ready_on_poll_stream"
+path = "tests/ready_on_poll_stream.rs"
+required-features = ["full"]
+
+[[test]]
+name = "unbuffered_stream"
+path = "tests/unbuffered_stream.rs"
+required-features = ["full"]
+
[[test]]
name = "server"
path = "tests/server.rs"
required-features = ["full"]
+
+[[test]]
+name = "h1_shutdown_while_buffered"
+path = "tests/h1_shutdown_while_buffered.rs"
+required-features = ["full"]
+
+[[test]]
+name = "h1_ready_in_flight"
+path = "tests/h1_ready_in_flight.rs"
+required-features = ["full"]
diff --git a/LICENSE b/LICENSE
index da8b6ec1b5..f7ae3a7107 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,4 +1,4 @@
-Copyright (c) 2014-2025 Sean McArthur
+Copyright (c) 2014-2026 Sean McArthur
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
diff --git a/SECURITY.md b/SECURITY.md
index aa5ac773bf..056e82a20d 100644
--- a/SECURITY.md
+++ b/SECURITY.md
@@ -1,6 +1,6 @@
# Security Policy
-hyper (and related projects in hyperium) take security seriously, and greatly appreciate responsibile disclosure.
+hyper (and related projects in hyperium) take security seriously, and greatly appreciate responsible disclosure.
## Report a security issue
diff --git a/benches/end_to_end.rs b/benches/end_to_end.rs
index 07f08c186b..127d8ca9a4 100644
--- a/benches/end_to_end.rs
+++ b/benches/end_to_end.rs
@@ -95,7 +95,7 @@ fn http1_parallel_x10_res_10mb(b: &mut test::Bencher) {
// HTTP2
-const HTTP2_MAX_WINDOW: u32 = std::u32::MAX >> 1;
+const HTTP2_MAX_WINDOW: u32 = u32::MAX >> 1;
#[bench]
fn http2_consecutive_x1_empty(b: &mut test::Bencher) {
@@ -368,13 +368,9 @@ impl Opts {
};
let mut send_request = |req| {
- let fut = match client {
- Client::Http1(ref mut tx) => {
- futures_util::future::Either::Left(tx.send_request(req))
- }
- Client::Http2(ref mut tx) => {
- futures_util::future::Either::Right(tx.send_request(req))
- }
+ let fut = match &mut client {
+ Client::Http1(tx) => futures_util::future::Either::Left(tx.send_request(req)),
+ Client::Http2(tx) => futures_util::future::Either::Right(tx.send_request(req)),
};
async {
let res = fut.await.expect("client wait");
diff --git a/capi/README.md b/capi/README.md
index becd57862f..490bb5dae7 100644
--- a/capi/README.md
+++ b/capi/README.md
@@ -10,7 +10,7 @@ Because of that, it's only accessible if `--cfg hyper_unstable_ffi` is passed to
## Building
-The C API is part of the Rust library, but isn't compiled by default. Using `cargo`, staring with `1.64.0`, it can be compiled with the following command:
+The C API is part of the Rust library, but isn't compiled by default. Using `cargo`, starting with `1.64.0`, it can be compiled with the following command:
```
RUSTFLAGS="--cfg hyper_unstable_ffi" cargo rustc --features client,http1,http2,ffi --crate-type cdylib
diff --git a/capi/cbindgen.toml b/capi/cbindgen.toml
index a4990840e1..a0a6bba00f 100644
--- a/capi/cbindgen.toml
+++ b/capi/cbindgen.toml
@@ -2,7 +2,7 @@
# a list of possible configuration values.
language = "C"
header = """/*
- * Copyright 2023 Sean McArthur. MIT License.
+ * Copyright 2026 Sean McArthur. MIT License.
* Generated by gen_header.sh. Do not edit directly.
*
* Full docs at: https://docs.rs/hyper/latest/hyper/ffi/index.html
diff --git a/capi/include/hyper.h b/capi/include/hyper.h
index b88bd39e9d..598d240b60 100644
--- a/capi/include/hyper.h
+++ b/capi/include/hyper.h
@@ -1,5 +1,5 @@
/*
- * Copyright 2023 Sean McArthur. MIT License.
+ * Copyright 2026 Sean McArthur. MIT License.
* Generated by gen_header.sh. Do not edit directly.
*
* Full docs at: https://docs.rs/hyper/latest/hyper/ffi/index.html
diff --git a/docs/COLLABORATORS.md b/docs/COLLABORATORS.md
new file mode 100644
index 0000000000..37c590a7ec
--- /dev/null
+++ b/docs/COLLABORATORS.md
@@ -0,0 +1,68 @@
+# hyper Collaborator Guide
+
+This document describes how collaborators manage hyper.
+
+- First off, a collaborator doesn't need to do all of the following perfectly.
+ - It takes time to learn.
+ - Instead, focus on growth.
+ - Get in the habit of checking this guide with some frequency, and improve rather than stagnate.
+
+## Exemplify the Code of Conduct
+
+- Our [code of conduct](./CODE_OF_CONDUCT.md) is not elaborate. It doesn't require much study, nor explication.
+- To be a good collaborator, it does require internalizing it.
+- The code of conduct is proactive, positive.
+- A collaborator must be a good example of being kind.
+ - It should not be possible to find significant fault in a collaborator against the simple principles in the code of conduct.
+
+## Internalize the Vision
+
+- Understand and use the project's [vision](./VISION.md) when helping guide decisions.
+
+## Review Pull Requests
+
+- Review pull requests promptly and kindly. Approve CI for new contributors.
+ - Consider telling a maintainer to add you to the automated review queue.
+- Collaborators should feel comfortable approving and merging straightforward changes. Such as documentation, chores, styling, simple refactors, and basic bug fixes.
+- When reviewing larger changes, keep in mind the points on [being API caretakers](#api-caretakers).
+- When approving a PR, allow time for other collaborators to look before merging.
+ - Especially if the PR is newer, or has a more significant change.
+- When merging, prefer squashing (it adds in the PR number). Touch up the commit message to match our [COMMITS](./COMMITS.md) style.
+
+## Provide Feedback on Features and Designs
+
+Collaborators try to participate early and often when new features are proposed. They also actively consider and recommend new features that users may need.
+
+### API Caretakers
+
+Collaborators are caretakers of hyper's API. When proposing, discussing, or reviewing changes to hyper, keep the following in mind:
+
+- Conservative additions
+ - Conservative APIs reduce breaking changes
+- Don't expose internal implementation details
+ - [Accessors can reveal internal repr](https://seanmonstar.com/micro/20260721-accessors-reveal-internal-repr/)
+- hyper-util is a place to explore first
+ - [Vision: Not quite stable, but utile](./VISION.md#not-quite-stable-but-utile-useful)
+ - [Roadmap: hyper-util](./ROADMAP.md#hyper-util)
+- Unstable features
+ - Unstable features are not bound by our stability promise.
+ - We don't just name a crate feature with unstable.
+ - This has the problem of an intermediary crate enabling it, and someone depending on _that_ crate and not realizing they are accessing unstable hyper features.
+ - All unstable features require a conditional config flag passed to the Rust compiler.
+ - For example, `hyper_unstable_ffi`.
+- Breaking changes
+ - These would usually be reserved for a new SemVer major release, such as v2.0, and thus would usually be very rare.
+ - An exception is fixing a mistake quickly after release of a new feature.
+ - All breaking changes require approval by a maintainer.
+
+## Welcome New Contributors
+
+- Help new contributors find their way around the project and its processes. Follow the guidance on [acknowledging people when triaging](./ISSUES.md#acknowledge).
+
+## Answer Questions
+
+- Answer questions in issues or chat when you can, or help find someone who can. See the project's [help channels](../CONTRIBUTING.md#help).
+
+## Mentor Contributors
+
+- Help contributors grow toward becoming project members. Issue triaging often provides opportunities for [mentoring](./ISSUES.md#mentoring), but the ideas can be applied to reviews, or when answering questions.
diff --git a/docs/GOVERNANCE.md b/docs/GOVERNANCE.md
index e2d6b7f75f..45d7a515c9 100644
--- a/docs/GOVERNANCE.md
+++ b/docs/GOVERNANCE.md
@@ -43,7 +43,6 @@ All contributors MUST follow the [Code of Conduct][coc].
š **New here?** [This could be you!][contrib]
-
### Triager
Triagers assess issues on the issue tracker. They help make sure the work is
@@ -72,6 +71,7 @@ How to become:
### Collaborator
Collaborators are contributors who have been helping out in a consistent basis.
+See the [collaborator guide](./COLLABORATORS.md) for practical guidance on the role.
Responsibilities:
@@ -100,6 +100,26 @@ How to become:
Don't be afraid to ask a collaborator for what you could work on to reach this
goal!
+### Advisor
+
+Advisors are contributors with a lot of domain expertise. They offer advice on
+proposals, complicated bugs or features, based on their experience elsewhere in
+the industry.
+
+Responsibilities:
+
+- Adhere to the [Code of Conduct][coc]
+- Try to offer advice when the `@hyperium/advisors` team is tagged for input.
+ (This is not a frequent occurrence, so the time commitment should generally be
+ low.)
+
+How to become:
+
+- Advisors are invited by a maintainer.
+- Sometimes they are experienced Collaborators who must step back from active
+ contributing. Sometimes they are experts placed elsewhere who are friendly to
+ the project.
+
### Maintainer
Maintainers are the project admins. Besides being a collaborator, they take care
diff --git a/docs/MAINTAINERS.md b/docs/MAINTAINERS.md
index 5b6f48c6fa..3bd87307a0 100644
--- a/docs/MAINTAINERS.md
+++ b/docs/MAINTAINERS.md
@@ -11,26 +11,27 @@ To see what these roles do, and how to become one, look at [GOVERNANCE](./GOVERN
- Sean McArthur (@seanmonstar)
- Steven Fackler (@sfackler)
-- Oliver Gould (@olix0r)
-- Eliza Weisman (@hawkw)
-- Lucio Franco (@LucioFranco)
-- Anthony Ramine (@nox)
-- David Pedersen (@davidpdrsn)
-- Adam Foltzer (@acfoltzer)
-- Noah Kennedy (@Noah-Kennedy)
- dswij (@dswij)
- tottoto (@tottoto)
- katelyn martin (@cratelyn)
-
-
-Emeriti
+- 0x676e67 (@0x676e67)
### Collaborator emeriti
- Jonathan Reem (@reem)
- Carl Lerche (@carllerche)
+- Oliver Gould (@olix0r)
+- Eliza Weisman (@hawkw)
+- Lucio Franco (@LucioFranco)
+- Adam Foltzer (@acfoltzer)
+- David Pedersen (@davidpdrsn)
+- Anthony Ramine (@nox)
+- Noah Kennedy (@Noah-Kennedy)
+
+## Advisors
-
+- Oliver Gould (@olix0r)
+- Adam Foltzer (@acfoltzer)
## Maintainers
diff --git a/docs/MSRV.md b/docs/MSRV.md
index 70752c9138..b8b68287eb 100644
--- a/docs/MSRV.md
+++ b/docs/MSRV.md
@@ -1,4 +1,4 @@
-# Minimum Support Rust Version (MSRV)
+# Minimum Supported Rust Version (MSRV)
hyper's current policy is to always support a Rust version at least 6 months
old. That is, a compiler version released within the last 6 months can compile
diff --git a/docs/hips/0000-template.md b/docs/hips/0000-template.md
new file mode 100644
index 0000000000..88c469754d
--- /dev/null
+++ b/docs/hips/0000-template.md
@@ -0,0 +1,41 @@
+# HIP-0000: Template
+
+- Authors:
+- Created: (date)
+- PR: (link to pull request)
+
+## Summary
+
+(The tl;dr. Concise statement of the reason for the document and recommendation.)
+
+## Tenets
+
+(List of tenets, values, guiding principles that are used to evaluate the proposal.)
+
+## Motivation
+
+(Clearly explain the problem.)
+
+## Recommendation
+
+### User Experience
+
+### Implementation
+
+### Testing Plan
+
+(Optional: are there any special considerations for testing required?)
+
+### Security Considerations
+
+(What security risks might exist for the recommendation? Can we reduce them? What should users consider when using it?)
+
+## Alternatives
+
+(List alternatives considered, and especially why they were not recommended. This portion of "documenting the why" helps Future Us to see if anything has changed since last considered.)
+
+## Unresolved Questions
+
+
+## References
+
diff --git a/docs/hips/0001-hyper-improvement-proposals.md b/docs/hips/0001-hyper-improvement-proposals.md
new file mode 100644
index 0000000000..fea679c90a
--- /dev/null
+++ b/docs/hips/0001-hyper-improvement-proposals.md
@@ -0,0 +1,134 @@
+# HIP-0001: hyper Improvement Proposals
+
+- Authors: seanmonstar
+- Created: 2026-09-01
+- PR: [#4131](https://github.com/hyperium/hyper/pull/4131)
+
+## Summary
+
+HIPs establish a predictable, writtenāfirst process for proposing and evaluating significant changes to hyper. They provide an authoritative place to articulate the problem, the design rationale, and the tradeoffs so contributors can make informed, durable decisions.
+
+## Tenets
+
+- Open: the decision-making process should be out in the open, for all to see and any to participate.
+- Understandable: Code and changelogs are not the complete theory. Others should be able to understand what we meant.
+- Writing is thinking: while the written artifact is extremely valuable, the process of creating is thinking itself. It refines our thought, sharpens it, makes it better.
+- Document the why: just as important is understanding why we thought that way. This allows evaluating when the decision should be reconsidered.
+
+## Motivation
+
+hyper's existing contribution process does not adequately support proposals for large features and design changes.
+
+Much of the shape of hyper can be seen in the [VISION](../VISION.md) and [1.0 ROADMAP](../ROADMAP-1.0.md). Adding new small features is very easy. However, designing large new systems in hyper must be handled with care. hyper is deployed at massive scale. The stability promise requires we be deliberate with what API to expose.
+
+The current process is to ask people to write up larger designs in an issue. GitHub issues are terrible places to discuss multiple points at once. Or people submit massive pull requests, and the discussion is sprinkled through the code reviews.
+
+It's extremely difficult for reviewers to be sure they understand the design, and to ensure that all critical aspects have been considered. If an implementation is denied for a desirable feature, new pull requests tend to be submitted which don't address the previous concerns. The barrier for someone to contribute goes up due to the lack of structure. And the review process slows down as reviewers struggle to continuously rebuild mental context.
+
+## Recommendation
+
+Introduce a structured, written process for proposing, discussing, and deciding large design changes. We call these **hyper Improvement Proposals** (HIPs). They are similar in spirit to [RFCs](https://en.wikipedia.org/wiki/Request_for_Comments).
+
+### Scope
+
+When should HIPs be used?
+
+HIPs are intended for changes that significantly affect hyper's architecture, public API, performance characteristics, or longāterm maintenance burden. They are useful when a decision is difficult to reverse, when there appear to be several strong options, or as a foundation for future proposals.
+
+When are they not needed?
+
+Small, localized changes that do not alter core design or guarantees generally do not require a HIP.
+
+### Content
+
+What should be included in a HIP?
+
+This defines at a high level the purpose of what content is included. The exact sections might change over time, and so are defined in a separate `0000-template.md` file.
+
+But in general, HIPs should provide the following information:
+
+- Objective - the problem, the motivation, the what are we even doing here.
+- Tenets - how to measure solutions
+ - A meta point here: spend more time on the objective, tenets, and alternatives. The recommendation will fall into place.
+- A single recommendation
+ - The job of a proposal is to propose a solution, not to propose a problem.
+ - There will often be several ways to solve anything.
+ - The proposal should research and measure the options using the facts and tenets, and decide which is the best.
+ - The discussion during the proposal period may change what is the recommended solution.
+ - The essence of those discussions should be recaptured into the proposal itself. Both in the recommendation section, and also potentially in an FAQ appendix.
+ - Future readers should not need to reconstruct the design from the review comments.
+- The whys (and why nots) of the solution
+ - A proposal should always include why the recommendation is the best option, and why the other options were not chosen
+- References
+ - Links to all the relevant research that was done
+ - Links to previous discussions
+- Appendices
+ - FAQ
+
+#### Do not use AI to write
+
+AI (LLMs) have their benefits, but since a major reason for writing this is the thinking process, do not use AI to write the actual English in a HIP.
+
+#### What not to include
+
+- Minor details that do not need to be discussed to accept the proposal.
+
+### Lifecycle
+
+- Start with an issue.
+ - (A HIP should almost never be started without a previous discussion)
+- If the feature request seems large enough, a collaborator may ask for a HIP.
+- In a new branch, copy the template file to `0000-your-feature-name.md`.
+- Fill out the sections based on the instructions in the template.
+ - You don't need to have fully polished prose in every section to submit for discussion.
+ - It can even be beneficial to submit initially with bullet points.
+ - Balance giving proper thought to the sections, with moving quickly to get appropriate feedback.
+- Consider sharing an early draft with someone else first.
+- Submit a pull request, and include a big reminder to prefer line comments over top-level PR comments.
+- As discussion occurs, revise the sections.
+ - Consider adding to an FAQ appendix.
+- Eventually, a [maintainer](../GOVERNANCE.md#maintainer) will decide whether the proposal is accepted or rejected.
+
+Once a proposal is accepted, a number will be assigned. The file in the PR can be renamed with the assigned number, and then merged into trunk.
+
+#### Rejection
+
+If a proposal is rejected or withdrawn, its pull request is closed without assigning a HIP number or merging the document. The pull request remains the record of that discussion. If the discussion reveals a generally applicable constraint or principle, that information should instead be incorporated into the appropriate project documentation.
+
+#### Mutability and History
+
+HIPs are mutable, even after being accepted.
+
+Significant changes can be added to a `## History` section of the document, inserted before any `## References` or appendices. Besides explaining what changed, the reasoning for _why_ should be included.
+
+## Alternatives
+
+- Ad-hoc design docs
+ - While design documents are an ideal artifact, without defining a process behind them, it's hard for contributors to know when to use one or how.
+- Module documentation
+ - A reader of module documentation is not the right audience.
+ - The purpose of module documentation is to explain how to use the library.
+ - It is not a great place to document the why.
+- Issues and Pull Requests
+ - This is the status quo.
+ - Discussion in issues is a horrible experience.
+ - Spreads out the full design across disconnected areas.
+ - Silos the knowledge directly in GitHub.
+- A chat application, such as Zulip
+ - Chat-style messages are extremely poor for having thoughtful discussion and decisions.
+ - They become littered with messages that have no longer-term value, and it makes it hard to piece back together what a decision is.
+ - No contributor should ever _have_ to participate in chat-style messaging.
+
+## Unresolved Questions
+
+- Is there a point when significant changes to an accepted HIP should itself be a new HIP?
+ - Possibly, but I'm not sure it's worth deciding yet.
+
+## References
+
+-
+-
+-
+-
+-
+-
diff --git a/examples/params.rs b/examples/params.rs
index 8774046a67..0f26efb8bb 100644
--- a/examples/params.rs
+++ b/examples/params.rs
@@ -52,7 +52,7 @@ async fn param_example(
return Ok(Response::builder()
.status(StatusCode::UNPROCESSABLE_ENTITY)
.body(full(MISSING))
- .unwrap());
+ .expect("constant status won't error"));
};
let number = if let Some(n) = params.get("number") {
if let Ok(v) = n.parse::() {
@@ -61,13 +61,13 @@ async fn param_example(
return Ok(Response::builder()
.status(StatusCode::UNPROCESSABLE_ENTITY)
.body(full(NOTNUMERIC))
- .unwrap());
+ .expect("constant status won't error"));
}
} else {
return Ok(Response::builder()
.status(StatusCode::UNPROCESSABLE_ENTITY)
.body(full(MISSING))
- .unwrap());
+ .expect("constant status won't error"));
};
// Render the response. This will often involve
@@ -86,7 +86,7 @@ async fn param_example(
return Ok(Response::builder()
.status(StatusCode::UNPROCESSABLE_ENTITY)
.body(full(MISSING))
- .unwrap());
+ .expect("constant status won't error"));
};
let params = form_urlencoded::parse(query.as_bytes())
.into_owned()
@@ -97,7 +97,7 @@ async fn param_example(
return Ok(Response::builder()
.status(StatusCode::UNPROCESSABLE_ENTITY)
.body(full(MISSING))
- .unwrap());
+ .expect("constant status won't error"));
};
let body = format!("You requested {}", page);
Ok(Response::new(full(body)))
@@ -105,7 +105,7 @@ async fn param_example(
_ => Ok(Response::builder()
.status(StatusCode::NOT_FOUND)
.body(empty())
- .unwrap()),
+ .expect("constant status won't error")),
}
}
diff --git a/examples/send_file.rs b/examples/send_file.rs
index b2ef328a1f..d3dc115a5d 100644
--- a/examples/send_file.rs
+++ b/examples/send_file.rs
@@ -61,7 +61,7 @@ fn not_found() -> Response> {
Response::builder()
.status(StatusCode::NOT_FOUND)
.body(Full::new(NOTFOUND.into()).map_err(|e| match e {}).boxed())
- .unwrap()
+ .expect("constant status won't error")
}
async fn simple_file_send(filename: &str) -> Result>> {
@@ -85,7 +85,7 @@ async fn simple_file_send(filename: &str) -> Result> for Svc {
fn call(&self, req: Request) -> Self::Future {
fn mk_response(s: String) -> Result>, hyper::Error> {
- Ok(Response::builder().body(Full::new(Bytes::from(s))).unwrap())
+ Ok(Response::new(Full::new(Bytes::from(s))))
}
if req.uri().path() != "/favicon.ico" {
diff --git a/examples/upgrades.rs b/examples/upgrades.rs
index b94a8b9c42..b5460d96c9 100644
--- a/examples/upgrades.rs
+++ b/examples/upgrades.rs
@@ -100,7 +100,7 @@ async fn client_upgrade_request(addr: SocketAddr) -> Result<()> {
.uri(format!("http://{}/", addr))
.header(UPGRADE, "foobar")
.body(Empty::::new())
- .unwrap();
+ .expect("uri/header parse won't error");
let stream = TcpStream::connect(addr).await?;
let io = TokioIo::new(stream);
diff --git a/examples/web_api.rs b/examples/web_api.rs
index ffc62ba89b..ff2ea90b3d 100644
--- a/examples/web_api.rs
+++ b/examples/web_api.rs
@@ -29,7 +29,7 @@ async fn client_request_response() -> Result> {
.uri(URL)
.header(header::CONTENT_TYPE, "application/json")
.body(Full::new(Bytes::from(POST_DATA)))
- .unwrap();
+ .expect("uri/header parse from constants won't error");
let host = req.uri().host().expect("uri has no host");
let port = req.uri().port_u16().expect("uri has no port");
@@ -73,11 +73,11 @@ async fn api_get_response() -> Result> {
Ok(json) => Response::builder()
.header(header::CONTENT_TYPE, "application/json")
.body(full(json))
- .unwrap(),
+ .expect("header parse from constant won't error"),
Err(_) => Response::builder()
.status(StatusCode::INTERNAL_SERVER_ERROR)
.body(full(INTERNAL_SERVER_ERROR))
- .unwrap(),
+ .expect("constant status won't error"),
};
Ok(res)
}
@@ -93,7 +93,7 @@ async fn response_examples(req: Request) -> Result (Sender, Receiver) {
+ let shared = Arc::new(Shared {
+ state: Mutex::new(State {
+ item: None,
+ pending_error: None,
+ trailers: None,
+ sender_open: true,
+ receiver_open: true,
+ want: !wanter,
+ }),
+ sender_waker: AtomicWaker::new(),
+ receiver_waker: AtomicWaker::new(),
+ });
+
+ (
+ Sender {
+ shared: Arc::clone(&shared),
+ trailers_sent: false,
+ },
+ Receiver {
+ shared,
+ terminated: false,
+ },
+ )
+}
+
+#[must_use = "Sender does nothing unless sent on"]
+pub(crate) struct Sender {
+ shared: Arc,
+ trailers_sent: bool,
+}
+
+pub(crate) struct Receiver {
+ shared: Arc,
+ terminated: bool,
+}
+
+struct Shared {
+ state: Mutex,
+ sender_waker: AtomicWaker,
+ receiver_waker: AtomicWaker,
+}
+
+struct State {
+ item: Option>,
+ // An error must not displace data which was already accepted. The old
+ // mpsc channel achieved this by sending the error from a cloned sender.
+ pending_error: Option,
+ trailers: Option,
+ sender_open: bool,
+ receiver_open: bool,
+ want: bool,
+}
+
+impl Sender {
+ pub(crate) fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> {
+ self.shared.sender_waker.register(cx.waker());
+ let state = self.shared.state.lock().panic_if_poisoned();
+ if !state.receiver_open {
+ Poll::Ready(Err(crate::Error::new_closed()))
+ } else if state.want && state.item.is_none() && state.pending_error.is_none() {
+ Poll::Ready(Ok(()))
+ } else {
+ Poll::Pending
+ }
+ }
+
+ #[cfg(test)]
+ pub(crate) async fn ready(&mut self) -> crate::Result<()> {
+ futures_util::future::poll_fn(|cx| self.poll_ready(cx)).await
+ }
+
+ #[cfg(test)]
+ #[allow(dead_code)]
+ pub(crate) async fn send_data(&mut self, chunk: Bytes) -> crate::Result<()> {
+ self.ready().await?;
+ self.try_send_data(chunk)
+ .map_err(|_| crate::Error::new_closed())
+ }
+
+ pub(crate) fn try_send_data(&mut self, chunk: Bytes) -> Result<(), Bytes> {
+ let mut state = self.shared.state.lock().panic_if_poisoned();
+ if !state.receiver_open
+ || !state.want
+ || state.item.is_some()
+ || state.pending_error.is_some()
+ {
+ return Err(chunk);
+ }
+ state.item = Some(Ok(chunk));
+ drop(state);
+ self.shared.receiver_waker.wake();
+ Ok(())
+ }
+
+ pub(crate) fn try_send_trailers(
+ &mut self,
+ trailers: HeaderMap,
+ ) -> Result<(), Option> {
+ if self.trailers_sent {
+ return Err(None);
+ }
+ self.trailers_sent = true;
+
+ let mut state = self.shared.state.lock().panic_if_poisoned();
+ if !state.receiver_open {
+ return Err(Some(trailers));
+ }
+ state.trailers = Some(trailers);
+ drop(state);
+ self.shared.receiver_waker.wake();
+ Ok(())
+ }
+
+ #[allow(dead_code)]
+ #[allow(clippy::unused_async_trait_impl)]
+ #[allow(clippy::map_err_ignore)]
+ pub(crate) async fn send_trailers(&mut self, trailers: HeaderMap) -> crate::Result<()> {
+ self.try_send_trailers(trailers)
+ .map_err(|_| crate::Error::new_closed())
+ }
+
+ pub(crate) fn send_error(&mut self, err: crate::Error) {
+ let mut state = self.shared.state.lock().panic_if_poisoned();
+ if !state.receiver_open {
+ return;
+ }
+ if state.item.is_none() {
+ state.item = Some(Err(err));
+ } else if state.pending_error.is_none() {
+ state.pending_error = Some(err);
+ }
+ drop(state);
+ self.shared.receiver_waker.wake();
+ }
+
+ #[cfg(test)]
+ pub(crate) fn abort(mut self) {
+ self.send_error(crate::Error::new_body_write_aborted());
+ }
+
+ fn is_closed(&self) -> bool {
+ !self.shared.state.lock().panic_if_poisoned().receiver_open
+ }
+}
+
+impl Receiver {
+ pub(crate) fn poll_next(
+ &mut self,
+ cx: &mut Context<'_>,
+ ) -> Poll>> {
+ if self.terminated {
+ return Poll::Ready(None);
+ }
+
+ self.shared.receiver_waker.register(cx.waker());
+ let mut state = self.shared.state.lock().panic_if_poisoned();
+ let wake_sender = if !state.want {
+ state.want = true;
+ true
+ } else {
+ false
+ };
+ if let Some(item) = state.item.take() {
+ drop(state);
+ self.shared.sender_waker.wake();
+ return Poll::Ready(Some(item));
+ }
+ if let Some(err) = state.pending_error.take() {
+ drop(state);
+ if wake_sender {
+ self.shared.sender_waker.wake();
+ }
+ return Poll::Ready(Some(Err(err)));
+ }
+ let sender_open = state.sender_open;
+ drop(state);
+ if wake_sender {
+ self.shared.sender_waker.wake();
+ }
+ if sender_open {
+ Poll::Pending
+ } else {
+ self.terminated = true;
+ Poll::Ready(None)
+ }
+ }
+
+ pub(crate) fn take_trailers(&mut self) -> Option {
+ debug_assert!(self.terminated, "data channel still open before trailers");
+ let mut state = self.shared.state.lock().panic_if_poisoned();
+ state.trailers.take()
+ }
+}
+
+impl Drop for Sender {
+ fn drop(&mut self) {
+ let mut state = self.shared.state.lock().panic_if_poisoned();
+ state.sender_open = false;
+ let receiver_open = state.receiver_open;
+ drop(state);
+ if receiver_open {
+ self.shared.receiver_waker.wake();
+ }
+ }
+}
+
+impl Drop for Receiver {
+ fn drop(&mut self) {
+ let mut state = self.shared.state.lock().panic_if_poisoned();
+ state.receiver_open = false;
+ let sender_open = state.sender_open;
+ drop(state);
+ if sender_open {
+ self.shared.sender_waker.wake();
+ }
+ }
+}
+
+impl fmt::Debug for Sender {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ if self.is_closed() {
+ f.debug_tuple("Sender").field(&"Closed").finish()
+ } else {
+ f.debug_tuple("Sender").field(&"Open").finish()
+ }
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use std::{
+ sync::atomic::{AtomicUsize, Ordering},
+ task::{Wake, Waker},
+ };
+
+ use super::*;
+
+ async fn recv(rx: &mut Receiver) -> Option> {
+ futures_util::future::poll_fn(|cx| rx.poll_next(cx)).await
+ }
+
+ #[tokio::test]
+ async fn error_queued_behind_accepted_data() {
+ let (mut tx, mut rx) = channel(false);
+ tx.try_send_data(Bytes::from_static(b"data")).unwrap();
+ tx.send_error(crate::Error::new_incomplete());
+ drop(tx);
+
+ assert_eq!(recv(&mut rx).await.unwrap().unwrap(), "data");
+ assert!(recv(&mut rx).await.unwrap().is_err());
+ assert!(recv(&mut rx).await.is_none());
+ }
+
+ #[tokio::test]
+ async fn trailers_follow_data_close() {
+ let (mut tx, mut rx) = channel(false);
+ let mut trailers = HeaderMap::new();
+ trailers.insert("x-trailer", "value".parse().unwrap());
+ tx.try_send_trailers(trailers).unwrap();
+ drop(tx);
+
+ assert!(recv(&mut rx).await.is_none());
+ assert_eq!(rx.take_trailers().unwrap()["x-trailer"], "value");
+ }
+
+ #[test]
+ fn drop_wakes_only_an_open_peer() {
+ struct Count(AtomicUsize);
+
+ impl Wake for Count {
+ fn wake(self: Arc) {
+ self.0.fetch_add(1, Ordering::Relaxed);
+ }
+ }
+
+ let count = Arc::new(Count(AtomicUsize::new(0)));
+ let waker = Waker::from(count.clone());
+ let mut cx = Context::from_waker(&waker);
+ let wakes = || count.0.load(Ordering::Relaxed);
+
+ // A receiver dropped after the sender does not wake the sender's stale waker.
+ let (mut tx, rx) = channel(true);
+ assert!(tx.poll_ready(&mut cx).is_pending());
+ drop(tx);
+ drop(rx);
+ assert_eq!(wakes(), 0);
+
+ // Nor does a sender dropped after the receiver.
+ let (tx, mut rx) = channel(false);
+ assert!(rx.poll_next(&mut cx).is_pending());
+ drop(rx);
+ drop(tx);
+ assert_eq!(wakes(), 0);
+
+ // An open peer is still woken to observe the close.
+ let (mut tx, rx) = channel(true);
+ assert!(tx.poll_ready(&mut cx).is_pending());
+ drop(rx);
+ assert_eq!(wakes(), 1);
+ let (tx, mut rx) = channel(false);
+ assert!(rx.poll_next(&mut cx).is_pending());
+ drop(tx);
+ assert_eq!(wakes(), 2);
+ }
+}
diff --git a/src/body/incoming.rs b/src/body/incoming.rs
index 64ee5001a9..b026a24d6b 100644
--- a/src/body/incoming.rs
+++ b/src/body/incoming.rs
@@ -1,38 +1,25 @@
use std::fmt;
-#[cfg(all(feature = "http1", any(feature = "client", feature = "server")))]
-use std::future::Future;
use std::pin::Pin;
use std::task::{Context, Poll};
use bytes::Bytes;
-#[cfg(all(feature = "http1", any(feature = "client", feature = "server")))]
-use futures_channel::{mpsc, oneshot};
#[cfg(all(
any(feature = "http1", feature = "http2"),
any(feature = "client", feature = "server")
))]
use futures_core::ready;
-#[cfg(all(feature = "http1", any(feature = "client", feature = "server")))]
-use futures_core::{stream::FusedStream, Stream}; // for mpsc::Receiver
-#[cfg(all(feature = "http1", any(feature = "client", feature = "server")))]
-use http::HeaderMap;
use http_body::{Body, Frame, SizeHint};
+#[cfg(all(feature = "http1", any(feature = "client", feature = "server")))]
+use super::chan;
#[cfg(all(
any(feature = "http1", feature = "http2"),
any(feature = "client", feature = "server")
))]
use super::DecodedLength;
-#[cfg(all(feature = "http1", any(feature = "client", feature = "server")))]
-use crate::common::watch;
#[cfg(all(feature = "http2", any(feature = "client", feature = "server")))]
use crate::proto::h2::ping;
-#[cfg(all(feature = "http1", any(feature = "client", feature = "server")))]
-type BodySender = mpsc::Sender>;
-#[cfg(all(feature = "http1", any(feature = "client", feature = "server")))]
-type TrailersSender = oneshot::Sender;
-
/// A stream of `Bytes`, used when receiving bodies from the network.
///
/// Note that Users should not instantiate this struct directly. When working with the hyper client,
@@ -58,9 +45,7 @@ enum Kind {
#[cfg(all(feature = "http1", any(feature = "client", feature = "server")))]
Chan {
content_length: DecodedLength,
- want_tx: watch::Sender,
- data_rx: mpsc::Receiver>,
- trailers_rx: oneshot::Receiver,
+ rx: chan::Receiver,
},
#[cfg(all(feature = "http2", any(feature = "client", feature = "server")))]
H2 {
@@ -86,18 +71,8 @@ enum Kind {
///
/// [`Body::channel()`]: struct.Body.html#method.channel
/// [`Sender::abort()`]: struct.Sender.html#method.abort
-#[must_use = "Sender does nothing unless sent on"]
-#[cfg(all(feature = "http1", any(feature = "client", feature = "server")))]
-pub(crate) struct Sender {
- want_rx: watch::Receiver,
- data_tx: BodySender,
- trailers_tx: Option,
-}
-
-#[cfg(all(feature = "http1", any(feature = "client", feature = "server")))]
-const WANT_PENDING: usize = 1;
#[cfg(all(feature = "http1", any(feature = "client", feature = "server")))]
-const WANT_READY: usize = 2;
+pub(crate) use super::chan::Sender;
impl Incoming {
/// Create a `Body` stream with an associated sender half.
@@ -112,26 +87,8 @@ impl Incoming {
#[cfg(all(feature = "http1", any(feature = "client", feature = "server")))]
pub(crate) fn new_channel(content_length: DecodedLength, wanter: bool) -> (Sender, Incoming) {
- let (data_tx, data_rx) = mpsc::channel(0);
- let (trailers_tx, trailers_rx) = oneshot::channel();
-
- // If wanter is true, `Sender::poll_ready()` won't becoming ready
- // until the `Body` has been polled for data once.
- let want = if wanter { WANT_PENDING } else { WANT_READY };
-
- let (want_tx, want_rx) = watch::channel(want);
-
- let tx = Sender {
- want_rx,
- data_tx,
- trailers_tx: Some(trailers_tx),
- };
- let rx = Incoming::new(Kind::Chan {
- content_length,
- want_tx,
- data_rx,
- trailers_rx,
- });
+ let (tx, rx) = chan::channel(wanter);
+ let rx = Incoming::new(Kind::Chan { content_length, rx });
(tx, rx)
}
@@ -172,15 +129,12 @@ impl Incoming {
#[cfg(feature = "ffi")]
pub(crate) fn as_ffi_mut(&mut self) -> &mut crate::ffi::UserBody {
- match self.kind {
- Kind::Ffi(ref mut body) => return body,
- _ => {
- self.kind = Kind::Ffi(crate::ffi::UserBody::new());
- }
+ if !matches!(self.kind, Kind::Ffi(_)) {
+ self.kind = Kind::Ffi(crate::ffi::UserBody::new());
}
- match self.kind {
- Kind::Ffi(ref mut body) => body,
+ match &mut self.kind {
+ Kind::Ffi(body) => body,
_ => unreachable!(),
}
}
@@ -208,36 +162,25 @@ impl Body for Incoming {
)]
cx: &mut Context<'_>,
) -> Poll, Self::Error>>> {
- match self.kind {
+ match &mut self.kind {
Kind::Empty => Poll::Ready(None),
#[cfg(all(feature = "http1", any(feature = "client", feature = "server")))]
Kind::Chan {
- content_length: ref mut len,
- ref mut data_rx,
- ref mut want_tx,
- ref mut trailers_rx,
+ content_length: len,
+ rx,
} => {
- want_tx.send(WANT_READY);
-
- if !data_rx.is_terminated() {
- if let Some(chunk) = ready!(Pin::new(data_rx).poll_next(cx)?) {
- len.sub_if(chunk.len() as u64);
- return Poll::Ready(Some(Ok(Frame::data(chunk))));
- }
- }
-
- // check trailers after data is terminated
- match ready!(Pin::new(trailers_rx).poll(cx)) {
- Ok(t) => Poll::Ready(Some(Ok(Frame::trailers(t)))),
- Err(_) => Poll::Ready(None),
+ if let Some(chunk) = ready!(rx.poll_next(cx)?) {
+ len.sub_if(chunk.len() as u64);
+ return Poll::Ready(Some(Ok(Frame::data(chunk))));
}
+ Poll::Ready(rx.take_trailers().map(Frame::trailers).map(Ok))
}
#[cfg(all(feature = "http2", any(feature = "client", feature = "server")))]
Kind::H2 {
- ref mut data_done,
- ref ping,
- recv: ref mut h2,
- content_length: ref mut len,
+ data_done,
+ ping,
+ recv: h2,
+ content_length: len,
} => {
if !*data_done {
match ready!(h2.poll_data(cx)) {
@@ -248,14 +191,14 @@ impl Body for Incoming {
return Poll::Ready(Some(Ok(Frame::data(bytes))));
}
Some(Err(e)) => {
- return match e.reason() {
- // These reasons should cause the body reading to stop, but not fail it.
- // The same logic as for `Read for H2Upgraded` is applied here.
- Some(h2::Reason::NO_ERROR) | Some(h2::Reason::CANCEL) => {
- Poll::Ready(None)
- }
- _ => Poll::Ready(Some(Err(crate::Error::new_body(e)))),
- };
+ if let Some(h2::Reason::NO_ERROR) = e.reason() {
+ // As mentioned in RFC 7540 Section 8.1, a RST_STREAM with NO_ERROR
+ // indicates an early response, and should cause the body reading
+ // to stop, but not fail it:
+ return Poll::Ready(None);
+ } else {
+ return Poll::Ready(Some(Err(crate::Error::new_body(e))));
+ }
}
None => {
*data_done = true;
@@ -270,22 +213,31 @@ impl Body for Incoming {
ping.record_non_data();
Poll::Ready(Ok(t.map(Frame::trailers)).transpose())
}
- Err(e) => Poll::Ready(Some(Err(crate::Error::new_h2(e)))),
+ Err(e) => {
+ if let Some(h2::Reason::NO_ERROR) = e.reason() {
+ // Same as above, a RST_STREAM with NO_ERROR indicates an early
+ // response, and should cause reading the trailers to stop, but
+ // not fail it:
+ Poll::Ready(None)
+ } else {
+ Poll::Ready(Some(Err(crate::Error::new_h2(e))))
+ }
+ }
}
}
#[cfg(feature = "ffi")]
- Kind::Ffi(ref mut body) => body.poll_data(cx),
+ Kind::Ffi(body) => body.poll_data(cx),
}
}
fn is_end_stream(&self) -> bool {
- match self.kind {
+ match &self.kind {
Kind::Empty => true,
#[cfg(all(feature = "http1", any(feature = "client", feature = "server")))]
- Kind::Chan { content_length, .. } => content_length == DecodedLength::ZERO,
+ Kind::Chan { content_length, .. } => *content_length == DecodedLength::ZERO,
#[cfg(all(feature = "http2", any(feature = "client", feature = "server")))]
- Kind::H2 { recv: ref h2, .. } => h2.is_end_stream(),
+ Kind::H2 { recv: h2, .. } => h2.is_end_stream(),
#[cfg(feature = "ffi")]
Kind::Ffi(..) => false,
}
@@ -347,123 +299,22 @@ impl fmt::Debug for Incoming {
}
}
-#[cfg(all(feature = "http1", any(feature = "client", feature = "server")))]
-impl Sender {
- /// Check to see if this `Sender` can send more data.
- pub(crate) fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> {
- // Check if the receiver end has tried polling for the body yet
- ready!(self.poll_want(cx)?);
- self.data_tx
- .poll_ready(cx)
- .map_err(|_| crate::Error::new_closed())
- }
-
- fn poll_want(&mut self, cx: &mut Context<'_>) -> Poll> {
- match self.want_rx.load(cx) {
- WANT_READY => Poll::Ready(Ok(())),
- WANT_PENDING => Poll::Pending,
- watch::CLOSED => Poll::Ready(Err(crate::Error::new_closed())),
- unexpected => unreachable!("want_rx value: {}", unexpected),
- }
- }
-
- #[cfg(test)]
- async fn ready(&mut self) -> crate::Result<()> {
- futures_util::future::poll_fn(|cx| self.poll_ready(cx)).await
- }
-
- /// Send data on data channel when it is ready.
- #[cfg(test)]
- #[allow(unused)]
- pub(crate) async fn send_data(&mut self, chunk: Bytes) -> crate::Result<()> {
- self.ready().await?;
- self.data_tx
- .try_send(Ok(chunk))
- .map_err(|_| crate::Error::new_closed())
- }
-
- /// Send trailers on trailers channel.
- #[allow(unused)]
- pub(crate) async fn send_trailers(&mut self, trailers: HeaderMap) -> crate::Result<()> {
- let tx = match self.trailers_tx.take() {
- Some(tx) => tx,
- None => return Err(crate::Error::new_closed()),
- };
- tx.send(trailers).map_err(|_| crate::Error::new_closed())
- }
-
- /// Try to send data on this channel.
- ///
- /// # Errors
- ///
- /// Returns `Err(Bytes)` if the channel could not (currently) accept
- /// another `Bytes`.
- ///
- /// # Note
- ///
- /// This is mostly useful for when trying to send from some other thread
- /// that doesn't have an async context. If in an async context, prefer
- /// `send_data()` instead.
- #[cfg(feature = "http1")]
- pub(crate) fn try_send_data(&mut self, chunk: Bytes) -> Result<(), Bytes> {
- self.data_tx
- .try_send(Ok(chunk))
- .map_err(|err| err.into_inner().expect("just sent Ok"))
- }
-
- #[cfg(feature = "http1")]
- pub(crate) fn try_send_trailers(
- &mut self,
- trailers: HeaderMap,
- ) -> Result<(), Option> {
- let tx = match self.trailers_tx.take() {
- Some(tx) => tx,
- None => return Err(None),
- };
-
- tx.send(trailers).map_err(Some)
- }
-
- #[cfg(test)]
- pub(crate) fn abort(mut self) {
- self.send_error(crate::Error::new_body_write_aborted());
- }
-
- pub(crate) fn send_error(&mut self, err: crate::Error) {
- let _ = self
- .data_tx
- // clone so the send works even if buffer is full
- .clone()
- .try_send(Err(err));
- }
-}
-
-#[cfg(all(feature = "http1", any(feature = "client", feature = "server")))]
-impl fmt::Debug for Sender {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- #[derive(Debug)]
- struct Open;
- #[derive(Debug)]
- struct Closed;
-
- let mut builder = f.debug_tuple("Sender");
- match self.want_rx.peek() {
- watch::CLOSED => builder.field(&Closed),
- _ => builder.field(&Open),
- };
-
- builder.finish()
- }
-}
-
#[cfg(test)]
mod tests {
+ #[cfg(all(feature = "http1", any(feature = "client", feature = "server")))]
use std::mem;
+ #[cfg(all(feature = "nightly", not(miri)))]
+ use std::pin::Pin;
+ #[cfg(all(feature = "http1", any(feature = "client", feature = "server")))]
use std::task::Poll;
+ #[cfg(all(feature = "http1", any(feature = "client", feature = "server")))]
use super::{Body, Incoming, SizeHint};
#[cfg(all(feature = "http1", any(feature = "client", feature = "server")))]
use super::{DecodedLength, Sender};
+ #[cfg(all(feature = "nightly", not(miri)))]
+ use bytes::Bytes;
+ #[cfg(all(feature = "http1", any(feature = "client", feature = "server")))]
use http_body_util::BodyExt;
#[cfg(all(feature = "http1", any(feature = "client", feature = "server")))]
@@ -481,11 +332,9 @@ mod tests {
body_expected_size,
);
- //assert_eq!(body_size, mem::size_of::>(), "Option");
-
assert_eq!(
mem::size_of::(),
- mem::size_of::() * 5,
+ mem::size_of::() * 2,
"Sender"
);
@@ -496,6 +345,51 @@ mod tests {
);
}
+ #[cfg(all(feature = "nightly", not(miri)))]
+ #[bench]
+ fn bench_channel_create_and_drop(b: &mut test::Bencher) {
+ b.iter(|| {
+ let _ = test::black_box(Incoming::new_channel(
+ DecodedLength::CHUNKED,
+ /* wanter = */ false,
+ ));
+ });
+ }
+
+ #[cfg(all(feature = "nightly", not(miri)))]
+ #[bench]
+ fn bench_channel_data_handoff(b: &mut test::Bencher) {
+ let (mut tx, mut body) =
+ Incoming::new_channel(DecodedLength::CHUNKED, /* wanter = */ false);
+ let mut cx = std::task::Context::from_waker(std::task::Waker::noop());
+
+ b.iter(|| {
+ assert!(tx.poll_ready(&mut cx).is_ready());
+ tx.try_send_data(Bytes::from_static(b"hello world"))
+ .unwrap();
+ let frame = match Pin::new(&mut body).poll_frame(&mut cx) {
+ Poll::Ready(Some(Ok(frame))) => frame,
+ unexpected => panic!("unexpected body poll: {unexpected:?}"),
+ };
+ test::black_box(frame);
+ });
+ }
+
+ #[cfg(all(feature = "nightly", not(miri)))]
+ #[bench]
+ fn bench_channel_want_transition(b: &mut test::Bencher) {
+ let mut cx = std::task::Context::from_waker(std::task::Waker::noop());
+
+ b.iter(|| {
+ let (mut tx, mut body) =
+ Incoming::new_channel(DecodedLength::CHUNKED, /* wanter = */ true);
+ assert!(tx.poll_ready(&mut cx).is_pending());
+ assert!(Pin::new(&mut body).poll_frame(&mut cx).is_pending());
+ assert!(tx.poll_ready(&mut cx).is_ready());
+ let _ = test::black_box((tx, body));
+ });
+ }
+
#[cfg(all(feature = "http1", any(feature = "client", feature = "server")))]
#[test]
fn size_hint() {
@@ -620,8 +514,8 @@ mod tests {
drop(rx);
assert!(tx_ready.is_woken(), "dropping rx wakes tx");
- match tx_ready.poll() {
- Poll::Ready(Err(ref e)) if e.is_closed() => (),
+ match &tx_ready.poll() {
+ Poll::Ready(Err(e)) if e.is_closed() => (),
unexpected => panic!("tx poll ready unexpected: {:?}", unexpected),
}
}
diff --git a/src/body/length.rs b/src/body/length.rs
index e5eab7449f..1ed5e0d031 100644
--- a/src/body/length.rs
+++ b/src/body/length.rs
@@ -31,7 +31,7 @@ impl DecodedLength {
/// Takes the length as a content-length without other checks.
///
/// Should only be called if previously confirmed this isn't
- /// CLOSE_DELIMITED or CHUNKED.
+ /// `CLOSE_DELIMITED` or `CHUNKED`.
#[inline]
#[cfg(all(any(feature = "client", feature = "server"), feature = "http1"))]
pub(crate) fn danger_len(self) -> u64 {
@@ -67,9 +67,9 @@ impl DecodedLength {
any(feature = "client", feature = "server")
))]
pub(crate) fn sub_if(&mut self, amt: u64) {
- match *self {
- DecodedLength::CHUNKED | DecodedLength::CLOSE_DELIMITED => (),
- DecodedLength(ref mut known) => {
+ match self {
+ &mut DecodedLength::CHUNKED | &mut DecodedLength::CLOSE_DELIMITED => (),
+ DecodedLength(known) => {
*known -= amt;
}
}
@@ -102,7 +102,7 @@ impl fmt::Display for DecodedLength {
DecodedLength::CLOSE_DELIMITED => f.write_str("close-delimited"),
DecodedLength::CHUNKED => f.write_str("chunked encoding"),
DecodedLength::ZERO => f.write_str("empty"),
- DecodedLength(n) => write!(f, "content-length ({} bytes)", n),
+ DecodedLength(n) => write!(f, "content-length ({n} bytes)"),
}
}
}
diff --git a/src/body/mod.rs b/src/body/mod.rs
index 7b71d98be4..42678b63d1 100644
--- a/src/body/mod.rs
+++ b/src/body/mod.rs
@@ -1,4 +1,4 @@
-//! Streaming bodies for Requests and Responses
+//! Streaming bodies for Requests and Responses.
//!
//! For both [Clients](crate::client) and [Servers](crate::server), requests and
//! responses use streaming bodies, instead of complete buffering. This
@@ -17,7 +17,52 @@
//! There are additional implementations available in [`http-body-util`][],
//! such as a `Full` or `Empty` body.
//!
+//! ## Reading a body
+//!
+//! The [`BodyExt`][] extension trait provides an asynchronous way to read the
+//! frames of a body. A frame can contain either data or trailers:
+//!
+//! ```
+//! use http_body_util::BodyExt as _;
+//! use hyper::body::Incoming;
+//!
+//! async fn read_body(mut body: Incoming) -> Result<(), hyper::Error> {
+//! while let Some(frame) = body.frame().await {
+//! let frame = frame?;
+//!
+//! if let Some(data) = frame.data_ref() {
+//! println!("received {} bytes", data.len());
+//! }
+//!
+//! if let Some(trailers) = frame.trailers_ref() {
+//! println!("received trailers: {trailers:?}");
+//! }
+//! }
+//!
+//! Ok(())
+//! }
+//! ```
+//!
+//! A body only advances when it is polled. Processing each frame before
+//! polling for the next one preserves back-pressure on the connection.
+//!
+//! If a body is known to be small, it can be collected into memory instead:
+//!
+//! ```
+//! use http_body_util::BodyExt as _;
+//! use hyper::body::{Bytes, Incoming};
+//!
+//! /// Consider using `Limited` if the body is untrusted.
+//! async fn read_entire_body(body: Incoming) -> Result {
+//! Ok(body.collect().await?.to_bytes())
+//! }
+//! ```
+//!
+//! Collecting buffers the whole body, so it should be avoided for large or
+//! untrusted bodies unless their size is limited.
+//!
//! [`http-body-util`]: https://docs.rs/http-body-util
+//! [`BodyExt`]: https://docs.rs/http-body-util/latest/http_body_util/trait.BodyExt.html
pub use bytes::{Buf, Bytes};
pub use http_body::Body;
@@ -34,6 +79,8 @@ pub(crate) use self::incoming::Sender;
))]
pub(crate) use self::length::DecodedLength;
+#[cfg(all(any(feature = "client", feature = "server"), feature = "http1"))]
+mod chan;
mod incoming;
#[cfg(all(
any(feature = "http1", feature = "http2"),
diff --git a/src/cfg.rs b/src/cfg.rs
index 71a5351d21..dbd52e1381 100644
--- a/src/cfg.rs
+++ b/src/cfg.rs
@@ -15,7 +15,7 @@ macro_rules! cfg_proto {
($($item:item)*) => {
cfg_feature! {
#![all(
- any(feature = "http1", feature = "http2"),
+ any(feature = "http1", feature = "http2", feature = "http3"),
any(feature = "client", feature = "server"),
)]
$($item)*
diff --git a/src/client/conn/http1.rs b/src/client/conn/http1.rs
index 85c1fdc665..43c2564280 100644
--- a/src/client/conn/http1.rs
+++ b/src/client/conn/http1.rs
@@ -1,4 +1,4 @@
-//! HTTP/1 client connections
+//! HTTP/1 client connections.
use std::error::Error as StdError;
use std::fmt;
@@ -49,7 +49,14 @@ pub struct Parts {
/// In most cases, this should just be spawned into an executor, so that it
/// can process incoming and outgoing messages, notice hangups, and the like.
///
-/// Instances of this type are typically created via the [`handshake`] function
+/// Instances of this type are typically created via the [`handshake`] function.
+///
+/// # Drop behavior
+///
+/// Dropping the `Connection` will close the underlying IO resource.
+/// Any in-flight requests that have not received a response will be
+/// interrupted. If graceful shutdown is desired, poll the connection
+/// until it completes instead of dropping.
#[must_use = "futures do nothing unless polled"]
pub struct Connection
where
@@ -90,11 +97,22 @@ where
/// Prevent shutdown of the underlying IO object at the end of service the request,
/// instead run `into_parts`. This is a convenience wrapper over `poll_without_shutdown`.
+ ///
+ /// # Errors
+ ///
+ /// Returns an error if the connection encounters an error while being polled to completion.
+ #[allow(clippy::missing_panics_doc)]
pub async fn without_shutdown(self) -> crate::Result> {
let mut conn = Some(self);
crate::common::future::poll_fn(move |cx| -> Poll>> {
- ready!(conn.as_mut().unwrap().poll_without_shutdown(cx))?;
- Poll::Ready(Ok(conn.take().unwrap().into_parts()))
+ ready!(conn
+ .as_mut()
+ .expect("client connection polled after completion")
+ .poll_without_shutdown(cx))?;
+ Poll::Ready(Ok(conn
+ .take()
+ .expect("client connection missing before completion")
+ .into_parts()))
})
.await
}
@@ -114,6 +132,7 @@ pub struct Builder {
h1_title_case_headers: bool,
h1_preserve_header_case: bool,
h1_max_headers: Option,
+ h1_max_header_size: Option,
#[cfg(feature = "ffi")]
h1_preserve_header_order: bool,
h1_read_buf_exact_size: Option,
@@ -124,6 +143,10 @@ pub struct Builder {
///
/// This is a shortcut for `Builder::new().handshake(io)`.
/// See [`client::conn`](crate::client::conn) for more.
+///
+/// # Errors
+///
+/// Returns an error if the HTTP/1 connection handshake fails.
pub async fn handshake(io: T) -> crate::Result<(SendRequest, Connection)>
where
T: Read + Write + Unpin,
@@ -144,7 +167,9 @@ impl SendRequest {
self.dispatch.poll_ready(cx)
}
- /// Waits until the dispatcher is ready
+ /// Waits until the dispatcher is ready.
+ ///
+ /// # Errors
///
/// If the associated connection is closed, this returns an Error.
pub async fn ready(&mut self) -> crate::Result<()> {
@@ -188,6 +213,20 @@ where
///
/// This is however not enforced or validated and it is up to the user
/// of this method to ensure the `Uri` is correct for their intended purpose.
+ ///
+ /// # Cancel safety
+ ///
+ /// Dropping the returned future is the supported way to cancel an
+ /// in-flight HTTP/1 request. Because HTTP/1 has no in-protocol way to
+ /// abort a single request without affecting the shared connection,
+ /// hyper closes the underlying connection when a request future is
+ /// dropped before completion. Any subsequent calls on the same
+ /// [`SendRequest`] will return a `canceled` error.
+ ///
+ /// # Errors
+ ///
+ /// Returns an error if the connection is not ready or if an error occurs while
+ /// processing the request.
pub fn send_request(
&mut self,
req: Request,
@@ -200,7 +239,7 @@ where
Ok(Ok(resp)) => Ok(resp),
Ok(Err(err)) => Err(err),
// this is definite bug if it happens, but it shouldn't happen!
- Err(_canceled) => panic!("dispatch dropped without returning error"),
+ Err(_canceled) => unreachable!("dispatch dropped without returning error"),
},
Err(_req) => {
debug!("connection was not ready");
@@ -214,10 +253,11 @@ where
///
/// Returns a future that if successful, yields the `Response`.
///
- /// # Error
+ /// # Errors
///
/// If there was an error before trying to serialize the request to the
/// connection, the message will be returned as part of this error.
+ #[allow(clippy::result_large_err)]
pub fn try_send_request(
&mut self,
req: Request,
@@ -229,7 +269,7 @@ where
Ok(Ok(res)) => Ok(res),
Ok(Err(err)) => Err(err),
// this is definite bug if it happens, but it shouldn't happen!
- Err(_) => panic!("dispatch dropped without returning error"),
+ Err(_) => unreachable!("dispatch dropped without returning error"),
},
Err(req) => {
debug!("connection was not ready");
@@ -310,10 +350,11 @@ impl Builder {
h09_responses: false,
h1_writev: None,
h1_read_buf_exact_size: None,
- h1_parser_config: Default::default(),
+ h1_parser_config: ParserConfig::default(),
h1_title_case_headers: false,
h1_preserve_header_case: false,
h1_max_headers: None,
+ h1_max_header_size: None,
#[cfg(feature = "ffi")]
h1_preserve_header_order: false,
h1_max_buf_size: None,
@@ -409,11 +450,11 @@ impl Builder {
/// but may also improve performance when an IO transport doesn't
/// support vectored writes well, such as most TLS implementations.
///
- /// Setting this to true will force hyper to use queued strategy
- /// which may eliminate unnecessary cloning on some TLS backends
+ /// Setting this to true will force hyper to use queued strategy,
+ /// which may eliminate unnecessary cloning on some TLS backends.
///
/// Default is `auto`. In this mode hyper will try to guess which
- /// mode to use
+ /// mode to use.
pub fn writev(&mut self, enabled: bool) -> &mut Builder {
self.h1_writev = Some(enabled);
self
@@ -462,6 +503,21 @@ impl Builder {
self
}
+ /// Set the maximum size of response headers (including the status line) in bytes.
+ ///
+ /// If the server sends headers exceeding this limit, the error "message head is too large"
+ /// is returned.
+ ///
+ /// If not configured, then the [`max_buf_size`](Builder::max_buf_size) will naturally be reached and applied.
+ ///
+ /// This value is also used as the maximum size limit for chunked trailers.
+ ///
+ /// Default is `None`.
+ pub fn max_header_size(&mut self, val: usize) -> &mut Self {
+ self.h1_max_header_size = Some(val);
+ self
+ }
+
/// Set whether to support preserving original header order.
///
/// Currently, this will record the order in which headers are received, and store this
@@ -511,6 +567,10 @@ impl Builder {
///
/// Note, if [`Connection`] is not `await`-ed, [`SendRequest`] will
/// do nothing.
+ ///
+ /// # Errors
+ ///
+ /// Returns an error if the HTTP/1connection handshake fails.
pub fn handshake(
&self,
io: T,
@@ -545,6 +605,9 @@ impl Builder {
if let Some(max_headers) = opts.h1_max_headers {
conn.set_http1_max_headers(max_headers);
}
+ if let Some(max_header_size) = opts.h1_max_header_size {
+ conn.set_http1_max_header_size(max_header_size);
+ }
#[cfg(feature = "ffi")]
if opts.h1_preserve_header_order {
conn.set_preserve_header_order();
@@ -569,10 +632,11 @@ impl Builder {
}
mod upgrades {
+ use super::{Connection, Context, Future, Parts, Pin, Poll, Read, StdError, Write};
+ use crate::body::Body;
+ use crate::proto::Dispatched;
use crate::upgrade::Upgraded;
-
- use super::*;
-
+ use futures_core::ready;
// A future binding a connection with a Service with Upgrade support.
//
// This type is unnameable outside the crate.
@@ -597,10 +661,22 @@ mod upgrades {
type Output = crate::Result<()>;
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll {
- match ready!(Pin::new(&mut self.inner.as_mut().unwrap().inner).poll(cx)) {
- Ok(proto::Dispatched::Shutdown) => Poll::Ready(Ok(())),
- Ok(proto::Dispatched::Upgrade(pending)) => {
- let Parts { io, read_buf } = self.inner.take().unwrap().into_parts();
+ match ready!(Pin::new(
+ &mut self
+ .inner
+ .as_mut()
+ .expect("upgradeable client connection polled after upgrade")
+ .inner,
+ )
+ .poll(cx))
+ {
+ Ok(Dispatched::Shutdown) => Poll::Ready(Ok(())),
+ Ok(Dispatched::Upgrade(pending)) => {
+ let Parts { io, read_buf } = self
+ .inner
+ .take()
+ .expect("upgradeable client connection missing after upgrade")
+ .into_parts();
pending.fulfill(Upgraded::new(io, read_buf));
Poll::Ready(Ok(()))
}
diff --git a/src/client/conn/http2.rs b/src/client/conn/http2.rs
index 0efaabe41e..4df19a10cc 100644
--- a/src/client/conn/http2.rs
+++ b/src/client/conn/http2.rs
@@ -1,4 +1,4 @@
-//! HTTP/2 client connections
+//! HTTP/2 client connections.
use std::error::Error;
use std::fmt;
@@ -38,7 +38,14 @@ impl Clone for SendRequest {
/// In most cases, this should just be spawned into an executor, so that it
/// can process incoming and outgoing messages, notice hangups, and the like.
///
-/// Instances of this type are typically created via the [`handshake`] function
+/// Instances of this type are typically created via the [`handshake`] function.
+///
+/// # Drop behavior
+///
+/// Dropping the `Connection` will close the underlying IO resource.
+/// Any in-flight requests that have not received a response will be
+/// interrupted. If graceful shutdown is desired, poll the connection
+/// until it completes instead of dropping.
#[must_use = "futures do nothing unless polled"]
pub struct Connection
where
@@ -67,6 +74,10 @@ pub struct Builder {
///
/// This is a shortcut for `Builder::new(exec).handshake(io)`.
/// See [`client::conn`](crate::client::conn) for more.
+///
+/// # Errors
+///
+/// Returns an error if the HTTP/2 connection handshake fails.
pub async fn handshake(
exec: E,
io: T,
@@ -95,7 +106,9 @@ impl SendRequest {
}
}
- /// Waits until the dispatcher is ready
+ /// Waits until the dispatcher is ready.
+ ///
+ /// # Errors
///
/// If the associated connection is closed, this returns an Error.
pub async fn ready(&mut self) -> crate::Result<()> {
@@ -131,6 +144,20 @@ where
///
/// Absolute-form `Uri`s are not required. If received, they will be serialized
/// as-is.
+ ///
+ /// # Cancel safety
+ ///
+ /// Dropping the returned future is the supported way to cancel an
+ /// in-flight HTTP/2 request. The stream is reset with `RST_STREAM`
+ /// (`CANCEL` error code); the shared connection remains usable for
+ /// other in-flight and future requests. The peer is notified
+ /// immediately rather than continuing to send a response body that
+ /// would be discarded.
+ ///
+ /// # Errors
+ ///
+ /// Returns an error if the connection is not ready or if an error occurs while
+ /// processing the request.
pub fn send_request(
&mut self,
req: Request,
@@ -143,7 +170,7 @@ where
Ok(Ok(resp)) => Ok(resp),
Ok(Err(err)) => Err(err),
// this is definite bug if it happens, but it shouldn't happen!
- Err(_canceled) => panic!("dispatch dropped without returning error"),
+ Err(_canceled) => unreachable!("dispatch dropped without returning error"),
},
Err(_req) => {
debug!("connection was not ready");
@@ -158,10 +185,11 @@ where
///
/// Returns a future that if successful, yields the `Response`.
///
- /// # Error
+ /// # Errors
///
/// If there was an error before trying to serialize the request to the
/// connection, the message will be returned as part of this error.
+ #[allow(clippy::result_large_err)]
pub fn try_send_request(
&mut self,
req: Request,
@@ -173,7 +201,7 @@ where
Ok(Ok(res)) => Ok(res),
Ok(Err(err)) => Err(err),
// this is definite bug if it happens, but it shouldn't happen!
- Err(_) => panic!("dispatch dropped without returning error"),
+ Err(_) => unreachable!("dispatch dropped without returning error"),
},
Err(req) => {
debug!("connection was not ready");
@@ -216,6 +244,26 @@ where
pub fn is_extended_connect_protocol_enabled(&self) -> bool {
self.inner.1.is_extended_connect_protocol_enabled()
}
+
+ /// Returns the current maximum send stream count.
+ ///
+ /// This setting is configured in a [`SETTINGS_MAX_CONCURRENT_STREAMS` parameter][1] in a `SETTINGS` frame,
+ /// and may change throughout the connection lifetime.
+ ///
+ /// [1]: https://datatracker.ietf.org/doc/html/rfc7540#section-5.1.2
+ pub fn current_max_send_streams(&self) -> usize {
+ self.inner.1.current_max_send_streams()
+ }
+
+ /// Returns the current maximum receive stream count.
+ ///
+ /// This setting is configured in a [`SETTINGS_MAX_CONCURRENT_STREAMS` parameter][1] in a `SETTINGS` frame,
+ /// and may change throughout the connection lifetime.
+ ///
+ /// [1]: https://datatracker.ietf.org/doc/html/rfc7540#section-5.1.2
+ pub fn current_max_recv_streams(&self) -> usize {
+ self.inner.1.current_max_recv_streams()
+ }
}
impl fmt::Debug for Connection
@@ -262,7 +310,7 @@ where
Builder {
exec,
timer: Time::Empty,
- h2_builder: Default::default(),
+ h2_builder: proto::h2::client::Config::default(),
}
}
@@ -291,7 +339,7 @@ where
self
}
- /// Sets the max connection-level flow control for HTTP2
+ /// Sets the max connection-level flow control for HTTP2.
///
/// Passing `None` will do nothing.
///
@@ -387,7 +435,7 @@ where
///
/// See [Section 5.1.2] in the HTTP/2 spec for more details.
///
- /// [Section 5.1.2]: https://http2.github.io/http2-spec/#rfc.section.5.1.2
+ /// [Section 5.1.2]: https://httpwg.org/specs/rfc7540.html#rfc.section.5.1.2
pub fn max_concurrent_streams(&mut self, max: impl Into>) -> &mut Self {
self.h2_builder.max_concurrent_streams = max.into();
self
@@ -465,11 +513,56 @@ where
self
}
+ /// Configures the maximum number of local resets due to protocol errors made by the remote end.
+ ///
+ /// See the documentation of [`h2::client::Builder::max_local_error_reset_streams`] for more
+ /// details.
+ ///
+ /// The default value is 1024.
+ pub fn max_local_error_reset_streams(&mut self, max: impl Into >) -> &mut Self {
+ self.h2_builder.max_local_error_reset_streams = max.into();
+ self
+ }
+
+ /// Sets the duration to remember locally reset streams.
+ ///
+ /// When a stream is explicitly reset by either the client or the server,
+ /// the HTTP/2 specification requires that any further frames received for
+ /// that stream must be ignored for "some time".
+ ///
+ /// In order to satisfy the specification, internal state must be maintained
+ /// to implement the behavior. This state grows linearly with the number of
+ /// streams that are locally reset.
+ ///
+ /// The `reset_stream_duration` setting configures the max amount of time
+ /// this state will be maintained in memory. Once the duration elapses, the
+ /// stream state is purged from memory.
+ ///
+ /// Once the stream has been fully purged from memory, any additional frames
+ /// received for that stream will result in a connection level protocol
+ /// error, forcing the connection to terminate.
+ ///
+ /// The default value is determined by the `h2` crate, and is currently
+ /// 1 second.
+ ///
+ /// See the documentation of [`h2::client::Builder::reset_stream_duration`] for more
+ /// details.
+ ///
+ /// [`h2::client::Builder::reset_stream_duration`]: https://docs.rs/h2/client/struct.Builder.html#method.reset_stream_duration
+ pub fn reset_stream_duration(&mut self, dur: Duration) -> &mut Self {
+ self.h2_builder.reset_stream_duration = Some(dur);
+ self
+ }
+
/// Constructs a connection with the configured options and IO.
/// See [`client::conn`](crate::client::conn) for more.
///
/// Note, if [`Connection`] is not `await`-ed, [`SendRequest`] will
/// do nothing.
+ ///
+ /// # Errors
+ ///
+ /// Returns an error if the HTTP/2 connection handshake fails.
pub fn handshake(
&self,
io: T,
diff --git a/src/client/dispatch.rs b/src/client/dispatch.rs
index 2cbc1b9225..76a9c3e20c 100644
--- a/src/client/dispatch.rs
+++ b/src/client/dispatch.rs
@@ -66,7 +66,7 @@ pub(crate) struct Sender {
/// has been dropped. However, this version can be cloned.
#[cfg(feature = "http2")]
pub(crate) struct UnboundedSender {
- /// Only used for `is_closed`, since mpsc::UnboundedSender cannot be checked.
+ /// Only used for `is_closed`, since `mpsc::UnboundedSender` cannot be checked.
giver: want::SharedGiver,
inner: mpsc::UnboundedSender>,
}
@@ -111,7 +111,7 @@ impl Sender {
let (tx, rx) = oneshot::channel();
self.inner
.send(Envelope(Some((val, Callback::Retry(Some(tx))))))
- .map(move |_| rx)
+ .map(move |()| rx)
.map_err(|mut e| (e.0).0.take().expect("envelope not dropped").0)
}
@@ -123,7 +123,7 @@ impl Sender {
let (tx, rx) = oneshot::channel();
self.inner
.send(Envelope(Some((val, Callback::NoRetry(Some(tx))))))
- .map(move |_| rx)
+ .map(move |()| rx)
.map_err(|mut e| (e.0).0.take().expect("envelope not dropped").0)
}
@@ -150,7 +150,7 @@ impl UnboundedSender {
let (tx, rx) = oneshot::channel();
self.inner
.send(Envelope(Some((val, Callback::Retry(Some(tx))))))
- .map(move |_| rx)
+ .map(move |()| rx)
.map_err(|mut e| (e.0).0.take().expect("envelope not dropped").0)
}
@@ -158,7 +158,7 @@ impl UnboundedSender {
let (tx, rx) = oneshot::channel();
self.inner
.send(Envelope(Some((val, Callback::NoRetry(Some(tx))))))
- .map(move |_| rx)
+ .map(move |()| rx)
.map_err(|mut e| (e.0).0.take().expect("envelope not dropped").0)
}
}
@@ -182,6 +182,12 @@ impl Receiver {
pub(crate) fn poll_recv(&mut self, cx: &mut Context<'_>) -> Poll)>> {
match self.inner.poll_recv(cx) {
Poll::Ready(item) => {
+ // A want signaled after finding the queue empty can land after
+ // the Sender's `give()` for the message taken here, as can one
+ // signaled on a coop-budget Pending with a message queued.
+ // Withdraw it, or the Sender reports this connection as ready
+ // while it is still serving this message.
+ self.taker.unwant();
Poll::Ready(item.map(|mut env| env.0.take().expect("envelope not dropped")))
}
Poll::Pending => {
@@ -197,6 +203,15 @@ impl Receiver {
self.inner.close();
}
+ #[cfg(feature = "http1")]
+ pub(crate) fn poll_close(&mut self, cx: &mut Context<'_>) -> Poll<()> {
+ self.close();
+ // A send that started before close may still be publishing its envelope.
+ // Keep the receiver alive until all such envelopes have been canceled.
+ while futures_core::ready!(self.inner.poll_recv(cx)).is_some() {}
+ Poll::Ready(())
+ }
+
#[cfg(feature = "http1")]
pub(crate) fn try_recv(&mut self) -> Option<(T, Callback)> {
match crate::common::task::now_or_never(self.inner.recv()) {
@@ -266,28 +281,34 @@ fn dispatch_gone() -> crate::Error {
impl Callback {
#[cfg(feature = "http2")]
pub(crate) fn is_canceled(&self) -> bool {
- match *self {
- Callback::Retry(Some(ref tx)) => tx.is_closed(),
- Callback::NoRetry(Some(ref tx)) => tx.is_closed(),
+ match self {
+ Callback::Retry(Some(tx)) => tx.is_closed(),
+ Callback::NoRetry(Some(tx)) => tx.is_closed(),
_ => unreachable!(),
}
}
pub(crate) fn poll_canceled(&mut self, cx: &mut Context<'_>) -> Poll<()> {
- match *self {
- Callback::Retry(Some(ref mut tx)) => tx.poll_closed(cx),
- Callback::NoRetry(Some(ref mut tx)) => tx.poll_closed(cx),
+ match self {
+ Callback::Retry(Some(tx)) => tx.poll_closed(cx),
+ Callback::NoRetry(Some(tx)) => tx.poll_closed(cx),
_ => unreachable!(),
}
}
pub(crate) fn send(mut self, val: Result>) {
- match self {
- Callback::Retry(ref mut tx) => {
- let _ = tx.take().unwrap().send(val);
+ match &mut self {
+ Callback::Retry(tx) => {
+ let _ = tx
+ .take()
+ .expect("callback sender not dropped before send")
+ .send(val);
}
- Callback::NoRetry(ref mut tx) => {
- let _ = tx.take().unwrap().send(val.map_err(|e| e.error));
+ Callback::NoRetry(tx) => {
+ let _ = tx
+ .take()
+ .expect("callback sender not dropped before send")
+ .send(val.map_err(|e| e.error));
}
}
}
@@ -364,8 +385,11 @@ where
this.call_back.set(Some(call_back));
return Poll::Pending;
}
- };
+ }
trace!("send_when canceled");
+ // Tell pipe_task to reset the h2 stream so that
+ // RST_STREAM is sent and flow-control capacity freed.
+ this.when.as_mut().cancel();
Poll::Ready(())
}
Poll::Ready(Err((error, message))) => {
@@ -399,7 +423,7 @@ mod tests {
}
/// Helper to check if the future is ready after polling once.
- struct PollOnce<'a, F>(&'a mut F);
+ struct PollOnce<'fut, F>(&'fut mut F);
impl Future for PollOnce<'_, F>
where
@@ -444,7 +468,7 @@ mod tests {
let (mut tx, mut rx) = channel::();
// one is allowed to buffer, second is rejected
- let _ = tx.try_send(Custom(1)).expect("1 buffered");
+ tx.try_send(Custom(1)).expect("1 buffered");
tx.try_send(Custom(2)).expect_err("2 not ready");
assert!(PollOnce(&mut rx).await.is_some(), "rx once");
@@ -455,7 +479,7 @@ mod tests {
assert!(PollOnce(&mut rx).await.is_none(), "rx empty");
- let _ = tx.try_send(Custom(2)).expect("2 ready");
+ tx.try_send(Custom(2)).expect("2 ready");
}
#[cfg(feature = "http2")]
@@ -464,13 +488,13 @@ mod tests {
let (tx, rx) = channel::();
let mut tx = tx.unbound();
- let _ = tx.try_send(Custom(1)).unwrap();
- let _ = tx.try_send(Custom(2)).unwrap();
- let _ = tx.try_send(Custom(3)).unwrap();
+ tx.try_send(Custom(1)).unwrap();
+ tx.try_send(Custom(2)).unwrap();
+ tx.try_send(Custom(3)).unwrap();
drop(rx);
- let _ = tx.try_send(Custom(4)).unwrap_err();
+ tx.try_send(Custom(4)).unwrap_err();
}
#[cfg(feature = "nightly")]
@@ -484,7 +508,7 @@ mod tests {
let (mut tx, mut rx) = channel::, Response>();
b.iter(move || {
- let _ = tx.send(Request::new(Incoming::empty())).unwrap();
+ tx.send(Request::new(Incoming::empty())).unwrap();
rt.block_on(async {
loop {
let poll_once = PollOnce(&mut rx);
diff --git a/src/client/mod.rs b/src/client/mod.rs
index 86e3897388..c4a5fddca3 100644
--- a/src/client/mod.rs
+++ b/src/client/mod.rs
@@ -1,4 +1,4 @@
-//! HTTP Client
+//! HTTP Client.
//!
//! hyper provides HTTP over a single connection. See the [`conn`] module.
//!
diff --git a/src/common/buf.rs b/src/common/buf.rs
index d00071551b..0bf02497f4 100644
--- a/src/common/buf.rs
+++ b/src/common/buf.rs
@@ -56,7 +56,7 @@ impl Buf for BufList {
}
#[inline]
- fn chunks_vectored<'t>(&'t self, dst: &mut [IoSlice<'t>]) -> usize {
+ fn chunks_vectored<'data>(&'data self, dst: &mut [IoSlice<'data>]) -> usize {
if dst.is_empty() {
return 0;
}
diff --git a/src/common/date.rs b/src/common/date.rs
index 59837cd60e..d348ff7bc7 100644
--- a/src/common/date.rs
+++ b/src/common/date.rs
@@ -14,14 +14,14 @@ pub(crate) const DATE_VALUE_LENGTH: usize = 29;
pub(crate) fn extend(dst: &mut Vec) {
CACHED.with(|cache| {
dst.extend_from_slice(cache.borrow().buffer());
- })
+ });
}
#[cfg(feature = "http1")]
pub(crate) fn update() {
CACHED.with(|cache| {
cache.borrow_mut().check();
- })
+ });
}
#[cfg(feature = "http2")]
@@ -74,13 +74,13 @@ impl CachedDate {
.subsec_nanos();
self.render(now);
- self.next_update = now + Duration::new(1, 0) - Duration::from_nanos(nanos as u64);
+ self.next_update = now + Duration::new(1, 0) - Duration::from_nanos(u64::from(nanos));
}
fn render(&mut self, now: SystemTime) {
self.pos = 0;
let _ = write!(self, "{}", HttpDate::from(now));
- debug_assert!(self.pos == DATE_VALUE_LENGTH);
+ debug_assert_eq!(self.pos, DATE_VALUE_LENGTH);
self.render_http2();
}
diff --git a/src/common/io/compat.rs b/src/common/io/compat.rs
index d026b6d38b..c390b130c1 100644
--- a/src/common/io/compat.rs
+++ b/src/common/io/compat.rs
@@ -23,6 +23,7 @@ impl tokio::io::AsyncRead for Compat
where
T: crate::rt::Read,
{
+ /// `poll_read` fn implementation for `Compat`.
fn poll_read(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
@@ -30,6 +31,13 @@ where
) -> Poll> {
let init = tbuf.initialized().len();
let filled = tbuf.filled().len();
+ // SAFETY:
+ // 1. `tbuf.inner_mut()` returns a raw pointer/mutable slice which we wrap into
+ // a `crate::rt::ReadBuf` that is layout-compatible with the source `tokio::io::ReadBuf`.
+ // 2. We explicitly restore the `init` and `filled` states from the original buffer
+ // to maintain the invariant that the new `ReadBuf` tracks the same progress.
+ // 3. The underlying memory remains valid and uniquely accessible via `tbuf` for
+ // the duration of this poll operation.
let (new_init, new_filled) = unsafe {
let mut buf = crate::rt::ReadBuf::uninit(tbuf.inner_mut());
buf.set_init(init);
@@ -42,6 +50,11 @@ where
};
let n_init = new_init - init;
+ // SAFETY:
+ // 1. `tbuf.assume_init(n_init)` is safe because `crate::rt::Read::poll_read`
+ // guarantees that the bytes written into the buffer were initialized.
+ // 2. `tbuf.set_filled(new_filled)` is safe because `new_filled` is derived
+ // directly from the buffer state after the successful read operation.
unsafe {
tbuf.assume_init(n_init);
tbuf.set_filled(new_filled);
diff --git a/src/common/io/rewind.rs b/src/common/io/rewind.rs
index c1f7a4f1b9..7acd809685 100644
--- a/src/common/io/rewind.rs
+++ b/src/common/io/rewind.rs
@@ -14,7 +14,11 @@ pub(crate) struct Rewind {
}
impl Rewind {
- #[cfg(test)]
+ #[cfg(all(
+ test,
+ any(feature = "client", feature = "server"),
+ any(feature = "http1", feature = "http2")
+ ))]
pub(crate) fn new(io: T) -> Self {
Rewind {
pre: None,
@@ -29,7 +33,11 @@ impl Rewind {
}
}
- #[cfg(test)]
+ #[cfg(all(
+ test,
+ any(feature = "client", feature = "server"),
+ any(feature = "http1", feature = "http2")
+ ))]
pub(crate) fn rewind(&mut self, bs: Bytes) {
debug_assert!(self.pre.is_none());
self.pre = Some(bs);
@@ -38,10 +46,6 @@ impl Rewind {
pub(crate) fn into_inner(self) -> (T, Bytes) {
(self.inner, self.pre.unwrap_or_default())
}
-
- // pub(crate) fn get_mut(&mut self) -> &mut T {
- // &mut self.inner
- // }
}
impl Read for Rewind
diff --git a/src/common/lock.rs b/src/common/lock.rs
new file mode 100644
index 0000000000..24b5d104f1
--- /dev/null
+++ b/src/common/lock.rs
@@ -0,0 +1,16 @@
+use std::sync::LockResult;
+
+pub(crate) trait LockResultExt {
+ fn panic_if_poisoned(self) -> T;
+}
+
+#[allow(clippy::panic)]
+impl LockResultExt for LockResult {
+ #[track_caller]
+ fn panic_if_poisoned(self) -> T {
+ match self {
+ Ok(inner) => inner,
+ Err(err) => panic!("lock poisoned by panic: {err}"),
+ }
+ }
+}
diff --git a/src/common/mod.rs b/src/common/mod.rs
index 4b73437203..b2986a300f 100644
--- a/src/common/mod.rs
+++ b/src/common/mod.rs
@@ -7,9 +7,11 @@ pub(crate) mod either;
#[cfg(any(
all(feature = "client", any(feature = "http1", feature = "http2")),
all(feature = "server", feature = "http1"),
+ all(feature = "server", feature = "http3"),
))]
pub(crate) mod future;
pub(crate) mod io;
+pub(crate) mod lock;
#[cfg(all(any(feature = "client", feature = "server"), feature = "http1"))]
pub(crate) mod task;
#[cfg(any(
@@ -17,5 +19,3 @@ pub(crate) mod task;
all(any(feature = "client", feature = "server"), feature = "http2"),
))]
pub(crate) mod time;
-#[cfg(all(any(feature = "client", feature = "server"), feature = "http1"))]
-pub(crate) mod watch;
diff --git a/src/common/task.rs b/src/common/task.rs
index 90a0883465..7e6024557a 100644
--- a/src/common/task.rs
+++ b/src/common/task.rs
@@ -36,8 +36,8 @@ fn noop_waker() -> Waker {
pub(crate) fn now_or_never(fut: F) -> Option {
let waker = noop_waker();
let mut cx = Context::from_waker(&waker);
- // TODO: replace with std::pin::pin! and drop pin-utils once MSRV >= 1.68
- pin_utils::pin_mut!(fut);
+ // TODO: replace with std::pin::pin! once MSRV >= 1.68
+ tokio::pin!(fut);
match fut.poll(&mut cx) {
Poll::Ready(res) => Some(res),
Poll::Pending => None,
diff --git a/src/common/time.rs b/src/common/time.rs
index ad88cb3cde..c238b4b3d4 100644
--- a/src/common/time.rs
+++ b/src/common/time.rs
@@ -32,37 +32,40 @@ impl fmt::Debug for Time {
impl Time {
#[cfg(all(any(feature = "client", feature = "server"), feature = "http2"))]
pub(crate) fn sleep(&self, duration: Duration) -> Pin> {
- match *self {
+ match &self {
+ #[allow(clippy::panic)]
Time::Empty => {
panic!("You must supply a timer.")
}
- Time::Timer(ref t) => t.sleep(duration),
+ Time::Timer(t) => t.sleep(duration),
}
}
#[cfg(all(feature = "server", feature = "http1"))]
pub(crate) fn sleep_until(&self, deadline: Instant) -> Pin> {
- match *self {
+ match &self {
+ #[allow(clippy::panic)]
Time::Empty => {
panic!("You must supply a timer.")
}
- Time::Timer(ref t) => t.sleep_until(deadline),
+ Time::Timer(t) => t.sleep_until(deadline),
}
}
pub(crate) fn now(&self) -> Instant {
- match *self {
+ match &self {
Time::Empty => Instant::now(),
- Time::Timer(ref t) => t.now(),
+ Time::Timer(t) => t.now(),
}
}
pub(crate) fn reset(&self, sleep: &mut Pin>, new_deadline: Instant) {
- match *self {
+ match &self {
+ #[allow(clippy::panic)]
Time::Empty => {
panic!("You must supply a timer.")
}
- Time::Timer(ref t) => t.reset(sleep, new_deadline),
+ Time::Timer(t) => t.reset(sleep, new_deadline),
}
}
@@ -77,7 +80,8 @@ impl Time {
Time::Timer(..) => Some(dur),
},
Dur::Configured(Some(dur)) => match self {
- Time::Empty => panic!("timeout `{}` set, but no timer set", name,),
+ #[allow(clippy::panic)]
+ Time::Empty => panic!("timeout `{name}` set, but no timer set"),
Time::Timer(..) => Some(dur),
},
Dur::Default(None) | Dur::Configured(None) => None,
diff --git a/src/common/watch.rs b/src/common/watch.rs
deleted file mode 100644
index 81acd5df70..0000000000
--- a/src/common/watch.rs
+++ /dev/null
@@ -1,73 +0,0 @@
-//! An SPSC broadcast channel.
-//!
-//! - The value can only be a `usize`.
-//! - The consumer is only notified if the value is different.
-//! - The value `0` is reserved for closed.
-
-use atomic_waker::AtomicWaker;
-use std::sync::{
- atomic::{AtomicUsize, Ordering},
- Arc,
-};
-use std::task;
-
-type Value = usize;
-
-pub(crate) const CLOSED: usize = 0;
-
-pub(crate) fn channel(initial: Value) -> (Sender, Receiver) {
- debug_assert!(
- initial != CLOSED,
- "watch::channel initial state of 0 is reserved"
- );
-
- let shared = Arc::new(Shared {
- value: AtomicUsize::new(initial),
- waker: AtomicWaker::new(),
- });
-
- (
- Sender {
- shared: shared.clone(),
- },
- Receiver { shared },
- )
-}
-
-pub(crate) struct Sender {
- shared: Arc,
-}
-
-pub(crate) struct Receiver {
- shared: Arc,
-}
-
-struct Shared {
- value: AtomicUsize,
- waker: AtomicWaker,
-}
-
-impl Sender {
- pub(crate) fn send(&mut self, value: Value) {
- if self.shared.value.swap(value, Ordering::SeqCst) != value {
- self.shared.waker.wake();
- }
- }
-}
-
-impl Drop for Sender {
- fn drop(&mut self) {
- self.send(CLOSED);
- }
-}
-
-impl Receiver {
- pub(crate) fn load(&mut self, cx: &mut task::Context<'_>) -> Value {
- self.shared.waker.register(cx.waker());
- self.shared.value.load(Ordering::SeqCst)
- }
-
- pub(crate) fn peek(&self) -> Value {
- self.shared.value.load(Ordering::Relaxed)
- }
-}
diff --git a/src/error.rs b/src/error.rs
index 8b41f9c93d..1ccd09b7a9 100644
--- a/src/error.rs
+++ b/src/error.rs
@@ -61,7 +61,7 @@ pub(super) enum Kind {
any(feature = "http1", feature = "http2")
))]
Io,
- /// User took too long to send headers
+ /// User took too long to send headers.
#[cfg(all(feature = "http1", feature = "server"))]
HeaderTimeout,
/// Error while reading a body from connection.
@@ -76,7 +76,7 @@ pub(super) enum Kind {
any(feature = "http1", feature = "http2")
))]
BodyWrite,
- /// Error calling AsyncWrite::shutdown()
+ /// Error calling `AsyncWrite::shutdown()`.
#[cfg(all(any(feature = "client", feature = "server"), feature = "http1"))]
Shutdown,
@@ -119,7 +119,7 @@ pub(super) enum Header {
#[derive(Debug)]
pub(super) enum User {
- /// Error calling user's Body::poll_data().
+ /// Error calling the user's `Body::poll_data()`.
#[cfg(all(
any(feature = "client", feature = "server"),
any(feature = "http1", feature = "http2")
@@ -131,7 +131,7 @@ pub(super) enum User {
feature = "ffi"
))]
BodyWriteAborted,
- /// User tried to send a connect request with a nonzero body
+ /// User tried to send a connect request with a nonzero body.
#[cfg(all(feature = "client", feature = "http2"))]
InvalidConnectWithBody,
/// Error from future of user's Service.
@@ -173,11 +173,22 @@ pub(super) struct TimedOut;
impl Error {
/// Returns true if this was an HTTP parse error.
+ ///
+ /// This can be caused by a malformed HTTP message, an invalid header,
+ /// an invalid URI, an invalid HTTP version, or a message head that is
+ /// too large. Use the more specific `is_parse_*` methods to determine
+ /// the exact cause.
pub fn is_parse(&self) -> bool {
matches!(self.inner.kind, Kind::Parse(_))
}
/// Returns true if this was an HTTP parse error caused by a message that was too large.
+ ///
+ /// This is triggered when the message head (request line plus headers for
+ /// HTTP/1, or header frame for HTTP/2) exceeds the configured
+ /// [`max_buf_size`](crate::server::conn::http1::Builder::max_buf_size).
+ /// It also covers the case where the URI alone exceeds the internal
+ /// maximum URI length.
#[cfg(all(feature = "http1", feature = "server"))]
pub fn is_parse_too_large(&self) -> bool {
matches!(
@@ -192,17 +203,40 @@ impl Error {
matches!(self.inner.kind, Kind::Parse(Parse::Status))
}
+ /// Returns true if this was an HTTP parse error caused by HTTP2 preface sent over an HTTP1
+ /// connection.
+ ///
+ /// This can happen when a client sends an HTTP/2 connection preface to a
+ /// server that is only expecting HTTP/1.x requests.
+ #[cfg(all(any(feature = "client", feature = "server"), feature = "http1"))]
+ pub fn is_parse_version_h2(&self) -> bool {
+ matches!(self.inner.kind, Kind::Parse(Parse::VersionH2))
+ }
+
/// Returns true if this error was caused by user code.
+ ///
+ /// For example, this can be returned when the user's `Service` returns
+ /// an error, the user's `Body` stream yields an error, or the user
+ /// sends an unexpected header combination (such as both
+ /// `content-length` and `transfer-encoding`).
pub fn is_user(&self) -> bool {
matches!(self.inner.kind, Kind::User(_))
}
/// Returns true if this was about a `Request` that was canceled.
+ ///
+ /// This typically happens when a pending request is dropped before
+ /// it can be dispatched to the connection, for example because the
+ /// connection was not ready.
pub fn is_canceled(&self) -> bool {
matches!(self.inner.kind, Kind::Canceled)
}
/// Returns true if a sender's channel is closed.
+ ///
+ /// This can occur when the other side of a client or body channel
+ /// has been dropped, indicating that the receiver is no longer
+ /// interested in the data.
pub fn is_closed(&self) -> bool {
#[cfg(not(any(
all(feature = "http1", any(feature = "client", feature = "server")),
@@ -243,6 +277,10 @@ impl Error {
}
/// Returns true if the body write was aborted.
+ ///
+ /// This occurs when the user's code explicitly aborts writing of the
+ /// outgoing body before it completes, for example by dropping the
+ /// body sender.
pub fn is_body_write_aborted(&self) -> bool {
#[cfg(not(any(
all(feature = "http1", any(feature = "client", feature = "server")),
@@ -258,6 +296,9 @@ impl Error {
}
/// Returns true if the error was caused while calling `AsyncWrite::shutdown()`.
+ ///
+ /// This can happen when the connection is being gracefully shut down
+ /// and the underlying IO reports an error during the shutdown sequence.
pub fn is_shutdown(&self) -> bool {
#[cfg(all(feature = "http1", any(feature = "client", feature = "server")))]
if matches!(self.inner.kind, Kind::Shutdown) {
@@ -267,6 +308,10 @@ impl Error {
}
/// Returns true if the error was caused by a timeout.
+ ///
+ /// For HTTP/1 servers, this includes the header read timeout (see
+ /// [`header_read_timeout`](crate::server::conn::http1::Builder::header_read_timeout)).
+ /// It also covers any timeout set via a user-provided timer.
pub fn is_timeout(&self) -> bool {
#[cfg(all(feature = "http1", feature = "server"))]
if matches!(self.inner.kind, Kind::HeaderTimeout) {
@@ -557,7 +602,7 @@ impl fmt::Debug for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut f = f.debug_tuple("hyper::Error");
f.field(&self.inner.kind);
- if let Some(ref cause) = self.inner.cause {
+ if let Some(cause) = &self.inner.cause {
f.field(cause);
}
f.finish()
diff --git a/src/ext/h1_reason_phrase.rs b/src/ext/h1_reason_phrase.rs
index adb4363689..3ebdfcf9a9 100644
--- a/src/ext/h1_reason_phrase.rs
+++ b/src/ext/h1_reason_phrase.rs
@@ -40,11 +40,16 @@ impl ReasonPhrase {
}
/// Converts a static byte slice to a reason phrase.
+ ///
+ /// # Panics
+ ///
+ /// This method will panic if 'reason' contains an invalid byte. In a const context,
+ /// this is a compile-time error instead.
pub const fn from_static(reason: &'static [u8]) -> Self {
- // TODO: this can be made const once MSRV is >= 1.57.0
- if find_invalid_byte(reason).is_some() {
- panic!("invalid byte in static reason phrase");
- }
+ assert!(
+ find_invalid_byte(reason).is_none(),
+ "invalid byte in static reason phrase"
+ );
Self(Bytes::from_static(reason))
}
diff --git a/src/ext/informational.rs b/src/ext/informational.rs
index e728580fa5..6c86b4974e 100644
--- a/src/ext/informational.rs
+++ b/src/ext/informational.rs
@@ -66,7 +66,7 @@ where
// being either a real reference, or moving the http::Response into the closure,
// in a backwards-compatible change in the future.
#[derive(Debug)]
-pub struct Response<'a>(&'a http::Response<()>);
+pub struct Response<'resp>(&'resp http::Response<()>);
impl Response<'_> {
#[inline]
diff --git a/src/ext/mod.rs b/src/ext/mod.rs
index b59d809dea..ba6886b835 100644
--- a/src/ext/mod.rs
+++ b/src/ext/mod.rs
@@ -113,8 +113,8 @@ impl Protocol {
}
#[cfg(feature = "http2")]
-impl<'a> From<&'a str> for Protocol {
- fn from(value: &'a str) -> Self {
+impl<'proto> From<&'proto str> for Protocol {
+ fn from(value: &'proto str) -> Self {
Self {
inner: h2::ext::Protocol::from(value),
}
@@ -165,10 +165,10 @@ impl HeaderCaseMap {
/// Returns a view of all spellings associated with that header name,
/// in the order they were found.
#[cfg(feature = "client")]
- pub(crate) fn get_all<'a>(
- &'a self,
+ pub(crate) fn get_all<'hdr>(
+ &'hdr self,
name: &HeaderName,
- ) -> impl Iterator- + 'a> + 'a {
+ ) -> impl Iterator
- + 'hdr> + 'hdr {
self.get_all_internal(name)
}
@@ -181,7 +181,7 @@ impl HeaderCaseMap {
#[cfg(any(feature = "client", feature = "server"))]
pub(crate) fn default() -> Self {
- Self(Default::default())
+ Self(HeaderMap::default())
}
#[cfg(any(test, feature = "ffi"))]
@@ -238,14 +238,17 @@ impl OriginalHeaderOrder {
N: IntoHeaderName + Into
+ Clone,
{
let name: HeaderName = name.into();
- let idx;
- if self.num_entries.contains_key(&name) {
- idx = self.num_entries[&name];
- *self.num_entries.get_mut(&name).unwrap() += 1;
- } else {
- idx = 0;
- self.num_entries.insert(name.clone(), 1);
- }
+ let idx = match self.num_entries.entry(name.clone()) {
+ std::collections::hash_map::Entry::Occupied(mut entry) => {
+ let idx = *entry.get();
+ *entry.get_mut() += 1;
+ idx
+ }
+ std::collections::hash_map::Entry::Vacant(entry) => {
+ entry.insert(1);
+ 0
+ }
+ };
self.entry_order.push((name, idx));
}
@@ -254,41 +257,6 @@ impl OriginalHeaderOrder {
// here.
/// This returns an iterator that provides header names and indexes
/// in the original order received.
- ///
- /// # Examples
- /// ```no_run
- /// use hyper::ext::OriginalHeaderOrder;
- /// use hyper::header::{HeaderName, HeaderValue, HeaderMap};
- ///
- /// let mut h_order = OriginalHeaderOrder::default();
- /// let mut h_map = Headermap::new();
- ///
- /// let name1 = b"Set-CookiE";
- /// let value1 = b"a=b";
- /// h_map.append(name1);
- /// h_order.append(name1);
- ///
- /// let name2 = b"Content-Encoding";
- /// let value2 = b"gzip";
- /// h_map.append(name2, value2);
- /// h_order.append(name2);
- ///
- /// let name3 = b"SET-COOKIE";
- /// let value3 = b"c=d";
- /// h_map.append(name3, value3);
- /// h_order.append(name3)
- ///
- /// let mut iter = h_order.get_in_order()
- ///
- /// let (name, idx) = iter.next();
- /// assert_eq!(b"a=b", h_map.get_all(name).nth(idx).unwrap());
- ///
- /// let (name, idx) = iter.next();
- /// assert_eq!(b"gzip", h_map.get_all(name).nth(idx).unwrap());
- ///
- /// let (name, idx) = iter.next();
- /// assert_eq!(b"c=d", h_map.get_all(name).nth(idx).unwrap());
- /// ```
pub(crate) fn get_in_order(&self) -> impl Iterator- {
self.entry_order.iter()
}
diff --git a/src/ffi/client.rs b/src/ffi/client.rs
index 63b03d874a..fcd3ef7179 100644
--- a/src/ffi/client.rs
+++ b/src/ffi/client.rs
@@ -147,9 +147,9 @@ ffi_fn! {
// Update request with original-case map of headers
req.finalize_request();
- let fut = match non_null! { &mut *conn ?= ptr::null_mut() }.tx {
- Tx::Http1(ref mut tx) => futures_util::future::Either::Left(tx.send_request(req.0)),
- Tx::Http2(ref mut tx) => futures_util::future::Either::Right(tx.send_request(req.0)),
+ let fut = match &mut non_null! { &mut *conn ?= ptr::null_mut() }.tx {
+ Tx::Http1(tx) => futures_util::future::Either::Left(tx.send_request(req.0)),
+ Tx::Http2(tx) => futures_util::future::Either::Right(tx.send_request(req.0)),
};
let fut = async move {
diff --git a/src/ffi/http_types.rs b/src/ffi/http_types.rs
index 3dc4a2549d..a9c6cf6b0f 100644
--- a/src/ffi/http_types.rs
+++ b/src/ffi/http_types.rs
@@ -189,9 +189,10 @@ ffi_fn! {
};
builder = builder.path_and_query(path_and_query_bytes);
}
+ let req = non_null!(&mut *req ?= hyper_code::HYPERE_INVALID_ARG);
match builder.build() {
Ok(u) => {
- *unsafe { &mut *req }.0.uri_mut() = u;
+ *req.0.uri_mut() = u;
hyper_code::HYPERE_OK
},
Err(_) => {
@@ -232,7 +233,8 @@ ffi_fn! {
/// This is not an owned reference, so it should not be accessed after the
/// `hyper_request` has been consumed.
fn hyper_request_headers(req: *mut hyper_request) -> *mut hyper_headers {
- hyper_headers::get_or_default(unsafe { &mut *req }.0.extensions_mut())
+ let req = non_null!(&mut *req ?= std::ptr::null_mut());
+ hyper_headers::get_or_default(req.0.extensions_mut())
} ?= std::ptr::null_mut()
}
@@ -367,7 +369,8 @@ ffi_fn! {
/// This is not an owned reference, so it should not be accessed after the
/// `hyper_response` has been freed.
fn hyper_response_headers(resp: *mut hyper_response) -> *mut hyper_headers {
- hyper_headers::get_or_default(unsafe { &mut *resp }.0.extensions_mut())
+ let resp = non_null!(&mut *resp ?= std::ptr::null_mut());
+ hyper_headers::get_or_default(resp.0.extensions_mut())
} ?= std::ptr::null_mut()
}
@@ -434,7 +437,8 @@ impl hyper_headers {
ext.insert(hyper_headers::default());
}
- ext.get_mut::
().unwrap()
+ ext.get_mut::()
+ .expect("hyper headers inserted into extensions")
}
}
diff --git a/src/ffi/io.rs b/src/ffi/io.rs
index 89978b9e57..a7bde95e24 100644
--- a/src/ffi/io.rs
+++ b/src/ffi/io.rs
@@ -147,7 +147,7 @@ impl Read for hyper_io {
cx: &mut Context<'_>,
mut buf: crate::rt::ReadBufCursor<'_>,
) -> Poll> {
- let buf_ptr = unsafe { buf.as_mut() }.as_mut_ptr() as *mut u8;
+ let buf_ptr = unsafe { buf.as_mut() }.as_mut_ptr().cast::();
let buf_len = buf.remaining();
match (self.read)(self.userdata, hyper_context::wrap(cx), buf_ptr, buf_len) {
diff --git a/src/ffi/task.rs b/src/ffi/task.rs
index 0c49936c6e..b02de525ac 100644
--- a/src/ffi/task.rs
+++ b/src/ffi/task.rs
@@ -10,6 +10,8 @@ use std::task::{Context, Poll};
use futures_util::stream::{FuturesUnordered, Stream};
+use crate::common::lock::LockResultExt;
+
use super::error::hyper_code;
use super::UserDataPointer;
@@ -126,7 +128,7 @@ struct TaskFuture {
/// its only purpose is to provide access to the waker. See `hyper_waker`.
///
/// Corresponding Rust type:
-pub struct hyper_context<'a>(Context<'a>);
+pub struct hyper_context<'ctx>(Context<'ctx>);
/// A waker that is saved and used to waken a pending task.
///
@@ -196,7 +198,7 @@ impl hyper_executor {
fn spawn(&self, task: Box) {
self.spawn_queue
.lock()
- .unwrap()
+ .panic_if_poisoned()
.push(TaskFuture { task: Some(task) });
}
@@ -211,7 +213,7 @@ impl hyper_executor {
{
// Scope the lock on the driver to ensure it is dropped before
// calling drain_queue below.
- let mut driver = self.driver.lock().unwrap();
+ let mut driver = self.driver.lock().panic_if_poisoned();
match Pin::new(&mut *driver).poll_next(&mut cx) {
Poll::Ready(val) => return val,
Poll::Pending => {}
@@ -238,12 +240,12 @@ impl hyper_executor {
/// drain_queue locks both self.spawn_queue and self.driver, so it requires
/// that neither of them be locked already.
fn drain_queue(&self) -> bool {
- let mut queue = self.spawn_queue.lock().unwrap();
+ let mut queue = self.spawn_queue.lock().panic_if_poisoned();
if queue.is_empty() {
return false;
}
- let driver = self.driver.lock().unwrap();
+ let driver = self.driver.lock().panic_if_poisoned();
for task in queue.drain(..) {
driver.push(task);
@@ -353,9 +355,9 @@ impl hyper_task {
}
fn output_type(&self) -> hyper_task_return_type {
- match self.output {
+ match &self.output {
None => hyper_task_return_type::HYPER_TASK_EMPTY,
- Some(ref val) => val.as_task_type(),
+ Some(val) => val.as_task_type(),
}
}
}
@@ -364,9 +366,20 @@ impl Future for TaskFuture {
type Output = Box;
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll {
- match Pin::new(&mut self.task.as_mut().unwrap().future).poll(cx) {
+ match Pin::new(
+ &mut self
+ .task
+ .as_mut()
+ .expect("ffi task future polled after completion")
+ .future,
+ )
+ .poll(cx)
+ {
Poll::Ready(val) => {
- let mut task = self.task.take().unwrap();
+ let mut task = self
+ .task
+ .take()
+ .expect("ffi task future missing task after completion");
task.output = Some(val);
Poll::Ready(task)
}
@@ -402,7 +415,7 @@ ffi_fn! {
let task = non_null!(&mut *task ?= ptr::null_mut());
if let Some(val) = task.output.take() {
- let p = Box::into_raw(val) as *mut c_void;
+ let p = Box::into_raw(val).cast();
// protect from returning fake pointers to empty types
if p == std::ptr::NonNull::::dangling().as_ptr() {
ptr::null_mut()
@@ -498,7 +511,9 @@ where
// ===== impl hyper_context =====
impl hyper_context<'_> {
- pub(crate) fn wrap<'a, 'b>(cx: &'a mut Context<'b>) -> &'a mut hyper_context<'b> {
+ pub(crate) fn wrap<'borrow, 'ctx>(
+ cx: &'borrow mut Context<'ctx>,
+ ) -> &'borrow mut hyper_context<'ctx> {
// A struct with only one field has the same layout as that field.
unsafe { std::mem::transmute::<&mut Context<'_>, &mut hyper_context<'_>>(cx) }
}
diff --git a/src/headers.rs b/src/headers.rs
index 8bebdb9bfa..caace71f2a 100644
--- a/src/headers.rs
+++ b/src/headers.rs
@@ -19,6 +19,17 @@ pub(super) fn connection_close(value: &HeaderValue) -> bool {
connection_has(value, "close")
}
+// Returns true if any `Connection` header field carries a `close` token.
+// A message may have more than one `Connection` header line, so all of them
+// must be inspected (`get`/`connection_close` alone only sees the first).
+#[cfg(feature = "http1")]
+pub(super) fn connection_any_close(headers: &http::HeaderMap) -> bool {
+ headers
+ .get_all(http::header::CONNECTION)
+ .iter()
+ .any(connection_close)
+}
+
#[cfg(feature = "http1")]
fn connection_has(value: &HeaderValue, needle: &str) -> bool {
if let Ok(s) = value.to_str() {
@@ -31,6 +42,26 @@ fn connection_has(value: &HeaderValue, needle: &str) -> bool {
false
}
+#[cfg(feature = "http1")]
+pub(super) fn te_is_trailers(headers: &http::HeaderMap) -> bool {
+ header_value_list_has(headers.get_all(http::header::TE).into_iter(), "trailers")
+}
+
+#[cfg(feature = "http1")]
+fn header_value_list_has(values: http::header::ValueIter<'_, HeaderValue>, needle: &str) -> bool {
+ for value in values {
+ if let Ok(line) = value.to_str() {
+ for token in line.split(',') {
+ if token.trim().eq_ignore_ascii_case(needle) {
+ return true;
+ }
+ }
+ }
+ }
+
+ false
+}
+
#[cfg(all(feature = "http1", feature = "server"))]
pub(super) fn content_length_parse(value: &HeaderValue) -> Option {
from_digits(value.as_bytes())
@@ -51,13 +82,11 @@ pub(super) fn content_length_parse_all_values(values: ValueIter<'_, HeaderValue>
for h in values {
if let Ok(line) = h.to_str() {
for v in line.split(',') {
- if let Some(n) = from_digits(v.trim().as_bytes()) {
- if content_length.is_none() {
- content_length = Some(n)
- } else if content_length != Some(n) {
- return None;
- }
- } else {
+ let n = from_digits(v.trim().as_bytes())?;
+
+ if content_length.is_none() {
+ content_length = Some(n);
+ } else if content_length != Some(n) {
return None;
}
}
@@ -84,7 +113,7 @@ fn from_digits(bytes: &[u8]) -> Option {
match b {
b'0'..=b'9' => {
result = result.checked_mul(RADIX)?;
- result = result.checked_add((b - b'0') as u64)?;
+ result = result.checked_add(u64::from(b - b'0'))?;
}
_ => {
// not a DIGIT, get outta here!
@@ -157,3 +186,35 @@ pub(super) fn add_chunked(mut entry: http::header::OccupiedEntry<'_, HeaderValue
entry.insert(HeaderValue::from_static(CHUNKED));
}
+
+#[cfg(all(test, feature = "http1"))]
+mod tests {
+ use super::te_is_trailers;
+ use http::header::{HeaderValue, TE};
+ use http::HeaderMap;
+
+ #[test]
+ fn te_is_trailers_accepts_comma_separated_values() {
+ let mut headers = HeaderMap::new();
+ headers.insert(TE, HeaderValue::from_static("gzip, Trailers"));
+
+ assert!(te_is_trailers(&headers));
+ }
+
+ #[test]
+ fn te_is_trailers_accepts_multiple_header_lines() {
+ let mut headers = HeaderMap::new();
+ headers.append(TE, HeaderValue::from_static("gzip"));
+ headers.append(TE, HeaderValue::from_static("trailers"));
+
+ assert!(te_is_trailers(&headers));
+ }
+
+ #[test]
+ fn te_is_trailers_rejects_missing_trailers_token() {
+ let mut headers = HeaderMap::new();
+ headers.insert(TE, HeaderValue::from_static("gzip"));
+
+ assert!(!te_is_trailers(&headers));
+ }
+}
diff --git a/src/lib.rs b/src/lib.rs
index 4683fd6597..8d6e2d2d5a 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -30,6 +30,27 @@
//! If looking for just a convenient HTTP client, consider the
//! [reqwest](https://crates.io/crates/reqwest) crate.
//!
+//! # Cancel safety
+//!
+//! Futures returned by hyper are cancel safe: dropping a future before it
+//! completes is the supported way to cancel the operation. The protocol in
+//! use changes what that cancellation actually does on the wire:
+//!
+//! - **HTTP/1** has no in-protocol way to abort a single request without
+//! affecting the shared connection, so dropping an in-flight request future
+//! closes the underlying TCP connection. Any subsequent call on the same
+//! `SendRequest` returns a `canceled` error; the connection cannot be
+//! reused.
+//! - **HTTP/2** resets the single stream with `RST_STREAM` (`CANCEL` error
+//! code) and notifies the peer immediately rather than continuing to
+//! deliver a response body that would be discarded. The shared connection
+//! stays usable for other in-flight and future requests.
+//!
+//! See the documentation on individual futures ā for example
+//! `SendRequest::send_request` in `client::conn::http1` and the equivalent
+//! in `client::conn::http2` ā for the protocol-specific behavior on
+//! cancellation.
+//!
//! # Optional Features
//!
//! hyper uses a set of [feature flags] to reduce the amount of compiled code.
diff --git a/src/mock.rs b/src/mock.rs
index 1dd57de319..322ea94f1d 100644
--- a/src/mock.rs
+++ b/src/mock.rs
@@ -16,6 +16,8 @@ use tokio_io::{AsyncRead, AsyncWrite};
#[cfg(feature = "runtime")]
use crate::client::connect::{Connect, Connected, Destination};
+#[cfg(feature = "runtime")]
+use crate::common::lock::LockResultExt;
@@ -59,14 +61,14 @@ impl Duplex {
#[cfg(feature = "runtime")]
impl Read for Duplex {
fn read(&mut self, buf: &mut [u8]) -> io::Result {
- self.inner.lock().unwrap().read.read(buf)
+ self.inner.lock().panic_if_poisoned().read.read(buf)
}
}
#[cfg(feature = "runtime")]
impl Write for Duplex {
fn write(&mut self, buf: &[u8]) -> io::Result {
- let mut inner = self.inner.lock().unwrap();
+ let mut inner = self.inner.lock().panic_if_poisoned();
let ret = inner.write.write(buf);
if let Some(task) = inner.handle_read_task.take() {
trace!("waking DuplexHandle read");
@@ -76,7 +78,7 @@ impl Write for Duplex {
}
fn flush(&mut self) -> io::Result<()> {
- self.inner.lock().unwrap().write.flush()
+ self.inner.lock().panic_if_poisoned().write.flush()
}
}
@@ -91,7 +93,7 @@ impl AsyncWrite for Duplex {
}
fn write_buf(&mut self, buf: &mut B) -> Poll {
- let mut inner = self.inner.lock().unwrap();
+ let mut inner = self.inner.lock().panic_if_poisoned();
if let Some(task) = inner.handle_read_task.take() {
task.notify();
}
@@ -107,7 +109,7 @@ pub struct DuplexHandle {
#[cfg(feature = "runtime")]
impl DuplexHandle {
pub fn read(&self, buf: &mut [u8]) -> Poll {
- let mut inner = self.inner.lock().unwrap();
+ let mut inner = self.inner.lock().panic_if_poisoned();
assert!(buf.len() >= inner.write.inner.len());
if inner.write.inner.is_empty() {
trace!("DuplexHandle read parking");
@@ -118,7 +120,7 @@ impl DuplexHandle {
}
pub fn write(&self, bytes: &[u8]) -> Poll {
- let mut inner = self.inner.lock().unwrap();
+ let mut inner = self.inner.lock().panic_if_poisoned();
assert_eq!(inner.read.inner.pos, 0);
assert_eq!(inner.read.inner.vec.len(), 0, "write but read isn't empty");
inner
@@ -136,7 +138,7 @@ impl Drop for DuplexHandle {
fn drop(&mut self) {
trace!("mock duplex handle drop");
if !::std::thread::panicking() {
- let mut inner = self.inner.lock().unwrap();
+ let mut inner = self.inner.lock().panic_if_poisoned();
inner.read.close();
inner.write.close();
}
@@ -187,7 +189,7 @@ impl MockConnector {
trace!("MockConnector mocked fut ready");
Ok((duplex, connected))
}));
- self.mocks.lock().unwrap().0.entry(key)
+ self.mocks.lock().panic_if_poisoned().0.entry(key)
.or_insert(Vec::new())
.push(fut);
@@ -208,7 +210,7 @@ impl Connect for MockConnector {
} else {
"".to_owned()
});
- let mut mocks = self.mocks.lock().unwrap();
+ let mut mocks = self.mocks.lock().panic_if_poisoned();
let mocks = mocks.0.get_mut(&key)
.expect(&format!("unknown mocks uri: {}", key));
assert!(!mocks.is_empty(), "no additional mocks for {}", key);
diff --git a/src/proto/h1/conn.rs b/src/proto/h1/conn.rs
index 3d71ed5bc5..8a5a734940 100644
--- a/src/proto/h1/conn.rs
+++ b/src/proto/h1/conn.rs
@@ -11,7 +11,7 @@ use std::time::Duration;
use crate::rt::{Read, Write};
use bytes::{Buf, Bytes};
use futures_core::ready;
-use http::header::{HeaderValue, CONNECTION, TE};
+use http::header::{Entry, HeaderValue, CONNECTION};
use http::{HeaderMap, Method, Version};
use http_body::Frame;
use httparse::ParserConfig;
@@ -58,6 +58,7 @@ where
method: None,
h1_parser_config: ParserConfig::default(),
h1_max_headers: None,
+ h1_max_header_size: None,
#[cfg(feature = "server")]
h1_header_read_timeout: None,
#[cfg(feature = "server")]
@@ -141,6 +142,10 @@ where
self.state.h1_max_headers = Some(val);
}
+ pub(crate) fn set_http1_max_header_size(&mut self, val: usize) {
+ self.state.h1_max_header_size = Some(val);
+ }
+
#[cfg(feature = "server")]
pub(crate) fn set_http1_header_read_timeout(&mut self, val: Duration) {
self.state.h1_header_read_timeout = Some(val);
@@ -220,8 +225,8 @@ where
if let Some(h1_header_read_timeout) = self.state.h1_header_read_timeout {
let deadline = self.state.timer.now() + h1_header_read_timeout;
self.state.h1_header_read_timeout_running = true;
- match self.state.h1_header_read_timeout_fut {
- Some(ref mut h1_header_read_timeout_fut) => {
+ match &mut self.state.h1_header_read_timeout_fut {
+ Some(h1_header_read_timeout_fut) => {
trace!("resetting h1 header read timeout timer");
self.state.timer.reset(h1_header_read_timeout_fut, deadline);
}
@@ -241,6 +246,7 @@ where
req_method: &mut self.state.method,
h1_parser_config: self.state.h1_parser_config.clone(),
h1_max_headers: self.state.h1_max_headers,
+ h1_max_header_size: self.state.h1_max_header_size,
preserve_header_case: self.state.preserve_header_case,
#[cfg(feature = "ffi")]
preserve_header_order: self.state.preserve_header_order,
@@ -254,8 +260,8 @@ where
Poll::Pending => {
#[cfg(feature = "server")]
if self.state.h1_header_read_timeout_running {
- if let Some(ref mut h1_header_read_timeout_fut) =
- self.state.h1_header_read_timeout_fut
+ if let Some(h1_header_read_timeout_fut) =
+ &mut self.state.h1_header_read_timeout_fut
{
if Pin::new(h1_header_read_timeout_fut).poll(cx).is_ready() {
self.state.h1_header_read_timeout_running = false;
@@ -309,7 +315,7 @@ where
self.try_keep_alive(cx);
}
} else if msg.expect_continue && msg.head.version.gt(&Version::HTTP_10) {
- let h1_max_header_size = None; // TODO: remove this when we land h1_max_header_size support
+ let h1_max_header_size = self.state.h1_max_header_size;
self.state.reading = Reading::Continue(Decoder::new(
msg.decode,
self.state.h1_max_headers,
@@ -317,7 +323,7 @@ where
));
wants = wants.add(Wants::EXPECT);
} else {
- let h1_max_header_size = None; // TODO: remove this when we land h1_max_header_size support
+ let h1_max_header_size = self.state.h1_max_header_size;
self.state.reading = Reading::Body(Decoder::new(
msg.decode,
self.state.h1_max_headers,
@@ -325,11 +331,7 @@ where
));
}
- self.state.allow_trailer_fields = msg
- .head
- .headers
- .get(TE)
- .map_or(false, |te_header| te_header == "trailers");
+ self.state.allow_trailer_fields = headers::te_is_trailers(&msg.head.headers);
Poll::Ready(Some(Ok((msg.head, msg.decode, wants))))
}
@@ -366,8 +368,8 @@ where
) -> Poll>>> {
debug_assert!(self.can_read_body());
- let (reading, ret) = match self.state.reading {
- Reading::Body(ref mut decoder) => {
+ let (reading, ret) = match &mut self.state.reading {
+ Reading::Body(decoder) => {
match ready!(decoder.decode(cx, &mut self.io)) {
Ok(frame) => {
if frame.is_data() {
@@ -393,7 +395,8 @@ where
};
(reading, Poll::Ready(maybe_frame))
} else if frame.is_trailers() {
- (Reading::Closed, Poll::Ready(Some(Ok(frame))))
+ debug!("incoming body completed with trailers");
+ (Reading::KeepAlive, Poll::Ready(Some(Ok(frame))))
} else {
trace!("discarding unknown frame");
(Reading::Closed, Poll::Ready(None))
@@ -405,7 +408,7 @@ where
}
}
}
- Reading::Continue(ref decoder) => {
+ Reading::Continue(decoder) => {
// Write the 100 Continue if not already responded...
if let Writing::Init = self.state.writing {
trace!("automatically sending 100 Continue");
@@ -528,7 +531,7 @@ where
return
}
Reading::Init => (),
- };
+ }
match self.state.writing {
Writing::Body(..) => return,
@@ -544,7 +547,7 @@ where
if self.state.is_idle() {
self.state.close();
} else {
- self.close_read()
+ self.close_read();
}
return;
}
@@ -591,6 +594,11 @@ where
self.io.can_buffer()
}
+ /// Whether bytes are sitting in the write buffer waiting to be flushed.
+ pub(crate) fn has_buffered_write(&self) -> bool {
+ self.io.has_buffered_write()
+ }
+
pub(crate) fn write_head(&mut self, head: MessageHead, body: Option) {
if let Some(encoder) = self.encode_head(head, body) {
self.state.writing = if !encoder.is_eof() {
@@ -612,6 +620,15 @@ where
if !T::should_read_first() {
self.state.busy();
+ // A client request carrying `Connection: close` must not be pooled or
+ // reused. hyper otherwise derives connection reuse from the response
+ // alone, so a backend that ignores the request-side close (omits
+ // `Connection: close` in its response) would leave the connection in
+ // the pool. Disable keep-alive up front so the connection is evicted
+ // regardless of the response.
+ if headers::connection_any_close(&head.headers) {
+ self.state.disable_keep_alive();
+ }
}
self.enforce_version(&mut head);
@@ -653,10 +670,11 @@ where
// Fix keep-alive when Connection: keep-alive header is not present
fn fix_keep_alive(&mut self, head: &mut MessageHead) {
- let outgoing_is_keep_alive = head
- .headers
- .get(CONNECTION)
- .map_or(false, headers::connection_keep_alive);
+ let connection_entry = head.headers.entry(CONNECTION);
+ let outgoing_is_keep_alive = match &connection_entry {
+ Entry::Occupied(entry) => entry.iter().any(headers::connection_keep_alive),
+ Entry::Vacant(_) => false,
+ };
if !outgoing_is_keep_alive {
match head.version {
@@ -665,12 +683,14 @@ where
Version::HTTP_10 => self.state.disable_keep_alive(),
// If response is version 1.1 and keep-alive is wanted, add
// Connection: keep-alive header when not present
- Version::HTTP_11 => {
- if self.state.wants_keep_alive() {
- head.headers
- .insert(CONNECTION, HeaderValue::from_static("keep-alive"));
+ Version::HTTP_11 if self.state.wants_keep_alive() => match connection_entry {
+ Entry::Occupied(mut entry) => {
+ entry.append(HeaderValue::from_static("keep-alive"));
}
- }
+ Entry::Vacant(entry) => {
+ entry.insert(HeaderValue::from_static("keep-alive"));
+ }
+ },
_ => (),
}
}
@@ -689,8 +709,16 @@ where
}
Version::HTTP_11 => {
if let KA::Disabled = self.state.keep_alive.status() {
- head.headers
- .insert(CONNECTION, HeaderValue::from_static("close"));
+ match head.headers.entry(CONNECTION) {
+ Entry::Occupied(mut entry) => {
+ if !entry.iter().any(headers::connection_close) {
+ entry.append(HeaderValue::from_static("close"));
+ }
+ }
+ Entry::Vacant(entry) => {
+ entry.insert(HeaderValue::from_static("close"));
+ }
+ }
}
}
_ => (),
@@ -705,8 +733,8 @@ where
// empty chunks should be discarded at Dispatcher level
debug_assert!(chunk.remaining() != 0);
- let state = match self.state.writing {
- Writing::Body(ref mut encoder) => {
+ let state = match &mut self.state.writing {
+ Writing::Body(encoder) => {
self.io.buffer(encoder.encode(chunk));
if !encoder.is_eof() {
@@ -732,8 +760,8 @@ where
}
debug_assert!(self.can_write_body() && self.can_buffer_body());
- match self.state.writing {
- Writing::Body(ref encoder) => {
+ match &mut self.state.writing {
+ Writing::Body(encoder) => {
if let Some(enc_buf) =
encoder.encode_trailers(trailers, self.state.title_case_headers)
{
@@ -755,8 +783,8 @@ where
// empty chunks should be discarded at Dispatcher level
debug_assert!(chunk.remaining() != 0);
- let state = match self.state.writing {
- Writing::Body(ref encoder) => {
+ let state = match &mut self.state.writing {
+ Writing::Body(encoder) => {
let can_keep_alive = encoder.encode_and_end(chunk, self.io.write_buf());
if can_keep_alive {
Writing::KeepAlive
@@ -773,8 +801,8 @@ where
pub(crate) fn end_body(&mut self) -> crate::Result<()> {
debug_assert!(self.can_write_body());
- let encoder = match self.state.writing {
- Writing::Body(ref mut enc) => enc,
+ let encoder = match &mut self.state.writing {
+ Writing::Body(enc) => enc,
_ => return Ok(()),
};
@@ -832,7 +860,7 @@ where
}
pub(crate) fn poll_shutdown(&mut self, cx: &mut Context<'_>) -> Poll> {
- match ready!(Pin::new(self.io.io_mut()).poll_shutdown(cx)) {
+ match ready!(self.io.poll_shutdown(cx)) {
Ok(()) => {
trace!("shut down IO complete");
Poll::Ready(Ok(()))
@@ -846,7 +874,7 @@ where
/// If the read side can be cheaply drained, do so. Otherwise, close.
pub(super) fn poll_drain_or_close_read(&mut self, cx: &mut Context<'_>) {
- if let Reading::Continue(ref decoder) = self.state.reading {
+ if let Reading::Continue(decoder) = &mut self.state.reading {
// skip sending the 100-continue
// just move forward to a read, in case a tiny body was included
self.state.reading = Reading::Body(decoder.clone());
@@ -910,7 +938,7 @@ impl Unpin for Conn {}
struct State {
allow_half_close: bool,
- /// Re-usable HeaderMap to reduce allocating new ones.
+ /// Re-usable `HeaderMap` to reduce allocating new ones.
cached_headers: Option,
/// If an error occurs when there wasn't a direct way to return it
/// back to the user, this is set.
@@ -924,6 +952,7 @@ struct State {
method: Option,
h1_parser_config: ParserConfig,
h1_max_headers: Option,
+ h1_max_header_size: Option,
#[cfg(feature = "server")]
h1_header_read_timeout: Option,
#[cfg(feature = "server")]
@@ -947,15 +976,15 @@ struct State {
/// Set to true when the Dispatcher should poll read operations
/// again. See the `maybe_notify` method for more.
notify_read: bool,
- /// State of allowed reads
+ /// State of allowed reads.
reading: Reading,
- /// State of allowed writes
+ /// State of allowed writes.
writing: Writing,
/// An expected pending HTTP upgrade.
upgrade: Option,
- /// Either HTTP/1.0 or 1.1 connection
+ /// Either HTTP/1.0 or 1.1 connection.
version: Version,
- /// Flag to track if trailer fields are allowed to be sent
+ /// Flag to track if trailer fields are allowed to be sent.
allow_trailer_fields: bool,
}
@@ -984,7 +1013,7 @@ impl fmt::Debug for State {
.field("keep_alive", &self.keep_alive);
// Only show error field if it's interesting...
- if let Some(ref error) = self.error {
+ if let Some(error) = &self.error {
builder.field("error", error);
}
@@ -994,15 +1023,15 @@ impl fmt::Debug for State {
// Purposefully leaving off other fields..
- builder.finish()
+ builder.finish_non_exhaustive()
}
}
impl fmt::Debug for Writing {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- match *self {
+ match self {
Writing::Init => f.write_str("Init"),
- Writing::Body(ref enc) => f.debug_tuple("Body").field(enc).finish(),
+ Writing::Body(enc) => f.debug_tuple("Body").field(enc).finish(),
Writing::KeepAlive => f.write_str("KeepAlive"),
Writing::Closed => f.write_str("Closed"),
}
@@ -1083,14 +1112,14 @@ impl State {
}
}
(&Reading::Closed, &Writing::KeepAlive) | (&Reading::KeepAlive, &Writing::Closed) => {
- self.close()
+ self.close();
}
_ => (),
}
}
fn disable_keep_alive(&mut self) {
- self.keep_alive.disable()
+ self.keep_alive.disable();
}
fn busy(&mut self) {
@@ -1195,336 +1224,233 @@ mod tests {
});
}
- /*
- //TODO: rewrite these using dispatch... someday...
- use futures::{Async, Future, Stream, Sink};
- use futures::future;
-
- use proto::{self, ClientTransaction, MessageHead, ServerTransaction};
- use super::super::Encoder;
- use mock::AsyncIo;
-
- use super::{Conn, Decoder, Reading, Writing};
- use ::uri::Uri;
-
- use std::str::FromStr;
-
+ // A client request carrying `Connection: close` must evict the connection
+ // (disable keep-alive) at request-encode time, so it is never returned to the
+ // pool for reuse ā independent of whether the backend response echoes
+ // `Connection: close`. hyper otherwise derives reuse from the response alone.
+ #[cfg(feature = "client")]
#[test]
- fn test_conn_init_read() {
- let good_message = b"GET / HTTP/1.1\r\n\r\n".to_vec();
- let len = good_message.len();
- let io = AsyncIo::new_buf(good_message, len);
- let mut conn = Conn::<_, proto::Bytes, ServerTransaction>::new(io);
-
- match conn.poll().unwrap() {
- Async::Ready(Some(Frame::Message { message, body: false })) => {
- assert_eq!(message, MessageHead {
- subject: ::proto::RequestLine(::Get, Uri::from_str("/").unwrap()),
- .. MessageHead::default()
- })
- },
- f => panic!("frame is not Frame::Message: {:?}", f)
- }
- }
+ fn client_request_connection_close_disables_keep_alive() {
+ use super::*;
+ use crate::common::io::Compat;
+ use crate::proto::RequestLine;
+
+ // Encodes a client GET (with the given Connection header lines, one
+ // `append` each) on a fresh client Conn and returns whether the
+ // connection remains reusable.
+ fn remains_reusable_after_get(connection_values: &[&'static str]) -> bool {
+ let io = Compat(tokio_test::io::Builder::new().build());
+ let mut conn = Conn::<_, bytes::Bytes, crate::proto::h1::ClientTransaction>::new(io);
+ assert!(
+ conn.state.wants_keep_alive(),
+ "a fresh client connection should want keep-alive"
+ );
- #[test]
- fn test_conn_parse_partial() {
- let _: Result<(), ()> = future::lazy(|| {
- let good_message = b"GET / HTTP/1.1\r\nHost: foo.bar\r\n\r\n".to_vec();
- let io = AsyncIo::new_buf(good_message, 10);
- let mut conn = Conn::<_, proto::Bytes, ServerTransaction>::new(io);
- assert!(conn.poll().unwrap().is_not_ready());
- conn.io.io_mut().block_in(50);
- let async = conn.poll().unwrap();
- assert!(async.is_ready());
- match async {
- Async::Ready(Some(Frame::Message { .. })) => (),
- f => panic!("frame is not Message: {:?}", f),
+ let mut headers = HeaderMap::new();
+ for value in connection_values {
+ headers.append(CONNECTION, HeaderValue::from_static(value));
}
- Ok(())
- }).wait();
- }
-
- #[test]
- fn test_conn_init_read_eof_idle() {
- let io = AsyncIo::new_buf(vec![], 1);
- let mut conn = Conn::<_, proto::Bytes, ServerTransaction>::new(io);
- conn.state.idle();
-
- match conn.poll().unwrap() {
- Async::Ready(None) => {},
- other => panic!("frame is not None: {:?}", other)
+ let head = MessageHead {
+ version: Version::HTTP_11,
+ subject: RequestLine(Method::GET, "/".parse().unwrap()),
+ headers,
+ extensions: http::Extensions::new(),
+ };
+ conn.write_head(head, None);
+ conn.state.wants_keep_alive()
}
- }
- #[test]
- fn test_conn_init_read_eof_idle_partial_parse() {
- let io = AsyncIo::new_buf(b"GET / HTTP/1.1".to_vec(), 100);
- let mut conn = Conn::<_, proto::Bytes, ServerTransaction>::new(io);
- conn.state.idle();
-
- match conn.poll() {
- Err(ref err) if err.kind() == std::io::ErrorKind::UnexpectedEof => {},
- other => panic!("unexpected frame: {:?}", other)
- }
+ // Control: a request without `Connection: close` leaves the connection reusable.
+ assert!(
+ remains_reusable_after_get(&[]),
+ "a keep-alive request must leave the connection reusable"
+ );
+ // Fix: a `Connection: close` request must disable keep-alive so the
+ // connection is evicted regardless of the response.
+ assert!(
+ !remains_reusable_after_get(&["close"]),
+ "a `Connection: close` request must disable keep-alive (connection evicted)"
+ );
+ // A `close` token in a comma-separated value is honored.
+ assert!(
+ !remains_reusable_after_get(&["keep-alive, close"]),
+ "a `close` token in a comma-separated Connection value must disable keep-alive"
+ );
+ // A `close` in ANY of multiple Connection header lines is honored
+ // (get_all, not just the first line).
+ assert!(
+ !remains_reusable_after_get(&["keep-alive", "close"]),
+ "a `close` in any Connection header line must disable keep-alive"
+ );
}
- #[test]
- fn test_conn_init_read_eof_busy() {
- let _: Result<(), ()> = future::lazy(|| {
- // server ignores
- let io = AsyncIo::new_eof();
- let mut conn = Conn::<_, proto::Bytes, ServerTransaction>::new(io);
- conn.state.busy();
-
- match conn.poll().unwrap() {
- Async::Ready(None) => {},
- other => panic!("unexpected frame: {:?}", other)
- }
-
- // client
- let io = AsyncIo::new_eof();
- let mut conn = Conn::<_, proto::Bytes, ClientTransaction>::new(io);
- conn.state.busy();
-
- match conn.poll() {
- Err(ref err) if err.kind() == std::io::ErrorKind::UnexpectedEof => {},
- other => panic!("unexpected frame: {:?}", other)
- }
- Ok(())
- }).wait();
+ use super::*;
+ use crate::common::io::Compat;
+ #[cfg(feature = "client")]
+ use crate::proto::h1::ClientTransaction;
+ #[cfg(feature = "server")]
+ use crate::proto::h1::ServerTransaction;
+ #[cfg(feature = "server")]
+ use crate::proto::RequestLine;
+ use bytes::Bytes;
+
+ fn poll_head(
+ conn: &mut Conn, Bytes, T>,
+ ) -> Poll, DecodedLength, Wants)>>>
+ where
+ I: tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin,
+ T: Http1Transaction + Unpin,
+ {
+ tokio_test::task::spawn(()).enter(|cx, _| conn.poll_read_head(cx))
+ }
+
+ fn ready(poll: Poll) -> T {
+ match poll {
+ Poll::Ready(value) => value,
+ Poll::Pending => panic!("expected ready"),
+ }
}
+ #[cfg(feature = "server")]
#[test]
- fn test_conn_body_finish_read_eof() {
- let _: Result<(), ()> = future::lazy(|| {
- let io = AsyncIo::new_eof();
- let mut conn = Conn::<_, proto::Bytes, ClientTransaction>::new(io);
- conn.state.busy();
- conn.state.writing = Writing::KeepAlive;
- conn.state.reading = Reading::Body(Decoder::length(0));
-
- match conn.poll() {
- Ok(Async::Ready(Some(Frame::Body { chunk: None }))) => (),
- other => panic!("unexpected frame: {:?}", other)
- }
+ fn conn_reads_request_head() {
+ let io = tokio_test::io::Builder::new()
+ .read(b"GET / HTTP/1.1\r\n\r\n")
+ .build();
+ let mut conn = Conn::<_, Bytes, ServerTransaction>::new(Compat::new(io));
- // conn eofs, but tokio-proto will call poll() again, before calling flush()
- // the conn eof in this case is perfectly fine
-
- match conn.poll() {
- Ok(Async::Ready(None)) => (),
- other => panic!("unexpected frame: {:?}", other)
- }
- Ok(())
- }).wait();
+ let (head, body, _) = ready(poll_head(&mut conn))
+ .expect("message")
+ .expect("valid request");
+ assert_eq!(head.subject, RequestLine(Method::GET, "/".parse().unwrap()));
+ assert_eq!(body, DecodedLength::ZERO);
}
+ #[cfg(feature = "server")]
#[test]
- fn test_conn_message_empty_body_read_eof() {
- let _: Result<(), ()> = future::lazy(|| {
- let io = AsyncIo::new_buf(b"HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n".to_vec(), 1024);
- let mut conn = Conn::<_, proto::Bytes, ClientTransaction>::new(io);
- conn.state.busy();
- conn.state.writing = Writing::KeepAlive;
-
- match conn.poll() {
- Ok(Async::Ready(Some(Frame::Message { body: false, .. }))) => (),
- other => panic!("unexpected frame: {:?}", other)
- }
-
- // conn eofs, but tokio-proto will call poll() again, before calling flush()
- // the conn eof in this case is perfectly fine
-
- match conn.poll() {
- Ok(Async::Ready(None)) => (),
- other => panic!("unexpected frame: {:?}", other)
- }
- Ok(())
- }).wait();
+ fn conn_reads_partial_request_head() {
+ tokio_test::task::spawn(()).enter(|cx, _| {
+ let (io, mut handle) = tokio_test::io::Builder::new().build_with_handle();
+ let mut conn = Conn::<_, Bytes, ServerTransaction>::new(Compat::new(io));
+ handle.read(b"GET / HTTP");
+ assert!(conn.poll_read_head(cx).is_pending());
+ handle.read(b"/1.1\r\nHost: foo.bar\r\n\r\n");
+ assert!(conn.poll_read_head(cx).is_ready());
+ });
}
+ #[cfg(feature = "server")]
#[test]
- fn test_conn_read_body_end() {
- let _: Result<(), ()> = future::lazy(|| {
- let io = AsyncIo::new_buf(b"POST / HTTP/1.1\r\nContent-Length: 5\r\n\r\n12345".to_vec(), 1024);
- let mut conn = Conn::<_, proto::Bytes, ServerTransaction>::new(io);
- conn.state.busy();
-
- match conn.poll() {
- Ok(Async::Ready(Some(Frame::Message { body: true, .. }))) => (),
- other => panic!("unexpected frame: {:?}", other)
- }
-
- match conn.poll() {
- Ok(Async::Ready(Some(Frame::Body { chunk: Some(_) }))) => (),
- other => panic!("unexpected frame: {:?}", other)
- }
-
- // When the body is done, `poll` MUST return a `Body` frame with chunk set to `None`
- match conn.poll() {
- Ok(Async::Ready(Some(Frame::Body { chunk: None }))) => (),
- other => panic!("unexpected frame: {:?}", other)
- }
-
- match conn.poll() {
- Ok(Async::NotReady) => (),
- other => panic!("unexpected frame: {:?}", other)
- }
- Ok(())
- }).wait();
+ fn conn_accepts_eof_when_idle() {
+ let io = tokio_test::io::Builder::new().build();
+ let mut conn = Conn::<_, Bytes, ServerTransaction>::new(Compat::new(io));
+ conn.state.idle::();
+ assert!(matches!(poll_head(&mut conn), Poll::Ready(None)));
}
+ #[cfg(feature = "server")]
#[test]
- fn test_conn_closed_read() {
- let io = AsyncIo::new_buf(vec![], 0);
- let mut conn = Conn::<_, proto::Bytes, ServerTransaction>::new(io);
- conn.state.close();
-
- match conn.poll().unwrap() {
- Async::Ready(None) => {},
- other => panic!("frame is not None: {:?}", other)
- }
+ fn conn_rejects_eof_during_partial_head() {
+ let io = tokio_test::io::Builder::new()
+ .read(b"GET / HTTP/1.1")
+ .build();
+ let mut conn = Conn::<_, Bytes, ServerTransaction>::new(Compat::new(io));
+ conn.state.idle::();
+ let err = ready(poll_head(&mut conn))
+ .expect("error result")
+ .expect_err("partial head must fail");
+ assert!(err.is_incomplete_message(), "unexpected error: {err:?}");
}
+ #[cfg(feature = "client")]
#[test]
- fn test_conn_body_write_length() {
- let _ = pretty_env_logger::try_init();
- let _: Result<(), ()> = future::lazy(|| {
- let io = AsyncIo::new_buf(vec![], 0);
- let mut conn = Conn::<_, proto::Bytes, ServerTransaction>::new(io);
- let max = super::super::io::DEFAULT_MAX_BUFFER_SIZE + 4096;
- conn.state.writing = Writing::Body(Encoder::length((max * 2) as u64));
-
- assert!(conn.start_send(Frame::Body { chunk: Some(vec![b'a'; max].into()) }).unwrap().is_ready());
- assert!(!conn.can_buffer_body());
-
- assert!(conn.start_send(Frame::Body { chunk: Some(vec![b'b'; 1024 * 8].into()) }).unwrap().is_not_ready());
-
- conn.io.io_mut().block_in(1024 * 3);
- assert!(conn.poll_complete().unwrap().is_not_ready());
- conn.io.io_mut().block_in(1024 * 3);
- assert!(conn.poll_complete().unwrap().is_not_ready());
- conn.io.io_mut().block_in(max * 2);
- assert!(conn.poll_complete().unwrap().is_ready());
-
- assert!(conn.start_send(Frame::Body { chunk: Some(vec![b'c'; 1024 * 8].into()) }).unwrap().is_ready());
- Ok(())
- }).wait();
+ fn client_rejects_eof_while_busy() {
+ let io = tokio_test::io::Builder::new().build();
+ let mut client = Conn::<_, Bytes, ClientTransaction>::new(Compat::new(io));
+ client.state.busy();
+ client.state.writing = Writing::KeepAlive;
+ let err = ready(poll_head(&mut client))
+ .expect("error result")
+ .expect_err("client EOF must fail");
+ assert!(err.is_incomplete_message(), "unexpected error: {err:?}");
}
+ #[cfg(feature = "server")]
#[test]
- fn test_conn_body_write_chunked() {
- let _: Result<(), ()> = future::lazy(|| {
- let io = AsyncIo::new_buf(vec![], 4096);
- let mut conn = Conn::<_, proto::Bytes, ServerTransaction>::new(io);
- conn.state.writing = Writing::Body(Encoder::chunked());
-
- assert!(conn.start_send(Frame::Body { chunk: Some("headers".into()) }).unwrap().is_ready());
- assert!(conn.start_send(Frame::Body { chunk: Some(vec![b'x'; 8192].into()) }).unwrap().is_ready());
- Ok(())
- }).wait();
+ fn server_accepts_eof_while_busy() {
+ let io = tokio_test::io::Builder::new().build();
+ let mut server = Conn::<_, Bytes, ServerTransaction>::new(Compat::new(io));
+ server.state.busy();
+ assert!(matches!(poll_head(&mut server), Poll::Ready(None)));
}
+ #[cfg(feature = "client")]
#[test]
- fn test_conn_body_flush() {
- let _: Result<(), ()> = future::lazy(|| {
- let io = AsyncIo::new_buf(vec![], 1024 * 1024 * 5);
- let mut conn = Conn::<_, proto::Bytes, ServerTransaction>::new(io);
- conn.state.writing = Writing::Body(Encoder::length(1024 * 1024));
- assert!(conn.start_send(Frame::Body { chunk: Some(vec![b'a'; 1024 * 1024].into()) }).unwrap().is_ready());
- assert!(!conn.can_buffer_body());
- conn.io.io_mut().block_in(1024 * 1024 * 5);
- assert!(conn.poll_complete().unwrap().is_ready());
- assert!(conn.can_buffer_body());
- assert!(conn.io.io_mut().flushed());
-
- Ok(())
- }).wait();
+ fn conn_reads_empty_response_before_eof() {
+ let io = tokio_test::io::Builder::new()
+ .read(b"HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n")
+ .build();
+ let mut conn = Conn::<_, Bytes, ClientTransaction>::new(Compat::new(io));
+ conn.state.busy();
+ conn.state.writing = Writing::KeepAlive;
+ let (_, body, _) = ready(poll_head(&mut conn))
+ .expect("response")
+ .expect("valid response");
+ assert_eq!(body, DecodedLength::ZERO);
}
+ #[cfg(feature = "server")]
#[test]
- fn test_conn_parking() {
- use std::sync::Arc;
- use futures::executor::Notify;
- use futures::executor::NotifyHandle;
-
- struct Car {
- permit: bool,
- }
- impl Notify for Car {
- fn notify(&self, _id: usize) {
- assert!(self.permit, "unparked without permit");
- }
- }
-
- fn car(permit: bool) -> NotifyHandle {
- Arc::new(Car {
- permit: permit,
- }).into()
- }
-
- // test that once writing is done, unparks
- let f = future::lazy(|| {
- let io = AsyncIo::new_buf(vec![], 4096);
- let mut conn = Conn::<_, proto::Bytes, ServerTransaction>::new(io);
- conn.state.reading = Reading::KeepAlive;
- assert!(conn.poll().unwrap().is_not_ready());
-
- conn.state.writing = Writing::KeepAlive;
- assert!(conn.poll_complete().unwrap().is_ready());
- Ok::<(), ()>(())
- });
- ::futures::executor::spawn(f).poll_future_notify(&car(true), 0).unwrap();
-
-
- // test that flushing when not waiting on read doesn't unpark
- let f = future::lazy(|| {
- let io = AsyncIo::new_buf(vec![], 4096);
- let mut conn = Conn::<_, proto::Bytes, ServerTransaction>::new(io);
- conn.state.writing = Writing::KeepAlive;
- assert!(conn.poll_complete().unwrap().is_ready());
- Ok::<(), ()>(())
- });
- ::futures::executor::spawn(f).poll_future_notify(&car(false), 0).unwrap();
-
-
- // test that flushing and writing isn't done doesn't unpark
- let f = future::lazy(|| {
- let io = AsyncIo::new_buf(vec![], 4096);
- let mut conn = Conn::<_, proto::Bytes, ServerTransaction>::new(io);
- conn.state.reading = Reading::KeepAlive;
- assert!(conn.poll().unwrap().is_not_ready());
- conn.state.writing = Writing::Body(Encoder::length(5_000));
- assert!(conn.poll_complete().unwrap().is_ready());
- Ok::<(), ()>(())
+ fn conn_reads_body_and_reports_end() {
+ let io = tokio_test::io::Builder::new()
+ .read(b"POST / HTTP/1.1\r\nContent-Length: 5\r\n\r\n12345")
+ .wait(std::time::Duration::from_secs(1))
+ .build();
+ let mut conn = Conn::<_, Bytes, ServerTransaction>::new(Compat::new(io));
+ let (_, body, _) = ready(poll_head(&mut conn))
+ .expect("request")
+ .expect("valid request");
+ assert_eq!(body, DecodedLength::new(5));
+
+ tokio_test::task::spawn(()).enter(|cx, _| {
+ let frame = conn.poll_read_body(cx);
+ let data = ready(frame)
+ .expect("body frame")
+ .expect("valid body")
+ .into_data()
+ .expect("data frame");
+ assert_eq!(data, "12345");
+ assert!(
+ !conn.can_read_body(),
+ "the complete body must return to head-reading state"
+ );
});
- ::futures::executor::spawn(f).poll_future_notify(&car(false), 0).unwrap();
}
+ #[cfg(feature = "server")]
#[test]
- fn test_conn_closed_write() {
- let io = AsyncIo::new_buf(vec![], 0);
- let mut conn = Conn::<_, proto::Bytes, ServerTransaction>::new(io);
+ fn closed_conn_cannot_read_or_write() {
+ let io = tokio_test::io::Builder::new().build();
+ let mut conn = Conn::<_, Bytes, ServerTransaction>::new(Compat::new(io));
conn.state.close();
-
- match conn.start_send(Frame::Body { chunk: Some(b"foobar".to_vec().into()) }) {
- Err(_e) => {},
- other => panic!("did not return Err: {:?}", other)
- }
-
- assert!(conn.state.is_write_closed());
+ assert!(conn.is_read_closed());
+ assert!(conn.is_write_closed());
+ assert!(!conn.can_read_head());
+ assert!(!conn.can_write_head());
}
+ #[cfg(feature = "server")]
#[test]
- fn test_conn_write_empty_chunk() {
- let io = AsyncIo::new_buf(vec![], 0);
- let mut conn = Conn::<_, proto::Bytes, ServerTransaction>::new(io);
- conn.state.writing = Writing::KeepAlive;
-
- assert!(conn.start_send(Frame::Body { chunk: None }).unwrap().is_ready());
- assert!(conn.start_send(Frame::Body { chunk: Some(Vec::new().into()) }).unwrap().is_ready());
- conn.start_send(Frame::Body { chunk: Some(vec![b'a'].into()) }).unwrap_err();
+ fn conn_writes_chunked_body() {
+ let io = tokio_test::io::Builder::new()
+ .write(b"7\r\nheaders\r\n0\r\n\r\n")
+ .build();
+ let mut conn = Conn::<_, Bytes, ServerTransaction>::new(Compat::new(io));
+ conn.state.writing = Writing::Body(Encoder::chunked());
+ conn.write_body(Bytes::from_static(b"headers"));
+ conn.end_body().unwrap();
+ tokio_test::task::spawn(()).enter(|cx, _| {
+ assert!(conn.poll_flush(cx).is_ready());
+ });
}
- */
}
diff --git a/src/proto/h1/decode.rs b/src/proto/h1/decode.rs
index e19dcb4400..9d8ed7f408 100644
--- a/src/proto/h1/decode.rs
+++ b/src/proto/h1/decode.rs
@@ -21,7 +21,7 @@ const CHUNKED_EXTENSIONS_LIMIT: u64 = 1024 * 16;
/// Maximum number of bytes allowed for all trailer fields.
///
-/// TODO: remove this when we land h1_max_header_size support
+/// TODO: remove this when we land `h1_max_header_size` support.
const TRAILER_LIMIT: usize = 1024 * 16;
/// Decoders to handle different Transfer-Encodings.
@@ -147,12 +147,12 @@ impl Decoder {
body: &mut R,
) -> Poll, io::Error>> {
trace!("decode; state={:?}", self.kind);
- match self.kind {
- Length(ref mut remaining) => {
+ match &mut self.kind {
+ Length(remaining) => {
if *remaining == 0 {
Poll::Ready(Ok(Frame::data(Bytes::new())))
} else {
- let to_read = *remaining as usize;
+ let to_read = usize::try_from(*remaining).unwrap_or(usize::MAX);
let buf = ready!(body.read_mem(cx, to_read))?;
let num = buf.as_ref().len() as u64;
if num > *remaining {
@@ -169,13 +169,13 @@ impl Decoder {
}
}
Chunked {
- ref mut state,
- ref mut chunk_len,
- ref mut extensions_cnt,
- ref mut trailers_buf,
- ref mut trailers_cnt,
- ref h1_max_headers,
- ref h1_max_header_size,
+ state,
+ chunk_len,
+ extensions_cnt,
+ trailers_buf,
+ trailers_cnt,
+ h1_max_headers,
+ h1_max_header_size,
} => {
let h1_max_headers = h1_max_headers.unwrap_or(DEFAULT_MAX_HEADERS);
let h1_max_header_size = h1_max_header_size.unwrap_or(TRAILER_LIMIT);
@@ -201,13 +201,6 @@ impl Decoder {
if trailers_buf.is_some() {
trace!("found possible trailers");
- // decoder enforces that trailers count will not exceed h1_max_headers
- if *trailers_cnt >= h1_max_headers {
- return Poll::Ready(Err(io::Error::new(
- io::ErrorKind::InvalidData,
- "chunk trailers count overflow",
- )));
- }
match decode_trailers(
&mut trailers_buf.take().expect("Trailer is None"),
*trailers_cnt,
@@ -228,7 +221,7 @@ impl Decoder {
}
}
}
- Eof(ref mut is_eof) => {
+ Eof(is_eof) => {
if *is_eof {
Poll::Ready(Ok(Frame::data(Bytes::new())))
} else {
@@ -293,12 +286,12 @@ macro_rules! put_u8 {
};
}
-struct StepArgs<'a> {
- chunk_size: &'a mut u64,
- chunk_buf: &'a mut Option,
- extensions_cnt: &'a mut u64,
- trailers_buf: &'a mut Option,
- trailers_cnt: &'a mut usize,
+struct StepArgs<'args> {
+ chunk_size: &'args mut u64,
+ chunk_buf: &'args mut Option,
+ extensions_cnt: &'args mut u64,
+ trailers_buf: &'args mut Option,
+ trailers_cnt: &'args mut usize,
max_headers_cnt: usize,
max_headers_bytes: usize,
}
@@ -321,7 +314,10 @@ impl ChunkedState {
max_headers_bytes,
}: StepArgs<'_>,
) -> Poll> {
- use self::ChunkedState::*;
+ use self::ChunkedState::{
+ Body, BodyCr, BodyLf, End, EndCr, EndLf, Extension, Size, SizeLf, SizeLws, Start,
+ Trailer, TrailerLf,
+ };
match *self {
Start => ChunkedState::read_start(cx, body, chunk_size),
Size => ChunkedState::read_size(cx, body, chunk_size),
@@ -357,15 +353,15 @@ impl ChunkedState {
match byte!(rdr, cx) {
b @ b'0'..=b'9' => {
*size = or_overflow!(size.checked_mul(radix));
- *size = or_overflow!(size.checked_add((b - b'0') as u64));
+ *size = or_overflow!(size.checked_add(u64::from(b - b'0')));
}
b @ b'a'..=b'f' => {
*size = or_overflow!(size.checked_mul(radix));
- *size = or_overflow!(size.checked_add((b + 10 - b'a') as u64));
+ *size = or_overflow!(size.checked_add(u64::from(b + 10 - b'a')));
}
b @ b'A'..=b'F' => {
*size = or_overflow!(size.checked_mul(radix));
- *size = or_overflow!(size.checked_add((b + 10 - b'A') as u64));
+ *size = or_overflow!(size.checked_add(u64::from(b + 10 - b'A')));
}
_ => {
return Poll::Ready(Err(io::Error::new(
@@ -389,15 +385,15 @@ impl ChunkedState {
match byte!(rdr, cx) {
b @ b'0'..=b'9' => {
*size = or_overflow!(size.checked_mul(radix));
- *size = or_overflow!(size.checked_add((b - b'0') as u64));
+ *size = or_overflow!(size.checked_add(u64::from(b - b'0')));
}
b @ b'a'..=b'f' => {
*size = or_overflow!(size.checked_mul(radix));
- *size = or_overflow!(size.checked_add((b + 10 - b'a') as u64));
+ *size = or_overflow!(size.checked_add(u64::from(b + 10 - b'a')));
}
b @ b'A'..=b'F' => {
*size = or_overflow!(size.checked_mul(radix));
- *size = or_overflow!(size.checked_add((b + 10 - b'A') as u64));
+ *size = or_overflow!(size.checked_add(u64::from(b + 10 - b'A')));
}
b'\t' | b' ' => return Poll::Ready(Ok(ChunkedState::SizeLws)),
b';' => return Poll::Ready(Ok(ChunkedState::Extension)),
@@ -489,12 +485,7 @@ impl ChunkedState {
trace!("Chunked read, remaining={:?}", rem);
// cap remaining bytes at the max capacity of usize
- let rem_cap = match *rem {
- r if r > usize::MAX as u64 => usize::MAX,
- r => r as usize,
- };
-
- let to_read = rem_cap;
+ let to_read = usize::try_from(*rem).unwrap_or(usize::MAX);
let slice = ready!(rdr.read_mem(cx, to_read))?;
let count = slice.len();
@@ -616,7 +607,7 @@ impl ChunkedState {
buf.put_u8(byte);
*trailers_buf = Some(buf);
}
- Some(ref mut trailers_buf) => {
+ Some(trailers_buf) => {
put_u8!(trailers_buf, byte, h1_max_header_size);
}
}
@@ -654,14 +645,14 @@ fn decode_trailers(buf: &mut BytesMut, count: usize) -> Result {
- for header in headers.iter() {
+ for header in headers {
use std::convert::TryFrom;
let name = match HeaderName::try_from(header.name) {
Ok(name) => name,
Err(_) => {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
- format!("Invalid header name: {:?}", &header),
+ format!("Invalid header name: {header:?}"),
));
}
};
@@ -671,12 +662,12 @@ fn decode_trailers(buf: &mut BytesMut, count: usize) -> Result {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
- format!("Invalid header value: {:?}", &header),
+ format!("Invalid header value: {header:?}"),
));
}
};
- trailers.insert(name, value);
+ trailers.append(name, value);
}
Ok(trailers)
@@ -1161,12 +1152,27 @@ mod tests {
assert_eq!(headers.get("X-Stream-Error").unwrap(), "failed to decode");
}
+ #[test]
+ fn test_decode_trailers_preserves_duplicate_values() {
+ let mut buf = BytesMut::new();
+ buf.extend_from_slice(b"X-Trace: first\r\nX-Trace: second\r\n\r\n");
+
+ let headers = decode_trailers(&mut buf, 2).expect("decode_trailers");
+ let values = headers
+ .get_all("X-Trace")
+ .iter()
+ .map(|value| value.to_str().unwrap())
+ .collect::>();
+
+ assert_eq!(values, ["first", "second"]);
+ }
+
#[tokio::test]
async fn test_trailer_max_headers_enforced() {
let h1_max_headers = 10;
let mut scratch = vec![];
scratch.extend(b"10\r\n1234567890abcdef\r\n0\r\n");
- for i in 0..h1_max_headers {
+ for i in 0..=h1_max_headers {
scratch.extend(format!("trailer{}: {}\r\n", i, i).as_bytes());
}
scratch.extend(b"\r\n");
@@ -1191,6 +1197,36 @@ mod tests {
assert_eq!(err.kind(), io::ErrorKind::InvalidData);
}
+ #[tokio::test]
+ async fn test_trailer_max_headers_allows_exact_limit() {
+ let h1_max_headers = 10;
+ let mut scratch = vec![];
+ scratch.extend(b"10\r\n1234567890abcdef\r\n0\r\n");
+ for i in 0..h1_max_headers {
+ scratch.extend(format!("trailer{}: {}\r\n", i, i).as_bytes());
+ }
+ scratch.extend(b"\r\n");
+ let mut mock_buf = Bytes::from(scratch);
+
+ let mut decoder = Decoder::chunked(Some(h1_max_headers), None);
+
+ let buf = decoder
+ .decode_fut(&mut mock_buf)
+ .await
+ .unwrap()
+ .into_data()
+ .expect("unknown frame type");
+ assert_eq!(16, buf.len());
+
+ let trailers = decoder
+ .decode_fut(&mut mock_buf)
+ .await
+ .expect("trailers at limit")
+ .into_trailers()
+ .expect("unknown frame type");
+ assert_eq!(trailers.len(), h1_max_headers);
+ }
+
#[tokio::test]
async fn test_trailer_max_header_size_huge_trailer() {
let max_header_size = 1024;
diff --git a/src/proto/h1/dispatch.rs b/src/proto/h1/dispatch.rs
index 5daeb5ebf6..6ccbecba94 100644
--- a/src/proto/h1/dispatch.rs
+++ b/src/proto/h1/dispatch.rs
@@ -22,9 +22,10 @@ use crate::upgrade::OnUpgrade;
pub(crate) struct Dispatcher {
conn: Conn,
dispatch: D,
- body_tx: Option,
+ body_tx: SenderDropGuard,
body_rx: Pin>>,
is_closing: bool,
+ pending_result: Option>,
}
pub(crate) trait Dispatch {
@@ -40,6 +41,9 @@ pub(crate) trait Dispatch {
-> crate::Result<()>;
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll>;
fn should_poll(&self) -> bool;
+ fn poll_close(&mut self, _cx: &mut Context<'_>) -> Poll<()> {
+ Poll::Ready(())
+ }
}
cfg_server! {
@@ -81,9 +85,10 @@ where
Dispatcher {
conn,
dispatch,
- body_tx: None,
+ body_tx: SenderDropGuard::none(),
body_rx: Box::pin(None),
is_closing: false,
+ pending_result: None,
}
}
@@ -125,18 +130,33 @@ where
cx: &mut Context<'_>,
should_shutdown: bool,
) -> Poll> {
- Poll::Ready(ready!(self.poll_inner(cx, should_shutdown)).or_else(|e| {
- // Be sure to alert a streaming body of the failure.
- if let Some(mut body) = self.body_tx.take() {
- body.send_error(crate::Error::new_body("connection error"));
+ if self.pending_result.is_none() {
+ let result = ready!(self.poll_inner(cx, should_shutdown)).or_else(|e| {
+ // Be sure to alert a streaming body of the failure with a
+ // more specific error than the drop guard would provide.
+ if let Some(mut body) = self.body_tx.take() {
+ body.send_error(crate::Error::new_body("connection error"));
+ }
+ // An error means we're shutting down either way.
+ // We just try to give the error to the user,
+ // and close the connection with an Ok. If we
+ // cannot give it to the user, then return the Err.
+ self.dispatch.recv_msg(Err(e))?;
+ Ok(Dispatched::Shutdown)
+ });
+ // An upgrade hands off the connection rather than shutting it down.
+ // Preserve the sender's readiness until the dispatcher is dropped.
+ if matches!(result, Ok(Dispatched::Upgrade(_))) {
+ return Poll::Ready(result);
}
- // An error means we're shutting down either way.
- // We just try to give the error to the user,
- // and close the connection with an Ok. If we
- // cannot give it to the user, then return the Err.
- self.dispatch.recv_msg(Err(e))?;
- Ok(Dispatched::Shutdown)
- }))
+ self.pending_result = Some(result);
+ }
+
+ // Closing a channel does not prevent an already-started send from
+ // publishing later. Drain it before completing so a caller holding
+ // the sender while awaiting its response cannot strand a callback.
+ ready!(self.dispatch.poll_close(cx));
+ Poll::Ready(self.pending_result.take().expect("closing result missing"))
}
fn poll_inner(
@@ -170,8 +190,14 @@ where
// benchmarks often use. Perhaps it should be a config option instead.
for _ in 0..16 {
let _ = self.poll_read(cx)?;
- let _ = self.poll_write(cx)?;
- let _ = self.poll_flush(cx)?;
+ let write_ready = self.poll_write(cx)?.is_ready();
+ let flush_ready = self.poll_flush(cx)?.is_ready();
+
+ // If we can write more body and the connection is ready, we should
+ // write again. If we return `Ready(Ok(())` here, we will yield
+ // without a guaranteed wake-up from the write side of the connection.
+ // This would lead to a deadlock if we also don't expect reads.
+ let wants_write_again = self.can_write_again() && (write_ready || flush_ready);
// This could happen if reading paused before blocking on IO,
// such as getting to the end of a framed message, but then
@@ -181,14 +207,37 @@ where
//
// Using this instead of task::current() and notify() inside
// the Conn is noticeably faster in pipelined benchmarks.
- if !self.conn.wants_read_again() {
- //break;
+ let wants_read_again = self.conn.wants_read_again();
+
+ // If we cannot write or read again, we yield and rely on the
+ // wake-up from the connection futures.
+ if !(wants_write_again || wants_read_again) {
return Poll::Ready(Ok(()));
}
- }
+ // If we are continuing only because "wants_write_again", re-check whether a second
+ // write poll can make progress. `poll_flush` can be ready even when there is no
+ // buffered data and the request body is still pending, so relying on the previous
+ // readiness can hot-loop.
+ if !wants_read_again && wants_write_again {
+ // Write was previously pending, but may have become ready since polling flush, so
+ // we need to check it again. If it is still pending, it is safe to yield and rely
+ // on wake-up from the connection futures.
+ if self.poll_write(cx)?.is_pending() {
+ // That write can have buffered bytes before going pending: a body that
+ // reached end-of-stream between the two write polls buffers the end of the
+ // message here, and then the write goes pending on the *next* message.
+ // Yielding without flushing would strand those bytes in the write buffer
+ // until the peer gives up, since the wake-ups we then rely on are for
+ // reads. Flush what was just buffered before yielding.
+ if self.conn.has_buffered_write() {
+ let _ = self.poll_flush(cx)?;
+ }
+ return Poll::Ready(Ok(()));
+ }
+ }
+ }
trace!("poll_loop yielding (self = {:p})", self);
-
task::yield_now(cx).map(|never| match never {})
}
@@ -203,7 +252,7 @@ where
match body.poll_ready(cx) {
Poll::Ready(Ok(())) => (),
Poll::Pending => {
- self.body_tx = Some(body);
+ self.body_tx.set(body);
return Poll::Pending;
}
Poll::Ready(Err(_canceled)) => {
@@ -220,7 +269,7 @@ where
let chunk = frame.into_data().unwrap_or_else(|_| unreachable!());
match body.try_send_data(chunk) {
Ok(()) => {
- self.body_tx = Some(body);
+ self.body_tx.set(body);
}
Err(_canceled) => {
if self.conn.can_read_body() {
@@ -234,7 +283,7 @@ where
frame.into_trailers().unwrap_or_else(|_| unreachable!());
match body.try_send_trailers(trailers) {
Ok(()) => {
- self.body_tx = Some(body);
+ self.body_tx.set(body);
}
Err(_canceled) => {
if self.conn.can_read_body() {
@@ -252,7 +301,7 @@ where
// just drop, the body will close automatically
}
Poll::Pending => {
- self.body_tx = Some(body);
+ self.body_tx.set(body);
return Poll::Pending;
}
Poll::Ready(Some(Err(e))) => {
@@ -270,13 +319,11 @@ where
fn poll_read_head(&mut self, cx: &mut Context<'_>) -> Poll> {
// can dispatch receive, or does it still care about other incoming message?
- match ready!(self.dispatch.poll_ready(cx)) {
- Ok(()) => (),
- Err(()) => {
- trace!("dispatch no longer receiving messages");
- self.close();
- return Poll::Ready(Ok(()));
- }
+ if let Ok(()) = ready!(self.dispatch.poll_ready(cx)) {
+ } else {
+ trace!("dispatch no longer receiving messages");
+ self.close();
+ return Poll::Ready(Ok(()));
}
// dispatch is ready for a message, try to read one
@@ -287,7 +334,7 @@ where
other => {
let (tx, rx) =
IncomingBody::new_channel(other, wants.contains(Wants::EXPECT));
- self.body_tx = Some(tx);
+ self.body_tx.set(tx);
rx
}
};
@@ -402,7 +449,6 @@ where
);
} else {
trace!("discarding unknown frame");
- continue;
}
} else {
*clear_body = true;
@@ -433,6 +479,12 @@ where
self.conn.close_write();
}
+ /// If there is pending data in `body_rx`, and the connection is still in a body-writing state,
+ /// we can make progress writing if the connection is ready.
+ fn can_write_again(&mut self) -> bool {
+ !self.is_closing && self.body_rx.is_some() && self.conn.can_write_body()
+ }
+
fn is_done(&self) -> bool {
if self.is_closing {
return true;
@@ -476,10 +528,10 @@ where
/// A drop guard to allow a mutable borrow of an Option while being able to
/// set whether the `Option` should be cleared on drop.
-struct OptGuard<'a, T>(Pin<&'a mut Option>, bool);
+struct OptGuard<'opt, T>(Pin<&'opt mut Option>, bool);
-impl<'a, T> OptGuard<'a, T> {
- fn new(pin: Pin<&'a mut Option>) -> Self {
+impl<'opt, T> OptGuard<'opt, T> {
+ fn new(pin: Pin<&'opt mut Option>) -> Self {
OptGuard(pin, false)
}
@@ -496,6 +548,38 @@ impl Drop for OptGuard<'_, T> {
}
}
+// ===== impl SenderDropGuard =====
+
+/// A drop guard for the body `Sender`.
+///
+/// If the `Dispatcher` future is dropped (e.g. the runtime driving the
+/// connection is shut down) while it still owns a body `Sender`, the guard
+/// sends an incomplete-message error so the receiver sees an error instead
+/// of a silent, clean end-of-stream.
+struct SenderDropGuard(Option);
+
+impl SenderDropGuard {
+ fn none() -> Self {
+ SenderDropGuard(None)
+ }
+
+ fn set(&mut self, sender: crate::body::Sender) {
+ self.0 = Some(sender);
+ }
+
+ fn take(&mut self) -> Option {
+ self.0.take()
+ }
+}
+
+impl Drop for SenderDropGuard {
+ fn drop(&mut self) {
+ if let Some(mut sender) = self.0.take() {
+ sender.send_error(crate::Error::new_incomplete());
+ }
+ }
+}
+
// ===== impl Server =====
cfg_server! {
@@ -534,7 +618,7 @@ cfg_server! {
cx: &mut Context<'_>,
) -> Poll>> {
let mut this = self.as_mut();
- let ret = if let Some(ref mut fut) = this.in_flight.as_mut().as_pin_mut() {
+ let ret = if let Some(fut) = &mut this.in_flight.as_mut().as_pin_mut() {
let resp = ready!(fut.as_mut().poll(cx)?);
let (parts, body) = resp.into_parts();
let head = MessageHead {
@@ -684,8 +768,8 @@ cfg_client! {
}
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> {
- match self.callback {
- Some(ref mut cb) => match cb.poll_canceled(cx) {
+ match &mut self.callback {
+ Some(cb) => match cb.poll_canceled(cx) {
Poll::Ready(()) => {
trace!("callback receiver has dropped");
Poll::Ready(Err(()))
@@ -699,6 +783,10 @@ cfg_client! {
fn should_poll(&self) -> bool {
self.callback.is_none()
}
+
+ fn poll_close(&mut self, cx: &mut Context<'_>) -> Poll<()> {
+ self.rx.poll_close(cx)
+ }
}
}
@@ -709,6 +797,147 @@ mod tests {
use crate::proto::h1::ClientTransaction;
use std::time::Duration;
+ #[cfg(not(miri))]
+ #[tokio::test]
+ async fn client_close_drains_pending_requests_on_reset() {
+ client_close_drains_pending_requests(true, true).await;
+ }
+
+ #[cfg(not(miri))]
+ #[tokio::test]
+ async fn client_close_drains_pending_requests_on_eof() {
+ client_close_drains_pending_requests(false, true).await;
+ }
+
+ #[cfg(not(miri))]
+ #[tokio::test]
+ async fn client_close_drains_pending_requests_without_shutdown() {
+ client_close_drains_pending_requests(false, false).await;
+ }
+
+ #[cfg(not(miri))]
+ async fn client_close_drains_pending_requests(reset: bool, should_shutdown: bool) {
+ use std::{cell::Cell, io, rc::Rc};
+
+ #[derive(Clone, Copy)]
+ enum ReadState {
+ Pending,
+ Response,
+ Close,
+ Done,
+ }
+
+ // Unlike Tokio IO, this reader does not consume cooperative budget, so
+ // the close is observed while the dispatch channel is forced to yield.
+ struct ClosingReader {
+ state: Rc>,
+ reset: bool,
+ }
+
+ impl tokio::io::AsyncRead for ClosingReader {
+ fn poll_read(
+ self: Pin<&mut Self>,
+ _cx: &mut Context<'_>,
+ buf: &mut tokio::io::ReadBuf<'_>,
+ ) -> Poll> {
+ match self.state.get() {
+ ReadState::Pending => Poll::Pending,
+ ReadState::Response => {
+ buf.put_slice(b"HTTP/1.1 200 OK\r\ncontent-length: 0\r\n\r\n");
+ self.state.set(ReadState::Pending);
+ Poll::Ready(Ok(()))
+ }
+ ReadState::Close => {
+ self.state.set(ReadState::Done);
+ if self.reset {
+ Poll::Ready(Err(io::Error::new(
+ io::ErrorKind::ConnectionReset,
+ "reset",
+ )))
+ } else {
+ Poll::Ready(Ok(()))
+ }
+ }
+ ReadState::Done => panic!("closed IO must not be polled again"),
+ }
+ }
+ }
+
+ let state = Rc::new(Cell::new(ReadState::Pending));
+ let io = Compat::new(tokio::io::join(
+ ClosingReader {
+ state: state.clone(),
+ reset,
+ },
+ Vec::::new(),
+ ));
+ let (mut tx, rx) = crate::client::dispatch::channel();
+ let conn = Conn::<_, bytes::Bytes, ClientTransaction>::new(io);
+ let mut dispatcher = Dispatcher::new(Client::new(rx), conn);
+
+ // Complete an exchange to put the connection into idle keep-alive.
+ let mut first = tokio_test::task::spawn(
+ tx.try_send(crate::Request::new(IncomingBody::empty()))
+ .unwrap(),
+ );
+ tokio_test::task::spawn(()).enter(|cx, _| {
+ assert!(Pin::new(&mut dispatcher).poll(cx).is_pending());
+ state.set(ReadState::Response);
+ assert!(Pin::new(&mut dispatcher).poll(cx).is_pending());
+ });
+ tokio_test::assert_ready_ok!(first.poll()).unwrap();
+ assert!(tx.is_ready());
+ state.set(ReadState::Close);
+ let req = crate::Request::builder()
+ .uri("/not-sent")
+ .body(IncomingBody::empty())
+ .unwrap();
+ let promise = tx.try_send(req).unwrap();
+ let mut dispatcher = tokio_test::task::spawn(futures_util::future::poll_fn(|cx| {
+ dispatcher.poll_catch(cx, should_shutdown)
+ }));
+
+ // Exhaust Tokio's cooperative budget so even a closed channel returns
+ // Pending when polled. This deterministically exercises the same drain
+ // path as a send that has reserved a slot but not yet published it.
+ let (budget_tx, mut budget_rx) = tokio::sync::mpsc::unbounded_channel();
+ loop {
+ budget_tx.send(()).unwrap();
+ if task::now_or_never(budget_rx.recv()).is_none() {
+ break;
+ }
+ }
+
+ assert!(
+ dispatcher.poll().is_pending(),
+ "must finish draining before exiting"
+ );
+ assert!(tx.is_closed(), "must reject new requests while draining");
+
+ // The dispatcher must retain its original result while the drain is
+ // pending, rather than polling the closed connection again.
+ tokio::task::yield_now().await;
+ assert!(dispatcher.is_woken());
+ let result = tokio_test::assert_ready!(dispatcher.poll());
+ if reset {
+ let err = result.err().expect("connection reset error");
+ assert_eq!(
+ err.find_source::().unwrap().kind(),
+ std::io::ErrorKind::ConnectionReset,
+ );
+ } else {
+ assert!(matches!(result, Ok(Dispatched::Shutdown)));
+ }
+
+ // Keep both the sender and dispatcher alive: cancellation must not
+ // depend on either one's destructor running.
+ let mut promise = tokio_test::task::spawn(promise);
+ let mut err = tokio_test::assert_ready_ok!(promise.poll()).unwrap_err();
+ assert!(err.error().is_canceled());
+ assert_eq!(err.take_message().unwrap().uri(), "/not-sent");
+ assert!(tx.is_closed());
+ }
+
#[test]
fn client_read_bytes_before_writing_request() {
let _ = pretty_env_logger::try_init();
diff --git a/src/proto/h1/encode.rs b/src/proto/h1/encode.rs
index 2df0c396b7..4a9fd4348b 100644
--- a/src/proto/h1/encode.rs
+++ b/src/proto/h1/encode.rs
@@ -1,4 +1,4 @@
-use std::collections::HashMap;
+use std::collections::HashSet;
use std::fmt;
use std::io::IoSlice;
@@ -9,7 +9,7 @@ use http::{
AUTHORIZATION, CACHE_CONTROL, CONTENT_ENCODING, CONTENT_LENGTH, CONTENT_RANGE,
CONTENT_TYPE, HOST, MAX_FORWARDS, SET_COOKIE, TE, TRAILER, TRANSFER_ENCODING,
},
- HeaderMap, HeaderName, HeaderValue,
+ HeaderMap, HeaderName,
};
use super::io::WriteBuf;
@@ -35,7 +35,7 @@ pub(crate) struct NotEof(u64);
#[derive(Debug, PartialEq, Clone)]
enum Kind {
/// An Encoder for when Transfer-Encoding includes `chunked`.
- Chunked(Option>),
+ Chunked(Option>),
/// An Encoder for when Content-Length is set.
///
/// Enforces that the body is not longer than the Content-Length header.
@@ -77,7 +77,7 @@ impl Encoder {
Encoder::new(Kind::CloseDelimited)
}
- pub(crate) fn into_chunked_with_trailing_fields(self, trailers: Vec) -> Encoder {
+ pub(crate) fn into_chunked_with_trailing_fields(self, trailers: Vec) -> Encoder {
match self.kind {
Kind::Chunked(_) => Encoder {
kind: Kind::Chunked(Some(trailers)),
@@ -132,7 +132,7 @@ impl Encoder {
let len = msg.remaining();
debug_assert!(len > 0, "encode() called with empty buf");
- let kind = match self.kind {
+ let kind = match &mut self.kind {
Kind::Chunked(_) => {
trace!("encoding chunked {}B", len);
let buf = ChunkSize::new(len)
@@ -140,15 +140,20 @@ impl Encoder {
.chain(b"\r\n" as &'static [u8]);
BufKind::Chunked(buf)
}
- Kind::Length(ref mut remaining) => {
+ Kind::Length(remaining) => {
trace!("sized write, len = {}", len);
- if len as u64 > *remaining {
- let limit = *remaining as usize;
- *remaining = 0;
- BufKind::Limited(msg.take(limit))
- } else {
- *remaining -= len as u64;
- BufKind::Exact(msg)
+ match usize::try_from(*remaining) {
+ // Holding more than is owed, so write only what is left.
+ Ok(limit) if limit < len => {
+ *remaining = 0;
+ BufKind::Limited(msg.take(limit))
+ }
+ // Ok(_) => Owed at least what we hold, write all of it.
+ // Err(_) => Owed more than `usize` can represent, write all of it.
+ Ok(_) | Err(_) => {
+ *remaining -= len as u64;
+ BufKind::Exact(msg)
+ }
}
}
#[cfg(feature = "server")]
@@ -168,7 +173,7 @@ impl Encoder {
trace!("encoding trailers");
match &self.kind {
Kind::Chunked(Some(allowed_trailer_fields)) => {
- let allowed_trailer_field_map = allowed_trailer_field_map(allowed_trailer_fields);
+ let allowed_set: HashSet<&HeaderName> = allowed_trailer_fields.iter().collect();
let mut cur_name = None;
let mut allowed_trailers = HeaderMap::new();
@@ -179,9 +184,9 @@ impl Encoder {
}
let name = cur_name.as_ref().expect("current header name");
- if allowed_trailer_field_map.contains_key(name.as_str()) {
+ if allowed_set.contains(name) {
if is_valid_trailer_field(name) {
- allowed_trailers.insert(name, value);
+ allowed_trailers.append(name, value);
} else {
debug!("trailer field is not valid: {}", &name);
}
@@ -241,6 +246,7 @@ impl Encoder {
dst.buffer(msg);
!self.is_last
}
+ #[allow(clippy::cast_possible_truncation)]
Ordering::Greater => {
dst.buffer(msg.take(remaining as usize));
!self.is_last
@@ -279,76 +285,56 @@ fn is_valid_trailer_field(name: &HeaderName) -> bool {
)
}
-fn allowed_trailer_field_map(allowed_trailer_fields: &Vec) -> HashMap {
- let mut trailer_map = HashMap::new();
-
- for header_value in allowed_trailer_fields {
- if let Ok(header_str) = header_value.to_str() {
- let items: Vec<&str> = header_str.split(',').map(|item| item.trim()).collect();
-
- for item in items {
- trailer_map.entry(item.to_string()).or_insert(());
- }
- }
- }
-
- trailer_map
-}
-
impl Buf for EncodedBuf
where
B: Buf,
{
#[inline]
fn remaining(&self) -> usize {
- match self.kind {
- BufKind::Exact(ref b) => b.remaining(),
- BufKind::Limited(ref b) => b.remaining(),
- BufKind::Chunked(ref b) => b.remaining(),
- BufKind::ChunkedEnd(ref b) => b.remaining(),
- BufKind::Trailers(ref b) => b.remaining(),
+ match &self.kind {
+ BufKind::Exact(b) => b.remaining(),
+ BufKind::Limited(b) => b.remaining(),
+ BufKind::Chunked(b) => b.remaining(),
+ BufKind::ChunkedEnd(b) => b.remaining(),
+ BufKind::Trailers(b) => b.remaining(),
}
}
#[inline]
fn chunk(&self) -> &[u8] {
- match self.kind {
- BufKind::Exact(ref b) => b.chunk(),
- BufKind::Limited(ref b) => b.chunk(),
- BufKind::Chunked(ref b) => b.chunk(),
- BufKind::ChunkedEnd(ref b) => b.chunk(),
- BufKind::Trailers(ref b) => b.chunk(),
+ match &self.kind {
+ BufKind::Exact(b) => b.chunk(),
+ BufKind::Limited(b) => b.chunk(),
+ BufKind::Chunked(b) => b.chunk(),
+ BufKind::ChunkedEnd(b) => b.chunk(),
+ BufKind::Trailers(b) => b.chunk(),
}
}
#[inline]
fn advance(&mut self, cnt: usize) {
- match self.kind {
- BufKind::Exact(ref mut b) => b.advance(cnt),
- BufKind::Limited(ref mut b) => b.advance(cnt),
- BufKind::Chunked(ref mut b) => b.advance(cnt),
- BufKind::ChunkedEnd(ref mut b) => b.advance(cnt),
- BufKind::Trailers(ref mut b) => b.advance(cnt),
+ match &mut self.kind {
+ BufKind::Exact(b) => b.advance(cnt),
+ BufKind::Limited(b) => b.advance(cnt),
+ BufKind::Chunked(b) => b.advance(cnt),
+ BufKind::ChunkedEnd(b) => b.advance(cnt),
+ BufKind::Trailers(b) => b.advance(cnt),
}
}
#[inline]
- fn chunks_vectored<'t>(&'t self, dst: &mut [IoSlice<'t>]) -> usize {
- match self.kind {
- BufKind::Exact(ref b) => b.chunks_vectored(dst),
- BufKind::Limited(ref b) => b.chunks_vectored(dst),
- BufKind::Chunked(ref b) => b.chunks_vectored(dst),
- BufKind::ChunkedEnd(ref b) => b.chunks_vectored(dst),
- BufKind::Trailers(ref b) => b.chunks_vectored(dst),
+ fn chunks_vectored<'data>(&'data self, dst: &mut [IoSlice<'data>]) -> usize {
+ match &self.kind {
+ BufKind::Exact(b) => b.chunks_vectored(dst),
+ BufKind::Limited(b) => b.chunks_vectored(dst),
+ BufKind::Chunked(b) => b.chunks_vectored(dst),
+ BufKind::ChunkedEnd(b) => b.chunks_vectored(dst),
+ BufKind::Trailers(b) => b.chunks_vectored(dst),
}
}
}
-#[cfg(target_pointer_width = "32")]
-const USIZE_BYTES: usize = 4;
-
-#[cfg(target_pointer_width = "64")]
-const USIZE_BYTES: usize = 8;
+const USIZE_BYTES: usize = std::mem::size_of::();
// each byte will become 2 hex
const CHUNK_SIZE_MAX_BYTES: usize = USIZE_BYTES * 2;
@@ -368,7 +354,7 @@ impl ChunkSize {
pos: 0,
len: 0,
};
- write!(&mut size, "{:X}\r\n", len).expect("CHUNK_SIZE_MAX_BYTES should fit any usize");
+ write!(&mut size, "{len:X}\r\n").expect("CHUNK_SIZE_MAX_BYTES should fit any usize");
size
}
}
@@ -385,6 +371,7 @@ impl Buf for ChunkSize {
}
#[inline]
+ #[allow(clippy::cast_possible_truncation)]
fn advance(&mut self, cnt: usize) {
assert!(cnt <= self.remaining());
self.pos += cnt as u8; // just asserted cnt fits in u8
@@ -401,12 +388,14 @@ impl fmt::Debug for ChunkSize {
}
impl fmt::Write for ChunkSize {
+ #[allow(clippy::cast_possible_truncation)]
fn write_str(&mut self, num: &str) -> fmt::Result {
use std::io::Write;
(&mut self.bytes[self.len.into()..])
.write_all(num.as_bytes())
.expect("&mut [u8].write() cannot error");
- self.len += num.len() as u8; // safe because bytes is never bigger than 256
+ debug_assert!(u8::try_from(num.len()).is_ok());
+ self.len += num.len() as u8; // safe because bytes is never bigger than 255
Ok(())
}
}
@@ -532,7 +521,7 @@ mod tests {
#[test]
fn chunked_with_valid_trailers() {
let encoder = Encoder::chunked();
- let trailers = vec![HeaderValue::from_static("chunky-trailer")];
+ let trailers = vec![HeaderName::from_static("chunky-trailer")];
let encoder = encoder.into_chunked_with_trailing_fields(trailers);
let headers = HeaderMap::from_iter(vec![
@@ -557,8 +546,8 @@ mod tests {
fn chunked_with_multiple_trailer_headers() {
let encoder = Encoder::chunked();
let trailers = vec![
- HeaderValue::from_static("chunky-trailer"),
- HeaderValue::from_static("chunky-trailer-2"),
+ HeaderName::from_static("chunky-trailer"),
+ HeaderName::from_static("chunky-trailer-2"),
];
let encoder = encoder.into_chunked_with_trailing_fields(trailers);
@@ -583,6 +572,32 @@ mod tests {
);
}
+ #[test]
+ fn chunked_with_duplicate_trailer_values() {
+ let encoder = Encoder::chunked();
+ let trailers = vec![HeaderName::from_static("chunky-trailer")];
+ let encoder = encoder.into_chunked_with_trailing_fields(trailers);
+
+ let mut headers = HeaderMap::new();
+ headers.append(
+ HeaderName::from_static("chunky-trailer"),
+ HeaderValue::from_static("first"),
+ );
+ headers.append(
+ HeaderName::from_static("chunky-trailer"),
+ HeaderValue::from_static("second"),
+ );
+
+ let buf1 = encoder.encode_trailers::<&[u8]>(headers, false).unwrap();
+
+ let mut dst = Vec::new();
+ dst.put(buf1);
+ assert_eq!(
+ dst,
+ b"0\r\nchunky-trailer: first\r\nchunky-trailer: second\r\n\r\n"
+ );
+ }
+
#[test]
fn chunked_with_no_trailer_header() {
let encoder = Encoder::chunked();
@@ -606,8 +621,7 @@ mod tests {
fn chunked_with_invalid_trailers() {
let encoder = Encoder::chunked();
- let trailers = format!(
- "{},{},{},{},{},{},{},{},{},{},{},{}",
+ let trailers = vec![
AUTHORIZATION,
CACHE_CONTROL,
CONTENT_ENCODING,
@@ -620,8 +634,7 @@ mod tests {
TRAILER,
TRANSFER_ENCODING,
TE,
- );
- let trailers = vec![HeaderValue::from_str(&trailers).unwrap()];
+ ];
let encoder = encoder.into_chunked_with_trailing_fields(trailers);
let mut headers = HeaderMap::new();
@@ -644,7 +657,7 @@ mod tests {
#[test]
fn chunked_with_title_case_headers() {
let encoder = Encoder::chunked();
- let trailers = vec![HeaderValue::from_static("chunky-trailer")];
+ let trailers = vec![HeaderName::from_static("chunky-trailer")];
let encoder = encoder.into_chunked_with_trailing_fields(trailers);
let headers = HeaderMap::from_iter(vec![(
@@ -657,4 +670,34 @@ mod tests {
dst.put(buf1);
assert_eq!(dst, b"0\r\nChunky-Trailer: header data\r\n\r\n");
}
+
+ #[test]
+ fn chunked_trailers_case_insensitive_matching() {
+ // Regression test for issue #4010: HTTP/1.1 trailers are case-sensitive
+ //
+ // Previously, the Trailer header values were stored as HeaderValue (preserving case)
+ // and compared against HeaderName (which is always lowercase). This caused trailers
+ // declared as "Chunky-Trailer" to not match actual trailers sent as "chunky-trailer".
+ //
+ // The fix converts Trailer header values to HeaderName during parsing, which
+ // normalizes the case and enables proper case-insensitive matching.
+ //
+ // Note: HeaderName::from_static() requires lowercase input. In real usage,
+ // HeaderName::from_bytes() is used to parse the Trailer header value, which
+ // normalizes mixed-case input like "Chunky-Trailer" to "chunky-trailer".
+ let encoder = Encoder::chunked();
+ let trailers = vec![HeaderName::from_static("chunky-trailer")];
+ let encoder = encoder.into_chunked_with_trailing_fields(trailers);
+
+ // The actual trailer being sent
+ let headers = HeaderMap::from_iter(vec![(
+ HeaderName::from_static("chunky-trailer"),
+ HeaderValue::from_static("trailer value"),
+ )]);
+
+ let buf = encoder.encode_trailers::<&[u8]>(headers, false).unwrap();
+ let mut dst = Vec::new();
+ dst.put(buf);
+ assert_eq!(dst, b"0\r\nchunky-trailer: trailer value\r\n\r\n");
+ }
}
diff --git a/src/proto/h1/io.rs b/src/proto/h1/io.rs
index 6a30d73c84..bcd0204d2f 100644
--- a/src/proto/h1/io.rs
+++ b/src/proto/h1/io.rs
@@ -47,7 +47,7 @@ where
f.debug_struct("Buffered")
.field("read_buf", &self.read_buf)
.field("write_buf", &self.write_buf)
- .finish()
+ .finish_non_exhaustive()
}
}
@@ -86,8 +86,7 @@ where
pub(crate) fn set_max_buf_size(&mut self, max: usize) {
assert!(
max >= MINIMUM_MAX_BUFFER_SIZE,
- "The max_buf_size cannot be smaller than {}.",
- MINIMUM_MAX_BUFFER_SIZE,
+ "The max_buf_size cannot be smaller than {MINIMUM_MAX_BUFFER_SIZE}.",
);
self.read_buf_strategy = ReadStrategy::with_max(max);
self.write_buf.max_buf_size = max;
@@ -101,14 +100,14 @@ where
pub(crate) fn set_write_strategy_flatten(&mut self) {
// this should always be called only at construction time,
// so this assert is here to catch myself
- debug_assert!(self.write_buf.queue.bufs_cnt() == 0);
+ debug_assert_eq!(self.write_buf.queue.bufs_cnt(), 0);
self.write_buf.set_strategy(WriteStrategy::Flatten);
}
pub(crate) fn set_write_strategy_queue(&mut self) {
// this should always be called only at construction time,
// so this assert is here to catch myself
- debug_assert!(self.write_buf.queue.bufs_cnt() == 0);
+ debug_assert_eq!(self.write_buf.queue.bufs_cnt(), 0);
self.write_buf.set_strategy(WriteStrategy::Queue);
}
@@ -147,7 +146,12 @@ where
}
pub(crate) fn buffer>(&mut self, buf: BB) {
- self.write_buf.buffer(buf)
+ self.write_buf.buffer(buf);
+ }
+
+ /// Whether there are bytes waiting in the write buffer to be flushed.
+ pub(crate) fn has_buffered_write(&self) -> bool {
+ self.write_buf.remaining() > 0
}
pub(crate) fn can_buffer(&self) -> bool {
@@ -176,7 +180,7 @@ where
S: Http1Transaction,
{
loop {
- match super::role::parse_headers::(
+ if let Some(msg) = super::role::parse_headers::(
&mut self.read_buf,
self.partial_len,
ParseContext {
@@ -184,6 +188,7 @@ where
req_method: parse_ctx.req_method,
h1_parser_config: parse_ctx.h1_parser_config.clone(),
h1_max_headers: parse_ctx.h1_max_headers,
+ h1_max_header_size: parse_ctx.h1_max_header_size,
preserve_header_case: parse_ctx.preserve_header_case,
#[cfg(feature = "ffi")]
preserve_header_order: parse_ctx.preserve_header_order,
@@ -192,25 +197,28 @@ where
on_informational: parse_ctx.on_informational,
},
)? {
- Some(msg) => {
- debug!("parsed {} headers", msg.head.headers.len());
- self.partial_len = None;
- return Poll::Ready(Ok(msg));
- }
- None => {
- let max = self.read_buf_strategy.max();
- let curr_len = self.read_buf.len();
- if curr_len >= max {
- debug!("max_buf_size ({}) reached, closing", max);
+ debug!("parsed {} headers", msg.head.headers.len());
+ self.partial_len = None;
+ return Poll::Ready(Ok(msg));
+ } else {
+ let curr_len = self.read_buf.len();
+ if let Some(max_header_size) = parse_ctx.h1_max_header_size {
+ if curr_len >= max_header_size {
+ debug!("max_header_size ({}) reached, closing", max_header_size);
return Poll::Ready(Err(crate::Error::new_too_large()));
}
- if curr_len > 0 {
- trace!("partial headers; {} bytes so far", curr_len);
- self.partial_len = Some(curr_len);
- } else {
- // 1xx gobled some bytes
- self.partial_len = None;
- }
+ }
+ let max = self.read_buf_strategy.max();
+ if curr_len >= max {
+ debug!("max_buf_size ({}) reached, closing", max);
+ return Poll::Ready(Err(crate::Error::new_too_large()));
+ }
+ if curr_len > 0 {
+ trace!("partial headers; {} bytes so far", curr_len);
+ self.partial_len = Some(curr_len);
+ } else {
+ // 1xx gobled some bytes
+ self.partial_len = None;
}
}
if ready!(self.poll_read_from_io(cx)).map_err(crate::Error::new_io)? == 0 {
@@ -222,7 +230,14 @@ where
pub(crate) fn poll_read_from_io(&mut self, cx: &mut Context<'_>) -> Poll> {
self.read_blocked = false;
- let next = self.read_buf_strategy.next();
+ // Get the next amount to allocate, but make sure we don't go over
+ // the max read buf size configured.
+ let next = cmp::min(
+ self.read_buf_strategy.next(),
+ self.read_buf_strategy
+ .max()
+ .saturating_sub(self.read_buf.len()),
+ );
if self.read_buf_remaining_mut() < next {
self.read_buf.reserve(next);
}
@@ -232,13 +247,13 @@ where
let dst = unsafe { self.read_buf.chunk_mut().as_uninit_slice_mut() };
let mut buf = ReadBuf::uninit(dst);
match Pin::new(&mut self.io).poll_read(cx, buf.unfilled()) {
- Poll::Ready(Ok(_)) => {
+ Poll::Ready(Ok(())) => {
let n = buf.filled().len();
trace!("received {} bytes", n);
+ // Safety: we just read that many bytes into the
+ // uninitialized part of the buffer, so this is okay.
+ // @tokio pls give me back `poll_read_buf` thanks
unsafe {
- // Safety: we just read that many bytes into the
- // uninitialized part of the buffer, so this is okay.
- // @tokio pls give me back `poll_read_buf` thanks
self.read_buf.advance_mut(n);
}
self.read_buf_strategy.record(n);
@@ -256,10 +271,6 @@ where
(self.io, self.read_buf.freeze())
}
- pub(crate) fn io_mut(&mut self) -> &mut T {
- &mut self.io
- }
-
pub(crate) fn is_read_blocked(&self) -> bool {
self.read_blocked
}
@@ -323,6 +334,11 @@ where
Pin::new(&mut self.io).poll_flush(cx)
}
+ pub(crate) fn poll_shutdown(&mut self, cx: &mut Context<'_>) -> Poll> {
+ ready!(self.poll_flush(cx))?;
+ Pin::new(&mut self.io).poll_shutdown(cx)
+ }
+
#[cfg(test)]
fn flush(&mut self) -> impl std::future::Future> + '_ {
futures_util::future::poll_fn(move |cx| self.poll_flush(cx))
@@ -390,15 +406,15 @@ impl ReadStrategy {
}
fn record(&mut self, bytes_read: usize) {
- match *self {
+ match self {
ReadStrategy::Adaptive {
- ref mut decrease_now,
- ref mut next,
+ decrease_now,
+ next,
max,
..
} => {
if bytes_read >= *next {
- *next = cmp::min(incr_power_of_two(*next), max);
+ *next = cmp::min(incr_power_of_two(*next), *max);
*decrease_now = false;
} else {
let decr_to = prev_power_of_two(*next);
@@ -508,10 +524,10 @@ impl> Buf for Cursor {
// an internal buffer to collect writes before flushes
pub(super) struct WriteBuf {
- /// Re-usable buffer that holds message headers
+ /// Re-usable buffer that holds message headers.
headers: Cursor>,
max_buf_size: usize,
- /// Deque of user buffers if strategy is Queue
+ /// Deque of user buffers if strategy is `Queue`.
queue: BufList,
strategy: WriteStrategy,
}
@@ -525,12 +541,7 @@ impl WriteBuf {
strategy,
}
}
-}
-impl WriteBuf
-where
- B: Buf,
-{
fn set_strategy(&mut self, strategy: WriteStrategy) {
self.strategy = strategy;
}
@@ -592,7 +603,7 @@ impl fmt::Debug for WriteBuf {
f.debug_struct("WriteBuf")
.field("remaining", &self.remaining())
.field("strategy", &self.strategy)
- .finish()
+ .finish_non_exhaustive()
}
}
@@ -628,7 +639,7 @@ impl Buf for WriteBuf {
}
#[inline]
- fn chunks_vectored<'t>(&'t self, dst: &mut [IoSlice<'t>]) -> usize {
+ fn chunks_vectored<'data>(&'data self, dst: &mut [IoSlice<'data>]) -> usize {
let n = self.headers.chunks_vectored(dst);
self.queue.chunks_vectored(&mut dst[n..]) + n
}
@@ -648,19 +659,6 @@ mod tests {
use tokio_test::io::Builder as Mock;
- // #[cfg(feature = "nightly")]
- // use test::Bencher;
-
- /*
- impl MemRead for AsyncIo {
- fn read_mem(&mut self, len: usize) -> Poll {
- let mut v = vec![0; len];
- let n = try_nb!(self.read(v.as_mut_slice()));
- Ok(Async::Ready(BytesMut::from(&v[..n]).freeze()))
- }
- }
- */
-
#[tokio::test]
#[ignore]
async fn iobuf_write_empty_slice() {
@@ -706,6 +704,7 @@ mod tests {
req_method: &mut None,
h1_parser_config: Default::default(),
h1_max_headers: None,
+ h1_max_header_size: None,
preserve_header_case: false,
#[cfg(feature = "ffi")]
preserve_header_order: false,
@@ -826,7 +825,7 @@ mod tests {
}
let mut max = 8192;
- while max < std::usize::MAX {
+ while max < usize::MAX {
fuzz(max);
max = (max / 2).saturating_mul(3);
}
diff --git a/src/proto/h1/mod.rs b/src/proto/h1/mod.rs
index a8f36f5fd9..344d5a8a4f 100644
--- a/src/proto/h1/mod.rs
+++ b/src/proto/h1/mod.rs
@@ -56,7 +56,7 @@ pub(crate) trait Http1Transaction {
fn update_date() {}
}
-/// Result newtype for Http1Transaction::parse.
+/// Result newtype for `Http1Transaction::parse`.
pub(crate) type ParseResult = Result>, crate::error::Parse>;
#[derive(Debug)]
@@ -68,26 +68,27 @@ pub(crate) struct ParsedMessage {
wants_upgrade: bool,
}
-pub(crate) struct ParseContext<'a> {
- cached_headers: &'a mut Option,
- req_method: &'a mut Option,
+pub(crate) struct ParseContext<'ctx> {
+ cached_headers: &'ctx mut Option,
+ req_method: &'ctx mut Option,
h1_parser_config: ParserConfig,
h1_max_headers: Option,
+ h1_max_header_size: Option,
preserve_header_case: bool,
#[cfg(feature = "ffi")]
preserve_header_order: bool,
h09_responses: bool,
#[cfg(feature = "client")]
- on_informational: &'a mut Option,
+ on_informational: &'ctx mut Option,
}
-/// Passed to Http1Transaction::encode
-pub(crate) struct Encode<'a, T> {
- head: &'a mut MessageHead,
+/// Passed to `Http1Transaction::encode`.
+pub(crate) struct Encode<'encode, T> {
+ head: &'encode mut MessageHead,
body: Option,
#[cfg(feature = "server")]
keep_alive: bool,
- req_method: &'a mut Option,
+ req_method: &'encode mut Option,
title_case_headers: bool,
#[cfg(feature = "server")]
date_header: bool,
diff --git a/src/proto/h1/role.rs b/src/proto/h1/role.rs
index f124c9ff2b..fce1df78af 100644
--- a/src/proto/h1/role.rs
+++ b/src/proto/h1/role.rs
@@ -43,10 +43,16 @@ macro_rules! header_name {
}
}};
}
-
+/// construct `HeaderValue` from a maybe shared expression.
macro_rules! header_value {
($bytes:expr) => {{
{
+ // unsafe used because of the call of `HeaderValue::from_maybe_shared_unchecked`.
+ // SAFETY:
+ // 1. The input `$bytes` must be a valid header value as per RFC 7230.
+ // 2. Specifically, it must not contain any prohibited characters (like `\r`, `\n`, or non-visible ASCII characters outside of allowed ranges).
+ // 3. This is safe because the caller is responsible for ensuring the byte content
+ // has been validated or is known to be a constant/static valid header value.
unsafe { HeaderValue::from_maybe_shared_unchecked($bytes) }
}
}};
@@ -100,7 +106,10 @@ fn is_complete_fast(bytes: &[u8], prev_len: usize) -> bool {
if bytes[i + 1..].chunks(3).next() == Some(&b"\n\r\n"[..]) {
return true;
}
- } else if b == b'\n' && bytes.get(i + 1) == Some(&b'\n') {
+ } else if b == b'\n'
+ && (bytes.get(i + 1) == Some(&b'\n')
+ || bytes[i + 1..].chunks(2).next() == Some(&b"\r\n"[..]))
+ {
return true;
}
}
@@ -164,17 +173,27 @@ impl Http1Transaction for Server {
trace!(bytes = buf.len(), "Request.parse");
let mut req = httparse::Request::new(&mut []);
let bytes = buf.as_ref();
- match req.parse_with_uninit_headers(bytes, &mut headers) {
+ match ctx.h1_parser_config.parse_request_with_uninit_headers(
+ &mut req,
+ bytes,
+ &mut headers,
+ ) {
Ok(httparse::Status::Complete(parsed_len)) => {
trace!("Request.parse Complete({})", parsed_len);
+ if let Some(max_header_size) = ctx.h1_max_header_size {
+ if parsed_len > max_header_size {
+ return Err(Parse::TooLarge);
+ }
+ }
len = parsed_len;
- let uri = req.path.unwrap();
+ let uri = req.path.expect("httparse completed");
if uri.len() > MAX_URI_LEN {
return Err(Parse::UriTooLong);
}
- method = Method::from_bytes(req.method.unwrap().as_bytes())?;
+ method =
+ Method::from_bytes(req.method.expect("httparse completed").as_bytes())?;
path_range = Server::record_path_range(bytes, uri);
- version = if req.version.unwrap() == 1 {
+ version = if req.version.expect("httparse completed") == 1 {
keep_alive = true;
is_http_11 = true;
Version::HTTP_11
@@ -223,6 +242,7 @@ impl Http1Transaction for Server {
let mut decoder = DecodedLength::ZERO;
let mut expect_continue = false;
let mut con_len = None;
+ let mut is_cl = false;
let mut is_te = false;
let mut is_te_chunked = false;
let mut wants_upgrade = subject.0 == Method::CONNECT;
@@ -241,6 +261,7 @@ impl Http1Transaction for Server {
};
let mut headers = ctx.cached_headers.take().unwrap_or_default();
+ let mut saw_connection_close = false;
headers.reserve(headers_len);
@@ -261,6 +282,9 @@ impl Http1Transaction for Server {
return Err(Parse::transfer_encoding_unexpected());
}
is_te = true;
+ if is_cl && con_len.take().is_some() {
+ headers.remove(header::CONTENT_LENGTH);
+ }
if headers::is_chunked_(&value) {
is_te_chunked = true;
decoder = DecodedLength::CHUNKED;
@@ -269,6 +293,7 @@ impl Http1Transaction for Server {
}
}
header::CONTENT_LENGTH => {
+ is_cl = true;
if is_te {
continue;
}
@@ -289,12 +314,14 @@ impl Http1Transaction for Server {
con_len = Some(len);
}
header::CONNECTION => {
- // keep_alive was previously set to default for Version
- if keep_alive {
- // HTTP/1.1
- keep_alive = !headers::connection_close(&value);
- } else {
- // HTTP/1.0
+ // A message may carry more than one `Connection` line. Once
+ // any of them asks to close, a later `keep-alive` must not
+ // undo it.
+ if saw_connection_close || headers::connection_close(&value) {
+ saw_connection_close = true;
+ keep_alive = false;
+ } else if !keep_alive {
+ // HTTP/1.0, which defaults to close
keep_alive = headers::connection_keep_alive(&value);
}
}
@@ -312,12 +339,12 @@ impl Http1Transaction for Server {
_ => (),
}
- if let Some(ref mut header_case_map) = header_case_map {
+ if let Some(header_case_map) = &mut header_case_map {
header_case_map.append(&name, slice.slice(header.name.0..header.name.1));
}
#[cfg(feature = "ffi")]
- if let Some(ref mut header_order) = header_order {
+ if let Some(header_order) = &mut header_order {
header_order.append(&name);
}
@@ -329,6 +356,10 @@ impl Http1Transaction for Server {
return Err(Parse::transfer_encoding_invalid());
}
+ if is_te && is_cl {
+ keep_alive = false;
+ }
+
let mut extensions = http::Extensions::default();
if let Some(header_case_map) = header_case_map {
@@ -410,7 +441,8 @@ impl Http1Transaction for Server {
debug!("response with HTTP2 version coerced to HTTP/1.1");
extend(dst, b"HTTP/1.1 ");
}
- other => panic!("unexpected response version: {:?}", other),
+ #[allow(clippy::panic)]
+ other => panic!("unexpected response version: {other:?}"),
}
extend(dst, msg.head.subject.as_str().as_bytes());
@@ -489,13 +521,13 @@ impl Http1Transaction for Server {
#[cfg(feature = "server")]
impl Server {
- fn can_have_body(method: &Option, status: StatusCode) -> bool {
+ fn can_have_body(method: Option<&Method>, status: StatusCode) -> bool {
Server::can_chunked(method, status)
}
- fn can_chunked(method: &Option, status: StatusCode) -> bool {
- if method == &Some(Method::HEAD)
- || method == &Some(Method::CONNECT) && status.is_success()
+ fn can_chunked(method: Option<&Method>, status: StatusCode) -> bool {
+ if method == Some(&Method::HEAD)
+ || method == Some(&Method::CONNECT) && status.is_success()
|| status.is_informational()
{
false
@@ -504,16 +536,16 @@ impl Server {
}
}
- fn can_have_content_length(method: &Option, status: StatusCode) -> bool {
- if status.is_informational() || method == &Some(Method::CONNECT) && status.is_success() {
+ fn can_have_content_length(method: Option<&Method>, status: StatusCode) -> bool {
+ if status.is_informational() || method == Some(&Method::CONNECT) && status.is_success() {
false
} else {
!matches!(status, StatusCode::NO_CONTENT | StatusCode::NOT_MODIFIED)
}
}
- fn can_have_implicit_zero_content_length(method: &Option, status: StatusCode) -> bool {
- Server::can_have_content_length(method, status) && method != &Some(Method::HEAD)
+ fn can_have_implicit_zero_content_length(method: Option<&Method>, status: StatusCode) -> bool {
+ Server::can_have_content_length(method, status) && method != Some(&Method::HEAD)
}
fn encode_headers_with_lower_case(
@@ -533,7 +565,7 @@ impl Server {
line: &str,
_: (HeaderName, &str),
) {
- extend(dst, line.as_bytes())
+ extend(dst, line.as_bytes());
}
#[inline]
@@ -543,12 +575,12 @@ impl Server {
name_with_colon: &str,
_: HeaderName,
) {
- extend(dst, name_with_colon.as_bytes())
+ extend(dst, name_with_colon.as_bytes());
}
#[inline]
fn write_header_name(&mut self, dst: &mut Vec, name: &HeaderName) {
- extend(dst, name.as_str().as_bytes())
+ extend(dst, name.as_str().as_bytes());
}
}
@@ -598,9 +630,9 @@ impl Server {
fn write_header_name(&mut self, dst: &mut Vec, name: &HeaderName) {
let Self {
map,
- ref mut current,
+ current,
title_case_headers,
- } = *self;
+ } = self;
if current.as_ref().map_or(true, |(last, _)| last != name) {
*current = None;
}
@@ -609,7 +641,7 @@ impl Server {
if let Some(orig_name) = values.next() {
extend(dst, orig_name);
- } else if title_case_headers {
+ } else if *title_case_headers {
title_case(dst, name.as_str().as_bytes());
} else {
extend(dst, name.as_str().as_bytes());
@@ -646,7 +678,7 @@ impl Server {
};
let mut encoder = Encoder::length(0);
- let mut allowed_trailer_fields: Option> = None;
+ let mut allowed_trailer_fields: Option> = None;
let mut wrote_date = false;
let mut cur_name = None;
let mut is_name_written = false;
@@ -701,9 +733,7 @@ impl Server {
if msg.req_method != &Some(Method::HEAD) || known_len != 0 {
assert!(
len == known_len,
- "payload claims content-length of {}, custom content-length header claims {}",
- known_len,
- len,
+ "payload claims content-length of {known_len}, custom content-length header claims {len}",
);
}
}
@@ -790,7 +820,7 @@ impl Server {
}
// check that we actually can send a chunked body...
if msg.head.version == Version::HTTP_10
- || !Server::can_chunked(msg.req_method, msg.head.subject)
+ || !Server::can_chunked(msg.req_method.as_ref(), msg.head.subject)
{
continue;
}
@@ -838,7 +868,7 @@ impl Server {
header::TRAILER => {
// check that we actually can send a chunked body...
if msg.head.version == Version::HTTP_10
- || !Server::can_chunked(msg.req_method, msg.head.subject)
+ || !Server::can_chunked(msg.req_method.as_ref(), msg.head.subject)
{
continue;
}
@@ -856,12 +886,22 @@ impl Server {
extend(dst, value.as_bytes());
}
- match allowed_trailer_fields {
- Some(ref mut allowed_trailer_fields) => {
- allowed_trailer_fields.push(value);
- }
- None => {
- allowed_trailer_fields = Some(vec![value]);
+ // Parse the Trailer header value into HeaderNames.
+ // The value may contain comma-separated names.
+ // HeaderName normalizes to lowercase for case-insensitive matching.
+ if let Ok(value_str) = value.to_str() {
+ let names: Vec = value_str
+ .split(',')
+ .filter_map(|s| HeaderName::from_bytes(s.trim().as_bytes()).ok())
+ .collect();
+
+ match &mut allowed_trailer_fields {
+ Some(fields) => {
+ fields.extend(names);
+ }
+ None => {
+ allowed_trailer_fields = Some(names);
+ }
}
}
@@ -875,8 +915,7 @@ impl Server {
// non-special write Name and Value
debug_assert!(
!is_name_written,
- "{:?} set is_name_written and didn't continue loop",
- name,
+ "{name:?} set is_name_written and didn't continue loop",
);
header_name_writer.write_header_name(dst, name);
extend(dst, b": ");
@@ -890,7 +929,7 @@ impl Server {
encoder = match msg.body {
Some(BodyLength::Unknown) => {
if msg.head.version == Version::HTTP_10
- || !Server::can_chunked(msg.req_method, msg.head.subject)
+ || !Server::can_chunked(msg.req_method.as_ref(), msg.head.subject)
{
Encoder::close_delimited()
} else {
@@ -904,19 +943,19 @@ impl Server {
}
None | Some(BodyLength::Known(0)) => {
if Server::can_have_implicit_zero_content_length(
- msg.req_method,
+ msg.req_method.as_ref(),
msg.head.subject,
) {
header_name_writer.write_full_header_line(
dst,
"content-length: 0\r\n",
(header::CONTENT_LENGTH, ": 0\r\n"),
- )
+ );
}
Encoder::length(0)
}
Some(BodyLength::Known(len)) => {
- if !Server::can_have_content_length(msg.req_method, msg.head.subject) {
+ if !Server::can_have_content_length(msg.req_method.as_ref(), msg.head.subject) {
Encoder::length(0)
} else {
header_name_writer.write_header_name_with_colon(
@@ -932,7 +971,7 @@ impl Server {
};
}
- if !Server::can_have_body(msg.req_method, msg.head.subject) {
+ if !Server::can_have_body(msg.req_method.as_ref(), msg.head.subject) {
trace!(
"server body forced to 0; method={:?}, status={:?}",
msg.req_method,
@@ -1022,10 +1061,15 @@ impl Http1Transaction for Client {
) {
Ok(httparse::Status::Complete(len)) => {
trace!("Response.parse Complete({})", len);
- let status = StatusCode::from_u16(res.code.unwrap())?;
+ if let Some(max_header_size) = ctx.h1_max_header_size {
+ if len > max_header_size {
+ return Err(Parse::TooLarge);
+ }
+ }
+ let status = StatusCode::from_u16(res.code.expect("httparse completed"))?;
let reason = {
- let reason = res.reason.unwrap();
+ let reason = res.reason.expect("httparse completed");
// Only save the reason phrase if it isn't the canonical reason
if Some(reason) != status.canonical_reason() {
Some(Bytes::copy_from_slice(reason.as_bytes()))
@@ -1034,7 +1078,7 @@ impl Http1Transaction for Client {
}
};
- let version = if res.version.unwrap() == 1 {
+ let version = if res.version.expect("httparse completed") == 1 {
Version::HTTP_11
} else {
Version::HTTP_10
@@ -1071,6 +1115,7 @@ impl Http1Transaction for Client {
let mut headers = ctx.cached_headers.take().unwrap_or_default();
let mut keep_alive = version == Version::HTTP_11;
+ let mut saw_connection_close = false;
let mut header_case_map = if ctx.preserve_header_case {
Some(HeaderCaseMap::default())
@@ -1093,22 +1138,24 @@ impl Http1Transaction for Client {
let value = header_value!(slice.slice(header.value.0..header.value.1));
if let header::CONNECTION = name {
- // keep_alive was previously set to default for Version
- if keep_alive {
- // HTTP/1.1
- keep_alive = !headers::connection_close(&value);
- } else {
- // HTTP/1.0
+ // A message may carry more than one `Connection` line. Once
+ // any of them asks to close, a later `keep-alive` must not
+ // undo it.
+ if saw_connection_close || headers::connection_close(&value) {
+ saw_connection_close = true;
+ keep_alive = false;
+ } else if !keep_alive {
+ // HTTP/1.0, which defaults to close
keep_alive = headers::connection_keep_alive(&value);
}
}
- if let Some(ref mut header_case_map) = header_case_map {
+ if let Some(header_case_map) = &mut header_case_map {
header_case_map.append(&name, slice.slice(header.name.0..header.name.1));
}
#[cfg(feature = "ffi")]
- if let Some(ref mut header_order) = header_order {
+ if let Some(header_order) = &mut header_order {
header_order.append(&name);
}
@@ -1191,7 +1238,8 @@ impl Http1Transaction for Client {
debug!("request with HTTP2 version coerced to HTTP/1.1");
extend(dst, b"HTTP/1.1");
}
- other => panic!("unexpected request version: {:?}", other),
+ #[allow(clippy::panic)]
+ other => panic!("unexpected request version: {other:?}"),
}
extend(dst, b"\r\n");
@@ -1226,9 +1274,9 @@ impl Http1Transaction for Client {
#[cfg(feature = "client")]
impl Client {
- /// Returns Some(length, wants_upgrade) if successful.
+ /// Returns `Some(length, wants_upgrade)` if successful.
///
- /// Returns None if this message head should be skipped (like a 100 status).
+ /// Returns `None` if this message head should be skipped (like a 100 status).
fn decoder(
inc: &MessageHead,
method: &mut Option,
@@ -1374,6 +1422,9 @@ impl Client {
Method::GET | Method::HEAD | Method::CONNECT => Some(Encoder::length(0)),
_ => {
te.insert(HeaderValue::from_static("chunked"));
+ // A valid Content-Length would have taken precedence above.
+ // Remove any invalid values now that we're using chunked.
+ should_remove_con_len = true;
Some(Encoder::chunked())
}
}
@@ -1385,8 +1436,16 @@ impl Client {
let encoder = encoder.map(|enc| {
if enc.is_chunked() {
- let allowed_trailer_fields: Vec =
- headers.get_all(header::TRAILER).iter().cloned().collect();
+ // Parse Trailer header values into HeaderNames.
+ // Each Trailer header value may contain comma-separated names.
+ // HeaderName normalizes to lowercase, enabling case-insensitive matching.
+ let allowed_trailer_fields: Vec = headers
+ .get_all(header::TRAILER)
+ .iter()
+ .filter_map(|hv| hv.to_str().ok())
+ .flat_map(|s| s.split(','))
+ .filter_map(|s| HeaderName::from_bytes(s.trim().as_bytes()).ok())
+ .collect();
if !allowed_trailer_fields.is_empty() {
return enc.into_chunked_with_trailing_fields(allowed_trailer_fields);
@@ -1399,7 +1458,7 @@ impl Client {
// This is because we need a second mutable borrow to remove
// content-length header.
if let Some(encoder) = encoder {
- if should_remove_con_len && existing_con_len.is_some() {
+ if should_remove_con_len {
headers.remove(header::CONTENT_LENGTH);
}
return encoder;
@@ -1614,7 +1673,7 @@ fn write_headers_original_case(
}
#[cfg(feature = "client")]
-struct FastWrite<'a>(&'a mut Vec);
+struct FastWrite<'data>(&'data mut Vec);
#[cfg(feature = "client")]
impl fmt::Write for FastWrite<'_> {
@@ -1654,6 +1713,7 @@ mod tests {
req_method: &mut method,
h1_parser_config: Default::default(),
h1_max_headers: None,
+ h1_max_header_size: None,
preserve_header_case: false,
#[cfg(feature = "ffi")]
preserve_header_order: false,
@@ -1682,6 +1742,7 @@ mod tests {
req_method: &mut Some(crate::Method::GET),
h1_parser_config: Default::default(),
h1_max_headers: None,
+ h1_max_header_size: None,
preserve_header_case: false,
#[cfg(feature = "ffi")]
preserve_header_order: false,
@@ -1706,6 +1767,7 @@ mod tests {
req_method: &mut None,
h1_parser_config: Default::default(),
h1_max_headers: None,
+ h1_max_header_size: None,
preserve_header_case: false,
#[cfg(feature = "ffi")]
preserve_header_order: false,
@@ -1727,6 +1789,7 @@ mod tests {
req_method: &mut Some(crate::Method::GET),
h1_parser_config: Default::default(),
h1_max_headers: None,
+ h1_max_header_size: None,
preserve_header_case: false,
#[cfg(feature = "ffi")]
preserve_header_order: false,
@@ -1750,6 +1813,7 @@ mod tests {
req_method: &mut Some(crate::Method::GET),
h1_parser_config: Default::default(),
h1_max_headers: None,
+ h1_max_header_size: None,
preserve_header_case: false,
#[cfg(feature = "ffi")]
preserve_header_order: false,
@@ -1777,6 +1841,7 @@ mod tests {
req_method: &mut Some(crate::Method::GET),
h1_parser_config,
h1_max_headers: None,
+ h1_max_header_size: None,
preserve_header_case: false,
#[cfg(feature = "ffi")]
preserve_header_order: false,
@@ -1801,6 +1866,7 @@ mod tests {
req_method: &mut Some(crate::Method::GET),
h1_parser_config: Default::default(),
h1_max_headers: None,
+ h1_max_header_size: None,
preserve_header_case: false,
#[cfg(feature = "ffi")]
preserve_header_order: false,
@@ -1811,6 +1877,63 @@ mod tests {
Client::parse(&mut raw, ctx).unwrap_err();
}
+ const REQUEST_WITH_MULTIPLE_SPACES_IN_REQUEST_LINE: &str =
+ "GET /echo HTTP/1.1\r\nHost: hyper.rs\r\n\r\n";
+
+ #[cfg(feature = "server")]
+ #[test]
+ fn test_parse_allow_request_with_multiple_spaces_in_request_line() {
+ use httparse::ParserConfig;
+
+ let _ = pretty_env_logger::try_init();
+ let mut raw = BytesMut::from(REQUEST_WITH_MULTIPLE_SPACES_IN_REQUEST_LINE);
+ let mut h1_parser_config = ParserConfig::default();
+ h1_parser_config.allow_multiple_spaces_in_request_line_delimiters(true);
+ let mut method = None;
+ let ctx = ParseContext {
+ cached_headers: &mut None,
+ req_method: &mut method,
+ h1_parser_config,
+ h1_max_headers: None,
+ h1_max_header_size: None,
+ preserve_header_case: false,
+ #[cfg(feature = "ffi")]
+ preserve_header_order: false,
+ h09_responses: false,
+ #[cfg(feature = "client")]
+ on_informational: &mut None,
+ };
+ let msg = Server::parse(&mut raw, ctx).unwrap().unwrap();
+ assert_eq!(raw.len(), 0);
+ assert_eq!(msg.head.subject.0, crate::Method::GET);
+ assert_eq!(msg.head.subject.1, "/echo");
+ assert_eq!(msg.head.version, crate::Version::HTTP_11);
+ assert_eq!(msg.head.headers.len(), 1);
+ assert_eq!(msg.head.headers["Host"], "hyper.rs");
+ assert_eq!(method, Some(crate::Method::GET));
+ }
+
+ #[cfg(feature = "server")]
+ #[test]
+ fn test_parse_reject_request_with_multiple_spaces_in_request_line() {
+ let _ = pretty_env_logger::try_init();
+ let mut raw = BytesMut::from(REQUEST_WITH_MULTIPLE_SPACES_IN_REQUEST_LINE);
+ let ctx = ParseContext {
+ cached_headers: &mut None,
+ req_method: &mut None,
+ h1_parser_config: Default::default(),
+ h1_max_headers: None,
+ h1_max_header_size: None,
+ preserve_header_case: false,
+ #[cfg(feature = "ffi")]
+ preserve_header_order: false,
+ h09_responses: false,
+ #[cfg(feature = "client")]
+ on_informational: &mut None,
+ };
+ Server::parse(&mut raw, ctx).unwrap_err();
+ }
+
#[cfg(feature = "server")]
#[test]
fn test_parse_preserve_header_case_in_request() {
@@ -1821,6 +1944,7 @@ mod tests {
req_method: &mut None,
h1_parser_config: Default::default(),
h1_max_headers: None,
+ h1_max_header_size: None,
preserve_header_case: true,
#[cfg(feature = "ffi")]
preserve_header_order: false,
@@ -1860,6 +1984,7 @@ mod tests {
req_method: &mut None,
h1_parser_config: Default::default(),
h1_max_headers: None,
+ h1_max_header_size: None,
preserve_header_case: false,
#[cfg(feature = "ffi")]
preserve_header_order: false,
@@ -1881,6 +2006,7 @@ mod tests {
req_method: &mut None,
h1_parser_config: Default::default(),
h1_max_headers: None,
+ h1_max_header_size: None,
preserve_header_case: false,
#[cfg(feature = "ffi")]
preserve_header_order: false,
@@ -1967,45 +2093,55 @@ mod tests {
);
// transfer-encoding and content-length = chunked
- assert_eq!(
- parse(
- "\
- POST / HTTP/1.1\r\n\
- content-length: 10\r\n\
- transfer-encoding: chunked\r\n\
- \r\n\
- "
- )
- .decode,
- DecodedLength::CHUNKED
+ let msg = parse(
+ "\
+ POST / HTTP/1.1\r\n\
+ content-length: 10\r\n\
+ transfer-encoding: chunked\r\n\
+ \r\n\
+ ",
);
+ assert_eq!(msg.decode, DecodedLength::CHUNKED);
+ assert!(!msg.head.headers.contains_key(header::CONTENT_LENGTH));
+ assert!(!msg.keep_alive);
- assert_eq!(
- parse(
- "\
- POST / HTTP/1.1\r\n\
- transfer-encoding: chunked\r\n\
- content-length: 10\r\n\
- \r\n\
- "
- )
- .decode,
- DecodedLength::CHUNKED
+ let msg = parse(
+ "\
+ POST / HTTP/1.1\r\n\
+ transfer-encoding: chunked\r\n\
+ content-length: 10\r\n\
+ \r\n\
+ ",
);
+ assert_eq!(msg.decode, DecodedLength::CHUNKED);
+ assert!(!msg.head.headers.contains_key(header::CONTENT_LENGTH));
+ assert!(!msg.keep_alive);
- assert_eq!(
- parse(
- "\
- POST / HTTP/1.1\r\n\
- transfer-encoding: gzip\r\n\
- content-length: 10\r\n\
- transfer-encoding: chunked\r\n\
- \r\n\
- "
- )
- .decode,
- DecodedLength::CHUNKED
+ let msg = parse(
+ "\
+ POST / HTTP/1.1\r\n\
+ transfer-encoding: gzip\r\n\
+ content-length: 10\r\n\
+ transfer-encoding: chunked\r\n\
+ \r\n\
+ ",
+ );
+ assert_eq!(msg.decode, DecodedLength::CHUNKED);
+ assert!(!msg.head.headers.contains_key(header::CONTENT_LENGTH));
+ assert!(!msg.keep_alive);
+
+ let msg = parse(
+ "\
+ POST / HTTP/1.1\r\n\
+ connection: keep-alive\r\n\
+ content-length: 10\r\n\
+ transfer-encoding: chunked\r\n\
+ \r\n\
+ ",
);
+ assert_eq!(msg.decode, DecodedLength::CHUNKED);
+ assert!(!msg.head.headers.contains_key(header::CONTENT_LENGTH));
+ assert!(!msg.keep_alive);
// multiple content-lengths of same value are fine
assert_eq!(
@@ -2111,6 +2247,7 @@ mod tests {
req_method: &mut Some(Method::GET),
h1_parser_config: Default::default(),
h1_max_headers: None,
+ h1_max_header_size: None,
preserve_header_case: false,
#[cfg(feature = "ffi")]
preserve_header_order: false,
@@ -2132,6 +2269,7 @@ mod tests {
req_method: &mut Some(m),
h1_parser_config: Default::default(),
h1_max_headers: None,
+ h1_max_header_size: None,
preserve_header_case: false,
#[cfg(feature = "ffi")]
preserve_header_order: false,
@@ -2153,6 +2291,7 @@ mod tests {
req_method: &mut Some(Method::GET),
h1_parser_config: Default::default(),
h1_max_headers: None,
+ h1_max_header_size: None,
preserve_header_case: false,
#[cfg(feature = "ffi")]
preserve_header_order: false,
@@ -2424,6 +2563,166 @@ mod tests {
);
}
+ #[cfg(feature = "server")]
+ #[test]
+ fn test_parse_request_multiple_connection_headers() {
+ fn parse(s: &str) -> ParsedMessage |