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 { + let mut bytes = BytesMut::from(s); + Server::parse( + &mut bytes, + 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, + }, + ) + .unwrap() + .unwrap() + } + + assert!( + !parse( + "\ + GET / HTTP/1.1\r\n\ + connection: close\r\n\ + connection: keep-alive\r\n\ + \r\n\ + " + ) + .keep_alive, + "close before keep-alive is still close" + ); + + assert!( + !parse( + "\ + GET / HTTP/1.1\r\n\ + connection: keep-alive\r\n\ + connection: close\r\n\ + \r\n\ + " + ) + .keep_alive, + "keep-alive before close is still close" + ); + + assert!( + parse( + "\ + GET / HTTP/1.0\r\n\ + connection: keep-alive\r\n\ + connection: foo\r\n\ + \r\n\ + " + ) + .keep_alive, + "HTTP/1.0 keep-alive is not undone by an unrelated token" + ); + + assert!( + !parse( + "\ + GET / HTTP/1.0\r\n\ + connection: keep-alive\r\n\ + connection: close\r\n\ + \r\n\ + " + ) + .keep_alive, + "HTTP/1.0 close wins over an earlier keep-alive" + ); + } + + #[cfg(feature = "client")] + #[test] + fn test_parse_response_multiple_connection_headers() { + fn parse(s: &str) -> ParsedMessage { + let mut bytes = BytesMut::from(s); + Client::parse( + &mut bytes, + ParseContext { + cached_headers: &mut None, + 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, + h09_responses: false, + #[cfg(feature = "client")] + on_informational: &mut None, + }, + ) + .unwrap() + .unwrap() + } + + assert!( + !parse( + "\ + HTTP/1.1 200 OK\r\n\ + content-length: 0\r\n\ + connection: close\r\n\ + connection: keep-alive\r\n\ + \r\n\ + " + ) + .keep_alive, + "close before keep-alive is still close" + ); + + assert!( + !parse( + "\ + HTTP/1.1 200 OK\r\n\ + content-length: 0\r\n\ + connection: keep-alive\r\n\ + connection: close\r\n\ + \r\n\ + " + ) + .keep_alive, + "keep-alive before close is still close" + ); + + assert!( + parse( + "\ + HTTP/1.0 200 OK\r\n\ + content-length: 0\r\n\ + connection: keep-alive\r\n\ + connection: foo\r\n\ + \r\n\ + " + ) + .keep_alive, + "HTTP/1.0 keep-alive is not undone by an unrelated token" + ); + + assert!( + !parse( + "\ + HTTP/1.0 200 OK\r\n\ + content-length: 0\r\n\ + connection: keep-alive\r\n\ + connection: close\r\n\ + \r\n\ + " + ) + .keep_alive, + "HTTP/1.0 close wins over an earlier keep-alive" + ); + } + #[cfg(feature = "client")] #[test] fn test_client_obs_fold_line() { @@ -2442,6 +2741,118 @@ mod tests { assert_eq!(unfold("obs\r\n fold\r\n\t line"), "obs fold line",); } + #[test] + fn test_client_request_encode_explicit_transfer_encoding_removes_content_length() { + use crate::proto::BodyLength; + use http::header::HeaderValue; + + for values in [ + vec!["10"], + vec![""], + vec!["abc"], + vec!["+10"], + vec!["18446744073709551616"], + vec!["10", "11"], + vec!["10, 11"], + vec!["10", "abc"], + ] { + for transfer_encoding in ["chunked", "gzip"] { + for body in [BodyLength::Known(10), BodyLength::Unknown] { + let mut head = MessageHead::default(); + for value in &values { + head.headers + .append("content-length", HeaderValue::from_static(value)); + } + head.headers.insert( + "transfer-encoding", + HeaderValue::from_static(transfer_encoding), + ); + + let mut vec = Vec::new(); + let encoder = Client::encode( + Encode { + head: &mut head, + body: Some(body), + #[cfg(feature = "server")] + keep_alive: true, + req_method: &mut None, + title_case_headers: false, + #[cfg(feature = "server")] + date_header: true, + }, + &mut vec, + ) + .unwrap(); + + assert!(encoder.is_chunked()); + let expected_te = if transfer_encoding == "chunked" { + "chunked" + } else { + "gzip, chunked" + }; + assert_eq!( + vec, + format!("GET / HTTP/1.1\r\ntransfer-encoding: {expected_te}\r\n\r\n") + .as_bytes(), + "content-length: {values:?}, transfer-encoding: {transfer_encoding}" + ); + } + } + } + } + + #[test] + fn test_client_request_encode_automatic_transfer_encoding_removes_content_length() { + use crate::proto::BodyLength; + use http::header::HeaderValue; + + for values in [ + vec![], + vec![""], + vec!["abc"], + vec!["+10"], + vec!["18446744073709551616"], + vec!["10", "11"], + vec!["10, 11"], + vec!["10", "abc"], + vec!["10"], + ] { + let mut head = RequestHead::default(); + head.subject.0 = Method::POST; + for value in &values { + head.headers + .append("content-length", HeaderValue::from_static(value)); + } + + let mut vec = Vec::new(); + let encoder = Client::encode( + Encode { + head: &mut head, + body: Some(BodyLength::Unknown), + #[cfg(feature = "server")] + keep_alive: true, + req_method: &mut None, + title_case_headers: false, + #[cfg(feature = "server")] + date_header: true, + }, + &mut vec, + ) + .unwrap(); + + if values == ["10"] { + assert_eq!(encoder, Encoder::length(10)); + assert_eq!(vec, b"POST / HTTP/1.1\r\ncontent-length: 10\r\n\r\n"); + } else { + assert!(encoder.is_chunked()); + assert_eq!( + vec, b"POST / HTTP/1.1\r\ntransfer-encoding: chunked\r\n\r\n", + "content-length: {values:?}" + ); + } + } + } + #[test] fn test_client_request_encode_title_case() { use crate::proto::BodyLength; @@ -2723,6 +3134,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, @@ -2767,6 +3179,7 @@ mod tests { req_method: &mut None, h1_parser_config: Default::default(), h1_max_headers: max_headers, + h1_max_header_size: None, preserve_header_case: false, #[cfg(feature = "ffi")] preserve_header_order: false, @@ -2791,6 +3204,7 @@ mod tests { req_method: &mut None, h1_parser_config: Default::default(), h1_max_headers: max_headers, + h1_max_header_size: None, preserve_header_case: false, #[cfg(feature = "ffi")] preserve_header_order: false, @@ -2867,6 +3281,78 @@ mod tests { parse(Some(200), 210, false); } + #[test] + #[cfg(feature = "server")] + fn test_h1_server_max_header_size() { + let _ = pretty_env_logger::try_init(); + + let req_str = "GET / HTTP/1.1\r\nHost: example.com\r\n\r\n"; + assert_eq!(req_str.len(), 37); + + let parse_req = |max_header_size: Option| { + let mut bytes = BytesMut::from(req_str); + Server::parse( + &mut bytes, + ParseContext { + cached_headers: &mut None, + req_method: &mut None, + h1_parser_config: Default::default(), + h1_max_headers: None, + h1_max_header_size: max_header_size, + preserve_header_case: false, + #[cfg(feature = "ffi")] + preserve_header_order: false, + h09_responses: false, + #[cfg(feature = "client")] + on_informational: &mut None, + }, + ) + }; + + // Server checks + parse_req(None).unwrap().unwrap(); + parse_req(Some(37)).unwrap().unwrap(); + parse_req(Some(50)).unwrap().unwrap(); + assert!(matches!(parse_req(Some(36)), Err(Parse::TooLarge))); + assert!(matches!(parse_req(Some(10)), Err(Parse::TooLarge))); + } + + #[test] + #[cfg(feature = "client")] + fn test_h1_client_max_header_size() { + let _ = pretty_env_logger::try_init(); + + let resp_str = "HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n"; + assert_eq!(resp_str.len(), 38); + + let parse_resp = |max_header_size: Option| { + let mut bytes = BytesMut::from(resp_str); + Client::parse( + &mut bytes, + ParseContext { + cached_headers: &mut None, + req_method: &mut None, + h1_parser_config: Default::default(), + h1_max_headers: None, + h1_max_header_size: max_header_size, + preserve_header_case: false, + #[cfg(feature = "ffi")] + preserve_header_order: false, + h09_responses: false, + #[cfg(feature = "client")] + on_informational: &mut None, + }, + ) + }; + + // Client checks + parse_resp(None).unwrap().unwrap(); + parse_resp(Some(38)).unwrap().unwrap(); + parse_resp(Some(50)).unwrap().unwrap(); + assert!(matches!(parse_resp(Some(37)), Err(Parse::TooLarge))); + assert!(matches!(parse_resp(Some(10)), Err(Parse::TooLarge))); + } + #[test] fn test_is_complete_fast() { let s = b"GET / HTTP/1.1\r\na: b\r\n\r\n"; @@ -2877,6 +3363,10 @@ mod tests { for n in 0..s.len() { assert!(is_complete_fast(s, n)); } + let s = b"GET / HTTP/1.1\r\na: b\n\r\n"; + for n in 0..s.len() { + assert!(is_complete_fast(s, n), "{:?}; {}", s, n); + } // Not let s = b"GET / HTTP/1.1\r\na: b\r\n\r"; @@ -2887,6 +3377,37 @@ mod tests { for n in 0..s.len() { assert!(!is_complete_fast(s, n)); } + let s = b"GET / HTTP/1.1\r\na: b\n\r"; + for n in 0..s.len() { + assert!(!is_complete_fast(s, n)); + } + } + + #[cfg(feature = "server")] + #[test] + fn test_parse_accepts_lf_crlf_terminator() { + // The full parser (httparse) accepts a bare-LF line ending followed + // by a CRLF blank line as the end of the head, so the partial-read + // fast path must recognize it too. + let mut bytes = BytesMut::from("GET / HTTP/1.1\r\na: b\n\r\n"); + Server::parse( + &mut bytes, + 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, + }, + ) + .expect("parse ok") + .expect("parse complete"); } #[test] @@ -2960,6 +3481,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, @@ -3005,6 +3527,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, diff --git a/src/proto/h2/client.rs b/src/proto/h2/client.rs index 455c70980c..eb768baa76 100644 --- a/src/proto/h2/client.rs +++ b/src/proto/h2/client.rs @@ -74,8 +74,10 @@ pub(crate) struct Config { pub(crate) max_concurrent_reset_streams: Option, pub(crate) max_send_buffer_size: usize, pub(crate) max_pending_accept_reset_streams: Option, + pub(crate) max_local_error_reset_streams: Option, pub(crate) header_table_size: Option, pub(crate) max_concurrent_streams: Option, + pub(crate) reset_stream_duration: Option, } impl Default for Config { @@ -93,8 +95,10 @@ impl Default for Config { max_concurrent_reset_streams: None, max_send_buffer_size: DEFAULT_MAX_SEND_BUF_SIZE, max_pending_accept_reset_streams: None, + max_local_error_reset_streams: Some(1024), header_table_size: None, max_concurrent_streams: None, + reset_stream_duration: None, } } } @@ -107,6 +111,7 @@ fn new_builder(config: &Config) -> Builder { .initial_connection_window_size(config.initial_conn_window_size) .max_header_list_size(config.max_header_list_size) .max_send_buffer_size(config.max_send_buffer_size) + .max_local_error_reset_streams(config.max_local_error_reset_streams) .enable_push(false); if let Some(max) = config.max_frame_size { builder.max_frame_size(max); @@ -123,6 +128,9 @@ fn new_builder(config: &Config) -> Builder { if let Some(max) = config.max_concurrent_streams { builder.max_concurrent_streams(max); } + if let Some(dur) = config.reset_stream_duration { + builder.reset_stream_duration(dur); + } builder } @@ -439,6 +447,12 @@ where pub(crate) fn is_extended_connect_protocol_enabled(&self) -> bool { self.h2_tx.is_extended_connect_protocol_enabled() } + pub(crate) fn current_max_send_streams(&self) -> usize { + self.h2_tx.current_max_send_streams() + } + pub(crate) fn current_max_recv_streams(&self) -> usize { + self.h2_tx.current_max_recv_streams() + } } pin_project! { @@ -452,6 +466,7 @@ pin_project! { conn_drop_ref: Option>, #[pin] ping: Option, + cancel_rx: Option>, } } @@ -465,6 +480,26 @@ where fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> std::task::Poll { let mut this = self.project(); + // Check if the client cancelled the request (e.g. dropped the + // response future due to a timeout). If so, reset the h2 stream + // so that a RST_STREAM is sent and flow-control capacity is freed. + let cancel_result = this.cancel_rx.as_mut().map(|rx| Pin::new(rx).poll(cx)); + match cancel_result { + Some(Poll::Ready(Ok(()))) => { + debug!("client request body send cancelled, resetting stream"); + this.pipe.as_mut().send_reset(h2::Reason::CANCEL); + drop(this.conn_drop_ref.take().expect("Future polled twice")); + drop(this.ping.take().expect("Future polled twice")); + return Poll::Ready(()); + } + Some(Poll::Ready(Err(_))) => { + // Sender dropped without cancelling (normal response or error). + // Stop polling the receiver. + *this.cancel_rx = None; + } + Some(Poll::Pending) | None => {} + } + match Pin::new(&mut this.pipe).poll(cx) { Poll::Ready(result) => { if let Err(_e) = result { @@ -475,7 +510,7 @@ where return Poll::Ready(()); } Poll::Pending => (), - }; + } Poll::Pending } } @@ -491,6 +526,10 @@ where fn poll_pipe(&mut self, f: FutCtx, cx: &mut Context<'_>) { let ping = self.ping.clone(); + // A one-shot channel so that send_task can tell pipe_task to + // reset the stream when the client cancels the request. + let (cancel_tx, cancel_rx) = oneshot::channel::<()>(); + let send_stream = if !f.is_connect { if !f.eos { let mut pipe = PipeToSendStream::new(f.body, f.body_tx); @@ -510,6 +549,7 @@ where pipe, conn_drop_ref: Some(conn_drop_ref), ping: Some(ping), + cancel_rx: Some(cancel_rx), }; // Clear send task self.executor @@ -530,6 +570,7 @@ where ping: Some(ping), send_stream: Some(send_stream), exec: self.executor.clone(), + cancel_tx: Some(cancel_tx), }, call_back: Some(f.cb), }, @@ -549,6 +590,16 @@ pin_project! { #[pin] send_stream: Option::Data>>>>, exec: E, + cancel_tx: Option>, + } +} + +impl ResponseFutMap { + /// Signal the `pipe_task` to reset the stream (e.g. on client cancellation). + pub(crate) fn cancel(self: Pin<&mut Self>) { + if let Some(cancel_tx) = self.project().cancel_tx.take() { + let _ = cancel_tx.send(()); + } } } @@ -637,7 +688,7 @@ where Poll::Ready(Err(crate::Error::new_h2(err))) }; } - }; + } // If we were waiting on pending open // continue where we left off. @@ -655,7 +706,7 @@ where } let (head, body) = req.into_parts(); let mut req = ::http::Request::from_parts(head, ()); - super::strip_connection_headers(req.headers_mut(), true); + super::strip_connection_headers(req.headers_mut(), super::MessageKind::Request); if let Some(len) = body.size_hint().exact() { if len != 0 || headers::method_has_defined_payload_semantics(req.method()) { headers::set_content_length_if_missing(req.headers_mut(), len); @@ -721,7 +772,6 @@ where } } self.poll_pipe(f, cx); - continue; } Poll::Ready(None) => { diff --git a/src/proto/h2/mod.rs b/src/proto/h2/mod.rs index e6f40f3467..393d4179ab 100644 --- a/src/proto/h2/mod.rs +++ b/src/proto/h2/mod.rs @@ -7,7 +7,7 @@ use std::task::{Context, Poll}; use bytes::Buf; use futures_core::ready; use h2::SendStream; -use http::header::{HeaderName, CONNECTION, TE, TRANSFER_ENCODING, UPGRADE}; +use http::header::{HeaderName, CONNECTION, TRANSFER_ENCODING, UPGRADE}; use http::HeaderMap; use pin_project_lite::pin_project; @@ -40,22 +40,32 @@ static CONNECTION_HEADERS: [HeaderName; 4] = [ UPGRADE, ]; -fn strip_connection_headers(headers: &mut HeaderMap, is_request: bool) { +enum MessageKind { + #[cfg(feature = "client")] + Request, + #[cfg(feature = "server")] + Response, +} + +fn strip_connection_headers(headers: &mut HeaderMap, kind: MessageKind) { for header in &CONNECTION_HEADERS { if headers.remove(header).is_some() { warn!("Connection header illegal in HTTP/2: {}", header.as_str()); } } - if is_request { + #[cfg(not(feature = "client"))] + let _ = kind; + #[cfg(feature = "client")] + if matches!(kind, MessageKind::Request) { if headers - .get(TE) + .get(http::header::TE) .map_or(false, |te_header| te_header != "trailers") { warn!("TE headers not set to \"trailers\" are illegal in HTTP/2 requests"); - headers.remove(TE); + headers.remove(http::header::TE); } - } else if headers.remove(TE).is_some() { + } else if headers.remove(http::header::TE).is_some() { warn!("TE headers illegal in HTTP/2 responses"); } @@ -64,17 +74,17 @@ fn strip_connection_headers(headers: &mut HeaderMap, is_request: bool) { "Connection header illegal in HTTP/2: {}", CONNECTION.as_str() ); - let header_contents = header.to_str().unwrap(); - // A `Connection` header may have a comma-separated list of names of other headers that // are meant for only this specific connection. // // Iterate these names and remove them as headers. Connection-specific headers are // forbidden in HTTP2, as that information has been moved into frame types of the h2 // protocol. - for name in header_contents.split(',') { - let name = name.trim(); - headers.remove(name); + if let Ok(header_contents) = header.to_str() { + for name in header_contents.split(',') { + let name = name.trim(); + headers.remove(name); + } } } } @@ -88,11 +98,21 @@ pin_project! { { body_tx: SendStream>, data_done: bool, + // A data chunk that has been polled from the body but is still waiting + // for stream-level capacity before it can be shipped. Stored here so + // it survives across `Poll::Pending` returns from `poll_capacity`; if + // we left the chunk in a local, it would be dropped on every repoll. + buffered_data: Option>, #[pin] stream: S, } } +struct Peeked { + data: D, + is_eos: bool, +} + impl PipeToSendStream where S: Body, @@ -101,9 +121,15 @@ where PipeToSendStream { body_tx: tx, data_done: false, + buffered_data: None, stream, } } + + #[cfg(feature = "client")] + fn send_reset(self: Pin<&mut Self>, reason: h2::Reason) { + self.project().body_tx.send_reset(reason); + } } impl Future for PipeToSendStream @@ -116,13 +142,23 @@ where fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { let mut me = self.project(); loop { - // we don't have the next chunk of data yet, so just reserve 1 byte to make - // sure there's some capacity available. h2 will handle the capacity management - // for the actual body chunk. - me.body_tx.reserve_capacity(1); + // Register for RST_STREAM notification while we wait for the next + // body chunk or for send capacity, so the task wakes up if the + // peer resets the stream. + if let Poll::Ready(reason) = me + .body_tx + .poll_reset(cx) + .map_err(crate::Error::new_body_write)? + { + debug!("stream received RST_STREAM: {:?}", reason); + return Poll::Ready(Err(crate::Error::new_body_write(::h2::Error::from(reason)))); + } - if me.body_tx.capacity() == 0 { - loop { + // If a previously-polled chunk is still waiting for stream-level + // send capacity, drive that to completion before touching the + // body again. + if me.buffered_data.is_some() { + while me.body_tx.capacity() == 0 { match ready!(me.body_tx.poll_capacity(cx)) { Some(Ok(0)) => {} Some(Ok(_)) => break, @@ -137,34 +173,68 @@ where } } } - } else if let Poll::Ready(reason) = me - .body_tx - .poll_reset(cx) - .map_err(crate::Error::new_body_write)? - { - debug!("stream received RST_STREAM: {:?}", reason); - return Poll::Ready(Err(crate::Error::new_body_write(::h2::Error::from(reason)))); + + let peeked = me.buffered_data.take().expect("checked is_some above"); + let buf = SendBuf::Buf(peeked.data); + me.body_tx + .send_data(buf, peeked.is_eos) + .map_err(crate::Error::new_body_write)?; + + if peeked.is_eos { + return Poll::Ready(Ok(())); + } + continue; } + // Poll for the next body frame *before* reserving any connection + // flow-control capacity. Reserving capacity speculatively (even a + // single byte) pins that capacity on the connection-level window, + // which can deadlock a second stream when talking to peers that + // only emit WINDOW_UPDATE once their receive window is fully + // exhausted. See #4003. match ready!(me.stream.as_mut().poll_frame(cx)) { Some(Ok(frame)) => { if frame.is_data() { let chunk = frame.into_data().unwrap_or_else(|_| unreachable!()); let is_eos = me.stream.is_end_stream(); - trace!( - "send body chunk: {} bytes, eos={}", - chunk.remaining(), - is_eos, - ); + let len = chunk.remaining(); + trace!("send body chunk: {} bytes, eos={}", len, is_eos); - let buf = SendBuf::Buf(chunk); - me.body_tx - .send_data(buf, is_eos) - .map_err(crate::Error::new_body_write)?; + if len == 0 { + // Zero-length data frames need no capacity; send + // them straight through so trailing empty frames + // (e.g. an explicit end-of-stream marker) are + // delivered. + let buf = SendBuf::Buf(chunk); + me.body_tx + .send_data(buf, is_eos) + .map_err(crate::Error::new_body_write)?; - if is_eos { - return Poll::Ready(Ok(())); + if is_eos { + return Poll::Ready(Ok(())); + } + continue; } + + // Reserve a minimal claim on the connection-level + // flow-control window rather than the whole chunk. The + // chunk is already in hand, so this still cannot pin + // capacity against a body that never produces data + // (#4003), and h2 raises the request to the buffered + // length inside `send_data`, so the demand eventually + // signalled to the peer is unchanged. Claiming the full + // length up front instead makes every in-flight stream a + // heavyweight claimant while it waits, which is costly + // once the streams on a connection collectively demand + // more than the window the peer advertises. Stash the + // chunk in `self` so it survives the upcoming + // `poll_capacity` wait even if it returns + // `Poll::Pending`. + me.body_tx.reserve_capacity(1); + *me.buffered_data = Some(Peeked { + data: chunk, + is_eos, + }); } else if frame.is_trailers() { // no more DATA, so give any capacity back me.body_tx.reserve_capacity(0); @@ -224,35 +294,35 @@ enum SendBuf { impl Buf for SendBuf { #[inline] fn remaining(&self) -> usize { - match *self { - Self::Buf(ref b) => b.remaining(), - Self::Cursor(ref c) => Buf::remaining(c), + match self { + Self::Buf(b) => b.remaining(), + Self::Cursor(c) => Buf::remaining(c), Self::None => 0, } } #[inline] fn chunk(&self) -> &[u8] { - match *self { - Self::Buf(ref b) => b.chunk(), - Self::Cursor(ref c) => c.chunk(), + match self { + Self::Buf(b) => b.chunk(), + Self::Cursor(c) => c.chunk(), Self::None => &[], } } #[inline] fn advance(&mut self, cnt: usize) { - match *self { - Self::Buf(ref mut b) => b.advance(cnt), - Self::Cursor(ref mut c) => c.advance(cnt), + match self { + Self::Buf(b) => b.advance(cnt), + Self::Cursor(c) => c.advance(cnt), Self::None => {} } } - fn chunks_vectored<'a>(&'a self, dst: &mut [IoSlice<'a>]) -> usize { - match *self { - Self::Buf(ref b) => b.chunks_vectored(dst), - Self::Cursor(ref c) => c.chunks_vectored(dst), + fn chunks_vectored<'data>(&'data self, dst: &mut [IoSlice<'data>]) -> usize { + match self { + Self::Buf(b) => b.chunks_vectored(dst), + Self::Cursor(c) => c.chunks_vectored(dst), Self::None => 0, } } diff --git a/src/proto/h2/ping.rs b/src/proto/h2/ping.rs index 4952e38518..33016a28d4 100644 --- a/src/proto/h2/ping.rs +++ b/src/proto/h2/ping.rs @@ -1,4 +1,4 @@ -//! HTTP2 Ping usage +//! HTTP2 Ping usage. //! //! hyper uses HTTP2 pings for two purposes: //! @@ -28,6 +28,7 @@ use std::time::{Duration, Instant}; use h2::{Ping, PingPong}; +use crate::common::lock::LockResultExt; use crate::common::time::Time; use crate::rt::Sleep; @@ -137,11 +138,11 @@ struct Shared { } struct Bdp { - /// Current BDP in bytes + /// Current BDP in bytes. bdp: u32, /// Largest bandwidth we've seen so far. max_bandwidth: f64, - /// Round trip time in seconds + /// Round trip time in seconds. rtt: f64, /// Delay the next ping by this amount. /// @@ -190,20 +191,20 @@ impl Config { impl Recorder { pub(crate) fn record_data(&self, len: usize) { - let shared = if let Some(ref shared) = self.shared { + let shared = if let Some(shared) = &self.shared { shared } else { return; }; - let mut locked = shared.lock().unwrap(); + let mut locked = shared.lock().panic_if_poisoned(); locked.update_last_read_at(); // are we ready to send another bdp ping? // if not, we don't need to record bytes either - if let Some(ref next_bdp_at) = locked.next_bdp_at { + if let Some(next_bdp_at) = &locked.next_bdp_at { if locked.timer.now() < *next_bdp_at { return; } else { @@ -211,7 +212,7 @@ impl Recorder { } } - if let Some(ref mut bytes) = locked.bytes { + if let Some(bytes) = &mut locked.bytes { *bytes += len; } else { // no need to send bdp ping if bdp is disabled @@ -224,13 +225,13 @@ impl Recorder { } pub(crate) fn record_non_data(&self) { - let shared = if let Some(ref shared) = self.shared { + let shared = if let Some(shared) = &self.shared { shared } else { return; }; - let mut locked = shared.lock().unwrap(); + let mut locked = shared.lock().panic_if_poisoned(); locked.update_last_read_at(); } @@ -247,8 +248,8 @@ impl Recorder { } pub(super) fn ensure_not_timed_out(&self) -> crate::Result<()> { - if let Some(ref shared) = self.shared { - let locked = shared.lock().unwrap(); + if let Some(shared) = &self.shared { + let locked = shared.lock().panic_if_poisoned(); if locked.is_keep_alive_timed_out { return Err(KeepAliveTimedOut.crate_error()); } @@ -263,11 +264,11 @@ impl Recorder { impl Ponger { pub(super) fn poll(&mut self, cx: &mut task::Context<'_>) -> Poll { - let mut locked = self.shared.lock().unwrap(); + let mut locked = self.shared.lock().panic_if_poisoned(); let now = locked.timer.now(); // hoping this is fine to move within the lock let is_idle = self.is_idle(); - if let Some(ref mut ka) = self.keep_alive { + if let Some(ka) = &mut self.keep_alive { ka.maybe_schedule(is_idle, &locked); ka.maybe_ping(cx, is_idle, &mut locked); } @@ -286,13 +287,13 @@ impl Ponger { let rtt = now - start; trace!("recv pong"); - if let Some(ref mut ka) = self.keep_alive { + if let Some(ka) = &mut self.keep_alive { locked.update_last_read_at(); ka.maybe_schedule(is_idle, &locked); ka.maybe_ping(cx, is_idle, &mut locked); } - if let Some(ref mut bdp) = self.bdp { + if let Some(bdp) = &mut self.bdp { let bytes = locked.bytes.expect("bdp enabled implies bytes"); locked.bytes = Some(0); // reset trace!("received BDP ack; bytes = {}, rtt = {:?}", bytes, rtt); @@ -308,7 +309,7 @@ impl Ponger { debug!("pong error: {}", _e); } Poll::Pending => { - if let Some(ref mut ka) = self.keep_alive { + if let Some(ka) = &mut self.keep_alive { if let Err(KeepAliveTimedOut) = ka.maybe_timeout(cx) { self.keep_alive = None; locked.is_keep_alive_timed_out = true; @@ -360,12 +361,12 @@ impl Shared { // ===== impl Bdp ===== /// Any higher than this likely will be hitting the TCP flow control. -const BDP_LIMIT: usize = 1024 * 1024 * 16; +const BDP_LIMIT: WindowSize = 1024 * 1024 * 16; impl Bdp { fn calculate(&mut self, bytes: usize, rtt: Duration) -> Option { // No need to do any math if we're at the limit. - if self.bdp as usize == BDP_LIMIT { + if self.bdp == BDP_LIMIT { self.stabilize_delay(); return None; } @@ -394,8 +395,11 @@ impl Bdp { // if the current `bytes` sample is at least 2/3 the previous // bdp, increase to double the current sample. + #[allow(clippy::integer_division_remainder_used)] + #[allow(clippy::integer_division)] if bytes >= self.bdp as usize * 2 / 3 { - self.bdp = (bytes * 2).min(BDP_LIMIT) as WindowSize; + self.bdp = + WindowSize::try_from((bytes * 2).min(BDP_LIMIT as usize)).unwrap_or(BDP_LIMIT); trace!("BDP increased to {}", self.bdp); self.stable_count = 0; @@ -422,7 +426,7 @@ impl Bdp { fn seconds(dur: Duration) -> f64 { const NANOS_PER_SEC: f64 = 1_000_000_000.0; let secs = dur.as_secs() as f64; - secs + (dur.subsec_nanos() as f64) / NANOS_PER_SEC + secs + (f64::from(dur.subsec_nanos())) / NANOS_PER_SEC } // ===== impl KeepAlive ===== diff --git a/src/proto/h2/server.rs b/src/proto/h2/server.rs index 483ed96dd9..2098758d9e 100644 --- a/src/proto/h2/server.rs +++ b/src/proto/h2/server.rs @@ -53,6 +53,7 @@ pub(crate) struct Config { pub(crate) keep_alive_interval: Option, pub(crate) keep_alive_timeout: Duration, pub(crate) max_send_buffer_size: usize, + pub(crate) header_table_size: Option, pub(crate) max_header_list_size: u32, pub(crate) date_header: bool, } @@ -68,6 +69,7 @@ impl Default for Config { max_concurrent_streams: Some(200), max_pending_accept_reset_streams: None, max_local_error_reset_streams: Some(DEFAULT_MAX_LOCAL_ERROR_RESET_STREAMS), + header_table_size: None, keep_alive_interval: None, keep_alive_timeout: Duration::from_secs(20), max_send_buffer_size: DEFAULT_MAX_SEND_BUF_SIZE, @@ -92,6 +94,8 @@ pin_project! { } } +//#[expect(clippy::large_enum_variant, reason = "the whole future is boxed")] +#[allow(clippy::large_enum_variant)] enum State where B: Body, @@ -142,6 +146,9 @@ where if let Some(max) = config.max_pending_accept_reset_streams { builder.max_pending_accept_reset_streams(max); } + if let Some(size) = config.header_table_size { + builder.header_table_size(size); + } if config.enable_connect_protocol { builder.enable_connect_protocol(); } @@ -177,11 +184,11 @@ where pub(crate) fn graceful_shutdown(&mut self) { trace!("graceful_shutdown"); - match self.state { + match &mut self.state { State::Handshaking { .. } => { self.close_pending = true; } - State::Serving(ref mut srv) => { + State::Serving(srv) => { if srv.closing.is_none() { srv.conn.graceful_shutdown(); } @@ -203,11 +210,8 @@ where fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { let me = &mut *self; loop { - let next = match me.state { - State::Handshaking { - ref mut hs, - ref ping_config, - } => { + let next = match &mut me.state { + State::Handshaking { hs, ping_config } => { let mut conn = ready!(Pin::new(hs).poll(cx).map_err(crate::Error::new_h2))?; let ping = if ping_config.is_enabled() { let pp = conn.ping_pong().expect("conn.ping_pong"); @@ -222,7 +226,7 @@ where date_header: me.date_header, }) } - State::Serving(ref mut srv) => { + State::Serving(srv) => { // graceful_shutdown was called before handshaking finished, if me.close_pending && srv.closing.is_none() { srv.conn.graceful_shutdown(); @@ -263,8 +267,7 @@ where let ping = self .ping .as_ref() - .map(|ping| ping.0.clone()) - .unwrap_or_else(ping::disabled); + .map_or_else(ping::disabled, |ping| ping.0.clone()); // Record the headers received ping.record_non_data(); @@ -317,7 +320,7 @@ where } None => { // no more incoming streams... - if let Some((ref ping, _)) = self.ping { + if let Some((ping, _)) = &self.ping { ping.ensure_not_timed_out()?; } @@ -339,7 +342,7 @@ where } fn poll_ping(&mut self, cx: &mut Context<'_>) { - if let Some((_, ref mut estimator)) = self.ping { + if let Some((_, estimator)) = &mut self.ping { match estimator.poll(cx) { Poll::Ready(ping::Ponged::SizeUpdate(wnd)) => { self.conn.set_target_window_size(wnd); @@ -466,7 +469,10 @@ where let (head, body) = res.into_parts(); let mut res = ::http::Response::from_parts(head, ()); - super::strip_connection_headers(res.headers_mut(), false); + super::strip_connection_headers( + res.headers_mut(), + super::MessageKind::Response, + ); // set Date header if it isn't already set if instructed if *me.date_header { diff --git a/src/proto/h2/upgrade.rs b/src/proto/h2/upgrade.rs index 40a98de08a..def1fcfc96 100644 --- a/src/proto/h2/upgrade.rs +++ b/src/proto/h2/upgrade.rs @@ -31,6 +31,7 @@ pub(super) fn pair( UpgradedSendStreamTask { h2_tx: send_stream, rx, + buffered: None, error_tx: Some(error_tx), }, ) @@ -55,6 +56,7 @@ pin_project! { h2_tx: SendStream>, #[pin] rx: mpsc::Receiver>>, + buffered: Option>>, error_tx: Option>, } } @@ -73,21 +75,30 @@ where // one of the sides hanging up, so the task doesn't live around // longer than it's meant to. loop { - // we don't have the next chunk of data yet, so just reserve 1 byte to make - // sure there's some capacity available. h2 will handle the capacity management - // for the actual body chunk. - me.h2_tx.reserve_capacity(1); + match me.h2_tx.poll_reset(cx) { + Poll::Ready(Ok(reason)) => { + trace!("stream received RST_STREAM: {:?}", reason); + return Poll::Ready(Err(crate::Error::new_body_write(::h2::Error::from( + reason, + )))); + } + Poll::Ready(Err(err)) => { + return Poll::Ready(Err(crate::Error::new_body_write(err))) + } + Poll::Pending => (), + } - if me.h2_tx.capacity() == 0 { + // A write taken from the mpsc receiver waits here for h2 capacity, + // and the next one isn't pulled until it has been handed to h2, so + // the writer still sees h2 backpressure. + if me.buffered.is_some() { // poll_capacity oddly needs a loop - 'capacity: loop { - match me.h2_tx.poll_capacity(cx) { - Poll::Ready(Some(Ok(0))) => {} - Poll::Ready(Some(Ok(_))) => break, - Poll::Ready(Some(Err(e))) => { - return Poll::Ready(Err(crate::Error::new_body_write(e))) - } - Poll::Ready(None) => { + while me.h2_tx.capacity() == 0 { + match ready!(me.h2_tx.poll_capacity(cx)) { + Some(Ok(0)) => {} + Some(Ok(_)) => break, + Some(Err(e)) => return Poll::Ready(Err(crate::Error::new_body_write(e))), + None => { // None means the stream is no longer in a // streaming state, we either finished it // somehow, or the remote reset us. @@ -95,29 +106,25 @@ where "send stream capacity unexpectedly closed", ))); } - Poll::Pending => break 'capacity, } } - } - match me.h2_tx.poll_reset(cx) { - Poll::Ready(Ok(reason)) => { - trace!("stream received RST_STREAM: {:?}", reason); - return Poll::Ready(Err(crate::Error::new_body_write(::h2::Error::from( - reason, - )))); - } - Poll::Ready(Err(err)) => { - return Poll::Ready(Err(crate::Error::new_body_write(err))) - } - Poll::Pending => (), + let cursor = me.buffered.take().expect("checked is_some above"); + me.h2_tx + .send_data(SendBuf::Cursor(cursor), false) + .map_err(crate::Error::new_body_write)?; + continue; } match me.rx.as_mut().poll_next(cx) { Poll::Ready(Some(cursor)) => { - me.h2_tx - .send_data(SendBuf::Cursor(cursor), false) - .map_err(crate::Error::new_body_write)?; + // Only reserve capacity once there is something to send. + // Reserving while idle, even a single byte, pins that + // capacity on the connection-level window (#4003). As in + // `PipeToSendStream`, h2 raises the request to the + // buffered length inside `send_data`. + me.h2_tx.reserve_capacity(1); + *me.buffered = Some(cursor); } Poll::Ready(None) => { me.h2_tx @@ -165,9 +172,7 @@ impl Read for H2Upgraded { self.buf = loop { match ready!(self.recv_stream.poll_data(cx)) { None => return Poll::Ready(Ok(())), - Some(Ok(buf)) if buf.is_empty() && !self.recv_stream.is_end_stream() => { - continue - } + Some(Ok(buf)) if buf.is_empty() && !self.recv_stream.is_end_stream() => {} Some(Ok(buf)) => { self.ping.record_data(buf.len()); break buf; @@ -273,7 +278,8 @@ fn io_error(e: crate::Error) -> std::io::Error { fn h2_to_io_error(e: h2::Error) -> std::io::Error { if e.is_io() { - e.into_io().unwrap() + e.into_io() + .expect("h2 error reported io cause without an underlying io error") } else { std::io::Error::new(std::io::ErrorKind::Other, e) } diff --git a/src/proto/h3/glue.rs b/src/proto/h3/glue.rs new file mode 100644 index 0000000000..a2335d5aed --- /dev/null +++ b/src/proto/h3/glue.rs @@ -0,0 +1,165 @@ +use std::task::{Context, Poll}; + +use bytes::Buf; + +pub(super) struct Conn(Q); + +pub(super) struct BidiStream(S); +pub(super) struct SendStream(S); +pub(super) struct RecvStream(S); + +impl h3::quic::Connection for Conn +where + Q: crate::rt::quic::Connection, + B: Buf, +{ + type RecvStream = RecvStream; + type OpenStreams = Self; + + fn poll_accept_recv(&mut self, _cx: &mut Context<'_>) + -> Poll> + { + todo!(); + } + + fn poll_accept_bidi(&mut self, _cx: &mut Context<'_>) + -> Poll> + { + todo!(); + } + + fn opener(&self) -> Self::OpenStreams { + todo!(); + } +} + +impl h3::quic::OpenStreams for Conn +where + Q: crate::rt::quic::Connection, + B: Buf, +{ + type BidiStream = BidiStream; + type SendStream = SendStream; + + fn poll_open_send(&mut self, _cx: &mut Context<'_>) + -> Poll> + { + todo!(); + } + + fn poll_open_bidi(&mut self, _cx: &mut Context<'_>) + -> Poll> + { + todo!(); + } + + fn close(&mut self, _: h3::error::Code, _: &[u8]) { + + } +} + +impl h3::quic::SendStream for BidiStream +where + S: crate::rt::quic::SendStream, + B: Buf, +{ + // Required methods + fn poll_ready( + &mut self, + cx: &mut Context<'_>, + ) -> Poll> { + todo!(); + } + fn send_data>>( + &mut self, + data: T, + ) -> Result<(), h3::quic::StreamErrorIncoming> { + todo!(); + } + fn poll_finish( + &mut self, + cx: &mut Context<'_>, + ) -> Poll> { + todo!(); + } + fn reset(&mut self, reset_code: u64) { + todo!(); + } + fn send_id(&self) -> h3::quic::StreamId { + todo!() + } +} + +impl h3::quic::SendStream for SendStream +where + S: crate::rt::quic::SendStream, + B: Buf, +{ + // Required methods + fn poll_ready( + &mut self, + cx: &mut Context<'_>, + ) -> Poll> { + todo!(); + } + fn send_data>>( + &mut self, + data: T, + ) -> Result<(), h3::quic::StreamErrorIncoming> { + todo!(); + } + fn poll_finish( + &mut self, + cx: &mut Context<'_>, + ) -> Poll> { + todo!(); + } + fn reset(&mut self, reset_code: u64) { + todo!(); + } + fn send_id(&self) -> h3::quic::StreamId { + todo!() + } +} + +impl h3::quic::RecvStream for BidiStream +where + S: crate::rt::quic::RecvStream, +{ + type Buf = S::Buf; + + // Required methods + fn poll_data( + &mut self, + cx: &mut Context<'_>, + ) -> Poll, h3::quic::StreamErrorIncoming>> { + todo!(); + } + fn stop_sending(&mut self, error_code: u64) { + todo!(); + } + fn recv_id(&self) -> h3::quic::StreamId { + todo!(); + } +} + +impl h3::quic::RecvStream for RecvStream +where + S: crate::rt::quic::RecvStream, +{ + type Buf = S::Buf; + + // Required methods + fn poll_data( + &mut self, + cx: &mut Context<'_>, + ) -> Poll, h3::quic::StreamErrorIncoming>> { + todo!(); + } + fn stop_sending(&mut self, error_code: u64) { + todo!(); + } + fn recv_id(&self) -> h3::quic::StreamId { + todo!(); + } +} diff --git a/src/proto/h3/mod.rs b/src/proto/h3/mod.rs new file mode 100644 index 0000000000..2138ed50b2 --- /dev/null +++ b/src/proto/h3/mod.rs @@ -0,0 +1,34 @@ +use std::future::Future; +use std::pin::Pin; +use std::task::{Context, Poll}; + +use bytes::Buf; +use h3::server::Connection; + +use pin_project_lite::pin_project; + +mod glue; + +pin_project! { + pub(crate) struct Server + where + Q: crate::rt::quic::Connection, + B: Buf, + { + exec: E, + q: Connection, B>, + s: S, + } +} + +impl Future for Server +where + Q: crate::rt::quic::Connection, + B: Buf, +{ + type Output = crate::Result<()>; + + fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + todo!() + } +} diff --git a/src/proto/mod.rs b/src/proto/mod.rs index fcdf2b97c0..bcc9bda48d 100644 --- a/src/proto/mod.rs +++ b/src/proto/mod.rs @@ -16,6 +16,9 @@ cfg_feature! { #[cfg(feature = "http2")] pub(crate) mod h2; +#[cfg(feature = "http3")] +pub(crate) mod h3; + /// An Incoming Message head. Includes request/status line, and headers. #[cfg(feature = "http1")] #[derive(Debug, Default)] @@ -45,9 +48,9 @@ pub(crate) type ResponseHead = MessageHead; #[derive(Debug)] #[cfg(feature = "http1")] pub(crate) enum BodyLength { - /// Content-Length + /// `Content-Length`. Known(u64), - /// Transfer-Encoding: chunked (if h1) + /// `Transfer-Encoding: chunked` (if h1). Unknown, } diff --git a/src/rt/bounds.rs b/src/rt/bounds.rs index 0af623b1bf..6837776046 100644 --- a/src/rt/bounds.rs +++ b/src/rt/bounds.rs @@ -1,6 +1,6 @@ -//! Trait aliases +//! Trait aliases. //! -//! Traits in this module ease setting bounds and usually automatically +//! Traits in this module ease setting bounds and are usually automatically //! implemented by implementing another trait. #[cfg(all(feature = "client", feature = "http2"))] @@ -27,7 +27,7 @@ mod h2_common { E: Executor>, { fn execute_upgrade(&self, fut: UpgradedSendStreamTask) { - self.execute(fut) + self.execute(fut); } } } @@ -70,7 +70,7 @@ mod h2_client { T: Read + Write + Unpin, { fn execute_h2_future(&mut self, future: H2ClientFuture) { - self.execute(future) + self.execute(future); } } @@ -123,7 +123,7 @@ mod h2_server { B: Body, { fn execute_h2stream(&mut self, fut: H2Stream) { - self.execute(fut) + self.execute(fut); } } diff --git a/src/rt/io.rs b/src/rt/io.rs index ed4af0929f..31e699d73a 100644 --- a/src/rt/io.rs +++ b/src/rt/io.rs @@ -25,6 +25,52 @@ use std::task::{Context, Poll}; /// Reads bytes from a source. /// /// This trait is similar to `std::io::Read`, but supports asynchronous reads. +/// +/// # Implementing `Read` +/// +/// Implementations should read data into the provided [`ReadBufCursor`] and +/// advance the cursor to indicate how many bytes were written. The simplest +/// and safest approach is to use [`ReadBufCursor::put_slice`]: +/// +/// ``` +/// use hyper::rt::{Read, ReadBufCursor}; +/// use std::pin::Pin; +/// use std::task::{Context, Poll}; +/// use std::io; +/// +/// struct MyReader { +/// data: Vec, +/// position: usize, +/// } +/// +/// impl Read for MyReader { +/// fn poll_read( +/// mut self: Pin<&mut Self>, +/// _cx: &mut Context<'_>, +/// mut buf: ReadBufCursor<'_>, +/// ) -> Poll> { +/// let remaining_data = &self.data[self.position..]; +/// if remaining_data.is_empty() { +/// // No more data to read, signal EOF by returning Ok without +/// // advancing the buffer +/// return Poll::Ready(Ok(())); +/// } +/// +/// // Calculate how many bytes we can write +/// let to_copy = remaining_data.len().min(buf.remaining()); +/// // Use put_slice to safely copy data and advance the cursor +/// buf.put_slice(&remaining_data[..to_copy]); +/// +/// self.position += to_copy; +/// Poll::Ready(Ok(())) +/// } +/// } +/// ``` +/// +/// For more advanced use cases where you need direct access to the buffer +/// (e.g., when interfacing with APIs that write directly to a pointer), +/// you can use the unsafe [`ReadBufCursor::as_mut`] and [`ReadBufCursor::advance`] +/// methods. See their documentation for safety requirements. pub trait Read { /// Attempts to read bytes into the `buf`. /// @@ -118,18 +164,71 @@ pub trait Write { /// It is undefined behavior to de-initialize any bytes from the uninitialized /// region, since it is merely unknown whether this region is uninitialized or /// not, and if part of it turns out to be initialized, it must stay initialized. -pub struct ReadBuf<'a> { - raw: &'a mut [MaybeUninit], +pub struct ReadBuf<'data> { + raw: &'data mut [MaybeUninit], filled: usize, init: usize, } -/// The cursor part of a [`ReadBuf`]. +/// The cursor part of a [`ReadBuf`], representing the unfilled portion. +/// +/// This is created by calling [`ReadBuf::unfilled()`]. +/// +/// `ReadBufCursor` provides safe and unsafe methods for writing data into the +/// buffer: +/// +/// - **Safe approach**: Use [`put_slice`](Self::put_slice) to copy data from +/// a slice. This handles initialization tracking and cursor advancement +/// automatically. +/// +/// - **Unsafe approach**: For zero-copy scenarios or when interfacing with +/// low-level APIs, use [`as_mut`](Self::as_mut) to get a mutable slice +/// of `MaybeUninit`, then call [`advance`](Self::advance) after writing. +/// This is more efficient but requires careful attention to safety invariants. +/// +/// # Example using safe methods +/// +/// ``` +/// use hyper::rt::ReadBuf; +/// +/// let mut backing = [0u8; 64]; +/// let mut read_buf = ReadBuf::new(&mut backing); +/// +/// { +/// let mut cursor = read_buf.unfilled(); +/// // put_slice handles everything safely +/// cursor.put_slice(b"hello"); +/// } +/// +/// assert_eq!(read_buf.filled(), b"hello"); +/// ``` +/// +/// # Example using unsafe methods +/// +/// ``` +/// use hyper::rt::ReadBuf; +/// +/// let mut backing = [0u8; 64]; +/// let mut read_buf = ReadBuf::new(&mut backing); +/// +/// { +/// let mut cursor = read_buf.unfilled(); +/// // SAFETY: we will initialize exactly 5 bytes +/// let slice = unsafe { cursor.as_mut() }; +/// slice[0].write(b'h'); +/// slice[1].write(b'e'); +/// slice[2].write(b'l'); +/// slice[3].write(b'l'); +/// slice[4].write(b'o'); +/// // SAFETY: we have initialized 5 bytes +/// unsafe { cursor.advance(5) }; +/// } /// -/// This is created by calling `ReadBuf::unfilled()`. +/// assert_eq!(read_buf.filled(), b"hello"); +/// ``` #[derive(Debug)] -pub struct ReadBufCursor<'a> { - buf: &'a mut ReadBuf<'a>, +pub struct ReadBufCursor<'buf> { + buf: &'buf mut ReadBuf<'buf>, } impl<'data> ReadBuf<'data> { @@ -159,7 +258,7 @@ impl<'data> ReadBuf<'data> { #[inline] pub fn filled(&self) -> &[u8] { // SAFETY: We only slice the filled part of the buffer, which is always valid - unsafe { &*(&self.raw[0..self.filled] as *const [MaybeUninit] as *const [u8]) } + unsafe { &*(std::ptr::addr_of!(self.raw[0..self.filled]) as *const [u8]) } } /// Get a cursor to the unfilled portion of the buffer. @@ -238,6 +337,11 @@ impl ReadBufCursor<'_> { /// # Safety /// /// The caller must take care that `n` more bytes have been initialized. + /// + /// # Panics + /// + /// This method will panic if advancing would cause the filled cursor to + /// overflow `usize`. #[inline] pub unsafe fn advance(&mut self, n: usize) { self.buf.filled = self.buf.filled.checked_add(n).expect("overflow"); @@ -247,13 +351,13 @@ impl ReadBufCursor<'_> { /// Returns the number of bytes that can be written from the current /// position until the end of the buffer is reached. /// - /// This value is equal to the length of the slice returned by `as_mut()``. + /// This value is equal to the length of the slice returned by `as_mut()`. #[inline] pub fn remaining(&self) -> usize { self.buf.remaining() } - /// Transfer bytes into `self`` from `src` and advance the cursor + /// Transfer bytes into `self` from `src` and advance the cursor /// by the number of bytes written. /// /// # Panics @@ -283,6 +387,46 @@ impl ReadBufCursor<'_> { } self.buf.filled = end; } + + /// Returns a mutable reference to the unfilled part of the buffer, ensuring it is fully initialized. + #[inline] + pub fn initialize_unfilled(&mut self) -> &mut [u8] { + self.initialize_unfilled_to(self.remaining()) + } + + /// Returns a mutable reference to the first `n` bytes of the unfilled part of the buffer, ensuring it is + /// fully initialized. + /// + /// # Panics + /// + /// Panics if `self.remaining()` is less than `n`. + #[inline] + pub fn initialize_unfilled_to(&mut self, n: usize) -> &mut [u8] { + assert!(self.remaining() >= n, "n overflows remaining"); + + // This can't overflow as the assert above would have panicked otherwise. + let end = self.buf.filled + n; + + if self.buf.init < end { + // SAFETY: + // 1. The raw pointer is not null and not dangling either as the slice outlives the + // pointer's usage. The pointer is also properly aligned. + // 2. Further, `end - self.buf.init` is inbounds and hence safe to write + // to. + unsafe { + self.buf.raw[self.buf.init..end] + .as_mut_ptr() + .write_bytes(0, end - self.buf.init); + } + self.buf.init = end; + } + + let slice = &mut self.buf.raw[self.buf.filled..end]; + + // SAFETY: `MaybeUninit` has the same memory layout as u8 and we properly initialized + // the slice above. + unsafe { &mut *(slice as *mut [MaybeUninit] as *mut [u8]) } + } } macro_rules! deref_async_read { @@ -396,8 +540,8 @@ where } } -/// Polyfill for Pin::as_deref_mut() -/// TODO: use Pin::as_deref_mut() instead once stabilized +/// Polyfill for `Pin::as_deref_mut()`. +/// TODO: use `Pin::as_deref_mut()` instead once stabilized. fn pin_as_deref_mut(pin: Pin<&mut Pin

>) -> Pin<&mut P::Target> { // SAFETY: we go directly from Pin<&mut Pin

> to Pin<&mut P::Target>, without moving or // giving out the &mut Pin

in the process. See Pin::as_deref_mut() for more detail. diff --git a/src/rt/mod.rs b/src/rt/mod.rs index 0eb266974f..d957ac66be 100644 --- a/src/rt/mod.rs +++ b/src/rt/mod.rs @@ -1,4 +1,4 @@ -//! Runtime components +//! Runtime components. //! //! This module provides traits and types that allow hyper to be runtime-agnostic. //! By abstracting over async runtimes, hyper can work with different executors, timers, and IO transports. @@ -17,6 +17,10 @@ pub mod bounds; mod io; mod timer; +//#[cfg(hyper_unstable_quic)] +#[cfg_attr(docsrs, doc(cfg(hyper_unstable_quic)))] +pub mod quic; + pub use self::io::{Read, ReadBuf, ReadBufCursor, Write}; pub use self::timer::{Sleep, Timer}; diff --git a/src/rt/quic.rs b/src/rt/quic.rs new file mode 100644 index 0000000000..18271d34a8 --- /dev/null +++ b/src/rt/quic.rs @@ -0,0 +1,91 @@ +//! Generic QUIC support + +use std::pin::Pin; +use std::task::{Context, Poll}; + +use bytes::Buf; + +// TODO: Should this be gated by an `http3` feature? + +/// A QUIC connection. +pub trait Connection { + /// Send streams that can be opened by this connection. + type SendStream: SendStream; + /// Receive streams that can be accepted by this connection. + type RecvStream: RecvStream; + /// Bidirectional streams that can be opened or accepted by this connection. + type BidiStream: SendStream + RecvStream; + /// Errors that may occur opening or accepting streams. + type Error; + + // Accepting streams + + // Q: shorten to bidi? + /// Accept a bidirection stream. + fn poll_accept_bidirectional_stream( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + ) -> Poll, Self::Error>>; + + /// Accept a unidirectional receive stream. + fn poll_accept_recv_stream( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + ) -> Poll, Self::Error>>; + + // Creating streams + + // Q: shorten to bidi? + /// Open a bidirectional stream. + fn poll_open_bidirectional_stream( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + ) -> Poll>; + + /// Open a unidirectional send stream. + fn poll_open_send_stream( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + ) -> Poll>; +} + +/// The send portion of a QUIC stream. +pub trait SendStream { + /// Errors that may happen trying to send data. + type Error; // bounds? + /// Polls that the stream is ready to send more data. + // Q: Should this be Pin<&mut Self>? + fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll>; + /// Send data on the stream. + fn send_data(&mut self, data: B) -> Result<(), Self::Error>; + // fn poll_flush? + /// finish? + fn poll_finish(&mut self, cx: &mut Context<'_>) -> Poll>; + /// Close the stream with an error code. + fn reset(&mut self, reset_code: u64); +} + +/// The receive portion of a QUIC stream. +pub trait RecvStream { + /// Buffers of data that can be received. + type Buf: Buf; + /// Errors that may be received. + type Error; // bounds? + + // Q: should this be Pin? + /// Poll for more data received from the remote on this stream. + fn poll_data(&mut self, cx: &mut Context<'_>) -> Poll, Self::Error>>; + /// Signal to the remote peer to stop sending data. + fn stop_sending(&mut self, error_code: u64); +} + +/// An optional trait if a QUIC stream can be split into two sides. +pub trait BidiStream: SendStream + RecvStream { + /// The send side of a stream. + type SendStream: SendStream; + /// The receive side of a stream. + type RecvStream: RecvStream; + + /// Split this stream into two sides. + fn split(self) -> (Self::SendStream, Self::RecvStream); +} diff --git a/src/rt/timer.rs b/src/rt/timer.rs index a9633ca8e8..b3b5aa0a39 100644 --- a/src/rt/timer.rs +++ b/src/rt/timer.rs @@ -1,4 +1,4 @@ -//! Provides a timer trait with timer-like functions +//! Provides a timer trait with timer-like functions. //! //! Example using tokio timer: //! ```rust @@ -90,7 +90,7 @@ pub trait Timer { /// A future returned by a `Timer`. pub trait Sleep: Send + Sync + Future { #[doc(hidden)] - /// This method is private and can not be implemented by downstream crate + /// This method is private and can not be implemented by downstream crates. fn __type_id(&self, _: private::Sealed) -> TypeId where Self: 'static, @@ -100,26 +100,35 @@ pub trait Sleep: Send + Sync + Future { } impl dyn Sleep { - //! This is a re-implementation of downcast methods from std::any::Any + // This is a re-implementation of downcast methods from `std::any::Any`. - /// Check whether the type is the same as `T` + /// Check whether the type is the same as `T`. pub fn is(&self) -> bool where T: Sleep + 'static, { - self.__type_id(private::Sealed {}) == TypeId::of::() + self.__type_id(private::Sealed) == TypeId::of::() } - /// Downcast a pinned &mut Sleep object to its original type + /// Downcast a pinned `&mut Sleep` object to its original type. pub fn downcast_mut_pin(self: Pin<&mut Self>) -> Option> where T: Sleep + 'static, { if self.is::() { + // SAFETY: + // 1. `self.is::()` guarantees that the underlying object is indeed of type `T`. + // 2. We use `Pin::into_inner_unchecked` to gain mutable access to the underlying + // value because we are maintaining the pinning contract. + // 3. We convert the reference to `dyn Sleep` to a reference to `T` via raw pointers. + // Since we've verified the type, this is a sound downcast. + // 4. `Pin::new_unchecked` is safe here because the original object was already pinned, + // and by pinning the downcasted reference, we continue to uphold the pinning guarantee + // that the object will not be moved. unsafe { let inner = Pin::into_inner_unchecked(self); Some(Pin::new_unchecked( - &mut *(&mut *inner as *mut dyn Sleep as *mut T), + &mut *(&mut *inner as *mut dyn Sleep).cast(), )) } } else { @@ -130,5 +139,5 @@ impl dyn Sleep { mod private { #![allow(missing_debug_implementations)] - pub struct Sealed {} + pub struct Sealed; } diff --git a/src/server/conn/http1.rs b/src/server/conn/http1.rs index 36dda98069..8dc83ab586 100644 --- a/src/server/conn/http1.rs +++ b/src/server/conn/http1.rs @@ -1,4 +1,4 @@ -//! HTTP/1 Server Connections +//! HTTP/1 Server Connections. use std::error::Error as StdError; use std::fmt; @@ -12,6 +12,7 @@ use crate::rt::{Read, Write}; use crate::upgrade::Upgraded; use bytes::Bytes; use futures_core::ready; +use httparse::ParserConfig; use crate::body::{Body, Incoming as IncomingBody}; use crate::proto; @@ -69,13 +70,14 @@ pin_project_lite::pin_project! { /// to bind the built connection to a service. #[derive(Clone, Debug)] pub struct Builder { - h1_parser_config: httparse::ParserConfig, + h1_parser_config: ParserConfig, timer: Time, h1_half_close: bool, h1_keep_alive: bool, h1_title_case_headers: bool, h1_preserve_header_case: bool, h1_max_headers: Option, + h1_max_header_size: Option, h1_header_read_timeout: Dur, h1_writev: Option, max_buf_size: Option, @@ -177,11 +179,19 @@ where /// # Error /// /// This errors if the underlying connection protocol is not HTTP/1. + #[allow(clippy::missing_panics_doc)] pub fn without_shutdown(self) -> impl Future>> { let mut zelf = Some(self); crate::common::future::poll_fn(move |cx| { - ready!(zelf.as_mut().unwrap().conn.poll_without_shutdown(cx))?; - Poll::Ready(Ok(zelf.take().unwrap().into_parts())) + ready!(zelf + .as_mut() + .expect("server connection polled after completion") + .conn + .poll_without_shutdown(cx))?; + Poll::Ready(Ok(zelf + .take() + .expect("server connection missing before completion") + .into_parts())) }) } @@ -218,7 +228,7 @@ where // error letting them know about that. pending.manual(); } - }; + } Poll::Ready(Ok(())) } Err(e) => Poll::Ready(Err(e)), @@ -232,13 +242,14 @@ impl Builder { /// Create a new connection builder. pub fn new() -> Self { Self { - h1_parser_config: Default::default(), + h1_parser_config: ParserConfig::default(), timer: Time::Empty, h1_half_close: false, h1_keep_alive: true, h1_title_case_headers: false, h1_preserve_header_case: false, h1_max_headers: None, + h1_max_header_size: None, h1_header_read_timeout: Dur::Default(Some(Duration::from_secs(30))), h1_writev: None, max_buf_size: None, @@ -332,6 +343,21 @@ impl Builder { self } + /// Set the maximum size of request headers (including the start line) in bytes. + /// + /// If the client sends headers exceeding this limit, the server responds to the + /// client with "431 Request Header Fields Too Large" and closes the connection. + /// + /// 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 a timeout for reading client request headers. If a client does not /// transmit the entire header within this time, the connection is closed. /// @@ -353,11 +379,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, val: bool) -> &mut Self { self.h1_writev = Some(val); self @@ -467,12 +493,15 @@ impl Builder { if let Some(max_headers) = self.h1_max_headers { conn.set_http1_max_headers(max_headers); } + if let Some(max_header_size) = self.h1_max_header_size { + conn.set_http1_max_header_size(max_header_size); + } if let Some(dur) = self .timer .check(self.h1_header_read_timeout, "header_read_timeout") { conn.set_http1_header_read_timeout(dur); - }; + } if let Some(writev) = self.h1_writev { if writev { conn.set_write_strategy_queue(); @@ -519,9 +548,15 @@ where // Connection (`inner`) is `None` if it was upgraded (and `poll` is `Ready`). // In that case, we don't need to call `graceful_shutdown`. if let Some(conn) = self.inner.as_mut() { - Pin::new(conn).graceful_shutdown() + Pin::new(conn).graceful_shutdown(); } } + + /// Return the inner IO object, and additional information provided the connection + /// has not yet been upgraded. + pub fn into_parts(self) -> Option> { + self.inner.map(|conn| conn.into_parts()) + } } impl Future for UpgradeableConnection @@ -539,7 +574,12 @@ where match ready!(Pin::new(&mut conn.conn).poll(cx)) { Ok(proto::Dispatched::Shutdown) => Poll::Ready(Ok(())), Ok(proto::Dispatched::Upgrade(pending)) => { - let (io, buf, _) = self.inner.take().unwrap().conn.into_inner(); + let (io, buf, _) = self + .inner + .take() + .expect("upgradeable server connection missing after upgrade") + .conn + .into_inner(); pending.fulfill(Upgraded::new(io, buf)); Poll::Ready(Ok(())) } diff --git a/src/server/conn/http2.rs b/src/server/conn/http2.rs index 0452109c3c..e4dac78bea 100644 --- a/src/server/conn/http2.rs +++ b/src/server/conn/http2.rs @@ -1,4 +1,4 @@ -//! HTTP/2 Server Connections +//! HTTP/2 Server Connections. use std::error::Error as StdError; use std::fmt; @@ -116,7 +116,7 @@ impl Builder { Self { exec, timer: Time::Empty, - h2_builder: Default::default(), + h2_builder: proto::h2::server::Config::default(), } } @@ -260,6 +260,18 @@ impl Builder { self } + /// Sets the header table size. + /// + /// This setting informs the peer of the maximum size of the header compression + /// table used to encode header blocks, in octets. The encoder may select any value + /// equal to or less than the header table size specified by the sender. + /// + /// The default value of crate `h2` is 4,096. + pub fn header_table_size(&mut self, size: impl Into>) -> &mut Self { + self.h2_builder.header_table_size = size.into(); + self + } + /// Sets the max size of received header frames. /// /// Default is currently 16KB, but can change. diff --git a/src/server/conn/http3.rs b/src/server/conn/http3.rs new file mode 100644 index 0000000000..20b149aea3 --- /dev/null +++ b/src/server/conn/http3.rs @@ -0,0 +1,58 @@ +//! HTTP/3 Server Connections + +use std::error::Error as StdError; + +use pin_project_lite::pin_project; + +use crate::body::{Body, Incoming as IncomingBody}; +use crate::rt::quic; +use crate::service::HttpService; + +pin_project! { + /// A Future representing an HTTP/3 connection. + #[must_use = "futures do nothing unless polled"] + pub struct Connection { + _i: (Q, S, E), + } +} + +/// A configuration builder for HTTP/3 server connections. +/// +/// **Note**: The default values of options are *not considered stable*. They +/// are subject to change at any time. +#[derive(Clone, Debug)] +pub struct Builder { + exec: E, +} + +// ===== impl Connection ===== + +// ===== impl Builder ===== + +impl Builder { + /// Create a new connection builder. + pub fn new(exec: E) -> Self { + Self { + exec, + } + } + + /// Bind a connection together with a [`Service`](crate::service::Service). + /// + /// This returns a Future that must be polled in order for HTTP to be + /// driven on the connection. + pub fn serve_connection(&self, quic: Q, service: S) -> Connection + where + S: HttpService, + S::Error: Into>, + Bd: Body + 'static, + Bd::Error: Into>, + Q: quic::Connection, + //E: Http2ServerConnExec, + E: Clone, + { + Connection { + _i: (quic, service, self.exec.clone()), + } + } +} diff --git a/src/server/conn/mod.rs b/src/server/conn/mod.rs index 54b309e88e..63f3089a9f 100644 --- a/src/server/conn/mod.rs +++ b/src/server/conn/mod.rs @@ -18,3 +18,5 @@ pub mod http1; #[cfg(feature = "http2")] pub mod http2; +//#[cfg(feature = "http3")] +pub mod http3; diff --git a/src/server/mod.rs b/src/server/mod.rs index 980553e5e9..c1143a4cfc 100644 --- a/src/server/mod.rs +++ b/src/server/mod.rs @@ -1,4 +1,4 @@ -//! HTTP Server +//! HTTP Server. //! //! A "server" is usually created by listening on a port for new connections, //! parse HTTP requests, and hand them off to a `Service`. diff --git a/src/service/mod.rs b/src/service/mod.rs index 28ffaddbab..0e49c45bd7 100644 --- a/src/service/mod.rs +++ b/src/service/mod.rs @@ -1,4 +1,4 @@ -//! Asynchronous Services +//! Asynchronous Services. //! //! A [`Service`] is a trait representing an asynchronous //! function of a request to a response. It's similar to @@ -11,7 +11,7 @@ //! - `HttpService`: This is blanketly implemented for all types that //! implement `Service, Response = http::Response>`. //! -//! # HttpService +//! # `HttpService` //! //! In hyper, especially in the server setting, a `Service` is usually bound //! to a single connection. It defines how to respond to **all** requests that diff --git a/src/service/service.rs b/src/service/service.rs index caded3f752..1f13b6b944 100644 --- a/src/service/service.rs +++ b/src/service/service.rs @@ -37,7 +37,7 @@ pub trait Service { /// /// Note: Returning an `Error` to a hyper server, the behavior depends on the /// protocol. In most cases, hyper will cause the connection to be abruptly aborted. - /// It will abort the request however the protocol allows, either with some sort of RST_STREAM, + /// It will abort the request however the protocol allows, either with some sort of `RST_STREAM`, /// or killing the connection if that doesn't exist. type Error; diff --git a/src/service/util.rs b/src/service/util.rs index 3e017a782c..69602c7f29 100644 --- a/src/service/util.rs +++ b/src/service/util.rs @@ -38,7 +38,7 @@ where } } -/// Service returned by [`service_fn`] +/// Service returned by [`service_fn`]. pub struct ServiceFn { f: F, _req: PhantomData, diff --git a/src/trace.rs b/src/trace.rs index 88f9a243a0..54f931f1c7 100644 --- a/src/trace.rs +++ b/src/trace.rs @@ -10,6 +10,27 @@ compile_error!( " ); +/// A type representing a RAII guard from a disabled tracing span. +/// +/// When the `tracing` feature flag is not active we do not enter tracing +/// spans. This type does nothing when dropped, and does not guard any +/// resources. +/// +/// This helps avoid expressions that conditionally compile to either `()` or +/// `tracing::span::Entered` depending on which feature flags are enabled. +// +// This ZST (zero sized type) marker only exists when the tracing flag is not +// active. +#[cfg(not(feature = "tracing"))] +// This module allows `unused_macros`. Similarly, we allow dead code in case +// none of the macros exported by this module are used elsewhere. +#[allow(dead_code)] +// This type is marked as a `#[must_use]` type not for its own semantic +// constraints, but to enforce consistency with code paths associated with +// the `tracing` feature flag. +#[must_use = "once a span has been entered, it should be exited"] +pub(crate) struct Disabled; + macro_rules! debug { ($($arg:tt)+) => { #[cfg(feature = "tracing")] @@ -27,6 +48,10 @@ macro_rules! debug_span { let _span = tracing::debug_span!($($arg)+); _span.entered() } + #[cfg(not(feature = "tracing"))] + { + crate::trace::Disabled + } } } } @@ -48,6 +73,10 @@ macro_rules! error_span { let _span = tracing::error_span!($($arg)+); _span.entered() } + #[cfg(not(feature = "tracing"))] + { + crate::trace::Disabled + } } } } @@ -69,6 +98,10 @@ macro_rules! info_span { let _span = tracing::info_span!($($arg)+); _span.entered() } + #[cfg(not(feature = "tracing"))] + { + crate::trace::Disabled + } } } } @@ -90,6 +123,10 @@ macro_rules! trace_span { let _span = tracing::trace_span!($($arg)+); _span.entered() } + #[cfg(not(feature = "tracing"))] + { + crate::trace::Disabled + } } } } @@ -123,6 +160,10 @@ macro_rules! warn_span { let _span = tracing::warn_span!($($arg)+); _span.entered() } + #[cfg(not(feature = "tracing"))] + { + crate::trace::Disabled + } } } } diff --git a/src/upgrade.rs b/src/upgrade.rs index 9d23a29081..bb350e78ed 100644 --- a/src/upgrade.rs +++ b/src/upgrade.rs @@ -1,4 +1,4 @@ -//! HTTP Upgrades +//! HTTP Upgrades. //! //! This module deals with managing [HTTP Upgrades][mdn] in hyper. Since //! several concepts in HTTP allow for first talking HTTP, and then converting @@ -53,6 +53,7 @@ use bytes::Bytes; use tokio::sync::oneshot; use crate::common::io::Rewind; +use crate::common::lock::LockResultExt; /// An upgraded HTTP connection. /// @@ -146,8 +147,10 @@ impl Upgraded { /// Tries to downcast the internal trait object to the type passed. /// - /// On success, returns the downcasted parts. On error, returns the - /// `Upgraded` back. + /// On success, returns the downcasted parts. + /// + /// # Errors + /// On error, returns the `Upgraded` back. pub fn downcast(self) -> Result, Self> { let (io, buf) = self.io.into_inner(); match io.__hyper_downcast() { @@ -225,16 +228,18 @@ impl Future for OnUpgrade { type Output = Result; fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { - match self.rx { - Some(ref rx) => Pin::new(&mut *rx.lock().unwrap()) - .poll(cx) - .map(|res| match res { - Ok(Ok(upgraded)) => Ok(upgraded), - Ok(Err(err)) => Err(err), - Err(_oneshot_canceled) => { - Err(crate::Error::new_canceled().with(UpgradeExpected)) - } - }), + match &self.rx { + Some(rx) => { + Pin::new(&mut *rx.lock().panic_if_poisoned()) + .poll(cx) + .map(|res| match res { + Ok(Ok(upgraded)) => Ok(upgraded), + Ok(Err(err)) => Err(err), + Err(_oneshot_canceled) => { + Err(crate::Error::new_canceled().with(UpgradeExpected)) + } + }) + } None => Poll::Ready(Err(crate::Error::new_user_no_upgrade())), } } @@ -300,13 +305,23 @@ impl dyn Io + Send { let t = TypeId::of::(); self.__hyper_type_id() == t } - + /// downcast a Box wrapped Type to a Box + /// implemented by raw pointer cast. fn __hyper_downcast(self: Box) -> Result, Box> { if self.__hyper_is::() { // Taken from `std::error::Error::downcast()`. + // SAFETY: + // 1. `self.__hyper_is::()` performs a runtime type check (typically via `TypeId`), + // guaranteeing that the underlying concrete type is indeed `T`. + // 2. We use `Box::into_raw` to obtain a pointer to the trait object, which + // has the same memory layout as the underlying concrete type `T` at the + // location identified by the runtime check. + // 3. `Box::from_raw` is safe to call here because we are reconstructing the + // box from the pointer that was originally created by `Box::into_raw`, + // and the type `T` matches the original type of the allocated memory. unsafe { let raw: *mut dyn Io = Box::into_raw(self); - Ok(Box::from_raw(raw as *mut T)) + Ok(Box::from_raw(raw.cast())) } } else { Err(self) @@ -392,7 +407,6 @@ mod tests { _: &mut Context<'_>, buf: &[u8], ) -> Poll> { - // panic!("poll_write shouldn't be called"); Poll::Ready(Ok(buf.len())) } diff --git a/tests/client.rs b/tests/client.rs index d5a0e4e005..4cb6615657 100644 --- a/tests/client.rs +++ b/tests/client.rs @@ -346,7 +346,7 @@ macro_rules! test { let mut builder = Uri::builder(); if req.method() == Method::CONNECT { - builder = builder.path_and_query(format!("{}:{}", req.uri().host().unwrap(), req.uri().port_u16().unwrap())); + builder = builder.authority(format!("{}:{}", req.uri().host().unwrap(), req.uri().port_u16().unwrap())); } else { builder = builder.path_and_query(req.uri().path_and_query().cloned().unwrap_or(PathAndQuery::from_static("/"))); } @@ -704,6 +704,44 @@ test! { body: None, } +test! { + name: client_post_req_body_chunked_with_trailer_titlecase, + + server: + expected: "\ + POST / HTTP/1.1\r\n\ + trailer: Chunky-Trailer\r\n\ + host: {addr}\r\n\ + transfer-encoding: chunked\r\n\ + \r\n\ + 5\r\n\ + hello\r\n\ + 0\r\n\ + chunky-trailer: header data\r\n\ + \r\n\ + ", + reply: REPLY_OK, + + client: + request: { + method: POST, + url: "http://{addr}/", + headers: { + "trailer" => "Chunky-Trailer", + }, + body_stream_with_trailers: ( + (futures_util::stream::once(async { Ok::<_, Infallible>(Bytes::from("hello"))})), + HeaderMap::from_iter(vec![( + HeaderName::from_static("chunky-trailer"), + HeaderValue::from_static("header data") + )].into_iter())), + }, + response: + status: OK, + headers: {}, + body: None, +} + test! { name: client_res_body_chunked_with_trailer, @@ -740,6 +778,42 @@ test! { }, } +test! { + name: client_res_body_chunked_with_trailer_titlecase, + + server: + expected: "GET / HTTP/1.1\r\nte: trailers\r\nhost: {addr}\r\n\r\n", + reply: "\ + HTTP/1.1 200 OK\r\n\ + transfer-encoding: chunked\r\n\ + trailer: Chunky-Trailer\r\n\ + \r\n\ + 5\r\n\ + hello\r\n\ + 0\r\n\ + chunky-trailer: header data\r\n\ + \r\n\ + ", + + client: + request: { + method: GET, + url: "http://{addr}/", + headers: { + "te" => "trailers", + }, + }, + response: + status: OK, + headers: { + "Transfer-Encoding" => "chunked", + }, + body: &b"hello"[..], + trailers: { + "chunky-trailer" => "header data", + }, +} + test! { name: client_res_body_chunked_with_pathological_trailers, @@ -1484,11 +1558,44 @@ test! { body: None, } +// https://github.com/hyperium/hyper/issues/4195 +test! { + name: client_hop_by_hop_headers, + + server: + expected: "\ + GET / HTTP/1.1\r\n\ + connection: close, x-hop\r\n\ + x-hop: ...\r\n\ + host: {addr}\r\n\ + \r\n\ + ", + reply: REPLY_OK, + + client: + request: { + method: GET, + url: "http://{addr}/", + headers: { + "connection" => "close, x-hop", + "x-hop" => "...", + }, + }, + response: + status: OK, + headers: {}, + body: None, +} + mod conn { use std::error::Error; use std::io::{self, Read, Write}; use std::net::{SocketAddr, TcpListener}; use std::pin::Pin; + use std::sync::{ + atomic::{AtomicUsize, Ordering}, + Arc, + }; use std::task::{Context, Poll}; use std::thread; use std::time::Duration; @@ -1496,6 +1603,7 @@ mod conn { use bytes::{Buf, Bytes}; use futures_channel::{mpsc, oneshot}; use futures_util::future::{self, poll_fn, FutureExt, TryFutureExt}; + use http_body_util::combinators::BoxBody; use http_body_util::{BodyExt, Empty, Full, StreamBody}; use hyper::rt::Timer; use tokio::io::{AsyncReadExt as _, AsyncWriteExt as _, DuplexStream}; @@ -1683,6 +1791,61 @@ mod conn { assert_eq!(chunk.data_ref().unwrap().len(), 5); } + #[tokio::test] + async fn dropped_conn_sends_incomplete_body_error() { + let (listener, addr) = setup_tk_test_server().await; + let (release_tx, release_rx) = oneshot::channel(); + + let server = async move { + let mut sock = listener.accept().await.unwrap().0; + let mut buf = [0; 4096]; + let n = sock.read(&mut buf).await.expect("read 1"); + + let expected = "GET / HTTP/1.1\r\n\r\n"; + assert_eq!(s(&buf[..n]), expected); + + sock.write_all(b"HTTP/1.1 200 OK\r\nContent-Length: 5\r\n\r\n") + .await + .unwrap(); + + release_rx.await.expect("release server"); + }; + + let client = async move { + let tcp = tcp_connect(&addr).await.expect("connect"); + let (mut client, conn) = conn::http1::handshake(tcp).await.expect("handshake"); + + let conn = tokio::task::spawn(async move { + conn.await.expect("http conn"); + }); + + let req = Request::builder() + .uri("/") + .body(Empty::::new()) + .unwrap(); + let mut res = client.send_request(req).await.expect("send_request"); + assert_eq!(res.status(), hyper::StatusCode::OK); + assert_eq!(res.body().size_hint().exact(), Some(5)); + assert!(!res.body().is_end_stream()); + + conn.abort(); + let err = conn.await.expect_err("conn task should be aborted"); + assert!(err.is_cancelled(), "{err:?}"); + + let err = res + .body_mut() + .frame() + .await + .expect("body frame") + .unwrap_err(); + assert!(err.is_incomplete_message(), "{err:?}"); + + release_tx.send(()).expect("release server"); + }; + + future::join(server, client).await; + } + #[test] fn aborted_body_isnt_completed() { let _ = ::pretty_env_logger::try_init(); @@ -2113,6 +2276,71 @@ mod conn { let _res = client.send_request(req).await.expect("send_request"); } + #[tokio::test] + async fn client_max_header_size_exceeded() { + let (server, addr) = setup_std_test_server(); + + thread::spawn(move || { + let mut sock = server.accept().unwrap().0; + let mut buf = [0; 1024]; + sock.read(&mut buf).unwrap(); + sock.write_all(b"HTTP/1.1 200 OK\r\nX-Long: ").unwrap(); + sock.write_all(&[b'a'; 1000]).unwrap(); + sock.write_all(b"\r\n\r\n").unwrap(); + }); + + let tcp = tcp_connect(&addr).await.unwrap(); + + let (mut client, conn) = conn::http1::Builder::new() + .max_header_size(512) + .handshake(tcp) + .await + .unwrap(); + + tokio::spawn(async move { + let _ = conn.await; + }); + + let req = Request::builder() + .uri("/a") + .body(Empty::::new()) + .unwrap(); + let err = client.send_request(req).await.unwrap_err(); + assert!(err.is_parse_too_large()); + } + + #[tokio::test] + async fn client_max_header_size_accepted() { + let (server, addr) = setup_std_test_server(); + + thread::spawn(move || { + let mut sock = server.accept().unwrap().0; + let mut buf = [0; 1024]; + sock.read(&mut buf).unwrap(); + sock.write_all(b"HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n") + .unwrap(); + }); + + let tcp = tcp_connect(&addr).await.unwrap(); + + let (mut client, conn) = conn::http1::Builder::new() + .max_header_size(512) + .handshake(tcp) + .await + .unwrap(); + + tokio::spawn(async move { + let _ = conn.await; + }); + + let req = Request::builder() + .uri("/a") + .body(Empty::::new()) + .unwrap(); + let res = client.send_request(req).await.expect("send_request"); + assert_eq!(res.status(), StatusCode::OK); + } + #[tokio::test] async fn client_on_informational_ext() { use std::sync::atomic::{AtomicUsize, Ordering}; @@ -2217,7 +2445,7 @@ mod conn { if let Poll::Ready(res) = fut2.as_mut().poll(cx) { return Poll::Ready(res); } - if let Some(ref mut conn) = conn_opt { + if let Some(conn) = &mut conn_opt { match Pin::new(conn).poll(cx) { Poll::Ready(_) => { conn_opt = None; @@ -2552,7 +2780,7 @@ mod conn { } #[tokio::test] - async fn http2_responds_before_consuming_request_body() { + async fn http2_responds_before_consuming_request_body_no_trailers() { // Test that a early-response from server works correctly (request body wasn't fully consumed). // https://github.com/hyperium/hyper/issues/2872 use hyper::service::service_fn; @@ -2596,15 +2824,96 @@ mod conn { let resp = client.send_request(req).await.expect("send_request"); assert!(resp.status().is_success()); - let mut body = String::new(); - concat(resp.into_body()) + let (body, trailers) = crate::concat_with_trailers(resp.into_body()).await.unwrap(); + assert_eq!(body.as_ref(), b"No bread for you!"); + assert!(trailers.is_none()); + } + + #[tokio::test] + async fn http2_responds_before_consuming_request_body_with_trailers() { + // Test that a early-response from server works correctly (request body wasn't fully consumed). + // https://github.com/hyperium/hyper/issues/2872 + use hyper::body::{Body, Frame, SizeHint}; + use hyper::header::{HeaderMap, HeaderValue}; + use hyper::service::service_fn; + + let _ = pretty_env_logger::try_init(); + + let (listener, addr) = setup_tk_test_server().await; + + /// An `HttpBody` implementation whose `is_end_stream()` will + /// return `true` after sending trailers. + pub struct TrailersBody(Option); + + impl Body for TrailersBody { + type Data = bytes::Bytes; + type Error = hyper::Error; + + fn poll_frame( + mut self: Pin<&mut Self>, + _cx: &mut Context<'_>, + ) -> Poll, Self::Error>>> { + if let Some(trailers) = self.0.take() { + Poll::Ready(Some(Ok(Frame::trailers(trailers)))) + } else { + Poll::Ready(None) + } + } + + fn is_end_stream(&self) -> bool { + self.0.is_none() + } + + fn size_hint(&self) -> SizeHint { + SizeHint::with_exact(0) + } + } + + // Spawn an HTTP2 server that responds before reading the whole request body. + // It's normal case to decline the request due to headers or size of the body. + tokio::spawn(async move { + let sock = TokioIo::new(listener.accept().await.unwrap().0); + hyper::server::conn::http2::Builder::new(TokioExecutor) + .timer(TokioTimer) + .serve_connection( + sock, + service_fn(|_req| async move { + let mut trailers = HeaderMap::new(); + trailers.insert("grpc", HeaderValue::from_static("0")); + let body = TrailersBody(Some(trailers)); + Ok::<_, hyper::Error>(http::Response::new(body)) + }), + ) + .await + .expect("serve_connection"); + }); + + let io = tcp_connect(&addr).await.expect("tcp connect"); + let (mut client, conn) = conn::http2::Builder::new(TokioExecutor) + .timer(TokioTimer) + .handshake(io) .await - .unwrap() - .reader() - .read_to_string(&mut body) - .unwrap(); + .expect("http handshake"); - assert_eq!(&body, "No bread for you!"); + tokio::spawn(async move { + conn.await.expect("client conn shouldn't error"); + }); + + // Use a channel to keep request stream open + let (_tx, recv) = mpsc::channel::, Box>>(0); + let req = Request::post("/a").body(StreamBody::new(recv)).unwrap(); + let resp = client.send_request(req).await.expect("send_request"); + assert!(resp.status().is_success()); + + let (body, trailers) = crate::concat_with_trailers(resp.into_body()).await.unwrap(); + + // No body: + assert!(body.is_empty()); + + // Have our `grpc` trailer: + let trailers = trailers.expect("response has trailers"); + assert_eq!(trailers.len(), 1); + assert_eq!(trailers.get("grpc").unwrap(), "0"); } #[tokio::test] @@ -2794,6 +3103,424 @@ mod conn { Pin::new(&mut self.tcp).poll_read(cx, buf) } } + + struct CountingStream { + tcp: TokioIo, + flush_count: Arc, + } + + impl hyper::rt::Write for CountingStream { + fn poll_shutdown( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + ) -> Poll> { + Pin::new(&mut self.tcp).poll_shutdown(cx) + } + + fn poll_flush( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + ) -> Poll> { + self.flush_count.fetch_add(1, Ordering::Relaxed); + Pin::new(&mut self.tcp).poll_flush(cx) + } + + fn poll_write( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &[u8], + ) -> Poll> { + Pin::new(&mut self.tcp).poll_write(cx, buf) + } + } + + impl hyper::rt::Read for CountingStream { + fn poll_read( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: hyper::rt::ReadBufCursor<'_>, + ) -> Poll> { + Pin::new(&mut self.tcp).poll_read(cx, buf) + } + } + + // https://github.com/hyperium/hyper/issues/4085 + #[tokio::test] + async fn http1_half_closed_peer_with_open_request_body_does_not_spin() { + let (listener, addr) = setup_tk_test_server().await; + let (headers_seen_tx, headers_seen_rx) = oneshot::channel(); + + tokio::spawn(async move { + let mut sock = listener.accept().await.unwrap().0; + let mut buf = [0; 1024]; + let mut received = Vec::new(); + + loop { + let n = sock.read(&mut buf).await.expect("server read request"); + assert_ne!(n, 0, "client closed before sending request headers"); + received.extend_from_slice(&buf[..n]); + + if received.windows(4).any(|window| window == b"\r\n\r\n") { + break; + } + } + + headers_seen_tx.send(()).unwrap(); + sock.shutdown().await.expect("server half-close write"); + tokio::time::sleep(Duration::from_millis(200)).await; + }); + + let flush_count = Arc::new(AtomicUsize::new(0)); + let io = CountingStream { + tcp: tcp_connect(&addr).await.unwrap(), + flush_count: flush_count.clone(), + }; + let (mut client, conn) = conn::http1::Builder::new() + .handshake::<_, StreamBody, io::Error>>>>(io) + .await + .expect("handshake"); + + let conn_task = tokio::spawn(async move { + let _ = conn.await; + }); + + let (_tx, rx) = mpsc::channel::, io::Error>>(0); + let req = Request::post("/a").body(StreamBody::new(rx)).unwrap(); + let response_task = tokio::spawn(async move { + let _ = client.send_request(req).await; + }); + + headers_seen_rx.await.unwrap(); + tokio::time::sleep(Duration::from_millis(100)).await; + + let flushes = flush_count.load(Ordering::Relaxed); + assert!( + flushes < 100, + "client spun after peer half-close with open request body: poll_flush={flushes}", + ); + + response_task.abort(); + conn_task.abort(); + } + + // https://github.com/hyperium/hyper/issues/4040 + #[tokio::test] + async fn h2_pipe_task_cancelled_on_response_future_drop() { + let (client_io, server_io, _) = setup_duplex_test_server(); + let (rst_tx, rst_rx) = oneshot::channel::(); + + tokio::spawn(async move { + let mut builder = h2::server::Builder::new(); + builder.initial_window_size(0); + let mut h2 = builder.handshake::<_, Bytes>(server_io).await.unwrap(); + let (req, _respond) = h2.accept().await.unwrap().unwrap(); + tokio::spawn(async move { + let _ = poll_fn(|cx| h2.poll_closed(cx)).await; + }); + + let mut body = req.into_body(); + let got_rst = tokio::time::timeout(Duration::from_secs(2), body.data()) + .await + .map_or(false, |frame| matches!(frame, Some(Err(_)) | None)); + let _ = rst_tx.send(got_rst); + }); + + let io = TokioIo::new(client_io); + let (mut client, conn) = conn::http2::Builder::new(TokioExecutor) + .handshake(io) + .await + .expect("http handshake"); + tokio::spawn(async move { + let _ = conn.await; + }); + + let req = Request::post("http://localhost/") + .body(Full::new(Bytes::from(vec![b'x'; 50]))) + .unwrap(); + let res = tokio::time::timeout(Duration::from_millis(5), client.send_request(req)).await; + assert!(res.is_err(), "should timeout waiting for response"); + + let got_rst = rst_rx.await.expect("server task should complete"); + assert!(got_rst, "server should receive RST_STREAM"); + } + + // https://github.com/hyperium/hyper/issues/4003 + // + // An idle `PipeToSendStream` must not reserve any connection-level flow + // control capacity speculatively. If it does, a first stream that has + // filled the connection window will pin the remaining byte(s), and no + // second stream can make progress when talking to a peer that only emits + // `WINDOW_UPDATE` after its receive window is fully exhausted. + #[tokio::test] + async fn h2_idle_stream_does_not_pin_connection_window() { + use std::sync::{Arc, Mutex}; + + // The HTTP/2 spec fixes the initial connection-level window at 65535 + // (RFC 9113 section 6.9.2), and it can only be increased via + // WINDOW_UPDATE. Stream A therefore sends 65534 bytes to leave exactly + // one byte of connection window for stream B. + const STREAM_A_LEN: usize = 65534; + + let (client_io, server_io, _) = setup_duplex_test_server(); + let (stream_a_full_tx, stream_a_full_rx) = oneshot::channel::<()>(); + let (stream_b_got_tx, stream_b_got_rx) = oneshot::channel::(); + + // Raw h2 server that never calls `release_capacity`, so no + // connection-level WINDOW_UPDATE is ever sent — mimicking peers that + // only emit WINDOW_UPDATE after their receive window is fully + // exhausted. The main server task accepts streams in a loop so the + // h2 codec is driven continuously; each stream is dispatched to a + // spawned handler that reads the body without ever releasing + // capacity. + // + // The `stream_a_done` channel keeps stream A's server-side request + // alive until the test is done. Dropping the recv side of stream A + // would let h2 auto-release its in-flight recv capacity and emit a + // WINDOW_UPDATE, which would hide the bug. + let (stream_a_done_tx, stream_a_done_rx) = oneshot::channel::<()>(); + let stream_a_full_tx = Arc::new(Mutex::new(Some(stream_a_full_tx))); + let stream_b_got_tx = Arc::new(Mutex::new(Some(stream_b_got_tx))); + let stream_a_done_rx = Arc::new(Mutex::new(Some(stream_a_done_rx))); + tokio::spawn(async move { + let mut h2 = h2::server::handshake(server_io).await.unwrap(); + let mut seen = 0u32; + while let Some(result) = h2.accept().await { + let (req, mut respond) = result.unwrap(); + seen += 1; + let which = seen; + let stream_a_full_tx = stream_a_full_tx.clone(); + let stream_b_got_tx = stream_b_got_tx.clone(); + let stream_a_done_rx = stream_a_done_rx.clone(); + tokio::spawn(async move { + let mut body = req.into_body(); + if which == 1 { + // Stream A: drain the burst of body data without ever + // releasing recv capacity, then park on the done + // channel to hold on to the recv stream. + let mut received = 0usize; + while received < STREAM_A_LEN { + let frame = match body.data().await { + Some(Ok(f)) => f, + _ => return, + }; + received += frame.len(); + // Intentionally do NOT call release_capacity. + } + if let Some(tx) = stream_a_full_tx.lock().unwrap().take() { + let _ = tx.send(()); + } + // Keep the recv stream alive so that dropping it + // cannot auto-release connection-level recv capacity + // and emit a WINDOW_UPDATE mid-test. + let done = stream_a_done_rx.lock().unwrap().take(); + if let Some(done) = done { + let _ = done.await; + } + // Keep `body` in scope until here. + drop(body); + } else { + // Stream B: record the first data frame and respond. + let mut received = 0usize; + if let Some(Ok(frame)) = body.data().await { + received += frame.len(); + } + if let Some(tx) = stream_b_got_tx.lock().unwrap().take() { + let _ = tx.send(received); + } + let mut send = respond.send_response(Response::new(()), false).unwrap(); + let _ = send.send_data(Bytes::from_static(b"ok"), true); + } + }); + } + }); + + let io = TokioIo::new(client_io); + let (mut client, conn) = conn::http2::Builder::new(TokioExecutor) + .handshake::<_, BoxBody>>(io) + .await + .expect("http handshake"); + tokio::spawn(async move { + let _ = conn.await; + }); + + // Request A: streaming body that sends STREAM_A_LEN bytes and then + // stays open, waiting for more data. This fills the advertised + // connection-level window down to one byte remaining. + let (mut tx_a, rx_a) = + mpsc::channel::, Box>>(4); + let body_a: BoxBody> = + BodyExt::boxed(StreamBody::new(rx_a)); + let req_a = Request::post("http://localhost/a").body(body_a).unwrap(); + let mut client_a = client.clone(); + let a_handle = tokio::spawn(async move { client_a.send_request(req_a).await }); + + // Push stream A's body in 16 KiB chunks to match the default h2 + // `SETTINGS_MAX_FRAME_SIZE`. + use futures_util::SinkExt; + let mut remaining = STREAM_A_LEN; + while remaining > 0 { + let take = remaining.min(16_384); + let bytes = Bytes::from(vec![b'A'; take]); + tx_a.send(Ok(Frame::data(bytes))) + .await + .expect("stream A channel send"); + remaining -= take; + } + + // Wait for the server to confirm it received the full body on stream + // A, which means the connection window is now down to its last byte. + tokio::time::timeout(Duration::from_secs(5), stream_a_full_rx) + .await + .expect("server should receive full stream A body in time") + .expect("stream_a_full_rx"); + + // Give the client's `PipeToSendStream` for stream A a moment to park + // itself waiting for more body frames, which (with the bug) would + // speculatively reserve the last byte of connection-level capacity. + for _ in 0..16 { + tokio::task::yield_now().await; + } + + // Request B: one byte of body. With the bug in `PipeToSendStream`, + // stream A pins the last byte of connection window via a speculative + // reserve, so stream B can never ship its data frame. + let body_b: BoxBody> = BodyExt::boxed( + http_body_util::Full::new(Bytes::from_static(b"b")) + .map_err(|never: std::convert::Infallible| match never {}), + ); + let req_b = Request::post("http://localhost/b").body(body_b).unwrap(); + let b_fut = client.send_request(req_b); + + let received_b = tokio::time::timeout(Duration::from_secs(5), stream_b_got_rx) + .await + .expect("stream B must reach the server even while stream A is idle") + .expect("stream_b_got_rx"); + assert_eq!( + received_b, 1, + "stream B should deliver its single body byte" + ); + + // Drive request B to completion so we don't leak the future. + let _ = tokio::time::timeout(Duration::from_secs(5), b_fut).await; + + // Close stream A cleanly: first release the server-side handler so + // it drops the recv stream, then drop the body sender. + let _ = stream_a_done_tx.send(()); + drop(tx_a); + let _ = tokio::time::timeout(Duration::from_secs(5), a_handle).await; + } + + // https://github.com/hyperium/hyper/issues/4003, for HTTP/2 CONNECT + // + // Like `h2_idle_stream_does_not_pin_connection_window`, but the idle + // stream is the send side of an `Upgraded` tunnel. It must not reserve + // connection-level flow control capacity while it has nothing to write. + #[tokio::test] + async fn h2_idle_upgraded_does_not_pin_connection_window() { + // One byte short of the initial connection-level window. + const STREAM_A_LEN: usize = 65534; + + let (client_io, server_io, _) = setup_duplex_test_server(); + let (stream_a_full_tx, stream_a_full_rx) = oneshot::channel::<()>(); + let (stream_a_done_tx, stream_a_done_rx) = oneshot::channel::<()>(); + let (stream_b_got_tx, stream_b_got_rx) = oneshot::channel::(); + + // Raw h2 server that never calls `release_capacity`, so it never + // sends a connection-level WINDOW_UPDATE. + tokio::spawn(async move { + let mut h2 = h2::server::handshake(server_io).await.unwrap(); + let mut stream_a_full_tx = Some(stream_a_full_tx); + let mut stream_a_done_rx = Some(stream_a_done_rx); + let mut stream_b_got_tx = Some(stream_b_got_tx); + while let Some(result) = h2.accept().await { + let (req, mut respond) = result.unwrap(); + if req.method() == Method::CONNECT { + let full_tx = stream_a_full_tx.take().unwrap(); + let done_rx = stream_a_done_rx.take().unwrap(); + tokio::spawn(async move { + let _send = respond.send_response(Response::new(()), false).unwrap(); + let mut body = req.into_body(); + let mut received = 0usize; + while received < STREAM_A_LEN { + match body.data().await { + Some(Ok(frame)) => received += frame.len(), + _ => return, + } + } + let _ = full_tx.send(()); + // Hold on to the recv stream, dropping it would release + // its capacity and send a WINDOW_UPDATE. + let _ = done_rx.await; + drop(body); + }); + } else { + let got_tx = stream_b_got_tx.take().unwrap(); + tokio::spawn(async move { + let mut body = req.into_body(); + let mut received = 0usize; + if let Some(Ok(frame)) = body.data().await { + received += frame.len(); + } + let _ = got_tx.send(received); + let mut send = respond.send_response(Response::new(()), false).unwrap(); + let _ = send.send_data(Bytes::from_static(b"ok"), true); + }); + } + } + }); + + let io = TokioIo::new(client_io); + let (mut client, conn) = conn::http2::Builder::new(TokioExecutor) + .handshake::<_, Full>(io) + .await + .expect("http handshake"); + tokio::spawn(async move { + let _ = conn.await; + }); + + // Stream A: a tunnel that writes STREAM_A_LEN bytes and then stays + // open, leaving one byte of connection window. + let req_a = Request::connect("localhost") + .body(Full::new(Bytes::new())) + .unwrap(); + let res_a = client.send_request(req_a).await.expect("send_request A"); + assert_eq!(res_a.status(), StatusCode::OK); + let mut upgraded = TokioIo::new(hyper::upgrade::on(res_a).await.unwrap()); + upgraded + .write_all(&vec![b'A'; STREAM_A_LEN]) + .await + .expect("write to tunnel"); + + tokio::time::timeout(Duration::from_secs(5), stream_a_full_rx) + .await + .expect("server should receive all of stream A in time") + .expect("stream_a_full_rx"); + + // Let the tunnel's send task park waiting for more writes. + for _ in 0..16 { + tokio::task::yield_now().await; + } + + // Stream B: one byte of body, which needs the last byte of window. + let req_b = Request::post("http://localhost/b") + .body(Full::new(Bytes::from_static(b"b"))) + .unwrap(); + let b_fut = client.send_request(req_b); + + let received_b = tokio::time::timeout(Duration::from_secs(5), stream_b_got_rx) + .await + .expect("stream B must reach the server even while the tunnel is idle") + .expect("stream_b_got_rx"); + assert_eq!( + received_b, 1, + "stream B should deliver its single body byte" + ); + + let _ = tokio::time::timeout(Duration::from_secs(5), b_fut).await; + + let _ = stream_a_done_tx.send(()); + drop(upgraded); + } } trait FutureHyperExt: TryFuture { diff --git a/tests/h1_flush_before_yield.rs b/tests/h1_flush_before_yield.rs new file mode 100644 index 0000000000..0e6db9bddb --- /dev/null +++ b/tests/h1_flush_before_yield.rs @@ -0,0 +1,118 @@ +// Test: `poll_loop` must never yield leaving buffered bytes unflushed. +// +// `poll_loop`'s main path always calls `poll_flush` after `poll_write`. The +// "wants_write_again" re-check added a *second* `poll_write` whose `Pending` +// returned straight out of the loop, skipping that flush. +// +// That second write can buffer bytes before it pends. A response body that +// reaches end-of-stream between the two write polls has its end-of-message +// buffered by `end_body()`, and the write then pends on the *next* message +// (`poll_msg`). Returning there strands the terminating chunk in the write +// buffer: the wake-ups the connection then waits on are for reads, so nothing +// flushes it and the peer waits for a response that is already written but +// never sent. +// +// The body below drives exactly that interleaving deterministically: one data +// frame, then `Pending`, then end-of-stream on the very next poll, both of +// which happen inside a single `poll_loop` iteration. + +use std::convert::Infallible; +use std::pin::Pin; +use std::task::{Context, Poll}; +use std::time::Duration; + +use bytes::Bytes; +use hyper::body::{Body, Frame}; +use hyper::server::conn::http1; +use hyper::service::service_fn; +use hyper::Response; +use support::TokioIo; +use tokio::io::{AsyncReadExt, AsyncWriteExt}; +use tokio::net::{TcpListener, TcpStream}; +use tokio::time::timeout; + +mod support; + +/// Yields one data frame, then pends once, then ends the stream. +/// +/// The `Pending` deliberately arranges no wake-up: it stands in for a body +/// whose readiness changes between hyper's two write polls of the same +/// `poll_loop` iteration, which is what leaves the end of the message buffered +/// by the re-check write. +#[derive(Default)] +struct PendOnceThenEnd { + polls: u8, +} + +impl Body for PendOnceThenEnd { + type Data = Bytes; + type Error = Infallible; + + fn poll_frame( + mut self: Pin<&mut Self>, + _cx: &mut Context<'_>, + ) -> Poll, Self::Error>>> { + self.polls += 1; + match self.polls { + 1 => Poll::Ready(Some(Ok(Frame::data(Bytes::from_static(b"hello"))))), + 2 => Poll::Pending, + _ => Poll::Ready(None), + } + } +} + +#[tokio::test] +async fn h1_server_flushes_end_of_body_buffered_by_write_recheck() { + let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); + let addr = listener.local_addr().unwrap(); + + tokio::spawn(async move { + let (socket, _) = listener.accept().await.unwrap(); + let service = service_fn(|_req| async { + Ok::<_, Infallible>(Response::new(PendOnceThenEnd::default())) + }); + let _ = http1::Builder::new() + .serve_connection(TokioIo::new(socket), service) + .await; + }); + + let mut client = TcpStream::connect(addr).await.unwrap(); + client + .write_all(b"GET / HTTP/1.1\r\nHost: localhost\r\n\r\n") + .await + .unwrap(); + + // The whole response must arrive, terminating chunk included. Before the + // fix the body's chunk arrives but `0\r\n\r\n` never does, so this read + // waits forever. + let mut received = Vec::new(); + let read = timeout(Duration::from_secs(5), async { + let mut buf = [0u8; 256]; + loop { + let n = client.read(&mut buf).await.unwrap(); + if n == 0 { + break; + } + received.extend_from_slice(&buf[..n]); + if received.ends_with(b"\r\n0\r\n\r\n") { + break; + } + } + }) + .await; + + assert!( + read.is_ok(), + "response never completed; got {:?}", + String::from_utf8_lossy(&received) + ); + let received = String::from_utf8_lossy(&received); + assert!( + received.ends_with("\r\n0\r\n\r\n"), + "missing terminating chunk; got {received:?}" + ); + assert!( + received.contains("hello"), + "missing body content; got {received:?}" + ); +} diff --git a/tests/h1_ready_in_flight.rs b/tests/h1_ready_in_flight.rs new file mode 100644 index 0000000000..f2317ba308 --- /dev/null +++ b/tests/h1_ready_in_flight.rs @@ -0,0 +1,95 @@ +// Test: an HTTP/1 client connection must not report ready while a request is +// in flight on it. +// +// The dispatch `Receiver` signals want when its queue reports `Pending`, and +// `SendRequest::is_ready` reports that want. The signal can go stale: the +// connection task can find its queue empty and a request land before it +// signals, or tokio's coop budget can make the queue report `Pending` with a +// request already in it. The request is then taken with want still set, so +// `is_ready` stays true until the response is complete. Pools that return a +// connection on `is_ready`, such as hyper-util's legacy client, then give that +// connection to the next request, which waits behind the whole response. +// +// The coop path is deterministic, so the test drives that one. + +use std::future::Future; +use std::io; +use std::pin::Pin; +use std::task::{Context, Poll}; + +use bytes::Bytes; +use futures_util::future::poll_fn; +use http_body_util::Empty; +use hyper::client::conn::http1::{self, Connection}; +use hyper::rt::{Read, ReadBufCursor, Write}; +use hyper::Request; + +/// Accepts every write and never answers, so a sent request stays in flight. +struct SilentIo; + +impl Read for SilentIo { + fn poll_read( + self: Pin<&mut Self>, + _: &mut Context<'_>, + _: ReadBufCursor<'_>, + ) -> Poll> { + Poll::Pending + } +} + +impl Write for SilentIo { + fn poll_write( + self: Pin<&mut Self>, + _: &mut Context<'_>, + buf: &[u8], + ) -> Poll> { + Poll::Ready(Ok(buf.len())) + } + + fn poll_flush(self: Pin<&mut Self>, _: &mut Context<'_>) -> Poll> { + Poll::Ready(Ok(())) + } + + fn poll_shutdown(self: Pin<&mut Self>, _: &mut Context<'_>) -> Poll> { + Poll::Ready(Ok(())) + } +} + +/// Runs the connection task once; it never finishes, as the peer never answers. +fn poll_conn(conn: &mut Connection>, cx: &mut Context<'_>) -> Poll<()> { + assert!(Pin::new(conn).poll(cx).is_pending()); + Poll::Ready(()) +} + +#[tokio::test] +async fn h1_connection_is_not_ready_while_a_request_is_in_flight() { + let (mut sender, mut conn) = http1::handshake(SilentIo).await.unwrap(); + + // Idle, the connection finds its queue empty and signals it is ready. + poll_fn(|cx| poll_conn(&mut conn, cx)).await; + assert!(sender.is_ready()); + + // Kept alive: dropping the response future cancels the request. + let _response = sender.send_request(Request::new(Empty::new())); + assert!(!sender.is_ready()); + + // Out of coop budget, the queue reports Pending with the request in it, so + // the connection signals ready again. A connection task preempted between + // finding its queue empty and signaling leaves the same stale signal. + poll_fn(|cx| { + while let Poll::Ready(restore) = tokio::task::coop::poll_proceed(cx) { + restore.made_progress(); + } + poll_conn(&mut conn, cx) + }) + .await; + tokio::task::yield_now().await; + + // With a fresh budget it takes and writes the request, which then waits + // for a response that never comes. + poll_fn(|cx| poll_conn(&mut conn, cx)).await; + assert!( + !sender.is_ready(), + "connection reports ready with a request in flight" + ); +} diff --git a/tests/h1_server/fixture.rs b/tests/h1_server/fixture.rs new file mode 100644 index 0000000000..6b31efecbc --- /dev/null +++ b/tests/h1_server/fixture.rs @@ -0,0 +1,136 @@ +use http_body_util::StreamBody; +use hyper::body::Bytes; +use hyper::body::Frame; +use hyper::server::conn::http1; +use hyper::service::service_fn; +use hyper::{Response, StatusCode}; +use std::convert::Infallible; +use std::time::Duration; +use tokio::sync::mpsc; +use tokio::time::timeout; +use tracing::{error, info}; + +pub struct TestConfig { + pub total_chunks: usize, + pub chunk_size: usize, + pub chunk_timeout: Duration, +} + +impl TestConfig { + pub fn with_timeout(chunk_timeout: Duration) -> Self { + Self { + total_chunks: 16, + chunk_size: 64 * 1024, + chunk_timeout, + } + } +} + +pub struct Client { + pub rx: mpsc::UnboundedReceiver>, + pub tx: mpsc::UnboundedSender>, +} + +pub async fn run(server: S, mut client: Client, config: TestConfig) +where + S: hyper::rt::Read + hyper::rt::Write + Send + Unpin + 'static, +{ + let mut http_builder = http1::Builder::new(); + http_builder.max_buf_size(config.chunk_size); + + let total_chunks = config.total_chunks; + let chunk_size = config.chunk_size; + + let service = service_fn(move |_| { + let total_chunks = total_chunks; + let chunk_size = chunk_size; + async move { + info!( + "Creating payload of {} chunks of {} KiB each ({} MiB total)...", + total_chunks, + chunk_size / 1024, + total_chunks * chunk_size / (1024 * 1024) + ); + let bytes = Bytes::from(vec![0; chunk_size]); + let data = vec![bytes.clone(); total_chunks]; + let stream = futures_util::stream::iter( + data.into_iter() + .map(|b| Ok::<_, Infallible>(Frame::data(b))), + ); + let body = StreamBody::new(stream); + info!("Server: Sending data response..."); + Ok::<_, hyper::Error>( + Response::builder() + .status(StatusCode::OK) + .header("content-type", "application/octet-stream") + .header("content-length", (total_chunks * chunk_size).to_string()) + .body(body) + .unwrap(), + ) + } + }); + + let server_task = tokio::spawn(async move { + let conn = http_builder.serve_connection(Box::pin(server), service); + let conn_result = conn.await; + if let Err(e) = &conn_result { + error!("Server connection error: {}", e); + } + conn_result + }); + + let get_request = "GET / HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n"; + client + .tx + .send(get_request.as_bytes().to_vec()) + .map_err(|e| { + Box::new(std::io::Error::new( + std::io::ErrorKind::Other, + format!("Failed to send request: {}", e), + )) + }) + .unwrap(); + + info!("Client is reading response..."); + let mut bytes_received = 0; + let mut all_data = Vec::new(); + loop { + match timeout(config.chunk_timeout, client.rx.recv()).await { + Ok(Some(chunk)) => { + bytes_received += chunk.len(); + all_data.extend_from_slice(&chunk); + } + Ok(None) => break, + Err(_) => { + panic!( + "Chunk timeout: chunk took longer than {:?}", + config.chunk_timeout + ); + } + } + } + + // Clean up + let result = server_task.await.unwrap(); + result.unwrap(); + + // Parse HTTP response to find body start + // HTTP response format: "HTTP/1.1 200 OK\r\n...headers...\r\n\r\n" + let body_start = all_data + .windows(4) + .position(|w| w == b"\r\n\r\n") + .map(|pos| pos + 4) + .unwrap_or(0); + + let body_bytes = bytes_received - body_start; + assert_eq!( + body_bytes, + config.total_chunks * config.chunk_size, + "Expected {} body bytes, got {} (total received: {}, headers: {})", + config.total_chunks * config.chunk_size, + body_bytes, + bytes_received, + body_start + ); + info!(bytes_received, body_bytes, "Client done receiving bytes"); +} diff --git a/tests/h1_server/mod.rs b/tests/h1_server/mod.rs new file mode 100644 index 0000000000..7b2ee0a350 --- /dev/null +++ b/tests/h1_server/mod.rs @@ -0,0 +1,97 @@ +pub mod fixture; + +use hyper::rt::{Read, ReadBufCursor}; +use pin_project_lite::pin_project; +use std::io; +use std::pin::Pin; +use std::task::{ready, Context, Poll}; +use tokio::sync::mpsc; + +// Common read half shared by both stream types +pin_project! { + #[derive(Debug)] + pub struct StreamReadHalf { + #[pin] + read_rx: mpsc::UnboundedReceiver>, + read_buffer: Vec, + } +} + +impl StreamReadHalf { + pub fn new(read_rx: mpsc::UnboundedReceiver>) -> Self { + Self { + read_rx, + read_buffer: Vec::new(), + } + } +} + +impl Read for StreamReadHalf { + fn poll_read( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + mut buf: ReadBufCursor<'_>, + ) -> Poll> { + let mut this = self.as_mut().project(); + + // First, try to satisfy the read request from the internal buffer + if !this.read_buffer.is_empty() { + let to_read = std::cmp::min(this.read_buffer.len(), buf.remaining()); + // Copy data from internal buffer to the read buffer + buf.put_slice(&this.read_buffer[..to_read]); + // Remove the consumed data from the internal buffer + this.read_buffer.drain(..to_read); + return Poll::Ready(Ok(())); + } + + // If internal buffer is empty, try to get data from the channel + match this.read_rx.as_mut().get_mut().try_recv() { + Ok(data) => { + // Copy as much data as we can fit in the buffer + let to_read = std::cmp::min(data.len(), buf.remaining()); + buf.put_slice(&data[..to_read]); + + // Store any remaining data in the internal buffer for next time + if to_read < data.len() { + let remaining = &data[to_read..]; + this.read_buffer.extend_from_slice(remaining); + } + Poll::Ready(Ok(())) + } + Err(mpsc::error::TryRecvError::Empty) => { + match ready!(this.read_rx.poll_recv(cx)) { + Some(data) => { + // Copy as much data as we can fit in the buffer + let to_read = std::cmp::min(data.len(), buf.remaining()); + buf.put_slice(&data[..to_read]); + + // Store any remaining data in the internal buffer for next time + if to_read < data.len() { + let remaining = &data[to_read..]; + this.read_buffer.extend_from_slice(remaining); + } + Poll::Ready(Ok(())) + } + None => Poll::Ready(Ok(())), + } + } + Err(mpsc::error::TryRecvError::Disconnected) => { + // Channel closed, return EOF + Poll::Ready(Ok(())) + } + } + } +} + +pub fn init_tracing() { + use std::sync::Once; + static INIT: Once = Once::new(); + INIT.call_once(|| { + tracing_subscriber::fmt() + .with_max_level(tracing::Level::INFO) + .with_target(true) + .with_thread_ids(true) + .with_thread_names(true) + .init(); + }); +} diff --git a/tests/h1_shutdown_while_buffered.rs b/tests/h1_shutdown_while_buffered.rs new file mode 100644 index 0000000000..a7695bdb38 --- /dev/null +++ b/tests/h1_shutdown_while_buffered.rs @@ -0,0 +1,205 @@ +// Test: Ensures poll_shutdown() is never called with buffered data +// +// Reproduces rare timing bug where HTTP/1.1 server calls shutdown() on a socket while response +// data is still buffered (not flushed), leading to data loss. +// +// Scenario: +// 1. Request fully received and read. +// 2. Server computes a "large" response with Full::new() +// 3. Socket accepts only a chunk of response and then pends +// 3. Flush returns Pending (remaining data still buffered), result ignored +// 4. self.conn.wants_read_again() is false and poll_loop returns Ready +// 5. BUG: poll_shutdown called despite body remaining and buffered body is lost +// 6. FIX: Ideally never try to shutdown without body being flushed completely. + +use std::{ + pin::Pin, + sync::{Arc, Mutex}, + task::Poll, + time::Duration, +}; + +use bytes::Bytes; +use http::{Request, Response}; +use http_body_util::Full; +use hyper::{body::Incoming, service::service_fn}; +use support::TokioIo; +use tokio::{ + io::{AsyncRead, AsyncWrite}, + net::{TcpListener, TcpStream}, + time::{sleep, timeout}, +}; +mod support; + +#[derive(Debug, Default)] +struct PendingStreamStatistics { + bytes_written: usize, + total_attempted: usize, + shutdown_called_with_buffered: bool, + buffered_at_shutdown: usize, +} + +// Simple struct that simply does one write and then pends perpetually +struct PendingStream { + inner: TcpStream, + // Keep track of how many times we entered poll_write so as to be able to write only the first + // time out + write_count: usize, + // Only write this chunk size out of full buffer + write_chunk_size: usize, + stats: Arc>, +} + +impl PendingStream { + fn new( + inner: TcpStream, + write_chunk_size: usize, + stats: Arc>, + ) -> Self { + Self { + inner, + stats, + write_chunk_size, + write_count: 0, + } + } +} + +impl AsyncRead for PendingStream { + fn poll_read( + mut self: Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + buf: &mut tokio::io::ReadBuf<'_>, + ) -> Poll> { + Pin::new(&mut self.inner).poll_read(cx, buf) + } +} + +impl AsyncWrite for PendingStream { + fn poll_write( + mut self: Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + buf: &[u8], + ) -> Poll> { + self.write_count += 1; + + let mut stats = self.stats.lock().unwrap(); + stats.total_attempted += buf.len(); + + if self.write_count == 1 { + // First write: partial only + let partial = std::cmp::min(buf.len(), self.write_chunk_size); + drop(stats); + + let result = Pin::new(&mut self.inner).poll_write(cx, &buf[..partial]); + if let Poll::Ready(Ok(n)) = result { + self.stats.lock().unwrap().bytes_written += n; + } + return result; + } + + // Block all further writes to simulate pending buffer + Poll::Pending + } + + fn poll_shutdown( + mut self: Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> Poll> { + let mut stats = self.stats.lock().unwrap(); + let buffered = stats.total_attempted - stats.bytes_written; + + if buffered > 0 { + eprintln!( + "\nāŒBUG: shutdown() called with {} bytes buffered", + buffered + ); + stats.shutdown_called_with_buffered = true; + stats.buffered_at_shutdown = buffered; + } + drop(stats); + Pin::new(&mut self.inner).poll_shutdown(cx) + } + + fn poll_flush( + mut self: Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> Poll> { + let stats = self.stats.lock().unwrap(); + let buffered = stats.total_attempted - stats.bytes_written; + + if buffered > 0 { + return Poll::Pending; + } + + drop(stats); + Pin::new(&mut self.inner).poll_flush(cx) + } +} + +// Test doesn't necessarily check that the connections ended successfully but mainly that shutdown +// wasn't called with data still remaining within hyper's internal buffer +#[tokio::test] +async fn test_no_premature_shutdown_while_buffered() { + let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); + let addr = listener.local_addr().unwrap(); + let stats = Arc::new(Mutex::new(PendingStreamStatistics::default())); + + let stats_clone = stats.clone(); + let server = tokio::spawn(async move { + let (stream, _) = listener.accept().await.unwrap(); + let pending_stream = PendingStream::new(stream, 212_992, stats_clone); + let io = TokioIo::new(pending_stream); + + let service = service_fn(|_req: Request| async move { + // Larger Full response than write_chunk_size + let body = Full::new(Bytes::from(vec![b'X'; 500_000])); + Ok::<_, hyper::Error>(Response::new(body)) + }); + + hyper::server::conn::http1::Builder::new() + .serve_connection(io, service) + .await + }); + + // Wait for server to be ready + sleep(Duration::from_millis(50)).await; + + // Client sends request + tokio::spawn(async move { + let mut stream = TcpStream::connect(addr).await.unwrap(); + + use tokio::io::AsyncWriteExt; + + stream + .write_all( + b"POST / HTTP/1.1\r\n\ + Host: localhost\r\n\ + Transfer-Encoding: chunked\r\n\ + \r\n", + ) + .await + .unwrap(); + + stream.write_all(b"A\r\nHello World\r\n").await.unwrap(); + stream.write_all(b"0\r\n\r\n").await.unwrap(); + stream.flush().await.unwrap(); + + // keep connection open + sleep(Duration::from_secs(2)).await; + }); + + // Wait for completion + let result = timeout(Duration::from_millis(900), server).await; + + let stats = stats.lock().unwrap(); + + assert!( + !stats.shutdown_called_with_buffered, + "shutdown() called with {} bytes still buffered (wrote {} of {} bytes)", + stats.buffered_at_shutdown, stats.bytes_written, stats.total_attempted + ); + if let Ok(Ok(conn_result)) = result { + conn_result.ok(); + } +} diff --git a/tests/ready_on_poll_stream.rs b/tests/ready_on_poll_stream.rs new file mode 100644 index 0000000000..7e5b4dbe22 --- /dev/null +++ b/tests/ready_on_poll_stream.rs @@ -0,0 +1,144 @@ +#[path = "h1_server/mod.rs"] +mod h1_server; + +use h1_server::{fixture, init_tracing, StreamReadHalf}; +use hyper::rt::{Read, ReadBufCursor, Write}; +use pin_project_lite::pin_project; +use std::future::Future; +use std::io; +use std::pin::Pin; +use std::task::{ready, Context, Poll}; +use std::time::Duration; +use tokio::sync::mpsc; +use tokio::time::Sleep; +use tracing::error; + +pin_project! { + #[derive(Debug)] + pub struct ReadyOnPollStream { + #[pin] + read_half: StreamReadHalf, + write_tx: mpsc::UnboundedSender>, + #[pin] + pending_write: Option>>, + poll_since_write: bool, + flush_count: usize, + } +} + +impl ReadyOnPollStream { + fn new( + read_rx: mpsc::UnboundedReceiver>, + write_tx: mpsc::UnboundedSender>, + ) -> Self { + Self { + read_half: StreamReadHalf::new(read_rx), + write_tx, + poll_since_write: true, + flush_count: 0, + pending_write: None, + } + } + + /// Create a new server stream and client pair. + /// Returns a server stream (Read+Write) and a client (rx/tx channels). + pub fn new_pair() -> (Self, fixture::Client) { + let (client_tx, server_rx) = mpsc::unbounded_channel(); + let (server_tx, client_rx) = mpsc::unbounded_channel(); + let server = Self::new(server_rx, server_tx); + let client = fixture::Client { + rx: client_rx, + tx: client_tx, + }; + (server, client) + } +} + +impl Read for ReadyOnPollStream { + fn poll_read( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: ReadBufCursor<'_>, + ) -> Poll> { + self.as_mut().project().read_half.poll_read(cx, buf) + } +} + +const WRITE_DELAY: Duration = Duration::from_millis(100); + +impl Write for ReadyOnPollStream { + fn poll_write( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &[u8], + ) -> Poll> { + if let Some(sleep) = self.pending_write.as_mut() { + let sleep = sleep.as_mut(); + ready!(Future::poll(sleep, cx)); + } + { + let mut this = self.as_mut().project(); + this.pending_write + .set(Some(Box::pin(tokio::time::sleep(WRITE_DELAY)))); + } + let Some(sleep) = self.pending_write.as_mut() else { + panic!("Sleep should have just been set"); + }; + // poll the future so that we can woken + let sleep = sleep.as_mut(); + let Poll::Pending = Future::poll(sleep, cx) else { + panic!("Sleep always be pending on first poll") + }; + + let this = self.project(); + let buf = Vec::from(&buf[..buf.len()]); + let len = buf.len(); + + // Send data through the channel - this should always be ready for unbounded channels + match this.write_tx.send(buf) { + Ok(_) => Poll::Ready(Ok(len)), + Err(_) => { + error!("ReadyStream::poll_write failed - channel closed"); + Poll::Ready(Err(io::Error::new( + io::ErrorKind::BrokenPipe, + "Write channel closed", + ))) + } + } + } + + fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + self.flush_count += 1; + + if self.pending_write.is_none() { + return Poll::Ready(Ok(())); + } + + // We require two flushes to complete each chunk, simulating a success at the end of the old + // poll loop. After all chunks are written, we always succeed on flush to allow for finish. + const TOTAL_CHUNKS: usize = 16; + if self.flush_count % 2 != 0 && self.flush_count < TOTAL_CHUNKS * 2 { + if let Some(sleep) = self.pending_write.as_mut() { + let sleep = sleep.as_mut(); + ready!(Future::poll(sleep, cx)); + } else { + return Poll::Pending; + } + } + let mut this = self.as_mut().project(); + this.pending_write.set(None); + Poll::Ready(Ok(())) + } + + fn poll_shutdown(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll> { + Poll::Ready(Ok(())) + } +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn body_test() { + init_tracing(); + let (server, client) = ReadyOnPollStream::new_pair(); + let config = fixture::TestConfig::with_timeout(WRITE_DELAY * 2); + fixture::run(server, client, config).await; +} diff --git a/tests/server.rs b/tests/server.rs index 2ba6f92ca3..094558cb61 100644 --- a/tests/server.rs +++ b/tests/server.rs @@ -23,7 +23,7 @@ use http_body_util::{combinators::BoxBody, BodyExt, Empty, Full, StreamBody}; use hyper::rt::Timer; use hyper::rt::{Read as AsyncRead, Write as AsyncWrite}; use support::{TokioExecutor, TokioIo, TokioTimer}; -use tokio::io::{AsyncReadExt, AsyncWriteExt}; +use tokio::io::{AsyncReadExt, AsyncWriteExt, DuplexStream}; use tokio::net::{TcpListener as TkTcpListener, TcpListener, TcpStream as TkTcpStream}; use hyper::body::{Body, Incoming as IncomingBody}; @@ -1004,6 +1004,21 @@ fn setup_tcp_listener() -> (TcpListener, SocketAddr) { (listener, addr) } +fn setup_duplex_test_server() -> (DuplexStream, DuplexStream, SocketAddr) { + use std::net::{IpAddr, Ipv6Addr}; + let _ = pretty_env_logger::try_init(); + + const BUF_SIZE: usize = 1024; + let (ioa, iob) = tokio::io::duplex(BUF_SIZE); + + /// A test address inside the 'documentation' address range. + /// See: + const TEST_ADDR: IpAddr = IpAddr::V6(Ipv6Addr::new(0x3fff, 0, 0, 0, 0, 0, 0, 1)); + const TEST_SOCKET: SocketAddr = SocketAddr::new(TEST_ADDR, 8080); + + (ioa, iob, TEST_SOCKET) +} + #[tokio::test] async fn expect_continue_waits_for_body_poll() { let (listener, addr) = setup_tcp_listener(); @@ -1189,19 +1204,26 @@ fn http_11_uri_too_long() { #[tokio::test] async fn disable_keep_alive_mid_request() { - let (listener, addr) = setup_tcp_listener(); + let (client_io, server_io, _) = setup_duplex_test_server(); let (tx1, rx1) = oneshot::channel(); - let (tx2, rx2) = mpsc::channel(); + let (tx2, rx2) = oneshot::channel(); - let child = thread::spawn(move || { - let mut req = connect(&addr); - req.write_all(b"GET / HTTP/1.1\r\n").unwrap(); - thread::sleep(Duration::from_millis(10)); + let client_task = tokio::spawn(async move { + let mut client_io = client_io; + // Send partial request + client_io.write_all(b"GET / HTTP/1.1\r\n").await.unwrap(); + // Signal server that partial request sent tx1.send(()).unwrap(); - rx2.recv().unwrap(); - req.write_all(b"Host: localhost\r\n\r\n").unwrap(); + // Wait for server to be ready for rest of request + rx2.await.unwrap(); + // Send rest of request + client_io + .write_all(b"Host: localhost\r\n\r\n") + .await + .unwrap(); + // Read response let mut buf = vec![]; - req.read_to_end(&mut buf).unwrap(); + client_io.read_to_end(&mut buf).await.unwrap(); assert!( buf.starts_with(b"HTTP/1.1 200 OK\r\n"), "should receive OK response, but buf: {:?}", @@ -1215,9 +1237,8 @@ async fn disable_keep_alive_mid_request() { ); }); - let (socket, _) = listener.accept().await.unwrap(); - let socket = TokioIo::new(socket); - let srv = http1::Builder::new().serve_connection(socket, HelloWorld); + let server_io = TokioIo::new(server_io); + let srv = http1::Builder::new().serve_connection(server_io, HelloWorld); future::try_select(srv, rx1) .then(|r| match r { Ok(Either::Left(_)) => panic!("expected rx first"), @@ -1232,7 +1253,7 @@ async fn disable_keep_alive_mid_request() { .await .unwrap(); - child.join().unwrap(); + client_task.await.unwrap(); } #[tokio::test] @@ -2279,6 +2300,371 @@ async fn h2_connect_empty_frames() { .unwrap(); } +#[tokio::test] +async fn h2_connect_backpressure_respected() { + let (listener, addr) = setup_tcp_listener(); + let conn = connect_async(addr).await; + + let mut builder = h2::client::Builder::new(); + builder.initial_window_size(1024); + builder.initial_connection_window_size(1024); + let (h2, connection) = builder.handshake::<_, Bytes>(conn).await.unwrap(); + tokio::spawn(async move { + connection.await.unwrap(); + }); + let mut h2 = h2.ready().await.unwrap(); + + const CHUNK: &[u8] = b"backpressure test data chunk!\n"; + const TOTAL_LEN: usize = CHUNK.len() * 2000; + + let client_handle = tokio::spawn(async move { + let request = Request::connect("localhost").body(()).unwrap(); + let (response, _send_stream) = h2.send_request(request, false).unwrap(); + let response = response.await.unwrap(); + assert_eq!(response.status(), StatusCode::OK); + + let mut body = response.into_body(); + let mut received = 0usize; + + while let Some(chunk) = body.data().await { + let chunk = chunk.unwrap(); + if chunk.is_empty() { + break; + } + let len = chunk.len(); + received += len; + let _ = body.flow_control().release_capacity(len); + } + + assert_eq!(received, TOTAL_LEN); + }); + + let svc = service_fn(move |req: Request| { + let on_upgrade = hyper::upgrade::on(req); + + tokio::spawn(async move { + let mut upgraded = TokioIo::new(on_upgrade.await.expect("on_upgrade")); + + for _ in 0..2000 { + upgraded.write_all(CHUNK).await.unwrap(); + } + + upgraded.shutdown().await.unwrap(); + }); + + future::ok::<_, hyper::Error>( + Response::builder() + .status(200) + .body(Empty::::new()) + .unwrap(), + ) + }); + + let (socket, _) = listener.accept().await.unwrap(); + let socket = TokioIo::new(socket); + http2::Builder::new(TokioExecutor) + .serve_connection(socket, svc) + .await + .unwrap(); + + client_handle.await.unwrap(); +} + +#[tokio::test] +async fn h2_connect_zero_window_then_release() { + let (listener, addr) = setup_tcp_listener(); + let conn = connect_async(addr).await; + + let mut builder = h2::client::Builder::new(); + builder.initial_window_size(65535); + let (h2, connection) = builder.handshake::<_, Bytes>(conn).await.unwrap(); + tokio::spawn(async move { + connection.await.unwrap(); + }); + let mut h2 = h2.ready().await.unwrap(); + + const DATA: &[u8] = b"Hello from upgraded stream"; + + let client_handle = tokio::spawn(async move { + let request = Request::connect("localhost").body(()).unwrap(); + let (response, _send_stream) = h2.send_request(request, false).unwrap(); + let response = response.await.unwrap(); + assert_eq!(response.status(), StatusCode::OK); + + let mut body = response.into_body(); + let mut received = Vec::new(); + + while let Some(chunk) = body.data().await { + let chunk = chunk.unwrap(); + if chunk.is_empty() { + break; + } + let len = chunk.len(); + received.extend_from_slice(&chunk); + let _ = body.flow_control().release_capacity(len); + } + + assert_eq!(&received[..], DATA); + }); + + let svc = service_fn(move |req: Request| { + let on_upgrade = hyper::upgrade::on(req); + + tokio::spawn(async move { + let mut upgraded = TokioIo::new(on_upgrade.await.expect("on_upgrade")); + upgraded.write_all(DATA).await.unwrap(); + upgraded.shutdown().await.unwrap(); + }); + + future::ok::<_, hyper::Error>( + Response::builder() + .status(200) + .body(Empty::::new()) + .unwrap(), + ) + }); + + let (socket, _) = listener.accept().await.unwrap(); + let socket = TokioIo::new(socket); + http2::Builder::new(TokioExecutor) + .serve_connection(socket, svc) + .await + .unwrap(); + + client_handle.await.unwrap(); +} + +#[tokio::test] +async fn h2_connect_shutdown_while_send_backpressured() { + let (listener, addr) = setup_tcp_listener(); + let conn = connect_async(addr).await; + + let mut builder = h2::client::Builder::new(); + builder.initial_window_size(1024); + builder.initial_connection_window_size(1024); + let (h2, connection) = builder.handshake::<_, Bytes>(conn).await.unwrap(); + tokio::spawn(async move { + let _ = connection.await; + }); + let mut h2 = h2.ready().await.unwrap(); + + let (shutdown_tx, shutdown_rx) = oneshot::channel::(); + let shutdown_tx = Arc::new(Mutex::new(Some(shutdown_tx))); + + let client_handle = tokio::spawn(async move { + let request = Request::connect("localhost").body(()).unwrap(); + let (response, _send_stream) = h2.send_request(request, false).unwrap(); + let response = response.await.unwrap(); + assert_eq!(response.status(), StatusCode::OK); + + let mut body = response.into_body(); + let bytes = body.data().await.unwrap().unwrap(); + assert_eq!(bytes.len(), 1024); + + // Do not release capacity. The server-side upgraded writer should + // still observe shutdown of its mpsc sender instead of waiting for + // more h2 send capacity. + let shutdown_completed = shutdown_rx.await.unwrap_or(false); + assert!( + shutdown_completed, + "upgraded shutdown should not wait for h2 capacity after the writer closes" + ); + }); + + let svc = service_fn(move |req: Request| { + let on_upgrade = hyper::upgrade::on(req); + let shutdown_tx = shutdown_tx.clone(); + + tokio::spawn(async move { + let mut upgraded = TokioIo::new(on_upgrade.await.expect("on_upgrade")); + upgraded.write_all(&[b'x'; 1024]).await.unwrap(); + + // Regression trigger: shutdown closes the mpsc sender while the + // send task is already parked waiting for h2 capacity. + let shutdown_completed = + tokio::time::timeout(Duration::from_secs(1), upgraded.shutdown()) + .await + .is_ok(); + + if let Some(tx) = shutdown_tx.lock().unwrap().take() { + let _ = tx.send(shutdown_completed); + } + }); + + future::ok::<_, hyper::Error>( + Response::builder() + .status(200) + .body(Empty::::new()) + .unwrap(), + ) + }); + + let (socket, _) = listener.accept().await.unwrap(); + let socket = TokioIo::new(socket); + let _ = http2::Builder::new(TokioExecutor) + .serve_connection(socket, svc) + .await; + + client_handle.await.unwrap(); +} + +#[tokio::test] +async fn h2_connect_reset_during_backpressure() { + let (listener, addr) = setup_tcp_listener(); + let conn = connect_async(addr).await; + + let mut builder = h2::client::Builder::new(); + builder.initial_window_size(1024); + builder.initial_connection_window_size(1024); + let (h2, connection) = builder.handshake::<_, Bytes>(conn).await.unwrap(); + tokio::spawn(async move { + let _ = connection.await; + }); + let mut h2 = h2.ready().await.unwrap(); + + let (write_err_tx, write_err_rx) = oneshot::channel::(); + let write_err_tx = Arc::new(Mutex::new(Some(write_err_tx))); + let (reset_tx, reset_rx) = oneshot::channel::<()>(); + let reset_rx = Arc::new(Mutex::new(Some(reset_rx))); + + let client_handle = tokio::spawn(async move { + let request = Request::connect("localhost").body(()).unwrap(); + let (response, mut send_stream) = h2.send_request(request, false).unwrap(); + let response = response.await.unwrap(); + assert_eq!(response.status(), StatusCode::OK); + + let mut body = response.into_body(); + let mut received = 0; + while received < 1024 { + let bytes = body.data().await.unwrap().unwrap(); + received += bytes.len(); + } + assert_eq!(received, 1024); + + send_stream.send_reset(h2::Reason::CANCEL); + let _ = reset_tx.send(()); + drop(body); + drop(send_stream); + + let got_err = write_err_rx.await.unwrap_or(false); + assert!(got_err, "server write side should have observed RST_STREAM"); + }); + + let svc = service_fn(move |req: Request| { + let on_upgrade = hyper::upgrade::on(req); + let write_err_tx = write_err_tx.clone(); + let reset_rx = reset_rx.clone(); + + tokio::spawn(async move { + let mut upgraded = TokioIo::new(on_upgrade.await.expect("on_upgrade")); + upgraded.write_all(&[b'x'; 1024]).await.unwrap(); + + let reset_rx = reset_rx.lock().unwrap().take().unwrap(); + reset_rx.await.unwrap(); + + let large_data = vec![b'x'; 1024 * 1024]; + let write = upgraded.write_all(&large_data).await; + let shutdown = upgraded.shutdown().await; + + if let Some(tx) = write_err_tx.lock().unwrap().take() { + let _ = tx.send(write.is_err() || shutdown.is_err()); + } + }); + + future::ok::<_, hyper::Error>( + Response::builder() + .status(200) + .body(Empty::::new()) + .unwrap(), + ) + }); + + let (socket, _) = listener.accept().await.unwrap(); + let socket = TokioIo::new(socket); + let _ = http2::Builder::new(TokioExecutor) + .serve_connection(socket, svc) + .await; + + client_handle.await.unwrap(); +} + +#[tokio::test] +async fn h2_connect_backpressure_bidirectional() { + let (listener, addr) = setup_tcp_listener(); + let conn = connect_async(addr).await; + + let mut builder = h2::client::Builder::new(); + builder.initial_window_size(2048); + builder.initial_connection_window_size(4096); + let (h2, connection) = builder.handshake::<_, Bytes>(conn).await.unwrap(); + tokio::spawn(async move { + connection.await.unwrap(); + }); + let mut h2 = h2.ready().await.unwrap(); + + const PATTERN: &[u8] = b"All work and no bread makes nox a dull boy.\n"; + const REPEAT: usize = 500; + let expected_len = PATTERN.len() * REPEAT; + + let client_handle = tokio::spawn(async move { + let request = Request::connect("localhost").body(()).unwrap(); + let (response, mut send_stream) = h2.send_request(request, false).unwrap(); + let response = response.await.unwrap(); + assert_eq!(response.status(), StatusCode::OK); + + let mut body = response.into_body(); + let mut received = 0usize; + + while let Some(chunk) = body.data().await { + let chunk = chunk.unwrap(); + if chunk.is_empty() { + break; + } + let len = chunk.len(); + received += len; + let _ = body.flow_control().release_capacity(len); + } + + assert_eq!(received, expected_len); + + send_stream.send_data("client done".into(), true).unwrap(); + }); + + let svc = service_fn(move |req: Request| { + let on_upgrade = hyper::upgrade::on(req); + + tokio::spawn(async move { + let mut upgraded = TokioIo::new(on_upgrade.await.expect("on_upgrade")); + + for _ in 0..REPEAT { + upgraded.write_all(PATTERN).await.unwrap(); + } + + upgraded.shutdown().await.unwrap(); + + let mut response_buf = vec![0u8; 64]; + let n = upgraded.read(&mut response_buf).await.unwrap(); + assert_eq!(&response_buf[..n], b"client done"); + }); + + future::ok::<_, hyper::Error>( + Response::builder() + .status(200) + .body(Empty::::new()) + .unwrap(), + ) + }); + + let (socket, _) = listener.accept().await.unwrap(); + let socket = TokioIo::new(socket); + http2::Builder::new(TokioExecutor) + .serve_connection(socket, svc) + .await + .unwrap(); + + client_handle.await.unwrap(); +} + #[tokio::test] async fn parse_errors_send_4xx_response() { let (listener, addr) = setup_tcp_listener(); @@ -2366,6 +2752,125 @@ async fn max_buf_size() { .expect_err("should TooLarge error"); } +#[cfg(feature = "http1")] +#[tokio::test] +async fn max_buf_size_split_header_boundary() { + let (listener, addr) = setup_tcp_listener(); + + const MAX: usize = 8192; + + thread::spawn(move || { + let mut tcp = connect(&addr); + tcp.write_all(b"GET / HTTP/1.1\r\nHost: x\r\nConnection: close\r\nX: ") + .expect("write 1"); + tcp.write_all(&[b'a'; 7000]).expect("write 2"); + thread::sleep(Duration::from_millis(100)); + tcp.write_all(&[b'a'; 5000]).expect("write 3"); + tcp.write_all(b"\r\n\r\n").expect("write 4"); + + let mut buf = String::new(); + tcp.read_to_string(&mut buf).expect("read response"); + + let expected = "HTTP/1.1 431 "; + assert_eq!(&buf[..expected.len()], expected); + }); + + let (socket, _) = listener.accept().await.unwrap(); + let socket = TokioIo::new(socket); + http1::Builder::new() + .max_buf_size(MAX) + .serve_connection(socket, HelloWorld) + .await + .expect_err("should TooLarge error"); +} + +#[cfg(feature = "http1")] +#[tokio::test] +async fn max_header_size_exceeded() { + let (listener, addr) = setup_tcp_listener(); + + const MAX_HEADER: usize = 512; + + thread::spawn(move || { + let mut tcp = connect(&addr); + tcp.write_all(b"GET / HTTP/1.1\r\nHost: x\r\nX-Long: ") + .expect("write 1"); + tcp.write_all(&[b'a'; 1000]).expect("write 2"); + tcp.write_all(b"\r\n\r\n").expect("write 3"); + let mut buf = [0; 256]; + tcp.read(&mut buf).expect("read 1"); + + let expected = "HTTP/1.1 431 "; + assert_eq!(s(&buf[..expected.len()]), expected); + }); + + let (socket, _) = listener.accept().await.unwrap(); + let socket = TokioIo::new(socket); + http1::Builder::new() + .max_header_size(MAX_HEADER) + .serve_connection(socket, HelloWorld) + .await + .expect_err("should TooLarge error"); +} + +#[cfg(feature = "http1")] +#[tokio::test] +async fn max_header_size_accepted() { + let (listener, addr) = setup_tcp_listener(); + + const MAX_HEADER: usize = 512; + + thread::spawn(move || { + let mut tcp = connect(&addr); + tcp.write_all(b"GET / HTTP/1.1\r\nHost: x\r\nConnection: close\r\n\r\n") + .expect("write"); + let mut buf = String::new(); + tcp.read_to_string(&mut buf).expect("read"); + + let expected = "HTTP/1.1 200 "; + assert_eq!(&buf[..expected.len()], expected); + }); + + let (socket, _) = listener.accept().await.unwrap(); + let socket = TokioIo::new(socket); + http1::Builder::new() + .max_header_size(MAX_HEADER) + .serve_connection(socket, HelloWorld) + .await + .expect("should succeed"); +} + +#[cfg(feature = "http1")] +#[tokio::test] +async fn max_header_size_with_large_max_buf_size() { + let (listener, addr) = setup_tcp_listener(); + + const MAX_HEADER: usize = 512; + const MAX_BUF: usize = 64 * 1024; + + thread::spawn(move || { + let mut tcp = connect(&addr); + tcp.write_all(b"GET / HTTP/1.1\r\nHost: x\r\nX-Long: ") + .expect("write 1"); + tcp.write_all(&[b'a'; 1000]).expect("write 2"); + tcp.write_all(b"\r\n\r\n").expect("write 3"); + let mut buf = [0; 256]; + tcp.read(&mut buf).expect("read 1"); + + let expected = "HTTP/1.1 431 "; + assert_eq!(s(&buf[..expected.len()]), expected); + }); + + let (socket, _) = listener.accept().await.unwrap(); + let socket = TokioIo::new(socket); + http1::Builder::new() + .max_buf_size(MAX_BUF) + .max_header_size(MAX_HEADER) + .serve_connection(socket, HelloWorld) + .await + .expect_err("should TooLarge error even with large max_buf_size"); +} + #[cfg(feature = "http1")] #[tokio::test] async fn graceful_shutdown_before_first_request_no_block() { @@ -2829,6 +3334,51 @@ fn http1_trailer_send_fields() { assert_eq!(body, expected_body); } +#[test] +fn http1_trailer_send_fields_titlecase() { + let body = futures_util::stream::once(async move { Ok("hello".into()) }); + let mut headers = HeaderMap::new(); + headers.insert("chunky-trailer", "header data".parse().unwrap()); + // Invalid trailer field that should not be sent + headers.insert("Host", "www.example.com".parse().unwrap()); + // Not specified in Trailer header, so should not be sent + headers.insert("foo", "bar".parse().unwrap()); + + let server = serve(); + server + .reply() + .header("transfer-encoding", "chunked") + .header("trailer", "Chunky-Trailer") + .body_stream_with_trailers(body, headers); + let mut req = connect(server.addr()); + req.write_all( + b"\ + GET / HTTP/1.1\r\n\ + Host: example.domain\r\n\ + Connection: keep-alive\r\n\ + TE: trailers\r\n\ + \r\n\ + ", + ) + .expect("writing"); + + let chunky_trailer_chunk = b"\r\nchunky-trailer: header data\r\n\r\n"; + let res = read_until(&mut req, |buf| buf.ends_with(chunky_trailer_chunk)).expect("reading"); + let sres = s(&res); + + let expected_head = + "HTTP/1.1 200 OK\r\ntransfer-encoding: chunked\r\ntrailer: Chunky-Trailer\r\n"; + assert_eq!(&sres[..expected_head.len()], expected_head); + + // skip the date header + let date_fragment = "GMT\r\n\r\n"; + let pos = sres.find(date_fragment).expect("find GMT"); + let body = &sres[pos + date_fragment.len()..]; + + let expected_body = "5\r\nhello\r\n0\r\nchunky-trailer: header data\r\n\r\n"; + assert_eq!(body, expected_body); +} + #[test] fn http1_trailer_fields_not_allowed() { let body = futures_util::stream::once(async move { Ok("hello".into()) }); @@ -2872,6 +3422,79 @@ fn http1_trailer_fields_not_allowed() { assert_eq!(body, expected_body); } +#[test] +fn http1_trailer_fields_allowed_with_comma_separated_te() { + let body = futures_util::stream::once(async move { Ok("hello".into()) }); + let mut headers = HeaderMap::new(); + headers.insert("chunky-trailer", "header data".parse().unwrap()); + + let server = serve(); + server + .reply() + .header("transfer-encoding", "chunked") + .header("trailer", "chunky-trailer") + .body_stream_with_trailers(body, headers); + let mut req = connect(server.addr()); + req.write_all( + b"\ + GET / HTTP/1.1\r\n\ + Host: example.domain\r\n\ + Connection: keep-alive\r\n\ + TE: gzip, Trailers\r\n\ + \r\n\ + ", + ) + .expect("writing"); + + let chunky_trailer_chunk = b"\r\nchunky-trailer: header data\r\n\r\n"; + let res = read_until(&mut req, |buf| buf.ends_with(chunky_trailer_chunk)).expect("reading"); + let sres = s(&res); + + let date_fragment = "GMT\r\n\r\n"; + let pos = sres.find(date_fragment).expect("find GMT"); + let body = &sres[pos + date_fragment.len()..]; + + let expected_body = "5\r\nhello\r\n0\r\nchunky-trailer: header data\r\n\r\n"; + assert_eq!(body, expected_body); +} + +#[test] +fn http1_trailer_fields_allowed_with_multiple_te_headers() { + let body = futures_util::stream::once(async move { Ok("hello".into()) }); + let mut headers = HeaderMap::new(); + headers.insert("chunky-trailer", "header data".parse().unwrap()); + + let server = serve(); + server + .reply() + .header("transfer-encoding", "chunked") + .header("trailer", "chunky-trailer") + .body_stream_with_trailers(body, headers); + let mut req = connect(server.addr()); + req.write_all( + b"\ + GET / HTTP/1.1\r\n\ + Host: example.domain\r\n\ + Connection: keep-alive\r\n\ + TE: gzip\r\n\ + TE: trailers\r\n\ + \r\n\ + ", + ) + .expect("writing"); + + let chunky_trailer_chunk = b"\r\nchunky-trailer: header data\r\n\r\n"; + let res = read_until(&mut req, |buf| buf.ends_with(chunky_trailer_chunk)).expect("reading"); + let sres = s(&res); + + let date_fragment = "GMT\r\n\r\n"; + let pos = sres.find(date_fragment).expect("find GMT"); + let body = &sres[pos + date_fragment.len()..]; + + let expected_body = "5\r\nhello\r\n0\r\nchunky-trailer: header data\r\n\r\n"; + assert_eq!(body, expected_body); +} + #[test] fn http1_trailer_recv_fields() { let server = serve(); @@ -2901,6 +3524,58 @@ fn http1_trailer_recv_fields() { ); } +#[test] +fn http1_trailer_recv_keep_alive() { + let server = serve(); + server.reply().header("content-length", "2").body(b"ok"); + let mut req = connect(server.addr()); + + // First request: chunked POST with trailers + req.write_all( + b"\ + POST / HTTP/1.1\r\n\ + trailer: chunky-trailer\r\n\ + host: example.domain\r\n\ + transfer-encoding: chunked\r\n\ + \r\n\ + 5\r\n\ + hello\r\n\ + 0\r\n\ + chunky-trailer: header data\r\n\ + \r\n\ + ", + ) + .expect("writing 1"); + + assert_eq!(server.body(), b"hello"); + + let trailers = server.trailers(); + assert_eq!( + trailers.get("chunky-trailer"), + Some(&"header data".parse().unwrap()) + ); + + read_until(&mut req, |buf| buf.ends_with(b"ok")).expect("reading 1"); + + // Second request: reuse the same connection to verify keep-alive + let quux = b"zar quux"; + server + .reply() + .header("content-length", quux.len().to_string()) + .body(quux); + req.write_all( + b"\ + GET /quux HTTP/1.1\r\n\ + Host: example.domain\r\n\ + Connection: close\r\n\ + \r\n\ + ", + ) + .expect("writing 2"); + + read_until(&mut req, |buf| buf.ends_with(quux)).expect("reading 2"); +} + // ------------------------------------------------- // the Server that is used to run all the tests with // ------------------------------------------------- @@ -2954,8 +3629,8 @@ impl Serve { type BoxError = Box; type BoxFuture = Pin, BoxError>> + Send>>; -struct ReplyBuilder<'a> { - tx: &'a Mutex>, +struct ReplyBuilder<'mtx> { + tx: &'mtx Mutex>, } impl ReplyBuilder<'_> { @@ -3046,13 +3721,6 @@ impl Drop for Serve { fn drop(&mut self) { drop(self.shutdown_signal.take()); drop(self.thread.take()); - /* - let r = self.thread.take().unwrap().join(); - if let Err(ref e) = r { - println!("{:?}", e); - } - r.unwrap(); - */ } } diff --git a/tests/support/mod.rs b/tests/support/mod.rs index cf3f1bc366..608deb8849 100644 --- a/tests/support/mod.rs +++ b/tests/support/mod.rs @@ -235,7 +235,7 @@ macro_rules! __internal_headers_eq { (@val $name: expr, $val:expr) => ({ let __val = Option::from($val); std::sync::Arc::new(move |__hdrs: &hyper::HeaderMap| { - if let Some(ref val) = __val { + if let Some(val) = &__val { assert_eq!(__hdrs.get($name).expect(stringify!($name)), val.to_string().as_str(), stringify!($name)); } else { assert_eq!(__hdrs.get($name), None, stringify!($name)); diff --git a/tests/unbuffered_stream.rs b/tests/unbuffered_stream.rs new file mode 100644 index 0000000000..9cea28d5f8 --- /dev/null +++ b/tests/unbuffered_stream.rs @@ -0,0 +1,126 @@ +#[path = "h1_server/mod.rs"] +mod h1_server; + +use h1_server::{fixture, init_tracing, StreamReadHalf}; +use hyper::rt::{Read, ReadBufCursor, Write}; +use pin_project_lite::pin_project; +use std::future::Future; +use std::io; +use std::pin::Pin; +use std::task::{ready, Context, Poll}; +use std::time::Duration; +use tokio::sync::mpsc; +use tokio::time::Sleep; +use tracing::error; + +pin_project! { + #[derive(Debug)] + pub struct UnbufferedStream { + #[pin] + read_half: StreamReadHalf, + #[pin] + pending_write: Option>>, + write_tx: mpsc::UnboundedSender>, + poll_cnt: usize, + } +} + +impl UnbufferedStream { + fn new( + read_rx: mpsc::UnboundedReceiver>, + write_tx: mpsc::UnboundedSender>, + ) -> Self { + Self { + read_half: StreamReadHalf::new(read_rx), + write_tx, + pending_write: None, + poll_cnt: 0, + } + } + + /// Create a new server stream and client pair. + /// Returns a server stream (Read+Write) and a client (rx/tx channels). + pub fn new_pair() -> (Self, fixture::Client) { + let (client_tx, server_rx) = mpsc::unbounded_channel(); + let (server_tx, client_rx) = mpsc::unbounded_channel(); + let server = Self::new(server_rx, server_tx); + let client = fixture::Client { + rx: client_rx, + tx: client_tx, + }; + (server, client) + } +} + +const WRITE_DELAY: Duration = Duration::from_millis(100); + +impl Read for UnbufferedStream { + fn poll_read( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: ReadBufCursor<'_>, + ) -> Poll> { + let response = self.as_mut().project().read_half.poll_read(cx, buf); + response + } +} + +impl Write for UnbufferedStream { + fn poll_write( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &[u8], + ) -> Poll> { + self.poll_cnt += 1; + let poll_cnt = self.poll_cnt; + if let Some(sleep) = self.pending_write.as_mut() { + let sleep = sleep.as_mut(); + if poll_cnt > 4 { + return Poll::Ready(Err(io::Error::other("We are being hot polled!"))); + } + ready!(Future::poll(sleep, cx)); + let mut this = self.as_mut().project(); + this.pending_write.set(None); + *this.poll_cnt = 0; + } + let len = buf.len(); + { + let mut this = self.as_mut().project(); + let buf = Vec::from(&buf[..buf.len()]); + // Send data through the channel - this should always be ready for unbounded channels + let Ok(_) = this.write_tx.send(buf) else { + error!("UnbufferedStream::poll_write failed - channel closed"); + return Poll::Ready(Err(io::Error::new( + io::ErrorKind::BrokenPipe, + "Write channel closed", + ))); + }; + this.pending_write + .set(Some(Box::pin(tokio::time::sleep(WRITE_DELAY)))) + } + let Some(sleep) = self.pending_write.as_mut() else { + panic!("Sleep should have just been set"); + }; + let sleep = sleep.as_mut(); + let Poll::Pending = Future::poll(sleep, cx) else { + panic!("Sleep always be pending on first poll") + }; + Poll::Ready(Ok(len)) + } + + fn poll_flush(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll> { + Poll::Ready(Ok(())) + } + + fn poll_shutdown(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll> { + Poll::Ready(Ok(())) + } +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn body_test() { + init_tracing(); + let (server, client) = UnbufferedStream::new_pair(); + let config = fixture::TestConfig::with_timeout(WRITE_DELAY * 2); + fixture::run(server, client, config).await; +}